oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
244 lines (204 loc) • 10.4 kB
JavaScript
import { strict as assert } from 'node:assert';
import Router from '@koa/router';
import devInteractions from '../actions/interaction.js';
import cors from '../shared/cors.js';
import * as grants from '../actions/grants/index.js';
import * as responseModes from '../response_modes/index.js';
import error from '../shared/error_handler.js';
import getAuthError from '../shared/authorization_error_handler.js';
import {
getAuthorization, userinfo, getToken, jwks, registration, getRevocation,
getIntrospection, discovery, endSession, codeVerification, challenge,
} from '../actions/index.js';
import als from './als.js';
import instance from './weak_cache.js';
export default function initializeApp() {
const { configuration, features } = instance(this);
const maxAge = 3600;
function exposeHeaders({
dpop = features.dPoP.enabled && features.dPoP.nonceSecret,
attest = features.attestClientAuth.enabled,
wwwAuth = true,
} = {}) {
return [
dpop ? 'DPoP-Nonce' : undefined,
attest ? 'OAuth-Client-Attestation-Challenge' : undefined,
wwwAuth ? 'WWW-Authenticate' : undefined,
].filter(Boolean);
}
const CORS = {
open: cors({ allowMethods: 'GET', maxAge }),
challenge: cors({ allowMethods: 'POST', maxAge, exposeHeaders: exposeHeaders({ wwwAuth: false }) }),
userinfo: cors({
allowMethods: 'GET,POST', clientBased: true, maxAge, exposeHeaders: exposeHeaders({ attest: false }),
}),
client: cors({
allowMethods: 'POST', clientBased: true, maxAge, exposeHeaders: exposeHeaders({ dpop: false }),
}),
clientWithDPoP: cors({
allowMethods: 'POST', clientBased: true, maxAge, exposeHeaders: exposeHeaders(),
}),
respond: () => {},
};
const router = new Router();
instance(this).router = router;
const ensureOIDC = async (ctx, next) => {
let oidcCtx;
Object.defineProperty(ctx, 'oidc', {
get: () => {
oidcCtx ||= new this.OIDCContext(ctx);
return oidcCtx;
},
});
return als.run(ctx, () => next());
};
const routeMap = new Map();
function normalizeRoute(name, route, ...stack) {
assert(typeof name === 'string' && name.charAt(0) !== '/', `invalid route name ${name}`);
assert(typeof route === 'string' && route.charAt(0) === '/', `invalid route ${route}`);
route = route.replace(/\/\//g, '/'); // eslint-disable-line no-param-reassign
stack.forEach((middleware) => assert.equal(typeof middleware, 'function'), 'invalid middleware');
routeMap.set(name, route);
return route;
}
async function ensureSessionSave(ctx, next) {
try {
await next();
} finally {
if (ctx.oidc.session?.touched && !ctx.oidc.session.destroyed) {
await ctx.oidc.session.persist();
}
}
}
const get = (name, route, ...stack) => {
route = normalizeRoute(name, route, ...stack); // eslint-disable-line no-param-reassign
router.get(name, route, ensureOIDC, ensureSessionSave, ...stack);
};
const post = (name, route, ...stack) => {
route = normalizeRoute(name, route, ...stack); // eslint-disable-line no-param-reassign
router.post(name, route, ensureOIDC, ensureSessionSave, ...stack);
};
const del = (name, route, ...stack) => {
route = normalizeRoute(name, route, ...stack); // eslint-disable-line no-param-reassign
router.delete(name, route, ensureOIDC, ...stack);
};
const put = (name, route, ...stack) => {
route = normalizeRoute(name, route, ...stack); // eslint-disable-line no-param-reassign
router.put(name, route, ensureOIDC, ...stack);
};
const options = (name, route, ...stack) => {
route = normalizeRoute(name, route, ...stack); // eslint-disable-line no-param-reassign
router.options(name, route, ensureOIDC, ...stack);
};
const { routes, enableHttpPostMethods } = configuration;
for (const { handler, parameters, grantType } of Object.values(grants)) {
const { grantTypeHandlers } = instance(this);
if (configuration.grantTypes.has(grantType) && !grantTypeHandlers.has(grantType)) {
let dupes;
if (features.resourceIndicators.enabled) {
parameters.add('resource');
dupes = new Set(['resource']);
}
if (features.richAuthorizationRequests.enabled) {
parameters.add('authorization_details');
}
this.registerGrantType(grantType, handler, parameters, dupes);
}
}
['query', 'fragment', 'form_post'].forEach((mode) => {
this.registerResponseMode(mode, responseModes[mode]);
});
if (features.webMessageResponseMode.enabled) {
this.registerResponseMode('web_message', responseModes.webMessage);
}
if (features.jwtResponseModes.enabled) {
this.registerResponseMode('jwt', responseModes.jwt);
['query', 'fragment', 'form_post'].forEach((mode) => {
this.registerResponseMode(`${mode}.jwt`, responseModes.jwt);
});
if (features.webMessageResponseMode.enabled) {
this.registerResponseMode('web_message.jwt', responseModes.jwt);
}
}
const authorization = getAuthorization(this, 'authorization');
const authError = getAuthError(this);
get('authorization', routes.authorization, authError, ...authorization);
if (enableHttpPostMethods) {
post('authorization', routes.authorization, authError, ...authorization);
}
const resume = getAuthorization(this, 'resume');
get('resume', `${routes.authorization}/:uid`, authError, ...resume);
if (features.userinfo.enabled) {
get('userinfo', routes.userinfo, CORS.userinfo, error(this, 'userinfo.error'), ...userinfo);
post('userinfo', routes.userinfo, CORS.userinfo, error(this, 'userinfo.error'), ...userinfo);
options('cors.userinfo', routes.userinfo, CORS.userinfo, CORS.respond);
}
const token = getToken(this);
post('token', routes.token, error(this, 'grant.error'), CORS.clientWithDPoP, ...token);
options('cors.token', routes.token, CORS.clientWithDPoP, CORS.respond);
get('jwks', routes.jwks, CORS.open, error(this, 'jwks.error'), jwks);
options('cors.jwks', routes.jwks, CORS.open, CORS.respond);
const oauthDiscoveryRoute = '/.well-known/oauth-authorization-server';
get('discovery', oauthDiscoveryRoute, CORS.open, error(this, 'discovery.error'), discovery);
options('cors.discovery', oauthDiscoveryRoute, CORS.open, CORS.respond);
const openidDiscoveryRoute = '/.well-known/openid-configuration';
get('discovery', openidDiscoveryRoute, CORS.open, error(this, 'discovery.error'), discovery);
options('cors.discovery', openidDiscoveryRoute, CORS.open, CORS.respond);
if (features.attestClientAuth.enabled) {
post('challenge', routes.challenge, error(this, 'challenge.error'), CORS.challenge, ...challenge);
options('cors.challenge', routes.challenge, CORS.challenge, CORS.respond);
}
if (features.registration.enabled) {
const clientRoute = `${routes.registration}/:clientId`;
post('registration', routes.registration, error(this, 'registration_create.error'), ...registration.post);
get('client', clientRoute, error(this, 'registration_read.error'), ...registration.get);
if (features.registrationManagement.enabled) {
put('client_update', clientRoute, error(this, 'registration_update.error'), ...registration.put);
del('client_delete', clientRoute, error(this, 'registration_delete.error'), ...registration.del);
}
}
if (features.revocation.enabled) {
const revocation = getRevocation(this);
post('revocation', routes.revocation, error(this, 'revocation.error'), CORS.client, ...revocation);
options('cors.revocation', routes.revocation, CORS.client, CORS.respond);
}
if (features.introspection.enabled) {
const introspection = getIntrospection(this);
post('introspection', routes.introspection, error(this, 'introspection.error'), CORS.client, ...introspection);
options('cors.introspection', routes.introspection, CORS.client, CORS.respond);
}
post('end_session_confirm', `${routes.end_session}/confirm`, error(this, 'end_session_confirm.error'), ...endSession.confirm);
if (features.rpInitiatedLogout.enabled) {
if (enableHttpPostMethods) {
post('end_session', routes.end_session, error(this, 'end_session.error'), ...endSession.init);
}
get('end_session', routes.end_session, error(this, 'end_session.error'), ...endSession.init);
get('end_session_success', `${routes.end_session}/success`, error(this, 'end_session_success.error'), ...endSession.success);
}
if (features.deviceFlow.enabled) {
const deviceAuthorization = getAuthorization(this, 'device_authorization');
post('device_authorization', routes.device_authorization, error(this, 'device_authorization.error'), CORS.client, ...deviceAuthorization);
options('cors.device_authorization', routes.device_authorization, CORS.client, CORS.respond);
const postCodeVerification = getAuthorization(this, 'code_verification');
get('code_verification', routes.code_verification, error(this, 'code_verification.error'), ...codeVerification.get);
post('code_verification', routes.code_verification, error(this, 'code_verification.error'), ...codeVerification.post, ...postCodeVerification);
const deviceResume = getAuthorization(this, 'device_resume');
get('device_resume', `${routes.code_verification}/:uid`, error(this, 'device_resume.error'), ...deviceResume);
}
if (features.pushedAuthorizationRequests.enabled) {
const pushedAuthorizationRequests = getAuthorization(this, 'pushed_authorization_request');
post('pushed_authorization_request', routes.pushed_authorization_request, error(this, 'pushed_authorization_request.error'), CORS.clientWithDPoP, ...pushedAuthorizationRequests);
options('cors.pushed_authorization_request', routes.pushed_authorization_request, CORS.clientWithDPoP, CORS.respond);
}
if (features.ciba.enabled) {
const ciba = getAuthorization(this, 'backchannel_authentication');
post('backchannel_authentication', routes.backchannel_authentication, error(this, 'backchannel_authentication.error'), ...ciba);
}
if (features.devInteractions.enabled) {
const interaction = devInteractions(this);
get('interaction', '/interaction/:uid', error(this), ...interaction.render);
post('submit', '/interaction/:uid', error(this), ...interaction.submit);
get('abort', '/interaction/:uid/abort', error(this), ...interaction.abort);
}
return router.routes();
}