ADR-003: Authentication Strategy
Status
Accepted
Context
We need to design a comprehensive authentication and authorization system for the GISE methodology platform that supports multiple user types, integrates with enterprise systems, and provides secure access to both web and API interfaces.
Requirements
User Types:
- Individual developers using personal accounts
- Enterprise users with SSO integration requirements
- API consumers needing programmatic access
- Admin users requiring elevated privileges
Integration Needs:
- Support for popular enterprise SSO providers (Azure AD, Google Workspace, Okta)
- Social login options for individual users (GitHub, Google)
- API authentication for automated tools and CI/CD integration
- Mobile app authentication (future requirement)
Security Requirements:
- Multi-factor authentication (MFA) support
- Session management with appropriate timeouts
- Audit trail for authentication events
- Protection against common attacks (brute force, session hijacking)
- Compliance with SOC 2 and GDPR requirements
Technical Constraints:
- Must integrate with existing Node.js/TypeScript backend
- Support for stateless operation (horizontal scaling)
- Performance targets: <100ms authentication latency
- High availability: 99.9% uptime requirement
Decision
We will implement a JWT-based authentication system with OAuth 2.0/OpenID Connect for external integrations and multi-factor authentication support.
Architecture Overview
Core Components
1. JWT Token Strategy
Token Structure:
interface AccessToken {
// Standard JWT claims
iss: string; // Issuer (gise-platform)
sub: string; // Subject (user ID)
aud: string[]; // Audience (services)
exp: number; // Expiration time
iat: number; // Issued at
jti: string; // JWT ID (for blacklisting)
// Custom claims
user: {
id: string;
email: string;
role: UserRole;
permissions: string[];
org?: string; // Organization ID
};
// Security claims
session_id: string; // Session identifier
device_id?: string; // Device fingerprint
mfa_verified: boolean; // MFA completion status
}
interface RefreshToken {
iss: string;
sub: string;
exp: number; // Longer expiration (7 days)
token_use: 'refresh';
session_id: string;
version: number; // For token revocation
}
Token Lifecycle:
class TokenService {
// Short-lived access tokens (15 minutes)
generateAccessToken(user: User, session: Session): AccessToken {
return jwt.sign(
{
sub: user.id,
user: {
id: user.id,
email: user.email,
role: user.role,
permissions: user.permissions,
org: user.organizationId
},
session_id: session.id,
mfa_verified: session.mfaVerified
},
this.accessTokenSecret,
{
expiresIn: '15m',
issuer: 'gise-platform',
audience: ['gise-api', 'gise-web'],
jwtid: generateUniqueId()
}
);
}
// Long-lived refresh tokens (7 days)
generateRefreshToken(user: User, session: Session): RefreshToken {
return jwt.sign(
{
sub: user.id,
session_id: session.id,
token_use: 'refresh',
version: session.tokenVersion
},
this.refreshTokenSecret,
{
expiresIn: '7d',
issuer: 'gise-platform'
}
);
}
}
2. OAuth 2.0/OpenID Connect Integration
Provider Configuration:
interface OAuthProvider {
id: string;
name: string;
clientId: string;
clientSecret: string;
authorizationUrl: string;
tokenUrl: string;
userInfoUrl: string;
scopes: string[];
pkce: boolean;
}
const oauthProviders: Record<string, OAuthProvider> = {
github: {
id: 'github',
name: 'GitHub',
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
authorizationUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
userInfoUrl: 'https://api.github.com/user',
scopes: ['user:email'],
pkce: true
},
google: {
id: 'google',
name: 'Google',
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorizationUrl: 'https://accounts.google.com/o/oauth2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
userInfoUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',
scopes: ['openid', 'email', 'profile'],
pkce: true
},
azure: {
id: 'azure',
name: 'Azure AD',
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
authorizationUrl: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token',
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
scopes: ['openid', 'email', 'profile'],
pkce: true
}
};
OAuth Flow Implementation:
class OAuthService {
async initiateOAuthFlow(providerId: string, redirectUri: string): Promise<AuthURL> {
const provider = oauthProviders[providerId];
const state = generateSecureState();
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Store PKCE parameters
await this.storePKCEParams(state, {
codeVerifier,
redirectUri,
provider: providerId
});
const authUrl = new URL(provider.authorizationUrl);
authUrl.searchParams.set('client_id', provider.clientId);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('redirect_uri', redirectUri);
authUrl.searchParams.set('scope', provider.scopes.join(' '));
authUrl.searchParams.set('state', state);
if (provider.pkce) {
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
}
return { url: authUrl.toString(), state };
}
async completeOAuthFlow(code: string, state: string): Promise<AuthResult> {
const pkceParams = await this.getPKCEParams(state);
const provider = oauthProviders[pkceParams.provider];
// Exchange authorization code for tokens
const tokenResponse = await this.exchangeCodeForTokens(
provider,
code,
pkceParams.codeVerifier,
pkceParams.redirectUri
);
// Get user info from provider
const userInfo = await this.getUserInfo(provider, tokenResponse.access_token);
// Create or update user account
const user = await this.createOrUpdateUser(userInfo, pkceParams.provider);
// Create session and generate our tokens
const session = await this.createSession(user.id, {
provider: pkceParams.provider,
ipAddress: this.getClientIP(),
userAgent: this.getUserAgent()
});
return {
accessToken: this.tokenService.generateAccessToken(user, session),
refreshToken: this.tokenService.generateRefreshToken(user, session),
user
};
}
}
3. Multi-Factor Authentication
MFA Methods:
enum MFAMethod {
TOTP = 'totp', // Time-based OTP (Google Authenticator)
SMS = 'sms', // SMS-based OTP
EMAIL = 'email', // Email-based OTP
BACKUP_CODES = 'backup', // Backup recovery codes
WEBAUTHN = 'webauthn' // WebAuth/FIDO2 (future)
}
interface MFASetup {
userId: string;
method: MFAMethod;
identifier: string; // Phone number, email, or device name
secret?: string; // For TOTP
backupCodes?: string[]; // Recovery codes
verified: boolean;
createdAt: Date;
}
class MFAService {
async setupTOTP(userId: string): Promise<TOTPSetup> {
const secret = generateTOTPSecret();
const qrCode = generateQRCode(secret, userId);
// Store unverified setup
await this.storeMFASetup({
userId,
method: MFAMethod.TOTP,
identifier: 'authenticator-app',
secret,
verified: false
});
return { secret, qrCode };
}
async verifyTOTP(userId: string, token: string): Promise<boolean> {
const setup = await this.getMFASetup(userId, MFAMethod.TOTP);
if (!setup || !setup.secret) {
return false;
}
const isValid = verifyTOTPToken(setup.secret, token);
if (isValid && !setup.verified) {
// Complete MFA setup
await this.updateMFASetup(setup.id, {
verified: true,
backupCodes: generateBackupCodes(10)
});
}
return isValid;
}
async requireMFA(userId: string): Promise<MFAChallenge> {
const methods = await this.getUserMFAMethods(userId);
if (methods.length === 0) {
throw new Error('MFA not configured');
}
const challengeId = generateUniqueId();
const challenge: MFAChallenge = {
id: challengeId,
userId,
methods: methods.map(m => m.method),
expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes
completed: false
};
await this.storeMFAChallenge(challenge);
// Send SMS/email if applicable
for (const method of methods) {
if (method.method === MFAMethod.SMS) {
await this.sendSMSCode(method.identifier, challengeId);
} else if (method.method === MFAMethod.EMAIL) {
await this.sendEmailCode(method.identifier, challengeId);
}
}
return challenge;
}
}
4. Session Management
Session Architecture:
interface Session {
id: string;
userId: string;
createdAt: Date;
lastAccessedAt: Date;
expiresAt: Date;
ipAddress: string;
userAgent: string;
provider?: string; // OAuth provider used
mfaVerified: boolean;
deviceFingerprint?: string;
tokenVersion: number; // For refresh token invalidation
active: boolean;
}
class SessionService {
private readonly maxSessions = 5; // Max concurrent sessions per user
private readonly sessionDuration = 7 * 24 * 60 * 60 * 1000; // 7 days
async createSession(userId: string, metadata: SessionMetadata): Promise<Session> {
// Cleanup old sessions
await this.cleanupExpiredSessions(userId);
// Limit concurrent sessions
const activeSessions = await this.getActiveSessions(userId);
if (activeSessions.length >= this.maxSessions) {
// Remove oldest session
await this.invalidateSession(activeSessions[0].id);
}
const session: Session = {
id: generateUniqueId(),
userId,
createdAt: new Date(),
lastAccessedAt: new Date(),
expiresAt: new Date(Date.now() + this.sessionDuration),
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
provider: metadata.provider,
mfaVerified: false,
tokenVersion: 1,
active: true
};
await this.storeSession(session);
return session;
}
async validateSession(sessionId: string): Promise<Session | null> {
const session = await this.getSession(sessionId);
if (!session || !session.active || session.expiresAt < new Date()) {
return null;
}
// Update last accessed time
await this.updateSessionAccess(sessionId);
return session;
}
async invalidateSession(sessionId: string): Promise<void> {
await this.updateSession(sessionId, { active: false });
// Add refresh tokens to blacklist
await this.blacklistSessionTokens(sessionId);
}
async invalidateAllUserSessions(userId: string, exceptSessionId?: string): Promise<void> {
const sessions = await this.getActiveSessions(userId);
for (const session of sessions) {
if (session.id !== exceptSessionId) {
await this.invalidateSession(session.id);
}
}
}
}
API Design
Authentication Endpoints
// Authentication routes
app.post('/auth/login', async (req, res) => {
const { email, password, mfaToken } = req.body;
try {
// Validate credentials
const user = await authService.validateCredentials(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check if MFA is required
const mfaMethods = await mfaService.getUserMFAMethods(user.id);
if (mfaMethods.length > 0 && !mfaToken) {
// Require MFA
const challenge = await mfaService.requireMFA(user.id);
return res.status(200).json({
requiresMFA: true,
challenge: {
id: challenge.id,
methods: challenge.methods
}
});
}
if (mfaMethods.length > 0 && mfaToken) {
// Verify MFA
const mfaValid = await mfaService.verifyMFAToken(user.id, mfaToken);
if (!mfaValid) {
return res.status(401).json({ error: 'Invalid MFA token' });
}
}
// Create session and tokens
const session = await sessionService.createSession(user.id, {
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
provider: 'local'
});
if (mfaMethods.length > 0) {
session.mfaVerified = true;
await sessionService.updateSession(session.id, { mfaVerified: true });
}
const tokens = tokenService.generateTokenPair(user, session);
// Set secure HTTP-only cookie for refresh token
res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({
accessToken: tokens.accessToken,
user: {
id: user.id,
email: user.email,
role: user.role,
organization: user.organization
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
});
// Token refresh endpoint
app.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken || req.body.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
const decoded = tokenService.verifyRefreshToken(refreshToken);
const session = await sessionService.validateSession(decoded.session_id);
if (!session) {
return res.status(401).json({ error: 'Invalid session' });
}
const user = await userService.getUserById(session.userId);
// Check token version (for revocation)
if (decoded.version !== session.tokenVersion) {
return res.status(401).json({ error: 'Token revoked' });
}
// Generate new access token
const accessToken = tokenService.generateAccessToken(user, session);
res.json({ accessToken });
} catch (error) {
console.error('Token refresh error:', error);
res.status(401).json({ error: 'Token refresh failed' });
}
});
// OAuth initiation
app.get('/auth/oauth/:provider', async (req, res) => {
const { provider } = req.params;
const redirectUri = req.query.redirect_uri as string || '/dashboard';
try {
const authUrl = await oauthService.initiateOAuthFlow(provider, redirectUri);
res.json({ authUrl: authUrl.url });
} catch (error) {
res.status(400).json({ error: 'Invalid OAuth provider' });
}
});
// OAuth callback
app.get('/auth/oauth/:provider/callback', async (req, res) => {
const { code, state } = req.query;
try {
const result = await oauthService.completeOAuthFlow(code as string, state as string);
// Set refresh token cookie
res.cookie('refreshToken', result.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
// Redirect to frontend with access token
const redirectUrl = new URL('/auth/callback', process.env.FRONTEND_URL);
redirectUrl.searchParams.set('token', result.accessToken);
res.redirect(redirectUrl.toString());
} catch (error) {
console.error('OAuth callback error:', error);
res.redirect('/login?error=oauth_failed');
}
});
Security Considerations
Token Security
Access Token Protection:
// Short expiration times
const ACCESS_TOKEN_EXPIRY = '15m';
// Include security claims
interface SecurityClaims {
session_id: string; // For session validation
device_id?: string; // Device fingerprinting
ip_hash?: string; // IP address hash (for detection)
iat: number; // Issued at (for freshness checks)
}
// Token blacklisting for compromised tokens
class TokenBlacklistService {
async blacklistToken(jti: string, exp: number): Promise<void> {
// Store blacklisted token ID with expiration
await redis.setex(`blacklist:${jti}`, exp - Date.now() / 1000, 'true');
}
async isTokenBlacklisted(jti: string): Promise<boolean> {
return await redis.exists(`blacklist:${jti}`) === 1;
}
}
Refresh Token Security:
// Refresh token rotation
class RefreshTokenRotation {
async refreshTokens(oldRefreshToken: string): Promise<TokenPair> {
// Validate old refresh token
const decoded = this.verifyRefreshToken(oldRefreshToken);
const session = await this.validateSession(decoded.session_id);
// Increment token version (invalidates all previous refresh tokens)
await this.incrementSessionTokenVersion(session.id);
// Generate new token pair
const user = await this.getUserById(session.userId);
return this.generateTokenPair(user, session);
}
}
Attack Prevention
Brute Force Protection:
class BruteForceProtection {
private readonly maxAttempts = 5;
private readonly lockoutDuration = 15 * 60 * 1000; // 15 minutes
async checkBruteForce(identifier: string): Promise<boolean> {
const key = `bf:${identifier}`;
const attempts = await redis.get(key);
return parseInt(attempts || '0') >= this.maxAttempts;
}
async recordFailedAttempt(identifier: string): Promise<void> {
const key = `bf:${identifier}`;
const current = await redis.incr(key);
if (current === 1) {
// Set expiration on first attempt
await redis.expire(key, this.lockoutDuration / 1000);
}
}
async resetAttempts(identifier: string): Promise<void> {
await redis.del(`bf:${identifier}`);
}
}
Session Fixation Prevention:
// Always generate new session ID after login
async login(credentials: Credentials): Promise<AuthResult> {
const user = await this.validateCredentials(credentials);
// Create completely new session (don't reuse any existing session)
const session = await this.sessionService.createSession(user.id, metadata);
return {
accessToken: this.generateAccessToken(user, session),
refreshToken: this.generateRefreshToken(user, session),
user
};
}
Implementation Plan
Phase 1: Core Authentication (Weeks 1-3)
- JWT token service implementation
- Local authentication (email/password)
- Session management
- Basic security middleware
- Password hashing and validation
- Refresh token mechanism
Phase 2: OAuth Integration (Weeks 4-5)
- OAuth 2.0/OpenID Connect framework
- GitHub OAuth integration
- Google OAuth integration
- User account linking
- Provider-specific user mapping
Phase 3: Multi-Factor Authentication (Weeks 6-7)
- TOTP implementation (Google Authenticator)
- SMS-based OTP (via Twilio)
- Email-based OTP
- Backup codes generation
- MFA enforcement policies
Phase 4: Security Hardening (Weeks 8-9)
- Brute force protection
- Token blacklisting
- Session security enhancements
- Audit logging
- Security headers and CSRF protection
Phase 5: Enterprise Integration (Weeks 10-12)
- Azure AD integration
- Okta integration
- SAML 2.0 support (if needed)
- Enterprise user provisioning
- Role mapping from external providers
Success Metrics
Security Metrics
- Zero successful brute force attacks
- <0.1% false positive rate for legitimate users
- 100% of sessions invalidated after password changes
- <5 minutes average MFA setup time
Performance Metrics
- <100ms authentication response time (95th percentile)
- <50ms token validation response time
-
99.9% authentication service availability
- <1% token refresh failures
User Experience Metrics
- <30 seconds average login time (including MFA)
-
90% user satisfaction with authentication flow
- <5% support tickets related to authentication issues
-
80% MFA adoption rate among active users
Future Considerations
Planned Enhancements
- WebAuthn/FIDO2: Hardware security keys and biometric authentication
- Risk-Based Authentication: Adaptive MFA based on login patterns
- Zero Trust: Continuous authentication and authorization
- Mobile SDKs: Native mobile authentication libraries
Monitoring and Alerting
- Failed authentication attempt monitoring
- Unusual login pattern detection
- Token usage analytics
- Session hijacking detection
Decision Date: December 19, 2024
Participants: Security Team, Backend Developers, Product Team
Next Review: March 19, 2025