@strongnguyen/oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
222 lines (177 loc) • 6.19 kB
JavaScript
const { InvalidGrant } = require('../../helpers/errors');
const presence = require('../../helpers/validate_presence');
const instance = require('../../helpers/weak_cache');
const checkPKCE = require('../../helpers/pkce');
const revoke = require('../../helpers/revoke');
const filterClaims = require('../../helpers/filter_claims');
const dpopValidate = require('../../helpers/validate_dpop');
const resolveResource = require('../../helpers/resolve_resource');
const gty = 'authorization_code';
module.exports.handler = async function authorizationCodeHandler(ctx, next) {
const {
issueRefreshToken,
allowOmittingSingleRegisteredRedirectUri,
conformIdTokenClaims,
features: {
userinfo,
dPoP: { iatTolerance },
mTLS: { getCertificate },
resourceIndicators,
},
} = instance(ctx.oidc.provider).configuration();
if (allowOmittingSingleRegisteredRedirectUri && 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');
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.clientId !== ctx.oidc.client.clientId) {
throw new InvalidGrant('client mismatch');
}
if (code.isExpired) {
throw new InvalidGrant('authorization code is expired');
}
const grant = await ctx.oidc.provider.Grant.find(code.grantId, {
ignoreExpiration: true,
});
if (!grant) {
throw new InvalidGrant('grant not found');
}
if (grant.isExpired) {
throw new InvalidGrant('grant is expired');
}
checkPKCE(ctx.oidc.params.code_verifier, code.codeChallenge, code.codeChallengeMethod);
let cert;
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
cert = getCertificate(ctx);
if (!cert) {
throw new InvalidGrant('mutual TLS client certificate not provided');
}
}
if (grant.clientId !== ctx.oidc.client.clientId) {
throw new InvalidGrant('client mismatch');
}
if (code.redirectUri !== ctx.oidc.params.redirect_uri) {
throw new InvalidGrant('authorization code redirect_uri mismatch');
}
if (code.consumed) {
await revoke(ctx, code.grantId);
throw new InvalidGrant('authorization code already consumed');
}
await code.consume();
ctx.oidc.entity('AuthorizationCode', code);
ctx.oidc.entity('Grant', grant);
const account = await ctx.oidc.provider.Account.findAccount(ctx, code.accountId, code);
if (!account) {
throw new InvalidGrant('authorization code invalid (referenced account not found)');
}
if (code.accountId !== grant.accountId) {
throw new InvalidGrant('accountId mismatch');
}
ctx.oidc.entity('Account', account);
const {
AccessToken, IdToken, RefreshToken, ReplayDetection,
} = ctx.oidc.provider;
const at = new AccessToken({
accountId: account.accountId,
client: ctx.oidc.client,
expiresWithSession: code.expiresWithSession,
grantId: code.grantId,
gty,
sessionUid: code.sessionUid,
sid: code.sid,
});
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
at.setThumbprint('x5t', cert);
}
const dPoP = await dpopValidate(ctx);
if (dPoP) {
const unique = await ReplayDetection.unique(
ctx.oidc.client.clientId, dPoP.jti, dPoP.iat + iatTolerance,
);
ctx.assert(unique, new InvalidGrant('DPoP Token Replay detected'));
at.setThumbprint('jkt', dPoP.thumbprint);
}
const resource = await resolveResource(ctx, code, { userinfo, resourceIndicators });
if (resource) {
const resourceServerInfo = await resourceIndicators
.getResourceServerInfo(ctx, resource, ctx.oidc.client);
at.resourceServer = new ctx.oidc.provider.ResourceServer(resource, resourceServerInfo);
at.scope = grant.getResourceScopeFiltered(resource, code.scopes);
} else {
at.claims = code.claims;
at.scope = grant.getOIDCScopeFiltered(code.scopes);
}
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,
rotations: 0,
scope: code.scope,
sessionUid: code.sessionUid,
sid: code.sid,
});
if (ctx.oidc.client.tokenEndpointAuthMethod === 'none') {
if (at.jkt) {
rt.jkt = at.jkt;
}
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
rt['x5t#S256'] = at['x5t#S256'];
}
}
ctx.oidc.entity('RefreshToken', rt);
refreshToken = await rt.save();
}
let idToken;
if (code.scopes.has('openid')) {
const claims = filterClaims(code.claims, 'id_token', grant);
const rejected = grant.getRejectedOIDCClaims();
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 && userinfo.enabled && !at.aud) {
token.scope = 'openid';
} else {
token.scope = grant.getOIDCScopeFiltered(code.scopes);
}
token.mask = claims;
token.rejected = rejected;
token.set('nonce', code.nonce);
token.set('at_hash', accessToken);
token.set('sid', code.sid);
idToken = await token.issue({ use: 'idtoken' });
}
ctx.body = {
access_token: accessToken,
expires_in: at.expiration,
id_token: idToken,
refresh_token: refreshToken,
scope: at.scope,
token_type: at.tokenType,
};
await next();
};
module.exports.parameters = new Set(['code', 'redirect_uri']);