Skip to content

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:

  1. 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), and findAccount(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 internal cause; do not expose signature, parser, trust-store, or subject-mapping details to the client.
  2. Validate a supplied DPoP proof with validateDpop(provider, ctx, accessToken?); its optional accessToken argument checks the proof’s access-token hash when the operation requires one. Retrieve a required mutual TLS certificate with checkMtlsCert(provider, ctx, ErrorClass?), and enforce the client’s DPoP requirement with checkDpopRequired(provider, ctx, dPoP, ErrorClass?). These checks do not record DPoP replay. Normalize and authorize requested scopes with validateClientScope(provider, ctx, scopes?), which enforces the client’s static scope allow list. For an externally validated assertion or client-only grant, call resolveRequestedResources(provider, ctx); it populates ctx.oidc.resourceServers and 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 use resolveAndApplyResource(...) after constructing a token.
  3. 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.
  4. Construct the appropriate provider token model, apply resource policy, and call applyAuthorizationDetails(provider, ctx, token, source?). Apply the certificate with token.setThumbprint("x5t", certificate) or the DPoP key with token.setThumbprint("jkt", dPoP.thumbprint). Use shouldIssueRefreshToken(provider, ctx, source) before creating a Refresh Token and applyRefreshTokenBindings(provider, ctx, accessToken, refreshToken) to bind it. Reject conflicting bindings and finish authorization and output validation before recording DPoP replay.
  5. Assign relevant objects with ctx.oidc.entity(name, value). Call checkDpopReplay(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.
  6. Set ctx.body, usually with buildTokenResponse(provider, input). Throw the public errors exported by oidc-provider for 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.