Authentication
This guide covers authentication mechanisms, password hashing, multi-factor authentication (MFA), and session management in Cerberus IAM.
Overview
Cerberus supports multiple authentication methods:
- Session-based - Cookie-based sessions for admin UI
- Bearer tokens - JWT access tokens for API access
- API keys - Long-lived keys for server-to-server
- OAuth2/OIDC - Standard protocol flows
Authentication Middleware
Session Authentication
Used for admin UI and web applications:
import { authenticateSession } from '@/middleware/authn';
router.get('/me/profile', authenticateSession, async (req, res) => {
// req.user is populated with authenticated user
// req.authOrganisation contains the organization
res.json(req.user);
});How it Works:
- Extracts session cookie (
cerb_sidby default) - Validates session token (SHA-256 hash lookup)
- Checks expiration and idle timeout
- Loads user with roles and permissions
- Verifies user is not blocked
- Attaches
req.userandreq.authOrganisation
Response on Failure:
{
"type": "https://api.cerberus-iam.com/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid or expired session"
}Bearer Token Authentication
Used for OAuth2 clients and API access:
import { authenticateBearer } from '@/middleware/authn';
router.get('/v1/users', authenticateBearer, async (req, res) => {
// req.user contains JWT payload data
const users = await prisma.user.findMany({
where: { organisationId: req.user.organisationId },
});
res.json(users);
});How it Works:
- Extracts
Authorization: Bearer <token>header - Verifies JWT signature with public key
- Validates issuer, expiration
- Attaches minimal user info from JWT claims
API Key Authentication
Used for server-to-server integration:
import { authenticateApiKey } from '@/middleware/apikey';
router.post('/webhooks/incoming', authenticateApiKey, async (req, res) => {
// req.user contains API key user context
// req.tenant contains organization context
// req.apiKeyScopes contains granted scopes
res.json({ received: true });
});How it Works:
- Extracts
Authorization: Bearer <api_key>header - Looks up key by prefix
- Verifies hash with Argon2
- Checks revocation and expiration
- Updates
lastUsedAttimestamp - Attaches organization and scopes
Optional Authentication
Allows both authenticated and anonymous access:
import { optionalAuth } from '@/middleware/authn';
router.get('/oauth2/authorize', optionalAuth, async (req, res) => {
if (req.user) {
// User is authenticated
return showConsentPage(req, res);
} else {
// Redirect to login
return redirectToLogin(req, res);
}
});Password Authentication
Password Hashing
Cerberus uses Argon2id for password hashing:
import { hashPassword, verifyPassword } from '@/auth/password';
// Hash password
const hash = await hashPassword('user-password');
// $argon2id$v=19$m=65536,t=3,p=4$...
// Verify password
const valid = await verifyPassword(hash, 'user-password');
// true or falseAlgorithm Details:
- Algorithm: Argon2id (hybrid mode)
- Memory: 64 MB
- Iterations: 3
- Parallelism: 4 threads
- Salt: Random 16 bytes per password
Why Argon2id?
- Winner of Password Hashing Competition (2015)
- Resistant to GPU/ASIC attacks
- Memory-hard algorithm
- Hybrid mode (combines data-independent and data-dependent)
Password Validation
import { validatePasswordStrength } from '@/auth/password';
const result = validatePasswordStrength('weak');
// {
// valid: false,
// errors: [
// 'Password must be at least 8 characters',
// 'Password must contain at least one uppercase letter',
// 'Password must contain at least one number'
// ]
// }Default Policy:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
Custom Policy (Organization-Level):
// Organization password policy (JSON field)
{
"minLength": 12,
"requireUppercase": true,
"requireLowercase": true,
"requireNumber": true,
"requireSpecial": true,
"preventReuse": 5,
"maxAge": 90
}Login Flow
// POST /v1/auth/login
{
"email": "[email protected]",
"password": "SecurePass123",
"mfaToken": "123456" // optional - required when the user/org enforces MFA
}Process:
- Find user by email
- Check user is not blocked
- Verify password with Argon2
- Determine if MFA is required (organisation policy or prior enrolment)
- When required, verify the provided
mfaToken - Create the session and set the cookie
- Update login stats (
lastLoginAt,lastLoginIp,loginCount)
Response:
{
"message": "Login successful",
"user": {
"id": "usr_...",
"email": "[email protected]",
"name": "John Doe"
},
"organisation": {
"id": "org_...",
"slug": "acme-corp",
"name": "Acme Corporation"
}
}Password Reset Flow
1. Request Reset
// POST /v1/auth/forgot-password
{
"email": "[email protected]"
}Process:
- Find user by email
- Generate secure reset token (32 bytes)
- Store hashed token in database
- Set expiration (1 hour)
- Send reset email with link
Email Link:
https://admin.acme.com/reset-password?token=tok_...2. Reset Password
// POST /v1/auth/reset-password
{
"token": "tok_...",
"newPassword": "NewSecurePass123"
}Process:
- Validate token (format, existence, expiration, not yet consumed)
- Validate password strength
- Hash new password
- Update user password
- Mark token as consumed
- Revoke all existing sessions
Note: The API does not expose a separate token-inspection endpoint. Clients typically render a reset form using the token from the email link and rely on the POST request above to validate it.
Multi-Factor Authentication (MFA)
TOTP (Time-Based One-Time Password)
Cerberus implements TOTP based on RFC 6238.
Enable TOTP
POST /v1/me/mfa/enable
Returns the TOTP secret and OTP Auth URI for enrolment:
{
"secret": "JBSWY3DPEHPK3PXP",
"qrCodeUri": "otpauth://totp/Cerberus:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Cerberus"
}Process:
- Check MFA not already enabled
- Generate random 32-character secret
- Encrypt secret with
SECRET_ENCRYPTION_KEY - Generate QR code URI
- Store encrypted secret (not yet active)
QR Code Display: Users scan the QR code with authenticator apps:
- Google Authenticator
- Authy
- 1Password
- Microsoft Authenticator
Verify and Activate TOTP
POST /v1/me/mfa/verify
{
"token": "123456"
}Response:
{
"message": "MFA enabled successfully",
"backupCodes": ["ABCD-1234-EFGH-5678", "IJKL-9012-MNOP-3456"],
"warning": "Save these backup codes in a safe place. They can be used if you lose access to your authenticator."
}Process:
- Verify TOTP code against stored secret
- Generate 10 backup codes
- Hash backup codes with Argon2
- Set
mfaEnabled = true - Return plaintext backup codes (only shown once!)
TOTP Parameters:
- Period: 30 seconds
- Digits: 6
- Algorithm: SHA-1 (TOTP standard)
- Window: ±1 period (allows 30s clock skew)
Login with MFA
MFA is handled through the standard login endpoint. Supply the six-digit code via the optional mfaToken field when the organisation or user requires MFA.
Disable MFA
POST /v1/me/mfa/disable
{
"token": "123456"
}Process:
- Verify current TOTP code
- Set
mfaEnabled = false - Clear
totpSecret,backupCodes, andmfaMethods
Regenerate Backup Codes
POST /v1/me/mfa/backup-codes
{
"token": "123456"
}Response:
{
"backupCodes": ["NEW1-2345-CODE-6789"],
"message": "Backup codes regenerated successfully"
}Future MFA Methods
Planned support for:
- SMS: One-time codes via SMS
- Email: One-time codes via email
- WebAuthn: FIDO2/passkeys
- Push notifications: Mobile app approval
Session Management
Session Creation
import { createSession } from '@/auth/sessions';
const { session, sessionToken } = await createSession(userId, organisationId, ipAddress, userAgent);
// Set cookie
res.cookie(config.SESSION_COOKIE_NAME, sessionToken, {
httpOnly: true,
secure: config.SESSION_COOKIE_SECURE,
domain: config.SESSION_COOKIE_DOMAIN,
sameSite: 'lax',
maxAge: organisation.sessionLifetime * 1000,
});Session Properties:
- Token hashed with SHA-256
- Absolute expiration (from
organisation.sessionLifetime) - Idle timeout (from
organisation.sessionIdleTimeout) - IP address and user agent tracking
Session Validation
import { getSessionByToken } from '@/auth/sessions';
const sessionData = await getSessionByToken(sessionToken);
if (!sessionData) {
// Session invalid, expired, or idle timeout exceeded
return unauthorized('Invalid or expired session');
}
// Session is valid
const { user, organisation } = sessionData;Validation Checks:
- Session exists in database
- Not past
expiresAt - Not past idle timeout (
lastActivityAt + sessionIdleTimeout) - User not blocked
Automatic Cleanup:
- Expired sessions deleted on validation
- Idle sessions deleted on validation
lastActivityAtupdated on each request
Session Revocation
import { revokeSession, revokeAllUserSessions } from '@/auth/sessions';
// Revoke single session (logout)
await revokeSession(sessionId);
// Revoke all user sessions (security action)
await revokeAllUserSessions(userId);Session Lifetime Configuration
Per-organization configuration:
// Organisation table
{
"sessionLifetime": 3600, // 1 hour absolute expiration
"sessionIdleTimeout": 1800 // 30 minutes idle timeout
}Recommendations:
- Admin UI: 1 hour absolute, 30 min idle
- High-security: 15 min absolute, 5 min idle
- Low-security: 24 hours absolute, 2 hours idle
Email Verification
Verification Flow
1. Send Verification Email
// POST /v1/auth/send-verification-emailProcess:
- Generate verification token
- Create
EmailVerificationTokenrecord - Set expiration (24 hours)
- Send email with link
Email Link:
https://admin.acme.com/verify-email?token=tok_...2. Verify Email
// POST /v1/auth/verify-email
{
"token": "tok_..."
}Process:
- Find token
- Check not expired
- Check not consumed
- Set
user.emailVerifiedAt - Mark token as consumed
Account Security
Account Blocking
Administrators can block user accounts:
await prisma.user.update({
where: { id: userId },
data: {
blockedAt: new Date(),
blockedReason: 'Suspicious activity detected',
},
});Effects:
- All login attempts fail
- All sessions immediately invalidated
- All tokens remain valid (consider revoking separately)
Unblock:
await prisma.user.update({
where: { id: userId },
data: {
blockedAt: null,
blockedReason: null,
},
});Login Tracking
Every successful login updates:
{
"lastLoginAt": "2024-01-15T10:30:00Z",
"lastLoginIp": "203.0.113.42",
"loginCount": 47
}Use for:
- Security monitoring
- Detecting unusual activity
- User analytics
Failed Login Attempts
(Coming soon: Account lockout after N failed attempts)
Security Best Practices
1. Password Storage
- Never store plaintext passwords
- Never log passwords
- Use Argon2id for hashing
- Never send passwords in URLs
2. Session Security
// Production settings
SESSION_COOKIE_SECURE=true
SESSION_COOKIE_DOMAIN=.yourcompany.com- Always use
httpOnlycookies - Enable
secureflag in production - Use
sameSite: 'lax'or'strict' - Set appropriate domain
3. MFA Enforcement
// Organization-level MFA requirement
{
"requireMfa": true,
"allowedMfaMethods": ["totp"]
}4. Token Security
- Short access token lifetime (15-60 minutes)
- Long refresh token lifetime (days/weeks)
- Implement refresh token rotation
- Revoke on suspicious activity
5. Rate Limiting
Already configured for auth endpoints:
- Login: 30 requests/minute
- Password reset: 30 requests/minute
- Token endpoint: 30 requests/minute
Troubleshooting
Session Cookie Not Set
Problem: Login succeeds but session not created
Solutions:
- Check
SESSION_COOKIE_DOMAINmatches request domain - Verify
SESSION_COOKIE_SECUREappropriate for protocol - Check CORS credentials enabled
- Inspect browser cookie settings
MFA Code Invalid
Problem: TOTP code always fails
Solutions:
- Check device time is synchronized (NTP)
- Verify secret not corrupted
- Check
SECRET_ENCRYPTION_KEYconsistent - Try adjacent time window codes
Password Reset Link Expired
Problem: Token expired before user clicks
Solutions:
- Increase token expiration (default 1 hour)
- Check email delivery speed
- Allow token regeneration
- Clear expired tokens regularly
Next Steps
- Authorization - Roles and permissions
- Sessions - Session management deep dive
- OAuth2 - OAuth2/OIDC authentication