Custom Grant Types
The authorization server comes with the basic grants implemented, but implementations may register OAuth 2.0 extension grants, for example OAuth 2.0 Token Exchange (RFC 8693), JWT bearer grants (RFC 7523), SAML bearer grants (RFC 7522), or the Identity Assertion Authorization Grant.
import { errors } from "oidc-provider";import * as grants from "oidc-provider/lib/helpers/grants.js";
const parameters = [ "audience", "resource", "scope", "authorization_details", "requested_token_type", "subject_token", "subject_token_type", "actor_token", "actor_token_type",];const allowedDuplicateParameters = ["audience", "resource"];const grantType = "urn:ietf:params:oauth:grant-type:token-exchange";
async function tokenExchangeHandler(ctx) { // Grant-specific validation, authorization, issuance, and ctx.body assignment.}
provider.registerGrantType(grantType, tokenExchangeHandler, parameters, allowedDuplicateParameters);The handler is called as handler(ctx). Before it runs, oidc-provider parses the request, authenticates the client,
checks that the client may use the grant type, rejects unexpected duplicate parameters, validates scope syntax, and
validates Rich Authorization Requests when enabled. ctx.oidc.params contains only client-authentication parameters,
grant_type, and the parameters registered for the selected grant. Therefore, every custom parameter, including
resource and authorization_details, must be registered explicitly. Repeated values are rejected by default; list a
parameter in allowedDuplicateParameters only when its specification permits repetition. A permitted repeated value is
exposed as an array. Repeated grant_type values are always rejected.
The helpers keep protocol-specific code small while preserving control over policy and token shape. A custom handler should perform work in this order:
- Validate the grant’s required parameters, assertion or input-token integrity, issuer, audience, replay rules, and
deployment authorization policy. For provider-managed artifacts, use
findGrantSource(provider, ctx, Model, value, label),validateGrant(provider, ctx, grantId), andfindAccount(provider, ctx, accountId, source?)where applicable. The handler must still validate source-specific state and reject a missing account or an account/Grant mismatch. Translate assertion-verification failures to a sanitized OAuth error and retain the original exception only as its internalcause; do not expose signature, parser, trust-store, or subject-mapping details to the client. - Validate a supplied DPoP proof with
validateDpop(provider, ctx, accessToken?); its optionalaccessTokenargument checks the proof’s access-token hash when the operation requires one. Retrieve a required mutual TLS certificate withcheckMtlsCert(provider, ctx, ErrorClass?), and enforce the client’s DPoP requirement withcheckDpopRequired(provider, ctx, dPoP, ErrorClass?). These checks do not record DPoP replay. Normalize and authorize requested scopes withvalidateClientScope(provider, ctx, scopes?), which enforces the client’s static scope allow list. For an externally validated assertion or client-only grant, callresolveRequestedResources(provider, ctx); it populatesctx.oidc.resourceServersand returns the resolved Resource Servers. The handler must select and assign at most one of them to a provider token. A provider-managed source and Grant can instead useresolveAndApplyResource(...)after constructing a token. - Only after non-mutating validation succeeds, call
consumeGrantSource(provider, ctx, source, label)when the grant source is defined as single-use. Token Exchange input tokens, for example, are not consumed merely by being exchanged. - Construct the appropriate provider token model, apply resource policy, and call
applyAuthorizationDetails(provider, ctx, token, source?). Apply the certificate withtoken.setThumbprint("x5t", certificate)or the DPoP key withtoken.setThumbprint("jkt", dPoP.thumbprint). UseshouldIssueRefreshToken(provider, ctx, source)before creating a Refresh Token andapplyRefreshTokenBindings(provider, ctx, accessToken, refreshToken)to bind it. Reject conflicting bindings and finish authorization and output validation before recording DPoP replay. - Assign relevant objects with
ctx.oidc.entity(name, value). CallcheckDpopReplay(provider, ctx, dPoP, ctx.oidc.client.clientId, ErrorClass?)as the final step before persistence, then save the issued tokens. The helpers do not assign entities or persist newly constructed tokens. - Set
ctx.body, usually withbuildTokenResponse(provider, input). Throw the publicerrorsexported byoidc-providerfor OAuth errors; grant-specific validation and error selection remain the handler’s responsibility.
The optional ErrorClass argument defaults to errors.InvalidGrant. validateDpop returns undefined when no proof
is supplied or DPoP is disabled; checkDpopRequired enforces whether the client must supply one. checkMtlsCert returns
undefined when the client does not require certificate-bound Access Tokens. checkDpopReplay accepts an undefined
DPoP result, so a handler can call it regardless of whether a proof was supplied.
The following issuance tail shows a provider AccessToken. It assumes application code has already validated the custom
grant and produced a provider-compatible source, its persisted grant, and its account.
async function issueProviderAccessToken(ctx, source, grant, account, effectiveScopes) { const dPoP = await grants.validateDpop(provider, ctx); const certificate = grants.checkMtlsCert(provider, ctx); grants.checkDpopRequired(provider, ctx, dPoP); const scopes = grants.validateClientScope(provider, ctx, effectiveScopes);
ctx.oidc.entity("Grant", grant); ctx.oidc.entity("Account", account);
const token = new provider.AccessToken({ accountId: account.accountId, client: ctx.oidc.client, grantId: grant.jti, gty: ctx.oidc.params.grant_type, });
await grants.resolveAndApplyResource(provider, ctx, source, token, grant, scopes); await grants.applyAuthorizationDetails(provider, ctx, token, source); if (certificate) token.setThumbprint("x5t", certificate); if (dPoP) token.setThumbprint("jkt", dPoP.thumbprint); const expiresIn = token.expiration;
ctx.oidc.entity("AccessToken", token); await grants.checkDpopReplay(provider, ctx, dPoP, ctx.oidc.client.clientId); const accessToken = await token.save();
ctx.body = grants.buildTokenResponse(provider, { accessToken, tokenType: token.tokenType, expiresIn, scope: token.scope, authorizationDetails: token.rar, });}buildTokenResponse requires accessToken and tokenType. It also accepts expiresIn, scope,
authorizationDetails, refreshToken, idToken, and issuedTokenType, maps them to their wire names, and omits
undefined values. Additional response members may be supplied in a plain parameters object; it cannot override a
reserved response member.
RFC 8693 can return a security token that is not an OAuth Access Token and is not usable as one. The application is
responsible for producing and binding such a token; use the RFC 8693 N_A token type and include the required
issued-token type. In this example, application code validates and authorizes the exchange, constructs its token with
the supplied sender binding, and returns it without persisting it. The handler validates the response before calling
the replay helper directly. Any application persistence would follow the replay check.
async function tokenExchangeHandler(ctx) { const dPoP = await grants.validateDpop(provider, ctx); const certificate = grants.checkMtlsCert(provider, ctx); grants.checkDpopRequired(provider, ctx, dPoP); if (certificate && dPoP) { throw new errors.InvalidGrant("multiple proof-of-possession mechanisms are not allowed"); }
const issued = await validateExchangeAndCreateToken(ctx, { certificate, dpopJkt: dPoP?.thumbprint, }); const response = grants.buildTokenResponse(provider, { accessToken: issued.value, tokenType: "N_A", issuedTokenType: issued.type, expiresIn: issued.expiresIn, });
await grants.checkDpopReplay(provider, ctx, dPoP, ctx.oidc.client.clientId); ctx.body = response;}These helpers do not implement RFC 8693, RFC 7522, RFC 7523, or the Identity Assertion Authorization Grant; they do not
establish assertion trust, decide delegation or impersonation policy, sign arbitrary security-token formats, or relax
the token endpoint’s registered-client and grant_types checks. External assertion replay rejection also needs an
application-selected, atomic replay store; it is independent from stored-source consumption and DPoP replay detection.
Implementations can also examine the built-in grant handlers here, but should not treat their
internal structure as a public API.