โ† All flows

JWT Bearer

Prove who you are with a signed JWT instead of a shared secret

RFC 7523 lets a client use a signed JWT (a small, signed token) in place of a client_secret. This page shows the most common use, called private key JWT client authentication:

  1. The client keeps a private key and registers the matching public key.
  2. For each token request it signs a short-lived JWT and sends it as client_assertion.
  3. The login server verifies the signature and issues the token.

The RFC also defines a separate grant, urn:ietf:params:oauth:grant-type:jwt-bearer, where a JWT sent as assertion is itself the authorization to get a token.

See it step by step

โš™๏ธ
Service
๐Ÿ”
Login server
๐Ÿ“ฆ
API
Public key

Step 1 of 6

The service shares its public key

Once, at set-up, it gives the login server a public key. The matching private key never leaves the service.

When to use this flow

  • High-security server-to-server calls โ€” No secret is ever sent over the wire
  • Open banking and enterprise APIs โ€” Often required by the provider
  • Easy key rotation โ€” Publish new public keys with a JWKS
  • Browser and mobile apps โ€” They cannot protect a private key; use PKCE

Good practices

  • Keep the JWT short-lived โ€” A minute or less is plenty
  • Give each JWT a unique jti โ€” The server can then reject replays
  • Set aud to the token endpoint โ€” A JWT for one server should not work on another
  • Publish keys with a kid โ€” It makes rotation smooth

Common mistakes

  • Sharing or logging the private key โ€” Anyone with it can act as your client
  • Long-lived assertions โ€” A captured one stays useful for too long
  • Skipping iss and sub โ€” Both must equal your client_id
  • Using weak algorithms โ€” Prefer RS256, PS256 or ES256, never none
Code example
Private key JWT example
javascript
// Private key JWT client authentication (RFC 7523)
import { SignJWT } from 'jose';

const assertion = await new SignJWT({})
  .setProtectedHeader({ alg: 'RS256', kid: KEY_ID })
  .setIssuer(CLIENT_ID)
  .setSubject(CLIENT_ID)
  .setAudience('https://auth.provider.com/oauth/token')
  .setJti(crypto.randomUUID())
  .setExpirationTime('60s')
  .sign(privateKey);

const res = await fetch('https://auth.provider.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
    client_assertion: assertion,
    scope: 'api.read',
  }),
});
const { access_token } = await res.json();