Skip to content

Commit

Permalink
refactor: provider.registerGrantType accepts the handler directly
Browse files Browse the repository at this point in the history
BREAKING CHANGE: since provider is now available on `ctx.oidc.provider`
the registerGrantType now expects the second argument to be the handler
directly
  • Loading branch information
panva committed Apr 22, 2019
1 parent 94f85c6 commit e822918
Show file tree
Hide file tree
Showing 8 changed files with 432 additions and 457 deletions.
51 changes: 19 additions & 32 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,44 +305,31 @@ router.post('/interaction/:uid/login', async (ctx, next) => {

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

```js
const parameters = ['username', 'password'];

// For OAuth 2.0 Token Exchange you can specify allowedDuplicateParameters as ['audience', 'resource']
const allowedDuplicateParameters = [];
**Note: Since custom grant types are registered after instantiating a Provider instance they can
only be used by clients loaded by an adapter, statically configured clients will throw
InvalidClientMetadata errors.**

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

const accessToken = await at.save();
async function tokenExchangeHandler(ctx, next) {
// ctx.oidc.params holds the parsed parameters
// ctx.oidc.client has the authenticated client

ctx.body = {
access_token: accessToken,
expires_in: at.expiration,
token_type: 'Bearer',
};
} else {
ctx.body = {
error: 'invalid_grant',
error_description: 'invalid credentials provided',
};
ctx.status = 400;
}
// your grant implementation
// see /lib/actions/grants for references on how to instantiate and issue tokens
}

await next();
};
}, parameters, allowedDuplicateParameters);
provider.registerGrantType(grantType, tokenExchangeHandler, parameters, allowedDuplicateParameters);
```


Expand Down
234 changes: 116 additions & 118 deletions lib/actions/grants/authorization_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,163 +9,161 @@ const revokeGrant = require('../../helpers/revoke_grant');

const gty = 'authorization_code';

module.exports.handler = function getAuthorizationCodeHandler(provider) {
module.exports.handler = async function authorizationCodeHandler(ctx, next) {
const {
issueRefreshToken,
audiences,
conformIdTokenClaims,
} = instance(provider).configuration();

return async function authorizationCodeResponse(ctx, next) {
if (ctx.oidc.params.redirect_uri === undefined) {
// It is permitted to omit the redirect_uri if only ONE is registered on the client
const { 0: uri, length } = ctx.oidc.client.redirectUris;
if (uri && length === 1) {
ctx.oidc.params.redirect_uri = uri;
}
} = instance(ctx.oidc.provider).configuration();

if (ctx.oidc.params.redirect_uri === undefined) {
// It is permitted to omit the redirect_uri if only ONE is registered on the client
const { 0: uri, length } = ctx.oidc.client.redirectUris;
if (uri && length === 1) {
ctx.oidc.params.redirect_uri = uri;
}
}

presence(ctx, 'code', 'redirect_uri');
presence(ctx, 'code', 'redirect_uri');

const code = await provider.AuthorizationCode.find(ctx.oidc.params.code, {
ignoreExpiration: true,
});
const code = await ctx.oidc.provider.AuthorizationCode.find(ctx.oidc.params.code, {
ignoreExpiration: true,
});

if (!code) {
throw new InvalidGrant('authorization code not found');
}
if (!code) {
throw new InvalidGrant('authorization code not found');
}

uidToGrantId('switched from uid=%s to value of grantId=%s', ctx.oidc.uid, code.grantId);
ctx.oidc.uid = code.grantId;
uidToGrantId('switched from uid=%s to value of grantId=%s', ctx.oidc.uid, code.grantId);
ctx.oidc.uid = code.grantId;

if (code.isExpired) {
throw new InvalidGrant('authorization code is expired');
}
if (code.isExpired) {
throw new InvalidGrant('authorization code is expired');
}

checkPKCE(ctx.oidc.params.code_verifier, code.codeChallenge, code.codeChallengeMethod);
checkPKCE(ctx.oidc.params.code_verifier, code.codeChallenge, code.codeChallengeMethod);

if (code.clientId !== ctx.oidc.client.clientId) {
throw new InvalidGrant('authorization code client mismatch');
}
if (code.clientId !== ctx.oidc.client.clientId) {
throw new InvalidGrant('authorization code client mismatch');
}

if (code.consumed) {
await Promise.all([
code.destroy(),
revokeGrant(provider, ctx.oidc.client, code.grantId),
]);
provider.emit('grant.revoked', ctx, code.grantId);
throw new InvalidGrant('authorization code already consumed');
}
if (code.consumed) {
await Promise.all([
code.destroy(),
revokeGrant(ctx.oidc.provider, ctx.oidc.client, code.grantId),
]);
ctx.oidc.provider.emit('grant.revoked', ctx, code.grantId);
throw new InvalidGrant('authorization code already consumed');
}

await code.consume();
await code.consume();

if (code.redirectUri !== ctx.oidc.params.redirect_uri) {
throw new InvalidGrant('authorization code redirect_uri mismatch');
}
if (code.redirectUri !== ctx.oidc.params.redirect_uri) {
throw new InvalidGrant('authorization code redirect_uri mismatch');
}

ctx.oidc.entity('AuthorizationCode', code);

const account = await ctx.oidc.provider.Account.findById(ctx, code.accountId, code);

ctx.oidc.entity('AuthorizationCode', code);
if (!account) {
throw new InvalidGrant('authorization code invalid (referenced account not found)');
}
ctx.oidc.entity('Account', account);

const account = await provider.Account.findById(ctx, code.accountId, code);
const { AccessToken, IdToken, RefreshToken } = ctx.oidc.provider;
const at = new AccessToken({
accountId: account.accountId,
claims: code.claims,
client: ctx.oidc.client,
expiresWithSession: code.expiresWithSession,
grantId: code.grantId,
gty,
scope: code.scope,
sessionUid: code.sessionUid,
sid: code.sid,
});

if (!account) {
throw new InvalidGrant('authorization code invalid (referenced account not found)');
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
const cert = ctx.get('x-ssl-client-cert');

if (!cert) {
throw new InvalidGrant('mutual TLS client certificate missing');
}
ctx.oidc.entity('Account', account);
at.setS256Thumbprint(cert);
}

at.setAudiences(await audiences(ctx, account.accountId, at, 'access_token'));

const { AccessToken, IdToken, RefreshToken } = provider;
const at = new AccessToken({
ctx.oidc.entity('AccessToken', at);
const accessToken = await at.save();

let refreshToken;
if (await issueRefreshToken(ctx, ctx.oidc.client, code)) {
const rt = new RefreshToken({
accountId: account.accountId,
acr: code.acr,
amr: code.amr,
authTime: code.authTime,
claims: code.claims,
client: ctx.oidc.client,
expiresWithSession: code.expiresWithSession,
grantId: code.grantId,
gty,
nonce: code.nonce,
resource: code.resource,
scope: code.scope,
sessionUid: code.sessionUid,
sid: code.sid,
});

if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens && ctx.oidc.client.tokenEndpointAuthMethod === 'none') {
const cert = ctx.get('x-ssl-client-cert');

if (!cert) {
throw new InvalidGrant('mutual TLS client certificate missing');
}
at.setS256Thumbprint(cert);
// cert presence is already checked in the access token block
rt.setS256Thumbprint(cert);
}

at.setAudiences(await audiences(ctx, account.accountId, at, 'access_token'));

ctx.oidc.entity('AccessToken', at);
const accessToken = await at.save();

let refreshToken;
if (await issueRefreshToken(ctx, ctx.oidc.client, code)) {
const rt = new RefreshToken({
accountId: account.accountId,
acr: code.acr,
amr: code.amr,
authTime: code.authTime,
claims: code.claims,
client: ctx.oidc.client,
expiresWithSession: code.expiresWithSession,
grantId: code.grantId,
gty,
nonce: code.nonce,
resource: code.resource,
scope: code.scope,
sessionUid: code.sessionUid,
sid: code.sid,
});

if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens && ctx.oidc.client.tokenEndpointAuthMethod === 'none') {
const cert = ctx.get('x-ssl-client-cert');
// cert presence is already checked in the access token block
rt.setS256Thumbprint(cert);
}

ctx.oidc.entity('RefreshToken', rt);
refreshToken = await rt.save();
ctx.oidc.entity('RefreshToken', rt);
refreshToken = await rt.save();
}

let idToken;
if (code.scopes.has('openid')) {
const claims = get(code, 'claims.id_token', {});
const rejected = get(code, 'claims.rejected', []);
const token = new IdToken({
...await account.claims('id_token', code.scope, claims, rejected),
acr: code.acr,
amr: code.amr,
auth_time: code.authTime,
}, { ctx });

if (conformIdTokenClaims) {
token.scope = 'openid';
} else {
token.scope = code.scope;
}

let idToken;
if (code.scopes.has('openid')) {
const claims = get(code, 'claims.id_token', {});
const rejected = get(code, 'claims.rejected', []);
const token = new IdToken({
...await account.claims('id_token', code.scope, claims, rejected),
acr: code.acr,
amr: code.amr,
auth_time: code.authTime,
}, { ctx });

if (conformIdTokenClaims) {
token.scope = 'openid';
} else {
token.scope = code.scope;
}

token.mask = claims;
token.rejected = rejected;

token.set('nonce', code.nonce);
token.set('at_hash', accessToken);
token.set('sid', code.sid);

idToken = await token.sign();
}
token.mask = claims;
token.rejected = rejected;

ctx.body = {
access_token: accessToken,
expires_in: at.expiration,
id_token: idToken,
refresh_token: refreshToken,
scope: code.scope,
token_type: 'Bearer',
};
token.set('nonce', code.nonce);
token.set('at_hash', accessToken);
token.set('sid', code.sid);

await next();
idToken = await token.sign();
}

ctx.body = {
access_token: accessToken,
expires_in: at.expiration,
id_token: idToken,
refresh_token: refreshToken,
scope: code.scope,
token_type: 'Bearer',
};

await next();
};

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

0 comments on commit e822918

Please sign in to comment.