Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tokenParams option #497

Merged
merged 2 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion Providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
37 changes: 17 additions & 20 deletions lib/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -251,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) {
Expand Down Expand Up @@ -731,6 +729,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) ?? {};
};
4 changes: 4 additions & 0 deletions test/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
37 changes: 37 additions & 0 deletions test/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down