UNPKG

oidc-provider

Version:

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

1,132 lines (1,030 loc) 149 kB
/* 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