Chapter 113 โ Authentication Architecture
๐ Definition
Authentication architecture is the set of components and protocols that establish who a user is, while authorization determines what they can do.
๐ Common Approaches
| Approach | Storage | Pros | Cons |
|---|---|---|---|
| Session (server-side) | Session ID in cookie, data in Redis | Easy revocation, smaller cookie | Sticky sessions / shared store |
| Stateless JWT | Self-contained token in Authorization header |
Scales horizontally, no DB hop per request | Hard to revoke before expiry |
| JWT + Refresh | Short access in memory, long refresh in httpOnly cookie | Best of both worlds | More moving parts |
| OAuth 2.0 / OIDC | Tokens issued by an Identity Provider (Google, Auth0, Cognito) | No password handling on your servers | Setup complexity |
๐๏ธ Reference Architecture (JWT + Refresh)
[Browser]
โ POST /auth/login (email, password)
โผ
[API Gateway / Load Balancer]
โ
โผ
[Auth Service]
โ 1. validate creds
โ 2. issue Access JWT (15m)
โ 3. issue Refresh JWT (7d), store hash in DB
โ
โผ
Set-Cookie: refresh=...; HttpOnly; Secure; SameSite=Strict
Body: { accessToken }
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
[Browser stores access in memory]
โญโโโโโโโโโ subsequent requests โโโโโโโโโโฎ
[Browser]
โ GET /api/orders
โ Authorization: Bearer <access>
โผ
[API Gateway] โ verify JWT signature (public key, no DB hit)
โ
โผ
[Backend Service]
โ handle business logic
โผ
[Database]
โญโโโ access expires (401) โโโโฎ
[Browser]
โ POST /auth/refresh (cookie sent automatically)
โผ
[Auth Service]
โ 1. verify refresh signature
โ 2. check DB allowlist (revoke check)
โ 3. ROTATE โ delete old, issue new
โ 4. issue new access
โผ
Set-Cookie: refresh=new; Body: { accessToken }๐ป Code Example โ Multi-Provider Login
// Strategy pattern: same interface, multiple providers
const providers = {
password: async ({ email, password }) => {
const user = await User.findOne({ email }).select("+password");
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new AppError(401, "BAD_CREDS");
}
return user;
},
google: async ({ idToken }) => {
const profile = await verifyGoogleToken(idToken);
return User.findOneAndUpdate(
{ email: profile.email },
{ $setOnInsert: { name: profile.name } },
{ upsert: true, new: true }
);
},
github: async ({ code }) => {
const { access_token } = await exchangeGitHubCode(code);
const profile = await fetchGitHubProfile(access_token);
return User.findOneAndUpdate({ email: profile.email }, { ... }, { upsert: true, new: true });
},
};
app.post("/auth/:provider", async (req, res) => {
const fn = providers[req.params.provider];
if (!fn) return res.status(404).end();
const user = await fn(req.body);
// Issue tokens as in Chapter 31
});๐ Role-Based Access Control (RBAC)
// User โ Role โ Permissions
{
user: { id: "u1", roles: ["editor"] },
roles: {
admin: ["users.*", "posts.*"],
editor: ["posts.read", "posts.write"],
viewer: ["posts.read"],
},
}// NestJS guard
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const required = this.reflector.get<string>("permission", ctx.getHandler());
const { user } = ctx.switchToHttp().getRequest();
return user.permissions.includes(required);
}
}
@Post()
@RequirePermission("posts.write")
create(@Body() dto: CreatePostDto) {}๐ Attribute-Based Access Control (ABAC)
Decisions can depend on attributes of the user, resource, and environment:
function canEdit(user, post) {
if (user.role === "admin") return true;
if (user.id === post.authorId && post.status !== "published") return true;
return false;
}Use ABAC when permissions depend on relationships (ownership, team membership).
๐ก๏ธ Common Pitfalls
| Pitfall | Mitigation |
|---|---|
| Long-lived access tokens | Keep access tokens short (5โ15 min) |
| Refresh tokens forever | Rotate on every use, store hash in DB |
Storing JWT in localStorage |
Vulnerable to XSS โ prefer httpOnly cookie for refresh |
| No CSRF protection on cookies | Use SameSite=Strict or a CSRF token |
| Same secret across environments | Use env-specific secrets, rotate periodically |
| Password reset enumeration | Always respond identically whether the email exists or not |
๐ Choosing an Approach (Quick Guide)
| Scenario | Recommendation |
|---|---|
| SaaS for general users | JWT + refresh (httpOnly cookie) |
| Internal tools (single-domain) | Session cookies |
| Mobile apps | JWT + refresh (secure storage) |
| Enterprise SSO | OIDC with Auth0 / Cognito / Okta |
| Microservices | RS256 JWT (gateway verifies with public key) |
๐ป Code Example โ Password Reset Flow
1. POST /auth/forgot { email }
- Generate reset token: random 32 bytes
- Save HASH of token + expiry (15 min) in DB
- Email link: /reset?token=<raw>
2. POST /auth/reset { token, newPassword }
- Hash the incoming raw token
- Look up by hash; check expiry
- Update password, delete tokenimport crypto from "crypto";
async function forgotPassword({ email }) {
const user = await User.findOne({ email });
if (!user) return; // don't leak existence
const raw = crypto.randomBytes(32).toString("hex");
const hash = crypto.createHash("sha256").update(raw).digest("hex");
await ResetToken.create({ userId: user.id, hash, expiresAt: Date.now() + 15*60*1000 });
await sendEmail(email, `Reset: https://app.example/reset?token=${raw}`);
}๐ฏ Likely Interview Questions
- Walk me through your authentication flow.
- How do you handle token expiry?
- How do you revoke a JWT?
- Stateful vs stateless auth?
- How does OAuth differ from your own JWT auth?
- What's RBAC vs ABAC?