oidc-provider allows to be extended and configured in various ways to fit a variety of use cases. You will have to configure your instance with how to find your user accounts, where to store and retrieve persisted data from and where your end-user interactions happen. The example application is a good starting point to get an idea of what you should provide.
⚠️ ⚠️ This page now describes oidc-provider version v7.x documentation. See here for v6.x.
If you or your business use oidc-provider, or you need help using/upgrading the module, please consider becoming a sponsor so I can continue maintaining it and adding new features carefree. The only way to guarantee you get feedback from the author & sole maintainer of this module is to support the package through GitHub Sponsors. I make it a best effort to try and answer newcomers regardless of being a supporter or not, but if you're asking your n-th question and don't get an answer it's because I'm out of handouts and spare time to give.
Table of Contents
- Basic configuration example
- Accounts
- User flows
- Custom Grant Types ❗
- Registering module middlewares (helmet, ip-filters, rate-limiters, etc)
- Pre- and post-middlewares ❗
- Mounting oidc-provider
- Trusting TLS offloading proxies ❗
- Configuration options ❗
- FAQ ❗
const { Provider } = require('oidc-provider');
const configuration = {
// ... see the available options in Configuration options section
clients: [{
client_id: 'foo',
client_secret: 'bar',
redirect_uris: ['http://lvh.me:8080/cb'],
// + other client properties
}],
// ...
};
const oidc = new Provider('http://localhost:3000', configuration);
// express/nodejs style application callback (req, res, next) for use with express apps, see /examples/express.js
oidc.callback()
// koa application for use with koa apps, see /examples/koa.js
oidc.app
// or just expose a server standalone, see /examples/standalone.js
const server = oidc.listen(3000, () => {
console.log('oidc-provider listening on port 3000, check http://localhost:3000/.well-known/openid-configuration');
});
oidc-provider needs to be able to find an account and once found the account needs to have an
accountId
property as well as claims()
function returning an object with claims that correspond
to the claims your issuer supports. Tell oidc-provider how to find your account by an ID.
#claims()
can also return a Promise later resolved / rejected.
const oidc = new Provider('http://localhost:3000', {
async findAccount(ctx, id) {
return {
accountId: id,
async claims(use, scope) { return { sub: id }; },
};
}
});
Since oidc-provider only comes with feature-less views and interaction handlers it is up to you to fill those in, here is how oidc-provider allows you to do so:
When oidc-provider cannot fulfill the authorization request for any of the possible reasons (missing
user session, requested ACR not fulfilled, prompt requested, ...) it will resolve the
interactions.url
helper function and redirect the User-Agent to that url. Before
doing so it will save a short-lived "interaction session" and dump its identifier into a cookie scoped to the
resolved interaction path.
This interaction session contains:
- details of the interaction that is required
- all authorization request parameters
- current end-user session account ID should there be one
- the url to redirect the user to once interaction is finished
oidc-provider expects that you resolve the prompt interaction and then redirect the User-Agent back with the results.
Once the required interactions are finished you are expected to redirect back to the authorization endpoint, affixed by the uid of the interaction session and the interaction results stored in the interaction session object.
The Provider instance comes with helpers that aid with getting interaction details as well as packing the results. See them used in the in-repo examples.
#provider.interactionDetails(req, res)
// with express
expressApp.get('/interaction/:uid', async (req, res) => {
const details = await provider.interactionDetails(req, res);
// ...
});
// with koa
router.get('/interaction/:uid', async (ctx, next) => {
const details = await provider.interactionDetails(ctx.req, ctx.res);
// ...
});
#provider.interactionFinished(req, res, result)
// with express
expressApp.post('/interaction/:uid/login', async (req, res) => {
return provider.interactionFinished(req, res, result); // result object below
});
// with koa
router.post('/interaction/:uid', async (ctx, next) => {
return provider.interactionFinished(ctx.req, ctx.res, result); // result object below
});
// result should be an object with some or all the following properties
{
// authentication/login prompt got resolved, omit if no authentication happened, i.e. the user
// cancelled
login: {
accountId: '7ff1d19a-d3fd-4863-978e-8cce75fa880c', // logged-in account id
acr: string, // acr value for the authentication
remember: boolean, // true if provider should use a persistent cookie rather than a session one, defaults to true
ts: number, // unix timestamp of the authentication, defaults to now()
},
// consent was given by the user to the client for this session
consent: {
grantId: string, // the identifer of Grant object you saved during the interaction, resolved by Grant.prototype.save()
},
['custom prompt name resolved']: {},
}
// optionally, interactions can be primaturely exited with a an error by providing a result
// object as follow:
{
// an error field used as error code indicating a failure during the interaction
error: 'access_denied',
// an optional description for this error
error_description: 'Insufficient permissions: scope out of reach for this Account',
}
#provider.interactionResult
Unlike #provider.interactionFinished
authorization request resume uri is returned instead of
immediate http redirect.
// with express
expressApp.post('/interaction/:uid/login', async (req, res) => {
const redirectTo = await provider.interactionResult(req, res, result);
res.send({ redirectTo });
});
// with koa
router.post('/interaction/:uid', async (ctx, next) => {
const redirectTo = await provider.interactionResult(ctx.req, ctx.res, result);
ctx.body = { redirectTo };
});
oidc-provider comes with the basic grants implemented, but you can register your own grant types, for example to implement an OAuth 2.0 Token Exchange. You can check the standard grant factories here.
const parameters = [
'audience', 'resource', 'scope', 'requested_token_type',
'subject_token', 'subject_token_type',
'actor_token', 'actor_token_type'
];
const allowedDuplicateParameters = ['audience', 'resource'];
const grantType = 'urn:ietf:params:oauth:grant-type:token-exchange';
async function tokenExchangeHandler(ctx, next) {
// ctx.oidc.params holds the parsed parameters
// ctx.oidc.client has the authenticated client
// your grant implementation
// see /lib/actions/grants for references on how to instantiate and issue tokens
}
provider.registerGrantType(grantType, tokenExchangeHandler, parameters, allowedDuplicateParameters);
When using provider.app
or provider.callback()
as a mounted application in your own koa or express
stack just follow the respective module's documentation. However, when using the provider.app
Koa
instance directly to register i.e. koa-helmet you must push the middleware in
front of oidc-provider in the middleware stack.
const helmet = require('koa-helmet');
// Correct, pushes koa-helmet at the end of the middleware stack but BEFORE oidc-provider.
provider.use(helmet());
// Incorrect, pushes koa-helmet at the end of the middleware stack AFTER oidc-provider, not being
// executed when errors are encountered or during actions that do not "await next()".
provider.app.use(helmet());
You can push custom middleware to be executed before and after oidc-provider.
provider.use(async (ctx, next) => {
/** pre-processing
* you may target a specific action here by matching `ctx.path`
*/
console.log('pre middleware', ctx.method, ctx.path);
await next();
/** post-processing
* since internal route matching was already executed you may target a specific action here
* checking `ctx.oidc.route`, the unique route names used are
*
* `authorization`
* `backchannel_authentication`
* `client_delete`
* `client_update`
* `client`
* `code_verification`
* `cors.device_authorization`
* `cors.discovery`
* `cors.introspection`
* `cors.jwks`
* `cors.pushed_authorization_request`
* `cors.revocation`
* `cors.token`
* `cors.userinfo`
* `device_authorization`
* `device_resume`
* `discovery`
* `end_session_confirm`
* `end_session_success`
* `end_session`
* `introspection`
* `jwks`
* `pushed_authorization_request`
* `registration`
* `resume`
* `revocation`
* `token`
* `userinfo`
*/
console.log('post middleware', ctx.method, ctx.oidc.route);
});
The following snippets show how a provider instance can be mounted to existing applications with a
path prefix /oidc
.
Note: if you mount oidc-provider to a path it's likely you will have to also update the
interactions.url
configuration to reflect the new path.
// assumes connect ^3.0.0
connectApp.use('/oidc', oidc.callback());
// assumes fastify ^3.0.0
await app.register(require('fastify-express'));
// or
// await app.register(require('middie'));
fastifyApp.use('/oidc', oidc.callback());
// assumes @hapi/hapi ^20.0.0
const callback = oidc.callback();
hapiApp.route({
path: `/oidc/{any*}`,
method: '*',
config: { payload: { output: 'stream', parse: false } },
async handler({ raw: { req, res } }, h) {
req.originalUrl = req.url;
req.url = req.url.replace('/oidc', '');
await new Promise((resolve) => {
res.on('finish', resolve);
callback(req, res);
});
req.url = req.url.replace('/', '/oidc');
delete req.originalUrl;
return res.finished ? h.abandon : h.continue;
}
});
// assumes NestJS ^7.0.0
import { Controller, All, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
const callback = oidc.callback();
@Controller('oidc')
export class OidcController {
@All('/*')
public mountedOidc(@Req() req: Request, @Res() res: Response): void {
req.url = req.originalUrl.replace('/oidc', '');
return callback(req, res);
}
}
// assumes express ^4.0.0
expressApp.use('/oidc', oidc.callback());
// assumes koa ^2.0.0
// assumes koa-mount ^4.0.0
const mount = require('koa-mount');
koaApp.use(mount('/oidc', oidc.app));
Note: when the issuer identifier does not include the path prefix you should take care of rewriting
your ${root}/.well-known/openid-configuration
to ${root}${prefix}/.well-known/openid-configuration
so that your deployment remains conform to the
Discovery 1.0 specification.
Having a TLS offloading proxy in front of Node.js running oidc-provider is
the norm. To let your downstream application know of the original protocol and
ip you have to tell your app to trust x-forwarded-proto
and x-forwarded-for
headers commonly set by those proxies (as with any express/koa application).
This is needed for the provider responses to be correct (e.g. to have the right
https URL endpoints and keeping the right (secure) protocol).
Depending on your setup you should do the following in your downstream application code
setup | example |
---|---|
standalone oidc-provider | provider.proxy = true |
oidc-provider mounted to an express application |
provider.proxy = true |
oidc-provider mounted to a connect application |
provider.proxy = true |
oidc-provider mounted to a koa application |
yourKoaApp.proxy = true |
oidc-provider mounted to a fastify application |
provider.proxy = true |
oidc-provider mounted to a hapi application |
provider.proxy = true |
oidc-provider mounted to a nest application |
provider.proxy = true |
It is also necessary that the web server doing the offloading also passes those headers to the downstream application. Here is a common configuration for Nginx (assuming that the downstream application is listening on 127.0.0.1:8009). Your configuration may vary, please consult your web server documentation for details.
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8009;
proxy_redirect off;
}
Table of Contents
❗ marks the configuration you most likely want to take a look at.
- adapter ❗
- clients ❗
- findAccount ❗
- jwks ❗
- features ❗
- backchannelLogout
- ciba
- claimsParameter
- clientCredentials
- deviceFlow
- devInteractions ❗
- dPoP
- encryption
- fapi
- introspection
- issAuthResp
- jwtIntrospection
- jwtResponseModes
- jwtUserinfo
- mTLS
- pushedAuthorizationRequests
- registration
- registrationManagement
- requestObjects
- resourceIndicators ❗
- revocation
- userinfo
- acrValues
- allowOmittingSingleRegisteredRedirectUri
- claims ❗
- clientBasedCORS
- clientDefaults
- clockTolerance
- conformIdTokenClaims
- cookies
- discovery
- expiresWithSession
- extraClientMetadata
- extraParams
- extraTokenClaims
- httpOptions
- interactions ❗
- issueRefreshToken
- loadExistingGrant
- pairwiseIdentifier
- pkce ❗
- renderError
- responseTypes
- revokeGrantPolicy
- rotateRefreshToken
- routes
- sectorIdentifierUriValidate
- scopes
- subjectTypes
- tokenEndpointAuthMethods
- ttl ❗
- enabledJWA
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.
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.
default value:
[]
(Click to expand) 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
The following metadata is available but may not be recognized depending on your provider's configuration.
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
Function used to load an account and retrieve its available claims. The return value should be a Promise and #claims() can return a Promise too
default value:
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
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 };
},
};
}
JSON Web Key Set used by the provider for signing and decryption. The object must be in JWK Set format. All provided keys must be private keys.
recommendation: Be sure to follow best practices for distributing private keying material and secrets for your respective target deployment environment. Supported key types are:
- RSA
- OKP (Ed25519, Ed448, X25519, X448 sub types)
- EC (P-256, secp256k1, P-384, and P-521 curves) Provider key rotation** - The following action order is recommended when rotating signing keys on a distributed deployment with rolling reloads in place.
- push new keys at the very end of the "keys" array in your JWKS, this means the keys will become available for verification should they be encountered but not yet used for signing
- reload all your processes
- move your new key to the very front of the "keys" array in your JWKS, this means the key will be used for signing after reload
- reload all your processes
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.
(Click to expand) Acknowledging a draft / experimental feature
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.
Back-Channel Logout 1.0 - draft 06
Enables Back-Channel Logout features.
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 the version and be notified of breaking changes as part of your CI.
default value:
{
ack: undefined,
enabled: false
}
OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 - draft-03
Enables Core CIBA Flow, when combined with features.fapi
enables Financial-grade API: Client Initiated Backchannel Authentication Profile - Implementer's Draft 01 as well.
default value:
{
ack: undefined,
deliveryModes: [
'poll'
],
enabled: false,
processLoginHint: [AsyncFunction: processLoginHint], // see expanded details below
processLoginHintToken: [AsyncFunction: processLoginHintToken], // see expanded details below
triggerAuthenticationDevice: [AsyncFunction: triggerAuthenticationDevice], // see expanded details below
validateBindingMessage: [AsyncFunction: validateBindingMessage], // see expanded details below
validateRequestContext: [AsyncFunction: validateRequestContext], // see expanded details below
verifyUserCode: [AsyncFunction: verifyUserCode] // see expanded details below
}
(Click to expand) features.ciba options details
Fine-tune the supported token delivery modes. Supported values are
poll
ping
default value:
[
'poll'
]
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. Use return undefined
or when you can't determine the accountId from the login_hint.
default value:
async function processLoginHint(ctx, loginHint) {
// @param ctx - koa request context
// @param loginHint - string value of the login_hint parameter
throw new Error('features.ciba.processLoginHint not implemented');
}
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. Use throw Provider.errors.InvalidRequest('validation error message')
when login_hint_token is invalid. Use return undefined
or when you can't determine the accountId from the login_hint.
default value:
async function processLoginHintToken(ctx, loginHintToken) {
// @param ctx - koa request context
// @param loginHintToken - string value of the login_hint_token parameter
throw new Error('features.ciba.processLoginHintToken not implemented');
}
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.
default value:
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
throw new Error('features.ciba.triggerAuthenticationDevice not implemented');
}
(Click to expand) provider.backchannelResult()
method
backchannelResult
is a method on the Provider prototype, it returns a Promise
with no fulfillment value.
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.
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. Use return undefined
when a binding_message isn't required and wasn't provided.
default value:
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
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}$ )');
}
}
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. Use return undefined
when a request_context isn't required and wasn't provided.
default value:
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
throw new Error('features.ciba.validateRequestContext not implemented');
}
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. Use throw Provider.errors.InvalidUserCode('validation error message')
when the provided user_code is invalid. Use return undefined
when no user_code was provided and isn't required.
default value:
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
throw new Error('features.ciba.verifyUserCode not implemented');
}
Core 1.0 - Requesting Claims using the "claims" Request Parameter
Enables the use and validations of claims
parameter as described in the specification.
default value:
{
enabled: false
}
RFC6749 - Client Credentials
Enables grant_type=client_credentials
to be used on the token endpoint.
default value:
{
enabled: false
}
draft-ietf-oauth-dpop-03 - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP)
Enables DPoP
- mechanism for sender-constraining tokens via a proof-of-possession mechanism on the application level. Browser DPoP Proof generation here.
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 the version and be notified of breaking changes as part of your CI.
default value:
{
ack: undefined,
enabled: false,
iatTolerance: 60
}
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.
default value:
{
enabled: true
}
RFC8628 - OAuth 2.0 Device Authorization Grant (Device Flow)
Enables Device Authorization Grant
default value:
{
charset: 'base-20',
deviceInfo: [Function: deviceInfo], // see expanded details below
enabled: false,
mask: '****-****',
successSource: [AsyncFunction: successSource], // see expanded details below
userCodeConfirmSource: [AsyncFunction: userCodeConfirmSource], // see expanded details below
userCodeInputSource: [AsyncFunction: userCodeInputSource] // see expanded details below
}
(Click to expand) features.deviceFlow options details
alias for a character set of the generated user codes. Supported values are
base-20
uses BCDFGHJKLMNPQRSTVWXZdigits
uses 0123456789
default value:
'base-20'
Function used to extract details from the device authorization endpoint request. This is then available during the end-user confirm screen and is supposed to aid the user confirm that the particular authorization initiated by the user from a device in his possession
default value:
function deviceInfo(ctx) {
return {
ip: ctx.ip,
ua: ctx.get('user-agent'),
};
}
a string used as a template for the generated user codes, *
characters will be replaced by random chars from the charset, -
(dash) and
(space) characters may be included for readability. See the RFC for details about minimal recommended entropy
default value:
'****-****'
HTML source rendered when device code feature renders a success page for the User-Agent.
default value:
async function successSource(ctx) {
// @param ctx - koa request context
const {
clientId, clientName, clientUri, initiateLoginUri, logoUri, policyUri, tosUri,
} = ctx.oidc.client;
ctx.body = `<!DOCTYPE html>
<head>
<title>Sign-in Success</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<h1>Sign-in Success</h1>
<p>Your sign-in ${clientName ? `with ${clientName}` : ''} was successful, you can now close this page.</p>
</div>
</body>
</html>`;
}
HTML source rendered when device code feature renders an a confirmation prompt for ther User-Agent.
default value:
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
const {
clientId, clientName, clientUri, logoUri, policyUri, tosUri,
} = ctx.oidc.client;
ctx.body = `<!DOCTYPE html>
<head>
<title>Device Login Confirmation</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<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>
<button type="submit" form="op.deviceConfirmForm" value="yes" name="abort">[ Abort ]</button>
</div>
</div>
</body>
</html>`;
}
HTML source rendered when device code feature renders an input prompt for the User-Agent.
default value:
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
let msg;
if (err && (err.userCode || err.name === 'NoCodeError')) {
msg = '<p>The code you entered is incorrect. Try again</p>';
} else if (err && err.name === 'AbortedError') {
msg = '<p>The Sign-in request was interrupted</p>';
} else if (err) {
msg = '<p>There was an error processing your request</p>';
} else {
msg = '<p>Enter the code displayed on your device</p>';
}
ctx.body = `<!DOCTYPE html>
<head>
<title>Sign-in</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<h1>Sign-in</h1>
${msg}
${form}
<button type="submit" form="op.deviceInputForm">Continue</button>
</div>
</body>
</html>`;
}
Enables encryption features such as receiving encrypted UserInfo responses, encrypted ID Tokens and allow receiving encrypted Request Objects.
default value:
{
enabled: false
}
Financial-grade API Security Profile
Enables extra Authorization Server behaviours defined in FAPI that cannot be achieved by other configuration options.
default value:
{
enabled: false,
profile: '1.0 Final'
}
(Click to expand) other configuration needed to reach FAPI conformance
clientDefaults
for setting different default clienttoken_endpoint_auth_method
clientDefaults
for setting different default clientid_token_signed_response_alg
clientDefaults
for setting different default clientresponse_types
clientDefaults
for setting clienttls_client_certificate_bound_access_tokens
to trueclientDefaults
for setting clientrequire_signed_request_object
to trueclientDefaults
for setting clientdefault_acr_values
to whatever values are set by the specific FAPI ecosystemfeatures.mTLS
and enablecertificateBoundAccessTokens
features.mTLS
and enableselfSignedTlsClientAuth
and/ortlsClientAuth
features.claimsParameter
features.requestObjects
and enablerequest
and/orrequest_uri
enabledJWA
algorithm allow lists- (optional)
features.pushedAuthorizationRequests
- (optional)
features.jwtResponseModes
(Click to expand) features.fapi options details
The specific profile of FAPI to enable. Supported values are:
- '1.0 Final' (default) Enables behaviours from Financial-grade API Security Profile 1.0 - Part 2: Advanced
- '1.0 ID2' Enables behaviours from Financial-grade API - Part 2: Read and Write API Security Profile - Implementer's Draft 02
- Function returning one of the other supported values, or undefined if FAPI behaviours are to be ignored. The function is invoked with two arguments
(ctx, client)
and serves the purpose of allowing the used profile to be context-specific.
default value:
'1.0 Final'
RFC7662 - OAuth 2.0 Token Introspection
Enables Token Introspection for:
- opaque access tokens
- refresh tokens
default value:
{
allowedPolicy: [AsyncFunction: introspectionAllowedPolicy], // see expanded details below
enabled: false
}
(Click to expand) features.introspection options details
Helper function used to determine whether the client/RS (client argument) is allowed to introspect the given token (token argument).
default value:
async function introspectionAllowedPolicy(ctx, client, token) {
if (client.introspectionEndpointAuthMethod === 'none' && token.clientId !== ctx.oidc.client.clientId) {
return false;
}
return true;
}
draft-ietf-oauth-iss-auth-resp-01 - OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response
Enables iss
authorization response parameter for responses without existing countermeasures against mix-up attacks.
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 the version and be notified of breaking changes as part of your CI.
default value:
{
ack: undefined,
enabled: false
}
draft-ietf-oauth-jwt-introspection-response-10 - JWT Response for OAuth Token Introspection
Enables JWT responses for Token Introspection features
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 the version and be notified of breaking changes as part of your CI.
default value:
{
ack: undefined,
enabled: false
}
openid-financial-api-jarm-ID1 - JWT Secured Authorization Response Mode (JARM)
Enables JWT Secured Authorization Responses
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 the version and be notified of breaking changes as part of your CI.
default value:
{
ack: undefined,
enabled: false
}
Core 1.0 - JWT UserInfo Endpoint Responses
Enables the userinfo to optionally return signed and/or encrypted JWTs, also enables the relevant client metadata for setting up signing and/or encryption.
default value:
{
enabled: false
}
RFC8705 - OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens (MTLS)
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.
default value:
{
certificateAuthorized: [Function: certificateAuthorized], // see expanded details below
certificateBoundAccessTokens: false,
certificateSubjectMatches: [Function: certificateSubjectMatches], // see expanded details below
enabled: false,
getCertificate: [Function: getCertificate], // see expanded details below
selfSignedTlsClientAuth: false,
tlsClientAuth: false
}
(Click to expand) features.mTLS options details
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.
(Click to expand) 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
function certificateAuthorized(ctx) {
return ctx.get('x-ssl-client-verify') === 'SUCCESS';
}
(Click to expand) When using node's `https.createServer`
function certificateAuthorized(ctx) {
return ctx.socket.authorized;
}
Enables section 3 & 4 Mutual TLS Client Certificate-Bound Tokens by exposing the client's tls_client_certificate_bound_access_tokens
metadata property.
default value:
false
Function used to determine if the client certificate, used in the request, subject matches the registered client property. Only used for tls_client_auth
client authentication method.
(Click to expand) When behind a TLS terminating proxy (nginx/apache)
TLS terminating proxies can pass a header with the Subject DN pretty easily, for Nginx this would be $ssl_client_s_dn
, for apache %{SSL_CLIENT_S_DN}s
.
function certificateSubjectMatches(ctx, property, expected) {
switch (property) {
case 'tls_client_auth_subject_dn':
return ctx.get('x-ssl-client-s-dn') === expected;
default:
throw new Error(`${property} certificate subject matching not implemented`);
}
}
Function used to retrieve the PEM-formatted client certificate used in the request.
(Click to expand) 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
function getCertificate(ctx) {
return ctx.get('x-ssl-client-cert');
}
(Click to expand) When using node's `https.createServer`
function getCertificate(ctx) {
const peerCertificate = ctx.socket.getPeerCertificate();
if (peerCertificate.raw) {
return `-----BEGIN CERTIFICATE-----\n${peerCertificate.raw.toString('base64')}\n-----END CERTIFICATE-----`;
}
}
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.
default value:
false
Enables section 2.1. PKI Mutual TLS client authentication method tls_client_auth
for use in the server's tokenEndpointAuthMethods
configuration.
default value:
false
draft-ietf-oauth-par-08 - OAuth 2.0 Pushed Authorization Requests (PAR)
Enables the use of pushed_authorization_request_endpoint
defined by the Pushed Authorization Requests draft.
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 the version and be notified of breaking changes as part of your CI.
default value:
{
ack: undefined,
enabled: false,
requirePushedAuthorizationRequests: false
}
(Click to expand) features.pushedAuthorizationRequests options details
Makes the use of PAR required for all authorization requests as an OP policy.
default value:
false
Dynamic Client Registration 1.0
Enables Dynamic Client Registration.
default value:
{
enabled: false,
idFactory: [Function: idFactory], // see expanded details below
initialAccessToken: false,
issueRegistrationAccessToken: true,
policies: undefined,
secretFactory: [AsyncFunction: secretFactory] // see expanded details below
}
(Click to expand) features.registration options details
Function used to generate random client identifiers during dynamic client registration
default value:
function idFactory(ctx) {
return nanoid();
}
Enables registration_endpoint to check a valid initial access token is provided as a bearer token during the registration call. Supported types are
string
the string value will be checked as a static initial access tokenboolean
true/false to enable/disable adapter backed initial access tokens
default value:
false
(Click to expand) To add an adapter backed initial access token and retrive its value
new (provider.InitialAccessToken)({}).save().then(console.log);
Boolean or a function used to decide whether a registration access token will be issued or not. Supported values are
false
registration access tokens is issuedtrue
registration access tokens is not issued- function returning true/false, true when token should be issued, false when it shouldn't
default value:
true
(Click to expand) To determine if a registration access token should be issued dynamically
// @param ctx - koa request context
async issueRegistrationAccessToken(ctx) {
return policyImplementation(ctx)
}
define registration and registration management policies applied to client properties. Policies are sync/async functions that are assigned to an Initial Access Token that run before the regular client property validations are run. Multiple policies may be assigned to an Initial Access Token and by default the same policies will transfer over to the Registration Access Token. A policy may throw / reject and it may modify the properties object.
recommendation: referenced policies must always be present when encountered on a token, an AssertionError will be thrown inside the request context if it is not, resulting in a 500 Server Error. the same policies will be assigned to the Registration Access Token after a successful validation. If you wish to assign different policies to the Registration Access Token
// inside your final ran policy
ctx.oidc.entities.RegistrationAccessToken.policies = ['update-policy'];
default value:
undefined
(Click to expand) To define registration and registration management policies
To define policy functions configure features.registration
to be an object like so:
{
enabled: true,
initialAccessToken: true, // to enable adapter-backed initial access tokens
policies: {
'my-policy': function (ctx, properties) {
// @param ctx - koa request context
// @param properties - the client properties which are about to be validated
// example of setting a default
if (!('client_name' in properties)) {
properties.client_name = generateRandomClientName();
}
// example of forcing a value
properties.userinfo_signed_response_alg = 'RS256';
// example of throwing a validation error
if (someCondition(ctx, properties)) {
throw new Provider.errors.InvalidClientMetadata('validation error message');
}
},
'my-policy-2': async function (ctx, properties) {},
},
}
An Initial Access Token with those policies being executed (one by one in that order) is created like so
new (provider.InitialAccessToken)({ policies: ['my-policy', 'my-policy-2'] }).save().then(console.log);
Function used to generate random client secrets during dynamic client registration
default value:
async function secretFactory(ctx) {
const bytes = Buffer.allocUnsafe(64);
await randomFill(bytes);
return base64url.encodeBuffer(bytes);
}
OAuth 2.0 Dynamic Client Registration Management Protocol
Enables Update and Delete features described in the RFC
default value:
{
enabled: false,
rotateRegistrationAccessToken: false
}
(Click to expand) features.registrationManagement options details
Enables registration access token rotation. The provider will discard the current Registration Access Token with a successful update and issue a new one, returning it to the client with the Registration Update Response. Supported values are
false
registration access tokens are not rotatedtrue
registration access tokens are rotated when used- function returning true/false, true when rotation should occur, false when it shouldn't
default value:
false
(Click to expand) function use
{
features: {
registrationManagement: {
enabled: true,
async rotateRegistrationAccessToken(ctx) {
// return tokenRecentlyRotated(ctx.oidc.entities.RegistrationAccessToken);
// or
// return customClientBasedPolicy(ctx.oidc.entities.Client);
}
}
}
}
Core 1.0 and JWT Secured Authorization Request (JAR) - Request Object
Enables the use and validations of the request
and/or request_uri
parameters.
default value:
{
mode: 'lax',
request: false,
requestUri: true,
requireSignedRequestObject: false,
requireUriRegistration: true
}
(Click to expand) features.requestObjects options details
defines the provider's strategy when it comes to using regular OAuth 2.0 parameters that are present. Parameters inside the Request Object are ALWAYS used, this option controls whether to combine those with the regular ones or not.
Supported values are:
- 'lax' (default) This is the behaviour expected by OIDC Core 1.0 - all parameters that are not present in the Resource Object are used when resolving the authorization request.
- 'strict' This is the behaviour expected by FAPI or JAR, all parameters outside of the Request Object are ignored. For FAPI and FAPI-CIBA this value is enforced.
default value:
'lax'
Enables the use and validations of the request
parameter.
default value:
false
Enables the use and validations of the request_uri
parameter.
default value:
true
Makes the use of signed request objects required for all authorization requests as an OP policy.
default value:
false
Makes request_uri pre-registration mandatory (true) or optional (false).
default value:
true
RFC8707 - Resource Indicators for OAuth 2.0
Enables the use of resource
parameter for the authorization and token endpoints to enable issuing Access Tokens for Resource Servers (APIs).
- Multiple resource parameters may be present during Authorization Code Flow, Device Authorization Grant, and Backchannel Authentication Requests, but only a single audience for an Access Token is permitted.
- Authorization and Authentication Requests that result in an Access Token being issued by the Authorization Endpoint must only contain a single resource (or one must be resolved using the
defaultResource
helper). - Client Credentials grant must only contain a single resource parameter.
- During Authorization Code / Refresh Token / Device Code / Backchannel Authentication Request exchanges, if the exchanged code/token does not include the
'openid'
scope and only has a single resource then the resource parameter may be omitted - an Access Token for the single resource is returned. - During Authorization Code / Refresh Token / Device Code / Backchannel Authentication Request exchanges, if the exchanged code/token does not include the
'openid'
scope and has multiple resources then the resource parameter must be provided (or one must be resolved using thedefaultResource
helper). An Access Token for the provided/resolved resource is returned. - (with userinfo endpoint enabled and useGrantedResource helper returning falsy) During Authorization Code / Refresh Token / Device Code exchanges, if the exchanged code/token includes the
'openid'
scope and no resource parameter is present - an Access Token for the UserInfo Endpoint is returned. - (with userinfo endpoint enabled and useGrantedResource helper returning truthy) During Authorization Code / Refresh Token / Device Code exchanges, even if the exchanged code/token includes the
'openid'
scope and only has a single resource then the resource parameter may be omitted - an Access Token for the single resource is returned. - (with userinfo endpoint disabled) During Authorization Code / Refresh Token / Device Code exchanges, if the exchanged code/token includes the
'openid'
scope and only has a single resource then the resource parameter may be omitted - an Access Token for the single resource is returned. - Issued Access Tokens always only contain scopes that are defined on the respective Resource Server (returned from
features.resourceIndicators.getResourceServerInfo
).
default value:
{
defaultResource: [AsyncFunction: defaultResource], // see expanded details below
enabled: true,
getResourceServerInfo: [AsyncFunction: getResourceServerInfo], // see expanded details below
useGrantedResource: [AsyncFunction: useGrantedResource] // see expanded details below
}
(Click to expand) features.resourceIndicators options details
Function used to determine the default resource indicator for a request when none is provided by the client during the authorization request or when multiple are provided/resolved and only a single one is required during an Access Token Request.
default value:
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;
}
Function used to load information about a Resource Server (API) and check if the client is meant to request scopes for that particular resource.
recommendation: Only allow client's pre-registered resource values, to pre-register these you shall use the extraClientMetadata
configuration option to define a custom metadata and use that to implement your policy using this function.
default value:
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
throw new errors.InvalidTarget();
}
(Click to expand) Resource Server (API) with two scopes, an expected audience value, an Access Token TTL
and a JWT Access Token Format.
{
scope: 'api:read api:write',
audience: 'resource-server-audience-value',
accessTokenTTL: 2 * 60 * 60, // 2 hours
accessTokenFormat: 'jwt',
jwt: {
sign: { alg: 'ES256' },
},
}
(Click to expand) Resource Server (API) with two scopes and a symmetrically encrypted JWT Access Token Format.
{
scope: 'api:read api:write',
accessTokenFormat: 'jwt',
jwt: {
sign: false,
encrypt: {
alg: 'dir',
enc: 'A128CBC-HS256',
key: Buffer.from('f40dd9591646bebcb9c32aed02f5e610c2d15e1d38cde0c1fe14a55cf6bfe2d9', 'hex')
},
}
}
(Click to expand) Resource Server (API) with two scopes and a v1.local PASETO Access Token Format.
{
scope: 'api:read api:write',
accessTokenFormat: 'paseto',
paseto: {
version: 1,
purpose: 'local',
key: Buffer.from('f40dd9591646bebcb9c32aed02f5e610c2d15e1d38cde0c1fe14a55cf6bfe2d9', 'hex')
}
}
(Click to expand) Resource Server Definition
{
// REQUIRED
// available scope values (space-delimited string)
scope: string,
// OPTIONAL
// "aud" (Audience) value to use
// Default is the resource indicator value will be used as token audience
audience?: string,
// OPTIONAL
// Issued Token TTL
// Default is - see `ttl` configuration
accessTokenTTL?: number,
// Issued Token Format
// Default is - opaque
accessTokenFormat?: 'opaque' | 'jwt' | 'paseto',
// JWT Access Token Format (when accessTokenFormat is 'jwt')
// Default is `{ sign: { alg: 'RS256' }, encrypt: false }`
// Tokens may be signed, signed and then encrypted, or just encrypted JWTs.
jwt?: {
// Tokens will be signed
sign?:
| {
alg?: string, // 'PS256' | 'PS384' | 'PS512' | 'ES256' | 'ES256K' | 'ES384' | 'ES512' | 'EdDSA' | 'RS256' | 'RS384' | 'RS512'
kid?: string, // OPTIONAL `kid` to aid in signing key selection
}
| {
alg: string, // 'HS256' | 'HS384' | 'HS512'
key: crypto.KeyObject | Buffer, // shared symmetric secret to sign the JWT token with
kid?: string, // OPTIONAL `kid` JOSE Header Parameter to put in the token's JWS Header
},
// Tokens will be encrypted
encrypt?: {
alg: string, // 'dir' | 'RSA-OAEP' | 'RSA-OAEP-256' | 'RSA-OAEP-384' | 'RSA-OAEP-512' | 'RSA1_5' | 'ECDH-ES' | 'ECDH-ES+A128KW' | 'ECDH-ES+A192KW' | 'ECDH-ES+A256KW' | 'A128KW' | 'A192KW' | 'A256KW' | 'A128GCMKW' | 'A192GCMKW' | 'A256GCMKW' | 'PBES2-HS256+A128KW' | 'PBES2-HS384+A192KW' | 'PBES2-HS512+A256KW'
enc: string, // 'A128CBC-HS256' | 'A128GCM' | 'A192CBC-HS384' | 'A192GCM' | 'A256CBC-HS512' | 'A256GCM'
key: crypto.KeyObject | Buffer, // public key or shared symmetric secret to encrypt the JWT token with
kid?: string, // OPTIONAL `kid` JOSE Header Parameter to put in the token's JWE Header
}
}
// PASETO Access Token Format (when accessTokenFormat is 'paseto')
paseto?: {
version: 1 | 2 | 3 | 4,
purpose: 'local' | 'public',
key?: crypto.KeyObject, // required when purpose is 'local'
kid?: string, // OPTIONAL `kid` to aid in signing key selection or to put in the footer for 'local'
}
}
Function used to determine if an already granted resource indicator should be used without being explicitly requested by the client during the Token Endpoint request.
recommendation: Use return true
when it's allowed for a client skip providing the "resource" parameter at the Token Endpoint. Use return false
(default) when it's required for a client to explitly provide a "resource" parameter at the Token Endpoint or when other indication dictates an Access Token for the UserInfo Endpoint should returned.
default value:
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;
}
RFC7009 - OAuth 2.0 Token Revocation
Enables Token Revocation for:
- opaque access tokens
- refresh tokens
default value:
{
enabled: false
}
Enables RP-Initiated Logout features
default value:
{
enabled: true,
logoutSource: [AsyncFunction: logoutSource], // see expanded details below
postLogoutSuccessSource: [AsyncFunction: postLogoutSuccessSource] // see expanded details below
}
(Click to expand) features.rpInitiatedLogout options details
HTML source rendered when RP-Initiated Logout renders a confirmation prompt for the User-Agent.
default value:
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
ctx.body = `<!DOCTYPE html>
<head>
<title>Logout Request</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<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>`;
}
HTML source rendered when RP-Initiated Logout concludes a logout but there was no post_logout_redirect_uri
provided by the client.
default value:
async function postLogoutSuccessSource(ctx) {
// @param ctx - koa request context
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>
<title>Sign-out Success</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<h1>Sign-out Success</h1>
<p>Your sign-out ${display ? `with ${display}` : ''} was successful.</p>
</div>
</body>
</html>`;
}
Core 1.0 - UserInfo Endpoint
Enables the userinfo endpoint. Its use requires an opaque Access Token with at least openid
scope that's without a Resource Server audience.
default value:
{
enabled: true
}
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.
default value:
true
Array of strings, the Authentication Context Class References that the OP supports.
default value:
[]
Allow omitting the redirect_uri parameter when only a single one is registered for a client.
default value:
false
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'] }
)
default value:
{
acr: null,
auth_time: null,
iss: null,
openid: [
'sub'
],
sid: null
}
Function used to check whether a given CORS request should be allowed based on the request's client.
default value:
function clientBasedCORS(ctx, origin, client) {
return false;
}
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.
default value:
{
grant_types: [
'authorization_code'
],
id_token_signed_response_alg: 'RS256',
response_types: [
'code'
],
token_endpoint_auth_method: 'client_secret_basic'
}
(Click to expand) 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:
{
token_endpoint_auth_method: 'client_secret_post'
}
(Click to expand) 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:
{
response_types: ['code id_token'],
grant_types: ['authorization_code', 'implicit'],
}
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.
default value:
0
ID Token only contains End-User claims when the requested response_type
is id_token
Core 1.0 - Requesting Claims using Scope Values 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.
default value:
true
Options for the cookie module 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.
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
default value:
[]
Options for long-term cookies
recommendation: set cookies.keys and cookies.long.signed = true
default value:
{
httpOnly: true,
overwrite: true,
sameSite: 'none'
}
Cookie names used to store and transfer various states.
default value:
{
interaction: '_interaction',
resume: '_interaction_resume',
session: '_session'
}
Options for short-term cookies
recommendation: set cookies.keys and cookies.short.signed = true
default value:
{
httpOnly: true,
overwrite: true,
sameSite: 'lax'
}
Pass additional properties to this object to extend the discovery document
default value:
{
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
}
Function used to decide whether the given authorization code/ device code or implicit returned access token be bound to the user session. This will be applied to all tokens issued from the authorization / device code in the future. When tokens are session-bound the session will be loaded by its uid
every time the token is encountered. Session bound tokens will effectively get revoked if the end-user logs out.
default value:
async function expiresWithSession(ctx, token) {
return !token.scopes.has('offline_access');
}
Allows for custom client metadata to be defined, validated, manipulated as well as for existing property validations to be extended. Existing properties are snakeCased on a Client instance (e.g. client.redirectUris
), new properties (defined by this configuration) will be avaialable with their names verbatim (e.g. client['urn:example:client:my-property']
)
Array of property names that clients will be allowed to have defined.
default value:
[]
validator function that will be executed in order once for every property defined in extraClientMetadata.properties
, regardless of its value or presence on the client metadata passed in. Must be synchronous, async validators or functions returning Promise will be rejected during runtime. To modify the current client metadata values (for current key or any other) just modify the passed in metadata
argument.
default value:
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
}
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.
default value:
[]
Function used to assign additional claims to an Access Token when it is being issued. For opaque
Access Tokens these claims will be stored in your storage under the extra
property and returned by introspection as top level claims. For jwt
or paseto
Access Tokens these will be top level claims. Returned claims will not overwrite pre-existing top level claims.
default value:
async function extraTokenClaims(ctx, token) {
return undefined;
}
(Click to expand) To push additional claims to an Access Token
{
extraTokenClaims(ctx, token) {
return {
'urn:oidc-provider:example:foo': 'bar',
};
}
}
The value should be an integer (or a function returning an integer) and the resulting opaque token length is equal to Math.ceil(i / Math.log2(n))
where n is the number of symbols in the used alphabet, 64 in our case.
default value:
256
(Click to expand) To have e.g. Refresh Tokens values longer than Access Tokens.
function bitsOfOpaqueRandomness(ctx, token) {
if (token.kind === 'RefreshToken') {
return 384;
}
return 256;
}
Customizer functions used before issuing a structured Access Token.
default value:
{
jwt: undefined,
paseto: undefined
}
(Click to expand) To push additional headers and payload claims to a jwt
format Access Token
{
customizers: {
async jwt(ctx, token, jwt) {
jwt.header = { foo: 'bar' };
jwt.payload.foo = 'bar';
}
}
}
(Click to expand) To push a payload, a footer, and use an implicit assertion with a PASETO structured access token
{
customizers: {
paseto(ctx, token, structuredToken) {
structuredToken.payload.foo = 'bar';
structuredToken.footer = { foo: 'bar' };
structuredToken.assertion = 'foo'; // v3 and v4 tokens only
}
}
}
Function called whenever calls to an external HTTP(S) resource are being made. You can change the request timeout
duration, the agent
used as well as the lookup
resolver function.
default value:
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)
};
}
(Click to expand) To change the request's timeout
To change all request's timeout configure the httpOptions as a function like so:
{
httpOptions(url) {
return { timeout: 5000 };
}
}
Holds the configuration for interaction policy and url to send end-users to when the policy decides to require interaction.
structure of Prompts and their checks formed by Prompt and Check class instances. The default you can get a fresh instance for and the classes are available under Provider.interactionPolicy
.
default value:
[
/* LOGIN PROMPT */
new Prompt(
{ name: 'login', requestable: true },
(ctx) => {
const { oidc } = ctx;
return {
...(oidc.params.max_age === undefined ? undefined : { max_age: oidc.params.max_age }),
...(oidc.params.login_hint === undefined ? undefined : { login_hint: oidc.params.login_hint }),
...(oidc.params.id_token_hint === undefined ? undefined : { id_token_hint: oidc.params.id_token_hint }),
};
},
new Check('no_session', 'End-User authentication is required', (ctx) => {
const { oidc } = ctx;
if (oidc.session.accountId) {
return Check.NO_NEED_TO_PROMPT;
}
return Check.REQUEST_PROMPT;
}),
new Check('max_age', 'End-User authentication could not be obtained', (ctx) => {
const { oidc } = ctx;
if (oidc.params.max_age === undefined) {
return Check.NO_NEED_TO_PROMPT;
}
if (!oidc.session.accountId) {
return Check.REQUEST_PROMPT;
}
if (oidc.session.past(oidc.params.max_age) && (!ctx.oidc.result || !ctx.oidc.result.login)) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}),
new Check('id_token_hint', 'id_token_hint and authenticated subject do not match', async (ctx) => {
const { oidc } = ctx;
if (oidc.entities.IdTokenHint === undefined) {
return Check.NO_NEED_TO_PROMPT;
}
const { payload } = oidc.entities.IdTokenHint;
let sub = oidc.session.accountId;
if (sub === undefined) {
return Check.REQUEST_PROMPT;
}
if (oidc.client.subjectType === 'pairwise') {
sub = await instance(oidc.provider).configuration('pairwiseIdentifier')(ctx, sub, oidc.client);
}
if (payload.sub !== sub) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}),
new Check('claims_id_token_sub_value', 'requested subject could not be obtained', async (ctx) => {
const { oidc } = ctx;
if (!oidc.claims.id_token || !oidc.claims.id_token.sub || !('value' in oidc.claims.id_token.sub)) {
return Check.NO_NEED_TO_PROMPT;
}
let sub = oidc.session.accountId;
if (sub === undefined) {
return Check.REQUEST_PROMPT;
}
if (oidc.client.subjectType === 'pairwise') {
sub = await instance(oidc.provider).configuration('pairwiseIdentifier')(ctx, sub, oidc.client);
}
if (oidc.claims.id_token.sub.value !== sub) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ sub: oidc.claims.id_token.sub })),
new Check('essential_acrs', 'none of the requested ACRs could not be obtained', (ctx) => {
const { oidc } = ctx;
const request = get(oidc.claims, 'id_token.acr', {});
if (!request || !request.essential || !request.values) {
return Check.NO_NEED_TO_PROMPT;
}
if (!Array.isArray(oidc.claims.id_token.acr.values)) {
throw new errors.InvalidRequest('invalid claims.id_token.acr.values type');
}
if (request.values.includes(oidc.acr)) {
return Check.NO_NEED_TO_PROMPT;
}
return Check.REQUEST_PROMPT;
}, ({ oidc }) => ({ acr: oidc.claims.id_token.acr })),
new Check('essential_acr', 'requested ACR could not be obtained', (ctx) => {
const { oidc } = ctx;
const request = get(oidc.claims, 'id_token.acr', {});
if (!request || !request.essential || !request.value) {
return Check.NO_NEED_TO_PROMPT;
}
if (request.value === oidc.acr) {
return Check.NO_NEED_TO_PROMPT;
}
return Check.REQUEST_PROMPT;
}, ({ oidc }) => ({ acr: oidc.claims.id_token.acr })),
)
/* CONSENT PROMPT */
new Prompt(
{ name: 'consent', requestable: true },
new Check('native_client_prompt', 'native clients require End-User interaction', 'interaction_required', (ctx) => {
const { oidc } = ctx;
if (
oidc.client.applicationType === 'native'
&& oidc.params.response_type !== 'none'
&& (!oidc.result || !('consent' in oidc.result))
) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}),
new Check('op_scopes_missing', 'requested scopes not granted', (ctx) => {
const { oidc } = ctx;
const encounteredScopes = new Set(oidc.grant.getOIDCScopeEncountered().split(' '));
let missing;
for (const scope of oidc.requestParamOIDCScopes) { // eslint-disable-line no-restricted-syntax
if (!encounteredScopes.has(scope)) {
missing || (missing = []);
missing.push(scope);
}
}
if (missing && missing.length) {
ctx.oidc[missingOIDCScope] = missing;
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ missingOIDCScope: oidc[missingOIDCScope] })),
new Check('op_claims_missing', 'requested claims not granted', (ctx) => {
const { oidc } = ctx;
const encounteredClaims = new Set(oidc.grant.getOIDCClaimsEncountered());
let missing;
for (const claim of oidc.requestParamClaims) { // eslint-disable-line no-restricted-syntax
if (!encounteredClaims.has(claim) && !['sub', 'sid', 'auth_time', 'acr', 'amr', 'iss'].includes(claim)) {
missing || (missing = []);
missing.push(claim);
}
}
if (missing && missing.length) {
ctx.oidc[missingOIDCClaims] = missing;
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ missingOIDCClaims: oidc[missingOIDCClaims] })),
// checks resource server scopes
new Check('rs_scopes_missing', 'requested scopes not granted', (ctx) => {
const { oidc } = ctx;
let missing;
// eslint-disable-next-line no-restricted-syntax
for (const [indicator, resourceServer] of Object.entries(ctx.oidc.resourceServers)) {
const encounteredScopes = new Set(oidc.grant.getResourceScopeEncountered(indicator).split(' '));
const requestedScopes = ctx.oidc.requestParamScopes;
const availableScopes = resourceServer.scopes;
for (const scope of requestedScopes) { // eslint-disable-line no-restricted-syntax
if (availableScopes.has(scope) && !encounteredScopes.has(scope)) {
missing || (missing = {});
missing[indicator] || (missing[indicator] = []);
missing[indicator].push(scope);
}
}
}
if (missing && Object.keys(missing).length) {
ctx.oidc[missingResourceScopes] = missing;
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ missingResourceScopes: oidc[missingResourceScopes] })),
)
]
(Click to expand) default interaction policy description
The default interaction policy consists of two available prompts, login and consent
login
does the following checks:- no_session - checks that there's an established session, an authenticated end-user
- max_age - processes the max_age parameter (when the session's auth_time is too old it requires login)
- id_token_hint - processes the id_token_hint parameter (when the end-user sub differs it requires login)
- claims_id_token_sub_value - processes the claims parameter
sub
(when theclaims
parameter requested sub differs it requires login) - essential_acrs - processes the claims parameter
acr
(when the current acr is not amongst theclaims
parameter essentialacr.values
it requires login) - essential_acr - processes the claims parameter
acr
(when the current acr is not equal to theclaims
parameter essentialacr.value
it requires login) consent
does the following checks:- native_client_prompt - native clients always require re-consent
- op_scopes_missing - requires consent when the requested scope includes scope values previously not requested
- op_claims_missing - requires consent when the requested claims parameter includes claims previously not requested
- rs_scopes_missing - requires consent when the requested resource indicated scope values include scopes previously not requested
These checks are the best practice for various privacy and security reasons.
(Click to expand) disabling default consent checks
You may be required to skip (silently accept) some of the consent checks, while it is discouraged there are valid reasons to do that, for instance in some first-party scenarios or going with pre-existing, previously granted, consents. To simply silenty "accept" first-party/resource indicated scopes or pre-agreed upon claims use the loadExistingGrant
configuration helper function, in there you may just instantiate (and save!) a grant for the current clientId and accountId values.
(Click to expand) modifying the default interaction policy
const { interactionPolicy: { Prompt, Check, base } } = require('oidc-provider');
const basePolicy = base()
// basePolicy.get(name) => returns a Prompt instance by its name
// basePolicy.remove(name) => removes a Prompt instance by its name
// basePolicy.add(prompt, index) => adds a Prompt instance to a specific index, default is add the prompt as the last one
// prompt.checks.get(reason) => returns a Check instance by its reason
// prompt.checks.remove(reason) => removes a Check instance by its reason
// prompt.checks.add(check, index) => adds a Check instance to a specific index, default is add the check as the last one
Function used to determine where to redirect User-Agent for necessary interaction, can return both absolute and relative urls.
default value:
async function interactionsUrl(ctx, interaction) {
return `/interaction/${interaction.uid}`;
}
Function used to decide whether a refresh token will be issued or not
default value:
async function issueRefreshToken(ctx, client, code) {
return client.grantTypeAllowed('refresh_token') && code.scopes.has('offline_access');
}
(Click to expand) To always issue a refresh tokens ...
... If a client has the grant allowed and scope includes offline_access or the client is a public web client doing code flow. Configure issueRefreshToken
like so
async issueRefreshToken(ctx, client, code) {
if (!client.grantTypeAllowed('refresh_token')) {
return false;
}
return code.scopes.has('offline_access') || (client.applicationType === 'web' && client.tokenEndpointAuthMethod === 'none');
}
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.
default value:
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 used by the OP when resolving pairwise ID Token and Userinfo sub claim values. See Core 1.0
recommendation: Since this might be called several times in one request with the same arguments consider using memoization or otherwise caching the result based on account and client ids.
default value:
async function pairwiseIdentifier(ctx, accountId, client) {
return crypto.createHash('sha256')
.update(client.sectorIdentifier)
.update(accountId)
.update(os.hostname()) // put your own unique salt here, or implement other mechanism
.digest('hex');
}
RFC7636 - Proof Key for Code Exchange (PKCE)
PKCE configuration such as available methods and policy check on required use of PKCE
Fine-tune the supported code challenge methods. Supported values are
S256
plain
default value:
[
'S256'
]
Configures if and when the OP requires clients to use PKCE. This helper is called whenever an authorization request lacks the code_challenge parameter. Return
false
to allow the request to continue without PKCEtrue
to abort the request
default value:
function pkceRequired(ctx, client) {
return true;
}
Function used to present errors to the User-Agent
default value:
async function renderError(ctx, out, error) {
ctx.type = 'html';
ctx.body = `<!DOCTYPE html>
<head>
<title>oops! something went wrong</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<h1>oops! something went wrong</h1>
${Object.entries(out).map(([key, value]) => `<pre><strong>${key}</strong>: ${htmlSafe(value)}</pre>`).join('')}
</div>
</body>
</html>`;
}
Array of response_type values that the OP supports. The default omits all response types that result in access tokens being issued by the authorization endpoint directly as per OAuth 2.0 Security Best Current Practice You can still enable them if you need to.
default value:
[
'code id_token',
'code',
'id_token',
'none'
]
(Click to expand) Supported values list
These are values defined in Core 1.0 and OAuth 2.0 Multiple Response Type Encoding Practices
[
'code',
'id_token', 'id_token token',
'code id_token', 'code token', 'code id_token token',
'none',
]
Function called in a number of different context to determine whether an underlying Grant entry should also be revoked or not.
contexts:
- RP-Initiated Logout
- Refresh Token Revocation
- Authorization Code re-use
- Device Code re-use
- Backchannel Authentication Request re-use
- Rotated Refresh Token re-use
default value:
function revokeGrantPolicy(ctx) {
return true;
}
Configures if and how the OP rotates refresh tokens after they are used. Supported values are
false
refresh tokens are not rotated and their initial expiration date is finaltrue
refresh tokens are rotated when used, current token is marked as consumed and new one is issued with new TTL, when a consumed refresh token is encountered an error is returned instead and the whole token chain (grant) is revokedfunction
returning true/false, true when rotation should occur, false when it shouldn't
The default configuration value puts forth a sensible refresh token rotation policy- only allows refresh tokens to be rotated (have their TTL prolonged by issuing a new one) for one year
- otherwise always rotate public client tokens that are not sender-constrained
- otherwise only rotate tokens if they're being used close to their expiration (>= 70% TTL passed)
default value:
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;
}
Routing values used by the OP. Only provide routes starting with "/"
default value:
{
authorization: '/auth',
backchannel_authentication: '/backchannel',
code_verification: '/device',
device_authorization: '/device/auth',
end_session: '/session/end',
introspection: '/token/introspection',
jwks: '/jwks',
pushed_authorization_request: '/request',
registration: '/reg',
revocation: '/token/revocation',
token: '/token',
userinfo: '/me'
}
Array of additional scope values that the OP signals to support in the discovery endpoint. Only add scopes the OP has a corresponding resource for. Resource Server scopes don't belong here, see features.resourceIndicators
for configuring those.
default value:
[
'openid',
'offline_access'
]
Function called to make a decision about whether sectorIdentifierUri of a client being loaded, registered, or updated should be fetched and its contents validated against the client metadata.
default value:
function sectorIdentifierUriValidate(client) {
// @param client - the Client instance
return true;
}
Array of the Subject Identifier types that this OP supports. When only pairwise
is supported it becomes the default subject_type
client metadata value. Valid types are
public
pairwise
default value:
[
'public'
]
Array of Client Authentication methods supported by this OP's Token Endpoint
default value:
[
'client_secret_basic',
'client_secret_jwt',
'client_secret_post',
'private_key_jwt',
'none'
]
(Click to expand) Supported values list
[
'none',
'client_secret_basic', 'client_secret_post',
'client_secret_jwt', 'private_key_jwt',
'tls_client_auth', 'self_signed_tls_client_auth', // these methods are only available when features.mTLS is configured
]
description: Expirations for various token and session types. The value can be a number (in seconds) or a synchronous function that dynamically returns value based on the context.
recommendation: Do not set token TTLs longer then they absolutely have to be, the shorter the TTL, the better. Rather than setting crazy high Refresh Token TTL look into rotateRefreshToken
configuration option which is set up in way that when refresh tokens are regularly used they will have their TTL refreshed (via rotation). This is inline with the OAuth 2.0 Security Best Current Practice
default value:
{
AccessToken: function AccessTokenTTL(ctx, token, client) {
if (token.resourceServer) {
return token.resourceServer.accessTokenTTL || 60 * 60; // 1 hour in seconds
}
return 60 * 60; // 1 hour in seconds
},
AuthorizationCode: 600 /* 10 minutes in seconds */,
BackchannelAuthenticationRequest: function BackchannelAuthenticationRequestTTL(ctx, request, client) {
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
},
ClientCredentials: function ClientCredentialsTTL(ctx, token, client) {
if (token.resourceServer) {
return token.resourceServer.accessTokenTTL || 10 * 60; // 10 minutes in seconds
}
return 10 * 60; // 10 minutes in seconds
},
DeviceCode: 600 /* 10 minutes in seconds */,
Grant: 1209600 /* 14 days in seconds */,
IdToken: 3600 /* 1 hour in seconds */,
Interaction: 3600 /* 1 hour in seconds */,
RefreshToken: function RefreshTokenTTL(ctx, token, client) {
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
},
Session: 1209600 /* 14 days in seconds */
}
(Click to expand) To resolve a ttl on runtime for each new token
Configure ttl
for a given token type with a function like so, this must return a value, not a Promise.
{
ttl: {
AccessToken(ctx, token, client) {
// return a Number (in seconds) for the given token (first argument), the associated client is
// passed as a second argument
// Tip: if the values are entirely client based memoize the results
return resolveTTLfor(token, client);
},
},
}
Fine-tune the algorithms your provider will support by declaring algorithm values for each respective JWA use
recommendation: Only allow JWA algs that are necessary. The current defaults are based on recommendations from the JWA specification + enables RSASSA-PSS based on current guidance in FAPI. "none" JWT algs are disabled by default but available if you need them.
JWE "alg" Algorithm values the provider supports for JWT Authorization response (JARM) encryption
default value:
[
'A128KW',
'A256KW',
'ECDH-ES',
'RSA-OAEP',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512', 'RSA1_5',
// asymmetric ECDH-ES based
'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW',
// symmetric AES key wrapping
'A128KW', 'A192KW', 'A256KW', 'A128GCMKW', 'A192GCMKW', 'A256GCMKW',
// symmetric PBES2 + AES
'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW',
// direct encryption
'dir',
]
JWE "enc" Content Encryption Algorithm values the provider supports to encrypt JWT Authorization Responses (JARM) with
default value:
[
'A128CBC-HS256',
'A128GCM',
'A256CBC-HS512',
'A256GCM'
]
(Click to expand) Supported values list
[
'A128CBC-HS256', 'A128GCM', 'A192CBC-HS384', 'A192GCM', 'A256CBC-HS512', 'A256GCM',
]
JWS "alg" Algorithm values the provider supports to sign JWT Authorization Responses (JARM) with
default value:
[
'RS256',
'PS256',
'ES256',
'EdDSA'
]
(Click to expand) Supported values list
[
'RS256', 'RS384', 'RS512',
'PS256', 'PS384', 'PS512',
'ES256', 'ES256K', 'ES384', 'ES512',
'EdDSA',
'HS256', 'HS384', 'HS512',
]
JWS "alg" Algorithm values the provider supports to verify signed DPoP Proof JWTs with
default value:
[
'RS256',
'PS256',
'ES256',
'EdDSA'
]
(Click to expand) Supported values list
[
'RS256', 'RS384', 'RS512',
'PS256', 'PS384', 'PS512',
'ES256', 'ES256K', 'ES384', 'ES512',
'EdDSA',
]
JWE "alg" Algorithm values the provider supports for ID Token encryption
default value:
[
'A128KW',
'A256KW',
'ECDH-ES',
'RSA-OAEP',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512', 'RSA1_5',
// asymmetric ECDH-ES based
'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW',
// symmetric AES key wrapping
'A128KW', 'A192KW', 'A256KW', 'A128GCMKW', 'A192GCMKW', 'A256GCMKW',
// symmetric PBES2 + AES
'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW',
// direct encryption
'dir',
]
JWE "enc" Content Encryption Algorithm values the provider supports to encrypt ID Tokens with
default value:
[
'A128CBC-HS256',
'A128GCM',
'A256CBC-HS512',
'A256GCM'
]
(Click to expand) Supported values list
[
'A128CBC-HS256', 'A128GCM', 'A192CBC-HS384', 'A192GCM', 'A256CBC-HS512', 'A256GCM',
]
JWS "alg" Algorithm values the provider supports to sign ID Tokens with.
default value:
[
'RS256',
'PS256',
'ES256',
'EdDSA'
]
(Click to expand) Supported values list
[
'RS256', 'RS384', 'RS512',
'PS256', 'PS384', 'PS512',
'ES256', 'ES256K', 'ES384', 'ES512',
'EdDSA',
'HS256', 'HS384', 'HS512',
'none',
]
JWE "alg" Algorithm values the provider supports for JWT Introspection response encryption
default value:
[
'A128KW',
'A256KW',
'ECDH-ES',
'RSA-OAEP',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512', 'RSA1_5',
// asymmetric ECDH-ES based
'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW',
// symmetric AES key wrapping
'A128KW', 'A192KW', 'A256KW', 'A128GCMKW', 'A192GCMKW', 'A256GCMKW',
// symmetric PBES2 + AES
'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW',
// direct encryption
'dir',
]
JWE "enc" Content Encryption Algorithm values the provider supports to encrypt JWT Introspection responses with
default value:
[
'A128CBC-HS256',
'A128GCM',
'A256CBC-HS512',
'A256GCM'
]
(Click to expand) Supported values list
[
'A128CBC-HS256', 'A128GCM', 'A192CBC-HS384', 'A192GCM', 'A256CBC-HS512', 'A256GCM',
]
JWS "alg" Algorithm values the provider supports to sign JWT Introspection responses with
default value:
[
'RS256',
'PS256',
'ES256',
'EdDSA'
]
(Click to expand) Supported values list
[
'RS256', 'RS384', 'RS512',
'PS256', 'PS384', 'PS512',
'ES256', 'ES256K', 'ES384', 'ES512',
'EdDSA',
'HS256', 'HS384', 'HS512',
'none',
]
JWE "alg" Algorithm values the provider supports to receive encrypted Request Objects (JAR) with
default value:
[
'A128KW',
'A256KW',
'ECDH-ES',
'RSA-OAEP',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512', 'RSA1_5',
// asymmetric ECDH-ES based
'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW',
// symmetric AES key wrapping
'A128KW', 'A192KW', 'A256KW', 'A128GCMKW', 'A192GCMKW', 'A256GCMKW',
// symmetric PBES2 + AES
'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW',
// direct encryption
'dir',
]
JWE "enc" Content Encryption Algorithm values the provider supports to decrypt Request Objects (JAR) with
default value:
[
'A128CBC-HS256',
'A128GCM',
'A256CBC-HS512',
'A256GCM'
]
(Click to expand) Supported values list
[
'A128CBC-HS256', 'A128GCM', 'A192CBC-HS384', 'A192GCM', 'A256CBC-HS512', 'A256GCM',
]
JWS "alg" Algorithm values the provider supports to receive signed Request Objects (JAR) with
default value:
[
'HS256',
'RS256',
'PS256',
'ES256',
'EdDSA'
]
(Click to expand) Supported values list
[
'RS256', 'RS384', 'RS512',
'PS256', 'PS384', 'PS512',
'ES256', 'ES256K', 'ES384', 'ES512',
'EdDSA',
'HS256', 'HS384', 'HS512',
'none',
]
JWS "alg" Algorithm values the provider supports for signed JWT Client Authentication
default value:
[
'HS256',
'RS256',
'PS256',
'ES256',
'EdDSA'
]
(Click to expand) Supported values list
[
'RS256', 'RS384', 'RS512',
'PS256', 'PS384', 'PS512',
'ES256', 'ES256K', 'ES384', 'ES512',
'EdDSA',
'HS256', 'HS384', 'HS512',
]
JWE "alg" Algorithm values the provider supports for UserInfo Response encryption
default value:
[
'A128KW',
'A256KW',
'ECDH-ES',
'RSA-OAEP',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512', 'RSA1_5',
// asymmetric ECDH-ES based
'ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW',
// symmetric AES key wrapping
'A128KW', 'A192KW', 'A256KW', 'A128GCMKW', 'A192GCMKW', 'A256GCMKW',
// symmetric PBES2 + AES
'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW',
// direct encryption
'dir',
]
JWE "enc" Content Encryption Algorithm values the provider supports to encrypt UserInfo responses with
default value:
[
'A128CBC-HS256',
'A128GCM',
'A256CBC-HS512',
'A256GCM'
]
(Click to expand) Supported values list
[
'A128CBC-HS256', 'A128GCM', 'A192CBC-HS384', 'A192GCM', 'A256CBC-HS512', 'A256GCM',
]
JWS "alg" Algorithm values the provider supports to sign UserInfo responses with
default value:
[
'RS256',
'PS256',
'ES256',
'EdDSA'
]
(Click to expand) Supported values list
[
'RS256', 'RS384', 'RS512',
'PS256', 'PS384', 'PS512',
'ES256', 'ES256K', 'ES384', 'ES512',
'EdDSA',
'HS256', 'HS384', 'HS512',
'none',
]
Only response types that do not end up with an access_token (so, response_type=id_token) have
end-user claims other than sub
in their ID Tokens. This is the
Core 1.0 spec behaviour. Read
it you'll see requesting claims through the scope parameter only adds these claims to userinfo
unless the response_type is id_token
in which case they're added there. All other response types
have access to the userinfo endpoint which returns these scope-requested claims. The other option is
to allow clients to request specific claims from a source they expect it in via the claims
parameter.
But, if you absolutely need to have scope-requested claims in ID Tokens you can use the
conformIdTokenClaims
configuration option.
Your provider is behind a TLS terminating proxy, tell your provider instance to trust the proxy headers. More on this in Trusting TLS offloading proxies
You're likely using client_secret_basic client authentication and the oidc-provider is actually exhibiting conform behaviour. It's likely a bug in your client software - it's not encoding the header correctly.
client_secret_basic
is not 100% basic http auth, the username and password tokens are supposed to
be additionally formencoded.
A proper way of submitting client_id
and client_secret
using client_secret_basic
is
Authorization: base64(formEncode(client_id):formEncode(client_secret))
as per
https://tools.ietf.org/html/rfc6749#section-2.3.1 incl.
https://tools.ietf.org/html/rfc6749#appendix-B
Example:
const client_id = 'an:identifier';
const client_secret = 'some secure & non-standard secret';
// After formencoding these two tokens
const encoded_id = 'an%3Aidentifier';
const encoded_secret = 'some+secure+%26+non-standard+secret';
// Basic auth header format Authorization: Basic base64(encoded_id + ':' + encoded_secret)
// Authorization: Basic YW4lM0FpZGVudGlmaWVyOnNvbWUrc2VjdXJlKyUyNitub24tc3RhbmRhcmQrc2VjcmV0
So essentially, your client is not submitting the client auth in a conform way and you should fix that.
Every client is configured with one of 7 available
token_endpoint_auth_method
values
and it must adhere to how that given method must be submitted. Submitting multiple means of
authentication is also not possible. If you're a provider operator you're encouraged to set up
listeners for errors
(see events.md) and
deliver them to client developers out-of-band, e.g. by logs in an admin interface.
function handleClientAuthErrors({ headers: { authorization }, oidc: { body, client } }, err) {
if (err.statusCode === 401 && err.message === 'invalid_client') {
// console.log(err);
// save error details out-of-bands for the client developers, `authorization`, `body`, `client`
// are just some details available, you can dig in ctx object for more.
}
}
provider.on('grant.error', handleClientAuthErrors);
provider.on('introspection.error', handleClientAuthErrors);
provider.on('revocation.error', handleClientAuthErrors);
I'm not getting refresh_token from token_endpoint grant_type=authorization_code responses, why?
Do you support offline_access scope and consent prompt? Did the client request them in the authentication request?
Yeah, still no refresh_token
Does the client have grant_type=refresh_token configured?
Aaaah, that was it. (or one of the above if you follow Core 1.0#OfflineAccess)
The Authorization Server MAY grant Refresh Tokens in other contexts that are beyond the scope of this specification. How about that?
Yeah, yeah, see configuration
If you need it today something's wrong!
ROPC falls beyond the scope of what the library can do magically on it's own having only accountId
and the claims, it does not ask for an interface necessary to find an account by a username nor by
validating the password digest. Custom implementation using the provided
registerGrantType
API is simple enough if you absolutely need ROPC.
const ctx = provider.app.createContext(req, res)
const session = await provider.Session.get(ctx)
const signedIn = !!session.accountId
You're getting the redirect_uris is mandatory property
error but Client Credential clients
don't need redirect_uris
or response_types
... You're getting this error
because they are required properties, but they can be empty...
{
// ... rest of the client configuration
redirect_uris: [],
response_types: [],
grant_types: ['client_credentials']
}
You're getting the redirect_uris is mandatory property
error but the resource server needs
none. You're getting this error because they are required properties, but they can be empty...
{
// ... rest of the client configuration
redirect_uris: [],
response_types: [],
grant_types: []
}