@strongnguyen/oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
1,130 lines (1,028 loc) • 115 kB
JavaScript
/* 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) {