Skip to content

Support for OAuth 2.0 Token Exchange (RFC 8693) #116

@DeanMauro

Description

@DeanMauro

I'd like to add support for the OAuth 2.0 Token Exchange specification (RFC 8693).
Token Exchange allows clients or backend services to exchange an existing access token for a new one with modified characteristics like:

  • A narrowed set of scopes (downscoping)
  • A different target audience (resource server)
  • A shorter TTL
  • (Optionally) an “actor token” representing service-to-service delegation. Leave this out for now since we don't have such a concept in the library.

Use Cases

  • Public clients that want to obtain short-lived, least-privilege tokens for specific flows
  • Backends minting temporary delegated tokens for downstream APIs on behalf of a user
  • Federated environments that need audience-restricted tokens between multiple microservices

Why?

Right now there’s no clean way to mint derived tokens. A client would need to go through the authorization flow a second time to get a new token with narrower scopes.

How?

1. Expand the tokenEndpoint (RFC 8693)

In addition to the existing grant types, add support for urn:ietf:params:oauth:grant-type:token-exchange. The body should contain a subject_token (the user's current issued token) and optionally new scope or audience.

Example Request:

POST /token/exchange
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <client creds>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange&
subject_token=<access_token>&
subject_token_type=urn:ietf:params:oauth:token-type:access_token&
scope=user:read

2. Add a new exchangeToken method to OAuthHelpers

const shortToken = await exchangeToken({
  subjectToken: accessToken,
  scope: ["user:read"],            // Optional
  aud: "serviceB"                     // Optional
  expiresIn: 60                         // Optional (have a default built in)
});

3. Verify

exchangeToken first uses unwrapToken from #114 to validate the subject_token that was passed in and pull its grant info.
It then validates that the requested changes are allowed by the original grant:

if (!requestedScopes.every(scope => tokenSummary.grant.scope.includes(scope))) {
  return createErrorResponse("invalid_scope", "Requested scope exceeds parent grant");
}

4. Mint new token

If the request is kosher, mint a new token in the same way handleAuthorizationCodeGrant does.

Other Details

  • Just like we have an allowImplicitFlow flag, I would add an allowTokenExchangeGrant flag for those who don't want this capability on the public API.
  /**
   * Controls whether OAuth 2.0 Token Exchange (RFC 8693) is allowed.
   * When false, the token exchange grant type will not be advertised in metadata
   * and token exchange requests will be rejected.
   * Defaults to false.
   */
  allowTokenExchangeGrant?: boolean;
  • RFC 6749 should be implemented too, which allows tokens to be minted with a subset of the scopes authorized by the grant, in both the auth code and refresh flows.

For Maintainers

@kentonv @mattzcarey @threepointone

Implemented in #120

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions