Bearer Tokens: What 'Bearer' Means and What It Costs You

3 min readWeb Development

Bearer means whoever holds the token can use it. That security model is simpler than alternatives but requires careful token handling — expiry, storage, and transport matter more than they do with session cookies.

authenticationoauth2jwtsecurity

Why the word "Bearer"

HTTP defines multiple authentication schemes. The scheme name appears before the credential in the Authorization header and tells the server how to interpret what follows:

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Authorization: Digest username="alice", realm="example.com", ...

Bearer tokens come from OAuth 2.0 (RFC 6750). "Bearer" means whoever bears (holds) this token is granted access. The server does not check who is presenting the token — only that the token is valid.

| Scheme | Header Example | Meaning | |--------|---------------|---------| | Basic | Basic QWxhZGRpbjpvc... | Base64-encoded username:password | | Bearer | Bearer eyJhbGciOi... | OAuth 2.0 access token (often a JWT) | | Digest | Digest username="alice"... | Challenge-response, avoids sending password in clear |

Bearer token security model

ConceptWeb Authentication

A bearer token is a 'bearer instrument' — like cash. Whoever holds it can spend it. This is simpler than proof-of-possession schemes but shifts responsibility entirely to secure transmission and storage.

Prerequisites

  • HTTP headers
  • OAuth 2.0 basics
  • HTTPS

Key Points

  • No binding between token and client identity. A stolen bearer token is immediately usable by anyone.
  • All security depends on HTTPS. Send a bearer token over HTTP and it is trivially interceptable.
  • Tokens should have short expiry. A JWT with a 24-hour lifetime that is stolen is compromised for 24 hours.
  • The server validates the token's signature or makes an introspection request — it does not need a database lookup for every request (unlike sessions).

The token lifecycle in practice

In an OAuth 2.0 flow, the access token is short-lived (minutes to hours). A refresh token (longer-lived, stored securely) is used to obtain new access tokens without re-authentication.

1. User authenticates → authorization server issues:
   access_token (15 min TTL) + refresh_token (30 day TTL)

2. Client sends requests:
   Authorization: Bearer <access_token>

3. access_token expires → client uses refresh_token to get new tokens:
   POST /token { grant_type: refresh_token, refresh_token: <...> }
   → New access_token + optionally new refresh_token (rotation)

4. User logs out → refresh_token is revoked server-side
   Future refresh attempts fail → user must re-authenticate

The access token is typically a JWT — stateless, self-describing, verifiable by signature alone. The refresh token is opaque — a random string the authorization server maps to a user session in its database. Revocation of a refresh token is immediate. Revocation of an access token requires either waiting for expiry or maintaining a denylist.

Where to store tokens in a browser

This is the most common source of security mistakes in SPA authentication.

localStorage: persists across tabs and browser restarts. Accessible by any JavaScript on your domain. If your site has an XSS vulnerability, any injected script can read the token and exfiltrate it.

sessionStorage: cleared when the tab closes. Still accessible by JavaScript — XSS can still steal it.

Memory (JavaScript variable): cleared on page refresh. Not accessible by XSS from other documents. Requires re-authentication after refresh unless you use a silent token renewal strategy (hidden iframe or background fetch to the authorization server).

HttpOnly cookie: not accessible by JavaScript at all. Protects against XSS token theft. Introduces CSRF risk — mitigated by SameSite=Strict/Lax. Works for same-domain SPAs; has complications for cross-domain APIs.

💡The recommended pattern for SPAs: in-memory access tokens + HttpOnly refresh token cookie

The pattern most security guides now recommend:

  1. Store the access token in memory (JavaScript variable). Short TTL means loss on page reload is acceptable — the token will be refreshed.
  2. Store the refresh token in an HttpOnly, Secure, SameSite=Strict cookie. XSS cannot read it. CSRF cannot forge requests because the refresh endpoint checks the CSRF token or relies on SameSite.
  3. On page load, use the refresh token (in the cookie) to silently obtain a new access token.
// On app startup — refresh token is sent automatically via cookie
async function initAuth() {
    const response = await fetch('/auth/refresh', {
        method: 'POST',
        credentials: 'include',  // sends the HttpOnly cookie
    });
    if (response.ok) {
        const { access_token } = await response.json();
        setAccessToken(access_token);  // store in memory
    }
}

// On API calls — manually attach the in-memory access token
async function apiCall(url) {
    return fetch(url, {
        headers: { 'Authorization': `Bearer ${getAccessToken()}` }
    });
}

This approach protects against XSS (refresh token is HttpOnly, access token is short-lived in memory) and CSRF (access token is in a header, not auto-attached; refresh token endpoint uses SameSite).

Proof-of-possession: the alternative to bearer

Bearer tokens have a fundamental weakness: a stolen token is immediately usable. Proof-of-Possession (PoP) tokens bind the token to the client's private key. The server verifies that the request was signed by the holder of the key the token was issued to.

Authorization: PoP <token>
DPoP: <signed proof JWT>

DPoP (Demonstrating Proof of Possession, RFC 9449) is the OAuth 2.0 extension for this. The access token is bound to a public key. Each request includes a signed proof JWT proving the sender holds the private key. Even if an attacker captures the token and the proof JWT, they cannot reuse either — the proof JWT is request-specific (includes the HTTP method, URL, and a nonce).

DPoP is increasingly adopted for high-security contexts (financial APIs, FAPI compliance). For most applications, bearer tokens with short TTLs and HTTPS are sufficient.

An SPA stores its JWT access token in localStorage for convenience (persists across page refreshes). A comment section on the same origin is later found to have a stored XSS vulnerability. What is the impact?

medium

The JWT has a 24-hour expiry. The XSS payload can execute arbitrary JavaScript in the victim's browser.

  • AThe impact is limited because the JWT is validated server-side on each request
    Incorrect.Server-side validation checks the signature and expiry but cannot distinguish a legitimate holder from an attacker who stole the token. A valid JWT used from an attacker's machine passes all server-side checks.
  • BThe attacker can read the JWT from localStorage and replay it from their own machine for up to 24 hours
    Correct!localStorage is accessible to any JavaScript running on the origin, including injected XSS payloads. The script can read the token with localStorage.getItem('token') and send it to the attacker. The attacker then includes it in their own requests for the full 24-hour lifetime. This is why short JWT expiry and HttpOnly cookies for refresh tokens matter.
  • CThe XSS cannot access localStorage due to the Same-Origin Policy
    Incorrect.Same-Origin Policy restricts cross-origin access. An XSS payload runs in the same origin — it has full access to localStorage, cookies (non-HttpOnly), and can make authenticated requests.
  • DThe impact is limited to the duration of the user's session
    Incorrect.The token has a 24-hour expiry independent of the user's session. The attacker's window is 24 hours from token issuance, not the length of the user's current session.

Hint:Think about what 'XSS running in the same origin' means for access to browser storage.