Skip to content

Token Types

@saurbit/oauth2 uses token types to determine how access tokens are presented in the Authorization header and how they are validated on both the token endpoint and protected resource endpoints.

You set the token type on any flow builder using setTokenType(). If not set, BearerTokenType is used by default.

Overview

Token TypeClassSpecDescription
BearerBearerTokenTypeRFC 6750Standard bearer token — possession alone grants access.
DPoPDPoPTokenTypeRFC 9449Proof-of-possession token bound to a client key pair.

TokenType Interface

Both built-in token types implement the TokenType interface. You can provide your own implementation to support additional token schemes.

ts
interface TokenType {
  /** The prefix in the `Authorization` header (e.g. `"Bearer"`, `"DPoP"`). */
  readonly prefix: string;

  /** Validates a token on a protected resource endpoint. */
  isValid(
    request: Request,
    token: string,
  ): TokenTypeValidationResponse | Promise<TokenTypeValidationResponse>;

  /** Optional — validates the request at the token endpoint before client auth. */
  isValidTokenRequest?(
    request: Request,
  ): TokenTypeValidationResponse | Promise<TokenTypeValidationResponse>;
}

BearerTokenType

The default token type. By default, considers any non-empty token string valid. Use the validate() method to supply custom validation logic (e.g. JWT signature verification, database lookup).

Usage

ts
import { BearerTokenType } from "@saurbit/oauth2";

const bearer = new BearerTokenType();

Since BearerTokenType is the default, you don't need to call setTokenType() unless you want to configure custom validation:

ts
import { AuthorizationCodeFlowBuilder, BearerTokenType } from "@saurbit/oauth2";

const bearer = new BearerTokenType()
  .validate((request, token) => {
    // Custom validation logic (e.g. verify JWT, look up in database)
    return { isValid: !!token };
  });

const flow = new AuthorizationCodeFlowBuilder({ tokenEndpoint: "/token" })
  .setTokenType(bearer)
  // ... other builder methods
  .build();

Methods

validate(handler)

ts
.validate(handler: BearerTokenValidation): this

Overrides the default validation handler. The handler receives the incoming Request and the raw token string extracted from the Authorization: Bearer <token> header.


DPoPTokenType

Implements the DPoP (Demonstration of Proof-of-Possession) token scheme. DPoP binds access tokens to a client's key pair, preventing token theft and replay attacks.

The default handler validates DPoP proofs on both the token endpoint and protected resource endpoints by checking:

  • The DPoP header is present and is a valid JWT
  • The htm claim matches the HTTP method
  • The htu claim matches the request URL
  • The iat claim is within the configured lifetime
  • The jti claim has not been seen before (replay detection)

Setup

Additional dependency

DPoPTokenType requires a JWK verification function. Install @saurbit/oauth2-jwt (which wraps jose) to get a ready-made implementation.

sh
npm install @saurbit/oauth2-jwt
sh
yarn add @saurbit/oauth2-jwt
sh
pnpm add @saurbit/oauth2-jwt
sh
bun add @saurbit/oauth2-jwt
sh
deno add jsr:@saurbit/oauth2-jwt

Usage

ts
import { createInMemoryReplayStore, DPoPTokenType } from "@saurbit/oauth2";
import { calculateJwkThumbprint, verifyJwk } from "@saurbit/oauth2-jwt";

const dpop = new DPoPTokenType(
  verifyJwk,
  calculateJwkThumbprint,
  createInMemoryReplayStore(),
);

Then pass it to your flow builder:

ts
import { AuthorizationCodeFlowBuilder } from "@saurbit/oauth2";

const flow = new AuthorizationCodeFlowBuilder({ tokenEndpoint: "/token" })
  .setTokenType(dpop)
  // ... other builder methods
  .build();

Constructor

ts
new DPoPTokenType(jwkVerify: JwkVerify, jwkThumbprintCalculator: JwkThumbprintCalculator, replayDetector?: ReplayDetector)
ParameterTypeDescription
jwkVerifyJwkVerifyA function that verifies a DPoP proof JWT against a JWK Set.
jwkThumbprintCalculatorJwkThumbprintCalculatorA function that calculates the JWK thumbprint for a given JWK.
replayDetectorReplayDetector?An optional replay detector for JTI tracking. Defaults to an in-memory store.

