NestJS Authorization Standards
Yew Search uses cookie-based session authentication for all user interactions. This document explains our authentication architecture, the rationale behind choosing cookies over JWT, and how sessions are managed throughout the application.
Why Cookies Over JWT?
Yew Search exclusively uses cookie-based sessions and never uses JWT tokens for authentication. This is a deliberate architectural decision driven by privacy and security requirements.
The Remote Logout Problem
The fundamental issue with JWT tokens is that they cannot be invalidated remotely:
JWT Tokens:
- Self-contained and stateless
- Validated using cryptographic signature alone
- Not stored in any central database or session store
- Valid until they expire (typically hours or days)
- Cannot be revoked or invalidated before expiration
Cookie-Based Sessions:
- Session ID stored in cookie, session data stored server-side
- Validated against central session store (Redis, database, etc.)
- Can be deleted from session store at any time
- Immediately invalidated when removed from session store
Why Remote Logout Matters
For a privacy-focused search engine handling sensitive personal data, remote logout is critical:
Security Breach Response:
- If we detect suspicious activity, we can immediately terminate all sessions
- If a user reports their account compromised, we can revoke access instantly
- If credentials are leaked, existing sessions become worthless immediately
Privacy Control:
- Users can see all active sessions and terminate any of them
- Admins can force logout users if needed (e.g., account suspension)
- No waiting for tokens to expire while unauthorized access continues
Compliance & Audit:
- Central session store provides audit trail of all active sessions
- Can track when sessions were created, last used, and terminated
- Meets compliance requirements for access control and monitoring
The JWT Problem Illustrated
Scenario: User reports account compromise at 2:00 PM
With JWT (24-hour expiration):
- User changes password at 2:00 PM
- Attacker's JWT remains valid until 2:00 PM next day
- Attacker has 24 hours of continued access to user's emails
- User's privacy is violated for 24 hours
With Cookie Sessions:
- User changes password at 2:00 PM
- System deletes all sessions from session store
- Attacker's next request fails immediately
- Access revoked in milliseconds, not hours
For Yew Search, where users trust us with their most personal data (emails, files, messages), this difference is unacceptable. We must be able to terminate access immediately.
Authentication Architecture
Session Storage
Sessions are stored in a central session store, separate from the main application database:
Recommended: Redis
- Fast in-memory storage for quick session lookups
- Built-in TTL (time-to-live) for automatic session expiration
- Atomic operations for session updates
- Can be clustered for high availability
Alternative: PostgreSQL
- Store sessions in dedicated
user_sessiontable - Good for small deployments or when Redis isn't available
- Simpler infrastructure (one database instead of two)
- Trade-off: Slightly slower than Redis
Session Data Structure
Each session contains:
interface UserSession {
sessionId: string; // UUID - stored in cookie
userId: string; // Foreign key to user table
createdAt: Date; // When session was created
lastAccessedAt: Date; // Last time session was used
expiresAt: Date; // When session expires
ipAddress: string; // IP address of client
userAgent: string; // Browser/device information
metadata?: { // Optional additional data
loginMethod: 'password' | 'oauth';
deviceName?: string;
location?: string;
};
}
Cookie Configuration
Cookies must be configured with strict security settings:
{
name: 'yew_session', // Cookie name
httpOnly: true, // Prevents JavaScript access (XSS protection)
secure: true, // Only sent over HTTPS (production)
sameSite: 'strict', // CSRF protection
maxAge: 7 * 24 * 60 * 60, // 7 days in seconds
path: '/', // Available to entire application
domain: undefined, // Current domain only (no subdomains)
}
Important Security Flags:
- httpOnly: true - Cookie cannot be accessed by JavaScript, protecting against XSS attacks
- secure: true - Cookie only sent over HTTPS in production (prevents interception)
- sameSite: 'strict' - Cookie not sent on cross-site requests (CSRF protection)
Authentication Flow
Registration Flow
1. User submits registration form
→ POST /v1/auth/register
→ Body: { email, password, name }
↓
2. Backend validates input
→ Email format, password strength, uniqueness
↓
3. Backend creates user record
→ Hash password with Argon2
→ Store in user table
↓
4. Backend creates session
→ Generate UUID session ID
→ Store session in session store
→ Set session cookie
↓
5. Backend returns user data
→ Response: { user: { id, email, name } }
→ Cookie: yew_session=<sessionId>
↓
6. User is now authenticated
Login Flow
1. User submits login form
→ POST /v1/auth/login
→ Body: { email, password }
↓
2. Backend validates credentials
→ Look up user by email
→ Verify password with Argon2
↓
3. Backend creates new session
→ Generate UUID session ID
→ Store session with userId in session store
→ Set session cookie
↓
4. Backend returns user data
→ Response: { user: { id, email, name } }
→ Cookie: yew_session=<sessionId>
↓
5. User is now authenticated
Request Authentication Flow
Every authenticated request:
1. Browser sends request with cookie
→ Cookie: yew_session=<sessionId>
↓
2. Authentication guard extracts session ID
→ Read from cookie
↓
3. Guard looks up session in session store
→ Query Redis/PostgreSQL for session
↓
4. Guard validates session
→ Session exists?
→ Session not expired?
→ Session belongs to valid user?
↓
5a. Valid session:
→ Update lastAccessedAt timestamp
→ Attach user to request context
→ Allow request to proceed
5b. Invalid session:
→ Delete session cookie
→ Return 401 Unauthorized
→ Redirect to login page
Logout Flow
1. User clicks "Log Out"
→ POST /v1/auth/logout
↓
2. Backend extracts session ID from cookie
↓
3. Backend deletes session from session store
→ DELETE from Redis/PostgreSQL
↓
4. Backend clears session cookie
→ Set-Cookie: yew_session=; expires=Thu, 01 Jan 1970
↓
5. Backend returns success
→ Response: { success: true }
↓
6. User is now logged out
→ Next request will be unauthorized
Remote Logout Flow
Remote logout allows terminating sessions from anywhere:
Logout Specific Session:
1. User views "Active Sessions" page
→ GET /v1/auth/sessions
→ Returns list of all active sessions
↓
2. User clicks "Terminate" on suspicious session
→ DELETE /v1/auth/sessions/:sessionId
↓
3. Backend deletes that specific session
→ Remove from session store by sessionId
↓
4. Targeted session is immediately invalid
→ Other sessions remain active
Logout All Sessions:
1. User clicks "Log Out All Devices"
→ POST /v1/auth/logout-all
↓
2. Backend finds all sessions for user
→ Query session store for userId
↓
3. Backend deletes all sessions except current
→ Bulk delete from session store
→ Keep current session so user stays logged in
↓
4. All other sessions are immediately invalid
→ Any requests with old session IDs fail
Admin Force Logout:
1. Admin identifies compromised user account
↓
2. Admin triggers force logout
→ POST /v1/admin/users/:userId/force-logout
↓
3. Backend deletes all sessions for that user
→ Bulk delete from session store by userId
↓
4. User is logged out everywhere immediately
→ Must login again with new credentials
Session Lifecycle
Session Expiration
Sessions have two expiration mechanisms:
1. Absolute Expiration (Max Age)
- Sessions expire after fixed time from creation (e.g., 7 days)
- Prevents infinitely long-lived sessions
- Configurable per deployment (shorter for high-security environments)
2. Idle Timeout
- Sessions expire after period of inactivity (e.g., 24 hours)
lastAccessedAttimestamp updated on each request- If
now - lastAccessedAt > idleTimeout, session is invalid
Both checks happen during request authentication.
Session Renewal
For long-lived sessions, we can implement sliding expiration:
// On each authenticated request:
if (session.expiresAt - now < renewalThreshold) {
// Less than 1 day until expiration
session.expiresAt = now + maxAge; // Extend by another 7 days
await sessionStore.update(session);
}
This allows active users to stay logged in indefinitely while inactive sessions expire.
Session Cleanup
Expired sessions should be periodically cleaned from the session store:
Redis:
- Automatic with TTL (time-to-live) - no cleanup needed
- Set TTL when creating session:
SET session:{id} {data} EX 604800
PostgreSQL:
- Periodic cleanup job (cron or scheduled task)
DELETE FROM user_session WHERE expires_at < NOW()- Run daily during low-traffic hours
Security Considerations
Password Storage
Passwords are hashed using Argon2, the winner of the Password Hashing Competition:
import * as argon2 from 'argon2';
// Hash password on registration/password change
const hashedPassword = await argon2.hash(plainPassword);
// Verify password on login
const isValid = await argon2.verify(hashedPassword, plainPassword);
Never use:
- bcrypt (outdated, vulnerable to GPU attacks)
- MD5/SHA (not designed for passwords)
- Plain text (obviously never)
Session Fixation Protection
Prevent session fixation attacks by generating new session ID after login:
// Before login
const oldSessionId = request.session.id;
// After successful login
await sessionStore.delete(oldSessionId); // Delete old session
const newSessionId = generateUUID(); // Generate new ID
await sessionStore.create(newSessionId, { userId });
response.cookie('yew_session', newSessionId);
CSRF Protection
Cookie-based authentication is vulnerable to CSRF attacks. Mitigate with:
1. SameSite Cookie Attribute
sameSite: 'strict' // Cookie not sent on cross-site requests
2. CSRF Tokens (Optional) For additional protection, implement CSRF tokens for state-changing operations:
// Generate token on login, store in session
session.csrfToken = generateRandomToken();
// Require token on POST/PUT/DELETE requests
if (request.body.csrfToken !== session.csrfToken) {
throw new ForbiddenException('Invalid CSRF token');
}
Rate Limiting
Protect authentication endpoints from brute force attacks:
// Login endpoint rate limiting
@Throttle({ default: { limit: 5, ttl: 60000 } }) // 5 attempts per minute
@Post('login')
async login(@Body() body: LoginDto) {
// ...
}
Session Hijacking Protection
Detect and prevent session hijacking:
// Store IP and User-Agent on session creation
session.ipAddress = request.ip;
session.userAgent = request.headers['user-agent'];
// On each request, compare
if (session.ipAddress !== request.ip) {
// Log suspicious activity
await logger.warn('Session IP mismatch', {
sessionId: session.id,
expectedIp: session.ipAddress,
actualIp: request.ip,
});
// Optional: Invalidate session or require re-authentication
}
Note: IP comparison should be lenient (mobile users change IPs frequently).
Implementation Example
NestJS Authentication Module Structure
src/auth/
├── auth.controller.ts # Login, logout, register endpoints
├── auth.service.ts # Business logic for authentication
├── auth.module.ts # Module definition
├── session/
│ ├── session.service.ts # Session CRUD operations
│ ├── session.store.ts # Interface to Redis/PostgreSQL
│ └── session.entity.ts # PostgreSQL entity (if not using Redis)
├── guards/
│ └── session-auth.guard.ts # Guard to protect routes
└── decorators/
└── current-user.decorator.ts # Extract user from request
Session Service Interface
@Injectable()
export class SessionService {
/**
* Create a new session for a user
*/
async createSession(
userId: string,
ipAddress: string,
userAgent: string,
): Promise<string> {
// Implementation omitted
}
/**
* Retrieve session by ID and validate
*/
async getSession(sessionId: string): Promise<UserSession | null> {
// Implementation omitted
}
/**
* Update session last accessed timestamp
*/
async touchSession(sessionId: string): Promise<void> {
// Implementation omitted
}
/**
* Delete specific session (logout)
*/
async deleteSession(sessionId: string): Promise<void> {
// Implementation omitted
}
/**
* Delete all sessions for a user (logout all)
*/
async deleteUserSessions(
userId: string,
exceptSessionId?: string,
): Promise<void> {
// Implementation omitted
}
/**
* Get all active sessions for a user
*/
async getUserSessions(userId: string): Promise<UserSession[]> {
// Implementation omitted
}
/**
* Cleanup expired sessions (PostgreSQL only)
*/
async cleanupExpiredSessions(): Promise<void> {
// Implementation omitted
}
}
Session Authentication Guard
@Injectable()
export class SessionAuthGuard implements CanActivate {
constructor(
private sessionService: SessionService,
private userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const sessionId = request.cookies['yew_session'];
if (!sessionId) {
throw new UnauthorizedException('No session cookie');
}
const session = await this.sessionService.getSession(sessionId);
if (!session) {
throw new UnauthorizedException('Invalid session');
}
if (session.expiresAt < new Date()) {
await this.sessionService.deleteSession(sessionId);
throw new UnauthorizedException('Session expired');
}
// Load user and attach to request
const user = await this.userService.findById(session.userId);
if (!user) {
await this.sessionService.deleteSession(sessionId);
throw new UnauthorizedException('User not found');
}
request.user = user;
request.session = session;
// Update last accessed timestamp
await this.sessionService.touchSession(sessionId);
return true;
}
}
Protected Route Example
@Controller('v1/user')
@UseGuards(SessionAuthGuard) // Require authentication for all routes
export class UserController {
@Get('profile')
async getProfile(@CurrentUser() user: User) {
return new UserProfileResponseDTO({ result: user });
}
@Get('sessions')
async getSessions(@CurrentUser() user: User) {
const sessions = await this.sessionService.getUserSessions(user.id);
return new UserSessionsResponseDTO({ results: sessions });
}
@Delete('sessions/:sessionId')
async deleteSession(
@CurrentUser() user: User,
@Param('sessionId') sessionId: string,
) {
// Verify session belongs to user
const session = await this.sessionService.getSession(sessionId);
if (session.userId !== user.id) {
throw new ForbiddenException('Not your session');
}
await this.sessionService.deleteSession(sessionId);
return { success: true };
}
}
Environment Configuration
# Session configuration
SESSION_SECRET=random_secret_key_change_in_production
SESSION_MAX_AGE=604800 # 7 days in seconds
SESSION_IDLE_TIMEOUT=86400 # 24 hours in seconds
SESSION_SECURE=true # Require HTTPS (production only)
SESSION_SAME_SITE=strict # CSRF protection
# Redis session store (recommended)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Or PostgreSQL session store (alternative)
# Sessions stored in user_session table
# No additional configuration needed
Trade-offs and Limitations
Scalability
Cookies + Redis:
- ✅ Scales horizontally (stateless application servers)
- ✅ Fast session lookups (in-memory Redis)
- ⚠️ Requires Redis infrastructure and maintenance
- ⚠️ Redis failure means all sessions lost (use persistence/replication)
Cookies + PostgreSQL:
- ✅ Simpler infrastructure (one database)
- ✅ Sessions persist across restarts
- ⚠️ Slower session lookups (database query per request)
- ⚠️ Adds load to primary database
JWT Advantages We're Giving Up
What JWT does better:
- No session store needed (simpler infrastructure)
- Faster (no database lookup per request)
- Better for microservices (each service can verify independently)
Why we don't care:
- Yew Search is a monolith, not microservices
- Session lookup overhead is negligible compared to search operations
- Remote logout is more important than marginal performance gains
- Infrastructure complexity is acceptable for better security
Future Considerations
Multi-Factor Authentication (MFA)
When adding MFA in the future:
- Store MFA status in session:
session.mfaVerified = true - Require MFA verification for sensitive operations
- Invalidate session if MFA is disabled/compromised
Passwordless Authentication
Cookie sessions work perfectly with passwordless auth:
- Magic links via email
- WebAuthn/FIDO2 keys
- OAuth (already implemented for integrations)
All methods create a session after successful authentication.
Session Analytics
Track session metrics for security and UX:
- Average session duration
- Sessions per user
- Login times and patterns
- Geographic distribution of sessions
- Device types
This data helps detect anomalies and improve security.
Summary
Yew Search uses cookie-based sessions exclusively because:
- Remote logout is critical - We can terminate sessions immediately when needed
- Privacy and security first - Users trust us with sensitive data, we must protect it
- Compliance and audit - Central session store provides full visibility
- JWT's benefits don't apply - We're a monolith, not microservices
The slight infrastructure complexity and performance overhead are acceptable trade-offs for the security guarantees that cookie-based sessions provide.