diff --git a/README.md b/README.md index 7a3fba07f..dba3bce52 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ The following draft specifications are implemented by oidc-provider: - [Financial-grade API: Client Initiated Backchannel Authentication Profile (`FAPI-CIBA`) - Implementers Draft 01][fapi-ciba] - [FAPI 2.0 Message Signing (`FAPI 2.0`) - Implementers Draft 01][fapi2ms-id1] - [OIDC Relying Party Metadata Choices 1.0 - Implementers Draft 01][rp-metadata-choices] +- [OAuth 2.0 Attestation-Based Client Authentication][attestation-client-auth] Updates to draft specification versions are released as MINOR library versions, if you utilize these specification implementations consider using the tilde `~` operator in your @@ -168,3 +169,4 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a [Security Policy]: https://github.com/panva/node-oidc-provider/security/policy [rp-metadata-choices]: https://openid.net/specs/openid-connect-rp-metadata-choices-1_0-ID1.html [rfc8414]: https://www.rfc-editor.org/rfc/rfc8414.html +[attestation-client-auth]: https://www.ietf.org/archive/id/draft-ietf-oauth-attestation-based-client-auth-06.html diff --git a/docs/README.md b/docs/README.md index 9bae6c66e..bfc20e337 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,9 +1,12 @@ # oidc-provider API documentation -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](/example) application -is a good starting point to get an idea of what you should provide. +This module provides an OAuth 2.0 Authorization Server implementation with support for OpenID Connect and +additional features conforming to current security best practices and emerging standards. + +The authorization server is designed to be extended and configured in various ways to accommodate a wide variety of +deployment scenarios and use cases. Implementation requires configuring the authorization server instance with account +discovery methods, persistent data storage, and end-user interaction handlers. The [example](/example) application +serves as an excellent starting point to understand the required implementation components. ## Sponsor @@ -70,10 +73,10 @@ External type definitions are available via [DefinitelyTyped](https://npmjs.com/ ## Accounts -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. +The authorization server MUST be able to locate an account and once found the account object MUST contain an +`accountId` property as well as a `claims()` function returning an object with claims that correspond to the claims +the authorization server supports. The provider MUST be configured with an account discovery method by implementing +the `findAccount` function. The `claims()` function MAY return a Promise that is later resolved or rejected. ```js import * as oidc from "oidc-provider"; @@ -92,8 +95,8 @@ const provider = new oidc.Provider("http://localhost:3000", { ## User flows -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: +Since oidc-provider only comes with feature-less views and interaction handlers, implementations MUST provide these +components. The following describes how this module allows such customization: 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 @@ -108,14 +111,14 @@ This interaction session contains: - 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. +The authorization server expects that implementations 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 +Once the required interactions are finished the implementation is 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 +The authorization server instance comes with helpers that aid with getting interaction details as well as packing the results. See them used in the [in-repo](/example) examples. **`provider.interactionDetails(req, res)`** @@ -200,9 +203,9 @@ router.post("/interaction/:uid", async (ctx, next) => { ## Custom Grant Types -oidc-provider comes with the basic grants implemented, but you can register your own grant types, +The authorization server comes with the basic grants implemented, but implementations may register custom grant types, for example to implement an -[OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693.html). You can check the standard +[OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693.html). Implementations can examine the standard grant factories [here](/lib/actions/grants). ```js @@ -470,6 +473,7 @@ location / { - [rpInitiatedLogout](#featuresrpinitiatedlogout) - [userinfo](#featuresuserinfo) - Experimental features: + - [attestClientAuth](#featuresattestclientauth) - [externalSigningSupport (e.g. KMS)](#featuresexternalsigningsupport) - [richAuthorizationRequests](#featuresrichauthorizationrequests) - [rpMetadataChoices](#featuresrpmetadatachoices) @@ -478,7 +482,7 @@ location / { - [allowOmittingSingleRegisteredRedirectUri](#allowomittingsingleregisteredredirecturi) - [assertJwtClientAuthClaimsAndHeader](#assertjwtclientauthclaimsandheader) - [claims ❗](#claims) -- [clientBasedCORS](#clientbasedcors) +- [clientBasedCORS ❗](#clientbasedcors) - [clientDefaults](#clientdefaults) - [clockTolerance](#clocktolerance) - [conformIdTokenClaims](#conformidtokenclaims) @@ -511,11 +515,12 @@ location / { ### adapter -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. +Specifies the storage adapter implementation for persisting authorization server state. The default implementation provides a basic in-memory adapter suitable for development and testing purposes only. When this process is restarted, all stored information will be lost. Production deployments MUST provide a custom adapter implementation that persists data to external storage (e.g., database, Redis, etc.). + The adapter constructor will be instantiated for each model type when first accessed. See: -- [The interface oidc-provider expects](/example/my_adapter.js) +- [The expected interface](/example/my_adapter.js) - [Example MongoDB adapter implementation](https://github.com/panva/node-oidc-provider/discussions/1308) - [Example Redis adapter implementation](https://github.com/panva/node-oidc-provider/discussions/1309) - [Example Redis w/ JSON Adapter](https://github.com/panva/node-oidc-provider/discussions/1310) @@ -524,8 +529,9 @@ See: ### clients -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 authorization server 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. +An array of client metadata objects representing statically configured OAuth 2.0 and OpenID Connect clients. These clients are persistent, do not expire, and remain available throughout the authorization server's lifetime. For dynamic client discovery, the authorization server will invoke the adapter's `find` method when encountering unregistered client identifiers. + To restrict the authorization server to only statically configured clients and disable dynamic registration, configure the adapter to return falsy values for client lookup operations (e.g., `return Promise.resolve()`). + Each client's metadata shall be validated according to the specifications in which the respective properties are defined. @@ -536,14 +542,14 @@ _**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, tls_client_auth_san_dns, tls_client_auth_san_email, tls_client_auth_san_ip, tls_client_auth_san_uri, tls_client_auth_subject_dn, tls_client_certificate_bound_access_tokens, use_mtls_endpoint_aliases, token_endpoint_auth_signing_alg, userinfo_encrypted_response_alg, userinfo_encrypted_response_enc +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 this authorization server'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, tls_client_auth_san_dns, tls_client_auth_san_email, tls_client_auth_san_ip, tls_client_auth_san_uri, tls_client_auth_subject_dn, tls_client_certificate_bound_access_tokens, use_mtls_endpoint_aliases, token_endpoint_auth_signing_alg, userinfo_encrypted_response_alg, userinfo_encrypted_response_enc
### findAccount -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 +Specifies a function that shall be invoked to load an account and retrieve its available claims during authorization server operations. This function enables the authorization server to resolve end-user account information based on the provided account identifier. The function MUST return a Promise that resolves to an account object containing an `accountId` property and a `claims()` method that returns an object with claims corresponding to the claims supported by the issuer. The `claims()` method may also return a Promise that shall be resolved or rejected according to account availability and authorization server policy. _**default value**_: @@ -574,8 +580,8 @@ async function findAccount(ctx, sub, token) { ### jwks -JSON Web Key Set used by the authorization server for signing and decryption. The object must be in [JWK Set format](https://www.rfc-editor.org/rfc/rfc7517.html#section-5). All provided keys must be private keys. - Supported key types are: +Specifies the JSON Web Key Set that shall be used by the authorization server for cryptographic signing and decryption operations. The key set MUST be provided in [JWK Set format](https://www.rfc-editor.org/rfc/rfc7517.html#section-5) as defined in RFC 7517. All keys within the set MUST be private keys. + Supported key types include: - RSA - OKP (Ed25519 and X25519 sub types) - EC (P-256, P-384, and P-521 curves) @@ -592,8 +598,9 @@ _**recommendation**_: The following action order is recommended when rotating si ### features -Enable/disable features. - Some features may be experimental. Enabling those will produce a warning and you must be aware that breaking changes may occur and that those changes will be published as minor versions of oidc-provider. See the example below on how to acknowledge an experimental feature version (this will remove the warning) and ensure the Provider configuration will throw an error if a new version of oidc-provider includes breaking changes to this experimental feature. +Specifies the authorization server feature capabilities that shall be enabled or disabled. This configuration controls the availability of optional OAuth 2.0 and OpenID Connect extensions, experimental specifications, and proprietary enhancements. + Certain features may be designated as experimental implementations. When experimental features are enabled, the authorization server will emit warnings to indicate that breaking changes may occur in future releases. These changes will be published as minor version updates of the oidc-provider module. + To suppress experimental feature warnings and ensure configuration validation against breaking changes, implementations shall acknowledge the specific experimental feature version using the acknowledgment mechanism demonstrated in the example below. When an unacknowledged breaking change is detected, the authorization server configuration will throw an error during instantiation.
(Click to expand) Acknowledging an experimental feature @@ -638,11 +645,87 @@ new oidc.Provider('http://localhost:3000', { ```
+### features.attestClientAuth + +[`draft-ietf-oauth-attestation-based-client-auth-06`](https://www.ietf.org/archive/id/draft-ietf-oauth-attestation-based-client-auth-06.html) - OAuth 2.0 Attestation-Based Client Authentication + +> [!NOTE] +> This is an experimental feature. + +Specifies whether Attestation-Based Client Authentication capabilities shall be enabled. When enabled, the authorization server shall support the `attest_jwt_client_auth` authentication method within the server's `clientAuthMethods` configuration. This mechanism enables Client Instances to authenticate using a Client Attestation JWT issued by a trusted Client Attester and a corresponding Client Attestation Proof-of-Possession JWT generated by the Client Instance. + + + +_**default value**_: +```js +{ + ack: undefined, + assertAttestationJwtAndPop: [AsyncFunction: assertAttestationJwtAndPop], // see expanded details below + challengeSecret: undefined, + enabled: false, + getAttestationSignaturePublicKey: [AsyncFunction: getAttestationSignaturePublicKey] // see expanded details below +} +``` + +
(Click to expand) features.attestClientAuth options details
+ + +#### assertAttestationJwtAndPop + +Specifies a helper function that shall be invoked to perform additional validation of the Client Attestation JWT and Client Attestation Proof-of-Possession JWT beyond the specification requirements. This enables enforcement of extension profiles, deployment-specific policies, or additional security constraints. + At the point of invocation, both JWTs have undergone signature verification and standard validity claim validation. The function may throw errors to reject non-compliant attestations or return successfully to indicate acceptance of the client authentication attempt. + + +_**default value**_: +```js +async function assertAttestationJwtAndPop(ctx, attestation, pop, client) { + // @param ctx - koa request context + // @param attestation - verified and parsed Attestation JWT + // attestation.protectedHeader - parsed protected header object + // attestation.payload - parsed protected header object + // attestation.key - CryptoKey that verified the Attestation JWT signature + // @param pop - verified and parsed Attestation JWT PoP + // pop.protectedHeader - parsed protected header object + // pop.payload - parsed protected header object + // pop.key - CryptoKey that verified the Attestation JWT PoP signature + // @param client - client making the request +} +``` + +#### challengeSecret + +Specifies the cryptographic secret value used for generating server-provided challenges. This value MUST be a 32-byte length Buffer instance to ensure sufficient entropy for secure challenge generation. + + +_**default value**_: +```js +undefined +``` + +#### getAttestationSignaturePublicKey + +Specifies a helper function that shall be invoked to verify the issuer identifier of a Client Attestation JWT and retrieve the public key used for signature verification. At the point of this function's invocation, only the JWT format has been validated; no cryptographic or claims verification has occurred. + The function MUST return a public key in one of the supported formats: CryptoKey, KeyObject, or JSON Web Key (JWK) representation. The authorization server shall use this key to verify the Client Attestation JWT signature. + + +_**default value**_: +```js +async function getAttestationSignaturePublicKey(ctx, iss, header, client) { + // @param ctx - koa request context + // @param iss - Issuer Identifier from the Client Attestation JWT + // @param header - Protected Header of the Client Attestation JWT + // @param client - client making the request + throw new Error('features.attestClientAuth.getAttestationSignaturePublicKey not implemented'); +} +``` + +
+ ### features.backchannelLogout [`OIDC Back-Channel Logout 1.0`](https://openid.net/specs/openid-connect-backchannel-1_0-final.html) -Enables Back-Channel Logout features. +Specifies whether Back-Channel Logout capabilities shall be enabled. When enabled, the authorization server shall support propagating end-user logouts initiated by relying parties to clients that were involved throughout the lifetime of the terminated session. _**default value**_: @@ -681,9 +764,9 @@ _**default value**_: #### deliveryModes -Fine-tune the supported token delivery modes. Supported values are - - `poll` - - `ping` +Specifies the token delivery modes supported by this authorization server. The following delivery modes are defined: + - `poll` - Client polls the token endpoint for completion + - `ping` - Authorization server notifies client of completion via HTTP callback @@ -696,12 +779,12 @@ _**default value**_: #### processLoginHint -Helper function used to process the login_hint parameter and return the accountId value to use for processsing the request. +Specifies a helper function that shall be invoked to process the `login_hint` parameter and extract the corresponding accountId value for request processing. This function MUST validate the hint format and content according to authorization server policy. -_**recommendation**_: Use `throw new errors.InvalidRequest('validation error message')` when login_hint is invalid. +_**recommendation**_: Use `throw new errors.InvalidRequest('validation error message')` when the login_hint format or content is invalid. -_**recommendation**_: Use `return undefined` or when you can't determine the accountId from the login_hint. +_**recommendation**_: Use `return undefined` when the accountId cannot be determined from the provided login_hint. _**default value**_: @@ -715,14 +798,14 @@ async function processLoginHint(ctx, loginHint) { #### processLoginHintToken -Helper function used to process the login_hint_token parameter and return the accountId value to use for processsing the request. +Specifies a helper function that shall be invoked to process the `login_hint_token` parameter and extract the corresponding accountId value for request processing. This function MUST validate token expiration and format according to authorization server policy. -_**recommendation**_: Use `throw new errors.ExpiredLoginHintToken('validation error message')` when login_hint_token is expired. +_**recommendation**_: Use `throw new errors.ExpiredLoginHintToken('validation error message')` when the login_hint_token has expired. -_**recommendation**_: Use `throw new errors.InvalidRequest('validation error message')` when login_hint_token is invalid. +_**recommendation**_: Use `throw new errors.InvalidRequest('validation error message')` when the login_hint_token format or content is invalid. -_**recommendation**_: Use `return undefined` or when you can't determine the accountId from the login_hint. +_**recommendation**_: Use `return undefined` when the accountId cannot be determined from the provided login_hint_token. _**default value**_: @@ -736,8 +819,8 @@ async function processLoginHintToken(ctx, loginHintToken) { #### triggerAuthenticationDevice -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. +Specifies a helper function that shall be invoked to initiate authentication and authorization processes on the end-user's Authentication Device as defined in the CIBA specification. This function is executed after accepting the backchannel authentication request but before transmitting the response to the requesting client. + Upon successful end-user authentication, implementations shall use `provider.backchannelResult()` to complete the Consumption Device login process. @@ -767,19 +850,19 @@ await provider.backchannelResult(...); - `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. + - `options.authTime?`: number - Time when the end-user authentication occurred. #### validateBindingMessage -Helper function used to process the binding_message parameter and throw if its not following the authorization server's policy. +Specifies a helper function that shall be invoked to validate the `binding_message` parameter according to authorization server policy. This function MUST reject invalid binding messages by throwing appropriate error instances. -_**recommendation**_: Use `throw new errors.InvalidBindingMessage('validation error message')` when the binding_message is invalid. +_**recommendation**_: Use `throw new errors.InvalidBindingMessage('validation error message')` when the binding_message violates authorization server policy. -_**recommendation**_: Use `return undefined` when a binding_message isn't required and wasn't provided. +_**recommendation**_: Use `return undefined` when a binding_message is not required by policy and was not provided in the request. _**default value**_: @@ -797,12 +880,12 @@ async function validateBindingMessage(ctx, bindingMessage) { #### validateRequestContext -Helper function used to process the request_context parameter and throw if its not following the authorization server's policy. +Specifies a helper function that shall be invoked to validate the `request_context` parameter according to authorization server policy. This function MUST enforce policy requirements for request context validation and reject non-compliant requests. -_**recommendation**_: Use `throw new errors.InvalidRequest('validation error message')` when the request_context is required by policy and missing or invalid. +_**recommendation**_: Use `throw new errors.InvalidRequest('validation error message')` when the request_context is required by policy but missing or invalid. -_**recommendation**_: Use `return undefined` when a request_context isn't required and wasn't provided. +_**recommendation**_: Use `return undefined` when a request_context is not required by policy and was not provided in the request. _**default value**_: @@ -816,14 +899,14 @@ async function validateRequestContext(ctx, requestContext) { #### verifyUserCode -Helper function used to verify the user_code parameter value is present when required and verify its value. +Specifies a helper function that shall be invoked to verify the presence and validity of the `user_code` parameter when required by authorization server policy. -_**recommendation**_: Use `throw new errors.MissingUserCode('validation error message')` when user_code should have been provided but wasn't. +_**recommendation**_: Use `throw new errors.MissingUserCode('validation error message')` when user_code is required by policy but was not provided. -_**recommendation**_: Use `throw new errors.InvalidUserCode('validation error message')` when the provided user_code is invalid. +_**recommendation**_: Use `throw new errors.InvalidUserCode('validation error message')` when the provided user_code value is invalid or does not meet policy requirements. -_**recommendation**_: Use `return undefined` when no user_code was provided and isn't required. +_**recommendation**_: Use `return undefined` when no user_code was provided and it is not required by authorization server policy. _**default value**_: @@ -842,7 +925,7 @@ async function verifyUserCode(ctx, account, userCode) { [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#ClaimsParameter) - Requesting Claims using the "claims" Request Parameter -Enables the use and validations of `claims` parameter as described in the specification. +Specifies whether the `claims` request parameter shall be enabled for authorization requests. When enabled, the authorization server shall accept and process the `claims` parameter to enable fine-grained control over which claims are returned in ID Tokens and from the UserInfo Endpoint. @@ -859,7 +942,8 @@ _**default value**_: #### assertClaimsParameter -Helper function used to validate the claims parameter beyond what the OpenID Connect 1.0 specification requires. +Specifies a helper function that shall be invoked to perform additional validation of the `claims` parameter. This function enables enforcement of deployment-specific policies, security constraints, or extended claim validation logic according to authorization server requirements. + The function may throw errors to reject non-compliant claims requests or return successfully to indicate acceptance of the claims parameter content. _**default value**_: @@ -877,7 +961,7 @@ async function assertClaimsParameter(ctx, claims, client) { [`RFC6749`](https://www.rfc-editor.org/rfc/rfc6749.html#section-1.3.4) - Client Credentials -Enables `grant_type=client_credentials` to be used on the token endpoint. +Specifies whether the Client Credentials grant type shall be enabled. When enabled, the authorization server shall accept `grant_type=client_credentials` requests at the token endpoint, allowing clients to obtain access tokens. _**default value**_: @@ -891,7 +975,7 @@ _**default value**_: [`RFC9449`](https://www.rfc-editor.org/rfc/rfc9449.html) - 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. +Enables sender-constraining of OAuth 2.0 tokens through application-level proof-of-possession mechanisms. _**default value**_: @@ -909,7 +993,7 @@ _**default value**_: #### allowReplay -Controls whether DPoP Proof Replay is allowed or not. +Specifies whether DPoP Proof replay shall be permitted by the authorization server. When set to false, the server enforces strict replay protection by rejecting previously used DPoP proofs, enhancing security against replay attacks. _**default value**_: @@ -919,7 +1003,7 @@ false #### nonceSecret -A secret value used for generating server-provided DPoP nonces. Must be a 32-byte length Buffer instance when provided. +Specifies the cryptographic secret value used for generating server-provided DPoP nonces. When provided, this value MUST be a 32-byte length Buffer instance to ensure sufficient entropy for secure nonce generation. _**default value**_: @@ -929,7 +1013,7 @@ undefined #### requireNonce -Function used to determine whether a DPoP nonce is required or not. +Specifies a function that determines whether a DPoP nonce shall be required for proof-of-possession validation in the current request context. This function is invoked during DPoP proof validation to enforce nonce requirements based on authorization server policy. _**default value**_: @@ -943,8 +1027,8 @@ function requireNonce(ctx) { ### features.devInteractions -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. +Enables development-only interaction views that provide pre-built user interface components for rapid prototyping and testing of authorization flows. These views accept any username (used as the subject claim value) and any password for authentication, bypassing production-grade security controls. + Production deployments MUST disable this feature and implement proper end-user authentication and authorization mechanisms. These development views MUST NOT be used in production environments as they provide no security guarantees and accept arbitrary credentials. _**default value**_: @@ -958,7 +1042,7 @@ _**default value**_: [`RFC8628`](https://www.rfc-editor.org/rfc/rfc8628.html) - OAuth 2.0 Device Authorization Grant (`Device Flow`) -Enables Device Authorization Grant +Specifies whether the OAuth 2.0 Device Authorization Grant shall be enabled. When enabled, the authorization server shall support the device authorization flow, enabling OAuth clients on input-constrained devices to obtain user authorization by directing the user to perform the authorization flow on a secondary device with richer input and display capabilities. _**default value**_: @@ -979,9 +1063,9 @@ _**default value**_: #### charset -alias for a character set of the generated user codes. Supported values are - - `base-20` uses BCDFGHJKLMNPQRSTVWXZ - - `digits` uses 0123456789 +Specifies the character set used for generating user codes in the device authorization flow. This configuration determines the alphabet from which user codes are constructed. Supported values include: + - `base-20` - Uses characters BCDFGHJKLMNPQRSTVWXZ (excludes easily confused characters) + - `digits` - Uses characters 0123456789 (numeric only) _**default value**_: @@ -991,7 +1075,7 @@ _**default value**_: #### deviceInfo -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. +Specifies a helper function that shall be invoked to extract device-specific information from device authorization endpoint requests. The extracted information becomes available during the end-user confirmation screen to assist users in verifying that the authorization request originated from a device in their possession. This enhances security by enabling users to confirm device identity before granting authorization. _**default value**_: @@ -1006,7 +1090,7 @@ function deviceInfo(ctx) { #### mask -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. +Specifies the template pattern used for generating user codes in the device authorization flow. The authorization server shall replace `*` characters with random characters from the configured charset, while `-` (dash) and ` ` (space) characters may be included for enhanced readability. Refer to RFC 8628 for guidance on minimal recommended entropy requirements for user code generation. _**default value**_: @@ -1016,7 +1100,7 @@ _**default value**_: #### successSource -HTML source rendered when device code feature renders a success page for the User-Agent. +Specifies the HTML source that shall be rendered when the device flow feature displays a success page to the User-Agent. This template is presented upon successful completion of the device authorization flow to inform the end-user that authorization has been granted to the requesting device. _**default value**_: @@ -1041,7 +1125,7 @@ async function successSource(ctx) { #### userCodeConfirmSource -HTML source rendered when device code feature renders an a confirmation prompt for ther User-Agent. +Specifies the HTML source that shall be rendered when the device flow feature displays a confirmation prompt to the User-Agent. This template is presented after successful user code validation to confirm the authorization request before proceeding with the device authorization flow. _**default value**_: @@ -1082,7 +1166,7 @@ async function userCodeConfirmSource(ctx, form, client, deviceInfo, userCode) { #### userCodeInputSource -HTML source rendered when device code feature renders an input prompt for the User-Agent. +Specifies the HTML source that shall be rendered when the device flow feature displays a user code input prompt to the User-Agent. This template is presented during the device authorization flow when the authorization server requires the end-user to enter a device-generated user code for verification. _**default value**_: @@ -1127,7 +1211,7 @@ async function userCodeInputSource(ctx, form, out, err) { ### features.encryption -Enables encryption features such as receiving encrypted UserInfo responses, encrypted ID Tokens and allow receiving encrypted Request Objects. +Specifies whether encryption capabilities shall be enabled. When enabled, the authorization server shall support accepting and issuing encrypted tokens involved in its other enabled capabilities. _**default value**_: @@ -1144,7 +1228,7 @@ External Signing Support > [!NOTE] > This is an experimental feature. -Enables the use of the exported `ExternalSigningKey` class instances in place of a Private JWK in the `jwks.keys` configuration array. This allows Digital Signature Algorithm (such as PS256, ES256, or others) signatures to be produced externally, for example via a KMS service or an HSM. +Specifies whether external signing capabilities shall be enabled. When enabled, the authorization server shall support the use of `ExternalSigningKey` class instances in place of private JWK entries within the `jwks.keys` configuration array. This feature enables Digital Signature Algorithm operations (such as PS256, ES256, or other supported algorithms) to be performed by external cryptographic services, including Key Management Services (KMS) and Hardware Security Modules (HSM), providing enhanced security for private key material through externalized signing operations. See [KMS integration with AWS Key Management Service](https://github.com/panva/node-oidc-provider/discussions/1316) @@ -1159,9 +1243,9 @@ _**default value**_: ### features.fapi -Financial-grade API Security Profile (`FAPI`) +FAPI Security Profiles (`FAPI`) -Enables extra Authorization Server behaviours defined in FAPI that cannot be achieved by other configuration options. +Specifies whether FAPI Security Profile capabilities shall be enabled. When enabled, the authorization server shall implement additional security behaviors defined in FAPI specifications that cannot be achieved through other configuration options. _**default value**_: @@ -1177,10 +1261,10 @@ _**default value**_: #### profile -The specific profile of `FAPI` to enable. Supported values are: - - '2.0' Enables behaviours from [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-security-profile-2_0-final.html) - - '1.0 Final' Enables behaviours from [FAPI 1.0 Security Profile - Part 2: Advanced](https://openid.net/specs/openid-financial-api-part-2-1_0-final.html) - - 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. +Specifies the FAPI profile version that shall be applied for security policy enforcement. The authorization server shall implement the behaviors defined in the selected profile specification. Supported values include: + - '2.0' - The authorization server shall implement behaviors from [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-security-profile-2_0-final.html) + - '1.0 Final' - The authorization server shall implement behaviors from [FAPI 1.0 Security Profile - Part 2: Advanced](https://openid.net/specs/openid-financial-api-part-2-1_0-final.html) + - Function - A function that shall be invoked with arguments `(ctx, client)` to determine the profile contextually. The function shall return one of the supported profile values or undefined when FAPI behaviors should be ignored for the current request context. _**default value**_: @@ -1194,9 +1278,9 @@ undefined [`RFC7662`](https://www.rfc-editor.org/rfc/rfc7662.html) - OAuth 2.0 Token Introspection -Enables Token Introspection for: - - opaque access tokens - - refresh tokens +Specifies whether OAuth 2.0 Token Introspection capabilities shall be enabled. When enabled, the authorization server shall expose a token introspection endpoint that allows authorized clients and resource servers to query the metadata and status of the following token types: + - Opaque access tokens + - Refresh tokens @@ -1213,7 +1297,7 @@ _**default value**_: #### allowedPolicy -Helper function used to determine whether the client/RS (client argument) is allowed to introspect the given token (token argument). +Specifies a helper function that shall be invoked to determine whether the requesting client or resource server is authorized to introspect the specified token. This function enables enforcement of fine-grained access control policies for token introspection operations according to authorization server security requirements. _**default value**_: @@ -1238,7 +1322,7 @@ async function introspectionAllowedPolicy(ctx, client, token) { [`RFC9701`](https://www.rfc-editor.org/rfc/rfc9701.html) - JWT Response for OAuth Token Introspection -Enables JWT responses for Token Introspection features +Specifies whether JWT-formatted token introspection responses shall be enabled. When enabled, the authorization server shall support issuing introspection responses as JSON Web Tokens, providing enhanced security and integrity protection for token metadata transmission between authorized parties. _**default value**_: @@ -1252,7 +1336,7 @@ _**default value**_: [JWT Secured Authorization Response Mode (`JARM`)](https://openid.net/specs/oauth-v2-jarm-final.html) -Enables JWT Secured Authorization Responses +Specifies whether JWT Secured Authorization Response Mode capabilities shall be enabled. When enabled, the authorization server shall support encoding authorization responses as JSON Web Tokens, providing cryptographic protection and integrity assurance for authorization response parameters. _**default value**_: @@ -1266,7 +1350,7 @@ _**default value**_: [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#UserInfo) - 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. +Specifies whether JWT-formatted UserInfo endpoint responses shall be enabled. When enabled, the authorization server shall support returning UserInfo responses as signed and/or encrypted JSON Web Tokens, providing enhanced security and integrity protection for end-user claims transmission. This feature shall also enable the relevant client metadata parameters for configuring JWT signing and/or encryption algorithms according to client requirements. _**default value**_: @@ -1280,7 +1364,7 @@ _**default value**_: [`RFC8705`](https://www.rfc-editor.org/rfc/rfc8705.html) - 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. +Specifies whether Mutual TLS capabilities shall be enabled. The authorization server supports three distinct features that require separate configuration settings within this feature's configuration object. Implementations MUST provide deployment-specific helper functions for certificate validation and processing operations. @@ -1302,7 +1386,7 @@ _**default value**_: #### certificateAuthorized -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. +Specifies a helper function that shall be invoked to determine whether the client certificate used in the request is verified and originates from a trusted Certificate Authority for the requesting client. This function MUST return a boolean value indicating certificate authorization status. This validation is exclusively used for the `tls_client_auth` client authentication method. _**default value**_: @@ -1314,7 +1398,7 @@ function certificateAuthorized(ctx) { #### certificateBoundAccessTokens -Enables section 3 & 4 Mutual TLS Client Certificate-Bound Tokens by exposing the client's `tls_client_certificate_bound_access_tokens` metadata property. +Specifies whether Certificate-Bound Access Tokens shall be enabled as defined in RFC 8705 sections 3 and 4. When enabled, the authorization server shall expose the client's `tls_client_certificate_bound_access_tokens` metadata property for mutual TLS certificate binding functionality. _**default value**_: @@ -1324,7 +1408,7 @@ false #### certificateSubjectMatches -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. +Specifies a helper function that shall be invoked to determine whether the client certificate subject used in the request matches the registered client property according to authorization server policy. This function MUST return a boolean value indicating subject matching status. This validation is exclusively used for the `tls_client_auth` client authentication method. _**default value**_: @@ -1336,7 +1420,7 @@ function certificateSubjectMatches(ctx, property, expected) { #### getCertificate -Function used to retrieve a `crypto.X509Certificate` instance, or a PEM-formatted string, representation of client certificate used in the request. +Specifies a helper function that shall be invoked to retrieve the client certificate used in the current request. This function MUST return either a `crypto.X509Certificate` instance or a PEM-formatted string representation of the client certificate for mutual TLS processing. _**default value**_: @@ -1348,7 +1432,7 @@ function getCertificate(ctx) { #### selfSignedTlsClientAuth -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. +Specifies whether Self-Signed Certificate Mutual TLS client authentication shall be enabled as defined in RFC 8705 section 2.2. When enabled, the authorization server shall support the `self_signed_tls_client_auth` authentication method within the server's `clientAuthMethods` configuration. _**default value**_: @@ -1358,7 +1442,7 @@ false #### tlsClientAuth -Enables section 2.1. PKI Mutual TLS client authentication method `tls_client_auth` for use in the server's `clientAuthMethods` configuration. +Specifies whether PKI Mutual TLS client authentication shall be enabled as defined in RFC 8705 section 2.1. When enabled, the authorization server shall support the `tls_client_auth` authentication method within the server's `clientAuthMethods` configuration. _**default value**_: @@ -1372,7 +1456,7 @@ false [`RFC9126`](https://www.rfc-editor.org/rfc/rfc9126.html) - OAuth 2.0 Pushed Authorization Requests (`PAR`) -Enables the use of `pushed_authorization_request_endpoint` defined by the Pushed Authorization Requests RFC. +Specifies whether Pushed Authorization Request capabilities shall be enabled. When enabled, the authorization server shall expose a pushed authorization request endpoint that allows clients to lodge authorization request parameters at the authorization server prior to redirecting end-users to the authorization endpoint, enhancing security by removing the need to transmit parameters via query string parameters. _**default value**_: @@ -1389,7 +1473,7 @@ _**default value**_: #### allowUnregisteredRedirectUris -Allows unregistered redirect_uri values to be used by authenticated clients using PAR that do not use a `sector_identifier_uri`. +Specifies whether unregistered redirect_uri values shall be permitted for authenticated clients using PAR that do not utilize a sector_identifier_uri. This configuration enables dynamic redirect URI specification within the security constraints of the pushed authorization request mechanism. _**default value**_: @@ -1399,7 +1483,7 @@ false #### requirePushedAuthorizationRequests -Makes the use of `PAR` required for all authorization requests as an authorization server policy. +Specifies whether PAR usage shall be mandatory for all authorization requests as an authorization server security policy. When enabled, the authorization server shall reject authorization endpoint requests that do not utilize the pushed authorization request mechanism. _**default value**_: @@ -1413,7 +1497,7 @@ false [`OIDC Dynamic Client Registration 1.0`](https://openid.net/specs/openid-connect-registration-1_0-errata2.html) and [`RFC7591`](https://www.rfc-editor.org/rfc/rfc7591.html) - OAuth 2.0 Dynamic Client Registration Protocol -Enables Dynamic Client Registration. +Specifies whether Dynamic Client Registration capabilities shall be enabled. When enabled, the authorization server shall expose a client registration endpoint that allows clients to dynamically register themselves with the authorization server at runtime, enabling automated client onboarding and configuration management. _**default value**_: @@ -1433,7 +1517,7 @@ _**default value**_: #### idFactory -Function used to generate random client identifiers during dynamic client registration +Specifies a helper function that shall be invoked to generate random client identifiers during dynamic client registration operations. This function enables customization of client identifier generation according to authorization server requirements and conventions. _**default value**_: @@ -1445,9 +1529,9 @@ function idFactory(ctx) { #### initialAccessToken -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 token - - `boolean` true/false to enable/disable adapter backed initial access tokens +Specifies whether the registration endpoint shall require an initial access token as authorization for client registration requests. This configuration controls access to the dynamic registration functionality. Supported values include: + - `string` - The authorization server shall validate the provided bearer token against this static initial access token value + - `boolean` - When true, the authorization server shall require adapter-backed initial access tokens; when false, registration requests are processed without initial access tokens. @@ -1465,10 +1549,10 @@ new (provider.InitialAccessToken)({}).save().then(console.log); #### issueRegistrationAccessToken -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 issued - - `false` registration access tokens is not issued - - function returning true/false, true when token should be issued, false when it shouldn't +Specifies whether a registration access token shall be issued upon successful client registration. This configuration determines if clients receive tokens for subsequent registration management operations. Supported values include: + - `true` - Registration access tokens shall be issued for all successful registrations + - `false` - Registration access tokens shall not be issued + - Function - A function that shall be invoked to dynamically determine token issuance based on request context and authorization server policy @@ -1489,12 +1573,12 @@ async issueRegistrationAccessToken(ctx) { #### policies -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. +Specifies registration and registration management policies that shall be applied to client metadata properties during dynamic registration operations. Policies are synchronous or asynchronous functions assigned to Initial Access Tokens that execute before standard client property validations. Multiple policies may be assigned to an Initial Access Token, and by default, the same policies shall transfer to the Registration Access Token. Policy functions may throw errors to reject registration requests or modify the client properties object before validation. -_**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**_: Referenced policies MUST always be present when encountered on a token; an AssertionError will be thrown inside the request context if a policy is not found, 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 +_**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: ```js // inside your final ran policy ctx.oidc.entities.RegistrationAccessToken.policies = ['update-policy']; @@ -1544,7 +1628,7 @@ new (provider.InitialAccessToken)({ policies: ['my-policy', 'my-policy-2'] }).sa #### secretFactory -Function used to generate random client secrets during dynamic client registration +Specifies a helper function that shall be invoked to generate random client secrets during dynamic client registration operations. This function enables customization of client secret generation according to authorization server security requirements and entropy specifications. _**default value**_: @@ -1560,7 +1644,7 @@ async function secretFactory(ctx) { [`RFC7592`](https://www.rfc-editor.org/rfc/rfc7592.html) - OAuth 2.0 Dynamic Client Registration Management Protocol -Enables Update and Delete features described in the RFC +Specifies whether Dynamic Client Registration Management capabilities shall be enabled. When enabled, the authorization server shall expose Update and Delete operations as defined in RFC 7592, allowing clients to modify or remove their registration entries using Registration Access Tokens for client lifecycle management operations. _**default value**_: @@ -1576,17 +1660,19 @@ _**default value**_: #### rotateRegistrationAccessToken -Enables registration access token rotation. The authorization server 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 rotated - - `true` registration access tokens are rotated when used - - function returning true/false, true when rotation should occur, false when it shouldn't +Specifies whether registration access token rotation shall be enabled as a security policy for client registration management operations. When token rotation is active, the authorization server shall discard the current Registration Access Token upon successful update operations and issue a new token, returning it to the client with the Registration Update Response. + Supported values include: + - `false` - Registration access tokens shall not be rotated and remain valid after use + - `true` - Registration access tokens shall be rotated when used for management operations + - Function - A function that shall be invoked to dynamically determine whether rotation should occur based on request context and authorization server policy + _**default value**_: ```js true ``` -
(Click to expand) function use +
(Click to expand) Dynamic token rotation policy implementation
```js @@ -1611,7 +1697,7 @@ true [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#RequestObject) and [`RFC9101`](https://www.rfc-editor.org/rfc/rfc9101.html#name-passing-a-request-object-by) - Passing a Request Object by Value (`JAR`) -Enables the use and validations of the `request` parameter. +Specifies whether Request Object capabilities shall be enabled. When enabled, the authorization server shall support the use and validation of the `request` parameter for conveying authorization request parameters as JSON Web Tokens, providing enhanced security and integrity protection for authorization requests. _**default value**_: @@ -1628,7 +1714,7 @@ _**default value**_: #### assertJwtClaimsAndHeader -Helper function used to validate the Request Object JWT Claims Set and Header beyond what the JAR specification requires. +Specifies a helper function that shall be invoked to perform additional validation of the Request Object JWT Claims Set and Header beyond the standard JAR specification requirements. This function enables enforcement of deployment-specific policies, security constraints, or extended validation logic according to authorization server requirements. _**default value**_: @@ -1666,7 +1752,7 @@ async function assertJwtClaimsAndHeader(ctx, claims, header, client) { #### requireSignedRequestObject -Makes the use of signed request objects required for all authorization requests as an authorization server policy. +Specifies whether the use of signed request objects shall be mandatory for all authorization requests as an authorization server security policy. When enabled, the authorization server shall reject authorization requests that do not include a signed Request Object JWT. _**default value**_: @@ -1680,16 +1766,17 @@ false [`RFC8707`](https://www.rfc-editor.org/rfc/rfc8707.html) - 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). +Specifies whether Resource Indicator capabilities shall be enabled. When enabled, the authorization server shall support the `resource` parameter at the authorization and token endpoints to enable issuing Access Tokens for specific Resource Servers (APIs) with enhanced audience control and scope management. + The authorization server implements the following resource indicator processing rules: - 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. + - 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 the `defaultResource` helper). An Access Token for the provided/resolved 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 the `defaultResource` 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`). + - Issued Access Tokens shall always only contain scopes that are defined on the respective Resource Server (returned from `features.resourceIndicators.getResourceServerInfo`). _**default value**_: @@ -1707,7 +1794,7 @@ _**default value**_: #### defaultResource -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. +Specifies a helper function that shall be invoked to determine the default resource indicator for a request when none is provided by the client during the authorization request or when multiple resources are provided/resolved and only a single one is required during an Access Token Request. This function enables authorization server policy-based resource selection according to deployment requirements. _**default value**_: @@ -1726,10 +1813,10 @@ async function defaultResource(ctx, client, oneOf) { #### getResourceServerInfo -Function used to load information about a Resource Server (API) and check if the client is meant to request scopes for that particular resource. +Specifies a helper function that shall be invoked to load information about a Resource Server (API) and determine whether the client is authorized to request scopes for that particular resource. This function enables resource-specific scope validation and Access Token configuration according to authorization server policy. -_**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. +_**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**_: @@ -1822,12 +1909,12 @@ async function getResourceServerInfo(ctx, resourceIndicator, client) { #### useGrantedResource -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. +Specifies a helper function that shall be invoked to determine whether an already granted resource indicator should be used without being explicitly requested by the client during the Token Endpoint request. This function enables flexible resource selection policies for token issuance operations. -_**recommendation**_: Use `return true` when it's allowed for a client skip providing the "resource" parameter at the Token Endpoint. +_**recommendation**_: Use `return true` when it's allowed for a client to 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. +_**recommendation**_: Use `return false` (default) when it's required for a client to explicitly provide a "resource" parameter at the Token Endpoint or when other indication dictates an Access Token for the UserInfo Endpoint should be returned. _**default value**_: @@ -1846,9 +1933,9 @@ async function useGrantedResource(ctx, model) { [`RFC7009`](https://www.rfc-editor.org/rfc/rfc7009.html) - OAuth 2.0 Token Revocation -Enables Token Revocation for: - - opaque access tokens - - refresh tokens +Specifies whether Token Revocation capabilities shall be enabled. When enabled, the authorization server shall expose a token revocation endpoint that allows authorized clients and resource servers to notify the authorization server that a particular token is no longer needed. This feature supports revocation of the following token types: + - Opaque access tokens + - Refresh tokens @@ -1865,7 +1952,7 @@ _**default value**_: #### allowedPolicy -Helper function used to determine whether the client/RS (client argument) is allowed to revoke the given token (token argument). +Specifies a helper function that shall be invoked to determine whether the requesting client or resource server is authorized to revoke the specified token. This function enables enforcement of fine-grained access control policies for token revocation operations according to authorization server security requirements. _**default value**_: @@ -1894,7 +1981,7 @@ async function revocationAllowedPolicy(ctx, client, token) { > [!NOTE] > This is an experimental feature. -Enables the use of `authorization_details` parameter for the authorization and token endpoints to enable issuing Access Tokens with fine-grained authorization data. +Specifies whether Rich Authorization Request capabilities shall be enabled. When enabled, the authorization server shall support the `authorization_details` parameter at the authorization and token endpoints to enable issuing Access Tokens with fine-grained authorization data and enhanced authorization scope control. _**default value**_: @@ -1915,7 +2002,7 @@ _**default value**_: #### rarForAuthorizationCode -Function used to transform the requested and granted RAR details that are then stored in the authorization code. Return array of details or undefined. +Specifies a helper function that shall be invoked to transform the requested and granted Rich Authorization Request details for storage in the authorization code. This function enables filtering and processing of authorization details according to authorization server policy before code persistence. The function shall return an array of authorization details or undefined. _**default value**_: @@ -1934,7 +2021,7 @@ rarForAuthorizationCode(ctx) { #### rarForCodeResponse -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. +Specifies a helper function that shall be invoked to transform the requested and granted Rich Authorization Request details for inclusion in the Access Token Response as authorization_details and assignment to the issued Access Token. This function enables resource-specific filtering and transformation of authorization details according to token endpoint policy. The function shall return an array of authorization details or undefined. _**default value**_: @@ -1954,7 +2041,7 @@ rarForCodeResponse(ctx, resourceServer) { #### rarForIntrospectionResponse -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. +Specifies a helper function that shall be invoked to transform the token's stored Rich Authorization Request details for inclusion in the Token Introspection Response. This function enables filtering and processing of authorization details according to introspection endpoint policy and requesting party authorization. The function shall return an array of authorization details or undefined. _**default value**_: @@ -1973,7 +2060,7 @@ rarForIntrospectionResponse(ctx, token) { #### rarForRefreshTokenResponse -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. +Specifies a helper function that shall be invoked to transform the requested and granted Rich Authorization Request details for inclusion in the Access Token Response during refresh token exchanges as authorization_details and assignment to the newly issued Access Token. This function enables resource-specific processing of previously granted authorization details according to refresh token policy. The function shall return an array of authorization details or undefined. _**default value**_: @@ -1993,7 +2080,7 @@ rarForRefreshTokenResponse(ctx, resourceServer) { #### types -Supported authorization details type identifiers. +Specifies the authorization details type identifiers that shall be supported by the authorization server. Each type identifier MUST have an associated validation function that defines the required structure and constraints for authorization details of that specific type according to authorization server policy. @@ -2001,7 +2088,7 @@ _**default value**_: ```js {} ``` -
(Click to expand) https://www.rfc-editor.org/rfc/rfc9396.html#appendix-A.3 +
(Click to expand) Authorization details type validation for tax data access
```js @@ -2056,7 +2143,7 @@ const configuration = { [`OIDC RP-Initiated Logout 1.0`](https://openid.net/specs/openid-connect-rpinitiated-1_0-final.html) -Enables RP-Initiated Logout features +Specifies whether RP-Initiated Logout capabilities shall be enabled. When enabled, the authorization server shall support logout requests initiated by relying parties, allowing clients to request termination of end-user sessions. _**default value**_: @@ -2073,7 +2160,7 @@ _**default value**_: #### logoutSource -HTML source rendered when RP-Initiated Logout renders a confirmation prompt for the User-Agent. +Specifies the HTML source that shall be rendered when RP-Initiated Logout displays a confirmation prompt to the User-Agent. This template shall be presented to request explicit end-user confirmation before proceeding with the logout operation, ensuring user awareness and consent for session termination. _**default value**_: @@ -2102,7 +2189,7 @@ async function logoutSource(ctx, form) { #### postLogoutSuccessSource -HTML source rendered when RP-Initiated Logout concludes a logout but there was no `post_logout_redirect_uri` provided by the client. +Specifies the HTML source that shall be rendered when an RP-Initiated Logout request concludes successfully but no `post_logout_redirect_uri` was provided by the requesting client. This template shall be presented to inform the end-user that the logout operation has completed successfully and provide appropriate post-logout guidance. _**default value**_: @@ -2135,7 +2222,7 @@ async function postLogoutSuccessSource(ctx) { > [!NOTE] > This is an experimental feature. -Enables the use of the following multi-valued input parameters metadata from the Relying Party Metadata Choices draft assuming their underlying feature is also enabled: +Specifies whether Relying Party Metadata Choices capabilities shall be enabled. When enabled, the authorization server shall support the following multi-valued input parameters metadata from the Relying Party Metadata Choices draft, provided that their underlying feature is also enabled: - subject_types_supported - id_token_signing_alg_values_supported - id_token_encryption_alg_values_supported @@ -2169,7 +2256,7 @@ _**default value**_: [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#UserInfo) - 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. +Specifies whether the UserInfo Endpoint shall be enabled. When enabled, the authorization server shall expose a UserInfo endpoint that returns claims about the authenticated end-user. Access to this endpoint requires an opaque Access Token with at least `openid` scope that does not have a Resource Server audience. _**default value**_: @@ -2186,7 +2273,7 @@ _**default value**_: > [!NOTE] > This is an experimental feature. -Enables `web_message` response mode. Only Simple Mode is supported. Requests containing the Relay Mode parameters will be rejected. +Specifies whether Web Message Response Mode capabilities shall be enabled. When enabled, the authorization server shall support the `web_message` response mode for returning authorization responses via HTML5 Web Messaging. The implementation shall support only Simple Mode operation; authorization requests containing Relay Mode parameters will be rejected. _**recommendation**_: Although a general advise to use a `helmet` (e.g. for [express](https://www.npmjs.com/package/helmet), [koa](https://www.npmjs.com/package/koa-helmet)) it is especially advised for your interaction views routes if Web Message Response Mode is enabled in your deployment. You will have to experiment with removal of the Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy headers at various endpoints throughout the authorization request end-user journey to finalize this feature. @@ -2202,7 +2289,7 @@ _**default value**_: ### acceptQueryParamAccessTokens -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. +Controls whether access tokens may be transmitted via URI query parameters. Several OAuth 2.0 and OpenID Connect profiles require that access tokens be transmitted exclusively via the Authorization header. When set to false, the authorization server shall reject requests attempting to transmit access tokens via query parameters. @@ -2213,7 +2300,7 @@ false ### acrValues -Array of strings, the Authentication Context Class References that the authorization server supports. +An array of strings representing the Authentication Context Class References that this authorization server supports. _**default value**_: @@ -2223,7 +2310,10 @@ _**default value**_: ### allowOmittingSingleRegisteredRedirectUri -Allow omitting the redirect_uri parameter when only a single one is registered for a client. +Redirect URI Parameter Omission for Single Registered URI + +Specifies whether clients may omit the `redirect_uri` parameter in authorization requests when only a single redirect URI is registered in their client metadata. When enabled, the authorization server shall automatically use the sole registered redirect URI for clients that have exactly one URI configured. + When disabled, all authorization requests MUST explicitly include the `redirect_uri` parameter regardless of the number of registered redirect URIs. _**default value**_: @@ -2233,7 +2323,7 @@ true ### assertJwtClientAuthClaimsAndHeader -Helper function used to validate the JWT Client Authentication Assertion Claims Set and Header beyond what its specification mandates. +Specifies a helper function that shall be invoked to perform additional validation of JWT Client Authentication assertion Claims Set and Header beyond the requirements mandated by the specification. This function enables enforcement of deployment-specific security policies and extended validation logic for `private_key_jwt` and `client_secret_jwt` client authentication methods according to authorization server requirements. _**default value**_: @@ -2253,7 +2343,7 @@ async function assertJwtClientAuthClaimsAndHeader(ctx, claims, header, client) { ### claims -Describes the claims that the OpenID Provider MAY be able to supply values for. +Describes the claims that this authorization server may be able to supply values for. It is used to achieve two different things related to claims: - which additional claims are available to RPs (configure as `{ claimName: null }`) - which claims fall under what scope (configure `{ scopeName: ['claim', 'another-claim'] }`) @@ -2276,7 +2366,8 @@ _**default value**_: ### clientAuthMethods -Array of supported Client Authentication methods +Specifies the client authentication methods that this authorization server shall support for authenticating clients at the token endpoint and other authenticated endpoints. + _**default value**_: @@ -2304,7 +2395,7 @@ _**default value**_: ### clientBasedCORS -Function used to check whether a given CORS request should be allowed based on the request's client. +Specifies a function that determines whether Cross-Origin Resource Sharing (CORS) requests shall be permitted based on the requesting client. This function is invoked for each CORS preflight and actual request to evaluate the client's authorization to access the authorization server from the specified origin. See [Configuring Client Metadata-based CORS Origin allow list](https://github.com/panva/node-oidc-provider/discussions/1298) @@ -2312,13 +2403,16 @@ See [Configuring Client Metadata-based CORS Origin allow list](https://github.co _**default value**_: ```js function clientBasedCORS(ctx, origin, client) { + if (ctx.oidc.route === 'userinfo' || client.clientAuthMethod === 'none') { + return client.redirectUris.some((uri) => URL.parse(uri)?.origin === origin); + } return false; } ``` ### clientDefaults -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. +Specifies default client metadata values that shall be applied when properties are not explicitly provided during Dynamic Client Registration or for statically configured clients. This configuration allows override of the authorization server's built-in default values for any supported client metadata property. @@ -2338,7 +2432,7 @@ _**default value**_:
(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: +To change the default client token_endpoint_auth_method, configure `clientDefaults` to be an object like so: ```js @@ -2350,7 +2444,7 @@ To change the default client token_endpoint_auth_method configure `clientDefault
(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: +To change the default client response_types, configure `clientDefaults` to be an object like so: ```js @@ -2363,9 +2457,10 @@ To change the default client response_types configure `clientDefaults` to be an ### clockTolerance -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 +Specifies the maximum acceptable clock skew tolerance (in seconds) for validating time-sensitive operations, including JWT validation for Request Objects, DPoP Proofs, and other timestamp-based security mechanisms. + -_**recommendation**_: Only set this to a reasonable value when needed to cover server-side client and oidc-provider server clock skew. +_**recommendation**_: This value should be kept as small as possible while accommodating expected clock drift between the authorization server and client systems. _**default value**_: @@ -2378,7 +2473,7 @@ _**default value**_: ID Token only contains End-User claims when the requested `response_type` is `id_token` [`OIDC Core 1.0` - Requesting Claims using Scope Values](https://openid.net/specs/openid-connect-core-1_0-errata2.html#ScopeClaims) defines that claims requested using the `scope` parameter are only returned from the UserInfo Endpoint unless the `response_type` is `id_token`. - Despite 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. + Despite this configuration, the ID Token always includes claims requested using the `scope` parameter when the userinfo endpoint is disabled, or when issuing an Access Token not applicable for access to the userinfo endpoint. @@ -2389,12 +2484,12 @@ true ### cookies -Options for the [cookies module](https://github.com/pillarjs/cookies#cookiesset-name--value---options--) used to keep track of various User-Agent states. The options `maxAge` and `expires` are ignored. Use `ttl.Session` and `ttl.Interaction` to configure the ttl and in turn the cookie expiration values for Session and Interaction models. +Configuration for HTTP cookies used to maintain User-Agent state throughout the authorization flow. These settings conform to the [cookies module interface specification](https://github.com/pillarjs/cookies/tree/0.9.1?tab=readme-ov-file#cookiessetname--values--options). The `maxAge` and `expires` properties are ignored; cookie lifetimes are instead controlled via the `ttl.Session` and `ttl.Interaction` configuration parameters. ### cookies.long -Options for long-term cookies +Options for long-term cookies. _**default value**_: @@ -2407,7 +2502,7 @@ _**default value**_: ### cookies.names -Cookie names used to store and transfer various states. +Specifies the HTTP cookie names used for state management during the authorization flow. _**default value**_: @@ -2421,7 +2516,7 @@ _**default value**_: ### cookies.short -Options for short-term cookies +Options for short-term cookies. _**default value**_: @@ -2434,7 +2529,7 @@ _**default value**_: ### discovery -Pass additional properties to this object to extend the discovery document +Pass additional properties to this object to extend the discovery document. _**default value**_: @@ -2454,7 +2549,7 @@ _**default value**_: ### enableHttpPostMethods -Enables HTTP POST Method support at the Authorization Endpoint and the Logout Endpoint (if enabled). This setting can only be used when the `cookies.long.sameSite` configuration value is `none`. +Specifies whether HTTP POST method support shall be enabled at the Authorization Endpoint and the Logout Endpoint (if enabled). When enabled, the authorization server shall accept POST requests at these endpoints in addition to the standard GET requests. This configuration may only be used when the `cookies.long.sameSite` configuration value is `none`. _**default value**_: @@ -2464,7 +2559,7 @@ false ### expiresWithSession -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. +Specifies a helper function that shall be invoked to determine whether authorization codes, device codes, or authorization-endpoint-returned opaque access tokens shall be bound to the end-user session. When session binding is enabled, this policy shall be applied to all opaque tokens issued from the authorization code, device code, or subsequent refresh token exchanges. When artifacts are session-bound, their originating session will be loaded by its unique identifier every time the artifacts are encountered. Session-bound artifacts shall be effectively revoked when the end-user logs out, providing automatic cleanup of token state upon session termination. _**default value**_: @@ -2476,12 +2571,12 @@ async function expiresWithSession(ctx, code) { ### extraClientMetadata -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']`) +Specifies the configuration for custom client metadata properties that shall be supported by the authorization server for client registration and metadata validation purposes. This configuration enables extension of standard OAuth 2.0 and OpenID Connect client metadata with deployment-specific properties. Existing standards-defined properties are snakeCased on a Client instance (e.g. `client.redirectUris`), while new properties defined by this configuration shall be available with their names verbatim (e.g. `client['urn:example:client:my-property']`). ### extraClientMetadata.properties -Array of property names that clients will be allowed to have defined. +Specifies an array of property names that clients shall be allowed to have defined within their client metadata during registration and management operations. Each property name listed here extends the standard client metadata schema according to authorization server policy. _**default value**_: @@ -2491,7 +2586,7 @@ _**default value**_: ### extraClientMetadata.validator -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. +Specifies a validator function that shall be executed in order once for every property defined in `extraClientMetadata.properties`, regardless of its value or presence in the client metadata passed during registration or update operations. The function MUST be synchronous; async validators or functions returning Promise shall be rejected during runtime. To modify the current client metadata values (for the current key or any other) simply modify the passed in `metadata` argument within the validator function. _**default value**_: @@ -2509,9 +2604,10 @@ function extraClientMetadataValidator(ctx, key, value, metadata) { ### extraParams -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. +Specifies additional parameters that shall be recognized by the authorization, device authorization, backchannel authentication, and pushed authorization request endpoints. These extended parameters become available in `ctx.oidc.params` and are passed to interaction session details for processing. + This configuration accepts either an iterable object (array or Set of strings) for simple parameter registration, or a plain object with string properties representing parameter names and values being validation functions (synchronous or asynchronous) for the corresponding parameter values. + Parameter validators are executed regardless of the parameter's presence or value, enabling validation of parameter presence as well as assignment of default values. When the value is `null` or `undefined`, the parameter is registered without validation constraints. + Note: These validators execute during the final phase of the request validation process. Modifications to other parameters (such as assigning default values) will not trigger re-validation of the entire request. @@ -2548,7 +2644,7 @@ const extraParams = { ### extraTokenClaims -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. +Specifies a helper function that shall be invoked to add additional claims to Access Tokens during the token issuance process. For opaque Access Tokens, the returned claims shall be stored in the authorization server storage under the `extra` property and shall be returned by the introspection endpoint as top-level claims. For JWT-formatted Access Tokens, the returned claims shall be included as top-level claims within the JWT payload. Claims returned by this function will not overwrite pre-existing top-level claims in the token. @@ -2574,7 +2670,7 @@ async function extraTokenClaims(ctx, token) { ### fetch -Function called whenever calls to an external HTTP(S) resource are being made. The interface as well as expected return is the [Fetch API's](https://fetch.spec.whatwg.org/) [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch). The default is using timeout of 2500ms and not sending a user-agent header. +Specifies a function that shall be invoked whenever the authorization server needs to make calls to external HTTPS resources. The interface and expected return value shall conform to the [Fetch API specification](https://fetch.spec.whatwg.org/) [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) standard. The default implementation uses a timeout of 2500ms and does not send a user-agent header. @@ -2606,7 +2702,7 @@ To change all request's timeout configure the fetch as a function like so: ### formats.bitsOfOpaqueRandomness -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. +Specifies the entropy configuration for opaque token generation. The value shall be an integer (or a function returning an integer) that determines the cryptographic strength of generated opaque tokens. The resulting opaque token length shall be calculated as `Math.ceil(i / Math.log2(n))` where `i` is the specified bit count and `n` is the number of symbols in the encoding alphabet (64 characters in the base64url character set used by this implementation). @@ -2629,7 +2725,7 @@ function bitsOfOpaqueRandomness(ctx, token) { ### formats.customizers -Customizer functions used before issuing a structured Access Token. +Specifies customizer functions that shall be invoked immediately before issuing structured Access Tokens to enable modification of token headers and payload claims according to authorization server policy. These functions shall be called during the token formatting process to apply deployment-specific customizations to the token structure before signing. @@ -2656,13 +2752,13 @@ _**default value**_: ### interactions -Holds the configuration for interaction policy and a URL to send end-users to when the policy decides to require interaction. +Specifies the configuration for interaction policy and end-user redirection that shall be applied to determine that user interaction is required during the authorization process. This configuration enables customization of authentication and consent flows according to deployment-specific requirements. ### interactions.policy -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 exported. +Specifies the structure of Prompts and their associated checks that shall be applied during authorization request processing. The policy is formed by Prompt and Check class instances that define the conditions under which user interaction is required. The default policy implementation provides a fresh instance that can be customized, and the relevant classes are exported for configuration purposes. @@ -2946,7 +3042,7 @@ The default interaction policy consists of two available prompts, login and cons
(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. +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.
@@ -2968,7 +3064,7 @@ const basePolicy = base() ### interactions.url -Function used to determine where to redirect User-Agent for necessary interaction, can return both absolute and relative urls. +Specifies a function that shall be invoked to determine the destination URL for redirecting the User-Agent when user interaction is required during authorization processing. This function enables customization of the interaction endpoint location and may return both absolute and relative URLs according to deployment requirements. _**default value**_: @@ -2980,7 +3076,7 @@ async function interactionsUrl(ctx, interaction) { ### issueRefreshToken -Function used to decide whether a refresh token will be issued or not +Specifies a helper function that shall be invoked to determine whether a refresh token shall be issued during token endpoint operations. This function enables policy-based control over refresh token issuance according to authorization server requirements, client capabilities, and granted scope values. @@ -3011,7 +3107,7 @@ async issueRefreshToken(ctx, client, code) { ### loadExistingGrant -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. +Helper function invoked to load existing authorization grants that may be used to resolve an Authorization Request without requiring additional end-user interaction. The default implementation attempts to load grants based on the interaction result's `consent.grantId` property, falling back to the existing grantId for the requesting client in the current session. _**default value**_: @@ -3028,9 +3124,10 @@ async function loadExistingGrant(ctx) { ### pairwiseIdentifier -Function used by the authorization server when resolving pairwise ID Token and Userinfo sub claim values. See [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#PairwiseAlg) +Specifies a helper function that shall be invoked to generate pairwise subject identifier values for ID Tokens and UserInfo responses, as specified in OpenID Connect Core 1.0. This function enables privacy-preserving subject identifier generation that provides unique identifiers per client while maintaining consistent identification for the same end-user across requests to the same client. + -_**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. +_**recommendation**_: Implementations should employ memoization or caching mechanisms when this function may be invoked multiple times with identical arguments within a single request. _**default value**_: @@ -3049,13 +3146,13 @@ async function pairwiseIdentifier(ctx, accountId, client) { [`RFC7636`](https://www.rfc-editor.org/rfc/rfc7636.html) - Proof Key for Code Exchange (`PKCE`) -`PKCE` configuration such as policy check on the required use of `PKCE` +`PKCE` configuration such as policy check on the required use of `PKCE`. ### pkce.required -Configures if and when the authorization server requires clients to use `PKCE`. This helper is called whenever an authorization request lacks the code_challenge parameter. Return +Configures if and when the authorization server requires clients to use `PKCE`. This helper is called whenever an authorization request lacks the code_challenge parameter. Return: - `false` to allow the request to continue without `PKCE` - `true` to abort the request @@ -3088,7 +3185,7 @@ function pkceRequired(ctx, client) { ### renderError -Function used to present errors to the User-Agent +Specifies a function that shall be invoked to present error responses to the User-Agent during authorization server operations. This function enables customization of error presentation according to deployment-specific user interface requirements. _**default value**_: @@ -3113,7 +3210,7 @@ async function renderError(ctx, out, error) { ### responseTypes -Array of response_type values that the authorization server supports. The default omits all response types that result in access tokens being issued by the authorization endpoint directly as per [`RFC9700 - Best Current Practice for OAuth 2.0 Security`](https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.2) You can still enable them if you need to. +Specifies the response_type values supported by this authorization server. In accordance with RFC 9700 (OAuth 2.0 Security Best Current Practice), the default configuration excludes response types that result in access tokens being issued directly by the authorization endpoint. @@ -3144,8 +3241,7 @@ These are values defined in [`OIDC Core 1.0`](https://openid.net/specs/openid-co ### revokeGrantPolicy -Function called in a number of different context to determine whether an underlying Grant entry should also be revoked or not. - contexts: +Specifies a helper function that shall be invoked to determine whether an underlying Grant entry shall be revoked in addition to the specific token or code being processed. This function enables enforcement of grant revocation policies according to authorization server security requirements. The function is invoked in the following contexts: - RP-Initiated Logout - Opaque Access Token Revocation - Refresh Token Revocation @@ -3167,15 +3263,15 @@ function revokeGrantPolicy(ctx) { ### rotateRefreshToken -Configures if and how the authorization server rotates refresh tokens after they are used. Supported values are - - `false` refresh tokens are not rotated and their initial expiration date is final - - `true` 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 revoked - - `function` returning true/false, true when rotation should occur, false when it shouldn't +Specifies the refresh token rotation policy that shall be applied by the authorization server when refresh tokens are used. This configuration determines whether and under what conditions refresh tokens shall be rotated. Supported values include: + - `false` - refresh tokens shall not be rotated and their initial expiration date is final + - `true` - refresh tokens shall be rotated when used, with the current token marked as consumed and a new one issued with new TTL; when a consumed refresh token is encountered an error shall be returned and the whole token chain (grant) is revoked + - `function` - a function returning true/false that shall be invoked to determine whether rotation should occur based on request context and authorization server policy

- The default configuration value puts forth a sensible refresh token rotation policy + The default configuration value implements a sensible refresh token rotation policy that: - 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) + - otherwise always rotates public client tokens that are not sender-constrained + - otherwise only rotates tokens if they're being used close to their expiration (>= 70% TTL passed) _**default value**_: @@ -3201,7 +3297,7 @@ function rotateRefreshToken(ctx) { ### routes -Routing values used by the authorization server. Only provide routes starting with "/" +Defines the URL path mappings for authorization server endpoints. All route values are relative and shall begin with a forward slash ("/") character. _**default value**_: @@ -3209,6 +3305,7 @@ _**default value**_: { authorization: '/auth', backchannel_authentication: '/backchannel', + challenge: '/challenge', code_verification: '/device', device_authorization: '/device/auth', end_session: '/session/end', @@ -3224,7 +3321,7 @@ _**default value**_: ### scopes -Array of additional scope values that the authorization server signals to support in the discovery endpoint. Only add scopes the authorization server has a corresponding resource for. Resource Server scopes don't belong here, see `features.resourceIndicators` for configuring those. +Specifies additional OAuth 2.0 scope values that this authorization server shall support and advertise in its discovery document. Resource Server-specific scopes shall be configured via the `features.resourceIndicators` mechanism. _**default value**_: @@ -3237,7 +3334,7 @@ _**default value**_: ### sectorIdentifierUriValidate -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. +Specifies a function that shall be invoked to determine whether the sectorIdentifierUri of a client being loaded, registered, or updated should be fetched and its contents validated against the client metadata. _**default value**_: @@ -3250,9 +3347,9 @@ function sectorIdentifierUriValidate(client) { ### subjectTypes -Array of the Subject Identifier types that this authorization server supports. When only `pairwise` is supported it becomes the default `subject_type` client metadata value. Valid types are - - `public` - - `pairwise` +Specifies the array of Subject Identifier types that this authorization server shall support for end-user identification purposes. When only `pairwise` is supported, it shall become the default `subject_type` client metadata value. Supported identifier types shall include: + - `public` - provides the same subject identifier value to all clients + - `pairwise` - provides a unique subject identifier value per client to enhance privacy _**default value**_: @@ -3264,12 +3361,12 @@ _**default value**_: ### ttl -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. +Specifies the Time-To-Live (TTL) values that shall be applied to various artifacts within the authorization server. TTL values may be specified as either a numeric value (in seconds) or a synchronous function that returns a numeric value based on the current request context and authorization server policy. -_**recommendation**_: Do not set token TTLs longer then they absolutely have to be, the shorter the TTL, the better. +_**recommendation**_: Token TTL values should be set to the minimum duration necessary for the intended use case to minimize security exposure. -_**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). +_**recommendation**_: For refresh tokens requiring extended lifetimes, consider utilizing the `rotateRefreshToken` configuration option, which extends effective token lifetime through rotation rather than extended initial TTL values. _**default value**_: @@ -3331,8 +3428,35 @@ Configure `ttl` for a given token type with a function like so, this must return ### enabledJWA -Fine-tune the algorithms the authorization server supports by declaring algorithm values for each respective JWA use +Specifies the JSON Web Algorithm (JWA) values supported by this authorization server for various cryptographic operations, as defined in RFC 7518 and related specifications. + + +### enabledJWA.attestSigningAlgValues + +JWS "alg" Algorithm values the authorization server supports to verify signed Client Attestation and Client Attestation PoP JWTs with + + +_**default value**_: +```js +[ + 'ES256', + 'Ed25519', + 'EdDSA' +] +``` +
(Click to expand) Supported values list +
+ +```js +[ + 'RS256', 'RS384', 'RS512', + 'PS256', 'PS384', 'PS512', + 'ES256', 'ES384', 'ES512', + 'Ed25519', 'EdDSA', +] +``` +
### enabledJWA.authorizationEncryptionAlgValues @@ -3425,7 +3549,7 @@ _**default value**_: ### enabledJWA.clientAuthSigningAlgValues -JWS "alg" Algorithm values the authorization server supports for signed JWT Client Authentication +JWS "alg" Algorithm values the authorization server supports for signed JWT Client Authentication (`private_key_jwt` and `client_secret_jwt`) @@ -3897,7 +4021,7 @@ that. Every client is configured with one of 7 available [`token_endpoint_auth_method` values](https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method) and it must adhere to how that given method must be submitted. Submitting multiple means of -authentication is also not possible. If you're an authorization server operator you're encouraged to set up +authentication is also not possible. Authorization server operators are encouraged to set up listeners for errors (see [events.md](https://github.com/panva/node-oidc-provider/blob/v9.x/docs/events.md)) and deliver them to client developers out-of-band, e.g. by logs in an admin interface. @@ -3941,10 +4065,10 @@ If you need it today something's wrong! - https://www.youtube.com/watch?v=qMtYaDmhnHU - https://www.youtube.com/watch?v=zuVuhl_Axbs -ROPC falls beyond the scope of what the library can do magically on it's own having only accountId +ROPC falls beyond the scope of what the library can do magically on its 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`](#custom-grant-types) API is simple enough if you absolutely need ROPC. +[`registerGrantType`](#custom-grant-types) API is simple enough if ROPC is absolutely required. ### How to display, on the website of the authorization server itself, if the user is signed-in or not @@ -3956,8 +4080,8 @@ const signedIn = !!session.accountId; ### Client Credentials only clients -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 +The `redirect_uris is mandatory property` error occurs but Client Credential clients +don't need `redirect_uris` or `response_types`... This error appears because they are required properties, but they can be empty... ```js @@ -3971,8 +4095,8 @@ because they are required properties, but they can be empty... ### Resource Server only clients (e.g. for token introspection) -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... +The `redirect_uris is mandatory property` error occurs but the resource server needs +none. This error appears because they are required properties, but they can be empty... ```js { diff --git a/example/my_adapter.js b/example/my_adapter.js index e97e5cfeb..1a9bdea7d 100644 --- a/example/my_adapter.js +++ b/example/my_adapter.js @@ -69,6 +69,8 @@ class MyAdapter { * refresh token * - 'jkt' {string} - JWK SHA-256 Thumbprint (according to [RFC7638]) of a DPoP bound * access or refresh token + * - 'attestationJkt' {string} - [BackchannelAuthenticationRequest, DeviceCode, RefreshToken, PushedAuthorizationRequest only] + * JWK SHA-256 Thumbprint (according to [RFC7638]) of an attest_jwt_client_auth client instance * - gty {string} - [AccessToken, RefreshToken only] space delimited grant values, indicating * the grant type(s) they originate from (implicit, authorization_code, refresh_token or * device_code) the original one is always first, second is refresh_token if refreshed diff --git a/lib/actions/authorization/backchannel_request_response.js b/lib/actions/authorization/backchannel_request_response.js index 700a2dfc4..6376565cd 100644 --- a/lib/actions/authorization/backchannel_request_response.js +++ b/lib/actions/authorization/backchannel_request_response.js @@ -14,6 +14,10 @@ export default async function backchannelRequestResponse(ctx) { scope: [...ctx.oidc.requestParamScopes].join(' '), }); + if (ctx.oidc.client.clientAuthMethod === 'attest_jwt_client_auth') { + await request.setAttestBinding(ctx); + } + // eslint-disable-next-line default-case switch (request.resource.length) { case 0: diff --git a/lib/actions/authorization/check_dpop_jkt.js b/lib/actions/authorization/check_dpop_jkt.js index 5936500e0..a47e35c1c 100644 --- a/lib/actions/authorization/check_dpop_jkt.js +++ b/lib/actions/authorization/check_dpop_jkt.js @@ -1,5 +1,5 @@ import { InvalidRequest } from '../../helpers/errors.js'; -import dpopValidate, { DPOP_OK_WINDOW } from '../../helpers/validate_dpop.js'; +import dpopValidate, { CHALLENGE_OK_WINDOW } from '../../helpers/validate_dpop.js'; import epochTime from '../../helpers/epoch_time.js'; import instance from '../../helpers/weak_cache.js'; @@ -18,7 +18,7 @@ export default async function checkDpopJkt(ctx, next) { const unique = await ReplayDetection.unique( ctx.oidc.client.clientId, dPoP.jti, - epochTime() + DPOP_OK_WINDOW, + epochTime() + CHALLENGE_OK_WINDOW, ); ctx.assert(unique, new InvalidRequest('DPoP proof JWT Replay detected')); diff --git a/lib/actions/authorization/device_authorization_response.js b/lib/actions/authorization/device_authorization_response.js index 853b8d4df..e2cf5163f 100644 --- a/lib/actions/authorization/device_authorization_response.js +++ b/lib/actions/authorization/device_authorization_response.js @@ -12,6 +12,10 @@ export default async function deviceAuthorizationResponse(ctx) { userCode: normalize(userCode), }); + if (ctx.oidc.client.clientAuthMethod === 'attest_jwt_client_auth') { + await dc.setAttestBinding(ctx); + } + ctx.oidc.entity('DeviceCode', dc); ctx.body = { device_code: await dc.save(), diff --git a/lib/actions/authorization/pushed_authorization_request_response.js b/lib/actions/authorization/pushed_authorization_request_response.js index c12dbf180..d234ecd2f 100644 --- a/lib/actions/authorization/pushed_authorization_request_response.js +++ b/lib/actions/authorization/pushed_authorization_request_response.js @@ -48,6 +48,10 @@ export default async function pushedAuthorizationRequestResponse(ctx) { trusted: ctx.oidc.client.clientAuthMethod !== 'none' || !!ctx.oidc.trusted?.length, }); + if (ctx.oidc.client.clientAuthMethod === 'attest_jwt_client_auth') { + await requestObject.setAttestBinding(ctx); + } + const id = await requestObject.save(ttl); ctx.oidc.entity('PushedAuthorizationRequest', requestObject); diff --git a/lib/actions/challenge.js b/lib/actions/challenge.js new file mode 100644 index 000000000..1aa70d64c --- /dev/null +++ b/lib/actions/challenge.js @@ -0,0 +1,22 @@ +import instance from '../helpers/weak_cache.js'; +import noCache from '../shared/no_cache.js'; + +export default [ + noCache, + function challenge(ctx) { + const { DPoPNonces, AttestChallenges } = instance(ctx.oidc.provider); + + ctx.body = {}; + + const nextNonce = DPoPNonces?.nextChallenge(); + if (nextNonce) { + ctx.set('dpop-nonce', nextNonce); + } + + const nextChallenge = AttestChallenges?.nextChallenge(); + if (nextChallenge) { + ctx.set('oauth-client-attestation-challenge', nextChallenge); + ctx.body.attestation_challenge = nextChallenge; + } + }, +]; diff --git a/lib/actions/discovery.js b/lib/actions/discovery.js index ab7cab5bf..bd3ca4d8f 100644 --- a/lib/actions/discovery.js +++ b/lib/actions/discovery.js @@ -139,5 +139,9 @@ export default function discovery(ctx) { ctx.body.authorization_details_types_supported = Object.keys(richAuthorizationRequests.types); } + if (features.attestClientAuth.enabled) { + ctx.body.challenge_endpoint = ctx.oidc.urlFor('challenge'); + } + defaults(ctx.body, configuration.discovery); } diff --git a/lib/actions/grants/authorization_code.js b/lib/actions/grants/authorization_code.js index 7ec765302..77070e949 100644 --- a/lib/actions/grants/authorization_code.js +++ b/lib/actions/grants/authorization_code.js @@ -4,11 +4,13 @@ import instance from '../../helpers/weak_cache.js'; import checkPKCE from '../../helpers/pkce.js'; import revoke from '../../helpers/revoke.js'; import filterClaims from '../../helpers/filter_claims.js'; -import dpopValidate, { DPOP_OK_WINDOW } from '../../helpers/validate_dpop.js'; +import dpopValidate, { CHALLENGE_OK_WINDOW } from '../../helpers/validate_dpop.js'; import resolveResource from '../../helpers/resolve_resource.js'; import epochTime from '../../helpers/epoch_time.js'; import checkRar from '../../shared/check_rar.js'; import getCtxAccountClaims from '../../helpers/account_claims.js'; +import { setRefreshTokenBindings } from '../../helpers/set_rt_bindings.js'; +import { checkAttestBinding } from '../../helpers/check_attest_binding.js'; const gty = 'authorization_code'; @@ -89,6 +91,10 @@ export const handler = async function authorizationCodeHandler(ctx) { throw new InvalidGrant('authorization code redirect_uri mismatch'); } + if (ctx.oidc.client.clientAuthMethod === 'attest_jwt_client_auth' && code.attestationJkt) { + await checkAttestBinding(ctx, code); + } + if (code.consumed) { await revoke(ctx, code.grantId); throw new InvalidGrant('authorization code already consumed'); @@ -138,7 +144,7 @@ export const handler = async function authorizationCodeHandler(ctx) { const unique = await ReplayDetection.unique( ctx.oidc.client.clientId, dPoP.jti, - epochTime() + DPOP_OK_WINDOW, + epochTime() + CHALLENGE_OK_WINDOW, ); ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected')); @@ -192,15 +198,7 @@ export const handler = async function authorizationCodeHandler(ctx) { rar: code.rar, }); - if (ctx.oidc.client.clientAuthMethod === 'none') { - if (at.jkt) { - rt.jkt = at.jkt; - } - - if (at['x5t#S256']) { - rt['x5t#S256'] = at['x5t#S256']; - } - } + await setRefreshTokenBindings(ctx, at, rt); ctx.oidc.entity('RefreshToken', rt); refreshToken = await rt.save(); diff --git a/lib/actions/grants/ciba.js b/lib/actions/grants/ciba.js index 3d950e8d0..41ed7dda8 100644 --- a/lib/actions/grants/ciba.js +++ b/lib/actions/grants/ciba.js @@ -5,10 +5,12 @@ import presence from '../../helpers/validate_presence.js'; import instance from '../../helpers/weak_cache.js'; import filterClaims from '../../helpers/filter_claims.js'; import revoke from '../../helpers/revoke.js'; -import dpopValidate, { DPOP_OK_WINDOW } from '../../helpers/validate_dpop.js'; +import dpopValidate, { CHALLENGE_OK_WINDOW } from '../../helpers/validate_dpop.js'; import resolveResource from '../../helpers/resolve_resource.js'; import epochTime from '../../helpers/epoch_time.js'; import getCtxAccountClaims from '../../helpers/account_claims.js'; +import { setRefreshTokenBindings } from '../../helpers/set_rt_bindings.js'; +import { checkAttestBinding } from '../../helpers/check_attest_binding.js'; const { AuthorizationPending, @@ -72,6 +74,10 @@ export const handler = async function cibaHandler(ctx) { throw new AuthorizationPending(); } + if (ctx.oidc.client.clientAuthMethod === 'attest_jwt_client_auth') { + await checkAttestBinding(ctx, request); + } + if (request.consumed) { await revoke(ctx, request.grantId); throw new InvalidGrant('backchannel authentication request already consumed'); @@ -141,7 +147,7 @@ export const handler = async function cibaHandler(ctx) { const unique = await ReplayDetection.unique( ctx.oidc.client.clientId, dPoP.jti, - epochTime() + DPOP_OK_WINDOW, + epochTime() + CHALLENGE_OK_WINDOW, ); ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected')); @@ -185,15 +191,7 @@ export const handler = async function cibaHandler(ctx) { sid: request.sid, }); - if (ctx.oidc.client.clientAuthMethod === 'none') { - if (at.jkt) { - rt.jkt = at.jkt; - } - - if (at['x5t#S256']) { - rt['x5t#S256'] = at['x5t#S256']; - } - } + await setRefreshTokenBindings(ctx, at, rt); ctx.oidc.entity('RefreshToken', rt); refreshToken = await rt.save(); diff --git a/lib/actions/grants/client_credentials.js b/lib/actions/grants/client_credentials.js index a2a4a50cb..166655f50 100644 --- a/lib/actions/grants/client_credentials.js +++ b/lib/actions/grants/client_credentials.js @@ -2,7 +2,7 @@ import instance from '../../helpers/weak_cache.js'; import { InvalidGrant, InvalidTarget, InvalidScope, InvalidRequest, } from '../../helpers/errors.js'; -import dpopValidate, { DPOP_OK_WINDOW } from '../../helpers/validate_dpop.js'; +import dpopValidate, { CHALLENGE_OK_WINDOW } from '../../helpers/validate_dpop.js'; import checkResource from '../../shared/check_resource.js'; import epochTime from '../../helpers/epoch_time.js'; @@ -65,7 +65,7 @@ export const handler = async function clientCredentialsHandler(ctx) { const unique = await ReplayDetection.unique( client.clientId, dPoP.jti, - epochTime() + DPOP_OK_WINDOW, + epochTime() + CHALLENGE_OK_WINDOW, ); ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected')); diff --git a/lib/actions/grants/device_code.js b/lib/actions/grants/device_code.js index dbec4c35f..585e8fe13 100644 --- a/lib/actions/grants/device_code.js +++ b/lib/actions/grants/device_code.js @@ -5,10 +5,12 @@ import presence from '../../helpers/validate_presence.js'; import instance from '../../helpers/weak_cache.js'; import filterClaims from '../../helpers/filter_claims.js'; import revoke from '../../helpers/revoke.js'; -import dpopValidate, { DPOP_OK_WINDOW } from '../../helpers/validate_dpop.js'; +import dpopValidate, { CHALLENGE_OK_WINDOW } from '../../helpers/validate_dpop.js'; import resolveResource from '../../helpers/resolve_resource.js'; import epochTime from '../../helpers/epoch_time.js'; import getCtxAccountClaims from '../../helpers/account_claims.js'; +import { setRefreshTokenBindings } from '../../helpers/set_rt_bindings.js'; +import { checkAttestBinding } from '../../helpers/check_attest_binding.js'; const { AuthorizationPending, @@ -51,6 +53,10 @@ export const handler = async function deviceCodeHandler(ctx) { throw new InvalidGrant('client mismatch'); } + if (ctx.oidc.client.clientAuthMethod === 'attest_jwt_client_auth') { + await checkAttestBinding(ctx, code); + } + let cert; if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) { cert = getCertificate(ctx); @@ -140,7 +146,7 @@ export const handler = async function deviceCodeHandler(ctx) { const unique = await ReplayDetection.unique( ctx.oidc.client.clientId, dPoP.jti, - epochTime() + DPOP_OK_WINDOW, + epochTime() + CHALLENGE_OK_WINDOW, ); ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected')); @@ -184,15 +190,7 @@ export const handler = async function deviceCodeHandler(ctx) { sid: code.sid, }); - if (ctx.oidc.client.clientAuthMethod === 'none') { - if (at.jkt) { - rt.jkt = at.jkt; - } - - if (at['x5t#S256']) { - rt['x5t#S256'] = at['x5t#S256']; - } - } + await setRefreshTokenBindings(ctx, at, rt); ctx.oidc.entity('RefreshToken', rt); refreshToken = await rt.save(); diff --git a/lib/actions/grants/refresh_token.js b/lib/actions/grants/refresh_token.js index 9de107eed..37705983f 100644 --- a/lib/actions/grants/refresh_token.js +++ b/lib/actions/grants/refresh_token.js @@ -6,11 +6,12 @@ import revoke from '../../helpers/revoke.js'; import certificateThumbprint from '../../helpers/certificate_thumbprint.js'; import * as formatters from '../../helpers/formatters.js'; import filterClaims from '../../helpers/filter_claims.js'; -import dpopValidate, { DPOP_OK_WINDOW } from '../../helpers/validate_dpop.js'; +import dpopValidate, { CHALLENGE_OK_WINDOW } from '../../helpers/validate_dpop.js'; import resolveResource from '../../helpers/resolve_resource.js'; import epochTime from '../../helpers/epoch_time.js'; import checkRar from '../../shared/check_rar.js'; import getCtxAccountClaims from '../../helpers/account_claims.js'; +import { checkAttestBinding } from '../../helpers/check_attest_binding.js'; import { gty as cibaGty } from './ciba.js'; import { gty as deviceCodeGty } from './device_code.js'; @@ -104,7 +105,7 @@ export const handler = async function refreshTokenHandler(ctx) { const unique = await ReplayDetection.unique( client.clientId, dPoP.jti, - epochTime() + DPOP_OK_WINDOW, + epochTime() + CHALLENGE_OK_WINDOW, ); ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected')); @@ -114,6 +115,10 @@ export const handler = async function refreshTokenHandler(ctx) { throw new InvalidGrant('failed jkt verification'); } + if (ctx.oidc.client.clientAuthMethod === 'attest_jwt_client_auth') { + await checkAttestBinding(ctx, refreshToken); + } + ctx.oidc.entity('RefreshToken', refreshToken); ctx.oidc.entity('Grant', grant); @@ -168,6 +173,7 @@ export const handler = async function refreshTokenHandler(ctx) { rar: refreshToken.rar, 'x5t#S256': refreshToken['x5t#S256'], jkt: refreshToken.jkt, + attestationJkt: refreshToken.attestationJkt, }); if (refreshToken.gty && !refreshToken.gty.endsWith(gty)) { diff --git a/lib/actions/index.js b/lib/actions/index.js index 9b501e1f7..7db6ba916 100644 --- a/lib/actions/index.js +++ b/lib/actions/index.js @@ -6,6 +6,7 @@ import * as registration from './registration.js'; import getRevocation from './revocation.js'; import getIntrospection from './introspection.js'; import discovery from './discovery.js'; +import challenge from './challenge.js'; import * as endSession from './end_session.js'; import * as codeVerification from './code_verification.js'; @@ -20,4 +21,5 @@ export { discovery, endSession, codeVerification, + challenge, }; diff --git a/lib/actions/introspection.js b/lib/actions/introspection.js index 760c22057..bfd3e2870 100644 --- a/lib/actions/introspection.js +++ b/lib/actions/introspection.js @@ -7,6 +7,7 @@ import rejectDupes from '../shared/reject_dupes.js'; import paramsMiddleware from '../shared/assemble_params.js'; import { InvalidRequest } from '../helpers/errors.js'; import rejectStructuredTokens from '../shared/reject_structured_tokens.js'; +import { checkAttestBinding } from '../helpers/check_attest_binding.js'; const introspectable = new Set(['AccessToken', 'ClientCredentials', 'RefreshToken']); const JWT = 'application/token-introspection+jwt'; @@ -150,6 +151,18 @@ export default function introspectionAction(provider) { return; } + if ( + token.kind === 'RefreshToken' + && ctx.oidc.client.clientId === token.clientId + && ctx.oidc.client.clientAuthMethod === 'attest_jwt_client_auth' + ) { + try { + await checkAttestBinding(ctx, token); + } catch { + return; + } + } + if (!(await allowedPolicy(ctx, ctx.oidc.client, token))) { return; } diff --git a/lib/actions/revocation.js b/lib/actions/revocation.js index bc69e3701..4f4fe2491 100644 --- a/lib/actions/revocation.js +++ b/lib/actions/revocation.js @@ -6,6 +6,7 @@ import rejectDupes from '../shared/reject_dupes.js'; import paramsMiddleware from '../shared/assemble_params.js'; import rejectStructuredTokens from '../shared/reject_structured_tokens.js'; import revoke from '../helpers/revoke.js'; +import { checkAttestBinding } from '../helpers/check_attest_binding.js'; const revokeable = new Set(['AccessToken', 'ClientCredentials', 'RefreshToken']); @@ -98,6 +99,18 @@ export default function revocationAction(provider) { return; } + if ( + token.kind === 'RefreshToken' + && ctx.oidc.client.clientId === token.clientId + && ctx.oidc.client.clientAuthMethod === 'attest_jwt_client_auth' + ) { + try { + await checkAttestBinding(ctx, token); + } catch { + return; + } + } + if (!(await allowedPolicy(ctx, ctx.oidc.client, token))) { return; } diff --git a/lib/actions/userinfo.js b/lib/actions/userinfo.js index 4aaf3616a..bc03dda36 100644 --- a/lib/actions/userinfo.js +++ b/lib/actions/userinfo.js @@ -7,7 +7,7 @@ import noCache from '../shared/no_cache.js'; import certificateThumbprint from '../helpers/certificate_thumbprint.js'; import instance from '../helpers/weak_cache.js'; import filterClaims from '../helpers/filter_claims.js'; -import dpopValidate, { DPOP_OK_WINDOW } from '../helpers/validate_dpop.js'; +import dpopValidate, { CHALLENGE_OK_WINDOW } from '../helpers/validate_dpop.js'; import epochTime from '../helpers/epoch_time.js'; import { InvalidToken, InsufficientScope, InvalidDpopProof, UseDpopNonce, @@ -106,7 +106,7 @@ export default [ const unique = await ctx.oidc.provider.ReplayDetection.unique( accessToken.clientId, dPoP.jti, - epochTime() + DPOP_OK_WINDOW, + epochTime() + CHALLENGE_OK_WINDOW, ); ctx.assert(unique, new InvalidToken('DPoP proof JWT Replay detected')); diff --git a/lib/consts/jwa.js b/lib/consts/jwa.js index 9766eae61..6cd8605c6 100644 --- a/lib/consts/jwa.js +++ b/lib/consts/jwa.js @@ -38,3 +38,4 @@ export const userinfoEncryptionEncValues = [...encryptionEncValues]; export const introspectionEncryptionEncValues = [...encryptionEncValues]; export const authorizationEncryptionEncValues = [...encryptionEncValues]; export const dPoPSigningAlgValues = [...signingAlgValues].filter((alg) => !alg.startsWith('HS')); +export const attestSigningAlgValues = [...signingAlgValues].filter((alg) => !alg.startsWith('HS')); diff --git a/lib/helpers/dpop_nonces.js b/lib/helpers/challenge.js similarity index 70% rename from lib/helpers/dpop_nonces.js rename to lib/helpers/challenge.js index aecf0052f..c237acdbb 100644 --- a/lib/helpers/dpop_nonces.js +++ b/lib/helpers/challenge.js @@ -14,10 +14,10 @@ function sixfourbeify(value) { return buf; } -function compute(secret, step) { +function compute(secret, info, step) { return base64url.encodeBuffer( Buffer.from( - hkdfSync('sha256', secret, sixfourbeify(step), '', 32), + hkdfSync('sha256', secret, sixfourbeify(step), info, 32), ), ); } @@ -38,10 +38,13 @@ function compare(server, client) { } const STEP = 60; +export const CHALLENGE_OK_WINDOW = STEP * 5; -export default class DPoPNonces { +export default class ServerChallenge { #counter; + #info; + #secret; #prevprev; @@ -54,11 +57,16 @@ export default class DPoPNonces { #nextnext; - constructor(secret) { + constructor(secret, info) { if (!Buffer.isBuffer(secret) || secret.byteLength !== 32) { - throw new TypeError('features.dPoP.nonceSecret must be a 32-byte Buffer instance'); + throw new TypeError('Challenge secret must be a 32-byte Buffer instance'); + } + + if (typeof info !== 'string' || !info.length) { + throw new TypeError('Challenge info must be a non-empty string'); } + this.#info = info; this.#secret = Uint8Array.prototype.slice.call(secret); this.#counter = Math.floor(Date.now() / 1000 / STEP); @@ -68,7 +76,7 @@ export default class DPoPNonces { this.#counter, this.#counter + 1, this.#counter++ + 2, - ].map(compute.bind(undefined, this.#secret)); + ].map(compute.bind(undefined, this.#secret, this.#info)); setInterval(() => { [ @@ -82,20 +90,20 @@ export default class DPoPNonces { this.#now, this.#next, this.#nextnext, - compute(this.#secret, this.#counter++ + 2), + compute(this.#secret, this.#info, this.#counter++ + 2), ]; }, STEP * 1000).unref(); } - nextNonce() { + nextChallenge() { return this.#next; } - checkNonce(nonce) { + checkChallenge(challenge) { let result = 0; for (const server of [this.#prevprev, this.#prev, this.#now, this.#next, this.#nextnext]) { - result ^= compare(server, nonce); + result ^= compare(server, challenge); } return result === 0; diff --git a/lib/helpers/check_attest_binding.js b/lib/helpers/check_attest_binding.js new file mode 100644 index 000000000..51df5fb43 --- /dev/null +++ b/lib/helpers/check_attest_binding.js @@ -0,0 +1,10 @@ +import * as jose from 'jose'; + +import { InvalidGrant } from './errors.js'; + +export async function checkAttestBinding(ctx, model) { + const { cnf: { jwk } } = jose.decodeJwt(ctx.get('oauth-client-attestation')); + if (model.attestationJkt !== await jose.calculateJwkThumbprint(jwk)) { + throw new InvalidGrant('oauth-client-attestation instance public key mismatch'); + } +} diff --git a/lib/helpers/configuration.js b/lib/helpers/configuration.js index 082e1ddd1..2277fc6a9 100644 --- a/lib/helpers/configuration.js +++ b/lib/helpers/configuration.js @@ -270,6 +270,7 @@ class Configuration { jwtIntrospection: this.features.jwtIntrospection.enabled, jwtResponseModes: this.features.jwtResponseModes.enabled, dPoP: this.features.dPoP.enabled, + attestClientAuth: this.features.attestClientAuth.enabled, }; this.setAlgs('idTokenSigningAlgValues', allowList.idTokenSigningAlgValues.filter(filterHS)); @@ -293,6 +294,7 @@ class Configuration { this.setAlgs('authorizationEncryptionEncValues', allowList.authorizationEncryptionEncValues.slice(), enabled.jwtResponseModes, enabled.encryption); this.setAlgs('dPoPSigningAlgValues', allowList.dPoPSigningAlgValues.slice(), enabled.dPoP); + this.setAlgs('attestSigningAlgValues', allowList.attestSigningAlgValues.slice(), enabled.attestClientAuth); this.clientAuthSigningAlgValues = this.enabledJWA.clientAuthSigningAlgValues; @@ -434,6 +436,10 @@ class Configuration { authMethods.add('self_signed_tls_client_auth'); } + if (this.features.attestClientAuth.enabled) { + authMethods.add('attest_jwt_client_auth'); + } + if (this.clientAuthMethods) { this.clientAuthMethods.forEach((method) => { if (!authMethods.has(method)) { diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 42b1cd701..1dfb02a8e 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -25,7 +25,10 @@ function mustChange(name, msg) { } function clientBasedCORS(ctx, origin, client) { - mustChange('clientBasedCORS', 'control CORS allowed Origins based on the client making a CORS request'); + shouldChange('clientBasedCORS', 'control allowed CORS Origins based on the client making a CORS request'); + if (ctx.oidc.route === 'userinfo' || client.clientAuthMethod === 'none') { + return client.redirectUris.some((uri) => URL.parse(uri)?.origin === origin); + } return false; } @@ -107,6 +110,28 @@ function requireNonce(ctx) { return false; } +async function getAttestationSignaturePublicKey(ctx, iss, header, client) { + // @param ctx - koa request context + // @param iss - Issuer Identifier from the Client Attestation JWT + // @param header - Protected Header of the Client Attestation JWT + // @param client - client making the request + mustChange('features.attestClientAuth.getAttestationSignaturePublicKey', 'be able to verify the Client Attestation JWT signature'); + throw new Error('features.attestClientAuth.getAttestationSignaturePublicKey not implemented'); +} + +async function assertAttestationJwtAndPop(ctx, attestation, pop, client) { + // @param ctx - koa request context + // @param attestation - verified and parsed Attestation JWT + // attestation.protectedHeader - parsed protected header object + // attestation.payload - parsed protected header object + // attestation.key - CryptoKey that verified the Attestation JWT signature + // @param pop - verified and parsed Attestation JWT PoP + // pop.protectedHeader - parsed protected header object + // pop.payload - parsed protected header object + // pop.key - CryptoKey that verified the Attestation JWT PoP signature + // @param client - client making the request +} + async function userCodeConfirmSource(ctx, form, client, deviceInfo, userCode) { // @param ctx - koa request context // @param form - form source (id="op.deviceConfirmForm") to be embedded in the page and @@ -616,22 +641,23 @@ function makeDefaults() { /* * acrValues * - * description: Array of strings, the Authentication Context Class References that the authorization server supports. + * description: An array of strings representing the Authentication Context Class References + * that this authorization server supports. */ acrValues: [], /* * adapter * - * description: The provided example and any new instance of oidc-provider will use the basic - * in-memory adapter for storing issued tokens, codes, user sessions, dynamically registered - * clients, etc. This is fine as long as you develop, configure and generally just play around - * since every time you restart your process all information will be lost. As soon as you cannot - * live with this limitation you will be required to provide your own custom adapter constructor - * for oidc-provider to use. This constructor will be called for every model accessed the first - * time it is needed. + * description: Specifies the storage adapter implementation for persisting authorization server + * state. The default implementation provides a basic in-memory adapter suitable for development + * and testing purposes only. When this process is restarted, all stored information will be lost. + * Production deployments MUST provide a custom adapter implementation that persists data to + * external storage (e.g., database, Redis, etc.). + * + * The adapter constructor will be instantiated for each model type when first accessed. * - * see: [The interface oidc-provider expects](/example/my_adapter.js) + * see: [The expected interface](/example/my_adapter.js) * see: [Example MongoDB adapter implementation](https://github.com/panva/node-oidc-provider/discussions/1308) * see: [Example Redis adapter implementation](https://github.com/panva/node-oidc-provider/discussions/1309) * see: [Example Redis w/ JSON Adapter](https://github.com/panva/node-oidc-provider/discussions/1310) @@ -645,7 +671,7 @@ function makeDefaults() { /* * claims * - * description: Describes the claims that the OpenID Provider MAY be able to supply values for. + * description: Describes the claims that this authorization server may be able to supply values for. * * It is used to achieve two different things related to claims: * - which additional claims are available to RPs (configure as `{ claimName: null }`) @@ -664,8 +690,10 @@ function makeDefaults() { /* * clientBasedCORS * - * description: Function used to check whether a given CORS request should be allowed - * based on the request's client. + * description: Specifies a function that determines whether Cross-Origin Resource Sharing (CORS) + * requests shall be permitted based on the requesting client. This function + * is invoked for each CORS preflight and actual request to evaluate the client's authorization + * to access the authorization server from the specified origin. * * see: [Configuring Client Metadata-based CORS Origin allow list](https://github.com/panva/node-oidc-provider/discussions/1298) */ @@ -674,15 +702,17 @@ function makeDefaults() { /* * clients * - * description: Array of objects representing client metadata. These clients are referred to as - * static, they don't expire, never reload, are always available. In addition to these - * clients the authorization server 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. + * description: An array of client metadata objects representing statically configured OAuth 2.0 + * and OpenID Connect clients. These clients are persistent, do not expire, and remain available + * throughout the authorization server's lifetime. For dynamic client discovery, the authorization + * server will invoke the adapter's `find` method when encountering unregistered client identifiers. * - * Client's metadata is validated as defined by the respective specification they've been defined - * in. + * To restrict the authorization server to only statically configured clients and disable dynamic + * registration, configure the adapter to return falsy values for client lookup operations + * (e.g., `return Promise.resolve()`). + * + * Each client's metadata shall be validated according to the specifications in which the respective + * properties are defined. * * example: Available Metadata * @@ -692,8 +722,8 @@ function makeDefaults() { * 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.

+ *

The following metadata is available but may not be recognized depending on this + * authorization server's configuration.

* * authorization_encrypted_response_alg, authorization_encrypted_response_enc, * authorization_signed_response_alg, backchannel_logout_session_required, backchannel_logout_uri, @@ -713,14 +743,14 @@ function makeDefaults() { /* * clientDefaults * - * description: Default client metadata to be assigned when unspecified by the client metadata, - * e.g. during Dynamic Client Registration or for statically configured clients. The default value - * does not represent all default values, but merely copies its subset. You can provide any used - * client metadata property in this object. + * description: Specifies default client metadata values that shall be applied when properties + * are not explicitly provided during Dynamic Client Registration or for statically configured + * clients. This configuration allows override of the authorization server's built-in default + * values for any supported client metadata property. * * example: Changing the default client token_endpoint_auth_method * - * To change the default client token_endpoint_auth_method configure `clientDefaults` to be an + * To change the default client token_endpoint_auth_method, configure `clientDefaults` to be an * object like so: * * ```js @@ -730,7 +760,7 @@ function makeDefaults() { * ``` * example: Changing the default client response type to `code id_token` * - * To change the default client response_types configure `clientDefaults` to be an + * To change the default client response_types, configure `clientDefaults` to be an * object like so: * * ```js @@ -751,11 +781,12 @@ function makeDefaults() { /* * clockTolerance * - * description: A `Number` value (in seconds) describing the allowed system clock skew for - * validating client-provided JWTs, e.g. Request Objects, DPoP Proofs and otherwise comparing - * timestamps - * recommendation: Only set this to a reasonable value when needed to cover server-side client and - * oidc-provider server clock skew. + * description: Specifies the maximum acceptable clock skew tolerance (in seconds) for validating + * time-sensitive operations, including JWT validation for Request Objects, DPoP Proofs, and + * other timestamp-based security mechanisms. + * + * recommendation: This value should be kept as small as possible while accommodating expected + * clock drift between the authorization server and client systems. */ clockTolerance: 15, @@ -768,7 +799,7 @@ function makeDefaults() { * 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` + * Despite this configuration, the ID Token always includes claims requested using the `scope` * parameter when the userinfo endpoint is disabled, or when issuing an Access Token not applicable * for access to the userinfo endpoint. * @@ -778,26 +809,36 @@ function makeDefaults() { /* * loadExistingGrant * - * description: Helper function used to load existing but also just in time pre-established Grants - * to attempt to resolve an Authorization Request with. Default: loads a grant based on the - * interaction result `consent.grantId` first, falls back to the existing grantId for the client - * in the current session. + * description: Helper function invoked to load existing authorization grants that may be used + * to resolve an Authorization Request without requiring additional end-user interaction. + * The default implementation attempts to load grants based on the interaction result's + * `consent.grantId` property, falling back to the existing grantId for the requesting client + * in the current session. */ loadExistingGrant, /* * allowOmittingSingleRegisteredRedirectUri * - * title: Allow omitting the redirect_uri parameter when only a single one is registered for a client. + * title: Redirect URI Parameter Omission for Single Registered URI + * + * description: Specifies whether clients may omit the `redirect_uri` parameter in authorization + * requests when only a single redirect URI is registered in their client metadata. When enabled, + * the authorization server shall automatically use the sole registered redirect URI for clients + * that have exactly one URI configured. + * + * When disabled, all authorization requests MUST explicitly include the `redirect_uri` parameter + * regardless of the number of registered redirect URIs. */ allowOmittingSingleRegisteredRedirectUri: true, /* * acceptQueryParamAccessTokens * - * description: 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. + * description: Controls whether access tokens may be transmitted via URI query parameters. + * Several OAuth 2.0 and OpenID Connect profiles require that access tokens be transmitted + * exclusively via the Authorization header. When set to false, the authorization server + * shall reject requests attempting to transmit access tokens via query parameters. * */ acceptQueryParamAccessTokens: false, @@ -805,17 +846,19 @@ function makeDefaults() { /* * cookies * - * description: Options for the [cookies module](https://github.com/pillarjs/cookies#cookiesset-name--value---options--) - * used to keep track of various User-Agent states. The options `maxAge` and `expires` are ignored. Use `ttl.Session` - * and `ttl.Interaction` to configure the ttl and in turn the cookie expiration values for Session and Interaction - * models. + * description: Configuration for HTTP cookies used to maintain User-Agent state throughout + * the authorization flow. These settings conform to the + * [cookies module interface specification](https://github.com/pillarjs/cookies/tree/0.9.1?tab=readme-ov-file#cookiessetname--values--options). + * The `maxAge` and `expires` properties are ignored; cookie lifetimes are instead controlled + * via the `ttl.Session` and `ttl.Interaction` configuration parameters. * @nodefault */ cookies: { /* * cookies.names * - * description: Cookie names used to store and transfer various states. + * description: Specifies the HTTP cookie names used for state management during the + * authorization flow. */ names: { session: '_session', // used for main session reference @@ -826,32 +869,32 @@ function makeDefaults() { /* * cookies.long * - * description: Options for long-term cookies + * description: Options for long-term cookies. */ long: { - httpOnly: true, // cookies are not readable by client-side javascript + httpOnly: true, // cookies are not readable by client-side JavaScript sameSite: 'lax', }, /* * cookies.short * - * description: Options for short-term cookies + * description: Options for short-term cookies. */ short: { - httpOnly: true, // cookies are not readable by client-side javascript + httpOnly: true, // cookies are not readable by client-side JavaScript sameSite: 'lax', }, /* * cookies.keys * - * description: [Keygrip](https://www.npmjs.com/package/keygrip) Signing keys used for cookie + * description: [Keygrip](https://www.npmjs.com/package/keygrip) signing keys used for cookie * signing to prevent tampering. You may also pass your own KeyGrip instance. * * recommendation: Rotate regularly (by prepending new keys) with a reasonable interval and keep * a reasonable history of keys to allow for returning user session cookies to still be valid - * and re-signed + * and re-signed. * * @skip */ @@ -861,7 +904,7 @@ function makeDefaults() { /* * discovery * - * description: Pass additional properties to this object to extend the discovery document + * description: Pass additional properties to this object to extend the discovery document. */ discovery: { claim_types_supported: ['normal'], @@ -876,19 +919,23 @@ function makeDefaults() { /* * extraParams * - * description: 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. + * description: Specifies additional parameters that shall be recognized by the authorization, + * device authorization, backchannel authentication, and pushed authorization request endpoints. + * These extended parameters become available in `ctx.oidc.params` and are passed to interaction + * session details for processing. * + * This configuration accepts either an iterable object (array or Set of strings) for simple + * parameter registration, or a plain object with string properties representing parameter names + * and values being validation functions (synchronous or asynchronous) for the corresponding + * parameter values. * - * 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. + * Parameter validators are executed regardless of the parameter's presence or value, enabling + * validation of parameter presence as well as assignment of default values. When the value + * is `null` or `undefined`, the parameter is registered without validation constraints. + * + * Note: These validators execute during the final phase of the request validation process. + * Modifications to other parameters (such as assigning default values) will not trigger + * re-validation of the entire request. * * example: registering an extra `origin` parameter with its validator * @@ -924,15 +971,20 @@ function makeDefaults() { /* * features * - * description: Enable/disable features. + * description: Specifies the authorization server feature capabilities that shall be enabled + * or disabled. This configuration controls the availability of optional OAuth 2.0 and + * OpenID Connect extensions, experimental specifications, and proprietary enhancements. + * + * Certain features may be designated as experimental implementations. When experimental + * features are enabled, the authorization server will emit warnings to indicate that + * breaking changes may occur in future releases. These changes will be published as + * minor version updates of the oidc-provider module. * - * Some features may be experimental. - * Enabling those will produce a warning and you must - * be aware that breaking changes may occur and that those changes - * will be published as minor versions of oidc-provider. See the example below on how to - * acknowledge an experimental feature version (this will remove the warning) and ensure - * the Provider configuration will throw an error if a new version of oidc-provider includes - * breaking changes to this experimental feature. + * To suppress experimental feature warnings and ensure configuration validation against + * breaking changes, implementations shall acknowledge the specific experimental feature + * version using the acknowledgment mechanism demonstrated in the example below. When + * an unacknowledged breaking change is detected, the authorization server configuration + * will throw an error during instantiation. * * example: Acknowledging an experimental feature * @@ -983,13 +1035,15 @@ function makeDefaults() { /* * features.devInteractions * - * description: Development-ONLY out of the box interaction views bundled with the library allow - * you to skip the boring frontend part while experimenting with oidc-provider. Enter any - * username (will be used as sub claim value) and any password to proceed. + * description: Enables development-only interaction views that provide pre-built user + * interface components for rapid prototyping and testing of authorization flows. These + * views accept any username (used as the subject claim value) and any password for + * authentication, bypassing production-grade security controls. * - * 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. + * Production deployments MUST disable this feature and implement proper end-user + * authentication and authorization mechanisms. These development views MUST NOT + * be used in production environments as they provide no security guarantees and + * accept arbitrary credentials. */ devInteractions: { enabled: true }, @@ -998,8 +1052,8 @@ function makeDefaults() { * * title: [`RFC9449`](https://www.rfc-editor.org/rfc/rfc9449.html) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (`DPoP`) * - * description: Enables `DPoP` - mechanism for sender-constraining tokens via a - * proof-of-possession mechanism on the application level. + * description: Enables sender-constraining of OAuth 2.0 tokens through application-level + * proof-of-possession mechanisms. */ dPoP: { enabled: true, @@ -1007,20 +1061,26 @@ function makeDefaults() { /** * features.dPoP.nonceSecret * - * description: A secret value used for generating server-provided DPoP nonces. - * Must be a 32-byte length Buffer instance when provided. + * description: Specifies the cryptographic secret value used for generating server-provided + * DPoP nonces. When provided, this value MUST be a 32-byte length + * Buffer instance to ensure sufficient entropy for secure nonce generation. */ nonceSecret: undefined, /** * features.dPoP.requireNonce * - * description: Function used to determine whether a DPoP nonce is required or not. + * description: Specifies a function that determines whether a DPoP nonce shall be required + * for proof-of-possession validation in the current request context. This function is + * invoked during DPoP proof validation to enforce nonce requirements based on + * authorization server policy. */ requireNonce, /** * features.dPoP.allowReplay * - * description: Controls whether DPoP Proof Replay is allowed or not. + * description: Specifies whether DPoP Proof replay shall be permitted by the + * authorization server. When set to false, the server enforces strict replay protection + * by rejecting previously used DPoP proofs, enhancing security against replay attacks. */ allowReplay: false, }, @@ -1030,7 +1090,10 @@ function makeDefaults() { * * title: [`OIDC Back-Channel Logout 1.0`](https://openid.net/specs/openid-connect-backchannel-1_0-final.html) * - * description: Enables Back-Channel Logout features. + * description: Specifies whether Back-Channel Logout capabilities shall be enabled. When + * enabled, the authorization server shall support propagating end-user logouts initiated + * by relying parties to clients that were involved throughout the lifetime of the + * terminated session. */ backchannelLogout: { enabled: false }, @@ -1039,7 +1102,10 @@ function makeDefaults() { * * title: [OIDC Client Initiated Backchannel Authentication Flow (`CIBA`)](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-final.html) * - * description: Enables Core `CIBA` Flow, when combined with `features.fapi` and `features.requestObjects.enabled` enables [Financial-grade API: Client Initiated Backchannel Authentication Profile - Implementers Draft 01](https://openid.net/specs/openid-financial-api-ciba-ID1.html) as well. + * description: Enables Core `CIBA` Flow, when combined with `features.fapi` and + * `features.requestObjects.enabled` enables + * [Financial-grade API: Client Initiated Backchannel Authentication Profile - Implementers Draft 01](https://openid.net/specs/openid-financial-api-ciba-ID1.html) + * as well. * */ ciba: { @@ -1048,9 +1114,10 @@ function makeDefaults() { /* * features.ciba.deliveryModes * - * description: Fine-tune the supported token delivery modes. Supported values are - * - `poll` - * - `ping` + * description: Specifies the token delivery modes supported by this authorization server. + * The following delivery modes are defined: + * - `poll` - Client polls the token endpoint for completion + * - `ping` - Authorization server notifies client of completion via HTTP callback * */ deliveryModes: ['poll'], @@ -1058,10 +1125,13 @@ function makeDefaults() { /* * features.ciba.triggerAuthenticationDevice * - * description: Helper function used to trigger the authentication and authorization on end-user's Authentication Device. It is called after - * accepting the backchannel authentication request but before sending client back the response. + * description: Specifies a helper function that shall be invoked to initiate authentication + * and authorization processes on the end-user's Authentication Device as defined in the + * CIBA specification. This function is executed after accepting the backchannel + * authentication request but before transmitting the response to the requesting client. * - * When the end-user authenticates use `provider.backchannelResult()` to finish the Consumption Device login process. + * Upon successful end-user authentication, implementations shall use `provider.backchannelResult()` + * to complete the Consumption Device login process. * * example: `provider.backchannelResult()` method * @@ -1078,7 +1148,7 @@ function makeDefaults() { * - `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. + * - `options.authTime?`: number - Time when the end-user authentication occurred. * */ triggerAuthenticationDevice, @@ -1086,10 +1156,14 @@ function makeDefaults() { /* * features.ciba.validateBindingMessage * - * description: Helper function used to process the binding_message parameter and throw if its not following the authorization server's policy. + * description: Specifies a helper function that shall be invoked to validate the + * `binding_message` parameter according to authorization server policy. This function + * MUST reject invalid binding messages by throwing appropriate error instances. * - * 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. + * recommendation: Use `throw new errors.InvalidBindingMessage('validation error message')` + * when the binding_message violates authorization server policy. + * recommendation: Use `return undefined` when a binding_message is not required by policy + * and was not provided in the request. * */ validateBindingMessage, @@ -1097,11 +1171,15 @@ function makeDefaults() { /* * features.ciba.validateRequestContext * - * description: Helper function used to process the request_context parameter and throw if its not following the authorization server's policy. + * description: Specifies a helper function that shall be invoked to validate the + * `request_context` parameter according to authorization server policy. This function + * MUST enforce policy requirements for request context validation and reject + * non-compliant requests. * - * 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. + * recommendation: Use `throw new errors.InvalidRequest('validation error message')` + * when the request_context is required by policy but missing or invalid. + * recommendation: Use `return undefined` when a request_context is not required by policy + * and was not provided in the request. * */ validateRequestContext, @@ -1109,11 +1187,17 @@ function makeDefaults() { /* * features.ciba.processLoginHintToken * - * description: Helper function used to process the login_hint_token parameter and return the accountId value to use for processsing the request. + * description: Specifies a helper function that shall be invoked to process the + * `login_hint_token` parameter and extract the corresponding accountId value for + * request processing. This function MUST validate token expiration and format + * according to authorization server policy. * - * 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. + * recommendation: Use `throw new errors.ExpiredLoginHintToken('validation error message')` + * when the login_hint_token has expired. + * recommendation: Use `throw new errors.InvalidRequest('validation error message')` + * when the login_hint_token format or content is invalid. + * recommendation: Use `return undefined` when the accountId cannot be determined + * from the provided login_hint_token. * */ processLoginHintToken, @@ -1121,10 +1205,15 @@ function makeDefaults() { /* * features.ciba.processLoginHint * - * description: Helper function used to process the login_hint parameter and return the accountId value to use for processsing the request. + * description: Specifies a helper function that shall be invoked to process the + * `login_hint` parameter and extract the corresponding accountId value for + * request processing. This function MUST validate the hint format and content + * according to authorization server policy. * - * 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. + * recommendation: Use `throw new errors.InvalidRequest('validation error message')` + * when the login_hint format or content is invalid. + * recommendation: Use `return undefined` when the accountId cannot be determined + * from the provided login_hint. * */ processLoginHint, @@ -1132,11 +1221,15 @@ function makeDefaults() { /* * features.ciba.verifyUserCode * - * description: Helper function used to verify the user_code parameter value is present when required and verify its value. + * description: Specifies a helper function that shall be invoked to verify the presence + * and validity of the `user_code` parameter when required by authorization server policy. * - * 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. + * recommendation: Use `throw new errors.MissingUserCode('validation error message')` + * when user_code is required by policy but was not provided. + * recommendation: Use `throw new errors.InvalidUserCode('validation error message')` + * when the provided user_code value is invalid or does not meet policy requirements. + * recommendation: Use `return undefined` when no user_code was provided and it is not + * required by authorization server policy. * */ verifyUserCode, @@ -1147,9 +1240,10 @@ function makeDefaults() { * * title: [`RFC8705`](https://www.rfc-editor.org/rfc/rfc8705.html) - OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens (`MTLS`) * - * description: Enables specific features from the Mutual TLS specification. The three main - * features have their own specific setting in this feature's configuration object and - * you must provide functions for resolving some of the functions which are deployment-specific. + * description: Specifies whether Mutual TLS capabilities shall be enabled. + * The authorization server supports three distinct features that require separate configuration + * settings within this feature's configuration object. Implementations MUST provide + * deployment-specific helper functions for certificate validation and processing operations. * */ mTLS: { @@ -1158,63 +1252,133 @@ function makeDefaults() { /* * features.mTLS.certificateBoundAccessTokens * - * description: Enables section 3 & 4 Mutual TLS Client Certificate-Bound Tokens by exposing - * the client's `tls_client_certificate_bound_access_tokens` metadata property. + * description: Specifies whether Certificate-Bound Access Tokens shall be enabled as + * defined in RFC 8705 sections 3 and 4. When enabled, the authorization server shall + * expose the client's `tls_client_certificate_bound_access_tokens` metadata property + * for mutual TLS certificate binding functionality. */ certificateBoundAccessTokens: false, /* * features.mTLS.selfSignedTlsClientAuth * - * description: Enables section 2.2. Self-Signed Certificate Mutual TLS client authentication - * method `self_signed_tls_client_auth` for use in the server's `clientAuthMethods` - * configuration. + * description: Specifies whether Self-Signed Certificate Mutual TLS client authentication + * shall be enabled as defined in RFC 8705 section 2.2. When enabled, the authorization + * server shall support the `self_signed_tls_client_auth` authentication method within + * the server's `clientAuthMethods` configuration. */ selfSignedTlsClientAuth: false, /* * features.mTLS.tlsClientAuth * - * description: Enables section 2.1. PKI Mutual TLS client authentication method - * `tls_client_auth` for use in the server's `clientAuthMethods` - * configuration. + * description: Specifies whether PKI Mutual TLS client authentication shall be enabled + * as defined in RFC 8705 section 2.1. When enabled, the authorization server shall + * support the `tls_client_auth` authentication method within the server's + * `clientAuthMethods` configuration. */ tlsClientAuth: false, /* * features.mTLS.getCertificate * - * description: Function used to retrieve a `crypto.X509Certificate` instance, - * or a PEM-formatted string, representation of client certificate used in the request. + * description: Specifies a helper function that shall be invoked to retrieve the client + * certificate used in the current request. This function MUST return either a + * `crypto.X509Certificate` instance or a PEM-formatted string representation of + * the client certificate for mutual TLS processing. */ getCertificate, /* * features.mTLS.certificateAuthorized * - * description: Function used to determine if the client certificate, used in the - * request, is verified and comes from a trusted CA for the client. Should return true/false. - * Only used for `tls_client_auth` client authentication method. + * description: Specifies a helper function that shall be invoked to determine whether + * the client certificate used in the request is verified and originates from a trusted + * Certificate Authority for the requesting client. This function MUST return a boolean + * value indicating certificate authorization status. This validation is exclusively + * used for the `tls_client_auth` client authentication method. */ certificateAuthorized, /* * features.mTLS.certificateSubjectMatches * - * description: 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. + * description: Specifies a helper function that shall be invoked to determine whether + * the client certificate subject used in the request matches the registered client + * property according to authorization server policy. This function MUST return a + * boolean value indicating subject matching status. This validation is exclusively + * used for the `tls_client_auth` client authentication method. */ certificateSubjectMatches, }, + /* + * features.attestClientAuth + * + * title: [`draft-ietf-oauth-attestation-based-client-auth-06`](https://www.ietf.org/archive/id/draft-ietf-oauth-attestation-based-client-auth-06.html) - OAuth 2.0 Attestation-Based Client Authentication + * + * description: Specifies whether Attestation-Based Client Authentication capabilities + * shall be enabled. When enabled, the + * authorization server shall support the `attest_jwt_client_auth` authentication + * method within the server's `clientAuthMethods` configuration. This mechanism + * enables Client Instances to authenticate using a Client Attestation JWT issued + * by a trusted Client Attester and a corresponding Client Attestation Proof-of-Possession + * JWT generated by the Client Instance. + * + */ + attestClientAuth: { + ack: undefined, + enabled: false, + + /** + * features.attestClientAuth.challengeSecret + * + * description: Specifies the cryptographic secret value used for generating server-provided + * challenges. This value MUST be a 32-byte length + * Buffer instance to ensure sufficient entropy for secure challenge generation. + */ + challengeSecret: undefined, + + /** + * features.attestClientAuth.getAttestationSignaturePublicKey + * + * description: Specifies a helper function that shall be invoked to verify the issuer + * identifier of a Client Attestation JWT and retrieve the public key used for signature + * verification. At the point of this function's invocation, only the + * JWT format has been validated; no cryptographic or claims verification has occurred. + * + * The function MUST return a public key in one of the supported formats: CryptoKey, + * KeyObject, or JSON Web Key (JWK) representation. The authorization server shall + * use this key to verify the Client Attestation JWT signature. + */ + getAttestationSignaturePublicKey, + + /** + * features.attestClientAuth.assertAttestationJwtAndPop + * + * description: Specifies a helper function that shall be invoked to perform additional + * validation of the Client Attestation JWT and Client Attestation Proof-of-Possession + * JWT beyond the specification requirements. This enables enforcement of extension + * profiles, deployment-specific policies, or additional security constraints. + * + * At the point of invocation, both JWTs have undergone signature verification and + * standard validity claim validation. The function may throw errors to reject + * non-compliant attestations + * or return successfully to indicate acceptance of the client authentication attempt. + */ + assertAttestationJwtAndPop, + }, + /* * features.claimsParameter * * title: [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#ClaimsParameter) - Requesting Claims using the "claims" Request Parameter * - * description: Enables the use and validations of `claims` parameter as described in the - * specification. + * description: Specifies whether the `claims` request parameter shall be enabled for + * authorization requests. + * When enabled, the authorization server shall accept and process + * the `claims` parameter to enable fine-grained control over which claims are + * returned in ID Tokens and from the UserInfo Endpoint. * */ claimsParameter: { @@ -1223,7 +1387,13 @@ function makeDefaults() { /** * features.claimsParameter.assertClaimsParameter * - * description: Helper function used to validate the claims parameter beyond what the OpenID Connect 1.0 specification requires. + * description: Specifies a helper function that shall be invoked to perform additional + * validation of the `claims` parameter. This function enables enforcement of + * deployment-specific policies, security constraints, or extended claim validation + * logic according to authorization server requirements. + * + * The function may throw errors to reject non-compliant claims requests or return + * successfully to indicate acceptance of the claims parameter content. */ assertClaimsParameter, }, @@ -1233,7 +1403,10 @@ function makeDefaults() { * * title: [`RFC6749`](https://www.rfc-editor.org/rfc/rfc6749.html#section-1.3.4) - Client Credentials * - * description: Enables `grant_type=client_credentials` to be used on the token endpoint. + * description: Specifies whether the Client Credentials grant type shall be enabled. + * When enabled, the authorization server + * shall accept `grant_type=client_credentials` requests at the token endpoint, + * allowing clients to obtain access tokens. */ clientCredentials: { enabled: false }, @@ -1242,7 +1415,11 @@ function makeDefaults() { * * title: [`RFC8628`](https://www.rfc-editor.org/rfc/rfc8628.html) - OAuth 2.0 Device Authorization Grant (`Device Flow`) * - * description: Enables Device Authorization Grant + * description: Specifies whether the OAuth 2.0 Device Authorization Grant shall be enabled. + * When enabled, the authorization server shall support the device + * authorization flow, enabling OAuth clients on input-constrained devices to obtain + * user authorization by directing the user to perform the authorization flow on a + * secondary device with richer input and display capabilities. */ deviceFlow: { enabled: false, @@ -1250,51 +1427,62 @@ function makeDefaults() { /* * features.deviceFlow.charset * - * description: alias for a character set of the generated user codes. Supported values are - * - `base-20` uses BCDFGHJKLMNPQRSTVWXZ - * - `digits` uses 0123456789 + * description: Specifies the character set used for generating user codes in the device + * authorization flow. This configuration determines the alphabet from which user codes + * are constructed. Supported values include: + * - `base-20` - Uses characters BCDFGHJKLMNPQRSTVWXZ (excludes easily confused characters) + * - `digits` - Uses characters 0123456789 (numeric only) */ charset: 'base-20', /* * features.deviceFlow.mask * - * description: 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. + * description: Specifies the template pattern used for generating user codes in the device + * authorization flow. The authorization server shall replace `*` characters with random + * characters from the configured charset, while `-` (dash) and ` ` (space) characters + * may be included for enhanced readability. Refer to RFC 8628 + * for guidance on minimal recommended entropy requirements for user code generation. */ mask: '****-****', /* * features.deviceFlow.deviceInfo * - * description: 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. + * description: Specifies a helper function that shall be invoked to extract device-specific + * information from device authorization endpoint requests. The extracted information + * becomes available during the end-user confirmation screen to assist users in verifying + * that the authorization request originated from a device in their possession. This + * enhances security by enabling users to confirm device identity before granting authorization. */ deviceInfo, /* * features.deviceFlow.userCodeInputSource * - * description: HTML source rendered when device code feature renders an input prompt for the - * User-Agent. + * description: Specifies the HTML source that shall be rendered when the device flow + * feature displays a user code input prompt to the User-Agent. This template is + * presented during the device authorization flow when the authorization server + * requires the end-user to enter a device-generated user code for verification. */ userCodeInputSource, /* * features.deviceFlow.userCodeConfirmSource * - * description: HTML source rendered when device code feature renders an a confirmation prompt for - * ther User-Agent. + * description: Specifies the HTML source that shall be rendered when the device flow + * feature displays a confirmation prompt to the User-Agent. This template is + * presented after successful user code validation to confirm the authorization + * request before proceeding with the device authorization flow. */ userCodeConfirmSource, /* * features.deviceFlow.successSource * - * description: HTML source rendered when device code feature renders a success page for the - * User-Agent. + * description: Specifies the HTML source that shall be rendered when the device flow + * feature displays a success page to the User-Agent. This template is presented + * upon successful completion of the device authorization flow to inform the + * end-user that authorization has been granted to the requesting device. */ successSource, }, @@ -1302,29 +1490,36 @@ function makeDefaults() { /* * features.encryption * - * description: Enables encryption features such as receiving encrypted UserInfo responses, - * encrypted ID Tokens and allow receiving encrypted Request Objects. + * description: Specifies whether encryption capabilities shall be enabled. + * When enabled, the authorization server shall support accepting and issuing encrypted + * tokens involved in its other enabled capabilities. */ encryption: { enabled: false }, /* * features.fapi * - * title: Financial-grade API Security Profile (`FAPI`) + * title: FAPI Security Profiles (`FAPI`) * - * description: Enables extra Authorization Server behaviours defined in FAPI that cannot be - * achieved by other configuration options. + * description: Specifies whether FAPI Security Profile capabilities shall be + * enabled. When enabled, the authorization server shall implement additional security + * behaviors defined in FAPI specifications that cannot be achieved through other + * configuration options. */ fapi: { enabled: false, /* * features.fapi.profile * - * description: The specific profile of `FAPI` to enable. Supported values are: + * description: Specifies the FAPI profile version that shall be applied for security + * policy enforcement. The authorization server shall implement the behaviors defined + * in the selected profile specification. Supported values include: * - * - '2.0' Enables behaviours from [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-security-profile-2_0-final.html) - * - '1.0 Final' Enables behaviours from [FAPI 1.0 Security Profile - Part 2: Advanced](https://openid.net/specs/openid-financial-api-part-2-1_0-final.html) - * - 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. + * - '2.0' - The authorization server shall implement behaviors from [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-security-profile-2_0-final.html) + * - '1.0 Final' - The authorization server shall implement behaviors from [FAPI 1.0 Security Profile - Part 2: Advanced](https://openid.net/specs/openid-financial-api-part-2-1_0-final.html) + * - Function - A function that shall be invoked with arguments `(ctx, client)` to determine + * the profile contextually. The function shall return one of the supported profile values + * or undefined when FAPI behaviors should be ignored for the current request context. */ profile: undefined, }, @@ -1334,7 +1529,9 @@ function makeDefaults() { * * title: [`OIDC RP-Initiated Logout 1.0`](https://openid.net/specs/openid-connect-rpinitiated-1_0-final.html) * - * description: Enables RP-Initiated Logout features + * description: Specifies whether RP-Initiated Logout capabilities shall be enabled. When + * enabled, the authorization server shall support logout requests initiated by relying + * parties, allowing clients to request termination of end-user sessions. */ rpInitiatedLogout: { enabled: true, @@ -1342,16 +1539,21 @@ function makeDefaults() { /* * features.rpInitiatedLogout.postLogoutSuccessSource * - * description: HTML source rendered when RP-Initiated Logout concludes a logout but there - * was no `post_logout_redirect_uri` provided by the client. + * description: Specifies the HTML source that shall be rendered when an RP-Initiated + * Logout request concludes successfully but no `post_logout_redirect_uri` was provided + * by the requesting client. This template shall be presented to inform the end-user + * that the logout operation has completed successfully and provide appropriate + * post-logout guidance. */ postLogoutSuccessSource, /* * features.rpInitiatedLogout.logoutSource * - * description: HTML source rendered when RP-Initiated Logout renders a confirmation - * prompt for the User-Agent. + * description: Specifies the HTML source that shall be rendered when RP-Initiated Logout + * displays a confirmation prompt to the User-Agent. This template shall be presented + * to request explicit end-user confirmation before proceeding with the logout operation, + * ensuring user awareness and consent for session termination. */ logoutSource, }, @@ -1361,9 +1563,12 @@ function makeDefaults() { * * title: [`RFC7662`](https://www.rfc-editor.org/rfc/rfc7662.html) - OAuth 2.0 Token Introspection * - * description: Enables Token Introspection for: - * - opaque access tokens - * - refresh tokens + * description: Specifies whether OAuth 2.0 Token Introspection capabilities shall be enabled. + * When enabled, the authorization server shall expose a token introspection endpoint that + * allows authorized clients and resource servers to query the metadata and status of + * the following token types: + * - Opaque access tokens + * - Refresh tokens * */ introspection: { @@ -1372,8 +1577,10 @@ function makeDefaults() { /* * features.introspection.allowedPolicy * - * description: Helper function used to determine whether the client/RS (client argument) - * is allowed to introspect the given token (token argument). + * description: Specifies a helper function that shall be invoked to determine whether + * the requesting client or resource server is authorized to introspect the specified + * token. This function enables enforcement of fine-grained access control policies + * for token introspection operations according to authorization server security requirements. */ allowedPolicy: introspectionAllowedPolicy, }, @@ -1383,7 +1590,10 @@ function makeDefaults() { * * title: [`RFC9701`](https://www.rfc-editor.org/rfc/rfc9701.html) - JWT Response for OAuth Token Introspection * - * description: Enables JWT responses for Token Introspection features + * description: Specifies whether JWT-formatted token introspection responses shall be enabled. + * When enabled, the authorization server shall support issuing introspection responses + * as JSON Web Tokens, providing enhanced security and integrity protection for token + * metadata transmission between authorized parties. */ jwtIntrospection: { enabled: false }, @@ -1392,7 +1602,10 @@ function makeDefaults() { * * title: [JWT Secured Authorization Response Mode (`JARM`)](https://openid.net/specs/oauth-v2-jarm-final.html) * - * description: Enables JWT Secured Authorization Responses + * description: Specifies whether JWT Secured Authorization Response Mode capabilities shall + * be enabled. When enabled, the authorization server shall support encoding authorization + * responses as JSON Web Tokens, providing cryptographic protection and integrity + * assurance for authorization response parameters. */ jwtResponseModes: { enabled: false }, @@ -1401,8 +1614,11 @@ function makeDefaults() { * * title: [`RFC9126`](https://www.rfc-editor.org/rfc/rfc9126.html) - OAuth 2.0 Pushed Authorization Requests (`PAR`) * - * description: Enables the use of `pushed_authorization_request_endpoint` defined by the Pushed - * Authorization Requests RFC. + * description: Specifies whether Pushed Authorization Request capabilities shall be enabled. + * When enabled, the authorization server shall expose a pushed authorization request endpoint + * that allows clients to lodge authorization request parameters at the authorization + * server prior to redirecting end-users to the authorization endpoint, + * enhancing security by removing the need to transmit parameters via query string parameters. */ pushedAuthorizationRequests: { enabled: true, @@ -1410,15 +1626,20 @@ function makeDefaults() { /* * features.pushedAuthorizationRequests.requirePushedAuthorizationRequests * - * description: Makes the use of `PAR` required for all authorization - * requests as an authorization server policy. + * description: Specifies whether PAR usage shall be mandatory for all authorization + * requests as an authorization server security policy. When enabled, the authorization + * server shall reject authorization endpoint requests that do not utilize the pushed + * authorization request mechanism. */ requirePushedAuthorizationRequests: false, /* * features.pushedAuthorizationRequests.allowUnregisteredRedirectUris * - * description: Allows unregistered redirect_uri values to be used by authenticated clients using PAR that do not use a `sector_identifier_uri`. + * description: Specifies whether unregistered redirect_uri values shall be permitted + * for authenticated clients using PAR that do not utilize a sector_identifier_uri. + * This configuration enables dynamic redirect URI specification within the security + * constraints of the pushed authorization request mechanism. */ allowUnregisteredRedirectUris: false, }, @@ -1428,7 +1649,10 @@ function makeDefaults() { * * title: [`OIDC Dynamic Client Registration 1.0`](https://openid.net/specs/openid-connect-registration-1_0-errata2.html) and [`RFC7591`](https://www.rfc-editor.org/rfc/rfc7591.html) - OAuth 2.0 Dynamic Client Registration Protocol * - * description: Enables Dynamic Client Registration. + * description: Specifies whether Dynamic Client Registration capabilities shall be enabled. + * When enabled, the authorization server shall expose a client registration endpoint + * that allows clients to dynamically register themselves with the authorization server + * at runtime, enabling automated client onboarding and configuration management. */ registration: { enabled: false, @@ -1436,10 +1660,14 @@ function makeDefaults() { /* * features.registration.initialAccessToken * - * description: 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 token - * - `boolean` true/false to enable/disable adapter backed initial access tokens + * description: Specifies whether the registration endpoint shall require an initial + * access token as authorization for client registration requests. This configuration + * controls access to the dynamic registration functionality. Supported values include: + * - `string` - The authorization server shall validate the provided bearer token + * against this static initial access token value + * - `boolean` - When true, the authorization server shall require adapter-backed + * initial access tokens; when false, registration requests are processed without + * initial access tokens. * * example: To add an adapter backed initial access token and retrive its value * @@ -1452,12 +1680,13 @@ function makeDefaults() { /* * features.registration.policies * - * description: 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. + * description: Specifies registration and registration management policies that shall be + * applied to client metadata properties during dynamic registration operations. Policies + * are synchronous or asynchronous functions assigned to Initial Access Tokens that + * execute before standard client property validations. Multiple policies may be assigned + * to an Initial Access Token, and by default, the same policies shall transfer to the + * Registration Access Token. Policy functions may throw errors to reject registration + * requests or modify the client properties object before validation. * * example: To define registration and registration management policies * @@ -1495,11 +1724,11 @@ function makeDefaults() { * new (provider.InitialAccessToken)({ policies: ['my-policy', 'my-policy-2'] }).save().then(console.log); * ``` * - * 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: Referenced policies MUST always be present when encountered on a token; an AssertionError + * will be thrown inside the request context if a policy is not found, 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 + * 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: * ```js * // inside your final ran policy * ctx.oidc.entities.RegistrationAccessToken.policies = ['update-policy']; @@ -1510,28 +1739,33 @@ function makeDefaults() { /* * features.registration.idFactory * - * description: Function used to generate random client identifiers during dynamic - * client registration + * description: Specifies a helper function that shall be invoked to generate random + * client identifiers during dynamic client registration operations. This function + * enables customization of client identifier generation according to authorization + * server requirements and conventions. */ idFactory, /* * features.registration.secretFactory * - * description: Function used to generate random client secrets during dynamic - * client registration + * description: Specifies a helper function that shall be invoked to generate random + * client secrets during dynamic client registration operations. This function + * enables customization of client secret generation according to authorization + * server security requirements and entropy specifications. */ secretFactory, /* * features.registration.issueRegistrationAccessToken * - * description: 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 issued - * - `false` registration access tokens is not issued - * - function returning true/false, true when token should be issued, false when it shouldn't + * description: Specifies whether a registration access token shall be issued upon + * successful client registration. This configuration determines if clients receive + * tokens for subsequent registration management operations. Supported values include: + * - `true` - Registration access tokens shall be issued for all successful registrations + * - `false` - Registration access tokens shall not be issued + * - Function - A function that shall be invoked to dynamically determine token issuance + * based on request context and authorization server policy * * example: To determine if a registration access token should be issued dynamically * @@ -1550,7 +1784,10 @@ function makeDefaults() { * * title: [`RFC7592`](https://www.rfc-editor.org/rfc/rfc7592.html) - OAuth 2.0 Dynamic Client Registration Management Protocol * - * description: Enables Update and Delete features described in the RFC + * description: Specifies whether Dynamic Client Registration Management capabilities shall be enabled. + * When enabled, the authorization server shall expose Update and Delete operations as defined in RFC 7592, + * allowing clients to modify or remove their registration entries using Registration Access Tokens + * for client lifecycle management operations. */ registrationManagement: { enabled: false, @@ -1558,14 +1795,19 @@ function makeDefaults() { /* * features.registrationManagement.rotateRegistrationAccessToken * - * description: Enables registration access token rotation. The authorization server 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 rotated - * - `true` registration access tokens are rotated when used - * - function returning true/false, true when rotation should occur, false when it shouldn't - * example: function use + * description: Specifies whether registration access token rotation shall be enabled as a security + * policy for client registration management operations. When token rotation is active, the + * authorization server shall discard the current Registration Access Token upon successful + * update operations and issue a new token, returning it to the client with the Registration + * Update Response. + * + * Supported values include: + * - `false` - Registration access tokens shall not be rotated and remain valid after use + * - `true` - Registration access tokens shall be rotated when used for management operations + * - Function - A function that shall be invoked to dynamically determine whether rotation + * should occur based on request context and authorization server policy + * + * example: Dynamic token rotation policy implementation * ```js * { * features: { @@ -1589,8 +1831,10 @@ function makeDefaults() { * * title: [`RFC9396`](https://www.rfc-editor.org/rfc/rfc9396.html) - OAuth 2.0 Rich Authorization Requests * - * description: Enables the use of `authorization_details` parameter for the authorization and token - * endpoints to enable issuing Access Tokens with fine-grained authorization data. + * description: Specifies whether Rich Authorization Request capabilities shall be enabled. + * When enabled, the authorization server shall support the `authorization_details` parameter + * at the authorization and token endpoints to enable issuing Access Tokens with fine-grained + * authorization data and enhanced authorization scope control. */ richAuthorizationRequests: { enabled: false, @@ -1598,9 +1842,12 @@ function makeDefaults() { /** * features.richAuthorizationRequests.types * - * description: Supported authorization details type identifiers. + * description: Specifies the authorization details type identifiers that shall be supported + * by the authorization server. Each type identifier MUST have an associated validation + * function that defines the required structure and constraints for authorization details + * of that specific type according to authorization server policy. * - * example: https://www.rfc-editor.org/rfc/rfc9396.html#appendix-A.3 + * example: Authorization details type validation for tax data access * * ```js * import { z } from 'zod' @@ -1653,8 +1900,11 @@ function makeDefaults() { /* * features.richAuthorizationRequests.rarForAuthorizationCode * - * description: Function used to transform the requested and granted RAR details that are then stored - * in the authorization code. Return array of details or undefined. + * description: Specifies a helper function that shall be invoked to transform the requested + * and granted Rich Authorization Request details for storage in the authorization code. + * This function enables filtering and processing of authorization details according to + * authorization server policy before code persistence. The function shall return an + * array of authorization details or undefined. */ rarForAuthorizationCode(ctx) { // decision points: @@ -1670,9 +1920,11 @@ function makeDefaults() { /* * features.richAuthorizationRequests.rarForCodeResponse * - * description: 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. + * description: Specifies a helper function that shall be invoked to transform the requested + * and granted Rich Authorization Request details for inclusion in the Access Token Response + * as authorization_details and assignment to the issued Access Token. This function enables + * resource-specific filtering and transformation of authorization details according to + * token endpoint policy. The function shall return an array of authorization details or undefined. */ rarForCodeResponse(ctx, resourceServer) { // decision points: @@ -1689,9 +1941,12 @@ function makeDefaults() { /* * features.richAuthorizationRequests.rarForRefreshTokenResponse * - * description: 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. + * description: Specifies a helper function that shall be invoked to transform the requested + * and granted Rich Authorization Request details for inclusion in the Access Token Response + * during refresh token exchanges as authorization_details and assignment to the newly issued + * Access Token. This function enables resource-specific processing of previously granted + * authorization details according to refresh token policy. The function shall return an + * array of authorization details or undefined. */ rarForRefreshTokenResponse(ctx, resourceServer) { // decision points: @@ -1708,9 +1963,11 @@ function makeDefaults() { /* * features.richAuthorizationRequests.rarForIntrospectionResponse * - * description: 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. + * description: Specifies a helper function that shall be invoked to transform the token's + * stored Rich Authorization Request details for inclusion in the Token Introspection Response. + * This function enables filtering and processing of authorization details according to + * introspection endpoint policy and requesting party authorization. The function shall + * return an array of authorization details or undefined. */ rarForIntrospectionResponse(ctx, token) { // decision points: @@ -1730,23 +1987,26 @@ function makeDefaults() { * * title: [`RFC8707`](https://www.rfc-editor.org/rfc/rfc8707.html) - Resource Indicators for OAuth 2.0 * - * description: Enables the use of `resource` parameter for the authorization and token - * endpoints to enable issuing Access Tokens for Resource Servers (APIs). + * description: Specifies whether Resource Indicator capabilities shall be enabled. When + * enabled, the authorization server shall support the `resource` parameter at the + * authorization and token endpoints to enable issuing Access Tokens for specific + * Resource Servers (APIs) with enhanced audience control and scope management. * + * The authorization server implements the following resource indicator processing rules: * - 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 + * 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. + * - 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 the + * resources then the resource parameter MUST be provided (or one MUST be resolved using the * `defaultResource` helper). * An Access Token for the provided/resolved resource is returned. * - (with userinfo endpoint enabled and useGrantedResource helper returning falsy) @@ -1762,7 +2022,7 @@ function makeDefaults() { * 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 + * - Issued Access Tokens shall always only contain scopes that are defined on the respective Resource * Server (returned from `features.resourceIndicators.getResourceServerInfo`). */ resourceIndicators: { @@ -1771,34 +2031,39 @@ function makeDefaults() { /* * features.resourceIndicators.defaultResource * - * description: 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. + * description: Specifies a helper function that shall be invoked to determine the default + * resource indicator for a request when none is provided by the client during the + * authorization request or when multiple resources are provided/resolved and only a + * single one is required during an Access Token Request. This function enables + * authorization server policy-based resource selection according to deployment requirements. */ defaultResource, /* * features.resourceIndicators.useGrantedResource * - * description: 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. + * description: Specifies a helper function that shall be invoked to determine whether + * an already granted resource indicator should be used without being explicitly + * requested by the client during the Token Endpoint request. This function enables + * flexible resource selection policies for token issuance operations. * - * recommendation: Use `return true` when it's allowed for a client skip providing the "resource" + * recommendation: Use `return true` when it's allowed for a client to skip providing the "resource" * parameter at the Token Endpoint. - * recommendation: Use `return false` (default) when it's required for a client to explitly + * recommendation: Use `return false` (default) when it's required for a client to explicitly * provide a "resource" parameter at the Token Endpoint or when other indication - * dictates an Access Token for the UserInfo Endpoint should returned. + * dictates an Access Token for the UserInfo Endpoint should be returned. */ useGrantedResource, /* * features.resourceIndicators.getResourceServerInfo * - * description: Function used to load information about a Resource Server (API) and check if the - * client is meant to request scopes for that particular resource. + * description: Specifies a helper function that shall be invoked to load information about + * a Resource Server (API) and determine whether the client is authorized to request + * scopes for that particular resource. This function enables resource-specific scope + * validation and Access Token configuration according to authorization server policy. * - * recommendation: Only allow client's pre-registered resource values, to pre-register these + * 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. * @@ -1886,7 +2151,10 @@ function makeDefaults() { * * title: [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#RequestObject) and [`RFC9101`](https://www.rfc-editor.org/rfc/rfc9101.html#name-passing-a-request-object-by) - Passing a Request Object by Value (`JAR`) * - * description: Enables the use and validations of the `request` parameter. + * description: Specifies whether Request Object capabilities shall be enabled. When enabled, + * the authorization server shall support the use and validation of the `request` parameter + * for conveying authorization request parameters as JSON Web Tokens, providing enhanced + * security and integrity protection for authorization requests. */ requestObjects: { enabled: false, @@ -1894,16 +2162,21 @@ function makeDefaults() { /* * features.requestObjects.requireSignedRequestObject * - * description: Makes the use of signed request objects required for all authorization - * requests as an authorization server policy. + * description: Specifies whether the use of signed request objects shall be mandatory for + * all authorization requests as an authorization server security policy. When enabled, + * the authorization server shall reject authorization requests that do not include a + * signed Request Object JWT. */ requireSignedRequestObject: false, /** * features.requestObjects.assertJwtClaimsAndHeader * - * description: Helper function used to validate the Request Object JWT Claims Set and Header beyond - * what the JAR specification requires. + * description: Specifies a helper function that shall be invoked to perform additional + * validation of the Request Object JWT Claims Set and Header beyond the standard + * JAR specification requirements. This function enables enforcement of deployment-specific + * policies, security constraints, or extended validation logic according to authorization + * server requirements. */ assertJwtClaimsAndHeader, }, @@ -1913,7 +2186,10 @@ function makeDefaults() { * * title: [`OIDC Relying Party Metadata Choices 1.0 - Implementers Draft 01`](https://openid.net/specs/openid-connect-rp-metadata-choices-1_0-ID1.html) * - * description: Enables the use of the following multi-valued input parameters metadata from the Relying Party Metadata Choices draft assuming their underlying feature is also enabled: + * description: Specifies whether Relying Party Metadata Choices capabilities shall be enabled. + * When enabled, the authorization server shall support the following multi-valued input + * parameters metadata from the Relying Party Metadata Choices draft, provided that their + * underlying feature is also enabled: * * - subject_types_supported * - id_token_signing_alg_values_supported @@ -1942,9 +2218,12 @@ function makeDefaults() { * * title: [`RFC7009`](https://www.rfc-editor.org/rfc/rfc7009.html) - OAuth 2.0 Token Revocation * - * description: Enables Token Revocation for: - * - opaque access tokens - * - refresh tokens + * description: Specifies whether Token Revocation capabilities shall be enabled. When enabled, + * the authorization server shall expose a token revocation endpoint that allows authorized + * clients and resource servers to notify the authorization server that a particular token + * is no longer needed. This feature supports revocation of the following token types: + * - Opaque access tokens + * - Refresh tokens * */ revocation: { @@ -1953,8 +2232,10 @@ function makeDefaults() { /* * features.revocation.allowedPolicy * - * description: Helper function used to determine whether the client/RS (client argument) - * is allowed to revoke the given token (token argument). + * description: Specifies a helper function that shall be invoked to determine whether + * the requesting client or resource server is authorized to revoke the specified token. + * This function enables enforcement of fine-grained access control policies for token + * revocation operations according to authorization server security requirements. */ allowedPolicy: revocationAllowedPolicy, }, @@ -1964,8 +2245,10 @@ function makeDefaults() { * * title: [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#UserInfo) - UserInfo Endpoint * - * description: Enables the userinfo endpoint. Its use requires an opaque Access Token with at least - * `openid` scope that's without a Resource Server audience. + * description: Specifies whether the UserInfo Endpoint shall be enabled. When enabled, + * the authorization server shall expose a UserInfo endpoint that returns claims about + * the authenticated end-user. Access to this endpoint requires an opaque Access Token + * with at least `openid` scope that does not have a Resource Server audience. */ userinfo: { enabled: true }, @@ -1974,8 +2257,12 @@ function makeDefaults() { * * title: [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#UserInfo) - JWT UserInfo Endpoint Responses * - * description: Enables the userinfo to optionally return signed and/or encrypted JWTs, also - * enables the relevant client metadata for setting up signing and/or encryption. + * description: Specifies whether JWT-formatted UserInfo endpoint responses shall be enabled. + * When enabled, the authorization server shall support returning UserInfo responses as + * signed and/or encrypted JSON Web Tokens, providing enhanced security and integrity + * protection for end-user claims transmission. This feature shall also enable the + * relevant client metadata parameters for configuring JWT signing and/or encryption + * algorithms according to client requirements. */ jwtUserinfo: { enabled: false }, @@ -1984,8 +2271,11 @@ function makeDefaults() { * * title: [draft-sakimura-oauth-wmrm-01](https://tools.ietf.org/html/draft-sakimura-oauth-wmrm-01) - OAuth 2.0 Web Message Response Mode * - * description: Enables `web_message` response mode. Only Simple Mode is supported. Requests containing - * the Relay Mode parameters will be rejected. + * description: Specifies whether Web Message Response Mode capabilities shall be enabled. + * When enabled, the authorization server shall support the `web_message` response mode + * for returning authorization responses via HTML5 Web Messaging. The implementation + * shall support only Simple Mode operation; authorization requests containing Relay Mode + * parameters will be rejected. * * recommendation: Although a general advise to use a `helmet` (e.g. for [express](https://www.npmjs.com/package/helmet), * [koa](https://www.npmjs.com/package/koa-helmet)) it is especially advised for your interaction @@ -2000,10 +2290,13 @@ function makeDefaults() { * * title: External Signing Support * - * description: Enables the use of the exported `ExternalSigningKey` class instances in place - * of a Private JWK in the `jwks.keys` configuration array. This allows Digital Signature - * Algorithm (such as PS256, ES256, or others) signatures to be produced externally, for example - * via a KMS service or an HSM. + * description: Specifies whether external signing capabilities shall be enabled. When enabled, + * the authorization server shall support the use of `ExternalSigningKey` class instances + * in place of private JWK entries within the `jwks.keys` configuration array. This feature + * enables Digital Signature Algorithm operations (such as PS256, ES256, or other supported + * algorithms) to be performed by external cryptographic services, including Key Management + * Services (KMS) and Hardware Security Modules (HSM), providing enhanced security for + * private key material through externalized signing operations. * * see: [KMS integration with AWS Key Management Service](https://github.com/panva/node-oidc-provider/discussions/1316) */ @@ -2013,11 +2306,13 @@ function makeDefaults() { /* * extraTokenClaims * - * description: 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. + * description: Specifies a helper function that shall be invoked to add additional claims + * to Access Tokens during the token issuance process. For opaque Access Tokens, the + * returned claims shall be stored in the authorization server storage under the `extra` + * property and shall be returned by the introspection endpoint as top-level claims. + * For JWT-formatted Access Tokens, the returned claims shall be included as top-level + * claims within the JWT payload. Claims returned by this function will not overwrite + * pre-existing top-level claims in the token. * * example: To add an arbitrary claim to an Access Token * ```js @@ -2036,9 +2331,12 @@ function makeDefaults() { /* * formats.bitsOfOpaqueRandomness * - * description: 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. + * description: Specifies the entropy configuration for opaque token generation. The value + * shall be an integer (or a function returning an integer) that determines the + * cryptographic strength of generated opaque tokens. The resulting opaque token length + * shall be calculated as `Math.ceil(i / Math.log2(n))` where `i` is the specified + * bit count and `n` is the number of symbols in the encoding alphabet (64 characters + * in the base64url character set used by this implementation). * * example: To have e.g. Refresh Tokens values longer than Access Tokens. * ```js @@ -2056,7 +2354,11 @@ function makeDefaults() { /* * formats.customizers * - * description: Customizer functions used before issuing a structured Access Token. + * description: Specifies customizer functions that shall be invoked immediately before + * issuing structured Access Tokens to enable modification of token headers and payload + * claims according to authorization server policy. These functions shall be called + * during the token formatting process to apply deployment-specific customizations + * to the token structure before signing. * * example: To push additional headers and payload claims to a `jwt` format Access Token * ```js @@ -2077,18 +2379,25 @@ function makeDefaults() { /* * expiresWithSession - * description: 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. + * + * description: Specifies a helper function that shall be invoked to determine whether + * authorization codes, device codes, or authorization-endpoint-returned opaque access + * tokens shall be bound to the end-user session. When session binding is enabled, this + * policy shall be applied to all opaque tokens issued from the authorization code, device + * code, or subsequent refresh token exchanges. When artifacts are session-bound, their + * originating session will be loaded by its unique identifier every time the artifacts + * are encountered. Session-bound artifacts shall be effectively revoked when the end-user + * logs out, providing automatic cleanup of token state upon session termination. */ expiresWithSession, /* * issueRefreshToken * - * description: Function used to decide whether a refresh token will be issued or not + * description: Specifies a helper function that shall be invoked to determine whether + * a refresh token shall be issued during token endpoint operations. This function + * enables policy-based control over refresh token issuance according to authorization + * server requirements, client capabilities, and granted scope values. * * example: To always issue a refresh tokens ... * ... if a client has the grant allowed and scope includes offline_access or the client is a @@ -2109,11 +2418,12 @@ function makeDefaults() { /* * jwks * - * description: JSON Web Key Set used by the authorization server for signing and decryption. The object must - * be in [JWK Set format](https://www.rfc-editor.org/rfc/rfc7517.html#section-5). All provided keys must - * be private keys. + * description: Specifies the JSON Web Key Set that shall be used by the authorization server + * for cryptographic signing and decryption operations. The key set MUST be provided in + * [JWK Set format](https://www.rfc-editor.org/rfc/rfc7517.html#section-5) as defined in + * RFC 7517. All keys within the set MUST be private keys. * - * Supported key types are: + * Supported key types include: * * - RSA * - OKP (Ed25519 and X25519 sub types) @@ -2140,10 +2450,10 @@ function makeDefaults() { /* * responseTypes * - * description: Array of response_type values that the authorization server supports. The default omits all response - * types that result in access tokens being issued by the authorization endpoint directly as per - * [`RFC9700 - Best Current Practice for OAuth 2.0 Security`](https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.2) - * You can still enable them if you need to. + * description: Specifies the response_type values supported by this authorization server. + * In accordance with RFC 9700 (OAuth 2.0 Security Best Current Practice), the default + * configuration excludes response types that result in access tokens being issued directly + * by the authorization endpoint. * * example: Supported values list * These are values defined in [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#Authentication) @@ -2164,7 +2474,7 @@ function makeDefaults() { * * title: [`RFC7636`](https://www.rfc-editor.org/rfc/rfc7636.html) - Proof Key for Code Exchange (`PKCE`) * - * description: `PKCE` configuration such as policy check on the required use of `PKCE` + * description: `PKCE` configuration such as policy check on the required use of `PKCE`. * * @nodefault */ @@ -2174,7 +2484,7 @@ function makeDefaults() { * * description: Configures if and when the authorization server requires clients to use `PKCE`. This helper is called * whenever an authorization request lacks the code_challenge parameter. - * Return + * Return: * - `false` to allow the request to continue without `PKCE` * - `true` to abort the request */ @@ -2184,12 +2494,14 @@ function makeDefaults() { /* * routes * - * description: Routing values used by the authorization server. Only provide routes starting with "/" + * description: Defines the URL path mappings for authorization server endpoints. + * All route values are relative and shall begin with a forward slash ("/") character. */ routes: { authorization: '/auth', backchannel_authentication: '/backchannel', code_verification: '/device', + challenge: '/challenge', device_authorization: '/device/auth', end_session: '/session/end', introspection: '/token/introspection', @@ -2204,38 +2516,45 @@ function makeDefaults() { /* * scopes * - * description: Array of additional scope values that the authorization server signals to support in the discovery - * endpoint. Only add scopes the authorization server has a corresponding resource for. - * Resource Server scopes don't belong here, see `features.resourceIndicators` for configuring - * those. + * description: Specifies additional OAuth 2.0 scope values that this authorization server + * shall support and advertise in its discovery document. Resource Server-specific + * scopes shall be configured via the `features.resourceIndicators` mechanism. */ scopes: ['openid', 'offline_access'], /* * subjectTypes * - * description: Array of the Subject Identifier types that this authorization server supports. When only `pairwise` - * is supported it becomes the default `subject_type` client metadata value. Valid types are - * - `public` - * - `pairwise` + * description: Specifies the array of Subject Identifier types that this authorization server + * shall support for end-user identification purposes. When only `pairwise` is supported, + * it shall become the default `subject_type` client metadata value. Supported identifier + * types shall include: + * - `public` - provides the same subject identifier value to all clients + * - `pairwise` - provides a unique subject identifier value per client to enhance privacy */ subjectTypes: ['public'], /* * pairwiseIdentifier * - * description: Function used by the authorization server when resolving pairwise ID Token and Userinfo sub claim - * values. See [`OIDC Core 1.0`](https://openid.net/specs/openid-connect-core-1_0-errata2.html#PairwiseAlg) - * 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. + * description: Specifies a helper function that shall be invoked to generate pairwise subject + * identifier values for ID Tokens and UserInfo responses, as specified in OpenID Connect + * Core 1.0. This function enables privacy-preserving subject identifier generation that + * provides unique identifiers per client while maintaining consistent identification for + * the same end-user across requests to the same client. + * + * recommendation: Implementations should employ memoization or caching mechanisms when + * this function may be invoked multiple times with identical arguments within a single request. */ pairwiseIdentifier, /* * clientAuthMethods * - * description: Array of supported Client Authentication methods + * description: Specifies the client authentication methods that this authorization server + * shall support for authenticating clients at the token endpoint and other authenticated + * endpoints. + * * example: Supported values list * ```js * [ @@ -2257,16 +2576,17 @@ function makeDefaults() { /* * ttl * - * description: 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. + * description: Specifies the Time-To-Live (TTL) values that shall be applied to various + * artifacts within the authorization server. TTL values may be specified + * as either a numeric value (in seconds) or a synchronous function that returns a + * numeric value based on the current request context and authorization server policy. * - * recommendation: Do not set token TTLs longer then they absolutely have to be, the shorter - * the TTL, the better. + * recommendation: Token TTL values should be set to the minimum duration necessary for + * the intended use case to minimize security exposure. * - * 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). + * recommendation: For refresh tokens requiring extended lifetimes, consider utilizing the + * `rotateRefreshToken` configuration option, which extends effective token lifetime through + * rotation rather than extended initial TTL values. * * example: 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 @@ -2301,28 +2621,34 @@ function makeDefaults() { /* * extraClientMetadata * - * description: 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']`) + * description: Specifies the configuration for custom client metadata properties that shall + * be supported by the authorization server for client registration and metadata validation purposes. + * This configuration enables extension of standard OAuth 2.0 and OpenID Connect client + * metadata with deployment-specific properties. Existing standards-defined properties are snakeCased on + * a Client instance (e.g. `client.redirectUris`), while new properties defined by this + * configuration shall be available with their names verbatim (e.g. + * `client['urn:example:client:my-property']`). * @nodefault */ extraClientMetadata: { /* * extraClientMetadata.properties * - * description: Array of property names that clients will be allowed to have defined. + * description: Specifies an array of property names that clients shall be allowed to have + * defined within their client metadata during registration and management operations. + * Each property name listed here extends the standard client metadata schema according + * to authorization server policy. */ properties: [], /* * extraClientMetadata.validator * - * description: 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. + * description: Specifies a validator function that shall be executed in order once for every + * property defined in `extraClientMetadata.properties`, regardless of its value or presence + * in the client metadata passed during registration or update operations. The function MUST + * be synchronous; async validators or functions returning Promise shall be rejected during + * runtime. To modify the current client metadata values (for the current key or any other) + * simply modify the passed in `metadata` argument within the validator function. */ validator: extraClientMetadataValidator, }, @@ -2330,17 +2656,20 @@ function makeDefaults() { /* * renderError * - * description: Function used to present errors to the User-Agent + * description: Specifies a function that shall be invoked to present error responses to the + * User-Agent during authorization server operations. This function enables customization + * of error presentation according to deployment-specific user interface requirements. */ renderError, /* * revokeGrantPolicy * - * description: Function called in a number of different context to determine whether - * an underlying Grant entry should also be revoked or not. - * - * contexts: + * description: Specifies a helper function that shall be invoked to determine whether an + * underlying Grant entry shall be revoked in addition to the specific token or code being + * processed. This function enables enforcement of grant revocation policies according to + * authorization server security requirements. The function is invoked in the following + * contexts: * - RP-Initiated Logout * - Opaque Access Token Revocation * - Refresh Token Revocation @@ -2354,17 +2683,19 @@ function makeDefaults() { /* * sectorIdentifierUriValidate * - * description: 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. + * description: Specifies a function that shall be invoked to determine whether the + * sectorIdentifierUri of a client being loaded, registered, or updated should be fetched + * and its contents validated against the client metadata. */ sectorIdentifierUriValidate, /* * interactions * - * description: Holds the configuration for interaction policy and a URL to send end-users to - * when the policy decides to require interaction. + * description: Specifies the configuration for interaction policy and end-user redirection + * that shall be applied to determine that user interaction + * is required during the authorization process. This configuration enables customization + * of authentication and consent flows according to deployment-specific requirements. * * @nodefault */ @@ -2372,8 +2703,11 @@ function makeDefaults() { /* * interactions.policy * - * description: 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 exported. + * description: Specifies the structure of Prompts and their associated checks that shall + * be applied during authorization request processing. The policy is formed by Prompt + * and Check class instances that define the conditions under which user interaction + * is required. The default policy implementation provides a fresh instance that can + * be customized, and the relevant classes are exported for configuration purposes. * * example: default interaction policy description * @@ -2406,7 +2740,7 @@ function makeDefaults() { * 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` + * 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. * @@ -2431,8 +2765,10 @@ function makeDefaults() { /* * interactions.url * - * description: Function used to determine where to redirect User-Agent for necessary - * interaction, can return both absolute and relative urls. + * description: Specifies a function that shall be invoked to determine the destination URL + * for redirecting the User-Agent when user interaction is required during authorization + * processing. This function enables customization of the interaction endpoint location + * and may return both absolute and relative URLs according to deployment requirements. */ url: interactionsUrl, }, @@ -2440,36 +2776,46 @@ function makeDefaults() { /* * findAccount * - * description: 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 + * description: Specifies a function that shall be invoked to load an account and retrieve + * its available claims during authorization server operations. This function enables + * the authorization server to resolve end-user account information based on the provided + * account identifier. The function MUST return a Promise that resolves to an account + * object containing an `accountId` property and a `claims()` method that returns an + * object with claims corresponding to the claims supported by the issuer. The `claims()` + * method may also return a Promise that shall be resolved or rejected according to + * account availability and authorization server policy. */ findAccount, /* * rotateRefreshToken * - * description: Configures if and how the authorization server rotates refresh tokens after they are used. Supported - * values are - * - `false` refresh tokens are not rotated and their initial expiration date is final - * - `true` 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 revoked - * - `function` returning true/false, true when rotation should occur, false when it shouldn't + * description: Specifies the refresh token rotation policy that shall be applied by the + * authorization server when refresh tokens are used. + * This configuration determines whether and under what conditions refresh tokens shall + * be rotated. Supported values + * include: + * - `false` - refresh tokens shall not be rotated and their initial expiration date is final + * - `true` - refresh tokens shall be rotated when used, with the current token marked as + * consumed and a new one issued with new TTL; when a consumed refresh token is + * encountered an error shall be returned and the whole token chain (grant) is revoked + * - `function` - a function returning true/false that shall be invoked to determine + * whether rotation should occur based on request context and authorization server policy * *

* - * The default configuration value puts forth a sensible refresh token rotation policy + * The default configuration value implements a sensible refresh token rotation policy that: * - 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) + * - otherwise always rotates public client tokens that are not sender-constrained + * - otherwise only rotates tokens if they're being used close to their expiration (>= 70% TTL passed) */ rotateRefreshToken, /* * enabledJWA * - * description: Fine-tune the algorithms the authorization server supports by declaring algorithm - * values for each respective JWA use + * description: Specifies the JSON Web Algorithm (JWA) values supported by this authorization + * server for various cryptographic operations, as defined in RFC 7518 and related specifications. * @nodefault */ enabledJWA: { @@ -2477,6 +2823,7 @@ function makeDefaults() { * enabledJWA.clientAuthSigningAlgValues * * description: JWS "alg" Algorithm values the authorization server supports for signed JWT Client Authentication + * (`private_key_jwt` and `client_secret_jwt`) * * example: Supported values list * ```js @@ -2860,23 +3207,45 @@ function makeDefaults() { * ``` */ dPoPSigningAlgValues: ['ES256', 'Ed25519', 'EdDSA'], + + /* + * enabledJWA.attestSigningAlgValues + * + * description: JWS "alg" Algorithm values the authorization server supports to verify signed Client Attestation and Client Attestation PoP JWTs with + * + * example: Supported values list + * ```js + * [ + * 'RS256', 'RS384', 'RS512', + * 'PS256', 'PS384', 'PS512', + * 'ES256', 'ES384', 'ES512', + * 'Ed25519', 'EdDSA', + * ] + * ``` + */ + attestSigningAlgValues: ['ES256', 'Ed25519', 'EdDSA'], }, - /** + /* * assertJwtClientAuthClaimsAndHeader * - * description: Helper function used to validate the JWT Client Authentication Assertion Claims Set and Header beyond - * what its specification mandates. + * description: Specifies a helper function that shall be invoked to perform additional + * validation of JWT Client Authentication assertion Claims Set and Header beyond the + * requirements mandated by the specification. This function enables enforcement of + * deployment-specific security policies and extended validation logic for `private_key_jwt` + * and `client_secret_jwt` client authentication methods according to authorization + * server requirements. */ assertJwtClientAuthClaimsAndHeader, /* * fetch * - * description: Function called whenever calls to an external HTTP(S) resource are being made. The interface as well - * as expected return is the [Fetch API's](https://fetch.spec.whatwg.org/) - * [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch). The default is using timeout of 2500ms - * and not sending a user-agent header. + * description: Specifies a function that shall be invoked whenever the authorization server + * needs to make calls to external HTTPS resources. The interface and expected return + * value shall conform to the [Fetch API specification](https://fetch.spec.whatwg.org/) + * [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) standard. + * The default implementation uses a timeout of 2500ms and does not send a user-agent header. * * example: To change the request's timeout * @@ -2893,11 +3262,14 @@ function makeDefaults() { */ fetch, - /** + /* * enableHttpPostMethods * - * description: Enables HTTP POST Method support at the Authorization Endpoint and the Logout Endpoint (if enabled). - * This setting can only be used when the `cookies.long.sameSite` configuration value is `none`. + * description: Specifies whether HTTP POST method support shall be enabled at the + * Authorization Endpoint and the Logout Endpoint (if enabled). When enabled, the + * authorization server shall accept POST requests at these endpoints in addition + * to the standard GET requests. This configuration may only be used when the + * `cookies.long.sameSite` configuration value is `none`. */ enableHttpPostMethods: false, }; diff --git a/lib/helpers/errors.js b/lib/helpers/errors.js index fdb7e632e..6c0592706 100644 --- a/lib/helpers/errors.js +++ b/lib/helpers/errors.js @@ -157,3 +157,5 @@ export const UnsupportedResponseMode = E('unsupported_response_mode', 'unsupport export const UnsupportedResponseType = E('unsupported_response_type', 'unsupported response_type requested'); export const UseDpopNonce = E('use_dpop_nonce'); export const UnsupportedTokenType = E('unsupported_token_type'); +export const UseAttestationChallenge = E('use_attestation_challenge'); +export const InvalidClientAttestation = E('invalid_client_attestation'); diff --git a/lib/helpers/features.js b/lib/helpers/features.js index 1733df4ad..3b10999bb 100644 --- a/lib/helpers/features.js +++ b/lib/helpers/features.js @@ -40,4 +40,8 @@ export const EXPERIMENTS = new Map(Object.entries({ name: 'OpenID Connect Relying Party Metadata Choices', version: ['draft-02'], }, + attestClientAuth: { + name: 'OAuth 2.0 Attestation-Based Client Authentication', + version: 'draft-06', + }, })); diff --git a/lib/helpers/initialize_app.js b/lib/helpers/initialize_app.js index 8e9d8f73d..7a2f64de7 100644 --- a/lib/helpers/initialize_app.js +++ b/lib/helpers/initialize_app.js @@ -10,7 +10,7 @@ import error from '../shared/error_handler.js'; import getAuthError from '../shared/authorization_error_handler.js'; import { getAuthorization, userinfo, getToken, jwks, registration, getRevocation, - getIntrospection, discovery, endSession, codeVerification, + getIntrospection, discovery, endSession, codeVerification, challenge, } from '../actions/index.js'; import als from './als.js'; @@ -18,15 +18,31 @@ import instance from './weak_cache.js'; export default function initializeApp() { const { configuration, features } = instance(this); - - const CORS_AUTHORIZATION = { exposeHeaders: ['WWW-Authenticate'], maxAge: 3600 }; - if (features.dPoP.enabled && features.dPoP.nonceSecret) { - CORS_AUTHORIZATION.exposeHeaders.push('DPoP-Nonce'); + const maxAge = 3600; + function exposeHeaders({ + dpop = features.dPoP.enabled && features.dPoP.nonceSecret, + attest = features.attestClientAuth.enabled, + wwwAuth = true, + } = {}) { + return [ + dpop ? 'DPoP-Nonce' : undefined, + attest ? 'OAuth-Client-Attestation-Challenge' : undefined, + wwwAuth ? 'WWW-Authenticate' : undefined, + ].filter(Boolean); } + const CORS = { - open: cors({ allowMethods: 'GET', maxAge: 3600 }), - userinfo: cors({ allowMethods: 'GET,POST', clientBased: true, ...CORS_AUTHORIZATION }), - client: cors({ allowMethods: 'POST', clientBased: true, ...CORS_AUTHORIZATION }), + open: cors({ allowMethods: 'GET', maxAge }), + challenge: cors({ allowMethods: 'POST', maxAge, exposeHeaders: exposeHeaders({ wwwAuth: false }) }), + userinfo: cors({ + allowMethods: 'GET,POST', clientBased: true, maxAge, exposeHeaders: exposeHeaders({ attest: false }), + }), + client: cors({ + allowMethods: 'POST', clientBased: true, maxAge, exposeHeaders: exposeHeaders({ dpop: false }), + }), + clientWithDPoP: cors({ + allowMethods: 'POST', clientBased: true, maxAge, exposeHeaders: exposeHeaders(), + }), respond: () => {}, }; @@ -138,8 +154,8 @@ export default function initializeApp() { } const token = getToken(this); - post('token', routes.token, error(this, 'grant.error'), CORS.client, ...token); - options('cors.token', routes.token, CORS.client, CORS.respond); + post('token', routes.token, error(this, 'grant.error'), CORS.clientWithDPoP, ...token); + options('cors.token', routes.token, CORS.clientWithDPoP, CORS.respond); get('jwks', routes.jwks, CORS.open, error(this, 'jwks.error'), jwks); options('cors.jwks', routes.jwks, CORS.open, CORS.respond); @@ -152,6 +168,11 @@ export default function initializeApp() { get('discovery', openidDiscoveryRoute, CORS.open, error(this, 'discovery.error'), discovery); options('cors.discovery', openidDiscoveryRoute, CORS.open, CORS.respond); + if (features.attestClientAuth.enabled) { + post('challenge', routes.challenge, error(this, 'challenge.error'), CORS.challenge, ...challenge); + options('cors.challenge', routes.challenge, CORS.challenge, CORS.respond); + } + if (features.registration.enabled) { const clientRoute = `${routes.registration}/:clientId`; @@ -201,8 +222,8 @@ export default function initializeApp() { if (features.pushedAuthorizationRequests.enabled) { const pushedAuthorizationRequests = getAuthorization(this, 'pushed_authorization_request'); - post('pushed_authorization_request', routes.pushed_authorization_request, error(this, 'pushed_authorization_request.error'), CORS.client, ...pushedAuthorizationRequests); - options('cors.pushed_authorization_request', routes.pushed_authorization_request, CORS.client, CORS.respond); + post('pushed_authorization_request', routes.pushed_authorization_request, error(this, 'pushed_authorization_request.error'), CORS.clientWithDPoP, ...pushedAuthorizationRequests); + options('cors.pushed_authorization_request', routes.pushed_authorization_request, CORS.clientWithDPoP, CORS.respond); } if (features.ciba.enabled) { diff --git a/lib/helpers/oidc_context.js b/lib/helpers/oidc_context.js index f3e815626..06aba85ba 100644 --- a/lib/helpers/oidc_context.js +++ b/lib/helpers/oidc_context.js @@ -38,9 +38,7 @@ export default function getContext(provider) { super(); this.ctx = ctx; this.route = ctx._matchedRouteName; - this.authorization = {}; this.redirectUriCheckPerformed = false; - this.webMessageUriCheckPerformed = false; this.entities = {}; this.claims = {}; this.resourceServers = {}; diff --git a/lib/helpers/process_response_types.js b/lib/helpers/process_response_types.js index fba0e2071..ad12e6bc4 100644 --- a/lib/helpers/process_response_types.js +++ b/lib/helpers/process_response_types.js @@ -93,6 +93,10 @@ async function codeHandler(ctx) { dpopJkt: ctx.oidc.params.dpop_jkt, }); + if (ctx.oidc.entities.PushedAuthorizationRequest?.attestationJkt) { + code.attestationJkt = ctx.oidc.entities.PushedAuthorizationRequest.attestationJkt; + } + if (richAuthorizationRequests.enabled) { code.rar = await richAuthorizationRequests.rarForAuthorizationCode(ctx); } diff --git a/lib/helpers/script_src_sha.js b/lib/helpers/script_src_sha.js index 74f165ba9..718b6b31b 100644 --- a/lib/helpers/script_src_sha.js +++ b/lib/helpers/script_src_sha.js @@ -1,7 +1,7 @@ import * as crypto from 'node:crypto'; export default function pushScriptSrcSha(ctx, script) { - const csp = ctx.response.get('Content-Security-Policy'); + const csp = ctx.response.get('content-security-policy'); if (csp) { const directives = csp.split(';').reduce((acc, directive) => { const [name, ...values] = directive.trim().split(/\s+/g); @@ -14,7 +14,7 @@ export default function pushScriptSrcSha(ctx, script) { directives['script-src'].push(`'sha256-${digest}'`); const replaced = Object.entries(directives).map(([name, values]) => [name, ...values].join(' ')).join(';'); - ctx.response.set('Content-Security-Policy', replaced); + ctx.set('content-security-policy', replaced); } } return script; diff --git a/lib/helpers/set_rt_bindings.js b/lib/helpers/set_rt_bindings.js new file mode 100644 index 000000000..e2de64650 --- /dev/null +++ b/lib/helpers/set_rt_bindings.js @@ -0,0 +1,21 @@ +/* eslint-disable no-param-reassign */ + +export async function setRefreshTokenBindings(ctx, at, rt) { + switch (ctx.oidc.client.clientAuthMethod) { + case 'none': + if (at.jkt) { + rt.jkt = at.jkt; + } + + if (at['x5t#S256']) { + rt['x5t#S256'] = at['x5t#S256']; + } + break; + case 'attest_jwt_client_auth': { + await rt.setAttestBinding(ctx); + break; + } + default: + break; + } +} diff --git a/lib/helpers/validate_dpop.js b/lib/helpers/validate_dpop.js index 4bd109cad..4c22eaf92 100644 --- a/lib/helpers/validate_dpop.js +++ b/lib/helpers/validate_dpop.js @@ -9,9 +9,11 @@ import { import { InvalidDpopProof, UseDpopNonce } from './errors.js'; import instance from './weak_cache.js'; import epochTime from './epoch_time.js'; +import { CHALLENGE_OK_WINDOW } from './challenge.js'; + +export { CHALLENGE_OK_WINDOW }; const weakMap = new WeakMap(); -export const DPOP_OK_WINDOW = 300; export default async (ctx, accessToken) => { if (weakMap.has(ctx)) { @@ -43,7 +45,7 @@ export default async (ctx, accessToken) => { throw new Error('features.dPoP.nonceSecret configuration is missing'); } - const nextNonce = DPoPNonces?.nextNonce(); + const nextNonce = DPoPNonces?.nextChallenge(); let payload; let protectedHeader; try { @@ -64,9 +66,9 @@ export default async (ctx, accessToken) => { if (!payload.nonce) { const now = epochTime(); const diff = Math.abs(now - payload.iat); - if (diff > DPOP_OK_WINDOW) { + if (diff > CHALLENGE_OK_WINDOW) { if (nextNonce) { - ctx.set('DPoP-Nonce', nextNonce); + ctx.set('dpop-nonce', nextNonce); throw new UseDpopNonce('DPoP proof iat is not recent enough, use a DPoP nonce instead'); } throw new InvalidDpopProof('DPoP proof iat is not recent enough'); @@ -105,17 +107,17 @@ export default async (ctx, accessToken) => { } if (!payload.nonce && requireNonce) { - ctx.set('DPoP-Nonce', nextNonce); + ctx.set('dpop-nonce', nextNonce); throw new UseDpopNonce('nonce is required in the DPoP proof'); } - if (payload.nonce && !DPoPNonces.checkNonce(payload.nonce)) { - ctx.set('DPoP-Nonce', nextNonce); + if (payload.nonce && !DPoPNonces.checkChallenge(payload.nonce)) { + ctx.set('dpop-nonce', nextNonce); throw new UseDpopNonce('invalid nonce in DPoP proof'); } if (payload.nonce !== nextNonce) { - ctx.set('DPoP-Nonce', nextNonce); + ctx.set('dpop-nonce', nextNonce); } const thumbprint = await calculateJwkThumbprint(protectedHeader.jwk); diff --git a/lib/models/authorization_code.js b/lib/models/authorization_code.js index 84381e896..efc1c1b5e 100644 --- a/lib/models/authorization_code.js +++ b/lib/models/authorization_code.js @@ -2,6 +2,7 @@ import apply from './mixins/apply.js'; import consumable from './mixins/consumable.js'; import hasFormat from './mixins/has_format.js'; import hasGrantId from './mixins/has_grant_id.js'; +import isAttestationConstrained from './mixins/is_attestation_constrained.js'; import isSessionBound from './mixins/is_session_bound.js'; import storesAuth from './mixins/stores_auth.js'; import storesPKCE from './mixins/stores_pkce.js'; @@ -10,6 +11,7 @@ export default (provider) => class AuthorizationCode extends apply([ consumable, isSessionBound(provider), hasGrantId, + isAttestationConstrained, storesAuth, storesPKCE, hasFormat(provider, 'AuthorizationCode', provider.BaseToken), diff --git a/lib/models/backchannel_authentication_request.js b/lib/models/backchannel_authentication_request.js index 08ab9f2f1..c7246d7c0 100644 --- a/lib/models/backchannel_authentication_request.js +++ b/lib/models/backchannel_authentication_request.js @@ -2,12 +2,14 @@ import apply from './mixins/apply.js'; import consumable from './mixins/consumable.js'; import hasFormat from './mixins/has_format.js'; import hasGrantId from './mixins/has_grant_id.js'; +import isAttestationConstrained from './mixins/is_attestation_constrained.js'; import isSessionBound from './mixins/is_session_bound.js'; import storesAuth from './mixins/stores_auth.js'; export default (provider) => class BackchannelAuthenticationRequest extends apply([ consumable, hasGrantId, + isAttestationConstrained, isSessionBound(provider), storesAuth, hasFormat(provider, 'BackchannelAuthenticationRequest', provider.BaseToken), diff --git a/lib/models/client.js b/lib/models/client.js index 230dcae9b..b593b59c8 100644 --- a/lib/models/client.js +++ b/lib/models/client.js @@ -33,7 +33,13 @@ const validateJWKS = (jwks) => { } }; -const nonSecretAuthMethods = new Set(['private_key_jwt', 'none', 'tls_client_auth', 'self_signed_tls_client_auth']); +const nonSecretAuthMethods = new Set([ + 'private_key_jwt', + 'none', + 'tls_client_auth', + 'self_signed_tls_client_auth', + 'attest_jwt_client_auth', +]); const clientEncryptions = [ 'id_token_encrypted_response_alg', 'request_object_encryption_alg', diff --git a/lib/models/device_code.js b/lib/models/device_code.js index 42f7707f4..a402380f4 100644 --- a/lib/models/device_code.js +++ b/lib/models/device_code.js @@ -4,12 +4,14 @@ import apply from './mixins/apply.js'; import consumable from './mixins/consumable.js'; import hasFormat from './mixins/has_format.js'; import hasGrantId from './mixins/has_grant_id.js'; +import isAttestationConstrained from './mixins/is_attestation_constrained.js'; import isSessionBound from './mixins/is_session_bound.js'; import storesAuth from './mixins/stores_auth.js'; export default (provider) => class DeviceCode extends apply([ consumable, hasGrantId, + isAttestationConstrained, isSessionBound(provider), storesAuth, hasFormat(provider, 'DeviceCode', provider.BaseToken), diff --git a/lib/models/mixins/is_attestation_constrained.js b/lib/models/mixins/is_attestation_constrained.js new file mode 100644 index 000000000..2dbd2eb07 --- /dev/null +++ b/lib/models/mixins/is_attestation_constrained.js @@ -0,0 +1,15 @@ +import * as jose from 'jose'; + +export default (superclass) => class extends superclass { + static get IN_PAYLOAD() { + return [ + ...super.IN_PAYLOAD, + 'attestationJkt', + ]; + } + + async setAttestBinding(ctx) { + const { cnf: { jwk } } = jose.decodeJwt(ctx.get('oauth-client-attestation')); + this.attestationJkt = await jose.calculateJwkThumbprint(jwk); + } +}; diff --git a/lib/models/pushed_authorization_request.js b/lib/models/pushed_authorization_request.js index 4b0fc3547..557a01cff 100644 --- a/lib/models/pushed_authorization_request.js +++ b/lib/models/pushed_authorization_request.js @@ -3,9 +3,11 @@ import instance from '../helpers/weak_cache.js'; import apply from './mixins/apply.js'; import hasFormat from './mixins/has_format.js'; import consumable from './mixins/consumable.js'; +import isAttestationConstrained from './mixins/is_attestation_constrained.js'; export default (provider) => class PushedAuthorizationRequest extends apply([ consumable, + isAttestationConstrained, hasFormat(provider, 'PushedAuthorizationRequest', instance(provider).BaseModel), ]) { static get IN_PAYLOAD() { diff --git a/lib/models/refresh_token.js b/lib/models/refresh_token.js index 9b69692b6..362db902a 100644 --- a/lib/models/refresh_token.js +++ b/lib/models/refresh_token.js @@ -6,6 +6,7 @@ import hasFormat from './mixins/has_format.js'; import hasGrantId from './mixins/has_grant_id.js'; import hasGrantType from './mixins/has_grant_type.js'; import isSenderConstrained from './mixins/is_sender_constrained.js'; +import isAttestationConstrained from './mixins/is_attestation_constrained.js'; import isSessionBound from './mixins/is_session_bound.js'; import storesAuth from './mixins/stores_auth.js'; @@ -14,6 +15,7 @@ export default (provider) => class RefreshToken extends apply([ hasGrantType, hasGrantId, isSenderConstrained, + isAttestationConstrained, isSessionBound(provider), storesAuth, hasFormat(provider, 'RefreshToken', provider.BaseToken), diff --git a/lib/provider.js b/lib/provider.js index f673e423c..eb148a682 100644 --- a/lib/provider.js +++ b/lib/provider.js @@ -17,7 +17,7 @@ import getClaims from './helpers/claims.js'; import getContext from './helpers/oidc_context.js'; import { SessionNotFound, OIDCProviderError } from './helpers/errors.js'; import * as models from './models/index.js'; -import DPoPNonces from './helpers/dpop_nonces.js'; +import ServerChallenge from './helpers/challenge.js'; import als from './helpers/als.js'; export class Provider extends Koa { @@ -96,8 +96,26 @@ export class Provider extends Koa { this.keys = configuration.cookies.keys; } - if (configuration.features.dPoP.nonceSecret !== undefined) { - this.#int.DPoPNonces = new DPoPNonces(configuration.features.dPoP.nonceSecret); + if ( + configuration.features.dPoP.enabled + && configuration.features.dPoP.nonceSecret !== undefined + ) { + try { + this.#int.DPoPNonces = new ServerChallenge(configuration.features.dPoP.nonceSecret, 'DPoP'); + } catch (cause) { + throw new TypeError('features.dPoP.nonceSecret secret must be a 32-byte Buffer instance', { cause }); + } + } + + if (configuration.features.attestClientAuth.enabled) { + try { + this.#int.AttestChallenges = new ServerChallenge( + configuration.features.attestClientAuth.challengeSecret, + 'OAuth-Client-Attestation-PoP', + ); + } catch (cause) { + throw new TypeError('features.attestClientAuth.challengeSecret secret must be a 32-byte Buffer instance', { cause }); + } } this.#int.responseModes = new Map(); diff --git a/lib/response_modes/web_message.js b/lib/response_modes/web_message.js index f64d40084..609e34eff 100644 --- a/lib/response_modes/web_message.js +++ b/lib/response_modes/web_message.js @@ -11,10 +11,10 @@ export default function webMessage(ctx, redirectUri, response) { ctx.status = 'error' in response ? 400 : 200; } - ctx.response.remove('X-Frame-Options'); - const csp = ctx.response.get('Content-Security-Policy'); + ctx.response.remove('x-frame-options'); + const csp = ctx.response.get('content-security-policy'); if (csp?.includes('frame-ancestors')) { - ctx.response.set('Content-Security-Policy', csp.split(';') + ctx.set('content-security-policy', csp.split(';') .filter((directive) => !directive.includes('frame-ancestors')) .join(';')); } diff --git a/lib/shared/attest_client_auth.js b/lib/shared/attest_client_auth.js new file mode 100644 index 000000000..8a52eb7bd --- /dev/null +++ b/lib/shared/attest_client_auth.js @@ -0,0 +1,106 @@ +import * as jose from 'jose'; + +import { InvalidClientAuth, UseAttestationChallenge } from '../helpers/errors.js'; +import instance from '../helpers/weak_cache.js'; +import { CHALLENGE_OK_WINDOW } from '../helpers/challenge.js'; +import epochTime from '../helpers/epoch_time.js'; + +export default async function attestationClientAuth(ctx) { + const { + configuration: { + clockTolerance, + features: { attestClientAuth }, + attestSigningAlgValues, + }, + AttestChallenges, + } = instance(ctx.oidc.provider); + + const nextChallenge = AttestChallenges.nextChallenge(); + + const attestation = ctx.get('oauth-client-attestation'); + let verifiedAttestation; + try { + verifiedAttestation = await jose.jwtVerify( + attestation, + async (header) => { + const payload = jose.decodeJwt(attestation); + if (typeof payload.iss !== 'string') { + throw new Error('iss must be a string'); + } + const key = await attestClientAuth.getAttestationSignaturePublicKey( + ctx, + payload.iss, + header, + ctx.oidc.client, + ); + return key; + }, + { + algorithms: attestSigningAlgValues, + requiredClaims: ['iss', 'sub', 'exp', 'cnf'], + typ: 'oauth-client-attestation+jwt', + clockTolerance, + subject: ctx.oidc.client.clientId, + }, + ); + if (verifiedAttestation.key.type !== 'public') { + throw new Error('the resolved key must be a public key'); + } + if (typeof verifiedAttestation.payload.cnf?.jwk?.kty !== 'string' || verifiedAttestation.payload.cnf?.jwk?.d !== undefined || verifiedAttestation.payload.cnf?.jwk?.k !== undefined) { + throw new Error('invalid cnf.jwk'); + } + } catch (err) { + throw new InvalidClientAuth(`failed to validate oauth-client-attestation: ${err.message}`); + } + + const pop = ctx.get('oauth-client-attestation-pop'); + if (!pop) { + throw new InvalidClientAuth('oauth-client-attestation-pop missing'); + } + let verifiedPoP; + try { + verifiedPoP = await jose.jwtVerify( + pop, + async (header) => jose.importJWK(verifiedAttestation.payload.cnf.jwk, header.alg), + { + algorithms: attestSigningAlgValues, + requiredClaims: ['iss', 'aud', 'jti'], // challenge is checked later + typ: 'oauth-client-attestation-pop+jwt', + clockTolerance, + issuer: ctx.oidc.client.clientId, + audience: ctx.oidc.issuer, + }, + ); + if (typeof verifiedPoP.payload.aud !== 'string') { + throw new Error('aud must be a string'); + } + } catch (err) { + throw new InvalidClientAuth(`failed to validate oauth-client-attestation-pop: ${err.message}`); + } + + await attestClientAuth.assertAttestationJwtAndPop( + ctx, + verifiedAttestation, + verifiedPoP, + ctx.oidc.client, + ); + + const unique = await ctx.oidc.provider.ReplayDetection.unique( + verifiedPoP.payload.iss, + verifiedPoP.payload.jti, + epochTime() + CHALLENGE_OK_WINDOW, + ); + + if (!unique) { + throw new InvalidClientAuth('oauth-client-attestation-pop tokens must only be used once'); + } + + if (typeof verifiedPoP.payload.challenge !== 'string' || !AttestChallenges.checkChallenge(verifiedPoP.payload.challenge)) { + ctx.set('oauth-client-attestation-challenge', nextChallenge); + throw new UseAttestationChallenge(); + } + + if (verifiedPoP.payload.challenge !== nextChallenge) { + ctx.set('oauth-client-attestation-challenge', nextChallenge); + } +} diff --git a/lib/shared/client_auth.js b/lib/shared/client_auth.js index d0880cab2..3f68fddb4 100644 --- a/lib/shared/client_auth.js +++ b/lib/shared/client_auth.js @@ -6,7 +6,8 @@ import certificateThumbprint from '../helpers/certificate_thumbprint.js'; import { noVSCHAR } from '../consts/client_attributes.js'; import rejectDupes from './reject_dupes.js'; -import getJwtClientAuth from './jwt_client_auth.js'; +import jwtClientAuth from './jwt_client_auth.js'; +import attestClientAuth from './attest_client_auth.js'; const assertionType = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; @@ -20,7 +21,6 @@ function decodeAuthToken(token) { } export default function clientAuthentication(provider) { - const jwtClientAuth = getJwtClientAuth(provider); const authParams = new Set(['client_id']); const { configuration, features } = instance(provider); @@ -48,7 +48,7 @@ export default function clientAuthentication(provider) { try { await next(); } catch (err) { - if (err.statusCode === 401 && ctx.header.authorization !== undefined) { + if (err.statusCode === 401 && ctx.headers.authorization !== undefined) { appendWWWAuthenticate(ctx, 'Basic', { realm: provider.issuer, error: err.message, @@ -58,17 +58,37 @@ export default function clientAuthentication(provider) { throw err; } }, - async function findClientId(ctx, next) { - const { - params: { - client_id: clientId, - client_assertion: clientAssertion, - client_assertion_type: clientAssertionType, - client_secret: clientSecret, - }, - } = ctx.oidc; - - if (ctx.headers.authorization !== undefined) { + async function authenticateClient(ctx, next) { + let methods; + let clientId; + let clientSecret; + + const setClientId = (value) => { + if (clientId !== undefined && value !== clientId) { + throw new InvalidRequest('client_id mismatch'); + } + clientId = value; + }; + + const { length } = [ + ctx.headers.authorization, + ctx.headers['oauth-client-attestation'], + ctx.oidc.params.client_assertion, + ctx.oidc.params.client_secret, + ].filter(Boolean); + + if (length > 1) { + throw new InvalidRequest('client authentication must only be provided using one mechanism'); + } + + if (ctx.oidc.params.client_id !== undefined) { + setClientId(ctx.oidc.params.client_id); + } + + if (ctx.oidc.params.client_secret) { + clientSecret = ctx.oidc.params.client_secret; + methods = ['client_secret_basic', 'client_secret_post']; + } else if (ctx.headers.authorization !== undefined) { const parts = ctx.headers.authorization.split(' '); if (parts.length !== 2 || parts[0].toLowerCase() !== 'basic') { throw new InvalidRequest('invalid authorization header value format'); @@ -81,41 +101,39 @@ export default function clientAuthentication(provider) { throw new InvalidRequest('invalid authorization header value format'); } + let basicClientId; try { - ctx.oidc.authorization.clientId = decodeAuthToken(basic.slice(0, i)); - ctx.oidc.authorization.clientSecret = decodeAuthToken(basic.slice(i + 1)); + basicClientId = decodeAuthToken(basic.slice(0, i)); + clientSecret = decodeAuthToken(basic.slice(i + 1)); } catch (err) { throw new InvalidRequest('client_id and client_secret in the authorization header are not properly encoded'); } - if (clientId !== undefined && ctx.oidc.authorization.clientId !== clientId) { - throw new InvalidRequest('mismatch in body and authorization client ids'); - } + setClientId(basicClientId); - if (!ctx.oidc.authorization.clientSecret) { + if (!clientSecret) { throw new InvalidRequest('client_secret must be provided in the Authorization header'); } - if (clientSecret !== undefined) { - throw new InvalidRequest('client authentication must only be provided using one mechanism'); + methods = ['client_secret_basic', 'client_secret_post']; + } else if (ctx.headers['oauth-client-attestation'] !== undefined) { + let sub; + try { + ({ payload: { sub } } = JWT.decode(ctx.headers['oauth-client-attestation'])); + } catch (err) { + throw new InvalidRequest('invalid OAuth-Client-Attestation format'); } - ctx.oidc.authorization.methods = ['client_secret_basic', 'client_secret_post']; - } else if (clientId !== undefined) { - ctx.oidc.authorization.clientId = clientId; - ctx.oidc.authorization.methods = clientSecret - ? ['client_secret_basic', 'client_secret_post'] - : ['none', 'tls_client_auth', 'self_signed_tls_client_auth']; - } - - if (clientAssertion !== undefined) { - if (clientSecret !== undefined || ctx.headers.authorization !== undefined) { - throw new InvalidRequest('client authentication must only be provided using one mechanism'); + if (!sub) { + throw new InvalidClientAuth('sub (JWT subject) must be provided in the OAuth-Client-Attestation JWT'); } + setClientId(sub); + methods = ['attest_jwt_client_auth']; + } else if (ctx.oidc.params.client_assertion !== undefined) { let sub; try { - ({ payload: { sub } } = JWT.decode(clientAssertion)); + ({ payload: { sub } } = JWT.decode(ctx.oidc.params.client_assertion)); } catch (err) { throw new InvalidRequest('invalid client_assertion format'); } @@ -124,30 +142,25 @@ export default function clientAuthentication(provider) { throw new InvalidClientAuth('sub (JWT subject) must be provided in the client_assertion JWT'); } - if (clientId && sub !== clientId) { - throw new InvalidRequest('subject of client_assertion must be the same as client_id provided in the body'); - } - - if (clientAssertionType === undefined) { + if (ctx.oidc.params.client_assertion_type === undefined) { throw new InvalidRequest('client_assertion_type must be provided'); } - if (clientAssertionType !== assertionType) { + if (ctx.oidc.params.client_assertion_type !== assertionType) { throw new InvalidRequest(`client_assertion_type must have value ${assertionType}`); } - ctx.oidc.authorization.clientId = sub; - ctx.oidc.authorization.methods = ['client_secret_jwt', 'private_key_jwt']; + setClientId(sub); + methods = ['client_secret_jwt', 'private_key_jwt']; + } else { + methods = ['none', 'tls_client_auth', 'self_signed_tls_client_auth']; } - if (!ctx.oidc.authorization.clientId) { + if (!clientId) { throw new InvalidRequest('no client authentication mechanism provided'); } - return next(); - }, - async function loadClient(ctx, next) { - const client = await provider.Client.find(ctx.oidc.authorization.clientId); + const client = await provider.Client.find(clientId); if (!client) { throw new InvalidClientAuth('client not found'); @@ -155,34 +168,18 @@ export default function clientAuthentication(provider) { ctx.oidc.entity('Client', client); - await next(); - }, - async function auth(ctx, next) { - const { - params, - client: { - clientAuthMethod, - clientAuthSigningAlg, - }, - authorization: { - methods, - clientSecret, - }, - } = ctx.oidc; - - if (!methods.includes(clientAuthMethod)) { + if (methods?.includes(ctx.oidc.client.clientAuthMethod) !== true) { throw new InvalidClientAuth('the provided authentication mechanism does not match the registered client authentication method'); } - switch (clientAuthMethod) { // eslint-disable-line default-case + switch (ctx.oidc.client.clientAuthMethod) { // eslint-disable-line default-case case 'none': break; case 'client_secret_basic': case 'client_secret_post': { ctx.oidc.client.checkClientSecretExpiration('could not authenticate the client - its client secret is expired'); - const actual = params.client_secret || clientSecret; - const matches = await ctx.oidc.client.compareClientSecret(actual); + const matches = await ctx.oidc.client.compareClientSecret(clientSecret); if (!matches) { throw new InvalidClientAuth('invalid secret provided'); } @@ -192,20 +189,12 @@ export default function clientAuthentication(provider) { case 'client_secret_jwt': ctx.oidc.client.checkClientSecretExpiration('could not authenticate the client - its client secret used for the client_assertion is expired'); - await jwtClientAuth( - ctx, - ctx.oidc.client.symmetricKeyStore, - clientAuthSigningAlg ? [clientAuthSigningAlg] : configuration.clientAuthSigningAlgValues.filter((alg) => alg.startsWith('HS')), - ); + await jwtClientAuth(ctx, ctx.oidc.client.symmetricKeyStore, (alg) => alg.startsWith('HS')); break; case 'private_key_jwt': - await jwtClientAuth( - ctx, - ctx.oidc.client.asymmetricKeyStore, - clientAuthSigningAlg ? [clientAuthSigningAlg] : configuration.clientAuthSigningAlgValues.filter((alg) => !alg.startsWith('HS')), - ); + await jwtClientAuth(ctx, ctx.oidc.client.asymmetricKeyStore, (alg) => !alg.startsWith('HS')); break; @@ -258,6 +247,11 @@ export default function clientAuthentication(provider) { throw new InvalidClientAuth('unregistered client certificate provided'); } + break; + } + case 'attest_jwt_client_auth': { + await attestClientAuth(ctx); + break; } } diff --git a/lib/shared/jwt_client_auth.js b/lib/shared/jwt_client_auth.js index 675485031..883ac59dc 100644 --- a/lib/shared/jwt_client_auth.js +++ b/lib/shared/jwt_client_auth.js @@ -2,72 +2,78 @@ import { InvalidClientAuth } from '../helpers/errors.js'; import instance from '../helpers/weak_cache.js'; import * as JWT from '../helpers/jwt.js'; -export default function getJwtClientAuth(provider) { - const { clockTolerance, assertJwtClientAuthClaimsAndHeader } = instance(provider).configuration; - return async function jwtClientAuth( - ctx, - keystore, - algorithms, - ) { - const acceptedAud = ctx.oidc.clientJwtAuthExpectedAudience(); - const { header, payload } = JWT.decode(ctx.oidc.params.client_assertion); +export default async function jwtClientAuth(ctx, keystore, filter) { + const { + clockTolerance, + assertJwtClientAuthClaimsAndHeader, + clientAuthSigningAlgValues, + } = instance(ctx.oidc.provider).configuration; + + const acceptedAud = ctx.oidc.clientJwtAuthExpectedAudience(); + const { header, payload } = JWT.decode(ctx.oidc.params.client_assertion); + if (ctx.oidc.client.clientAuthSigningAlg) { + if (header.alg !== ctx.oidc.client.clientAuthSigningAlg) { + throw new InvalidClientAuth('alg mismatch'); + } + } else { + const algorithms = clientAuthSigningAlgValues.filter(filter); if (!algorithms.includes(header.alg)) { throw new InvalidClientAuth('alg mismatch'); } + } - if (!payload.exp) { - throw new InvalidClientAuth('expiration must be specified in the client_assertion JWT'); - } + if (!payload.exp) { + throw new InvalidClientAuth('expiration must be specified in the client_assertion JWT'); + } - if (!payload.jti) { - throw new InvalidClientAuth('unique jti (JWT ID) must be provided in the client_assertion JWT'); - } + if (!payload.jti) { + throw new InvalidClientAuth('unique jti (JWT ID) must be provided in the client_assertion JWT'); + } - if (!payload.iss) { - throw new InvalidClientAuth('iss (JWT issuer) must be provided in the client_assertion JWT'); - } + if (!payload.iss) { + throw new InvalidClientAuth('iss (JWT issuer) must be provided in the client_assertion JWT'); + } - if (payload.iss !== ctx.oidc.client.clientId) { - throw new InvalidClientAuth('iss (JWT issuer) must be the client_id'); - } + if (payload.iss !== ctx.oidc.client.clientId) { + throw new InvalidClientAuth('iss (JWT issuer) must be the client_id'); + } - if (!payload.aud) { - throw new InvalidClientAuth('aud (JWT audience) must be provided in the client_assertion JWT'); - } + if (!payload.aud) { + throw new InvalidClientAuth('aud (JWT audience) must be provided in the client_assertion JWT'); + } - if (Array.isArray(payload.aud)) { - if (!payload.aud.some((aud) => acceptedAud.has(aud))) { - throw new InvalidClientAuth('list of audience (aud) must include the endpoint url, issuer identifier or token endpoint url'); - } - } else if (!acceptedAud.has(payload.aud)) { - throw new InvalidClientAuth('audience (aud) must equal the endpoint url, issuer identifier or token endpoint url'); + if (Array.isArray(payload.aud)) { + if (!payload.aud.some((aud) => acceptedAud.has(aud))) { + throw new InvalidClientAuth('list of audience (aud) must include the endpoint url, issuer identifier or token endpoint url'); } + } else if (!acceptedAud.has(payload.aud)) { + throw new InvalidClientAuth('audience (aud) must equal the endpoint url, issuer identifier or token endpoint url'); + } - try { - await JWT.verify(ctx.oidc.params.client_assertion, keystore, { - clockTolerance, - ignoreAzp: true, - }); - } catch (err) { - throw new InvalidClientAuth(err.message); - } + try { + await JWT.verify(ctx.oidc.params.client_assertion, keystore, { + clockTolerance, + ignoreAzp: true, + }); + } catch (err) { + throw new InvalidClientAuth(err.message); + } - await assertJwtClientAuthClaimsAndHeader( - ctx, - structuredClone(payload), - structuredClone(header), - ctx.oidc.client, - ); + await assertJwtClientAuthClaimsAndHeader( + ctx, + structuredClone(payload), + structuredClone(header), + ctx.oidc.client, + ); - const unique = await provider.ReplayDetection.unique( - payload.iss, - payload.jti, - payload.exp + clockTolerance, - ); + const unique = await ctx.oidc.provider.ReplayDetection.unique( + payload.iss, + payload.jti, + payload.exp + clockTolerance, + ); - if (!unique) { - throw new InvalidClientAuth('client assertion tokens must only be used once'); - } - }; + if (!unique) { + throw new InvalidClientAuth('client assertion tokens must only be used once'); + } } diff --git a/test/attest_bindings/attest_bindings.config.js b/test/attest_bindings/attest_bindings.config.js new file mode 100644 index 000000000..32d011762 --- /dev/null +++ b/test/attest_bindings/attest_bindings.config.js @@ -0,0 +1,60 @@ +import { randomBytes, generateKeyPairSync } from 'node:crypto'; + +import merge from 'lodash/merge.js'; + +import getConfig from '../default.config.js'; + +const config = getConfig(); + +const attestationKeyPair = generateKeyPairSync('ed25519'); + +config.clientAuthMethods = [ + 'attest_jwt_client_auth', +]; +merge(config.features, { + introspection: { + enabled: true, + }, + revocation: { + enabled: true, + }, + attestClientAuth: { + enabled: true, + challengeSecret: randomBytes(32), + getAttestationSignaturePublicKey(ctx, iss, header, client) { + if (iss === 'https://attester.example.com') { + return client.jwks.keys[0]; + } + throw new Error('unexpected attestation jwt issuer'); + }, + }, + deviceFlow: { + enabled: true, + }, + ciba: { + enabled: true, + validateRequestContext() {}, + verifyUserCode() {}, + triggerAuthenticationDevice() {}, + processLoginHint() { + return 'sub'; + }, + }, +}); + +export default { + attestationKeyPair, + config, + clients: [{ + client_id: 'client', + redirect_uris: ['https://rp.example.com/cb'], + grant_types: ['authorization_code', 'refresh_token', 'urn:ietf:params:oauth:grant-type:device_code', 'urn:openid:params:grant-type:ciba'], + token_endpoint_auth_method: 'attest_jwt_client_auth', + backchannel_token_delivery_mode: 'poll', + jwks: { + keys: [ + attestationKeyPair.publicKey.export({ format: 'jwk' }), + ], + }, + }], +}; diff --git a/test/attest_bindings/attest_bindings.test.js b/test/attest_bindings/attest_bindings.test.js new file mode 100644 index 000000000..853d8219a --- /dev/null +++ b/test/attest_bindings/attest_bindings.test.js @@ -0,0 +1,372 @@ +import { generateKeyPairSync } from 'node:crypto'; + +import { createSandbox } from 'sinon'; +import { expect } from 'chai'; +import timekeeper from 'timekeeper'; +import { SignJWT } from 'jose'; + +import bootstrap, { skipConsent } from '../test_helper.js'; + +const sinon = createSandbox(); + +function errorDetail(spy) { + return spy.args[0][1].error_detail; +} + +function attestation(instanceKeyPair) { + return new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'Ed25519', + }) + .setIssuer('https://attester.example.com') + .setSubject('client') + .setExpirationTime('2h') + .sign(this.config.attestationKeyPair.privateKey); +} + +function pop(instanceKeyPair) { + const challenge = i(this.provider).AttestChallenges.nextChallenge(); + return new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('client') + .setAudience(this.provider.issuer) + .setJti(crypto.randomUUID()) + .sign(instanceKeyPair.privateKey); +} + +describe('attest_jwt_client_auth bindings', () => { + before(bootstrap(import.meta.url)); + + afterEach(() => timekeeper.reset()); + afterEach(function () { + this.provider.removeAllListeners(); + }); + afterEach(sinon.restore); + + beforeEach(function () { return this.login({ scope: 'openid email offline_access' }); }); + afterEach(function () { return this.logout(); }); + skipConsent(); + + afterEach(function () { + this.provider.removeAllListeners(); + }); + + it('refresh token binding', async function () { + const instanceKeyPair = generateKeyPairSync('ed25519'); + const { headers: { location } } = await this.agent.get('/auth') + .query({ + client_id: 'client', + scope: 'offline_access', + prompt: 'consent', + response_type: 'code', + }) + .expect(303); + + const callback = new URL(location).searchParams; + expect(callback.has('code')).to.be.true; + const code = new URL(location).searchParams.get('code'); + + const { body: { refresh_token } } = await this.agent.post('/token') + .send({ + code, + grant_type: 'authorization_code', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + + await this.agent.post('/token') + .send({ + refresh_token, + grant_type: 'refresh_token', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + + const spy = sinon.spy(); + this.provider.once('grant.error', spy); + const instanceKeyPair2 = generateKeyPairSync('ed25519'); + await this.agent.post('/token') + .send({ + refresh_token, + grant_type: 'refresh_token', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair2)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair2)) + .expect(400) + .expect(() => { + expect(spy.calledOnce).to.be.true; + expect(errorDetail(spy)).to.equal('oauth-client-attestation instance public key mismatch'); + }); + + { + const { body: { active } } = await this.agent.post('/token/introspection') + .send({ + token: refresh_token, + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair2)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair2)) + .expect(200); + + expect(active).to.be.false; + + await this.agent.post('/token/revocation') + .send({ + token: refresh_token, + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair2)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair2)) + .expect(200); + } + + { + const { body: { active } } = await this.agent.post('/token/introspection') + .send({ + token: refresh_token, + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + + expect(active).to.be.true; + + await this.agent.post('/token/revocation') + .send({ + token: refresh_token, + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + } + + { + const { body: { active } } = await this.agent.post('/token/introspection') + .send({ + token: refresh_token, + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + + expect(active).to.be.false; + } + }); + + it('PAR request_uri binding', async function () { + const instanceKeyPair = generateKeyPairSync('ed25519'); + + const { body: { request_uri } } = await this.agent.post('/request') + .send({ + client_id: 'client', + scope: 'offline_access', + prompt: 'consent', + response_type: 'code', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(201); + + const { headers: { location } } = await this.agent.get('/auth') + .query({ + client_id: 'client', + request_uri, + }) + .expect(303); + + const callback = new URL(location).searchParams; + expect(callback.has('code')).to.be.true; + const code = new URL(location).searchParams.get('code'); + + const { body: { refresh_token } } = await this.agent.post('/token') + .send({ + code, + grant_type: 'authorization_code', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + + expect(this.TestAdapter.for('RefreshToken').syncFind(refresh_token)).to.have.property('attestationJkt').and.is.string; + }); + + it('PAR request_uri binding (instance mismatch)', async function () { + const instanceKeyPair = generateKeyPairSync('ed25519'); + + const { body: { request_uri } } = await this.agent.post('/request') + .send({ + client_id: 'client', + scope: 'offline_access', + prompt: 'consent', + response_type: 'code', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(201); + + const { headers: { location } } = await this.agent.get('/auth') + .query({ + client_id: 'client', + request_uri, + }) + .expect(303); + + const callback = new URL(location).searchParams; + expect(callback.has('code')).to.be.true; + const code = new URL(location).searchParams.get('code'); + + let spy = sinon.spy(); + this.provider.once('grant.error', spy); + const instanceKeyPair2 = generateKeyPairSync('ed25519'); + + await this.agent.post('/token') + .send({ + code, + grant_type: 'authorization_code', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair2)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair2)) + .expect(400) + .expect(() => { + expect(spy.calledOnce).to.be.true; + expect(errorDetail(spy)).to.equal('oauth-client-attestation instance public key mismatch'); + }); + + spy = sinon.spy(); + this.provider.once('grant.error', spy); + await this.agent.post('/token') + .send({ + code, + grant_type: 'authorization_code', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + }); + + it('urn:ietf:params:oauth:grant-type:device_code', async function () { + const instanceKeyPair = generateKeyPairSync('ed25519'); + const { body } = await this.agent.post('/device/auth') + .send({ + client_id: 'client', + scope: 'offline_access', + prompt: 'consent', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + + const deviceCode = this.TestAdapter.for('DeviceCode').syncFind(body.device_code); + + expect(deviceCode).to.have.property('attestationJkt').and.is.string; + + deviceCode.grantId = this.getGrantId(); + deviceCode.accountId = this.getSub(); + deviceCode.scope = 'offline_access'; + + const spy = sinon.spy(); + this.provider.once('grant.error', spy); + const instanceKeyPair2 = generateKeyPairSync('ed25519'); + + await this.agent.post('/token') + .send({ + device_code: body.device_code, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair2)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair2)) + .expect(400) + .expect(() => { + expect(spy.calledOnce).to.be.true; + expect(errorDetail(spy)).to.equal('oauth-client-attestation instance public key mismatch'); + }); + + const { body: { refresh_token } } = await this.agent.post('/token') + .send({ + device_code: body.device_code, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + + expect(this.TestAdapter.for('RefreshToken').syncFind(refresh_token)).to.have.property('attestationJkt').and.is.string; + }); + + it('urn:openid:params:grant-type:ciba', async function () { + const instanceKeyPair = generateKeyPairSync('ed25519'); + const { body } = await this.agent.post('/backchannel') + .send({ + client_id: 'client', + scope: 'openid offline_access', + prompt: 'consent', + login_hint: 'sub', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + + const authReq = this.TestAdapter.for('BackchannelAuthenticationRequest').syncFind(body.auth_req_id); + + expect(authReq).to.have.property('attestationJkt').and.is.string; + + authReq.grantId = this.getGrantId(); + authReq.accountId = this.getSub(); + authReq.scope = 'openid offline_access'; + + const spy = sinon.spy(); + this.provider.once('grant.error', spy); + const instanceKeyPair2 = generateKeyPairSync('ed25519'); + + await this.agent.post('/token') + .send({ + auth_req_id: body.auth_req_id, + grant_type: 'urn:openid:params:grant-type:ciba', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair2)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair2)) + .expect(400) + .expect(() => { + expect(spy.calledOnce).to.be.true; + expect(errorDetail(spy)).to.equal('oauth-client-attestation instance public key mismatch'); + }); + + const { body: { refresh_token } } = await this.agent.post('/token') + .send({ + auth_req_id: body.auth_req_id, + grant_type: 'urn:openid:params:grant-type:ciba', + }) + .type('form') + .set('OAuth-Client-Attestation', await attestation.call(this, instanceKeyPair)) + .set('OAuth-Client-Attestation-PoP', await pop.call(this, instanceKeyPair)) + .expect(200); + + expect(this.TestAdapter.for('RefreshToken').syncFind(refresh_token)).to.have.property('attestationJkt').and.is.string; + }); +}); diff --git a/test/client_auth/client_auth.config.js b/test/client_auth/client_auth.config.js index 117ebaa56..6a5bd4913 100644 --- a/test/client_auth/client_auth.config.js +++ b/test/client_auth/client_auth.config.js @@ -1,5 +1,6 @@ -import { X509Certificate } from 'node:crypto'; +import { X509Certificate, randomBytes } from 'node:crypto'; import { readFileSync } from 'node:fs'; +import * as assert from 'node:assert/strict'; import cloneDeep from 'lodash/cloneDeep.js'; import merge from 'lodash/merge.js'; @@ -25,6 +26,12 @@ const clientKey = { const rsaKeys = cloneDeep(mtlsKeys); rsaKeys.keys.splice(0, 1); +const attestationKey = { + e: key.e, + n: key.n, + kty: key.kty, +}; + config.clientAuthMethods = [ 'none', 'client_secret_basic', @@ -33,9 +40,34 @@ config.clientAuthMethods = [ 'client_secret_jwt', 'tls_client_auth', 'self_signed_tls_client_auth', + 'attest_jwt_client_auth', ]; merge(config.features, { introspection: { enabled: true }, + attestClientAuth: { + enabled: true, + challengeSecret: randomBytes(32), + getAttestationSignaturePublicKey(ctx, iss, header, client) { + assert.ok(ctx); + assert.equal(typeof iss, 'string'); + assert.ok(header); + assert.ok(client); + if (iss === 'https://attester.example.com' && header.alg === 'RS256') { + return client.jwks.keys[0]; + } + throw new Error('unexpected attestation jwt issuer'); + }, + assertAttestationJwtAndPop(ctx, attestation, pop, client) { + assert.ok(ctx); + assert.ok(attestation?.payload?.iss); + assert.ok(attestation?.protectedHeader?.alg); + assert.ok(attestation?.key?.algorithm); + assert.ok(pop?.payload?.iss); + assert.ok(pop?.protectedHeader?.alg); + assert.ok(pop?.key?.algorithm); + assert.ok(client); + }, + }, mTLS: { enabled: true, selfSignedTlsClientAuth: true, @@ -102,6 +134,13 @@ export default { jwks: { keys: [clientKey], }, + }, { + client_id: 'attest_jwt_client_auth', + grant_types: ['foo'], + response_types: [], + redirect_uris: [], + token_endpoint_auth_method: 'attest_jwt_client_auth', + jwks: { keys: [attestationKey] }, }, { client_id: 'client-pki-mtls', grant_types: ['foo'], diff --git a/test/client_auth/client_auth.test.js b/test/client_auth/client_auth.test.js index 0f1c4e3cb..bfc27239d 100644 --- a/test/client_auth/client_auth.test.js +++ b/test/client_auth/client_auth.test.js @@ -1,8 +1,8 @@ -import { createPrivateKey, X509Certificate } from 'node:crypto'; +import { createPrivateKey, X509Certificate, generateKeyPairSync } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { request } from 'node:http'; -import { importJWK } from 'jose'; +import { importJWK, SignJWT } from 'jose'; import sinon from 'sinon'; import { expect } from 'chai'; import cloneDeep from 'lodash/cloneDeep.js'; @@ -286,7 +286,7 @@ describe('client authentication methods', () => { .expect(noW3A) .expect({ error: 'invalid_request', - error_description: 'mismatch in body and authorization client ids', + error_description: 'client_id mismatch', }); }); @@ -920,7 +920,7 @@ describe('client authentication methods', () => { .expect(noW3A) .expect({ error: 'invalid_request', - error_description: 'subject of client_assertion must be the same as client_id provided in the body', + error_description: 'client_id mismatch', })); }); @@ -1156,6 +1156,395 @@ describe('client authentication methods', () => { }); }); + describe('attest_jwt_client_auth auth', () => { + let challenge; + before(async function () { + const response = await this.agent.post('/challenge'); + expect(response.body).to.have.property('attestation_challenge'); + expect(response.headers['oauth-client-attestation-challenge']).to.equal(response.body.attestation_challenge); + challenge = response.body.attestation_challenge; + }); + + const privateKey = createPrivateKey({ format: 'jwk', key: clientKey }); + const instanceKeyPair = generateKeyPairSync('ed25519'); + + it('accepts the auth', async function () { + const [attestation, pop] = await Promise.all([ + new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + .setSubject('attest_jwt_client_auth') + .setExpirationTime('2h') + .sign(privateKey), + new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey), + ]); + + return this.agent.post(route) + .set('OAuth-Client-Attestation', attestation) + .set('OAuth-Client-Attestation-PoP', pop) + .send({ + grant_type: 'foo', + }) + .type('form') + .expect(tokenAuthSucceeded); + }); + + describe('oauth-client-attestation', () => { + before(function () { + this.signPop = () => new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey); + }); + + it('checks all required properties and their values', async function () { + for (const attestation of await Promise.all([ + new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + // .setIssuer('https://attester.example.com') + .setSubject('attest_jwt_client_auth') + .setExpirationTime('2h') + .sign(privateKey), + new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('foo') + .setSubject('attest_jwt_client_auth') + .setExpirationTime('2h') + .sign(privateKey), + new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + // .setSubject('attest_jwt_client_auth') + .setExpirationTime('2h') + .sign(privateKey), + new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + .setSubject('foo') + .setExpirationTime('2h') + .sign(privateKey), + new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + .setSubject('attest_jwt_client_auth') + // .setExpirationTime('2h') + .sign(privateKey), + new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + .setSubject('attest_jwt_client_auth') + .setExpirationTime(0) + .sign(privateKey), + new SignJWT({ + cnf: { + // jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + .setSubject('attest_jwt_client_auth') + .setExpirationTime('2h') + .sign(privateKey), + new SignJWT({ + // cnf: { + // jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + // }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + .setSubject('attest_jwt_client_auth') + .setExpirationTime('2h') + .sign(privateKey), + new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + // typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + .setSubject('attest_jwt_client_auth') + .setExpirationTime('2h') + .sign(privateKey), + new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'foo', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + .setSubject('attest_jwt_client_auth') + .setExpirationTime('2h') + .sign(privateKey), + ])) { + await this.agent.post(route) + .set('OAuth-Client-Attestation', attestation) + .set('OAuth-Client-Attestation-PoP', await this.signPop()) + .send({ + grant_type: 'foo', + }) + .type('form') + .expect(tokenAuthRejected); + } + }); + }); + + describe('oauth-client-attestation-pop', () => { + before(function () { + this.signAttestation = () => new SignJWT({ + cnf: { + jwk: instanceKeyPair.publicKey.export({ format: 'jwk' }), + }, + }) + .setProtectedHeader({ + typ: 'oauth-client-attestation+jwt', + alg: 'RS256', + }) + .setIssuer('https://attester.example.com') + .setSubject('attest_jwt_client_auth') + .setExpirationTime('2h') + .sign(privateKey); + }); + + it('must not be re-used', async function () { + const pop = await new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey); + + await this.agent.post(route) + .set('OAuth-Client-Attestation', await this.signAttestation()) + .set('OAuth-Client-Attestation-PoP', pop) + .send({ + grant_type: 'foo', + }) + .type('form') + .expect(tokenAuthSucceeded); + + await this.agent.post(route) + .set('OAuth-Client-Attestation', await this.signAttestation()) + .set('OAuth-Client-Attestation-PoP', pop) + .send({ + grant_type: 'foo', + }) + .type('form') + .expect(tokenAuthRejected); + }); + + describe('challenge handling', async () => { + it('requires challenge to be present', async function () { + const pop = await new SignJWT({ challenge: undefined }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey); + + await this.agent.post(route) + .set('OAuth-Client-Attestation', await this.signAttestation()) + .set('OAuth-Client-Attestation-PoP', pop) + .send({ + grant_type: 'foo', + }) + .type('form') + .expect(400) + .expect('OAuth-Client-Attestation-Challenge', /^[A-Za-z0-9_-]+$/) + .expect({ error: 'use_attestation_challenge' }); + }); + + it('requires a valid challenge to be present', async function () { + const pop = await new SignJWT({ challenge: 'invalid' }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey); + + await this.agent.post(route) + .set('OAuth-Client-Attestation', await this.signAttestation()) + .set('OAuth-Client-Attestation-PoP', pop) + .send({ + grant_type: 'foo', + }) + .type('form') + .expect(400) + .expect('OAuth-Client-Attestation-Challenge', /^[A-Za-z0-9_-]+$/) + .expect({ error: 'use_attestation_challenge' }); + }); + }); + + it('checks all required properties and their values', async function () { + for (const pop of await Promise.all([ + new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience(this.provider.issuer) + // .setJti(nanoid()) + .sign(instanceKeyPair.privateKey), + new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + // .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey), + new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience([this.provider.issuer]) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey), + new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience('foo') + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey), + new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + // .setIssuer('attest_jwt_client_auth') + .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey), + new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('foo') + .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey), + new SignJWT({ challenge }) + .setProtectedHeader({ + // typ: 'oauth-client-attestation-pop+jwt', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey), + new SignJWT({ challenge }) + .setProtectedHeader({ + typ: 'foo', + alg: 'Ed25519', + }) + .setIssuer('attest_jwt_client_auth') + .setAudience(this.provider.issuer) + .setJti(nanoid()) + .sign(instanceKeyPair.privateKey), + ])) { + await this.agent.post(route) + .set('OAuth-Client-Attestation', await this.signAttestation()) + .set('OAuth-Client-Attestation-PoP', pop) + .send({ + grant_type: 'foo', + }) + .type('form') + .expect(tokenAuthRejected); + } + }); + }); + }); + describe('tls_client_auth auth', () => { it('accepts the auth', function () { return this.agent.post(route) diff --git a/test/core/basic/isscookie.test.js b/test/core/basic/isscookie.test.js index 435da1399..5cebf52bf 100644 --- a/test/core/basic/isscookie.test.js +++ b/test/core/basic/isscookie.test.js @@ -5,7 +5,7 @@ describe('pre-middleware setting "set-cookie" header', () => { before(function () { this.provider.use((ctx, next) => { - ctx.response.set('set-cookie', 'foo=bar;'); + ctx.set('set-cookie', 'foo=bar;'); return next(); }); }); diff --git a/test/test_helper.js b/test/test_helper.js index fa55661d5..c9ba19364 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -407,6 +407,12 @@ export default function testHelper(importMetaUrl, { return raw; } + function getSub() { + const sessionId = getSessionId(); + const raw = TestAdapter.for('Session').syncFind(sessionId); + return raw.accountId; + } + function getGrantId(client_id) { const session = getSession(); let clientId = client_id; @@ -488,6 +494,7 @@ export default function testHelper(importMetaUrl, { getSession, getSessionId, getGrantId, + getSub, getTokenJti, login, logout, @@ -495,6 +502,7 @@ export default function testHelper(importMetaUrl, { TestAdapter, wrap, fetchAgent, + config: mod, }); switch (mountVia) { diff --git a/test/web_message/web_message.test.js b/test/web_message/web_message.test.js index 014d8e160..6f70f59f2 100644 --- a/test/web_message/web_message.test.js +++ b/test/web_message/web_message.test.js @@ -13,8 +13,8 @@ describe('configuration features.webMessageResponseMode', () => { before(function () { this.provider.use(async (ctx, next) => { - ctx.response.set('X-Frame-Options', 'SAMEORIGIN'); - ctx.response.set('Content-Security-Policy', "default-src 'none'; frame-ancestors 'self' example.com *.example.net; script-src 'self' 'nonce-foo'; connect-src 'self'; img-src 'self'; style-src 'self';"); + ctx.set('x-frame-options', 'SAMEORIGIN'); + ctx.set('content-security-policy', "default-src 'none'; frame-ancestors 'self' example.com *.example.net; script-src 'self' 'nonce-foo'; connect-src 'self'; img-src 'self'; style-src 'self';"); await next(); }); });