├── .gitignore ├── LICENSE.txt ├── README.md ├── index.js ├── package.json └── test └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Blink Mobile Interactive 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of the Blink Mobile Interactive nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hapi-oauth2orize 2 | === 3 | 4 | A bridge between [hapi8+](https://github.com/hapijs/hapi) and [OAuth2orize](https://github.com/jaredhanson/oauth2orize) 5 | 6 | OAuth2orize is an OAuth2 provider implemented as a middleware for express. Given that you are (presumably) using hapi, you will need a bridge to make it work in hapi land. Thus, hapi-oauth2orize. 7 | 8 | Note, this documentation is somewhat out of date. This project is currently being brought into the present. The most useful things to look at are currently the documentation/examples for [OAuth2orize](https://github.com/jaredhanson/oauth2orize) and [the OAuth2orize methods that are exposed by the plugin](https://github.com/blinkmobile/hapi-oauth2orize/blob/v2.0.0/index.js#L24). 9 | 10 | Usage 11 | --- 12 | 13 | `npm install hapi-oauth2orize --save` 14 | 15 | After this, the usage is similar to to using vanilla [OAuth2orize](https://github.com/jaredhanson/oauth2orize), but with a couple of tweaks to ensure compatiblity with hapi (>=8.x.x series). 16 | 17 | ```js 18 | // Require the plugin in hapi 19 | server.register(require('hapi-oauth2orize'), function (err) { 20 | console.log(err); 21 | }); 22 | 23 | var oauth = server.plugins['hapi-oauth2orize']; 24 | ``` 25 | 26 | Disclaimer 27 | --- 28 | The code below is extracted from a working, but incomplete project. It has not been secured, or even fully finished. However, along with the [OAuth2orize](https://github.com/jaredhanson/oauth2orize) docs, you should be able to create a working implementation of your own. 29 | 30 | Implicit Grant Flow 31 | --- 32 | ```js 33 | oauth.grant(oauth.grants.token(function (client, user, ares, done) { 34 | server.helpers.insert('token', { 35 | client: client._id, 36 | principal: user._id, 37 | scope: ares.scope, 38 | created: Date.now(), 39 | expires_in: 3600 40 | }, function (token) { 41 | done(null, token._id, {expires_in: token.expires_in}); 42 | }); 43 | })); 44 | ``` 45 | 46 | Authorization Code Exchange Flow 47 | --- 48 | ```js 49 | oauth.grant(oauth.grants.code(function (client, redirectURI, user, ares, done) { 50 | server.helpers.insert('code', { 51 | client: client._id, 52 | principal: user._id, 53 | scope: ares.scope, 54 | redirectURI: redirectURI 55 | }, function (code) { 56 | done(null, code._id); 57 | }); 58 | })); 59 | 60 | oauth.exchange(oauth.exchanges.code(function (client, code, redirectURI, done) { 61 | server.helpers.find('code', code, function (code) { 62 | if (!code || client.id !== code.client || redirectURI !== code.redirectURI) { 63 | return done(null, false); 64 | } 65 | server.helpers.insert('refreshToken', { 66 | client: code.client, 67 | principal: code.principal, 68 | scope: code.scope 69 | }, function (refreshToken) { 70 | server.helpers.insert('token', { 71 | client: code.client, 72 | principal: code.principal, 73 | scope: code.scope, 74 | created: Date.now(), 75 | expires_in: 3600 76 | }, function (token) { 77 | server.helpers.remove('code', code._id, function () { 78 | done(null, token._id, refreshToken._id, {expires_in: token.expires_in}); 79 | }); 80 | }); 81 | }); 82 | }); 83 | })); 84 | 85 | oauth.exchange(oauth.exchanges.refreshToken(function (client, refreshToken, scope, done) { 86 | server.helpers.find('refreshToken', refreshToken, function (refreshToken) { 87 | if (refreshToken.client !== client._id) { 88 | return done(null, false, { message: 'This refresh token is for a different client'}); 89 | } 90 | scope = scope || refreshToken.scope; 91 | server.helpers.insert('token', { 92 | client: client._id, 93 | principal: refreshToken.principal, 94 | scope: scope, 95 | created: Date.now(), 96 | expires_in: 3600 97 | }, function (token) { 98 | done(null, token._id, null, {expires_in: token.expires_in}); 99 | }); 100 | }); 101 | })); 102 | 103 | // Client Serializers 104 | oauth.serializeClient(function (client, done) { 105 | done(null, client._id); 106 | }); 107 | 108 | oauth.deserializeClient(function (id, done) { 109 | server.helpers.find('client', id, function (client) { 110 | done(null, client[0]); 111 | }); 112 | }); 113 | ``` 114 | 115 | OAuth Endpoints 116 | --- 117 | ```js 118 | server.route([{ 119 | method: 'GET', 120 | path: '/oauth/authorize', 121 | handler: authorize 122 | },{ 123 | method: 'POST', 124 | path: '/oauth/authorize/decision', 125 | handler: decision 126 | },{ 127 | method: 'POST', 128 | path: '/oauth/token', 129 | handler: token 130 | }]); 131 | 132 | function authorize(request, reply) { 133 | oauth.authorize(request, reply, function (req, res) { 134 | reply.view('oauth', {transactionID: req.oauth2.transactionID}); 135 | }, function (clientID, redirect, done) { 136 | server.helpers.find('client', clientID, function (docs) { 137 | done(null, docs[0], docs[0].redirect_uri); 138 | }); 139 | }); 140 | }; 141 | 142 | function decision(request, reply) { 143 | oauth.decision(request, reply); 144 | }; 145 | 146 | function token(request, reply) { 147 | oauth.authorize(function (clientID, redirect, done) { 148 | done(null, clientID, redirect); 149 | }); 150 | }; 151 | ``` 152 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Oauth2orize = require('oauth2orize'); 2 | var Boom = require('boom'); 3 | var Hoek = require('hoek'); 4 | var Url = require('url'); 5 | 6 | var internals = { 7 | defaults: { 8 | credentialsUserProperty: 'user' 9 | }, 10 | OauthServer: null, 11 | settings: null 12 | }; 13 | 14 | /*eslint camelcase: [1, {properties: "never"}]*/ 15 | exports.register = function (server, options, next) { 16 | 17 | // Need session support for transaction in authorization code grant 18 | server.dependency('yar'); 19 | 20 | internals.settings = Hoek.applyToDefaults(internals.defaults, options); 21 | 22 | internals.OauthServer = Oauth2orize.createServer(); 23 | 24 | server.expose('server' , internals.OauthServer); 25 | server.expose('settings' , internals.settings); 26 | server.expose('grant' , internals.grant); 27 | server.expose('grants' , Oauth2orize.grant); 28 | server.expose('exchange' , internals.exchange); 29 | server.expose('exchanges' , Oauth2orize.exchange); 30 | server.expose('authorize' , internals.authorize); 31 | server.expose('decision' , internals.decision); 32 | server.expose('token' , internals.token); 33 | server.expose('errorHandler', internals.errorHandler); 34 | server.expose('oauthToBoom' , internals.oauthToBoom); 35 | server.expose('errors', { 36 | AuthorizationError: Oauth2orize.AuthorizationError, 37 | TokenError: Oauth2orize.TokenError 38 | }); 39 | server.expose('serializeClient' , internals.serializeClient); 40 | server.expose('deserializeClient' , internals.deserializeClient); 41 | 42 | // Catch raw Token/AuthorizationErrors and turn them into legit OAuthified Boom errors 43 | server.ext('onPreResponse', function (request, reply) { 44 | 45 | var response = request.response; 46 | 47 | var newResponse; 48 | 49 | // Catch raw Token/AuthorizationErrors and process them 50 | if (response instanceof Oauth2orize.TokenError || 51 | response instanceof Oauth2orize.AuthorizationError) { 52 | 53 | newResponse = internals.oauthToBoom(response); 54 | } 55 | 56 | if (newResponse) { 57 | reply(newResponse); 58 | } else { 59 | reply.continue(); 60 | } 61 | }); 62 | 63 | next(); 64 | }; 65 | 66 | internals.grant = function (type, phase, fn) { 67 | 68 | internals.OauthServer.grant(type, phase, fn); 69 | }; 70 | 71 | internals.exchange = function (type, exchange) { 72 | 73 | internals.OauthServer.exchange(type, exchange); 74 | }; 75 | 76 | internals.errorHandler = function (options) { 77 | 78 | return internals.OauthServer.errorHandler(options); 79 | }; 80 | 81 | internals.authorize = function (request, reply, callback, options, validate, immediate) { 82 | 83 | var express = internals.convertToExpress(request, reply); 84 | 85 | internals.OauthServer.authorize(options, validate, immediate)(express.req, express.res, function (err) { 86 | 87 | if (err) { 88 | internals.errorHandler({ mode: 'indirect' })(err, express.req, express.res, 89 | function () { 90 | 91 | internals.errorHandler({ mode: 'direct' })(err, express.req, express.res, console.log); 92 | }); 93 | } 94 | 95 | callback(express.req, express.res); 96 | }); 97 | 98 | }; 99 | 100 | internals.decision = function (request, reply, options, parse) { 101 | 102 | var result; 103 | var express = internals.convertToExpress(request, reply); 104 | var handler = function (err) { 105 | 106 | if (err) { 107 | internals.errorHandler()(err, express.req, express.res, console.log); 108 | } 109 | }; 110 | 111 | options = options || {}; 112 | 113 | if (options && options.loadTransaction === false) { 114 | internals.OauthServer.decision(options, parse)(express.req, express.res, handler); 115 | } else { 116 | result = internals.OauthServer.decision(options, parse); 117 | result[0](express.req, express.res, function (err) { 118 | 119 | if (err) { 120 | console.log('Err2: ' + err); 121 | } 122 | result[1](express.req, express.res, handler); 123 | }); 124 | } 125 | }; 126 | 127 | internals.serializeClient = function (fn) { 128 | 129 | internals.OauthServer.serializeClient(fn); 130 | }; 131 | 132 | internals.deserializeClient = function (fn) { 133 | 134 | internals.OauthServer.deserializeClient(fn); 135 | }; 136 | 137 | internals.token = function (request, reply, options) { 138 | 139 | var express = internals.convertToExpress(request, reply); 140 | internals.OauthServer.token(options)(express.req, express.res, function (err) { 141 | 142 | if (err) { 143 | internals.errorHandler()(err, express.req, express.res, console.log); 144 | } 145 | }); 146 | }; 147 | 148 | // Takes in a Boom error and a oauth2orize error, and makes a custom Boom error to spec. 149 | internals.transformBoomError = function (boomE, authE) { 150 | 151 | if (!boomE.isBoom) { 152 | return boomE; 153 | } 154 | 155 | var overrides = authE || boomE.data || {}; 156 | 157 | Hoek.merge(boomE.output.payload, overrides); 158 | 159 | var origBoomMessage = boomE.output.payload.message; 160 | 161 | if (!boomE.output.payload.error_description && boomE.output.payload.message) { 162 | boomE.output.payload.error_description = boomE.output.payload.message; 163 | } 164 | 165 | // Hide server errors however Boom does it 166 | if (boomE.output.statusCode === 500 || 167 | boomE.output.payload.error === 'server_error') { 168 | 169 | boomE.output.payload.error_description = origBoomMessage; 170 | } 171 | 172 | delete boomE.output.payload.message; 173 | delete boomE.output.payload.statusCode; 174 | 175 | return boomE; 176 | }; 177 | 178 | internals.oauthToBoom = function (oauthError) { 179 | 180 | // These little bits of code are stolen from oauth2orize 181 | // to translate raw Token/AuthorizationErrors to OAuth2 style errors 182 | 183 | var newResponse = {}; 184 | newResponse.error = oauthError.code || 'server_error'; 185 | if (oauthError.message) { 186 | newResponse.error_description = oauthError.message; 187 | } 188 | if (oauthError.uri) { 189 | newResponse.error_uri = oauthError.uri; 190 | } 191 | 192 | // These little bits of code Boomify raw OAuth2 style errors 193 | newResponse = Boom.create(oauthError.status, null, newResponse); 194 | internals.transformBoomError(newResponse); 195 | 196 | return newResponse; 197 | }; 198 | 199 | internals.convertToExpress = function (request, reply) { 200 | 201 | request.session.lazy(true); 202 | 203 | var ExpressServer = { 204 | req: { 205 | session: request.session, 206 | query: request.query, 207 | body: request.payload, 208 | user: Hoek.reach(request.auth.credentials, internals.settings.credentialsUserProperty || '', 209 | { default: request.auth.credentials }) 210 | }, 211 | res: { 212 | redirect: function (uri) { 213 | 214 | // map errors in URL to be similar to our custom Boom errors. 215 | var uriObj = Url.parse(uri, true); 216 | 217 | if (uriObj.query.error) { 218 | 219 | // Hide detailed server error messages 220 | if (uriObj.query.error === 'server_error') { 221 | uriObj.query.error_description = 'An internal server error occurred'; 222 | } 223 | 224 | uri = Url.format(uriObj); 225 | } 226 | 227 | reply.redirect(uri); 228 | }, 229 | setHeader: function (header, value) { 230 | 231 | ExpressServer.headers.push([header, value]); 232 | }, 233 | end: function (content) { 234 | 235 | // Transform errors to be handled as Boomers 236 | if (typeof content === 'string') { 237 | 238 | var jsonContent; 239 | try { 240 | jsonContent = JSON.parse(content); 241 | } catch (e) { 242 | /* If we got a json error, ignore it. 243 | * The oauth2orize's response just wasn't json. 244 | */ 245 | } 246 | 247 | // If we have a json response and it's an error, let's Boomify/normalize it! 248 | if (jsonContent) { 249 | 250 | if (jsonContent.error && this.statusCode) { 251 | 252 | content = Boom.create(this.statusCode, null, jsonContent); 253 | 254 | // Transform Boom error using jsonContent data attached to it 255 | internals.transformBoomError(content); 256 | 257 | // Now that we have a Boom object, we can let Hapi handle headers and status codes 258 | ExpressServer.headers = []; 259 | this.statusCode = null; 260 | 261 | } else { 262 | // Respond non-error content as a json object if it is json. 263 | content = jsonContent; 264 | } 265 | 266 | } 267 | 268 | } 269 | 270 | var response = reply(content); 271 | 272 | // Non-boom error fallback 273 | ExpressServer.headers.forEach(function (element) { 274 | 275 | response.header(element[0], element[1]); 276 | }); 277 | 278 | if (this.statusCode) { 279 | response.code(this.statusCode); 280 | } 281 | 282 | } 283 | }, 284 | headers: [] 285 | }; 286 | 287 | return ExpressServer; 288 | }; 289 | 290 | exports.register.attributes = { 291 | pkg: require('./package.json') 292 | }; 293 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-oauth2orize", 3 | "description": "Wrapper around OAuth2orize for hapi", 4 | "version": "1.4.0", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/lab -L" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/blinkmobile/hapi-oauth2orize.git" 12 | }, 13 | "dependencies": { 14 | "oauth2orize": "~1.0.1", 15 | "boom": "~2.8.0", 16 | "hoek": "^2.14.0" 17 | }, 18 | "devDependencies": { 19 | "lab": "5.x.x" 20 | }, 21 | "peerDependencies": { 22 | "hapi": ">=8.x.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oneblink/hapi-oauth2orize/47454147ad9ca23a5267a6f72818a1346a3bc586/test/.gitkeep --------------------------------------------------------------------------------