Basic & Bearer Authentication
Once your connection is encrypted with HTTPS, you need a way to prove who you are to the server. Authentication is how a server confirms your identity before granting access to protected resources.
1. Authentication vs Authorisation
These terms are often confused:
| Concept | Question answered | Example |
|---|---|---|
| Authentication | Who are you? | "I'm Alice" — proving identity |
| Authorisation | What can you do? | "Alice can read, but not delete posts" — permission check |
You must be authenticated before you can be authorised.
2. The Authorization Header
HTTP carries authentication credentials in the Authorization request header:
Authorization: [Scheme] [Credentials]
│ │ │
│ │ └── The encoded credentials
│ └───────────── The method (e.g. Basic, Bearer, Digest, AWS4-HMAC-SHA256)
└──────────────────────────── The header name
3. Basic Authentication
HTTP Basic Auth is the simplest scheme. The client sends a username and password with every request.
Encoding
The credentials are Base64-encoded (not encrypted!):
username:password
↓ Base64 encode
dXNlcm5hbWU6cGFzc3dvcmQ=
Request header:
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
⚠️ Base64 is encoding, not encryption. Anyone who intercepts the header can decode it in one step. Basic Auth is only safe over HTTPS. Never use it over plain HTTP.
How Basic Auth Works
Client Server
│── GET /protected HTTP/1.1 ────►│
│ │
│◄── 401 Unauthorized ───────────│
│ WWW-Authenticate: Basic realm="Nandhoo API"
│
│ User enters username + password
│ Browser base64 encodes "alice:password123"
│
│── GET /protected HTTP/1.1 ────►│
│ Authorization: Basic YWxpY2U6cGFzc3dvcmQxMjM=
│ │
│◄── 200 OK ──────────────────── │
Using Basic Auth in JavaScript
const username = 'alice';
const password = 'password123';
// Manually encode credentials
const credentials = btoa(`${username}:${password}`);
const response = await fetch('https://api.nandhoo.com/protected', {
headers: {
'Authorization': `Basic ${credentials}`
}
});
// Or using fetch's built-in approach via a URL (not recommended for security)
// Never put credentials in URLs in production! They appear in logs.
When to Use Basic Auth
| ✅ Acceptable | ❌ Not Acceptable |
|---|---|
| Internal APIs between backend services | User-facing login flows |
| CI/CD systems with service accounts | Any HTTP (non-HTTPS) connection |
| Webhook endpoints | Anywhere the credentials persist long-term |
| Quick local development testing |
4. Bearer Token Authentication
Bearer Auth uses tokens instead of credentials. You authenticate once (with your password) to get a token, then use the token for all subsequent requests.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Bearer Token vs Basic Auth
Basic Auth (every request):
Client ──── username:password ──────────────────────────────────────► Server
Risky — password travels in every request
Bearer Auth (token-based):
Step 1 — Login:
Client ──── username:password ─────────────────────────────────────► Server
Client ◄─── access_token, refresh_token ─────────────────────────── Server
Step 2 — Use API (password NEVER sent again):
Client ──── Bearer access_token ───────────────────────────────────► Server
Client ◄─── Protected resource ──────────────────────────────────── Server
5. JWT — JSON Web Tokens
The most common type of bearer token is a JWT (JSON Web Token).
Structure
A JWT has three Base64URL-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiI0MiIsIm5hbWUiOiJBbGljZSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTg2NDI4MCwiZXhwIjoxNzExOTUwNjgwfQ
.
X5kLmQ3Rn8Ps7tH2MvJz4Yw9Dk1Nc6Ua0Fe5Bi3Go
Part 1 — Header (algorithm and type):
{
"alg": "HS256", // Signing algorithm
"typ": "JWT" // Token type
}
Part 2 — Payload (claims):
{
"sub": "42", // Subject — user ID
"name": "Alice", // Custom claim
"role": "admin", // Custom claim
"iat": 1711864280, // Issued At (Unix timestamp)
"exp": 1711950680 // Expiration (Unix timestamp, 24h later)
}
Part 3 — Signature:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
SECRET_KEY
)
Standard JWT Claims
| Claim | Name | Meaning |
|---|---|---|
sub | Subject | User ID — who this token is about |
iss | Issuer | Who issued the token (e.g. "nandhoo.com") |
aud | Audience | Who the token is intended for |
iat | Issued At | Unix timestamp when the token was created |
exp | Expiration | Unix timestamp when the token expires |
nbf | Not Before | Token is not valid before this time |
jti | JWT ID | Unique ID for this token (enables blacklisting) |
How the Server Validates a JWT
Incoming JWT: header.payload.signature
Server:
1. Base64URL-decode the header → get algorithm (HS256)
2. Re-compute expected signature:
HMACSHA256(header + "." + payload, SECRET_KEY)
3. Compare with received signature
- If they match → payload is authentic (not tampered with)
- If they don't → reject (403 Forbidden)
4. Check exp claim → reject if token is expired
5. Read sub, role, etc. from payload → authorise the request
No database lookup needed — the token is self-contained.
6. Access Tokens and Refresh Tokens
Since JWT access tokens can't be easily revoked, they are kept short-lived (minutes to hours). Refresh tokens are long-lived and used only to get new access tokens.
LOGIN FLOW:
Client ── POST /auth/login (email+password) ──────────────► Server
Client ◄── { Server
access_token: "eyJ..." (expires: 15 min),
refresh_token: "dRT..." (expires: 7 days)
}
USE API (while access token is valid):
Client ── GET /api/profile (Bearer access_token) ─────────► Server
Client ◄── 200 OK, user profile data Server
ACCESS TOKEN EXPIRES:
Client ── POST /auth/refresh (Bearer refresh_token) ──────► Server
Client ◄── { access_token: "eyJ..." (new, expires: 15 min) } Server
LOGOUT (revoke refresh token):
Client ── POST /auth/logout (Bearer refresh_token) ───────► Server
Server deletes/blacklists the refresh_token
Client ◄── 204 No Content Server
7. Full Authentication Flow Code Example
class AuthService {
constructor(apiBase) {
this.apiBase = apiBase;
this.accessToken = null;
}
// Step 1 — Login
async login(email, password) {
const res = await fetch(`${this.apiBase}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (res.status === 401) throw new Error('Invalid email or password');
if (!res.ok) throw new Error('Login failed');
const { access_token, refresh_token } = await res.json();
// Store in memory (most secure) or httpOnly cookie (better)
this.accessToken = access_token;
localStorage.setItem('refresh_token', refresh_token);
return access_token;
}
// Step 2 — Make authenticated requests
async get(path) {
if (!this.accessToken) throw new Error('Not logged in');
const res = await fetch(`${this.apiBase}${path}`, {
headers: { 'Authorization': `Bearer ${this.accessToken}` }
});
// Token expired — try to refresh silently
if (res.status === 401) {
await this.refresh();
return this.get(path); // Retry once
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Step 3 — Refresh access token silently
async refresh() {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) throw new Error('Session expired. Please log in again.');
const res = await fetch(`${this.apiBase}/auth/refresh`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${refreshToken}` }
});
if (!res.ok) {
this.logout();
throw new Error('Session expired. Please log in again.');
}
const { access_token } = await res.json();
this.accessToken = access_token;
}
// Step 4 — Logout
async logout() {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
await fetch(`${this.apiBase}/auth/logout`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${refreshToken}` }
}).catch(() => {}); // Fire and forget
}
this.accessToken = null;
localStorage.removeItem('refresh_token');
}
}
// Usage
const auth = new AuthService('https://api.nandhoo.com');
await auth.login('alice@example.com', 'password123');
const profile = await auth.get('/profile');
await auth.logout();
8. OAuth 2.0 — Delegated Access
OAuth 2.0 is the standard for allowing a third-party application to act on your behalf without sharing your password.
"Login with Google" Flow (Authorization Code Grant):
1. User clicks "Login with Google"
Your App → Redirects to: https://accounts.google.com/o/oauth2/auth
?client_id=YOUR_APP_ID
&redirect_uri=https://nandhoo.com/callback
&scope=email+profile
&response_type=code
2. User logs into Google and grants permission
3. Google redirects back to:
https://nandhoo.com/callback?code=AUTHORIZATION_CODE
4. Your backend exchanges the code for tokens:
POST https://oauth2.googleapis.com/token
{client_id, client_secret, code, redirect_uri}
→ {access_token, id_token, refresh_token}
5. Your backend reads user info from ID token
→ User is logged in!
OAuth 2.0 is used by GitHub, Google, Apple, Twitter, and most social login systems.
9. API Keys
API keys are a simpler form of bearer authentication used by public APIs.
// Common styles of passing API keys:
// 1. Authorization header (preferred)
fetch('https://api.openai.com/v1/chat', {
headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` }
});
// 2. Custom header
fetch('https://api.service.com/data', {
headers: { 'X-API-Key': process.env.API_KEY }
});
// 3. Query parameter (avoid — keys appear in logs/browser history)
fetch(`https://api.service.com/data?api_key=${API_KEY}`);
// ❌ Don't do this for sensitive keys!
10. Storing Tokens Securely
| Storage Location | XSS Risk | CSRF Risk | Notes |
|---|---|---|---|
| Memory (JS variable) | Low ✅ | None ✅ | Lost on page refresh — best for access tokens |
sessionStorage | High ❌ | None ✅ | Lost on tab close. XSS can read it |
localStorage | High ❌ | None ✅ | Persists across sessions. XSS can read it |
HttpOnly cookie | None ✅ | Medium ⚠️ | JS can't read it but CSRF must be mitigated |
Best practice: Store the access token in memory (JS variable); store the refresh token in an HttpOnly cookie so JavaScript can't steal it.
11. Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Using Basic Auth over HTTP | Credentials visible to anyone intercepting traffic | Always use HTTPS with Basic Auth |
| Storing JWTs in localStorage | XSS can steal tokens | Store access token in memory; refresh token in HttpOnly cookie |
| Long-lived access tokens | Compromised token valid for days/weeks | Keep access tokens short-lived (15 min) |
No token expiry (exp claim) | Tokens valid forever | Always set exp in JWTs |
| Including passwords in JWTs | Anyone can base64-decode the payload | JWTs are encoded, not encrypted — use claims only |
| Not revoking refresh tokens on logout | Old refresh tokens can get new access tokens | Delete/blacklist refresh token on logout |
12. Review Questions
- What is the difference between authentication and authorisation?
- Why is Basic Auth only safe over HTTPS?
- What are the three parts of a JWT?
- Is the JWT payload encrypted? What does Base64URL encoding do?
- Why are access tokens kept short-lived, and what is the role of a refresh token?
- What is OAuth 2.0 and when would you use it instead of your own auth?
- Where should you store a JWT refresh token for maximum security?