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..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); @@ -79,7 +84,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 @@ -230,7 +236,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 +294,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 }); @@ -385,10 +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: internals.Client.baseUri(options.provider.temporary), - token: internals.Client.baseUri(options.provider.token), + 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 @@ -723,6 +735,16 @@ internals.getProtocol = function (request, settings) { }; +internals.resolveProviderEndpoint = function (request, endpoint) { + + if (typeof endpoint === 'function') { + return request ? endpoint(request) : null; + } + + return 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..80e964a4 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); @@ -2154,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()', () => {