Skip to main content

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