โ 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:
- The client keeps a private key and registers the matching public key.
- For each token request it signs a short-lived JWT and sends it as
client_assertion. - 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
โ๏ธ
ServiceA backend program๐
Login serverChecks who you are and hands out tokens๐ฆ
APIHolds the data the app wantsPublic 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
audto 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
issandsubโ Both must equal yourclient_id - Using weak algorithms โ Prefer
RS256,PS256orES256, nevernone
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();