By AndyPublished
JWT vs Bearer Token: Is a JWT a Bearer Token?
Authorization: Bearer <token>. A JWT is a token format: signed JSON claims (RFC 7519). Bearer describes the usage, JWT describes the contents.They overlap heavily because most JWT access tokens are used as bearer tokens. But a bearer token does not have to be a JWT (it can be an opaque random string), and a JWT does not have to be a bearer token (with DPoP it is bound to a key the client must prove it holds).
Is a JWT a Bearer Token?
Usually, yes. When an OAuth server returns "token_type": "Bearer" alongside a JWT access token, that JWT is a bearer token: the API accepts it from anyone who sends it, as long as the signature and claims check out. The signature proves the token came from the issuer. It says nothing about whether the party presenting it is the one it was issued to.
RFC 6750 puts it plainly: any party in possession of a bearer token can use it to access the associated resources, without demonstrating possession of a cryptographic key. That is why a stolen JWT works exactly as well for the thief as for the legitimate client until its exp passes.
Are JWT and Bearer Token the Same?
No. They sit on different axes, and each can exist without the other:
| Aspect | Bearer token | JWT |
|---|---|---|
| What the term describes | How a token is used: possession alone grants access | What a token looks like: signed JSON claims |
| Specification | RFC 6750 (OAuth 2.0 Bearer Token Usage) | RFC 7519, signed with JWS (RFC 7515) |
| Format | Any string: opaque, JWT, or vendor-specific | Three Base64url segments: header.payload.signature |
| Sent as | Authorization: Bearer <token> | Anywhere: Bearer header, DPoP header, cookie, form field |
| Validated by | Whatever the format requires: lookup, introspection or signature | Signature plus iss, aud, exp checks |
| Opposite concept | Sender-constrained tokens (DPoP, mutual TLS) | Opaque (reference) tokens |
- ·Bearer, not JWT: an opaque access token like
tGzv3JOkF0XG5Qx2TlKWIA, validated by introspection. Also API keys sent in a Bearer header. - ·JWT, not bearer: a DPoP-bound access token, an OpenID Connect ID token (consumed by the client, never presented to an API), or a JWT used as a client assertion.
- ·Both: the typical JWT access token from Auth0, Okta, Entra ID or Cognito.
The format question, JWT versus opaque, is covered in JWT vs opaque tokens. To see whether the token you are sending is a JWT at all, paste it into the jwtdecode.app decoder; it decodes locally, and an opaque token simply will not parse.
JWT Bearer Authentication: How It Works
“JWT bearer authentication” means an API that reads a JWT from the Bearer scheme of the Authorization header and validates it. The request looks like this:
GET /orders HTTP/1.1 Host: api.example.com Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjIwMjYtMDkifQ.eyJpc3Mi...
RFC 6750 also allows the token in a form-encoded body field or an access_token query parameter, but the query form ends up in logs and browser history and should not be used. Clients must send the token in exactly one place per request.
A Bearer middleware for Express
A minimal Express 5 middleware using jose that validates a JWT bearer token and returns the RFC 6750 challenges:
import { createRemoteJWKSet, jwtVerify, errors } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));
export function requireBearer(requiredScope) {
return async (req, res, next) => {
const [scheme, token] = (req.headers.authorization ?? '').split(' ');
if (scheme?.toLowerCase() !== 'bearer' || !token) {
res.set('WWW-Authenticate', 'Bearer realm="api"');
return res.status(401).end();
}
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.example.com/',
audience: 'https://api.example.com',
algorithms: ['RS256'],
});
const scopes = String(payload.scope ?? '').split(' ');
if (requiredScope && !scopes.includes(requiredScope)) {
res.set('WWW-Authenticate', `Bearer error="insufficient_scope", scope="${requiredScope}"`);
return res.status(403).end();
}
req.auth = payload;
next();
} catch (err) {
const description = err instanceof errors.JWTExpired ? 'The access token expired' : 'The access token is invalid';
res.set('WWW-Authenticate', `Bearer error="invalid_token", error_description="${description}"`);
res.status(401).end();
}
};
}
// app.get('/orders', requireBearer('orders:read'), handler);The authentication scheme name is case-insensitive, hence the toLowerCase(). Frameworks ship this as a package: ASP.NET Core’s Microsoft.AspNetCore.Authentication.JwtBearer with AddJwtBearer(), Spring Security’s OAuth 2.0 resource server, and similar. The checks in the middleware are explained step by step in the JWT verification guide.
Bearer error responses
| error | Status | Meaning |
|---|---|---|
| (no error code) | 401 | No credentials were sent. Respond with a bare WWW-Authenticate: Bearer challenge. |
| invalid_request | 400 | Malformed request: missing parameter, token sent in more than one place. |
| invalid_token | 401 | Expired, revoked, malformed or failed signature. The client should get a new token. |
| insufficient_scope | 403 | Token is valid but lacks the scope for this resource. |
Keep error_description generic. It is for developers, and it should not reveal which check failed in detail to an unauthenticated caller. When you are on the receiving end of these errors, how to debug a JWT walks through the causes in order.
Sending a JWT Bearer Token From a Client
On the client side, a bearer token is just a header. With curl:
TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9..." curl -i https://api.example.com/orders -H "Authorization: Bearer $TOKEN"
And with fetch in the browser or Node.js:
const res = await fetch('https://api.example.com/orders', {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (res.status === 401) {
// Read res.headers.get('WWW-Authenticate'): invalid_token means refresh and retry once.
}The most common client-side mistakes are small: a missing space after Bearer, quotes copied around the token, the word Bearer included twice, or the ID token sent instead of the access token. Testing tools add their own twists, such as Postman’s Bearer Token auth type adding the prefix for you; see JWTs with curl and Postman. Browsers never attach a bearer token automatically, unlike a cookie, which is why header-based bearer tokens are not exposed to CSRF in the way cookie sessions are, and also why your client code is responsible for attaching and refreshing them on every call.
JWT Bearer Token Risks and Mitigations
Because possession equals access, the defences for bearer JWTs are about limiting exposure and lifetime:
- ·Short lifetimes: 5 to 15 minutes for access tokens, renewed with a refresh token. See the refresh token pattern.
- ·Narrow audience and scope: a token for one API should be rejected by every other API.
- ·Careful storage: in browsers, keep tokens out of places injected scripts can read where possible. The options are compared in localStorage vs cookies.
- ·No logging: strip the
Authorizationheader from access logs, error trackers and proxies. - ·HTTPS only: RFC 6750 requires TLS for every request that carries a bearer token.
The JWT Bearer Grant Type (RFC 7523)
A related but different use of the word: RFC 7523 defines how a JWT can be used to obtain an access token, rather than being the access token. The client signs a short-lived JWT assertion with its private key and posts it to the token endpoint:
POST /token HTTP/1.1 Host: auth.example.com Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer &assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6InNhLTEifQ.eyJpc3MiOiJzdmMtYWNjdEBleGFtcGxlIi...
The assertion must contain iss, sub, aud (the token endpoint) and exp. Google service accounts use this grant, and several platforms use it for server-to-server integrations. RFC 7523 also defines JWT client authentication, where the assertion is sent as client_assertion with client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer in place of a client secret. In both cases “bearer” refers to the assertion, and the access token that comes back may or may not be a JWT.
JWT With DPoP: Sender-Constrained Instead of Bearer
DPoP (Demonstrating Proof of Possession, RFC 9449) removes the bearer property. The client generates a key pair and, on every request, sends a small signed JWT called a DPoP proof. The authorisation server binds the access token to the client’s public key, so a stolen token is useless without the private key.
GET /orders HTTP/1.1
Host: api.example.com
Authorization: DPoP eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCJ9...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7...
# DPoP proof header # DPoP proof payload
{ {
"typ": "dpop+jwt", "jti": "e1j3V_bKic8-LAEB",
"alg": "ES256", "htm": "GET",
"jwk": { "kty": "EC", "htu": "https://api.example.com/orders",
"crv": "P-256", "iat": 1790240400,
"x": "...", "y": "..." } "ath": "..."
} }- ·The token endpoint returns
"token_type": "DPoP", and the access token carries acnfclaim withjkt, the SHA-256 thumbprint of the client’s public key. - ·The API checks the proof signature against the embedded
jwk, confirms the thumbprint matchescnf.jkt, and checkshtmandhtuagainst the actual request. - ·
athis the Base64url SHA-256 hash of the access token, tying the proof to that token;jtiandiatlimit replay, and servers can add aDPoP-Nonce.
Mutual-TLS certificate-bound tokens (RFC 8705) reach the same goal at the transport layer. Either way, the token is still a JWT; it has simply stopped being a bearer token.
Summary
- ·A bearer token grants access to whoever holds it (RFC 6750); a JWT is a signed token format (RFC 7519).
- ·Most JWT access tokens are bearer tokens, but opaque bearer tokens and non-bearer JWTs both exist.
- ·JWT bearer authentication reads the token from
Authorization: Bearerand returnsinvalid_tokenorinsufficient_scopechallenges on failure. - ·The JWT bearer grant (RFC 7523) uses a signed JWT to obtain an access token or to authenticate a client.
- ·DPoP (RFC 9449) binds a JWT to a client key, so possession of the token alone is no longer enough.