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
| Property | Recommendation |
|---|---|
| Type | JWT (self-contained) or opaque (referenced) |
| Lifetime | 5–15 min |
| Audience | Specific API identifier |
| Signing | RS256 / ES256 (asymmetric) |
| Scope | Least 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();
| Check | Why |
|---|---|
| Signature | Authenticity |
exp | Not expired |
nbf | Not before now |
iss | Trusted issuer |
aud | Intended for this API |
jti revocation | Not blacklisted |
4. Setting Token Expiration
| Token | Lifetime | Rationale |
|---|---|---|
| Access | 5–15 min | Minimize blast radius |
| Refresh | 1–30 days | UX, with rotation |
| ID Token (OIDC) | 5–60 min | Used at login time |
| Service-to-service | 1–5 min | Frequent 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
| Client | Recommended | Avoid |
|---|---|---|
| Web SPA | HttpOnly cookie (BFF pattern) | localStorage (XSS-exposed) |
| Mobile | iOS Keychain / Android Keystore | Plain prefs / SQLite |
| Server | Encrypted secret store | Code / env files in git |
| CLI | OS keyring, file mode 600 | Shell history |
7. Revoking Tokens
| Token Type | Revocation Approach |
|---|---|
| Opaque | Delete from DB / mark revoked |
| JWT short-lived | Wait for expiry, rely on refresh denial |
| JWT long-lived | Blacklist by jti in Redis until exp |
| Refresh | DB row delete + revoke family on reuse |
8. Using Opaque Tokens vs JWT
| Aspect | JWT | Opaque |
|---|---|---|
| Validation | Local (signature check) | Network call (introspection) |
| Revocation | Hard (needs blacklist) | Easy (delete row) |
| Size | Large (1KB+) | Small (32–64 bytes) |
| Inspectable | Yes (base64 decode) | No |
| Best for | Microservices, stateless APIs | Public APIs, frequent revocation |
9. Implementing Token Rotation
| Aspect | Detail |
|---|---|
| When | Every refresh exchange |
| Family ID | Group rotated tokens to detect reuse |
| Reuse detection | If revoked token used → revoke entire family + alert |
| Window | Tiny grace period for race conditions (5–30 sec) |
10. Handling Token Expiration Errors
| Server Response | Client Action |
|---|---|
401 + WWW-Authenticate: Bearer error="invalid_token" | Trigger refresh |
401 + error="invalid_grant" on refresh | Redirect to login |
| 403 | Insufficient 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
| Spec | RFC 6750 |
|---|---|
| Header | Authorization: Bearer eyJhbGciOi... |
| Query (discouraged) | ?access_token=... (logged in URLs!) |
| Body (form-encoded) | access_token=... |
| Security | HTTPS mandatory, treat as bearer = whoever has it can use it |