From 4eaa75f714a744f9e712615dedc6702f4f9b7a64 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 7 Nov 2019 16:43:20 +0100 Subject: [PATCH] feat: allow tokenType for userinfo to use as authorization header scheme Also makes it so that unless provided explicitly the token_type from a TokenSet will be used by default. Adds the userinfo options argument to docs and types. --- docs/README.md | 10 ++++-- lib/client.js | 21 ++++++++----- test/client/client_instance.test.js | 47 +++++++++++++++++++++++++++-- types/index.d.ts | 3 +- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/docs/README.md b/docs/README.md index 8f445014..b6f77fa3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -137,7 +137,7 @@ Performs [OpenID Provider Issuer Discovery][webfinger-discovery] based on End-Us - [client.callbackParams(input)](#clientcallbackparamsinput) - [client.callback(redirectUri, parameters[, checks[, extras]])](#clientcallbackredirecturi-parameters-checks-extras) - [client.refresh(refreshToken[, extras])](#clientrefreshrefreshtoken-extras) - - [client.userinfo(accessToken)](#clientuserinfoaccesstoken) + - [client.userinfo(accessToken[, options])](#clientuserinfoaccesstoken-options) - [client.grant(body[, extras])](#clientgrantbody-extras) - [client.introspect(token[, tokenTypeHint[, extras]])](#clientintrospecttoken-tokentypehint-extras) - [client.revoke(token[, tokenTypeHint[, extras]])](#clientrevoketoken-tokentypehint-extras) @@ -311,7 +311,7 @@ Performs `refresh_token` grant type exchange. --- -#### `client.userinfo(accessToken)` +#### `client.userinfo(accessToken[, options])` Fetches the OIDC `userinfo` response with the provided Access Token. Also handles signed and/or encrypted userinfo responses. When TokenSet is provided as an argument the userinfo `sub` property @@ -319,6 +319,12 @@ will also be checked to match the on in the TokenSet's ID Token. - `accessToken`: `` | `` Access Token value. When TokenSet instance is provided its `access_token` property will be used automatically. +- `extras`: `` + - `verb`: `` The HTTP verb to use for the request 'GET' or 'POST'. **Default:** 'GET' + - `via`: `` The mechanism to use to attach the Access Token to the request. Valid values + are `header`, `body`, or `query`. **Default:** 'header'. + - `tokenType`: `` The token type as the Authorization Header scheme. **Default:** 'Bearer' + or the `token_type` property from a passed in TokenSet. - Returns: `Promise` Parsed userinfo response. --- diff --git a/lib/client.js b/lib/client.js index 556c48e4..06c28355 100644 --- a/lib/client.js +++ b/lib/client.js @@ -35,8 +35,8 @@ function pickCb(input) { return pick(input, ...CALLBACK_PROPERTIES); } -function bearer(token) { - return `Bearer ${token}`; +function authorizationHeaderValue(token, tokenType = 'Bearer') { + return `${tokenType} ${token}`; } function cleanUpClaims(claims) { @@ -896,7 +896,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base assertIssuerConfiguration(this.issuer, 'userinfo_endpoint'); let token = accessToken; const opts = merge({ - verb: 'get', + verb: 'GET', via: 'header', }, options); @@ -904,6 +904,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base if (!token.access_token) { throw new TypeError('access_token not present in TokenSet'); } + opts.tokenType = opts.tokenType || token.token_type; token = token.access_token; } @@ -924,7 +925,11 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base requestOpts = { form: true, body: { access_token: token } }; break; default: - requestOpts = { headers: { Authorization: bearer(token) } }; + requestOpts = { + headers: { + Authorization: authorizationHeaderValue(token, opts.tokenType), + }, + }; } if (opts.params) { @@ -1184,7 +1189,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base const requestOpts = { headers: { Accept: 'application/jwt', - Authorization: bearer(def.access_token || tokens[sourceName]), + Authorization: authorizationHeaderValue(def.access_token || tokens[sourceName]), }, }; @@ -1258,7 +1263,9 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base } const response = await request.call(this, { - headers: initialAccessToken ? { Authorization: bearer(initialAccessToken) } : undefined, + headers: initialAccessToken ? { + Authorization: authorizationHeaderValue(initialAccessToken), + } : undefined, json: true, body: properties, url: this.issuer.registration_endpoint, @@ -1290,7 +1297,7 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base method: 'GET', url: registrationClientUri, json: true, - headers: { Authorization: bearer(registrationAccessToken) }, + headers: { Authorization: authorizationHeaderValue(registrationAccessToken) }, }); const responseBody = processResponse(response, { bearer: true }); diff --git a/test/client/client_instance.test.js b/test/client/client_instance.test.js index 77239607..054d13be 100644 --- a/test/client/client_instance.test.js +++ b/test/client/client_instance.test.js @@ -763,13 +763,30 @@ describe('Client', () => { nock('https://op.example.com') .matchHeader('Accept', 'application/json') - .get('/me').reply(200, {}); + .matchHeader('Authorization', 'Bearer tokenValue') + .get('/me') + .reply(200, {}); return client.userinfo('tokenValue').then(() => { expect(nock.isDone()).to.be.true; }); }); + it('takes a string token and a tokenType option', function () { + const issuer = new Issuer({ userinfo_endpoint: 'https://op.example.com/me' }); + const client = new issuer.Client({ client_id: 'identifier', token_endpoint_auth_method: 'none' }); + + nock('https://op.example.com') + .matchHeader('Accept', 'application/json') + .matchHeader('Authorization', 'DPoP tokenValue') + .get('/me') + .reply(200, {}); + + return client.userinfo('tokenValue', { tokenType: 'DPoP' }).then(() => { + expect(nock.isDone()).to.be.true; + }); + }); + it('takes a tokenset', function () { const issuer = new Issuer({ userinfo_endpoint: 'https://op.example.com/me' }); const client = new issuer.Client({ @@ -778,7 +795,32 @@ describe('Client', () => { }); nock('https://op.example.com') - .get('/me').reply(200, { + .matchHeader('Authorization', 'Bearer tokenValue') + .get('/me') + .reply(200, { + sub: 'subject', + }); + + return client.userinfo(new TokenSet({ + id_token: 'eyJhbGciOiJub25lIn0.eyJzdWIiOiJzdWJqZWN0In0.', + refresh_token: 'bar', + access_token: 'tokenValue', + })).then(() => { + expect(nock.isDone()).to.be.true; + }); + }); + + it('takes a tokenset with a token_type', function () { + const issuer = new Issuer({ userinfo_endpoint: 'https://op.example.com/me' }); + const client = new issuer.Client({ + client_id: 'identifier', + id_token_signed_response_alg: 'none', + }); + + nock('https://op.example.com') + .matchHeader('Authorization', 'DPoP tokenValue') + .get('/me') + .reply(200, { sub: 'subject', }); @@ -786,6 +828,7 @@ describe('Client', () => { id_token: 'eyJhbGciOiJub25lIn0.eyJzdWIiOiJzdWJqZWN0In0.', refresh_token: 'bar', access_token: 'tokenValue', + token_type: 'DPoP', })).then(() => { expect(nock.isDone()).to.be.true; }); diff --git a/types/index.d.ts b/types/index.d.ts index 9a5aa428..aca92a82 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -434,8 +434,9 @@ export class Client { * * @param accessToken Access Token value. When TokenSet instance is provided its access_token property * will be used automatically. + * @param options Options for the UserInfo request. */ - userinfo(accessToken: TokenSet | string): Promise; + userinfo(accessToken: TokenSet | string, options?: { verb?: 'GET' | 'POST', via?: 'header' | 'body' | 'query', tokenType?: string }): Promise; /** * Performs an arbitrary grant_type exchange at the token_endpoint.