Skip to content

Commit

Permalink
feat: allow tokenType for userinfo to use as authorization header scheme
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
panva committed Nov 7, 2019
1 parent 46adfdb commit 4eaa75f
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 12 deletions.
10 changes: 8 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -311,14 +311,20 @@ 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
will also be checked to match the on in the TokenSet's ID Token.

- `accessToken`: `<string>` &vert; `<TokenSet>` Access Token value. When TokenSet instance is
provided its `access_token` property will be used automatically.
- `extras`: `<Object>`
- `verb`: `<string>` The HTTP verb to use for the request 'GET' or 'POST'. **Default:** 'GET'
- `via`: `<string>` The mechanism to use to attach the Access Token to the request. Valid values
are `header`, `body`, or `query`. **Default:** 'header'.
- `tokenType`: `<string>` The token type as the Authorization Header scheme. **Default:** 'Bearer'
or the `token_type` property from a passed in TokenSet.
- Returns: `Promise<Object>` Parsed userinfo response.

---
Expand Down
21 changes: 14 additions & 7 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -896,14 +896,15 @@ 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);

if (token instanceof TokenSet) {
if (!token.access_token) {
throw new TypeError('access_token not present in TokenSet');
}
opts.tokenType = opts.tokenType || token.token_type;
token = token.access_token;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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]),
},
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 });

Expand Down
47 changes: 45 additions & 2 deletions test/client/client_instance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -778,14 +795,40 @@ 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',
});

return client.userinfo(new TokenSet({
id_token: 'eyJhbGciOiJub25lIn0.eyJzdWIiOiJzdWJqZWN0In0.',
refresh_token: 'bar',
access_token: 'tokenValue',
token_type: 'DPoP',
})).then(() => {
expect(nock.isDone()).to.be.true;
});
Expand Down
3 changes: 2 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserinfoResponse>;
userinfo(accessToken: TokenSet | string, options?: { verb?: 'GET' | 'POST', via?: 'header' | 'body' | 'query', tokenType?: string }): Promise<UserinfoResponse>;

/**
* Performs an arbitrary grant_type exchange at the token_endpoint.
Expand Down

0 comments on commit 4eaa75f

Please sign in to comment.