oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
1,132 lines (1,030 loc) • 149 kB
JavaScript
/* eslint-disable no-shadow */
/* eslint-disable no-unused-vars */
import * as crypto from 'node:crypto';
import * as attention from './attention.js';
import nanoid from './nanoid.js';
import { base as defaultPolicy } from './interaction_policy/index.js';
import htmlSafe from './html_safe.js';
import * as errors from './errors.js';
const warned = new Set();
function shouldChange(name, msg) {
if (!warned.has(name)) {
warned.add(name);
attention.info(`default ${name} function called, you SHOULD change it in order to ${msg}.`);
}
}
function mustChange(name, msg) {
if (!warned.has(name)) {
warned.add(name);
attention.warn(`default ${name} function called, you MUST change it in order to ${msg}.`);
}
}
function clientBasedCORS(ctx, origin, client) {
shouldChange('clientBasedCORS', 'control allowed CORS Origins based on the client making a CORS request');
if (ctx.oidc.route === 'userinfo' || client.clientAuthMethod === 'none') {
return client.redirectUris.some((uri) => URL.parse(uri)?.origin === origin);
}
return false;
}
function getCertificate(ctx) {
mustChange('features.mTLS.getCertificate', 'retrieve the PEM-formatted client certificate from the request context');
throw new Error('features.mTLS.getCertificate function not configured');
}
function certificateAuthorized(ctx) {
mustChange('features.mTLS.certificateAuthorized', 'determine if the client certificate is verified and comes from a trusted CA');
throw new Error('features.mTLS.certificateAuthorized function not configured');
}
function certificateSubjectMatches(ctx, property, expected) {
mustChange('features.mTLS.certificateSubjectMatches', 'verify that the tls_client_auth_* registered client property value matches the certificate one');
throw new Error('features.mTLS.certificateSubjectMatches function not configured');
}
function deviceInfo(ctx) {
return {
ip: ctx.ip,
ua: ctx.get('user-agent'),
};
}
function fetch(url, options) {
/* eslint-disable no-param-reassign */
options.signal = AbortSignal.timeout(2500);
options.headers = new Headers(options.headers);
options.headers.set('user-agent', ''); // removes the user-agent header in Node's global fetch()
// eslint-disable-next-line no-undef
return globalThis.fetch(url, options);
/* eslint-enable no-param-reassign */
}
async function userCodeInputSource(ctx, form, out, err) {
// @param ctx - koa request context
// @param form - form source (id="op.deviceInputForm") to be embedded in the page and submitted
// by the End-User.
// @param out - if an error is returned the out object contains details that are fit to be
// rendered, i.e. does not include internal error messages
// @param err - error object with an optional userCode property passed when the form is being
// re-rendered due to code missing/invalid/expired
shouldChange('features.deviceFlow.userCodeInputSource', 'customize the look of the user code input page');
let msg;
if (err && (err.userCode || err.name === 'NoCodeError')) {
msg = '<p class="red">The code you entered is incorrect. Try again</p>';
} else if (err && err.name === 'AbortedError') {
msg = '<p class="red">The Sign-in request was interrupted</p>';
} else if (err) {
msg = '<p class="red">There was an error processing your request</p>';
} else {
msg = '<p>Enter the code displayed on your device</p>';
}
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Sign-in</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);h1,h1+p{font-weight:100;text-align:center}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}h1{font-size:2.3em}p.red{color:#d50000}input[type=email],input[type=password],input[type=text]{height:44px;font-size:16px;width:100%;margin-bottom:10px;-webkit-appearance:none;background:#fff;border:1px solid #d9d9d9;border-top:1px solid silver;padding:0 8px;box-sizing:border-box;-moz-box-sizing:border-box}[type=submit]{width:100%;display:block;margin-bottom:10px;position:relative;text-align:center;font-size:14px;font-family:Arial,sans-serif;font-weight:700;height:36px;padding:0 8px;border:0;color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-color:#4d90fe;cursor:pointer}[type=submit]:hover{border:0;text-shadow:0 1px rgba(0,0,0,.3);background-color:#357ae8}input[type=text]{text-transform:uppercase;text-align: center}input[type=text]::placeholder{text-transform: none}
</style>
</head>
<body>
<div class="container">
<h1>Sign-in</h1>
${msg}
${form}
<button type="submit" form="op.deviceInputForm">Continue</button>
</div>
</body>
</html>`;
}
function requireNonce(ctx) {
return false;
}
async function getAttestationSignaturePublicKey(ctx, iss, header, client) {
// @param ctx - koa request context
// @param iss - Issuer Identifier from the Client Attestation JWT
// @param header - Protected Header of the Client Attestation JWT
// @param client - client making the request
mustChange('features.attestClientAuth.getAttestationSignaturePublicKey', 'be able to verify the Client Attestation JWT signature');
throw new Error('features.attestClientAuth.getAttestationSignaturePublicKey not implemented');
}
async function assertAttestationJwtAndPop(ctx, attestation, pop, client) {
// @param ctx - koa request context
// @param attestation - verified and parsed Attestation JWT
// attestation.protectedHeader - parsed protected header object
// attestation.payload - parsed protected header object
// attestation.key - CryptoKey that verified the Attestation JWT signature
// @param pop - verified and parsed Attestation JWT PoP
// pop.protectedHeader - parsed protected header object
// pop.payload - parsed protected header object
// pop.key - CryptoKey that verified the Attestation JWT PoP signature
// @param client - client making the request
}
async function userCodeConfirmSource(ctx, form, client, deviceInfo, userCode) {
// @param ctx - koa request context
// @param form - form source (id="op.deviceConfirmForm") to be embedded in the page and
// submitted by the End-User.
// @param deviceInfo - device information from the device_authorization_endpoint call
// @param userCode - formatted user code by the configured mask
shouldChange('features.deviceFlow.userCodeConfirmSource', 'customize the look of the user code confirmation page');
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Device Login Confirmation</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);.help,h1,h1+p{text-align:center}h1,h1+p{font-weight:100}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#f7f7f7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}h1{font-size:2.3em}button[autofocus]{width:100%;display:block;margin-bottom:10px;position:relative;font-size:14px;font-family:Arial,sans-serif;font-weight:700;height:36px;padding:0 8px;border:0;color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-color:#4d90fe;cursor:pointer}button[autofocus]:hover{border:0;text-shadow:0 1px rgba(0,0,0,.3);background-color:#357ae8}button[name=abort]{background:0 0!important;border:none;padding:0!important;font:inherit;cursor:pointer}a,button[name=abort]{text-decoration:none;color:#666;font-weight:400;display:inline-block;opacity:.6}.help{width:100%;font-size:12px}code{font-size:2em}
</style>
</head>
<body>
<div class="container">
<h1>Confirm Device</h1>
<p>
<strong>${ctx.oidc.client.clientName || ctx.oidc.client.clientId}</strong>
<br/><br/>
The following code should be displayed on your device<br/><br/>
<code>${userCode}</code>
<br/><br/>
<small>If you did not initiate this action, the code does not match or are unaware of such device in your possession please close this window or click abort.</small>
</p>
${form}
<button autofocus type="submit" form="op.deviceConfirmForm">Continue</button>
<div class="help">
<button type="submit" form="op.deviceConfirmForm" value="yes" name="abort">[ Abort ]</button>
</div>
</div>
</body>
</html>`;
}
async function successSource(ctx) {
// @param ctx - koa request context
shouldChange('features.deviceFlow.successSource', 'customize the look of the device code success page');
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Sign-in Success</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);h1,h1+p{font-weight:100;text-align:center}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}h1{font-size:2.3em}
</style>
</head>
<body>
<div class="container">
<h1>Sign-in Success</h1>
<p>Your sign-in ${ctx.oidc.client.clientName ? `with ${ctx.oidc.client.clientName}` : ''} was successful, you can now close this page.</p>
</div>
</body>
</html>`;
}
async function introspectionAllowedPolicy(ctx, client, token) {
// @param ctx - koa request context
// @param client - authenticated client making the request
// @param token - token being introspected
shouldChange('features.introspection.allowedPolicy', 'to check whether the caller is authorized to receive the introspection response');
if (
client.clientAuthMethod === 'none'
&& token.clientId !== ctx.oidc.client.clientId
) {
return false;
}
return true;
}
async function revocationAllowedPolicy(ctx, client, token) {
// @param ctx - koa request context
// @param client - authenticated client making the request
// @param token - token being revoked
shouldChange('features.revocation.allowedPolicy', 'to check whether the caller is authorized to revoke the token');
if (token.clientId !== client.clientId) {
if (client.clientAuthMethod === 'none') {
// do not revoke but respond as success to disallow guessing valid tokens
return false;
}
throw new errors.InvalidRequest('client is not authorized to revoke the presented token');
}
return true;
}
function idFactory(ctx) {
return nanoid();
}
async function secretFactory(ctx) {
return crypto.randomBytes(64).toString('base64url');
}
async function defaultResource(ctx, client, oneOf) {
// @param ctx - koa request context
// @param client - client making the request
// @param oneOf {string[]} - The authorization server needs to select **one** of the values provided.
// Default is that the array is provided so that the request will fail.
// This argument is only provided when called during
// Authorization Code / Refresh Token / Device Code exchanges.
if (oneOf) return oneOf;
return undefined;
}
async function useGrantedResource(ctx, model) {
// @param ctx - koa request context
// @param model - depending on the request's grant_type this can be either an AuthorizationCode, BackchannelAuthenticationRequest,
// RefreshToken, or DeviceCode model instance.
return false;
}
async function getResourceServerInfo(ctx, resourceIndicator, client) {
// @param ctx - koa request context
// @param resourceIndicator - resource indicator value either requested or resolved by the defaultResource helper.
// @param client - client making the request
mustChange('features.resourceIndicators.getResourceServerInfo', 'to provide details about the Resource Server identified by the Resource Indicator');
throw new errors.InvalidTarget();
}
async function extraTokenClaims(ctx, token) {
return undefined;
}
async function expiresWithSession(ctx, code) {
return !code.scopes.has('offline_access');
}
async function issueRefreshToken(ctx, client, code) {
return (
client.grantTypeAllowed('refresh_token')
&& code.scopes.has('offline_access')
);
}
function pkceRequired(ctx, client) {
// All public clients MUST use PKCE as per
// https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.1-2.1
if (client.clientAuthMethod === 'none') {
return true;
}
const fapiProfile = ctx.oidc.isFapi('2.0', '1.0 Final');
// FAPI 2.0 as per
// https://openid.net/specs/fapi-security-profile-2_0-final.html#section-5.3.2.2-2.5
if (fapiProfile === '2.0') {
return true;
}
// FAPI 1.0 Advanced as per
// https://openid.net/specs/openid-financial-api-part-2-1_0-final.html#authorization-server
if (fapiProfile === '1.0 Final' && ctx.oidc.route === 'pushed_authorization_request') {
return true;
}
// In all other cases use of PKCE is RECOMMENDED as per
// https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.1-2.2
// but the server doesn't force them to.
return false;
}
async function pairwiseIdentifier(ctx, accountId, client) {
mustChange('pairwiseIdentifier', 'provide an implementation for pairwise identifiers');
throw new Error('pairwiseIdentifier not implemented');
}
function AccessTokenTTL(ctx, token, client) {
shouldChange('ttl.AccessToken', 'define the expiration for AccessToken artifacts');
return token.resourceServer?.accessTokenTTL || 60 * 60; // 1 hour in seconds
}
function AuthorizationCodeTTL(ctx, code, client) {
return 60; // 1 minute in seconds
}
function ClientCredentialsTTL(ctx, token, client) {
shouldChange('ttl.ClientCredentials', 'define the expiration for ClientCredentials artifacts');
return token.resourceServer?.accessTokenTTL || 10 * 60; // 10 minutes in seconds
}
function DeviceCodeTTL(ctx, deviceCode, client) {
shouldChange('ttl.DeviceCode', 'define the expiration for DeviceCode artifacts');
return 10 * 60; // 10 minutes in seconds
}
function BackchannelAuthenticationRequestTTL(ctx, request, client) {
shouldChange('ttl.BackchannelAuthenticationRequest', 'define the expiration for BackchannelAuthenticationRequest artifacts');
if (ctx?.oidc?.params.requested_expiry) {
return Math.min(10 * 60, +ctx.oidc.params.requested_expiry); // 10 minutes in seconds or requested_expiry, whichever is shorter
}
return 10 * 60; // 10 minutes in seconds
}
function IdTokenTTL(ctx, token, client) {
shouldChange('ttl.IdToken', 'define the expiration for IdToken artifacts');
return 60 * 60; // 1 hour in seconds
}
function RefreshTokenTTL(ctx, token, client) {
shouldChange('ttl.RefreshToken', 'define the expiration for RefreshToken artifacts');
if (
ctx?.oidc?.entities.RotatedRefreshToken
&& client.applicationType === 'web'
&& client.clientAuthMethod === 'none'
&& !token.isSenderConstrained()
) {
// Non-Sender Constrained SPA RefreshTokens do not have infinite expiration through rotation
return ctx.oidc.entities.RotatedRefreshToken.remainingTTL;
}
return 14 * 24 * 60 * 60; // 14 days in seconds
}
function InteractionTTL(ctx, interaction) {
shouldChange('ttl.Interaction', 'define the expiration for Interaction artifacts');
return 60 * 60; // 1 hour in seconds
}
function SessionTTL(ctx, session) {
shouldChange('ttl.Session', 'define the expiration for Session artifacts');
return 14 * 24 * 60 * 60; // 14 days in seconds
}
function GrantTTL(ctx, grant, client) {
shouldChange('ttl.Grant', 'define the expiration for Grant artifacts');
return 14 * 24 * 60 * 60; // 14 days in seconds
}
function extraClientMetadataValidator(ctx, key, value, metadata) {
// @param ctx - koa request context (only provided when a client is being constructed during
// Client Registration Request or Client Update Request
// @param key - the client metadata property name
// @param value - the property value
// @param metadata - the current accumulated client metadata
// @param ctx - koa request context (only provided when a client is being constructed during
// Client Registration Request or Client Update Request
}
async function postLogoutSuccessSource(ctx) {
// @param ctx - koa request context
shouldChange('features.rpInitiatedLogout.postLogoutSuccessSource', 'customize the look of the default post logout success page');
const display = ctx.oidc.client?.clientName || ctx.oidc.client?.clientId;
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Sign-out Success</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);h1,h1+p{font-weight:100;text-align:center}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}h1{font-size:2.3em}
</style>
</head>
<body>
<div class="container">
<h1>Sign-out Success</h1>
<p>Your sign-out ${display ? `with ${display}` : ''} was successful.</p>
</div>
</body>
</html>`;
}
async function logoutSource(ctx, form) {
// @param ctx - koa request context
// @param form - form source (id="op.logoutForm") to be embedded in the page and submitted by
// the End-User
shouldChange('features.rpInitiatedLogout.logoutSource', 'customize the look of the logout page');
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Logout Request</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);button,h1{text-align:center}h1{font-weight:100;font-size:1.3em}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}button{font-size:14px;font-family:Arial,sans-serif;font-weight:700;height:36px;padding:0 8px;width:100%;display:block;margin-bottom:10px;position:relative;border:0;color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-color:#4d90fe;cursor:pointer}button:hover{border:0;text-shadow:0 1px rgba(0,0,0,.3);background-color:#357ae8}
</style>
</head>
<body>
<div class="container">
<h1>Do you want to sign-out from ${ctx.host}?</h1>
${form}
<button autofocus type="submit" form="op.logoutForm" value="yes" name="logout">Yes, sign me out</button>
<button type="submit" form="op.logoutForm">No, stay signed in</button>
</div>
</body>
</html>`;
}
async function renderError(ctx, out, error) {
shouldChange('renderError', 'customize the look of the error page');
ctx.type = 'html';
ctx.body = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>oops! something went wrong</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);h1{font-weight:100;text-align:center;font-size:2.3em}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;margin:0 0 0 1em;text-indent:-1em}
</style>
</head>
<body>
<div class="container">
<h1>oops! something went wrong</h1>
${Object.entries(out).map(([key, value]) => `<pre><strong>${key}</strong>: ${htmlSafe(value)}</pre>`).join('')}
</div>
</body>
</html>`;
}
async function interactionsUrl(ctx, interaction) {
return `/interaction/${interaction.uid}`;
}
async function findAccount(ctx, sub, token) {
// @param ctx - koa request context
// @param sub {string} - account identifier (subject)
// @param token - is a reference to the token used for which a given account is being loaded,
// is undefined in scenarios where claims are returned from authorization endpoint
mustChange('findAccount', 'use your own account model');
return {
accountId: sub,
// @param use {string} - can either be "id_token" or "userinfo", depending on
// where the specific claims are intended to be put in
// @param scope {string} - the intended scope, while oidc-provider will mask
// claims depending on the scope automatically you might want to skip
// loading some claims from external resources or through db projection etc. based on this
// detail or not return them in ID Tokens but only UserInfo and so on
// @param claims {object} - the part of the claims authorization parameter for either
// "id_token" or "userinfo" (depends on the "use" param)
// @param rejected {Array[String]} - claim names that were rejected by the end-user, you might
// want to skip loading some claims from external resources or through db projection
async claims(use, scope, claims, rejected) {
return { sub };
},
};
}
function rotateRefreshToken(ctx) {
const { RefreshToken: refreshToken, Client: client } = ctx.oidc.entities;
// cap the maximum amount of time a refresh token can be
// rotated for up to 1 year, afterwards its TTL is final
if (refreshToken.totalLifetime() >= 365.25 * 24 * 60 * 60) {
return false;
}
// rotate non sender-constrained public client refresh tokens
if (
client.clientAuthMethod === 'none'
&& !refreshToken.isSenderConstrained()
) {
return true;
}
// rotate if the token is nearing expiration (it's beyond 70% of its lifetime)
return refreshToken.ttlPercentagePassed() >= 70;
}
async function loadExistingGrant(ctx) {
const grantId = ctx.oidc.result?.consent?.grantId
|| ctx.oidc.session.grantIdFor(ctx.oidc.client.clientId);
if (grantId) {
return ctx.oidc.provider.Grant.find(grantId);
}
return undefined;
}
function revokeGrantPolicy(ctx) {
if (ctx.oidc.route === 'revocation' && ctx.oidc.entities.AccessToken) {
return false;
}
return true;
}
function sectorIdentifierUriValidate(client) {
// @param client - the Client instance
return true;
}
async function processLoginHintToken(ctx, loginHintToken) {
// @param ctx - koa request context
// @param loginHintToken - string value of the login_hint_token parameter
mustChange('features.ciba.processLoginHintToken', 'process the login_hint_token parameter and return the accountId value to use for processsing the request');
throw new Error('features.ciba.processLoginHintToken not implemented');
}
async function processLoginHint(ctx, loginHint) {
// @param ctx - koa request context
// @param loginHint - string value of the login_hint parameter
mustChange('features.ciba.processLoginHint', 'process the login_hint parameter and return the accountId value to use for processsing the request');
throw new Error('features.ciba.processLoginHint not implemented');
}
async function verifyUserCode(ctx, account, userCode) {
// @param ctx - koa request context
// @param account -
// @param userCode - string value of the user_code parameter, when not provided it is undefined
mustChange('features.ciba.verifyUserCode', 'verify the user_code parameter is present when required and verify its value');
throw new Error('features.ciba.verifyUserCode not implemented');
}
async function validateBindingMessage(ctx, bindingMessage) {
// @param ctx - koa request context
// @param bindingMessage - string value of the binding_message parameter, when not provided it is undefined
shouldChange('features.ciba.validateBindingMessage', 'verify the binding_message parameter is present when required and verify its value');
if (bindingMessage?.match(/^[a-zA-Z0-9-._+/!?#]{1,20}$/) === null) {
throw new errors.InvalidBindingMessage(
'the binding_message value, when provided, needs to be 1 - 20 characters in length and use only a basic set of characters (matching the regex: ^[a-zA-Z0-9-._+/!?#]{1,20}$ )',
);
}
}
async function validateRequestContext(ctx, requestContext) {
// @param ctx - koa request context
// @param requestContext - string value of the request_context parameter, when not provided it is undefined
mustChange('features.ciba.validateRequestContext', 'verify the request_context parameter is present when required and verify its value');
throw new Error('features.ciba.validateRequestContext not implemented');
}
async function triggerAuthenticationDevice(ctx, request, account, client) {
// @param ctx - koa request context
// @param request - the BackchannelAuthenticationRequest instance
// @param account - the account object retrieved by findAccount
// @param client - the Client instance
mustChange('features.ciba.triggerAuthenticationDevice', "to trigger the authentication and authorization process on end-user's Authentication Device");
throw new Error('features.ciba.triggerAuthenticationDevice not implemented');
}
async function assertClaimsParameter(ctx, claims, client) {
// @param ctx - koa request context
// @param claims - parsed claims parameter
// @param client - the Client instance
}
async function assertJwtClientAuthClaimsAndHeader(ctx, claims, header, client) {
// @param ctx - koa request context
// @param claims - parsed JWT Client Authentication Assertion Claims Set as object
// @param header - parsed JWT Client Authentication Assertion Headers as object
// @param client - the Client instance
if (ctx.oidc.isFapi('2.0') && claims.aud !== ctx.oidc.issuer) {
throw new errors.InvalidClientAuth(
'audience (aud) must equal the issuer identifier url',
);
}
}
async function assertJwtClaimsAndHeader(ctx, claims, header, client) {
// @param ctx - koa request context
// @param claims - parsed Request Object JWT Claims Set as object
// @param header - parsed Request Object JWT Headers as object
// @param client - the Client instance
const requiredClaims = [];
const fapiProfile = ctx.oidc.isFapi('1.0 Final', '2.0');
if (fapiProfile) {
requiredClaims.push('exp', 'aud', 'nbf');
}
if (ctx.oidc.route === 'backchannel_authentication') {
requiredClaims.push('exp', 'iat', 'nbf', 'jti');
}
for (const claim of new Set(requiredClaims)) {
if (claims[claim] === undefined) {
throw new errors.InvalidRequestObject(
`Request Object is missing the '${claim}' claim`,
);
}
}
if (fapiProfile) {
const diff = claims.exp - claims.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new errors.InvalidRequestObject(
"Request Object 'exp' claim too far from 'nbf' claim",
);
}
}
}
function makeDefaults() {
const defaults = {
/*
* acrValues
*
* description: An array of strings representing the Authentication Context Class References
* that this authorization server supports.
*/
acrValues: [],
/*
* adapter
*
* description: Specifies the storage adapter implementation for persisting authorization server
* state. The default implementation provides a basic in-memory adapter suitable for development
* and testing purposes only. When this process is restarted, all stored information will be lost.
* Production deployments MUST provide a custom adapter implementation that persists data to
* external storage (e.g., database, Redis, etc.).
*
* The adapter constructor will be instantiated for each model type when first accessed.
*
* see: [The expected interface](/example/my_adapter.js)
* see: [Example MongoDB adapter implementation](https://github.com/panva/node-oidc-provider/discussions/1308)
* see: [Example Redis adapter implementation](https://github.com/panva/node-oidc-provider/discussions/1309)
* see: [Example Redis w/ JSON Adapter](https://github.com/panva/node-oidc-provider/discussions/1310)
* see: [Default in-memory adapter implementation](/lib/adapters/memory_adapter.js)
* see: [Community Contributed Adapter Archive](https://github.com/panva/node-oidc-provider/discussions/1311)
*
* @nodefault
*/
adapter: undefined,
/*
* claims
*
* description: Describes the claims that this authorization server may be able to supply values for.
*
* It is used to achieve two different things related to claims:
* - which additional claims are available to RPs (configure as `{ claimName: null }`)
* - which claims fall under what scope (configure `{ scopeName: ['claim', 'another-claim'] }`)
*
* see: [Configuring OpenID Connect 1.0 Standard Claims](https://github.com/panva/node-oidc-provider/discussions/1299)
*/
claims: {
acr: null,
sid: null,
auth_time: null,
iss: null,
openid: ['sub'],
},
/*
* clientBasedCORS
*
* description: Specifies a function that determines whether Cross-Origin Resource Sharing (CORS)
* requests shall be permitted based on the requesting client. This function
* is invoked for each CORS preflight and actual request to evaluate the client's authorization
* to access the authorization server from the specified origin.
*
* see: [Configuring Client Metadata-based CORS Origin allow list](https://github.com/panva/node-oidc-provider/discussions/1298)
*/
clientBasedCORS,
/*
* clients
*
* description: An array of client metadata objects representing statically configured OAuth 2.0
* and OpenID Connect clients. These clients are persistent, do not expire, and remain available
* throughout the authorization server's lifetime. For dynamic client discovery, the authorization
* server will invoke the adapter's `find` method when encountering unregistered client identifiers.
*
* To restrict the authorization server to only statically configured clients and disable dynamic
* registration, configure the adapter to return falsy values for client lookup operations
* (e.g., `return Promise.resolve()`).
*
* Each client's metadata shall be validated according to the specifications in which the respective
* properties are defined.
*
* example: Available Metadata.
*
* application_type, client_id, client_name, client_secret, client_uri, contacts,
* default_acr_values, default_max_age, grant_types, id_token_signed_response_alg,
* initiate_login_uri, jwks, jwks_uri, logo_uri, policy_uri, post_logout_redirect_uris,
* redirect_uris, require_auth_time, response_types, response_modes, scope, sector_identifier_uri,
* subject_type, token_endpoint_auth_method, tos_uri, userinfo_signed_response_alg
*
* <br/><br/>The following metadata is available but may not be recognized depending on this
* authorization server's configuration.<br/><br/>
*
* authorization_encrypted_response_alg, authorization_encrypted_response_enc,
* authorization_signed_response_alg, backchannel_logout_session_required, backchannel_logout_uri,
* id_token_encrypted_response_alg,
* id_token_encrypted_response_enc, introspection_encrypted_response_alg,
* introspection_encrypted_response_enc, introspection_signed_response_alg,
* request_object_encryption_alg, request_object_encryption_enc, request_object_signing_alg,
* tls_client_auth_san_dns, tls_client_auth_san_email, tls_client_auth_san_ip,
* tls_client_auth_san_uri, tls_client_auth_subject_dn,
* tls_client_certificate_bound_access_tokens,
* use_mtls_endpoint_aliases, token_endpoint_auth_signing_alg,
* userinfo_encrypted_response_alg, userinfo_encrypted_response_enc
*
*/
clients: [],
/*
* clientDefaults
*
* description: Specifies default client metadata values that shall be applied when properties
* are not explicitly provided during Dynamic Client Registration or for statically configured
* clients. This configuration allows override of the authorization server's built-in default
* values for any supported client metadata property.
*
* example: Changing the default client token_endpoint_auth_method.
*
* To change the default client token_endpoint_auth_method, configure `clientDefaults` to be an
* object like so:
*
* ```js
* {
* token_endpoint_auth_method: 'client_secret_post'
* }
* ```
* example: Changing the default client response type to `code id_token`.
*
* To change the default client response_types, configure `clientDefaults` to be an
* object like so:
*
* ```js
* {
* response_types: ['code id_token'],
* grant_types: ['authorization_code', 'implicit'],
* }
* ```
*
*/
clientDefaults: {
grant_types: ['authorization_code'],
id_token_signed_response_alg: 'RS256',
response_types: ['code'],
token_endpoint_auth_method: 'client_secret_basic',
},
/*
* clockTolerance
*
* description: Specifies the maximum acceptable clock skew tolerance (in seconds) for validating
* time-sensitive operations, including JWT validation for Request Objects, DPoP Proofs, and
* other timestamp-based security mechanisms.
*
* recommendation: This value should be kept as small as possible while accommodating expected
* clock drift between the authorization server and client systems.
*/
clockTolerance: 15,
/*
* conformIdTokenClaims
*
* title: ID Token only contains End-User claims when the requested `response_type` is `id_token`
*
* description: [`OIDC Core 1.0` - Requesting Claims using Scope Values](https://openid.net/specs/openid-connect-core-1_0-errata2.html#ScopeClaims)
* defines that claims requested using the `scope` parameter are only returned from the UserInfo
* Endpoint unless the `response_type` is `id_token`.
*
* Despite this configuration, the ID Token always includes claims requested using the `scope`
* parameter when the userinfo endpoint is disabled, or when issuing an Access Token not applicable
* for access to the userinfo endpoint.
*
*/
conformIdTokenClaims: true,
/*
* loadExistingGrant
*
* description: Helper function invoked to load existing authorization grants that may be used
* to resolve an Authorization Request without requiring additional end-user interaction.
* The default implementation attempts to load grants based on the interaction result's
* `consent.grantId` property, falling back to the existing grantId for the requesting client
* in the current session.
*/
loadExistingGrant,
/*
* allowOmittingSingleRegisteredRedirectUri
*
* title: Redirect URI Parameter Omission for Single Registered URI
*
* description: Specifies whether clients may omit the `redirect_uri` parameter in authorization
* requests when only a single redirect URI is registered in their client metadata. When enabled,
* the authorization server shall automatically use the sole registered redirect URI for clients
* that have exactly one URI configured.
*
* When disabled, all authorization requests MUST explicitly include the `redirect_uri` parameter
* regardless of the number of registered redirect URIs.
*/
allowOmittingSingleRegisteredRedirectUri: true,
/*
* acceptQueryParamAccessTokens
*
* description: Controls whether access tokens may be transmitted via URI query parameters.
* Several OAuth 2.0 and OpenID Connect profiles require that access tokens be transmitted
* exclusively via the Authorization header. When set to false, the authorization server
* shall reject requests attempting to transmit access tokens via query parameters.
*
*/
acceptQueryParamAccessTokens: false,
/*
* cookies
*
* description: Configuration for HTTP cookies used to maintain User-Agent state throughout
* the authorization flow. These settings conform to the
* [cookies module interface specification](https://github.com/pillarjs/cookies/tree/0.9.1?tab=readme-ov-file#cookiessetname--values--options).
* The `maxAge` and `expires` properties are ignored; cookie lifetimes are instead controlled
* via the `ttl.Session` and `ttl.Interaction` configuration parameters.
* @nodefault
*/
cookies: {
/*
* cookies.names
*
* description: Specifies the HTTP cookie names used for state management during the
* authorization flow.
*/
names: {
session: '_session', // used for main session reference
interaction: '_interaction', // used by the interactions for interaction session reference
resume: '_interaction_resume', // used when interactions resume authorization for interaction session reference
},
/*
* cookies.long
*
* description: Options for long-term cookies.
*/
long: {
httpOnly: true, // cookies are not readable by client-side JavaScript
sameSite: 'lax',
},
/*
* cookies.short
*
* description: Options for short-term cookies.
*/
short: {
httpOnly: true, // cookies are not readable by client-side JavaScript
sameSite: 'lax',
},
/*
* cookies.keys
*
* description: [Keygrip](https://www.npmjs.com/package/keygrip) signing keys used for cookie
* signing to prevent tampering. You may also pass your own KeyGrip instance.
*
* recommendation: Rotate regularly (by prepending new keys) with a reasonable interval and keep
* a reasonable history of keys to allow for returning user session cookies to still be valid
* and re-signed.
*
* @skip
*/
keys: [],
},
/*
* discovery
*
* description: Pass additional properties to this object to extend the discovery document.
*/
discovery: {
claim_types_supported: ['normal'],
claims_locales_supported: undefined,
display_values_supported: undefined,
op_policy_uri: undefined,
op_tos_uri: undefined,
service_documentation: undefined,
ui_locales_supported: undefined,
},
/*
* extraParams
*
* description: Specifies additional parameters that shall be recognized by the authorization,
* device authorization, backchannel authentication, and pushed authorization request endpoints.
* These extended parameters become available in `ctx.oidc.params` and are passed to interaction
* session details for processing.
*
* This configuration accepts either an iterable object (array or Set of strings) for simple
* parameter registration, or a plain object with string properties representing parameter names
* and values being validation functions (synchronous or asynchronous) for the corresponding
* parameter values.
*
* Parameter validators are executed regardless of the parameter's presence or value, enabling
* validation of parameter presence as well as assignment of default values. When the value
* is `null` or `undefined`, the parameter is registered without validation constraints.
*
* Note: These validators execute during the final phase of the request validation process.
* Modifications to other parameters (such as assigning default values) will not trigger
* re-validation of the entire request.
*
* example: Registering an extra `origin` parameter with its validator.
*
* ```js
* import { errors } from 'oidc-provider';
*
* const extraParams = {
* async origin(ctx, value, client) {
* // @param ctx - koa request context
* // @param value - the `origin` parameter value (string or undefined)
* // @param client - client making the request
*
* if (hasDefaultOrigin(client)) {
* // assign default
* ctx.oidc.params.origin ||= value ||= getDefaultOrigin(client);
* }
*
* if (!value && requiresOrigin(ctx, client)) {
* // reject when missing but required
* throw new errors.InvalidRequest('"origin" is required for this request')
* }
*
* if (!allowedOrigin(value, client)) {
* // reject when not allowed
* throw new errors.InvalidRequest('requested "origin" is not allowed for this client')
* }
* }
* }
* ```
*/
extraParams: [],
/*
* features
*
* description: Specifies the authorization server feature capabilities that shall be enabled
* or disabled. This configuration controls the availability of optional OAuth 2.0 and
* OpenID Connect extensions, experimental specifications, and proprietary enhancements.
*
* Certain features may be designated as experimental implementations. When experimental
* features are enabled, the authorization server will emit warnings to indicate that
* breaking changes may occur in future releases. These changes will be published as
* minor version updates of the oidc-provider module.
*
* To suppress experimental feature warnings and ensure configuration validation against
* breaking changes, implementations shall acknowledge the specific experimental feature
* version using the acknowledgment mechanism demonstrated in the example below. When
* an unacknowledged breaking change is detected, the authorization server configuration
* will throw an error during instantiation.
*
* example: Acknowledging an experimental feature.
*
* ```js
* import * as oidc from 'oidc-provider'
*
* new oidc.Provider('http://localhost:3000', {
* features: {
* webMessageResponseMode: {
* enabled: true,
* },
* },
* });
*
* // The above code produces this NOTICE
* // NOTICE: The following experimental features are enabled and their implemented version not acknowledged
* // NOTICE: - OAuth 2.0 Web Message Response Mode - draft 01 (Acknowledging this feature's implemented version can be done with the value 'individual-draft-01')
* // NOTICE: Breaking changes between experimental feature updates may occur and these will be published as MINOR semver oidc-provider updates.
* // NOTICE: You may disable this notice and be warned when breaking updates occur by acknowledging the current experiment's version. See the documentation for more details.
*
* new oidc.Provider('http://localhost:3000', {
* features: {
* webMessageResponseMode: {
* enabled: true,
* ack: 'individual-draft-01',
* },
* },
* });
* // No more NOTICE, at this point if the experimental was updated and contained no breaking
* // changes, you're good to go, still no NOTICE, your code is safe to run.
*
* // Now lets assume you upgrade oidc-provider version and it includes a breaking change in
* // this experimental feature
* new oidc.Provider('http://localhost:3000', {
* features: {
* webMessageResponseMode: {
* enabled: true,
* ack: 'individual-draft-01',
* },
* },
* });
* // Thrown:
* // Error: An unacknowledged version of an experimental feature is included in this oidc-provider version.
* ```
* @nodefault
*/
features: {
/*
* features.devInteractions
*
* description: Enables development-only interaction views that provide pre-built user
* interface components for rapid prototyping and testing of authorization flows. These
* views accept any username (used as the subject claim value) and any password for
* authentication, bypassing production-grade security controls.
*
* Production deployments MUST disable this feature and implement proper end-user
* authentication and authorization mechanisms. These development views MUST NOT
* be used in production environments as they provide no security guarantees and
* accept arbitrary credentials.
*/
devInteractions: { enabled: true },
/*
* features.dPoP
*
* title: [`RFC9449`](https://www.rfc-editor.org/rfc/rfc9449.html) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (`DPoP`)
*
* description: Enables sender-constraining of OAuth 2.0 tokens through application-level
* proof-of-possession mechanisms.
*/
dPoP: {
enabled: true,
/**
* features.dPoP.nonceSecret
*
* description: Specifies the cryptographic secret value used for generating server-provided
* DPoP nonces. When provided, this value MUST be a 32-byte length
* Buffer instance to ensure sufficient entropy for secure nonce generation.
*/
nonceSecret: undefined,
/**
* features.dPoP.requireNonce
*
* description: Specifies a function that determines whether a DPoP nonce shall be required
* for proof-of-possession validation in the current request context. This function is
* invoked during DPoP proof validation to enforce nonce requirements based on
* authorization server policy.
*/
requireNonce,
/**
* features.dPoP.allowReplay
*
* description: Specifies whether DPoP Proof replay shall be permitted by the
* authorization server. When set to false, the server enforces strict replay protection
* by rejecting previously used DPoP proofs, enhancing security against replay attacks.
*/
allowReplay: false,
},
/*
* features.backchannelLogout
*
* title: [`OIDC Back-Channel Logout 1.0`](https://openid.net/specs/openid-connect-backchannel-1_0-final.html)
*
* description: Specifies whether Back-Channel Logout capabilities shall be enabled. When
* enabled, the authorization server shall support propagating end-user logouts initiated
* by relying parties to clients that were involved throughout the lifetime of the
* terminated session.
*/
backchannelLogout: { enabled: false },
/*
* features.ciba
*
* title: [OIDC Client Initiated Backchannel Authentication Flow (`CIBA`)](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-final.html)
*
* description: Enables Core `CIBA` Flow, when combined with `features.fapi` and
* `features.requestObjects.enabled` enables
* [Financial-grade API: Client Initiated Backchannel Authentication Profile - Implementers Draft 01](https://openid.net/specs/openid-financial-api-ciba-ID1.html)
* as well.
*
*/
ciba: {
enabled: false,
/*
* features.ciba.deliveryModes
*
* description: Specifies the token delivery modes supported by this authorization server.
* The following delivery modes are defined:
* - `poll` - Client polls the token endpoint for completion
* - `ping` - Authorization server notifies client of completion via HTTP callback
*
*/
deliveryModes: ['poll'],
/*
* features.ciba.triggerAuthenticationDevice
*
* description: Specifies a helper function that shall be invoked to initiate authentication
* and authorization processes on the end-user's Authentication Device as defined in the
* CIBA specification. This function is executed after accepting the backchannel
* authentication request but before transmitting the response to the requesting client.
*
* Upon successful end-user authentication, implementations shall use `provider.backchannelResult()`
* to complete the Consumption Device login process.
*
* example: `provider.backchannelResult()` method.
*
* `backchannelResult` is a method on the Provi