Methods

setTokenLifetime(tokenLifetime)

ts
.setTokenLifetime(tokenLifetime: number): this

Sets the maximum acceptable age of a DPoP proof in seconds. Default is 300 (5 minutes).

setReplayDetector(value)

ts
.setReplayDetector(value: ReplayDetector): this

Replaces the replay detector used for JTI tracking. Use this to provide a distributed store (e.g. Redis) in multi-process deployments.

validate(handler)

ts
.validate(handler: DPoPTokenTypeValidation): this

Overrides the default DPoP proof validation handler for protected resource requests. The handler receives the incoming Request, the DPoP-bound access token, and the configured token lifetime.

validateTokenRequest(handler)

ts
.validateTokenRequest(handler: DPoPTokenTypeRequestValidation): this

Overrides the default DPoP proof validation handler for token endpoint requests. The handler receives the incoming Request and the configured token lifetime.

addJwkThumbprintToCnfClaim(claims, thumbprint)

ts
.addJwkThumbprintToCnfClaim(claims: JwtPayload, thumbprint: string): JwtPayload
.addJwkThumbprintToCnfClaim(claims: JwtPayload, response: TokenTypeValidationResponse): JwtPayload

Adds the JWK thumbprint to the cnf.jkt claim of a JWT payload, binding the access token to the client's public key. Call this when issuing a DPoP-bound access token.

The second argument is either:

  • A string — the raw JWK thumbprint.
  • A TokenTypeValidationResponse — the validation response returned by isValidTokenRequest, from which the thumbprint is extracted automatically.
ts
// With a raw thumbprint string
dpop.addJwkThumbprintToCnfClaim(claims, "the-jwk-thumbprint");

// With a validation response
dpop.addJwkThumbprintToCnfClaim(claims, tokenTypeValidationResponse);

Throws if the thumbprint cannot be added to the payload.

validateThumbprint(response, thumbprint)

ts
.validateThumbprint(response: TokenTypeValidationResponse, thumbprint: string): boolean
.validateThumbprint(response: TokenTypeValidationResponse, payload: JwtPayload): boolean

Validates that the JWK thumbprint in a TokenTypeValidationResponse matches an expected value. Use this on protected resource endpoints to verify that the DPoP proof was created with the same key that the access token was bound to.

The second argument is either:

  • A string — the expected JWK thumbprint.
  • A JwtPayload object — the access token payload containing the cnf.jkt claim.
ts
// With a raw thumbprint string
dpop.validateThumbprint(tokenTypeValidationResponse, "the-jwk-thumbprint");

// With an access token payload
dpop.validateThumbprint(tokenTypeValidationResponse, accessTokenPayload);

Throws if the response is invalid or the thumbprint does not match.

validateAccessTokenHash(response, accessToken)

ts
.validateAccessTokenHash(response: TokenTypeValidationResponse, accessToken: string): Promise<void>

Validates the access token hash (ath) in a DPoP proof against the provided access token. Use this on protected resource endpoints to confirm that the DPoP proof was created for the specific access token presented in the request.

Throws if the response is invalid, if the ath claim is missing from the DPoP proof payload, or if the computed hash does not match the ath claim.

ts
await dpop.validateAccessTokenHash(tokenTypeValidationResponse, accessToken);

Replay Detection

DPoP requires tracking jti claims to prevent proof replay. @saurbit/oauth2 provides a ReplayStore interface and a built-in in-memory implementation.

InMemoryReplayStore

Suitable for single-process deployments. Entries are automatically evicted after their TTL expires.

ts
import { createInMemoryReplayStore } from "@saurbit/oauth2";

const store = createInMemoryReplayStore();

Custom replay store

For distributed or multi-process environments, implement the ReplayStore interface backed by a shared store (e.g. Redis):

ts
interface ReplayStore<T extends string | number> {
  has(value: T): Promise<boolean>;
  add(value: T, ttlSeconds: number): Promise<void>;
  delete(value: T): Promise<void>;
}