oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
1,154 lines (1,048 loc) • 117 kB
JavaScript
/* eslint-disable no-shadow */
/* eslint-disable no-unused-vars */
import * as crypto from 'node:crypto';
import * as os from 'node:os';
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) {
mustChange('clientBasedCORS', 'control CORS allowed Origins based on the client making a CORS request');
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 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) {
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;
}
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, the default one uses `os.hostname()` as salt and is therefore not fit for anything else than development');
return crypto
.createHash('sha256')
.update(client.sectorIdentifier)
.update(accountId)
.update(os.hostname()) // put your own unique salt here, or implement other mechanism
.digest('hex');
}
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: Array of strings, the Authentication Context Class References that the authorization server supports.
*/
acrValues: [],
/*
* adapter
*
* description: The provided example and any new instance of oidc-provider will use the basic
* in-memory adapter for storing issued tokens, codes, user sessions, dynamically registered
* clients, etc. This is fine as long as you develop, configure and generally just play around
* since every time you restart your process all information will be lost. As soon as you cannot
* live with this limitation you will be required to provide your own custom adapter constructor
* for oidc-provider to use. This constructor will be called for every model accessed the first
* time it is needed.
*
* see: [The interface oidc-provider expects](/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 the OpenID Provider 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: Function used to check whether a given CORS request should be allowed
* based on the request's client.
*
* see: [Configuring Client Metadata-based CORS Origin allow list](https://github.com/panva/node-oidc-provider/discussions/1298)
*/
clientBasedCORS,
/*
* clients
*
* description: Array of objects representing client metadata. These clients are referred to as
* static, they don't expire, never reload, are always available. In addition to these
* clients the authorization server will use your adapter's `find` method when a non-static client_id is
* encountered. If you only wish to support statically configured clients and
* no dynamic registration then make it so that your adapter resolves client find calls with a
* falsy value (e.g. `return Promise.resolve()`) and don't take unnecessary DB trips.
*
* Client's metadata is validated as defined by the respective specification they've been defined
* in.
*
* 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 your
* provider'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: Default client metadata to be assigned when unspecified by the client metadata,
* e.g. during Dynamic Client Registration or for statically configured clients. The default value
* does not represent all default values, but merely copies its subset. You can provide any used
* client metadata property in this object.
*
* 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: A `Number` value (in seconds) describing the allowed system clock skew for
* validating client-provided JWTs, e.g. Request Objects, DPoP Proofs and otherwise comparing
* timestamps
* recommendation: Only set this to a reasonable value when needed to cover server-side client and
* oidc-provider server clock skew.
*/
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 of 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 used to load existing but also just in time pre-established Grants
* to attempt to resolve an Authorization Request with. Default: loads a grant based on the
* interaction result `consent.grantId` first, falls back to the existing grantId for the client
* in the current session.
*/
loadExistingGrant,
/*
* allowOmittingSingleRegisteredRedirectUri
*
* title: Allow omitting the redirect_uri parameter when only a single one is registered for a client.
*/
allowOmittingSingleRegisteredRedirectUri: true,
/*
* acceptQueryParamAccessTokens
*
* description: Several OAuth 2.0 / OIDC profiles prohibit the use of query strings to carry
* access tokens. This setting either allows (true) or prohibits (false) that mechanism to be
* used.
*
*/
acceptQueryParamAccessTokens: false,
/*
* cookies
*
* description: Options for the [cookies module](https://github.com/pillarjs/cookies#cookiesset-name--value---options--)
* used to keep track of various User-Agent states. The options `maxAge` and `expires` are ignored. Use `ttl.Session`
* and `ttl.Interaction` to configure the ttl and in turn the cookie expiration values for Session and Interaction
* models.
* @nodefault
*/
cookies: {
/*
* cookies.names
*
* description: Cookie names used to store and transfer various states.
*/
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: Pass an iterable object (i.e. array or Set of strings) to extend the parameters
* recognised by the authorization, device authorization, backchannel authentication, and
* pushed authorization request endpoints. These parameters are then available in `ctx.oidc.params`
* as well as passed to interaction session details.
*
*
* This may also be a plain object with string properties representing parameter names and values being
* either a function or async function to validate said parameter value. These validators are executed
* regardless of the parameters' presence or value such that this can be used to validate presence of
* custom parameters as well as to assign default values for them. If the value is `null` or
* `undefined` the parameter is added without a validator. Note that these validators execute near the very end
* of the request's validation process and changes to (such as assigning default values) other parameters
* will not trigger any re-validation of the whole request again.
*
* 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: Enable/disable features.
*
* Some features may be experimental.
* Enabling those will produce a warning and you must
* be aware that breaking changes may occur and that those changes
* will be published as minor versions of oidc-provider. See the example below on how to
* acknowledge an experimental feature version (this will remove the warning) and ensure
* the Provider configuration will throw an error if a new version of oidc-provider includes
* breaking changes to this experimental feature.
*
* 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: Development-ONLY out of the box interaction views bundled with the library allow
* you to skip the boring frontend part while experimenting with oidc-provider. Enter any
* username (will be used as sub claim value) and any password to proceed.
*
* Be sure to disable and replace this feature with your actual frontend flows and End-User
* authentication flows as soon as possible. These views are not meant to ever be seen by actual
* users.
*/
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 `DPoP` - mechanism for sender-constraining tokens via a
* proof-of-possession mechanism on the application level.
*/
dPoP: {
enabled: true,
/**
* features.dPoP.nonceSecret
*
* description: A secret value used for generating server-provided DPoP nonces.
* Must be a 32-byte length Buffer instance when provided.
*/
nonceSecret: undefined,
/**
* features.dPoP.requireNonce
*
* description: Function used to determine whether a DPoP nonce is required or not.
*/
requireNonce,
/**
* features.dPoP.allowReplay
*
* description: Controls whether DPoP Proof Replay is allowed or not.
*/
allowReplay: false,
},
/*
* features.backchannelLogout
*
* title: [`OIDC Back-Channel Logout 1.0`](https://openid.net/specs/openid-connect-backchannel-1_0-final.html)
*
* description: Enables Back-Channel Logout features.
*/
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 - Implementer's Draft 01](https://openid.net/specs/openid-financial-api-ciba-ID1.html) as well.
*
*/
ciba: {
enabled: false,
/*
* features.ciba.deliveryModes
*
* description: Fine-tune the supported token delivery modes. Supported values are
* - `poll`
* - `ping`
*
*/
deliveryModes: ['poll'],
/*
* features.ciba.triggerAuthenticationDevice
*
* description: Helper function used to trigger the authentication and authorization on end-user's Authentication Device. It is called after
* accepting the backchannel authentication request but before sending client back the response.
*
* When the end-user authenticates use `provider.backchannelResult()` to finish the Consumption Device login process.
*
* example: `provider.backchannelResult()` method
*
* `backchannelResult` is a method on the Provider prototype, it returns a `Promise` with no fulfillment value.
*
* ```js
* import * as oidc from 'oidc-provider';
* const provider = new oidc.Provider(...);
* await provider.backchannelResult(...);
* ```
*
* `backchannelResult(request, result[, options]);`
* - `request` BackchannelAuthenticationRequest - BackchannelAuthenticationRequest instance.
* - `result` Grant | OIDCProviderError - instance of a persisted Grant model or an OIDCProviderError (all exported by errors).
* - `options.acr?`: string - Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied.
* - `options.amr?`: string[] - Identifiers for authentication methods used in the authentication.
* - `options.authTime?`: number - Time when the End-User authentication occurred.
*
*/
triggerAuthenticationDevice,
/*
* features.ciba.validateBindingMessage
*
* description: Helper function used to process the binding_message parameter and throw if its not following the authorization server's policy.
*
* recommendation: Use `throw new errors.InvalidBindingMessage('validation error message')` when the binding_message is invalid.
* recommendation: Use `return undefined` when a binding_message isn't required and wasn't provided.
*
*/
validateBindingMessage,
/*
* features.ciba.validateRequestContext
*
* description: Helper function used to process the request_context parameter and throw if its not following the authorization server's policy.
*
* recommendation: Use `throw new errors.InvalidRequest('validation error message')` when the request_context is required by policy and missing or
* invalid.
* recommendation: Use `return undefined` when a request_context isn't required and wasn't provided.
*
*/
validateRequestContext,
/*
* features.ciba.processLoginHintToken
*
* description: Helper function used to process the login_hint_token parameter and return the accountId value to use for processsing the request.
*
* recommendation: Use `throw new errors.ExpiredLoginHintToken('validation error message')` when login_hint_token is expired.
* recommendation: Use `throw new errors.InvalidRequest('validation error message')` when login_hint_token is invalid.
* recommendation: Use `return undefined` or when you can't determine the accountId from the login_hint.
*
*/
processLoginHintToken,
/*
* features.ciba.processLoginHint
*
* description: Helper function used to process the login_hint parameter and return the accountId value to use for processsing the request.
*
* recommendation: Use `throw new errors.InvalidRequest('validation error message')` when login_hint is invalid.
* recommendation: Use `return undefined` or when you can't determine the accountId from the login_hint.
*
*/
processLoginHint,
/*
* features.ciba.verifyUserCode
*
* description: Helper function used to verify the user_code parameter value is present when required and verify its value.
*
* recommendation: Use `throw new errors.MissingUserCode('validation error message')` when user_code should have been provided but wasn't.
* recommendation: Use `throw new errors.InvalidUserCode('validation error message')` when the provided user_code is invalid.
* recommendation: Use `return undefined` when no user_code was provided and isn't required.
*
*/
verifyUserCode,
},
/*
* features.mTLS
*
* title: [`RFC8705`](https://www.rfc-editor.org/rfc/rfc8705.html) - OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens (`MTLS`)
*
* description: Enables specific features from the Mutual TLS specification. The three main
* features have their own specific setting in this feature's configuration object and
* you must provide functions for resolving some of the functions which are deployment-specific.
*
*/
mTLS: {
enabled: false,
/*
* features.mTLS.certificateBoundAccessTokens
*
* description: Enables section 3 & 4 Mutual TLS Client Certificate-Bound Tokens by exposing
* the client's `tls_client_certificate_bound_access_tokens` metadata property.
*/
certificateBoundAccessTokens: false,
/*
* features.mTLS.selfSignedTlsClientAuth
*
* description: Enables section 2.2. Self-Signed Certificate Mutual TLS client authentication
* method `self_signed_tls_client_auth` for use in the server's `clientAuthMethods`
* configuration.
*/
selfSignedTlsClientAuth: false,