API authentication determines who can access your system. Choose the wrong approach and you'll face security vulnerabilities or user experience problems. This guide compares the main strategies to help you make the right choice.
Authentication vs Authorization
These terms are often confused:
Authentication: Verifying identity. "Who are you?" Authorization: Verifying permissions. "What can you do?"
Authentication happens first. Once you know who the user is, authorization determines their access level. This guide focuses on authentication;proving identity.
Session-Based Authentication
The traditional approach: user logs in, server creates a session, browser stores a session ID in a cookie.
1. User submits username/password
2. Server validates credentials
3. Server creates session (stored in database/Redis)
4. Server sends session ID as cookie
5. Browser sends cookie with every request
6. Server looks up session to identify user
Advantages:
- Simple to implement
- Easy to invalidate (delete the session)
- Works well for traditional web apps
- HTTP-only cookies protect against XSS
Disadvantages:
- Requires server-side storage
- Sticky sessions or shared storage needed for multiple servers
- Doesn't work well for mobile apps or third-party API access
- CSRF protection required
Sessions work well for first-party web applications where users interact through a browser and the backend and frontend share a domain.
JWT (JSON Web Tokens)
JWTs are self-contained tokens that encode user information and a signature. The server can verify the token without looking up a session.
1. User submits username/password
2. Server validates credentials
3. Server creates JWT containing user ID and expiration
4. Server signs JWT with secret key
5. Client stores JWT (localStorage, memory, or cookie)
6. Client sends JWT with requests (usually in Authorization header)
7. Server verifies signature and extracts user info
JWT structure:
header.payload.signature
header: {"alg": "HS256", "typ": "JWT"}
payload: {"sub": "user-123", "exp": 1234567890}
signature: HMAC-SHA256(header + payload, secret)
Advantages:
- Stateless;no server-side storage
- Works across domains and services
- Good for APIs consumed by mobile apps
- Contains user claims (role, permissions)
Disadvantages:
- Can't be invalidated without extra infrastructure
- Tokens can get large if you embed too much data
- Token storage on the client requires care
- Replay attacks possible before expiration
JWT Pitfalls
JWTs are often misused. Common mistakes:
Storing sensitive data: The payload is encoded, not encrypted. Anyone can decode it. Don't include passwords, full user data, or sensitive information.
Long expiration times: A compromised JWT works until it expires. Keep access tokens short-lived (minutes to hours).
Storing in localStorage: Vulnerable to XSS attacks. If possible, use HTTP-only cookies or keep tokens in memory with refresh token rotation.
Not validating properly: Always verify the signature. Validate the expiration. Check the issuer and audience claims.
OAuth 2.0 Flows
OAuth 2.0 is an authorization framework that enables third-party access to user resources. It's also commonly used for authentication (despite being designed for authorization).
Authorization Code Flow (for web apps with backends):
1. App redirects user to authorization server
2. User logs in and grants permission
3. Authorization server redirects back with code
4. App exchanges code for tokens (server-side)
5. App uses access token to call APIs
This is the most secure flow because tokens never pass through the browser.
PKCE Extension (for mobile/SPA):
1. App generates code verifier and challenge
2. App redirects user with code challenge
3. User logs in and grants permission
4. Authorization server redirects back with code
5. App exchanges code + verifier for tokens
PKCE (Proof Key for Code Exchange) protects against code interception attacks.
Client Credentials Flow (machine-to-machine):
1. App sends client ID and secret to auth server
2. Auth server returns access token
3. App uses token to call APIs
No user involvement;used for backend services calling other services.
When to Use Which Approach
Session cookies: First-party web apps where the frontend and backend share a domain. Traditional server-rendered apps.
JWT with short expiration + refresh tokens: APIs consumed by mobile apps or single-page apps. Microservices architectures where services need to verify identity without calling an auth service.
OAuth 2.0 Authorization Code: When users grant access to their data on another service (e.g., "Sign in with Google"). When you're building an API platform that third parties will integrate with.
OAuth 2.0 Client Credentials: Service-to-service authentication where no user is involved.
Token Storage Security
Where you store tokens affects security:
HTTP-only cookies: Protected from JavaScript access (XSS safe), but require CSRF protection and same-site configuration.
localStorage: Vulnerable to XSS;any script on the page can read it. Generally not recommended for sensitive tokens.
Memory (JavaScript variable): Safest from attacks, but lost on page refresh. Works with refresh tokens stored in HTTP-only cookies.
Secure cookie + token in memory pattern:
1. Store refresh token in HTTP-only, secure, same-site cookie
2. Store access token only in memory
3. On page load, use refresh token to get new access token
4. Access token never touches persistent storage
Refresh Token Patterns
Short-lived access tokens need refresh tokens for good user experience:
1. User logs in, receives access token (15 min) and refresh token (7 days)
2. Access token expires
3. Client sends refresh token to get new access token
4. Server validates refresh token and issues new tokens
5. Old refresh token is invalidated (rotation)
Refresh token rotation: Issue a new refresh token each time one is used. If an old refresh token is used, assume compromise and invalidate all tokens for that user.
Absolute expiration: Even with refresh, force re-authentication after some period (e.g., 7-30 days).
Common Vulnerabilities to Avoid
Timing attacks: Use constant-time comparison for tokens and secrets.
Token leakage in logs: Don't log full tokens. If needed, log only the last few characters.
Weak secrets: Use cryptographically random secrets of sufficient length (256+ bits for HMAC).
Missing expiration: All tokens should expire. Infinite tokens are a major risk.
Insufficient logout: Invalidate sessions and revoke refresh tokens on logout.
Token in URL: Never pass tokens as query parameters;they end up in server logs and browser history.
Implementation Example
A practical pattern for a web app with API:
// Login endpoint
POST /auth/login
Request: { email, password }
Response: {
accessToken: "...", // 15-minute expiration
expiresIn: 900
}
Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=Strict
// API requests
GET /api/users
Authorization: Bearer <accessToken>
// Refresh endpoint
POST /auth/refresh
Cookie: refreshToken=...
Response: {
accessToken: "...",
expiresIn: 900
}
Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=Strict
// Logout
POST /auth/logout
Cookie: refreshToken=...
Response: 204
Set-Cookie: refreshToken=; expires=past; HttpOnly; Secure; SameSite=Strict
Conclusion
There's no universal best approach to API authentication;the right choice depends on your use case. Sessions work well for traditional web apps. JWTs suit APIs and mobile apps. OAuth 2.0 enables third-party integrations.
Whatever you choose, focus on the fundamentals: validate tokens properly, keep secrets secure, use HTTPS, and implement proper token lifecycle management. Authentication vulnerabilities are among the most costly to fix after deployment.