UNPKG

@strongnguyen/oidc-provider

Version:

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

1,130 lines (1,028 loc) 115 kB
/* eslint-disable no-shadow */ /* eslint-disable no-unused-vars */ /* eslint-disable max-len */ const crypto = require('crypto'); const util = require('util'); const os = require('os'); const MemoryAdapter = require('../adapters/memory_adapter'); const { DEV_KEYSTORE } = require('../consts'); const base64url = require('./base64url'); const attention = require('./attention'); const nanoid = require('./nanoid'); const { base: defaultPolicy } = require('./interaction_policy'); const htmlSafe = require('./html_safe'); const errors = require('./errors'); const randomFill = util.promisify(crypto.randomFill); 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 approvalScopeValidate(ctx, accessToken, deviceCode) { mustChange('features.deviceFlow.approvalScopeValidate', 'Xác thực scope của access token'); throw new Error('features.deviceFlow.approvalScopeValidate function not configured'); } 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> <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>`; } 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'); const { clientId, clientName, clientUri, logoUri, policyUri, tosUri, } = ctx.oidc.client; ctx.body = `<!DOCTYPE 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>${clientName || 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'); const { clientId, clientName, clientUri, initiateLoginUri, logoUri, policyUri, tosUri, } = ctx.oidc.client; ctx.body = `<!DOCTYPE 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 ${clientName ? `with ${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.introspectionEndpointAuthMethod === 'none' && token.clientId !== ctx.oidc.client.clientId) { return false; } return true; } function idFactory(ctx) { return nanoid(); } async function secretFactory(ctx) { const bytes = Buffer.allocUnsafe(64); await randomFill(bytes); return base64url.encodeBuffer(bytes); } async function defaultResource(ctx, client, oneOf) { // @param ctx - koa request context // @param client - client making the request // @param oneOf {string[]} - The OP 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; } function httpOptions(url) { return { timeout: 2500, agent: undefined, // defaults to node's global agents (https.globalAgent or http.globalAgent) lookup: undefined, // defaults to CacheableLookup (https://github.com/szmarczak/cacheable-lookup) }; } async function expiresWithSession(ctx, token) { return !token.scopes.has('offline_access'); } async function issueRefreshToken(ctx, client, code) { return client.grantTypeAllowed('refresh_token') && code.scopes.has('offline_access'); } function pkceRequired(ctx, client) { return true; } 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 AccessTokenFormat(ctx, token) { if (token.resourceServer) { return token.resourceServer.accessTokenFormat || 'opaque'; } return 'opaque'; } function ClientCredentialsFormat(ctx, token) { if (token.resourceServer) { return token.resourceServer.accessTokenFormat || 'opaque'; } return 'opaque'; } function AccessTokenTTL(ctx, token, client) { shouldChange('ttl.AccessToken', 'define the expiration for AccessToken artifacts'); if (token.resourceServer) { return token.resourceServer.accessTokenTTL || 60 * 60; // 1 hour in seconds } return 60 * 60; // 1 hour in seconds } function AuthorizationCodeTTL(ctx, code, client) { return 10 * 60; // 10 minutes in seconds } function ClientCredentialsTTL(ctx, token, client) { shouldChange('ttl.ClientCredentials', 'define the expiration for ClientCredentials artifacts'); if (token.resourceServer) { return token.resourceServer.accessTokenTTL || 10 * 60; // 10 minutes in seconds } return 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 && ctx.oidc && 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 && ctx.oidc.entities.RotatedRefreshToken && client.applicationType === 'web' && client.tokenEndpointAuthMethod === '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 // validations for key, value, other related metadata // throw new Provider.errors.InvalidClientMetadata() to reject the client metadata // metadata[key] = value; to (re)assign metadata values // return not necessary, metadata is already a reference } async function postLogoutSuccessSource(ctx) { // @param ctx - koa request context shouldChange('features.rpInitiatedLogout.postLogoutSuccessSource', 'customize the look of the default post logout success page'); const { clientId, clientName, clientUri, initiateLoginUri, logoUri, policyUri, tosUri, } = ctx.oidc.client || {}; // client is defined if the user chose to stay logged in with the OP const display = clientName || clientId; ctx.body = `<!DOCTYPE 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> <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> <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.tokenEndpointAuthMethod === '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 && ctx.oidc.result.consent && 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) { 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 && !/^[a-zA-Z0-9-._+/!?#]{1,20}$/.exec(bindingMessage)) { 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'); } function getDefaults() { const defaults = { /* * acrValues * * description: Array of strings, the Authentication Context Class References that the OP 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. * The API oidc-provider expects is documented [here](/example/my_adapter.js). * * example: MongoDB adapter implementation * * See [/example/adapters/mongodb.js](/example/adapters/mongodb.js) * * example: Redis adapter implementation * * See [/example/adapters/redis.js](/example/adapters/redis.js) * * example: Redis w/ ReJSON adapter implementation * * See [/example/adapters/redis_rejson.js](/example/adapters/redis_rejson.js) * * example: Default in-memory adapter implementation * * See [/lib/adapters/memory_adapter.js](/lib/adapters/memory_adapter.js) * * @nodefault */ adapter: MemoryAdapter, /* * 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'] }`) * * example: OpenID Connect 1.0 Standard Claims * * See [/recipes/claim_configuration.md](/recipes/claim_configuration.md) * */ 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. * * example: Client Metadata-based CORS Origin allow list * * See [/recipes/client_based_origins.md](/recipes/client_based_origins.md) */ 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 provider 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, 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_endpoint_auth_method, * introspection_endpoint_auth_signing_alg, introspection_signed_response_alg, * request_object_encryption_alg, request_object_encryption_enc, request_object_signing_alg, * request_uris, revocation_endpoint_auth_method, revocation_endpoint_auth_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, token_endpoint_auth_signing_alg, * userinfo_encrypted_response_alg, userinfo_encrypted_response_enc, web_message_uris * */ 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: 0, /* * conformIdTokenClaims * * title: ID Token only contains End-User claims when the requested `response_type` is `id_token` * * description: [Core 1.0 - Requesting Claims using Scope Values](https://openid.net/specs/openid-connect-core-1_0.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: false, /* * 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: true, /* * cookies * * description: Options for the [cookie 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 sessionAccountId: '_SID', // used for account id reference, should not conflict with any other cookie names }, /* * cookies.long * * description: Options for long-term cookies * recommendation: set cookies.keys and cookies.long.signed = true */ long: { httpOnly: true, // cookies are not readable by client-side javascript overwrite: true, sameSite: 'none', }, /* * cookies.short * * description: Options for short-term cookies * recommendation: set cookies.keys and cookies.short.signed = true */ short: { httpOnly: true, // cookies are not readable by client-side javascript overwrite: true, sameSite: 'lax', }, share: { httpOnly: false, overwrite: true, sameSite: '', secure: false, domain: 'localhost', }, /* * cookies.keys * * description: [Keygrip](https://www.npmjs.com/package/keygrip) Signing keys used for cookie * signing to prevent tampering. * 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 */ 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, and pushed authorization request * endpoints. These parameters are then available in `ctx.oidc.params` as well as passed to * interaction session details. */ extraParams: [], /* * features * description: Enable/disable features. Some features are still either based on draft or * experimental RFCs. Enabling those will produce a warning in your console and you must * be aware that breaking changes may occur between draft implementations and that those * will be published as minor versions of oidc-provider. See the example below on how to * acknowledge the specification is a draft (this will remove the warning log) and ensure * the provider instance will fail to instantiate if a new version of oidc-provider bundles * newer version of the RFC with breaking changes in it. * * example: Acknowledging a draft / experimental feature * * ```js * new Provider('http://localhost:3000', { * features: { * backchannelLogout: { * enabled: true, * }, * }, * }); * * // The above code produces this NOTICE * // NOTICE: The following draft features are enabled and their implemented version not acknowledged * // NOTICE: - OpenID Connect Back-Channel Logout 1.0 - draft 06 (OIDF AB/Connect Working Group draft. URL: https://openid.net/specs/openid-connect-backchannel-1_0-06.html) * // NOTICE: Breaking changes between draft version updates may occur and these will be published as MINOR semver oidc-provider updates. * // NOTICE: You may disable this notice and these potentially breaking updates by acknowledging the current draft version. See https://github.com/panva/node-oidc-provider/tree/v7.3.0/docs/README.md#features * * new Provider('http://localhost:3000', { * features: { * backchannelLogout: { * enabled: true, * ack: 'draft-06', // < we're acknowledging draft 06 of the RFC * }, * }, * }); * // No more NOTICE, at this point if the draft implementation changed to 07 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 bundles draft 08 and it contains breaking * // changes * new Provider('http://localhost:3000', { * features: { * backchannelLogout: { * enabled: true, * ack: 'draft-06', // < bundled is draft-08, but we're still acknowledging draft-06 * }, * }, * }); * // Thrown: * // Error: An unacknowledged version of a draft 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: [draft-ietf-oauth-dpop-03](https://tools.ietf.org/html/draft-ietf-oauth-dpop-03) - 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. Browser DPoP Proof generation * [here](https://www.npmjs.com/package/dpop). * * recommendation: Updates to draft specification versions are released as MINOR library versions, * if you utilize these specification implementations consider using the tilde `~` operator * in your package.json since breaking changes may be introduced as part of these version * updates. Alternatively, [acknowledge](#features) the version and be notified of breaking * changes as part of your CI. */ dPoP: { enabled: false, iatTolerance: 60, ack: undefined }, /* * features.backchannelLogout * * title: [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: [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-final.html) * * description: Enables Core CIBA Flow, when combined with `features.fapi` 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 * const provider = new 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 Provider.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 Provider.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 Provider.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 Provider.errors.ExpiredLoginHintToken('validation error message')` when login_hint_token is expired. * recommendation: Use `throw Provider.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 Provider.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 Provider.errors.MissingUserCode('validation error message')` when user_code should have been provided but wasn't. * recommendation: Use `throw Provider.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 `tokenEndpointAuthMethods` * configuration. */ selfSignedTlsClientAuth: false, /* * features.mTLS.tlsClientAuth * * description: Enables section 2.1. PKI Mutual TLS client authentication method * `tls_client_auth` for use in the server's `tokenEndpointAuthMethods` * configuration. */ tlsClientAuth: false, /* * features.mTLS.getCertificate * * description: Function used to retrieve the PEM-formatted client certificate used * in the request. * * example: When behind a TLS terminating proxy (nginx/apache) * * When behind a TLS terminating proxy it is common that the certificate be passed * to the application as a sanitized header. This returns the chosen header value provided * by nginx's `$ssl_client_cert` or apache's `%{SSL_CLIENT_CERT}s` * * ```js * function getCertificate(ctx) { * return ctx.get('x-ssl-client-cert'); * } * ``` * * example: When using node's `https.createServer` * * ```js * function getCertificate(ctx) { * const peerCertificate = ctx.socket.getPeerCertificate(); * if (peerCertificate.raw) { * return `-----BEGIN CERTIFICATE-----\n${peerCertificate.raw.toString('base64')}\n-----END CERTIFICATE-----`; * } * } * ``` * * @nodefault */ getCertificate, /* * features.mTLS.certificateAuthorized * * description: Function used to determine if the client certificate, used in the * request, is verified and comes from a trusted CA for the client. Should return true/false. * Only used for `tls_client_auth` client authentication method. * * example: When behind a TLS terminating proxy (nginx/apache) * * When behind a TLS terminating proxy it is common that this detail be passed * to the application as a sanitized header. This returns the chosen header value provided * by nginx's `$ssl_client_verify` or apache's `%{SSL_CLIENT_VERIFY}s` * * ```js * function certificateAuthorized(ctx) { * return ctx.get('x-ssl-client-verify') === 'SUCCESS'; * } * ``` * * example: When using node's `https.createServer` * * ```js * function certificateAuthorized(ctx) {