The Bearer Token Flow, Step by Step

Log in once, receive a token, attach it to every request yourself — the scheme that dominates modern APIs.

SyntaxAuthorization: Bearer <access-token>

A bearer token is a credential with one rule: whoever bears it is treated as the user. No further proof is asked for. That makes it simple, portable, and completely dependent on the token never leaking.

The sequence

  1. The client posts credentials to /login (or completes an OAuth flow).
  2. The server verifies them and mints a token — signed, with an expiry, naming the user.
  3. The token comes back in the response body. No cookie is involved.
  4. The client stores it — in memory, ideally.
  5. Every request attaches it explicitly: Authorization: Bearer <token>. Your code does this; the browser does not.
  6. The API verifies the signature and expiry and reads the user id from the claims.
  7. On expiry the client refreshes using a longer-lived refresh token, and retries.
Bearer token issuance and useClient (abc.com)API (dfg.com)POST /login {email, password}200 {accessToken, expiresIn: 600}kept in memoryGET /api/ordersAuthorization: Bearer eyJhbGci… (added by your code)200 OK — verified locally, no lookup
Nothing is ambient: if your code does not attach the header, the request is anonymous.

What you gain

  • No CSRF. Another site's JavaScript cannot read your token, so it cannot forge an authenticated request. This is the big one.
  • Works everywhere — mobile, CLI, server-to-server, any origin. There is no cookie policy to fight.
  • Cross-origin is easy. abc.com calling dfg.com needs CORS for the header, but no SameSite gymnastics and no third-party cookie problem.
  • Cheap verification if the token is signed — no shared session store.

What you take on

  • You must store it safely. localStorage is readable by any XSS on your page.
  • Revocation is not free. A signed token is valid until it expires.
  • You own the plumbing — attaching headers, detecting expiry, refreshing, retrying, and not stampeding your refresh endpoint when ten requests expire at once.

Opaque or JWT?

"Bearer" says nothing about the token's format. It can be a random string the API looks up (opaque — easy to revoke) or a self-describing JWT (no lookup — hard to revoke). The next lessons cover JWTs because they are what you will meet most.

Example

Example · bash
# 1. Exchange credentials for a token
curl -X POST https://dfg.com/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"s3cret"}'

# {"accessToken":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
#  "tokenType":"Bearer","expiresIn":600}

# 2. Use it
TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
curl https://dfg.com/api/orders -H "Authorization: Bearer $TOKEN"

# 3. After 10 minutes
# {"error":"invalid_token","message":"expired"}  → refresh and retry

When to use it

  • A React SPA on abc.com calls an API on dfg.com with bearer tokens, sidestepping third-party cookie blocking entirely.
  • A mobile app stores its refresh token in the platform keychain and holds the short-lived access token only in memory.
  • A CI pipeline exchanges a service account credential for a 15-minute token, so a leaked build log exposes something that is already expired.

More examples

A client that refreshes once, not ten times

The shared refreshing promise is the detail that separates a working client from one that fires twenty refresh calls the moment a token expires on a busy screen.

Example · javascript
let accessToken = null;          // memory only — survives no reload, and no XSS read
let refreshing = null;           // the in-flight refresh promise, shared by all callers

async function refresh() {
  // The refresh token lives in an HttpOnly cookie scoped to /auth/refresh.
  const res = await fetch('https://dfg.com/auth/refresh', {
    method: 'POST', credentials: 'include',
  });
  if (!res.ok) { accessToken = null; throw new Error('session_expired'); }
  ({ accessToken } = await res.json());
  return accessToken;
}

export async function api(path, options = {}) {
  const call = (token) => fetch('https://dfg.com' + path, {
    ...options,
    headers: { 'Content-Type': 'application/json',
               ...options.headers,
               Authorization: `Bearer ${token}` },
  });

  if (!accessToken) accessToken = await (refreshing ??= refresh().finally(() => refreshing = null));

  let res = await call(accessToken);

  if (res.status === 401) {
    // Collapse concurrent refreshes into one request, then retry ONCE.
    const token = await (refreshing ??= refresh().finally(() => refreshing = null));
    res = await call(token);
  }

  if (!res.ok) throw new Error(res.statusText);
  return res.status === 204 ? null : res.json();
}

Issuing the pair on the server

The access token goes in the body so JavaScript can hold it in memory; the refresh token goes in an HttpOnly cookie so JavaScript can never read it. Each is placed where its risk is lowest.

Example · javascript
import jwt from 'jsonwebtoken';
import { randomBytes, createHash } from 'crypto';

app.post('/auth/login', loginLimiter, async (req, res) => {
  const user = await verifyCredentials(req.body);
  if (!user) return res.status(401).json({ error: 'invalid_credentials' });

  // Short-lived, stateless: this is what every API call carries.
  const accessToken = jwt.sign(
    { sub: String(user.id), scope: 'orders:read orders:write' },
    PRIVATE_KEY,
    { algorithm: 'RS256', expiresIn: '10m',
      issuer: 'https://dfg.com', audience: 'https://dfg.com/api', keyid: 'k1' },
  );

  // Long-lived, stateful: this is what makes revocation possible.
  const refreshToken = randomBytes(32).toString('base64url');
  await db.refreshTokens.insert({
    hash: createHash('sha256').update(refreshToken).digest('hex'),
    userId: user.id,
    familyId: crypto.randomUUID(),
    expiresAt: new Date(Date.now() + 30 * 24 * 3600e3),
  });

  res.cookie('rt', refreshToken, {
    httpOnly: true, secure: true, sameSite: 'strict',
    path: '/auth/refresh', maxAge: 30 * 24 * 3600e3,
  });

  res.json({ accessToken, tokenType: 'Bearer', expiresIn: 600 });
});

Discussion

  • Be the first to comment on this lesson.