This module 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.
If you want to quickly add OpenID Connect authentication to Node.js apps, feel free to check out Auth0's Node.js SDK and free plan. Create an Auth0 account; it's free!
If you or your company use this module, 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.
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 ❗
import Provider from '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',
)
})
This module 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 this module 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: string, // logged-in account id
acr: string, // acr value for the authentication
amr: string[], // amr values 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.
import helmet from '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 ^4.0.0
const fastify = new Fastify()
await fastify.register(require('@fastify/middie'))
// or
// await app.register(require('@fastify/express'));
fastify.use('/oidc', oidc.callback())
// assumes @hapi/hapi ^21.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', '')
callback(req, res)
await once(res, 'finish')
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
import mount from '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
- jwtIntrospection
- jwtResponseModes
- jwtUserinfo
- mTLS
- pushedAuthorizationRequests
- registration
- registrationManagement
- requestObjects
- resourceIndicators ❗
- revocation
- rpInitiatedLogout
- 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
- clientAuthMethods
- 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, response_modes, 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_signed_response_alg, request_object_encryption_alg, request_object_encryption_enc, request_object_signing_alg, request_uris, 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
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.
Supported key types are:
- RSA
- OKP (Ed25519, Ed448, X25519, X448 sub types)
- EC (P-256, secp256k1, P-384, and P-521 curves)
recommendation: Be sure to follow best practices for distributing private keying material and secrets for your respective target deployment environment.
recommendation: 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 an 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 experimental feature 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.
Enables Back-Channel Logout features.
default value:
{
enabled: false
}
OIDC Client Initiated Backchannel Authentication Flow (CIBA
)
Enables Core CIBA
Flow, when combined with features.fapi
and features.requestObjects.request
enables Financial-grade API: Client Initiated Backchannel Authentication Profile - Implementer's Draft 01 as well.
default value:
{
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 new errors.InvalidRequest('validation error message')
when login_hint is invalid.
recommendation: Use return undefined
or when you can't determine the accountId from the login_hint.
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 new errors.ExpiredLoginHintToken('validation error message')
when login_hint_token is expired.
recommendation: Use throw new errors.InvalidRequest('validation error message')
when login_hint_token is invalid.
recommendation: Use return undefined
or when you can't determine the accountId from the login_hint.
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 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 new errors.InvalidBindingMessage('validation error message')
when the binding_message is invalid.
recommendation: Use return undefined
when a binding_message isn't required and wasn't provided.
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 new errors.InvalidRequest('validation error message')
when the request_context is required by policy and missing or invalid.
recommendation: Use return undefined
when a request_context isn't required and wasn't provided.
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 new errors.MissingUserCode('validation error message')
when user_code should have been provided but wasn't.
recommendation: Use throw new errors.InvalidUserCode('validation error message')
when the provided user_code is invalid.
recommendation: Use return undefined
when no user_code was provided and isn't required.
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');
}
OIDC 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:
{
assertClaimsParameter: [AsyncFunction: assertClaimsParameter], // see expanded details below
enabled: false
}
(Click to expand) features.claimsParameter options details
Helper function used to validate the claims parameter beyond what the OpenID Connect 1.0 specification requires.
default value:
async function assertClaimsParameter(ctx, claims, client) {
// @param ctx - koa request context
// @param claims - parsed claims parameter
// @param client - the Client instance
}
RFC6749
- Client Credentials
Enables grant_type=client_credentials
to be used on the token endpoint.
default value:
{
enabled: false
}
RFC9449
- 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.
default value:
{
ack: undefined,
allowReplay: false,
enabled: false,
nonceSecret: undefined,
requireNonce: [Function: requireNonce] // see expanded details below
}
(Click to expand) features.dPoP options details
Controls whether DPoP Proof Replay Detection is used or not.
default value:
false
A secret value used for generating server-provided DPoP nonces. Must be a 32-byte length Buffer instance when provided.
default value:
undefined
Function used to determine whether a DPoP nonce is required or not.
default value:
function requireNonce(ctx) {
return false;
}
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 their 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>
<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>
<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>
<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 (FAPI
)
Enables extra Authorization Server behaviours defined in FAPI that cannot be achieved by other configuration options.
default value:
{
enabled: false,
profile: undefined
}
(Click to expand) features.fapi options details
The specific profile of FAPI
to enable. Supported values are:
- '2.0' (Experimental) Enables behaviours from FAPI 2.0 Security Profile - Implementer's Draft 02
- '1.0 Final' 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.
Versions marked as experimental will follow the specification's development milestones via MINOR library versions.
default value:
undefined
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.clientAuthMethod === 'none' && token.clientId !== ctx.oidc.client.clientId) {
return false;
}
return true;
}
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
}
JWT Secured Authorization Response Mode (JARM
)
Enables JWT Secured Authorization Responses
default value:
{
enabled: false
}
OIDC 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.
default value:
function certificateAuthorized(ctx) {
throw new Error('features.mTLS.certificateAuthorized function not configured');
}
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.
default value:
function certificateSubjectMatches(ctx, property, expected) {
throw new Error('features.mTLS.certificateSubjectMatches function not configured');
}
Function used to retrieve a crypto.X509Certificate
instance, or a PEM-formatted string, representation of client certificate used in the request.
default value:
function getCertificate(ctx) {
throw new Error('features.mTLS.getCertificate function not configured');
}
Enables section 2.2. Self-Signed Certificate Mutual TLS client authentication method self_signed_tls_client_auth
for use in the server's clientAuthMethods
configuration.
default value:
false
Enables section 2.1. PKI Mutual TLS client authentication method tls_client_auth
for use in the server's clientAuthMethods
configuration.
default value:
false
RFC9126
- OAuth 2.0 Pushed Authorization Requests (PAR
)
Enables the use of pushed_authorization_request_endpoint
defined by the Pushed Authorization Requests RFC.
default value:
{
allowUnregisteredRedirectUris: false,
enabled: true,
requirePushedAuthorizationRequests: false
}
(Click to expand) features.pushedAuthorizationRequests options details
Allows unregistered redirect_uri values to be used by authenticated clients using PAR that do not use a sector_identifier_uri
.
default value:
false
Makes the use of PAR
required for all authorization requests as an OP policy.
default value:
false
Dynamic Client Registration 1.0
and RFC7591
- OAuth 2.0 Dynamic Client Registration Protocol
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
true
registration access tokens is issuedfalse
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.
recommendation: 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 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: true
}
(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:
true
(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);
}
}
}
}
OIDC 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:
{
assertJwtClaimsAndHeader: [AsyncFunction: assertJwtClaimsAndHeader], // see expanded details below
mode: 'strict',
request: false,
requestUri: false,
requireSignedRequestObject: false,
requireUriRegistration: true
}
(Click to expand) features.requestObjects options details
Helper function used to validate the Request Object JWT Claims Set and Header beyond what the JAR specification requires.
default value:
async function assertJwtClaimsAndHeader(ctx, claims, header, client) {
// @param ctx - koa request context
// @param claims - parsed Request Object JWT Claims Set as object
// @param header - parsed Request Object JWT Headers as object
// @param client - the Client instance
const fapiProfile = ctx.oidc.isFapi('1.0 Final', '1.0 ID2', '2.0');
if (fapiProfile) {
if (!('exp' in claims)) {
throw new errors.InvalidRequestObject("Request Object is missing the 'exp' claim");
}
if (fapiProfile === '1.0 Final' || fapiProfile === '2.0') {
if (!('aud' in claims)) {
throw new errors.InvalidRequestObject("Request Object is missing the 'aud' claim");
}
if (!('nbf' in claims)) {
throw new errors.InvalidRequestObject("Request Object is missing the 'nbf' claim");
}
const diff = claims.exp - claims.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new errors.InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
}
}
}
if (ctx.oidc.route === 'backchannel_authentication') {
for (const claim of ['exp', 'iat', 'nbf', 'jti']) {
if (!(claim in claims)) {
throw new errors.InvalidRequestObject(`Request Object is missing the '${claim}' claim`);
}
}
if (fapiProfile) {
const diff = claims.exp - claims.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new errors.InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
}
}
}
}
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' 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' (default) All parameters outside of the Request Object are ignored. For
PAR
,FAPI
, andCIBA
this value is enforced.
default value:
'strict'
Enables the use and validations of the request
parameter.
default value:
false
Enables the use and validations of the request_uri
parameter.
default value:
false
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 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',
// 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' | 'ECDH-ES' | 'ECDH-ES+A128KW' | 'ECDH-ES+A192KW' | 'ECDH-ES+A256KW' | 'A128KW' | 'A192KW' | 'A256KW' | 'A128GCMKW' | 'A192GCMKW' | 'A256GCMKW'
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
}
}
}
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.
recommendation: 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
}
RFC9396
- OAuth 2.0 Rich Authorization Requests
Enables the use of authorization_details
parameter for the authorization and token endpoints to enable issuing Access Tokens with fine-grained authorization data.
default value:
{
ack: undefined,
enabled: false,
rarForAuthorizationCode: [Function: rarForAuthorizationCode], // see expanded details below
rarForCodeResponse: [Function: rarForCodeResponse], // see expanded details below
rarForIntrospectionResponse: [Function: rarForIntrospectionResponse], // see expanded details below
rarForRefreshTokenResponse: [Function: rarForRefreshTokenResponse], // see expanded details below
types: {}
}
(Click to expand) features.richAuthorizationRequests options details
Function used to transform the requested and granted RAR details that are then stored in the authorization code. Return array of details or undefined.
default value:
rarForAuthorizationCode(ctx) {
// decision points:
// - ctx.oidc.client
// - ctx.oidc.resourceServers
// - ctx.oidc.params.authorization_details (unparsed authorization_details from the authorization request)
// - ctx.oidc.grant.rar (authorization_details granted)
throw new Error('features.richAuthorizationRequests.rarForAuthorizationCode not implemented');
}
Function used to transform transform the requested and granted RAR details to be returned in the Access Token Response as authorization_details as well as assigned to the issued Access Token. Return array of details or undefined.
default value:
rarForCodeResponse(ctx, resourceServer) {
// decision points:
// - ctx.oidc.client
// - resourceServer
// - ctx.oidc.authorizationCode.rar (previously returned from rarForAuthorizationCode)
// - ctx.oidc.params.authorization_details (unparsed authorization_details from the body params in the Access Token Request)
// - ctx.oidc.grant.rar (authorization_details granted)
throw new Error('features.richAuthorizationRequests.rarForCodeResponse not implemented');
}
Function used to transform transform the requested and granted RAR details to be returned in the Access Token Response as authorization_details as well as assigned to the issued Access Token. Return array of details or undefined.
default value:
rarForIntrospectionResponse(ctx, token) {
// decision points:
// - ctx.oidc.client
// - token.kind
// - token.rar
// - ctx.oidc.grant.rar
throw new Error('features.richAuthorizationRequests.rarForIntrospectionResponse not implemented');
}
Function used to transform transform the requested and granted RAR details to be returned in the Access Token Response as authorization_details as well as assigned to the issued Access Token. Return array of details or undefined.
default value:
rarForRefreshTokenResponse(ctx, resourceServer) {
// decision points:
// - ctx.oidc.client
// - resourceServer
// - ctx.oidc.refreshToken.rar (previously returned from rarForAuthorizationCode and later assigned to the refresh token)
// - ctx.oidc.params.authorization_details (unparsed authorization_details from the body params in the Access Token Request)
// - ctx.oidc.grant.rar
throw new Error('features.richAuthorizationRequests.rarForRefreshTokenResponse not implemented');
}
Supported authorization details type identifiers.
default value:
{}
(Click to expand) https://www.rfc-editor.org/rfc/rfc9396.html#appendix-A.3
import { z } from 'zod';
const TaxData = z
.object({
duration_of_access: z.number().int().positive(),
locations: z.array(z.literal('https://taxservice.govehub.no.example.com')).length(1),
actions: z.array(z.literal('read_tax_declaration')).length(1),
periods: z
.array(
z.coerce
.number()
.max(new Date().getFullYear() - 1)
.min(1997)
)
.min(1),
tax_payer_id: z.string().min(1),
})
.strict();
const configuration = {
features: {
richAuthorizationRequests: {
enabled: true,
// ...
types: {
tax_data: {
validate(ctx, detail, client) {
const { success: valid, error } = TaxData.parse(detail);
if (!valid) {
throw new InvalidAuthorizationDetails()
}
}
}
}
}
}
}
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>
<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>
<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>`;
}
OIDC 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:
false
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:
true
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
}
Array of supported Client Authentication methods
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
]
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:
15
ID Token only contains End-User claims when the requested response_type
is id_token
OIDC 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,
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,
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 authorization-endpoint returned opaque access token be bound to the user session. This will be applied to all opaque tokens issued from the authorization code, device code, or subsequent refresh token use in the future. When artifacts are session-bound their originating session will be loaded by its uid
every time they are encountered. Session bound artefacts will effectively get revoked if the end-user logs out.
default value:
async function expiresWithSession(ctx, code) {
return !code.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 available 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 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, backchannel authentication, and pushed authorization request endpoints. These parameters are then available in ctx.oidc.params
as well as passed to interaction session details.
This may also be a plain object with string properties representing parameter names and values being either a function or async function to validate said parameter value. These validators are executed regardless of the parameters' presence or value such that this can be used to validate presence of custom parameters as well as to assign default values for them. If the value is null
or undefined
the parameter is added without a validator. Note that these validators execute near the very end of the request's validation process and changes to (such as assigning default values) other parameters will not trigger any re-validation of the whole request again.
default value:
[]
(Click to expand) registering an extra origin
parameter with its validator
import { errors } from 'oidc-provider';
const extraParams = {
async origin(ctx, value, client) {
// @param ctx - koa request context
// @param value - the `origin` parameter value (string or undefined)
// @param client - client making the request
if (hasDefaultOrigin(client)) {
// assign default
ctx.oidc.params.origin ||= value ||= getDefaultOrigin(client);
}
if (!value && requiresOrigin(ctx, client)) {
// reject when missing but required
throw new errors.InvalidRequest('"origin" is required for this request')
}
if (!allowedOrigin(value, client)) {
// reject when not allowed
throw new errors.InvalidRequest('requested "origin" is not allowed for this client')
}
}
}
Function used to add 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
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 add an arbitrary claim to an Access Token
{
async extraTokenClaims(ctx, token) {
return {
'urn:idp: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
}
(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';
}
}
}
Function called whenever calls to an external HTTP(S) resource are being made. You can change the request timeout through the signal
option, the request agent
used, the user-agent
string used for the user-agent
HTTP header, as well as the dnsLookup
resolver function.
default value:
function httpOptions(url) {
return {
signal: undefined, // defaults to AbortSignal.timeout(2500)
agent: undefined, // defaults to node's global agents (https.globalAgent or http.globalAgent)
dnsLookup: undefined, // defaults to `dns.lookup()` (https://nodejs.org/api/dns.html#dnslookuphostname-options-callback)
'user-agent': undefined, // defaults to not sending the user-agent HTTP header
};
}
(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 { signal: AbortSignal.timeout(5000) };
}
}
Holds the configuration for interaction policy and a 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) {
if (!encounteredScopes.has(scope)) {
missing ||= [];
missing.push(scope);
}
}
if (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) {
if (!encounteredClaims.has(claim) && !['sub', 'sid', 'auth_time', 'acr', 'amr', 'iss'].includes(claim)) {
missing ||= [];
missing.push(claim);
}
}
if (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;
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) {
if (availableScopes.has(scope) && !encounteredScopes.has(scope)) {
missing ||= {};
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] })),
// checks authorization_details
new Check('rar_prompt', 'authorization_details were requested', (ctx) => {
const { oidc } = ctx;
if (oidc.params.authorization_details && (!oidc.result || !('consent' in oidc.result))) {
return Check.REQUEST_PROMPT;
}
return Check.NO_NEED_TO_PROMPT;
}, ({ oidc }) => ({ rar: JSON.parse(oidc.params.authorization_details) })),
)
]
(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
import { interactionPolicy } from 'oidc-provider';
const { Prompt, Check, base } = interactionPolicy;
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.clientAuthMethod === '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?.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 OIDC 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 withoutPKCE
true
to abort the request
default value:
function pkceRequired(ctx, client) {
const fapiProfile = ctx.oidc.isFapi('2.0', '1.0 Final');
switch (true) {
// FAPI 2.0 as per
// https://openid.net/specs/fapi-2_0-security-profile-ID2.html#section-5.3.1.2-2.5.1
case fapiProfile === '2.0':
return true;
// FAPI 1.0 Advanced as per
// https://openid.net/specs/openid-financial-api-part-2-1_0.html#authorization-server
case fapiProfile === '1.0 Final' && ctx.oidc.route === 'pushed_authorization_request':
return true;
// All Public clients as per
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-22#section-2.1.1-2.1.1
case client.clientAuthMethod === 'none':
return true;
// All other cases RECOMMENDED as per
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-22#section-2.1.1-2.2.1
default:
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>
<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 OIDC 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.clientAuthMethod === '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'
]
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.
recommendation: 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) {
return token.resourceServer?.accessTokenTTL || 60 * 60; // 1 hour in seconds
},
AuthorizationCode: 60 /* 1 minute in seconds */,
BackchannelAuthenticationRequest: function BackchannelAuthenticationRequestTTL(ctx, request, client) {
if (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) {
return token.resourceServer?.accessTokenTTL || 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.clientAuthMethod === 'none'
&& !token.isSenderConstrained()
) {
// Non-Sender Constrained SPA RefreshTokens do not have infinite expiration through rotation
return ctx.oidc.entities.RotatedRefreshToken.remainingTTL;
}
return 14 * 24 * 60 * 60; // 14 days in seconds
},
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
JWE "alg" Algorithm values the provider supports for JWT Authorization response (JARM
) encryption
default value:
[
'A128KW',
'A256KW',
'ECDH-ES',
'RSA-OAEP',
'RSA-OAEP-256',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512',
// 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',
// 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 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',
]
JWS "alg" Algorithm values the provider supports to verify signed DPoP proof JWTs with
default value:
[
'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',
'RSA-OAEP-256',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512',
// 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',
// 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',
]
JWE "alg" Algorithm values the provider supports for JWT Introspection response encryption
default value:
[
'A128KW',
'A256KW',
'ECDH-ES',
'RSA-OAEP',
'RSA-OAEP-256',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512',
// 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',
// 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',
]
JWE "alg" Algorithm values the provider supports to receive encrypted Request Objects (JAR
) with
default value:
[
'A128KW',
'A256KW',
'ECDH-ES',
'RSA-OAEP',
'RSA-OAEP-256',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512',
// 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',
// 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',
]
JWE "alg" Algorithm values the provider supports for UserInfo Response encryption
default value:
[
'A128KW',
'A256KW',
'ECDH-ES',
'RSA-OAEP',
'RSA-OAEP-256',
'dir'
]
(Click to expand) Supported values list
[
// asymmetric RSAES based
'RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512',
// 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',
// 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',
]
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://www.rfc-editor.org/rfc/rfc6749.html#section-2.3.1 incl.
https://www.rfc-editor.org/rfc/rfc6749.html#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%2Dstandard+secret'
// Basic auth header format Authorization: Basic base64(encoded_id + ':' + encoded_secret)
// Authorization: Basic YW4lM0FpZGVudGlmaWVyOnNvbWUrc2VjdXJlKyUyNitub24lMkRzdGFuZGFyZCtzZWNyZXQ=
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: []
}