Skip to content

Commit e822918

Browse files
committed
refactor: provider.registerGrantType accepts the handler directly
BREAKING CHANGE: since provider is now available on `ctx.oidc.provider` the registerGrantType now expects the second argument to be the handler directly
1 parent 94f85c6 commit e822918

File tree

8 files changed

+432
-457
lines changed

8 files changed

+432
-457
lines changed

docs/README.md

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -305,44 +305,31 @@ router.post('/interaction/:uid/login', async (ctx, next) => {
305305

306306
## Custom Grant Types
307307
oidc-provider comes with the basic grants implemented, but you can register your own grant types,
308-
for example to implement a [OAuth 2.0 Token Exchange][token-exchange]. You can check the standard
308+
for example to implement an [OAuth 2.0 Token Exchange][token-exchange]. You can check the standard
309309
grant factories [here](/lib/actions/grants).
310310

311-
```js
312-
const parameters = ['username', 'password'];
313-
314-
// For OAuth 2.0 Token Exchange you can specify allowedDuplicateParameters as ['audience', 'resource']
315-
const allowedDuplicateParameters = [];
311+
**Note: Since custom grant types are registered after instantiating a Provider instance they can
312+
only be used by clients loaded by an adapter, statically configured clients will throw
313+
InvalidClientMetadata errors.**
316314

317-
provider.registerGrantType('password', function passwordGrantTypeFactory(providerInstance) {
318-
return async function passwordGrantType(ctx, next) {
319-
let account;
320-
if ((account = await Account.authenticate(ctx.oidc.params.username, ctx.oidc.params.password))) {
321-
const AccessToken = providerInstance.AccessToken;
322-
const at = new AccessToken({
323-
gty: 'password',
324-
accountId: account.id,
325-
client: ctx.oidc.client,
326-
});
315+
```js
316+
const parameters = [
317+
'audience', 'resource', 'scope', 'requested_token_type',
318+
'subject_token', 'subject_token_type',
319+
'actor_token', 'actor_token_type'
320+
];
321+
const allowedDuplicateParameters = ['audience', 'resource'];
322+
const grantType = 'urn:ietf:params:oauth:grant-type:token-exchange';
327323

328-
const accessToken = await at.save();
324+
async function tokenExchangeHandler(ctx, next) {
325+
// ctx.oidc.params holds the parsed parameters
326+
// ctx.oidc.client has the authenticated client
329327

330-
ctx.body = {
331-
access_token: accessToken,
332-
expires_in: at.expiration,
333-
token_type: 'Bearer',
334-
};
335-
} else {
336-
ctx.body = {
337-
error: 'invalid_grant',
338-
error_description: 'invalid credentials provided',
339-
};
340-
ctx.status = 400;
341-
}
328+
// your grant implementation
329+
// see /lib/actions/grants for references on how to instantiate and issue tokens
330+
}
342331

343-
await next();
344-
};
345-
}, parameters, allowedDuplicateParameters);
332+
provider.registerGrantType(grantType, tokenExchangeHandler, parameters, allowedDuplicateParameters);
346333
```
347334

348335

lib/actions/grants/authorization_code.js

Lines changed: 116 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -9,163 +9,161 @@ const revokeGrant = require('../../helpers/revoke_grant');
99

1010
const gty = 'authorization_code';
1111

12-
module.exports.handler = function getAuthorizationCodeHandler(provider) {
12+
module.exports.handler = async function authorizationCodeHandler(ctx, next) {
1313
const {
1414
issueRefreshToken,
1515
audiences,
1616
conformIdTokenClaims,
17-
} = instance(provider).configuration();
18-
19-
return async function authorizationCodeResponse(ctx, next) {
20-
if (ctx.oidc.params.redirect_uri === undefined) {
21-
// It is permitted to omit the redirect_uri if only ONE is registered on the client
22-
const { 0: uri, length } = ctx.oidc.client.redirectUris;
23-
if (uri && length === 1) {
24-
ctx.oidc.params.redirect_uri = uri;
25-
}
17+
} = instance(ctx.oidc.provider).configuration();
18+
19+
if (ctx.oidc.params.redirect_uri === undefined) {
20+
// It is permitted to omit the redirect_uri if only ONE is registered on the client
21+
const { 0: uri, length } = ctx.oidc.client.redirectUris;
22+
if (uri && length === 1) {
23+
ctx.oidc.params.redirect_uri = uri;
2624
}
25+
}
2726

28-
presence(ctx, 'code', 'redirect_uri');
27+
presence(ctx, 'code', 'redirect_uri');
2928

30-
const code = await provider.AuthorizationCode.find(ctx.oidc.params.code, {
31-
ignoreExpiration: true,
32-
});
29+
const code = await ctx.oidc.provider.AuthorizationCode.find(ctx.oidc.params.code, {
30+
ignoreExpiration: true,
31+
});
3332

34-
if (!code) {
35-
throw new InvalidGrant('authorization code not found');
36-
}
33+
if (!code) {
34+
throw new InvalidGrant('authorization code not found');
35+
}
3736

38-
uidToGrantId('switched from uid=%s to value of grantId=%s', ctx.oidc.uid, code.grantId);
39-
ctx.oidc.uid = code.grantId;
37+
uidToGrantId('switched from uid=%s to value of grantId=%s', ctx.oidc.uid, code.grantId);
38+
ctx.oidc.uid = code.grantId;
4039

41-
if (code.isExpired) {
42-
throw new InvalidGrant('authorization code is expired');
43-
}
40+
if (code.isExpired) {
41+
throw new InvalidGrant('authorization code is expired');
42+
}
4443

45-
checkPKCE(ctx.oidc.params.code_verifier, code.codeChallenge, code.codeChallengeMethod);
44+
checkPKCE(ctx.oidc.params.code_verifier, code.codeChallenge, code.codeChallengeMethod);
4645

47-
if (code.clientId !== ctx.oidc.client.clientId) {
48-
throw new InvalidGrant('authorization code client mismatch');
49-
}
46+
if (code.clientId !== ctx.oidc.client.clientId) {
47+
throw new InvalidGrant('authorization code client mismatch');
48+
}
5049

51-
if (code.consumed) {
52-
await Promise.all([
53-
code.destroy(),
54-
revokeGrant(provider, ctx.oidc.client, code.grantId),
55-
]);
56-
provider.emit('grant.revoked', ctx, code.grantId);
57-
throw new InvalidGrant('authorization code already consumed');
58-
}
50+
if (code.consumed) {
51+
await Promise.all([
52+
code.destroy(),
53+
revokeGrant(ctx.oidc.provider, ctx.oidc.client, code.grantId),
54+
]);
55+
ctx.oidc.provider.emit('grant.revoked', ctx, code.grantId);
56+
throw new InvalidGrant('authorization code already consumed');
57+
}
5958

60-
await code.consume();
59+
await code.consume();
6160

62-
if (code.redirectUri !== ctx.oidc.params.redirect_uri) {
63-
throw new InvalidGrant('authorization code redirect_uri mismatch');
64-
}
61+
if (code.redirectUri !== ctx.oidc.params.redirect_uri) {
62+
throw new InvalidGrant('authorization code redirect_uri mismatch');
63+
}
64+
65+
ctx.oidc.entity('AuthorizationCode', code);
66+
67+
const account = await ctx.oidc.provider.Account.findById(ctx, code.accountId, code);
6568

66-
ctx.oidc.entity('AuthorizationCode', code);
69+
if (!account) {
70+
throw new InvalidGrant('authorization code invalid (referenced account not found)');
71+
}
72+
ctx.oidc.entity('Account', account);
6773

68-
const account = await provider.Account.findById(ctx, code.accountId, code);
74+
const { AccessToken, IdToken, RefreshToken } = ctx.oidc.provider;
75+
const at = new AccessToken({
76+
accountId: account.accountId,
77+
claims: code.claims,
78+
client: ctx.oidc.client,
79+
expiresWithSession: code.expiresWithSession,
80+
grantId: code.grantId,
81+
gty,
82+
scope: code.scope,
83+
sessionUid: code.sessionUid,
84+
sid: code.sid,
85+
});
6986

70-
if (!account) {
71-
throw new InvalidGrant('authorization code invalid (referenced account not found)');
87+
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
88+
const cert = ctx.get('x-ssl-client-cert');
89+
90+
if (!cert) {
91+
throw new InvalidGrant('mutual TLS client certificate missing');
7292
}
73-
ctx.oidc.entity('Account', account);
93+
at.setS256Thumbprint(cert);
94+
}
95+
96+
at.setAudiences(await audiences(ctx, account.accountId, at, 'access_token'));
7497

75-
const { AccessToken, IdToken, RefreshToken } = provider;
76-
const at = new AccessToken({
98+
ctx.oidc.entity('AccessToken', at);
99+
const accessToken = await at.save();
100+
101+
let refreshToken;
102+
if (await issueRefreshToken(ctx, ctx.oidc.client, code)) {
103+
const rt = new RefreshToken({
77104
accountId: account.accountId,
105+
acr: code.acr,
106+
amr: code.amr,
107+
authTime: code.authTime,
78108
claims: code.claims,
79109
client: ctx.oidc.client,
80110
expiresWithSession: code.expiresWithSession,
81111
grantId: code.grantId,
82112
gty,
113+
nonce: code.nonce,
114+
resource: code.resource,
83115
scope: code.scope,
84116
sessionUid: code.sessionUid,
85117
sid: code.sid,
86118
});
87119

88-
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
120+
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens && ctx.oidc.client.tokenEndpointAuthMethod === 'none') {
89121
const cert = ctx.get('x-ssl-client-cert');
90-
91-
if (!cert) {
92-
throw new InvalidGrant('mutual TLS client certificate missing');
93-
}
94-
at.setS256Thumbprint(cert);
122+
// cert presence is already checked in the access token block
123+
rt.setS256Thumbprint(cert);
95124
}
96125

97-
at.setAudiences(await audiences(ctx, account.accountId, at, 'access_token'));
98-
99-
ctx.oidc.entity('AccessToken', at);
100-
const accessToken = await at.save();
101-
102-
let refreshToken;
103-
if (await issueRefreshToken(ctx, ctx.oidc.client, code)) {
104-
const rt = new RefreshToken({
105-
accountId: account.accountId,
106-
acr: code.acr,
107-
amr: code.amr,
108-
authTime: code.authTime,
109-
claims: code.claims,
110-
client: ctx.oidc.client,
111-
expiresWithSession: code.expiresWithSession,
112-
grantId: code.grantId,
113-
gty,
114-
nonce: code.nonce,
115-
resource: code.resource,
116-
scope: code.scope,
117-
sessionUid: code.sessionUid,
118-
sid: code.sid,
119-
});
120-
121-
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens && ctx.oidc.client.tokenEndpointAuthMethod === 'none') {
122-
const cert = ctx.get('x-ssl-client-cert');
123-
// cert presence is already checked in the access token block
124-
rt.setS256Thumbprint(cert);
125-
}
126-
127-
ctx.oidc.entity('RefreshToken', rt);
128-
refreshToken = await rt.save();
126+
ctx.oidc.entity('RefreshToken', rt);
127+
refreshToken = await rt.save();
128+
}
129+
130+
let idToken;
131+
if (code.scopes.has('openid')) {
132+
const claims = get(code, 'claims.id_token', {});
133+
const rejected = get(code, 'claims.rejected', []);
134+
const token = new IdToken({
135+
...await account.claims('id_token', code.scope, claims, rejected),
136+
acr: code.acr,
137+
amr: code.amr,
138+
auth_time: code.authTime,
139+
}, { ctx });
140+
141+
if (conformIdTokenClaims) {
142+
token.scope = 'openid';
143+
} else {
144+
token.scope = code.scope;
129145
}
130146

131-
let idToken;
132-
if (code.scopes.has('openid')) {
133-
const claims = get(code, 'claims.id_token', {});
134-
const rejected = get(code, 'claims.rejected', []);
135-
const token = new IdToken({
136-
...await account.claims('id_token', code.scope, claims, rejected),
137-
acr: code.acr,
138-
amr: code.amr,
139-
auth_time: code.authTime,
140-
}, { ctx });
141-
142-
if (conformIdTokenClaims) {
143-
token.scope = 'openid';
144-
} else {
145-
token.scope = code.scope;
146-
}
147-
148-
token.mask = claims;
149-
token.rejected = rejected;
150-
151-
token.set('nonce', code.nonce);
152-
token.set('at_hash', accessToken);
153-
token.set('sid', code.sid);
154-
155-
idToken = await token.sign();
156-
}
147+
token.mask = claims;
148+
token.rejected = rejected;
157149

158-
ctx.body = {
159-
access_token: accessToken,
160-
expires_in: at.expiration,
161-
id_token: idToken,
162-
refresh_token: refreshToken,
163-
scope: code.scope,
164-
token_type: 'Bearer',
165-
};
150+
token.set('nonce', code.nonce);
151+
token.set('at_hash', accessToken);
152+
token.set('sid', code.sid);
166153

167-
await next();
154+
idToken = await token.sign();
155+
}
156+
157+
ctx.body = {
158+
access_token: accessToken,
159+
expires_in: at.expiration,
160+
id_token: idToken,
161+
refresh_token: refreshToken,
162+
scope: code.scope,
163+
token_type: 'Bearer',
168164
};
165+
166+
await next();
169167
};
170168

171169
module.exports.parameters = new Set(['code', 'redirect_uri']);

0 commit comments

Comments
 (0)