What “bearer” actually means
The Bearer scheme was specified in RFC 6750, The OAuth 2.0 Authorization Framework:
Bearer Token Usage, as the companion to the OAuth 2.0 framework in RFC 6749. It is registered in the
IANA HTTP Authentication Scheme Registry alongside Basic (RFC 7617) and Digest, so
it is a first-class HTTP authentication scheme rather than a vendor convention — even though a great many
APIs now use it with no OAuth authorisation server anywhere in sight.
The word bearer is doing all the work, and it is borrowed from finance: a bearer bond pays whoever physically holds it, with no name attached. A bearer token behaves the same way. There is no proof-of-possession step, no signature computed over the request, and no binding to the caller's key, device or address. The server does not verify that you are the party the token was issued to; it verifies that the token is genuine and unexpired, and that is the whole check. Whoever holds the token is the client, permanently, until the token expires or is revoked.
Three operational rules fall straight out of that definition, and they are not negotiable:
- HTTPS only. A bearer token sent over plaintext HTTP is a password shouted across a room.
RFC 6750 requires TLS; pair it with
Strict-Transport-Securityso a client cannot be downgraded. - Short lifetimes. Because there is no way to prove a holder is legitimate, the only practical containment for a stolen token is that it stops working quickly.
- Never in a URL, never in a log. Anything that records the request line or the
Authorizationheader records a working credential.
If those constraints are unacceptable for your threat model, the alternative is a sender-constrained token — covered at the end of this guide — which cryptographically binds the token to the client so that possession alone is no longer enough.
Anatomy of the header
The wire format is one line, and it is unforgiving:
GET /v1/orders?limit=25 HTTP/1.1 Host: api.example.com Accept: application/json Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjRmM2EifQ.eyJzdWIiOiJ1c2VyXzQyIn0.Xk1sJ3
Read it left to right. Authorization is the field name, defined by RFC 9110 as carrying
credentials for the target resource. Bearer is the authentication scheme token — technically
case-insensitive, as all scheme names are, but conventionally capitalised, and a small number of strict
servers do compare it literally, so write Bearer. Then exactly one space, then
the token. RFC 6750 gives the credential a grammar called b64token, which permits the base64,
base64url and token characters — letters, digits, -, ., _,
~, +, / and trailing = padding. Notably it does
not permit spaces, quotes or newlines.
Almost every “my token is rejected but it looks fine” report traces to one of these:
| Mistake | What gets sent | Result |
|---|---|---|
| Scheme omitted | Authorization: eyJhbGciOi… | Server sees an unknown scheme; usually 400 or 401 |
| Doubled scheme | Authorization: Bearer Bearer eyJ… | Token parsed as the literal string “Bearer”; invalid_token |
| Token quoted | Authorization: Bearer "eyJ…" | Quotes are outside the b64token grammar; rejected or decoded to nonsense |
| Trailing newline from a copy-paste | Bearer eyJ…\n | Silently invalid signature, or a client-library header-injection error |
| Two spaces after the scheme | Bearer eyJ… | Lenient servers cope, strict ones do not |
| Wrong field name | Authentication: Bearer eyJ… | Header ignored entirely; looks like no credential was sent |
| Template not interpolated | Bearer ${TOKEN} | The literal placeholder reaches the server — surprisingly common in shell scripts |
If you are not sure which of these you are hitting, send the request from the API tester and look at the exact bytes it reports sending — an extra space or a stray newline is visible there in a way it never is inside application code.
Field names are case-insensitive, so authorization: is fine — HTTP/2 and HTTP/3 require
lowercase on the wire anyway. See the HTTP headers guide for the wider
rules on header syntax and which fields survive a proxy.
What is inside the token: JWT or opaque
This is the single most common misconception about the scheme. A bearer token is not necessarily a JWT. “Bearer” describes how the credential travels; the format of the credential is an entirely separate decision made by whoever issues it. There are two mainstream choices.
A JWT (RFC 7519) is self-contained: three base64url segments carrying a header, a claims payload and a signature. A resource server holding the right key can validate it without calling anybody, and can read the claims — subject, scope, expiry — directly out of it. An opaque token is just a long random string with no internal structure at all; it means nothing except as a lookup key into the issuer's database or cache, where the real session record lives.
| Property | JWT bearer token | Opaque bearer token |
|---|---|---|
| Size on the wire | Hundreds of bytes to a few KB; grows with every claim | Typically 20–60 characters, constant |
| Verification | Offline — signature check against a public key or shared secret | Requires a lookup at the issuer or a shared store |
| Revocation | Hard. A valid signature and a future exp means accepted, unless you add a denylist | Instant — delete the row and the next request fails |
| Information leakage | The payload is encoded, not encrypted; anyone who intercepts it can read every claim | Reveals nothing; the string is meaningless without the store |
| Introspection | Optional; the claims are already there | The point of RFC 7662 Token Introspection — POST the token to /introspect and get back active, scope, exp, sub |
| Best fit | Many resource servers, high request volume, tolerable staleness window | Sensitive operations, immediate logout, a single trusted issuer |
Neither wins outright, and the trade-off — statelessness against revocability — is examined in depth in the OAuth vs JWT guide, which also explains how tokens get issued in the first place. For the practical side, if you hold a token and want to know which kind you have, paste it into the JWT decoder: if it splits into three dot-separated parts and produces readable JSON, it is a JWT and the decoder will show you every claim, the algorithm and the expiry. If it does not, it is opaque, and the only party who can tell you anything about it is the issuer.
Decoding is not verifying. A decoder reads the payload without checking the signature — which is exactly why you must never trust claims you have not cryptographically validated. The JWT decoder page covers algorithm confusion and claim validation in detail.
Bearer vs Basic vs API key
Bearer is one of three patterns you will meet on real APIs, and the differences are substantive rather than cosmetic.
Basic authentication
RFC 7617 Basic sends Authorization: Basic base64(username:password) on
every single request. The critical point, which trips up a remarkable number of teams:
base64 is an encoding, not encryption. It is trivially and losslessly reversible by anyone,
with no key involved. You can prove it to yourself in ten seconds — take YWRtaW46aHVudGVyMg==,
paste it into the Base64 decoder, and read the password back out. Anything that
captures the header — a log aggregator, a proxy, a crash report — has captured the real password.
That is the structural problem with Basic: the credential it transmits is the user's long-lived password. It cannot be scoped down to “read orders only”, it cannot be expired independently of the account, revoking it means a password reset for a human being, and it is fundamentally incompatible with multi-factor authentication, because a second factor is an interactive step and Basic has no interaction in it.
Bearer
Bearer transmits a credential that is derived from an authentication event rather than being the authentication secret itself. That indirection is the entire benefit: the token can be minted with a narrow scope, given a short expiry, revoked without touching the account, rotated on a schedule, and issued only after whatever multi-factor ceremony the identity provider requires.
| Bearer | Basic | API key header | |
|---|---|---|---|
| What is transmitted | An issued access token | The user's actual password, base64-encoded | A long-lived shared secret |
| Reversible to a password? | No | Yes, trivially | No, but it is itself the secret |
| Typical lifetime | Minutes to an hour | Until the user changes the password | Months or years |
| Scoping | Per-token scopes and audience | None — full account rights | Sometimes, per key |
| Revocation | Revoke one token; others survive | Password reset, affecting everything | Delete the key |
| Rotation | Automatic, via refresh | Manual and disruptive | Manual, usually with an overlap window |
| Works with 2FA | Yes — factors happen before issuance | No | Not applicable; identifies a machine |
| Header | Authorization: Bearer … | Authorization: Basic … | X-API-Key: … or similar |
The API key in a custom header
The third pattern — X-API-Key: sk_live_…, or an equivalent vendor-named field — is not an HTTP
authentication scheme at all. It is a convention, which means no standard challenge on failure, no scheme
registry entry, and no interoperable tooling. It identifies an application rather than a user acting
through an application, and it is normally long-lived, so it lives in a secrets manager, not in a browser.
Note also that the X- prefix itself was deprecated by RFC 6648 back in 2012, so a new API is
better off naming the field without it. Where an API key genuinely represents a machine identity with stable
permissions, it is a perfectly reasonable choice; where it is used as a stand-in for user sessions, it is a
bearer token with none of the lifetime discipline.
For the wider landscape — sessions, mTLS, HMAC request signing and where each belongs — see the API authentication overview.
The other transmission methods, and why not to use them
RFC 6750 actually defines three ways to send a bearer token, and it is worth knowing all three so you can recognise them in someone else's API.
- Authorization request header field.
Authorization: Bearer <token>. This is the method the spec prefers and the only one you should implement. - Form-encoded body parameter. A parameter named
access_tokeninside a request body withContent-Type: application/x-www-form-urlencoded. Permitted only when the request is not aGET, the body is single-part, and the parameter is sent with UTF-8 encoding. It exists for constrained clients that genuinely cannot set headers. - URI query parameter.
GET /v1/orders?access_token=<token>. The specification includes it and then immediately warns against it, and later OAuth security guidance goes further and says not to use it.
The query-parameter method is discouraged for a very concrete reason: URLs are recorded everywhere. They land
in web-server access logs, in reverse-proxy and CDN logs, in browser history, in the Referer
header sent to any third-party resource the page loads, in analytics pipelines, in error trackers, and in the
bookmark someone pastes into a group chat. Each of those is a full copy of a working credential sitting in a
system that was never designed to hold secrets. A response served from such a URL is also more likely to be
cached by an intermediary that has no idea it is authorised content. RFC 6750 additionally requires that any
response to a query-parameter-authenticated request carries Cache-Control: private.
The rule is short: use the header. If you are forced to consume an API that only accepts a query parameter, treat the token as compromised-by-design — keep it extremely short-lived and narrowly scoped.
Failure responses: 401, 403 and the challenge
A resource server that rejects a bearer token has a defined vocabulary for saying so, and using it correctly is what lets generic client libraries recover automatically.
401 Unauthorized
401 means the credential is missing, malformed, expired or otherwise not
accepted. RFC 9110 requires a WWW-Authenticate header on every 401 — a
401 without a challenge is a spec violation, and it is depressingly common. For Bearer, the
challenge looks like this:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.example.com",
error="invalid_token",
error_description="The access token expired"
Content-Type: application/json
RFC 6750 defines three error codes for that error parameter:
| Error code | Meaning | Status |
|---|---|---|
invalid_request | The request is malformed — a duplicated parameter, a token sent two different ways, a missing parameter | 400 |
invalid_token | The token is expired, revoked, malformed or fails validation | 401 |
insufficient_scope | The token is genuine but does not carry the scope this request needs | 403 |
Note the distinction the spec draws, because it is the one people get backwards.
401 is about the credential; 403 is about the
permission. If the token cannot be validated, answer 401 and the client knows to refresh
or re-authenticate. If the token validates perfectly but lacks orders:write, answer
403 with error="insufficient_scope" and a scope parameter naming what
was required — retrying with the same token is pointless, and telling the client so saves it a wasted refresh
round-trip. Keep error_description human-readable and non-sensitive: it is a debugging aid, not a
place to explain your validation internals to an attacker.
Symptom to cause
| Symptom | Likely cause |
|---|---|
401 on every request, including ones that worked yesterday | Signing key rotated, or the token was issued for a different environment; check iss and the kid in the header |
Works in curl, fails in the browser | Not authentication at all — the preflight is rejecting the Authorization header. See the CORS guide |
Works for a while, then 401 mid-session | Access token hit its exp; the client needs a refresh path, not a longer token |
401 immediately after issuance | Clock skew — the resource server's clock is behind the issuer's, so nbf or iat is in its future. Allow a small leeway, typically 30–60 seconds |
403 on one endpoint only | Scope missing from the token; re-request authorisation with the scope included |
| Token “looks right” but signature fails | Whitespace, a truncated copy-paste, or the wrong audience. Decode it in the JWT decoder and compare aud |
| Header appears absent server-side | A proxy or load balancer stripped Authorization, which some strip by default on redirects and cross-host rewrites |
Lifetime, refresh and rotation
Because bearer semantics offer no way to tell a thief from the rightful holder, lifetime is the security control. Hence the standard two-token split.
The access token is the bearer token you attach to API calls. It is deliberately short-lived — five minutes to an hour is typical — so a leaked one has a small blast radius. The refresh token lives much longer, but it is never sent to a resource server: its only destination is the authorisation server's token endpoint, where it is exchanged for a fresh access token. That asymmetry is the point. The credential that travels constantly, across many services and log pipelines, expires fast; the credential that lasts is exposed to exactly one endpoint over one TLS connection.
For public clients — anything whose code the user can read, which means every SPA and mobile app — refresh token rotation is now the expected practice. Each refresh returns a new refresh token and invalidates the previous one. If an old refresh token is ever presented again, that is reuse detection: either the token was stolen and the thief got there first, or the legitimate client is replaying, and the server cannot distinguish the two. The correct response is to invalidate the whole token family and force re-authentication. It is disruptive by design, and it converts a silent long-lived compromise into a single visible logout.
Practical client rules: refresh proactively, a little before exp, rather than waiting
for a 401; serialise refreshes so that ten concurrent requests hitting expiry do not fire ten
refreshes and trip your own reuse detection; and retry the original request exactly once after a successful
refresh, then give up. How tokens are issued in the first place — the grant flows, the token endpoint, PKCE —
belongs to the OAuth vs JWT guide.
Storing the token in a client
On a server or a native app this is a solved problem: keep the token in memory, or in the OS keychain or credential store, and never write it to a config file that might reach version control. In a browser there is a genuine trade-off, and anyone who tells you it has one obviously correct answer is skipping a step.
localStorage or sessionStorage is readable by any JavaScript
running on the page. That means a single cross-site scripting flaw — in your own code, or in any dependency
in your bundle, or in a tag-manager script someone added last quarter — exfiltrates the token in one line.
The upside is that it is simple, it works cleanly across origins, and it is immune to CSRF because nothing is
attached automatically.
An httpOnly cookie inverts both properties. JavaScript cannot read it, so XSS
cannot steal the token directly — though a script running on your page can still use the session by
issuing requests, which is a smaller but non-zero exposure. The cost is that the browser attaches the cookie
automatically to qualifying requests, which is precisely the condition cross-site request forgery exploits.
You answer that with SameSite=Lax or Strict, the Secure flag, and an
anti-CSRF token for state-changing requests. The CORS guide is the place to
understand why cookies and the Authorization header behave so differently cross-origin.
A common middle path for SPAs: keep the access token in a JavaScript variable in memory only — gone on
refresh, never persisted — and keep the refresh token in an httpOnly, Secure,
SameSite cookie scoped to the token endpoint's path. XSS cannot read either one persistently, and
the cookie's automatic attachment is harmless because the token endpoint is not a state-changing API. It is
more work, and it is the honest answer to a problem with no free option.
Security checklist
- TLS everywhere. Reject plaintext outright, and send
Strict-Transport-Securitywith a longmax-ageso a client is never downgraded. - Short
exp. Minutes, not days. Pair with refresh rather than stretching the access token. - Validate
audandiss. A token minted for a different service by the same issuer must not be accepted by yours. Checking only the signature is how a low-value service's token becomes a high-value service's session. - Check scope per endpoint, not once at the gateway. Answer
403 with
insufficient_scopewhen it is missing. - Never log the
Authorizationheader. Add it to the redaction list in your logger, your APM agent, your error reporter and your HTTP debug dumps — all four, because they are separate code paths and each has leaked tokens for somebody. - Redact in support tooling. Session replay and “copy as cURL” buttons both capture headers by default.
- Rotate signing keys on a schedule, publish them at a JWKS endpoint, and key them by
kidso old and new coexist during the rollover. - Have a revocation story before you need one: an introspection endpoint, a short-lived
denylist keyed by
jti, or a lifetime short enough that revocation is moot. - Do not put secrets in a JWT payload. It is encoded, not encrypted — see the JWT decoder for a live demonstration.
Beyond bearer: sender-constrained tokens
If “possession is sufficient” is too weak for what you are protecting, the answer is a token bound to
something the client can prove it controls. Two standards do this.
Mutual-TLS client certificate-bound access tokens (RFC 8705) tie the token to the client's
TLS certificate: the token records a thumbprint of the certificate, and a resource server accepts it only on a
connection presenting that same certificate. DPoP (RFC 9449) — Demonstrating Proof of
Possession — works at the application layer instead: the client holds a key pair and sends a signed JWT proof
in a DPoP header alongside each request, covering the HTTP method and URI, and the access token
is bound to the public key. A stolen DPoP-bound token is useless without the private key. Both cost more to
operate than plain Bearer, which is why plain Bearer remains the default for most APIs — but for financial and
healthcare interfaces, sender-constraining is increasingly the requirement.
Try it yourself
The fastest way to internalise this is to send a few requests. Open the API tester, pick the
Auth tab, choose Bearer and paste a token — the tool builds
Authorization: Bearer <token> for you, with the one space in the right place and nothing
appended, which quietly eliminates half the mistakes in the table above. Then send the same request without
the token and read the WWW-Authenticate header the server returns; that challenge tells you
exactly what it wanted.
A useful three-step exercise: call a token-protected endpoint from the
REST API tester and confirm a 200; deliberately corrupt one
character of the token and watch it become a 401 with
error="invalid_token"; then call an endpoint your scopes do not cover and compare the
403. Use “copy as cURL” to lift the whole request into a terminal or a
teammate's message — and remember that the copied command contains a live credential, so treat it the way you
would treat a password before pasting it anywhere.
If a token is being rejected and you cannot see why, decode it in the
JWT decoder and check exp, aud and
iss before assuming the server is at fault. For the format-versus-framework question underneath
all of this, read OAuth vs JWT; for the other schemes you might use
instead, the API authentication guide surveys the field; and for
building a repeatable set of checks around an authenticated endpoint, the
REST API testing guide covers the workflow.
Frequently asked questions
What does Bearer mean in the Authorization header?
That possession of the token is the whole proof. Nothing is signed over the request and nothing binds the token to you, so whoever holds it can use it exactly as the real client would — hence HTTPS only, short lifetimes, and never in URLs or logs.
Is a bearer token always a JWT?
No. Bearer describes transmission, not format. A bearer token may be a JWT, verifiable offline, or an opaque random string resolved by a lookup. GitHub and many others issue opaque bearer tokens.
What is the correct format of a bearer token header?
Authorization: Bearer, one space, then the token — no quotes, no trailing newline. The scheme
name is case-insensitive but conventionally written Bearer, and the token must match RFC 6750's
b64token grammar.
Should a missing scope return 401 or 403?
401 when the token is missing, malformed or expired, with a
WWW-Authenticate: Bearer challenge. 403 with
insufficient_scope when the token is valid but lacks the required permission.
Can I send an access token in the URL query string?
RFC 6750 defines the method but discourages it. URLs reach access logs, browser history,
Referer headers and analytics, so the token leaks in several places at once. Use the header.
Where should a browser app store a bearer token?
There is no free answer. localStorage is readable by any XSS on the page;
httpOnly cookies are not, but they are attached automatically and bring CSRF considerations you
must answer with SameSite and anti-CSRF tokens.