From 21b03fbe44d2eb7dbe1e85079b08be3e41351a00 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Tue, 11 Mar 2025 17:10:49 +0100 Subject: [PATCH 1/2] Slight refactor to not clone object in resolveProviderParams() --- lib/oauth.js | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/oauth.js b/lib/oauth.js index 6253b7d..acc77bc 100755 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -72,12 +72,11 @@ exports.v1 = function (settings) { h.state(cookie, state); - const authQuery = internals.resolveProviderParams(request, settings.providerParams); - authQuery.oauth_token = temp.oauth_token; - - if (settings.allowRuntimeProviderParams) { - Hoek.merge(authQuery, request.query); - } + const authQuery = { + ...internals.resolveProviderParams(request, settings.providerParams), + oauth_token: temp.oauth_token, + ...(settings.allowRuntimeProviderParams && request.query) + }; return h.redirect(settings.provider.auth + '?' + internals.queryString(authQuery)).takeover(); } @@ -123,7 +122,7 @@ exports.v1 = function (settings) { const get = async (uri, params = {}) => { if (settings.profileParams) { - Hoek.merge(params, settings.profileParams); + Object.assign(params, settings.profileParams); } const { payload: resource } = await client.resource('get', uri, params, { token: token.oauth_token, secret: token.oauth_token_secret }); @@ -178,16 +177,14 @@ exports.v2 = function (settings) { credentials.query = request.query; const nonce = Cryptiles.randomAlphanumString(internals.nonceLength); - const query = internals.resolveProviderParams(request, settings.providerParams); - - if (settings.allowRuntimeProviderParams) { - Hoek.merge(query, request.query); - } - - query.client_id = settings.clientId; - query.response_type = 'code'; - query.redirect_uri = internals.location(request, protocol, settings.location); - query.state = nonce; + const query = { + ...internals.resolveProviderParams(request, settings.providerParams), + ...(settings.allowRuntimeProviderParams && request.query), + client_id: settings.clientId, + response_type: 'code', + redirect_uri: internals.location(request, protocol, settings.location), + state: nonce + }; if (settings.runtimeStateCallback) { const runtimeState = settings.runtimeStateCallback(request); @@ -731,6 +728,5 @@ internals.getProtocol = function (request, settings) { internals.resolveProviderParams = function (request, params) { - const obj = typeof params === 'function' ? params(request) : params; - return obj ? Hoek.clone(obj) : {}; + return (typeof params === 'function' ? params(request) : params) ?? {}; }; From 5c25ecfe196fb92b66716e0565741b0b5052d74c Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Tue, 11 Mar 2025 17:17:40 +0100 Subject: [PATCH 2/2] Add a tokenParam option that apply to the /token endpoint --- API.md | 4 ++++ Providers.md | 13 ++++++++++++- lib/index.d.ts | 6 ++++++ lib/index.js | 2 ++ lib/oauth.js | 3 ++- test/mock.js | 4 ++++ test/oauth.js | 37 +++++++++++++++++++++++++++++++++++++ 7 files changed, 67 insertions(+), 2 deletions(-) diff --git a/API.md b/API.md index 2299917..eef71f2 100755 --- a/API.md +++ b/API.md @@ -274,6 +274,10 @@ Each strategy accepts the following optional settings: - `uri` - allows pointing to a private enterprise installation (e.g. `'https://vpn.example.com'`). See [Providers documentation](https://github.com/hapijs/bell/blob/master/Providers.md) for more information. +- `tokenParams` - provider-specific query parameters for the token endpoint. It may be + passed either as an object to merge into the query string, or a function which takes the client's + `request` and returns an object. Each provider supports its own set of parameters which customize + the user's login experience. - `profileParams` - an object of key-value pairs that specify additional URL query parameters to send with the profile request to the provider. The built-in `facebook` provider, for example, could have `fields` specified to determine the fields returned from the user's graph, which would diff --git a/Providers.md b/Providers.md index b1be87d..ba9272b 100755 --- a/Providers.md +++ b/Providers.md @@ -37,12 +37,23 @@ credentials.profile = { [Provider Documentation](https://auth0.com/docs/protocols#oauth-server-side) -- `scope`: not applicable +- `scope`: Defaults to `['openid', 'email', 'profile']` - `config`: - `domain`: Your Auth0 domain name, such as `example.auth0.com` or `example.eu.auth0.com` - `auth`: [/authorize](https://auth0.com/docs/auth-api#!#get--authorize_social) - `token`: [/oauth/token](https://auth0.com/docs/protocols#3-getting-the-access-token) +To create a token for a specific endpoint, add it to the `providerParams` and `tokenParams` options, eg.: + +```js +providerParams: { + endpoint: 'https://api.service.com' +}, +tokenParams: { + endpoint: 'https://api.service.com' +} +``` + To authenticate a user with a specific identity provider directly, use `providerParams`. For example: ```javascript diff --git a/lib/index.d.ts b/lib/index.d.ts index 065ca2a..5443249 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -125,6 +125,12 @@ export interface OptionalOptions { | { extendedProfile?: boolean | undefined; getMethod?: string | undefined } | { uri?: string | undefined } | undefined; + /** + * provider-specific query parameters for the token endpoint. + * It may be passed either as an object to merge into the query string, + * or a function which takes the client's request and returns an object. + */ + tokenParams?: StringLikeMap | ((request: Request) => StringLikeMap) | undefined; /** * an object of key-value pairs that specify additional * URL query parameters to send with the profile request to the provider. diff --git a/lib/index.js b/lib/index.js index 20b5c80..f96fdb2 100755 --- a/lib/index.js +++ b/lib/index.js @@ -104,6 +104,8 @@ internals.schema = Joi.object({ config: Joi.object(), + tokenParams: Joi.alternatives(Joi.object(), Joi.func()), + profileParams: Joi.object(), skipProfile: internals.flexBoolean.optional().default(false), diff --git a/lib/oauth.js b/lib/oauth.js index acc77bc..408b349 100755 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -248,7 +248,8 @@ exports.v2 = function (settings) { const query = { grant_type: 'authorization_code', code: request.query.code, - redirect_uri: internals.location(request, protocol, settings.location) + redirect_uri: internals.location(request, protocol, settings.location), + ...internals.resolveProviderParams(request, settings.tokenParams) }; if (settings.provider.pkce) { diff --git a/test/mock.js b/test/mock.js index b7c9553..090668e 100755 --- a/test/mock.js +++ b/test/mock.js @@ -226,6 +226,10 @@ exports.v2 = async function (flags, options = {}) { payload.id = 'https://login.salesforce.com/id/foo/bar'; } + if (code.client_id === 'endpoint') { + payload.endpoint = request.payload.endpoint; + } + return h.response(payload).code(options.code ?? 200); } } diff --git a/test/oauth.js b/test/oauth.js index 97fcd92..0c8506f 100755 --- a/test/oauth.js +++ b/test/oauth.js @@ -1270,6 +1270,43 @@ describe('Bell', () => { expect(res.headers.location).to.contain(mock.uri + '/auth?special=true&runtime=5&client_id=test&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin&state='); }); + it('authenticates an endpoint token provider parameters', async (flags) => { + + const mock = await Mock.v2(flags); + const server = Hapi.server({ host: 'localhost', port: 8080 }); + await server.register(Bell); + + server.auth.strategy('custom', 'bell', { + password: 'cookie_encryption_password_secure', + isSecure: false, + clientId: 'endpoint', + clientSecret: 'secret', + provider: mock.provider, + tokenParams: { endpoint: 'https://test.com' } + }); + + server.route({ + method: '*', + path: '/login', + options: { + auth: 'custom', + handler: function (request, h) { + + return request.auth.artifacts; + } + } + }); + + const res1 = await server.inject('/login'); + const cookie = res1.headers['set-cookie'][0].split(';')[0] + ';'; + + const res2 = await mock.server.inject(res1.headers.location); + + const res3 = await server.inject({ url: res2.headers.location, headers: { cookie } }); + expect(res3.statusCode).to.equal(200); + expect(res3.result).to.include({ endpoint: 'https://test.com' }); + }); + it('authenticates an endpoint via oauth with plain PKCE', async (flags) => { const mock = await Mock.v2(flags);