UNPKG

@strongnguyen/oidc-provider

Version:

OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect

253 lines (205 loc) 7.42 kB
const difference = require('../../helpers/_/difference'); const { InvalidGrant, InvalidScope } = require('../../helpers/errors'); const presence = require('../../helpers/validate_presence'); const instance = require('../../helpers/weak_cache'); const revoke = require('../../helpers/revoke'); const { 'x5t#S256': thumbprint } = require('../../helpers/calculate_thumbprint'); const formatters = require('../../helpers/formatters'); const filterClaims = require('../../helpers/filter_claims'); const dpopValidate = require('../../helpers/validate_dpop'); const resolveResource = require('../../helpers/resolve_resource'); const gty = 'refresh_token'; module.exports.handler = async function refreshTokenHandler(ctx, next) { presence(ctx, 'refresh_token'); const conf = instance(ctx.oidc.provider).configuration(); const { conformIdTokenClaims, rotateRefreshToken, features: { userinfo, dPoP: { iatTolerance }, mTLS: { getCertificate }, resourceIndicators, }, } = conf; const { RefreshToken, Account, AccessToken, IdToken, ReplayDetection, } = ctx.oidc.provider; const { client } = ctx.oidc; let refreshTokenValue = ctx.oidc.params.refresh_token; let refreshToken = await RefreshToken.find(refreshTokenValue, { ignoreExpiration: true }); if (!refreshToken) { throw new InvalidGrant('refresh token not found'); } if (refreshToken.clientId !== client.clientId) { throw new InvalidGrant('client mismatch'); } if (refreshToken.isExpired) { throw new InvalidGrant('refresh token is expired'); } let cert; if (client.tlsClientCertificateBoundAccessTokens || refreshToken['x5t#S256']) { cert = getCertificate(ctx); if (!cert) { throw new InvalidGrant('mutual TLS client certificate not provided'); } } if (refreshToken['x5t#S256'] && refreshToken['x5t#S256'] !== thumbprint(cert)) { throw new InvalidGrant('failed x5t#S256 verification'); } const grant = await ctx.oidc.provider.Grant.find(refreshToken.grantId, { ignoreExpiration: true, }); if (!grant) { throw new InvalidGrant('grant not found'); } if (grant.isExpired) { throw new InvalidGrant('grant is expired'); } if (grant.clientId !== client.clientId) { throw new InvalidGrant('client mismatch'); } if (ctx.oidc.params.scope) { const missing = difference([...ctx.oidc.requestParamScopes], [...refreshToken.scopes]); if (missing.length !== 0) { throw new InvalidScope(`refresh token missing requested ${formatters.pluralize('scope', missing.length)}`, missing.join(' ')); } } const dPoP = await dpopValidate(ctx); if (dPoP) { const unique = await ReplayDetection.unique( client.clientId, dPoP.jti, dPoP.iat + iatTolerance, ); ctx.assert(unique, new InvalidGrant('DPoP Token Replay detected')); } if (refreshToken.jkt && (!dPoP || refreshToken.jkt !== dPoP.thumbprint)) { throw new InvalidGrant('failed jkt verification'); } ctx.oidc.entity('RefreshToken', refreshToken); ctx.oidc.entity('Grant', grant); const account = await Account.findAccount(ctx, refreshToken.accountId, refreshToken); if (!account) { throw new InvalidGrant('refresh token invalid (referenced account not found)'); } if (refreshToken.accountId !== grant.accountId) { throw new InvalidGrant('accountId mismatch'); } ctx.oidc.entity('Account', account); if (refreshToken.consumed) { await Promise.all([ refreshToken.destroy(), revoke(ctx, refreshToken.grantId), ]); throw new InvalidGrant('refresh token already used'); } if ( rotateRefreshToken === true || (typeof rotateRefreshToken === 'function' && await rotateRefreshToken(ctx)) ) { await refreshToken.consume(); ctx.oidc.entity('RotatedRefreshToken', refreshToken); refreshToken = new RefreshToken({ accountId: refreshToken.accountId, acr: refreshToken.acr, amr: refreshToken.amr, authTime: refreshToken.authTime, claims: refreshToken.claims, client, expiresWithSession: refreshToken.expiresWithSession, iiat: refreshToken.iiat, grantId: refreshToken.grantId, gty: refreshToken.gty, nonce: refreshToken.nonce, resource: refreshToken.resource, rotations: typeof refreshToken.rotations === 'number' ? refreshToken.rotations + 1 : 1, scope: refreshToken.scope, sessionUid: refreshToken.sessionUid, sid: refreshToken.sid, 'x5t#S256': refreshToken['x5t#S256'], jkt: refreshToken.jkt, }); if (refreshToken.gty && !refreshToken.gty.endsWith(gty)) { refreshToken.gty = `${refreshToken.gty} ${gty}`; } ctx.oidc.entity('RefreshToken', refreshToken); refreshTokenValue = await refreshToken.save(); } const at = new AccessToken({ accountId: account.accountId, client, expiresWithSession: refreshToken.expiresWithSession, grantId: refreshToken.grantId, gty: refreshToken.gty, sessionUid: refreshToken.sessionUid, sid: refreshToken.sid, }); if (client.tlsClientCertificateBoundAccessTokens) { at.setThumbprint('x5t', cert); } if (dPoP) { at.setThumbprint('jkt', dPoP.thumbprint); } if (at.gty && !at.gty.endsWith(gty)) { at.gty = `${at.gty} ${gty}`; } const scope = ctx.oidc.params.scope ? ctx.oidc.requestParamScopes : refreshToken.scopes; const resource = await resolveResource( ctx, refreshToken, { userinfo, resourceIndicators }, scope, ); 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, scope); } else { at.claims = refreshToken.claims; at.scope = grant.getOIDCScopeFiltered(scope); } ctx.oidc.entity('AccessToken', at); const accessToken = await at.save(); let idToken; if (scope.has('openid')) { const claims = filterClaims(refreshToken.claims, 'id_token', grant); const rejected = grant.getRejectedOIDCClaims(); const token = new IdToken(({ ...await account.claims('id_token', [...scope].join(' '), claims, rejected), acr: refreshToken.acr, amr: refreshToken.amr, auth_time: refreshToken.authTime, }), { ctx }); if (conformIdTokenClaims && userinfo.enabled && !at.aud) { token.scope = 'openid'; } else { token.scope = grant.getOIDCScopeFiltered(scope); } token.mask = claims; token.rejected = rejected; token.set('nonce', refreshToken.nonce); token.set('at_hash', accessToken); token.set('sid', refreshToken.sid); idToken = await token.issue({ use: 'idtoken' }); } ctx.body = { access_token: accessToken, expires_in: at.expiration, id_token: idToken, refresh_token: refreshTokenValue, scope: at.scope, token_type: at.tokenType, }; // add trackingAction to ctx switch (refreshToken.amr) { case 'fast_login': ctx.trackingAction = 'loginfast'; break; case 'pwd': ctx.trackingAction = 'login'; break; default: ctx.trackingAction = `login${refreshToken.amr}`; break; } ctx.oidc.provider.emit('refresh_token', ctx); await next(); }; module.exports.parameters = new Set(['refresh_token', 'scope']);