From c14d93532f0ac8985dbf7b43e89d84cd1bf2dee8 Mon Sep 17 00:00:00 2001 From: Charles B Johnson Date: Sat, 6 Jun 2020 23:19:41 -0500 Subject: [PATCH 1/3] Support functions for provider endpoint settings --- API.md | 3 ++ lib/index.js | 6 ++-- lib/oauth.js | 32 +++++++++++++------- test/oauth.js | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 14 deletions(-) diff --git a/API.md b/API.md index 35855cac..72fcbcdf 100755 --- a/API.md +++ b/API.md @@ -191,10 +191,13 @@ The `server.auth.strategy()` method requires the following strategy options: - `'HMAC-SHA1'` - default - `'RSA-SHA1'` - in that case, the `clientSecret` is your RSA private key - `temporary` - the temporary credentials (request token) endpoint (OAuth 1.0a only). + It may be passed either a string, or a function which takes the client's `request` and returns a string. - `useParamsAuth` - boolean that determines if OAuth client id and client secret will be sent as parameters as opposed to an Authorization header (OAuth 2.0 only). Defaults to `false`. - `auth` - the authorization endpoint URI. + It may be passed either a string, or a function which takes the client's `request` and returns a string. - `token` - the access token endpoint URI. + It may be passed either a string, or a function which takes the client's `request` and returns a string. - `scope` - an array of scope strings (OAuth 2.0 only). - `scopeSeparator` - the scope separator character (OAuth 2.0 only). Only required when a provider has a broken OAuth 2.0 implementation. Defaults to space (Facebook and GitHub diff --git a/lib/index.js b/lib/index.js index f999cfc9..23a8a147 100755 --- a/lib/index.js +++ b/lib/index.js @@ -39,8 +39,8 @@ exports.plugin = { internals.provider = Joi.object({ name: Joi.string().optional().default('custom'), protocol: ['oauth', 'oauth2'], - auth: Joi.string().required(), - token: Joi.string().required(), + auth: Joi.alternatives(Joi.string(), Joi.func()).required(), + token: Joi.alternatives(Joi.string(), Joi.func()).required(), headers: Joi.object(), profile: Joi.func(), profileMethod: Joi.valid('get', 'post').default('get') @@ -48,7 +48,7 @@ internals.provider = Joi.object({ .when('.protocol', { is: 'oauth', then: Joi.object({ - temporary: Joi.string().required(), + temporary: Joi.alternatives(Joi.string(), Joi.func()).required(), signatureMethod: Joi.valid('HMAC-SHA1', 'RSA-SHA1').default('HMAC-SHA1') }), otherwise: Joi.object({ diff --git a/lib/oauth.js b/lib/oauth.js index b93e0353..c15b2836 100755 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -56,9 +56,10 @@ exports.v1 = function (settings) { // Obtain temporary OAuth credentials + const temporaryEndpoint = internals.resolveProviderEndpoint(request, settings.provider.temporary); const oauth_callback = internals.location(request, protocol, settings.location); try { - var { payload: temp } = await client.temporary(oauth_callback); + var { payload: temp } = await client.temporary(temporaryEndpoint, oauth_callback); } catch (err) { return h.unauthenticated(err, { credentials }); @@ -79,7 +80,8 @@ exports.v1 = function (settings) { Hoek.merge(authQuery, request.query); } - return h.redirect(settings.provider.auth + '?' + internals.queryString(authQuery)).takeover(); + const authEndpoint = internals.resolveProviderEndpoint(request, settings.provider.auth); + return h.redirect(authEndpoint + '?' + internals.queryString(authQuery)).takeover(); } // Authorization callback @@ -102,8 +104,9 @@ exports.v1 = function (settings) { // Obtain token OAuth credentials + const tokenEndpoint = internals.resolveProviderEndpoint(request, settings.provider.token); try { - var { payload: token } = await client.token(state.token, request.query.oauth_verifier, state.secret); + var { payload: token } = await client.token(tokenEndpoint, state.token, request.query.oauth_verifier, state.secret); } catch (err) { return h.unauthenticated(err, { credentials }); @@ -230,7 +233,9 @@ exports.v2 = function (settings) { } h.state(cookie, state); - return h.redirect(settings.provider.auth + '?' + internals.queryString(query)).takeover(); + + const authEndpoint = internals.resolveProviderEndpoint(request, settings.provider.auth); + return h.redirect(authEndpoint + '?' + internals.queryString(query)).takeover(); } // Authorization callback @@ -286,8 +291,9 @@ exports.v2 = function (settings) { // Obtain token + const tokenEndpoint = internals.resolveProviderEndpoint(request, settings.provider.token); try { - var { res: tokenRes, payload } = await Wreck.post(settings.provider.token, requestOptions); + var { res: tokenRes, payload } = await Wreck.post(tokenEndpoint, requestOptions); } catch (err) { return h.unauthenticated(Boom.internal('Failed obtaining ' + name + ' access token', err), { credentials }); @@ -387,8 +393,6 @@ exports.Client = internals.Client = function (options) { this.provider = options.name; this.settings = { - temporary: internals.Client.baseUri(options.provider.temporary), - token: internals.Client.baseUri(options.provider.token), clientId: options.clientId, clientSecret: options.provider.signatureMethod === 'RSA-SHA1' ? options.clientSecret : internals.encode(options.clientSecret || '') + '&', signatureMethod: options.provider.signatureMethod @@ -396,7 +400,7 @@ exports.Client = internals.Client = function (options) { }; -internals.Client.prototype.temporary = function (oauth_callback) { +internals.Client.prototype.temporary = function (uri, oauth_callback) { // Temporary Credentials (2.1) @@ -404,11 +408,11 @@ internals.Client.prototype.temporary = function (oauth_callback) { oauth_callback }; - return this._request('post', this.settings.temporary, null, oauth, { desc: 'temporary credentials' }); + return this._request('post', internals.Client.baseUri(uri), null, oauth, { desc: 'temporary credentials' }); }; -internals.Client.prototype.token = function (oauthToken, oauthVerifier, tokenSecret) { +internals.Client.prototype.token = function (uri, oauthToken, oauthVerifier, tokenSecret) { // Token Credentials (2.3) @@ -417,7 +421,7 @@ internals.Client.prototype.token = function (oauthToken, oauthVerifier, tokenSec oauth_verifier: oauthVerifier }; - return this._request('post', this.settings.token, null, oauth, { secret: tokenSecret, desc: 'token credentials' }); + return this._request('post', internals.Client.baseUri(uri), null, oauth, { secret: tokenSecret, desc: 'token credentials' }); }; @@ -723,6 +727,12 @@ internals.getProtocol = function (request, settings) { }; +internals.resolveProviderEndpoint = function (request, endpoint) { + + return typeof endpoint === 'function' ? endpoint(request) : endpoint; +}; + + internals.resolveProviderParams = function (request, params) { const obj = typeof params === 'function' ? params(request) : params; diff --git a/test/oauth.js b/test/oauth.js index 7798eb08..35c5a2e4 100755 --- a/test/oauth.js +++ b/test/oauth.js @@ -344,6 +344,47 @@ describe('Bell', () => { expect(res.headers.location).to.equal(mock.uri + '/auth?oauth_token=1&runtime=true'); }); + it('authenticates an endpoint via oauth with auth provider (function)', async (flags) => { + + const mock = await Mock.v1(flags); + const server = Hapi.server({ host: 'localhost', port: 8080 }); + await server.register(Bell); + + const provider = Hoek.merge(mock.provider, { + temporary: (request) => request.query.host + '/temporary', + auth: (request) => request.query.host + '/auth', + token: (request) => request.query.host + '/token' + }); + + server.auth.strategy('custom', 'bell', { + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: 'test', + clientSecret: 'secret', + provider + }); + + server.route({ + method: '*', + path: '/login', + options: { + auth: 'custom', + handler: function (request, h) { + + return request.auth.credentials; + } + } + }); + + const res1 = await server.inject('/login?host=' + mock.uri); + + const cookie = res1.headers['set-cookie'][0].split(';')[0] + ';'; + const res2 = await mock.server.inject(res1.headers.location + '&host=' + mock.uri); + + const res3 = await server.inject({ url: res2.headers.location + '&host=' + mock.uri, headers: { cookie } }); + expect(res3.statusCode).to.equal(200); + }); + it('authenticates an endpoint via oauth with auth provider parameters', async (flags) => { const mock = await Mock.v1(flags); @@ -897,6 +938,46 @@ describe('Bell', () => { describe('v2()', () => { + it('authenticates an endpoint with provider (function)', async (flags) => { + + const mock = await Mock.v2(flags); + const server = Hapi.server({ host: 'localhost', port: 8080 }); + await server.register(Bell); + + const provider = Hoek.merge(mock.provider, { + auth: (request) => request.query.host + '/auth', + token: (request) => request.query.host + '/token' + }); + + server.auth.strategy('custom', 'bell', { + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: 'test', + clientSecret: 'secret', + provider + }); + + server.route({ + method: '*', + path: '/login', + options: { + auth: 'custom', + handler: function (request, h) { + + return request.auth.credentials; + } + } + }); + + const res1 = await server.inject('/login?host=' + mock.uri); + const cookie = res1.headers['set-cookie'][0].split(';')[0] + ';'; + + const res2 = await mock.server.inject(res1.headers.location + '&host=' + mock.uri); + + const res3 = await server.inject({ url: res2.headers.location + '&host=' + mock.uri, headers: { cookie } }); + expect(res3.statusCode).to.equal(200); + }); + it('authenticates an endpoint with provider parameters', async (flags) => { const mock = await Mock.v2(flags); From eadaa0370e12f81e03179edfbf78addfeb9e798d Mon Sep 17 00:00:00 2001 From: Charles B Johnson Date: Sun, 7 Jun 2020 19:06:46 -0500 Subject: [PATCH 2/3] Fix missing coverage --- test/oauth.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/oauth.js b/test/oauth.js index 35c5a2e4..80e964a4 100755 --- a/test/oauth.js +++ b/test/oauth.js @@ -2235,6 +2235,12 @@ describe('Bell', () => { expect(OAuth.Client.baseUri('http://example.com:8080/x')).to.equal('http://example.com:8080/x'); expect(OAuth.Client.baseUri('https://example.com:8080/x')).to.equal('https://example.com:8080/x'); }); + + it('passes through without port', () => { + + expect(OAuth.Client.baseUri('http://example.com/x')).to.equal('http://example.com/x'); + expect(OAuth.Client.baseUri('https://example.com/x')).to.equal('https://example.com/x'); + }); }); describe('signature()', () => { From 079656908ab531ba633ca9820b8db5e7e8639a4b Mon Sep 17 00:00:00 2001 From: Charles B Johnson Date: Sun, 5 Jul 2020 02:35:07 -0500 Subject: [PATCH 3/3] Restore compatibility --- lib/oauth.js | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/oauth.js b/lib/oauth.js index c15b2836..18c52d7b 100755 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -23,6 +23,11 @@ exports.v1 = function (settings) { return async function (request, h) { + client.settings.temporary = client.settings.temporary || + internals.Client.baseUri(internals.resolveProviderEndpoint(request, settings.provider.temporary)); + client.settings.token = client.settings.token || + internals.Client.baseUri(internals.resolveProviderEndpoint(request, settings.provider.token)); + const cookie = settings.cookie; const name = settings.name; const protocol = internals.getProtocol(request, settings); @@ -56,10 +61,9 @@ exports.v1 = function (settings) { // Obtain temporary OAuth credentials - const temporaryEndpoint = internals.resolveProviderEndpoint(request, settings.provider.temporary); const oauth_callback = internals.location(request, protocol, settings.location); try { - var { payload: temp } = await client.temporary(temporaryEndpoint, oauth_callback); + var { payload: temp } = await client.temporary(oauth_callback); } catch (err) { return h.unauthenticated(err, { credentials }); @@ -104,9 +108,8 @@ exports.v1 = function (settings) { // Obtain token OAuth credentials - const tokenEndpoint = internals.resolveProviderEndpoint(request, settings.provider.token); try { - var { payload: token } = await client.token(tokenEndpoint, state.token, request.query.oauth_verifier, state.secret); + var { payload: token } = await client.token(state.token, request.query.oauth_verifier, state.secret); } catch (err) { return h.unauthenticated(err, { credentials }); @@ -391,8 +394,13 @@ internals.refreshRedirect = function (request, name, protocol, settings, credent exports.Client = internals.Client = function (options) { + const temporary = internals.resolveProviderEndpoint(null, options.provider.temporary); + const token = internals.resolveProviderEndpoint(null, options.provider.token); + this.provider = options.name; this.settings = { + temporary: temporary ? internals.Client.baseUri(temporary) : null, + token: token ? internals.Client.baseUri(token) : null, clientId: options.clientId, clientSecret: options.provider.signatureMethod === 'RSA-SHA1' ? options.clientSecret : internals.encode(options.clientSecret || '') + '&', signatureMethod: options.provider.signatureMethod @@ -400,7 +408,7 @@ exports.Client = internals.Client = function (options) { }; -internals.Client.prototype.temporary = function (uri, oauth_callback) { +internals.Client.prototype.temporary = function (oauth_callback) { // Temporary Credentials (2.1) @@ -408,11 +416,11 @@ internals.Client.prototype.temporary = function (uri, oauth_callback) { oauth_callback }; - return this._request('post', internals.Client.baseUri(uri), null, oauth, { desc: 'temporary credentials' }); + return this._request('post', this.settings.temporary, null, oauth, { desc: 'temporary credentials' }); }; -internals.Client.prototype.token = function (uri, oauthToken, oauthVerifier, tokenSecret) { +internals.Client.prototype.token = function (oauthToken, oauthVerifier, tokenSecret) { // Token Credentials (2.3) @@ -421,7 +429,7 @@ internals.Client.prototype.token = function (uri, oauthToken, oauthVerifier, tok oauth_verifier: oauthVerifier }; - return this._request('post', internals.Client.baseUri(uri), null, oauth, { secret: tokenSecret, desc: 'token credentials' }); + return this._request('post', this.settings.token, null, oauth, { secret: tokenSecret, desc: 'token credentials' }); }; @@ -729,7 +737,11 @@ internals.getProtocol = function (request, settings) { internals.resolveProviderEndpoint = function (request, endpoint) { - return typeof endpoint === 'function' ? endpoint(request) : endpoint; + if (typeof endpoint === 'function') { + return request ? endpoint(request) : null; + } + + return endpoint; };