oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
231 lines (185 loc) • 7.21 kB
JavaScript
import difference from '../helpers/_/difference.js';
import appendWWWAuthenticate from '../helpers/append_www_authenticate.js';
import bodyParser from '../shared/conditional_body.js';
import rejectDupes from '../shared/reject_dupes.js';
import paramsMiddleware from '../shared/assemble_params.js';
import noCache from '../shared/no_cache.js';
import certificateThumbprint from '../helpers/certificate_thumbprint.js';
import instance from '../helpers/weak_cache.js';
import filterClaims from '../helpers/filter_claims.js';
import dpopValidate, { CHALLENGE_OK_WINDOW } from '../helpers/validate_dpop.js';
import epochTime from '../helpers/epoch_time.js';
import {
InvalidToken, InsufficientScope, InvalidDpopProof, UseDpopNonce,
} from '../helpers/errors.js';
import getCtxAccountClaims from '../helpers/account_claims.js';
const PARAM_LIST = new Set([
'scope',
'access_token',
]);
const parseBody = bodyParser.bind(undefined, 'application/x-www-form-urlencoded');
export default [
noCache,
async function setWWWAuthenticateHeader(ctx, next) {
try {
await next();
} catch (err) {
if (err.expose) {
const { dPoP } = instance(ctx.oidc.provider).features;
if (err.error_description === 'no access token provided') {
appendWWWAuthenticate(ctx, 'Bearer', {
realm: ctx.oidc.issuer,
scope: err.scope,
});
if (dPoP.enabled) {
appendWWWAuthenticate(ctx, 'DPoP', {
realm: ctx.oidc.issuer,
scope: err.scope,
algs: instance(ctx.oidc.provider).configuration.dPoPSigningAlgValues.join(' '),
});
}
} else {
let scheme;
if (/dpop/i.test(err.error_description) || ctx.oidc.accessToken?.jkt || (dPoP.enabled && ctx.get('DPoP'))) {
scheme = 'DPoP';
} else {
scheme = 'Bearer';
}
if (err instanceof InvalidDpopProof || err instanceof UseDpopNonce) {
// eslint-disable-next-line no-multi-assign
err.status = err.statusCode = 401;
}
appendWWWAuthenticate(ctx, scheme, {
realm: ctx.oidc.issuer,
error: err.message,
error_description: err.error_description,
scope: err.scope,
...(scheme === 'DPoP' ? {
algs: instance(ctx.oidc.provider).configuration.dPoPSigningAlgValues.join(' '),
} : undefined),
});
}
}
throw err;
}
},
parseBody,
paramsMiddleware.bind(undefined, PARAM_LIST),
rejectDupes.bind(undefined, {}),
async function validateAccessToken(ctx, next) {
const accessTokenValue = ctx.oidc.getAccessToken({ acceptDPoP: true });
const dPoP = await dpopValidate(ctx, accessTokenValue);
const accessToken = await ctx.oidc.provider.AccessToken.find(accessTokenValue);
ctx.assert(accessToken, new InvalidToken('access token not found'));
ctx.oidc.entity('AccessToken', accessToken);
const { scopes } = accessToken;
if (!scopes.size || !scopes.has('openid')) {
throw new InsufficientScope('access token missing openid scope', 'openid');
}
if (accessToken['x5t#S256']) {
const { getCertificate } = instance(ctx.oidc.provider).features.mTLS;
const cert = getCertificate(ctx);
if (!cert || accessToken['x5t#S256'] !== certificateThumbprint(cert)) {
throw new InvalidToken('failed x5t#S256 verification');
}
}
if (dPoP) {
const { allowReplay } = instance(ctx.oidc.provider).features.dPoP;
if (!allowReplay) {
const unique = await ctx.oidc.provider.ReplayDetection.unique(
accessToken.clientId,
dPoP.jti,
epochTime() + CHALLENGE_OK_WINDOW,
);
ctx.assert(unique, new InvalidToken('DPoP proof JWT Replay detected'));
}
if (!accessToken.jkt) {
throw new InvalidToken('access token is not sender-constrained but proof of possession was provided');
}
}
if (accessToken.jkt && (!dPoP || accessToken.jkt !== dPoP.thumbprint)) {
throw new InvalidToken('failed jkt verification');
}
await next();
},
function validateAudience(ctx, next) {
const { oidc: { entities: { AccessToken: accessToken } } } = ctx;
if (accessToken.aud !== undefined) {
throw new InvalidToken('token audience prevents accessing the userinfo endpoint');
}
return next();
},
async function validateScope(ctx, next) {
if (ctx.oidc.params.scope) {
const missing = difference(ctx.oidc.params.scope.split(' '), [...ctx.oidc.accessToken.scopes]);
if (missing.length !== 0) {
throw new InsufficientScope('access token missing requested scope', missing.join(' '));
}
}
await next();
},
async function loadClient(ctx, next) {
const client = await ctx.oidc.provider.Client.find(ctx.oidc.accessToken.clientId);
ctx.assert(client, new InvalidToken('associated client not found'));
ctx.oidc.entity('Client', client);
await next();
},
async function loadAccount(ctx, next) {
const account = await instance(ctx.oidc.provider).configuration.findAccount(
ctx,
ctx.oidc.accessToken.accountId,
ctx.oidc.accessToken,
);
ctx.assert(account, new InvalidToken('associated account not found'));
ctx.oidc.entity('Account', account);
await next();
},
async function loadGrant(ctx, next) {
const grant = await ctx.oidc.provider.Grant.find(ctx.oidc.accessToken.grantId, {
ignoreExpiration: true,
});
if (!grant) {
throw new InvalidToken('grant not found');
}
if (grant.isExpired) {
throw new InvalidToken('grant is expired');
}
if (grant.clientId !== ctx.oidc.accessToken.clientId) {
throw new InvalidToken('clientId mismatch');
}
if (grant.accountId !== ctx.oidc.accessToken.accountId) {
throw new InvalidToken('accountId mismatch');
}
ctx.oidc.entity('Grant', grant);
await next();
},
async function respond(ctx) {
const claims = filterClaims(ctx.oidc.accessToken.claims, 'userinfo', ctx.oidc.grant);
const rejected = ctx.oidc.grant.getRejectedOIDCClaims();
const scope = ctx.oidc.grant.getOIDCScopeFiltered(new Set((ctx.oidc.params.scope || ctx.oidc.accessToken.scope).split(' ')));
const { client } = ctx.oidc;
if (client.userinfoSignedResponseAlg || client.userinfoEncryptedResponseAlg) {
const token = new ctx.oidc.provider.IdToken(
await getCtxAccountClaims(ctx, 'userinfo', scope, claims, rejected),
{ ctx },
);
token.scope = scope;
token.mask = claims;
token.rejected = rejected;
ctx.body = await token.issue({
expiresAt: ctx.oidc.accessToken.exp,
use: 'userinfo',
});
ctx.type = 'application/jwt; charset=utf-8';
} else {
const mask = new ctx.oidc.provider.Claims(
await getCtxAccountClaims(ctx, 'userinfo', scope, claims, rejected),
{ ctx },
);
mask.scope(scope);
mask.mask(claims);
mask.rejected(rejected);
ctx.body = await mask.result();
}
},
];