Authentication and Authorization: JWT, OAuth2, and RBAC Implementation

22 november 2024 · CodeMatic Team

Authentication and Authorization

Authentication and authorization are fundamental to application security. This guide covers JWT tokens, OAuth2, role-based access control, and best practices for implementing secure access controls.

JWT (JSON Web Tokens) Implementation

JWT Structure

JWTs consist of three parts: Header, Payload, and Signature, separated by dots.

Creating and Verifying JWTs

import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = '15m';
const REFRESH_TOKEN_EXPIRES_IN = '7d';

// Create access token
function createAccessToken(userId: string, roles: string[]): string {
  return jwt.sign(
    {
      sub: userId,
      roles,
      type: 'access',
    },
    JWT_SECRET,
    {
      expiresIn: JWT_EXPIRES_IN,
      issuer: 'your-app',
      audience: 'your-app-users',
    }
  );
}

// Create refresh token
function createRefreshToken(userId: string): string {
  return jwt.sign(
    {
      sub: userId,
      type: 'refresh',
    },
    JWT_SECRET,
    {
      expiresIn: REFRESH_TOKEN_EXPIRES_IN,
    }
  );
}

// Verify token
function verifyToken(token: string): any {
  try {
    return jwt.verify(token, JWT_SECRET, {
      issuer: 'your-app',
      audience: 'your-app-users',
    });
  } catch (error) {
    throw new Error('Invalid token');
  }
}

Secure Token Storage

// ✅ Store access token in memory (not localStorage)
// ✅ Store refresh token in httpOnly cookie

// Server-side cookie setting
res.cookie('refreshToken', refreshToken, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});

// Client-side: Store access token in memory or sessionStorage
// Never in localStorage (vulnerable to XSS)

OAuth2 Implementation

Authorization Code Flow

// Step 1: Redirect to authorization server
const authUrl = new URL('https://oauth-provider.com/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'read write');
authUrl.searchParams.set('state', generateState()); // CSRF protection
res.redirect(authUrl.toString());

// Step 2: Handle callback
app.get('/oauth/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // Verify state
  if (state !== req.session.oauthState) {
    return res.status(400).json({ error: 'Invalid state' });
  }
  
  // Exchange code for token
  const tokenResponse = await fetch('https://oauth-provider.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code as string,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
    }),
  });
  
  const tokens = await tokenResponse.json();
  // Store tokens securely
});

Role-Based Access Control (RBAC)

RBAC Implementation

// Define roles and permissions
enum Role {
  ADMIN = 'admin',
  MODERATOR = 'moderator',
  USER = 'user',
}

enum Permission {
  READ_POSTS = 'read:posts',
  WRITE_POSTS = 'write:posts',
  DELETE_POSTS = 'delete:posts',
  MANAGE_USERS = 'manage:users',
}

const rolePermissions: Record = {
  [Role.ADMIN]: [
    Permission.READ_POSTS,
    Permission.WRITE_POSTS,
    Permission.DELETE_POSTS,
    Permission.MANAGE_USERS,
  ],
  [Role.MODERATOR]: [
    Permission.READ_POSTS,
    Permission.WRITE_POSTS,
    Permission.DELETE_POSTS,
  ],
  [Role.USER]: [
    Permission.READ_POSTS,
    Permission.WRITE_POSTS,
  ],
};

// Middleware to check permissions
function requirePermission(permission: Permission) {
  return (req: Request, res: Response, next: NextFunction) => {
    const user = req.user;
    const userPermissions = rolePermissions[user.role];
    
    if (!userPermissions.includes(permission)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    
    next();
  };
}

// Usage
app.delete('/posts/:id', 
  authenticate,
  requirePermission(Permission.DELETE_POSTS),
  deletePost
);

Session Management

Secure Session Configuration

import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient({
  url: process.env.REDIS_URL,
});

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET!,
  name: 'sessionId',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    sameSite: 'strict',
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  },
}));

// Regenerate session ID after login
function regenerateSession(req: Request) {
  return new Promise((resolve, reject) => {
    req.session.regenerate((err) => {
      if (err) reject(err);
      else resolve(undefined);
    });
  });
}

Multi-Factor Authentication (MFA)

import speakeasy from 'speakeasy';
import QRCode from 'qrcode';

// Generate secret for user
function generateMFASecret(userId: string) {
  const secret = speakeasy.generateSecret({
    name: `Your App (${userId})`,
    issuer: 'Your App',
  });
  
  // Store secret.uri in database
  return secret;
}

// Verify TOTP code
function verifyMFA(userId: string, token: string): boolean {
  const userSecret = getUserMFASecret(userId);
  
  return speakeasy.totp.verify({
    secret: userSecret,
    encoding: 'base32',
    token,
    window: 2, // Allow 2 time steps tolerance
  });
}

// Generate QR code
async function generateQRCode(secretUri: string): Promise {
  return await QRCode.toDataURL(secretUri);
}

Password Reset Security

import crypto from 'crypto';

// Generate secure reset token
function generateResetToken(): string {
  return crypto.randomBytes(32).toString('hex');
}

// Store token with expiration
async function createPasswordResetToken(userId: string) {
  const token = generateResetToken();
  const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
  
  await db.passwordResetTokens.create({
    data: {
      userId,
      token: await hashToken(token),
      expiresAt,
    },
  });
  
  return token;
}

// Verify and use token
async function resetPassword(token: string, newPassword: string) {
  const hashedToken = await hashToken(token);
  const resetToken = await db.passwordResetTokens.findUnique({
    where: { token: hashedToken },
  });
  
  if (!resetToken || resetToken.expiresAt < new Date()) {
    throw new Error('Invalid or expired token');
  }
  
  // Update password
  await db.users.update({
    where: { id: resetToken.userId },
    data: { password: await hashPassword(newPassword) },
  });
  
  // Delete used token
  await db.passwordResetTokens.delete({
    where: { id: resetToken.id },
  });
}

API Key Authentication

// Generate API key
function generateAPIKey(): string {
  return crypto.randomBytes(32).toString('base64url');
}

// Store hashed API key
async function createAPIKey(userId: string, name: string) {
  const apiKey = generateAPIKey();
  const hashedKey = await hashAPIKey(apiKey);
  
  await db.apiKeys.create({
    data: {
      userId,
      name,
      keyHash: hashedKey,
      lastUsed: null,
    },
  });
  
  // Return plain key only once
  return apiKey;
}

// Authenticate with API key
async function authenticateAPIKey(key: string) {
  const hashedKey = await hashAPIKey(key);
  const apiKey = await db.apiKeys.findUnique({
    where: { keyHash: hashedKey },
    include: { user: true },
  });
  
  if (!apiKey) {
    throw new Error('Invalid API key');
  }
  
  // Update last used
  await db.apiKeys.update({
    where: { id: apiKey.id },
    data: { lastUsed: new Date() },
  });
  
  return apiKey.user;
}

Best Practices

  • Use short-lived access tokens (15 minutes)
  • Implement refresh token rotation
  • Store tokens securely (httpOnly cookies for refresh tokens)
  • Implement token blacklisting for logout
  • Use strong, unique secrets for JWT signing
  • Validate tokens on every request
  • Implement rate limiting on authentication endpoints
  • Log authentication events for security monitoring
  • Require MFA for sensitive operations
  • Implement account lockout after failed attempts

Conclusion

Secure authentication and authorization are critical for application security. Use JWTs for stateless authentication, implement OAuth2 for third-party integration, and use RBAC for fine-grained access control. Always follow security best practices: use short token lifetimes, implement refresh token rotation, store tokens securely, and monitor authentication events.