Working with Token-Based Authentication

1. Understanding Token Authentication Flow

1. POST /login (creds) → Auth Server
2. ← {accessToken, refreshToken}
3. Client stores tokens
4. GET /api/* Authorization: Bearer <token>
5. API validates signature + claims
6. On 401: POST /refresh → new accessToken
      

2. Generating Access Tokens

PropertyRecommendation
TypeJWT (self-contained) or opaque (referenced)
Lifetime5–15 min
AudienceSpecific API identifier
SigningRS256 / ES256 (asymmetric)
ScopeLeast privilege per request

3. Validating Access Tokens

Example: JWT validation in Java (Spring)

JWTVerifier verifier = JWT.require(Algorithm.RSA256(publicKey, null))
    .withIssuer("https://auth.example.com")
    .withAudience("api://orders")
    .acceptLeeway(60)
    .build();
DecodedJWT jwt = verifier.verify(token);   // throws on invalid
String userId = jwt.getSubject();
CheckWhy
SignatureAuthenticity
expNot expired
nbfNot before now
issTrusted issuer
audIntended for this API
jti revocationNot blacklisted

4. Setting Token Expiration

TokenLifetimeRationale
Access5–15 minMinimize blast radius
Refresh1–30 daysUX, with rotation
ID Token (OIDC)5–60 minUsed at login time
Service-to-service1–5 minFrequent rotation

5. Implementing Token Refresh Mechanism

Example: Refresh endpoint

app.post("/oauth/token", async (req, res) => {
  const { grant_type, refresh_token } = req.body;
  if (grant_type !== "refresh_token") return res.status(400).json({ error: "invalid_grant" });

  const stored = await db.refreshTokens.findByHash(sha256(refresh_token));
  if (!stored || stored.revoked || stored.expiresAt < new Date())
    return res.status(401).json({ error: "invalid_grant" });

  await db.refreshTokens.revoke(stored.id);                 // rotation
  const newRefresh = await issueRefreshToken(stored.userId, stored.id);
  const access = issueAccessToken(stored.userId, stored.scope);
  res.json({ access_token: access, refresh_token: newRefresh, token_type: "Bearer", expires_in: 900 });
});

6. Storing Tokens Securely

ClientRecommendedAvoid
Web SPAHttpOnly cookie (BFF pattern)localStorage (XSS-exposed)
MobileiOS Keychain / Android KeystorePlain prefs / SQLite
ServerEncrypted secret storeCode / env files in git
CLIOS keyring, file mode 600Shell history

7. Revoking Tokens

Token TypeRevocation Approach
OpaqueDelete from DB / mark revoked
JWT short-livedWait for expiry, rely on refresh denial
JWT long-livedBlacklist by jti in Redis until exp
RefreshDB row delete + revoke family on reuse

8. Using Opaque Tokens vs JWT

AspectJWTOpaque
ValidationLocal (signature check)Network call (introspection)
RevocationHard (needs blacklist)Easy (delete row)
SizeLarge (1KB+)Small (32–64 bytes)
InspectableYes (base64 decode)No
Best forMicroservices, stateless APIsPublic APIs, frequent revocation

9. Implementing Token Rotation

AspectDetail
WhenEvery refresh exchange
Family IDGroup rotated tokens to detect reuse
Reuse detectionIf revoked token used → revoke entire family + alert
WindowTiny grace period for race conditions (5–30 sec)

10. Handling Token Expiration Errors

Server ResponseClient Action
401 + WWW-Authenticate: Bearer error="invalid_token"Trigger refresh
401 + error="invalid_grant" on refreshRedirect to login
403Insufficient scope — request consent

Example: Axios interceptor

let refreshing;
api.interceptors.response.use(r => r, async (err) => {
  if (err.response?.status !== 401) throw err;
  refreshing ??= refreshAccessToken().finally(() => refreshing = null);
  await refreshing;
  return api(err.config);
});

11. Using Bearer Token Format

SpecRFC 6750
HeaderAuthorization: Bearer eyJhbGciOi...
Query (discouraged)?access_token=... (logged in URLs!)
Body (form-encoded)access_token=...
SecurityHTTPS mandatory, treat as bearer = whoever has it can use it