🎉 RamAPI v1.0 is now available! Read the Getting Started Guide
Documentation
Authentication
Authentication Patterns

Authentication Patterns & Examples

Complete authentication patterns for common use cases. This guide covers login/register flows, protected routes, role-based access control, and real-world examples.

Table of Contents

  1. Complete Authentication System
  2. Login & Registration
  3. Protected Routes
  4. Role-Based Access Control
  5. Refresh Tokens
  6. Password Reset Flow
  7. Email Verification
  8. Multi-Factor Authentication
  9. Session Management
  10. Complete Examples

Complete Authentication System

Basic Setup

import {
  createApp,
  JWTService,
  passwordService,
  authenticate,
  validate,
  rateLimit,
} from 'ramapi';
import { z } from 'zod';
 
// JWT Service
const jwtService = new JWTService({
  secret: process.env.JWT_SECRET!,
  expiresIn: 86400, // 24 hours
  issuer: 'my-api',
});
 
const app = createApp();
 
// Global middleware
app.use(cors());
app.use(logger());

Login & Registration

Registration Endpoint

const registerSchema = {
  body: z.object({
    email: z.string().email(),
    password: z.string()
      .min(8, 'Password must be at least 8 characters')
      .regex(/[a-z]/, 'Must contain lowercase letter')
      .regex(/[A-Z]/, 'Must contain uppercase letter')
      .regex(/[0-9]/, 'Must contain number'),
    name: z.string().min(1),
  }),
};
 
app.post('/auth/register',
  rateLimit({ maxRequests: 5 }),
  validate(registerSchema),
  async (ctx) => {
    const { email, password, name } = ctx.body;
 
    // Check if user exists
    const existing = await db.users.findByEmail(email);
    if (existing) {
      throw new HTTPError(409, 'Email already registered');
    }
 
    // Hash password
    const passwordHash = await passwordService.hash(password);
 
    // Create user
    const user = await db.users.create({
      email,
      passwordHash,
      name,
      role: 'user',
      emailVerified: false,
      createdAt: new Date(),
    });
 
    // Generate token
    const token = jwtService.sign({
      sub: user.id,
      email: user.email,
      role: user.role,
    });
 
    // Send verification email (async)
    sendVerificationEmail(user.email, user.id).catch(console.error);
 
    ctx.json({
      token,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
      },
    }, 201);
  }
);

Login Endpoint

const loginSchema = {
  body: z.object({
    email: z.string().email(),
    password: z.string().min(1),
  }),
};
 
app.post('/auth/login',
  rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    maxRequests: 5,
    message: 'Too many login attempts',
  }),
  validate(loginSchema),
  async (ctx) => {
    const { email, password } = ctx.body;
 
    // Find user
    const user = await db.users.findByEmail(email);
 
    // Verify password (timing-safe)
    if (!user || !(await passwordService.verify(password, user.passwordHash))) {
      throw new HTTPError(401, 'Invalid credentials');
    }
 
    // Check if account is locked
    if (user.lockedUntil && user.lockedUntil > new Date()) {
      throw new HTTPError(423, 'Account is temporarily locked');
    }
 
    // Reset failed login attempts
    await db.users.update(user.id, { failedLoginAttempts: 0 });
 
    // Generate token
    const token = jwtService.sign({
      sub: user.id,
      email: user.email,
      role: user.role,
    });
 
    ctx.json({
      token,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
      },
    });
  }
);

Logout Endpoint

// Simple logout (client-side token removal)
app.post('/auth/logout',
  authenticate(jwtService),
  async (ctx) => {
    // Optional: Add token to blacklist
    await redis.set(`blacklist:${ctx.state.token}`, '1', 'EX', 86400);
 
    ctx.json({ message: 'Logged out successfully' });
  }
);

Protected Routes

Basic Protection

// Public route
app.get('/api/public', async (ctx) => {
  ctx.json({ message: 'Public data' });
});
 
// Protected route
app.get('/api/profile',
  authenticate(jwtService),
  async (ctx) => {
    const user = await db.users.findById(ctx.user.sub);
    ctx.json({ user });
  }
);

Optional Authentication

import { optionalAuthenticate } from 'ramapi';
 
app.get('/api/posts',
  optionalAuthenticate(jwtService),
  async (ctx) => {
    if (ctx.user) {
      // Authenticated - show all posts including drafts
      const posts = await db.posts.findByUserId(ctx.user.sub);
      ctx.json({ posts, authenticated: true });
    } else {
      // Not authenticated - show only published posts
      const posts = await db.posts.findPublished();
      ctx.json({ posts, authenticated: false });
    }
  }
);

Protected Route Groups

app.group('/api/private', (router) => {
  // Apply authentication to all routes in this group
  router.use(authenticate(jwtService));
 
  router.get('/profile', async (ctx) => {
    const user = await db.users.findById(ctx.user.sub);
    ctx.json({ user });
  });
 
  router.put('/profile', async (ctx) => {
    await db.users.update(ctx.user.sub, ctx.body);
    ctx.json({ message: 'Profile updated' });
  });
 
  router.get('/posts', async (ctx) => {
    const posts = await db.posts.findByUserId(ctx.user.sub);
    ctx.json({ posts });
  });
});

Role-Based Access Control

Role Middleware

function requireRole(...roles: string[]): Middleware {
  return async (ctx, next) => {
    if (!ctx.user) {
      throw new HTTPError(401, 'Authentication required');
    }
 
    if (!roles.includes(ctx.user.role)) {
      throw new HTTPError(403, 'Insufficient permissions');
    }
 
    await next();
  };
}

Admin-Only Routes

app.get('/api/admin/users',
  authenticate(jwtService),
  requireRole('admin'),
  async (ctx) => {
    const users = await db.users.findAll();
    ctx.json({ users });
  }
);
 
app.delete('/api/admin/users/:id',
  authenticate(jwtService),
  requireRole('admin'),
  async (ctx) => {
    await db.users.delete(ctx.params.id);
    ctx.json({ message: 'User deleted' });
  }
);

Multiple Roles

// Allow admin or moderator
app.post('/api/posts/:id/approve',
  authenticate(jwtService),
  requireRole('admin', 'moderator'),
  async (ctx) => {
    await db.posts.approve(ctx.params.id);
    ctx.json({ message: 'Post approved' });
  }
);

Permission-Based Access

function requirePermission(...permissions: string[]): Middleware {
  return async (ctx, next) => {
    if (!ctx.user) {
      throw new HTTPError(401, 'Authentication required');
    }
 
    // Get user permissions from database
    const userPermissions = await db.users.getPermissions(ctx.user.sub);
 
    // Check if user has all required permissions
    const hasPermissions = permissions.every(p => userPermissions.includes(p));
 
    if (!hasPermissions) {
      throw new HTTPError(403, 'Insufficient permissions');
    }
 
    await next();
  };
}
 
app.delete('/api/posts/:id',
  authenticate(jwtService),
  requirePermission('posts:delete'),
  async (ctx) => {
    await db.posts.delete(ctx.params.id);
    ctx.json({ message: 'Post deleted' });
  }
);

Resource Ownership

async function requireOwnership(resourceType: string): Promise<Middleware> {
  return async (ctx, next) => {
    if (!ctx.user) {
      throw new HTTPError(401, 'Authentication required');
    }
 
    const resourceId = ctx.params.id;
    const userId = ctx.user.sub;
 
    // Check ownership based on resource type
    let isOwner = false;
    if (resourceType === 'post') {
      const post = await db.posts.findById(resourceId);
      isOwner = post?.userId === userId;
    } else if (resourceType === 'comment') {
      const comment = await db.comments.findById(resourceId);
      isOwner = comment?.userId === userId;
    }
 
    // Admins can access any resource
    if (!isOwner && ctx.user.role !== 'admin') {
      throw new HTTPError(403, 'Access denied');
    }
 
    await next();
  };
}
 
app.put('/api/posts/:id',
  authenticate(jwtService),
  requireOwnership('post'),
  async (ctx) => {
    await db.posts.update(ctx.params.id, ctx.body);
    ctx.json({ message: 'Post updated' });
  }
);

Refresh Tokens

Generate Access & Refresh Tokens

interface TokenPair {
  accessToken: string;
  refreshToken: string;
}
 
async function generateTokens(userId: string): Promise<TokenPair> {
  // Short-lived access token
  const accessToken = jwtService.sign({
    sub: userId,
    type: 'access',
  });
 
  // Long-lived refresh token
  const refreshToken = crypto.randomBytes(32).toString('hex');
 
  // Store refresh token in database
  await db.refreshTokens.create({
    token: refreshToken,
    userId,
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
  });
 
  return { accessToken, refreshToken };
}

Login with Refresh Token

app.post('/auth/login', async (ctx) => {
  const { email, password } = ctx.body;
 
  const user = await db.users.findByEmail(email);
  if (!user || !(await passwordService.verify(password, user.passwordHash))) {
    throw new HTTPError(401, 'Invalid credentials');
  }
 
  const { accessToken, refreshToken } = await generateTokens(user.id);
 
  ctx.json({
    accessToken,
    refreshToken,
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
    },
  });
});

Refresh Token Endpoint

app.post('/auth/refresh',
  validate({
    body: z.object({
      refreshToken: z.string(),
    }),
  }),
  async (ctx) => {
    const { refreshToken } = ctx.body;
 
    // Verify refresh token
    const tokenRecord = await db.refreshTokens.findByToken(refreshToken);
 
    if (!tokenRecord) {
      throw new HTTPError(401, 'Invalid refresh token');
    }
 
    if (tokenRecord.expiresAt < new Date()) {
      await db.refreshTokens.delete(tokenRecord.id);
      throw new HTTPError(401, 'Refresh token expired');
    }
 
    // Generate new access token
    const accessToken = jwtService.sign({
      sub: tokenRecord.userId,
      type: 'access',
    });
 
    ctx.json({ accessToken });
  }
);

Revoke Refresh Token

app.post('/auth/revoke',
  authenticate(jwtService),
  validate({
    body: z.object({
      refreshToken: z.string(),
    }),
  }),
  async (ctx) => {
    const { refreshToken } = ctx.body;
 
    await db.refreshTokens.deleteByToken(refreshToken);
 
    ctx.json({ message: 'Token revoked' });
  }
);

Password Reset Flow

Request Password Reset

app.post('/auth/forgot-password',
  rateLimit({ maxRequests: 3 }),
  validate({
    body: z.object({
      email: z.string().email(),
    }),
  }),
  async (ctx) => {
    const { email } = ctx.body;
 
    const user = await db.users.findByEmail(email);
 
    // Always return success to prevent email enumeration
    if (!user) {
      ctx.json({ message: 'If the email exists, a reset link has been sent' });
      return;
    }
 
    // Generate reset token
    const resetToken = crypto.randomBytes(32).toString('hex');
    const resetTokenHash = crypto
      .createHash('sha256')
      .update(resetToken)
      .digest('hex');
 
    // Store reset token
    await db.users.update(user.id, {
      resetTokenHash,
      resetTokenExpiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
    });
 
    // Send email with reset link
    await sendPasswordResetEmail(user.email, resetToken);
 
    ctx.json({ message: 'If the email exists, a reset link has been sent' });
  }
);

Reset Password

app.post('/auth/reset-password',
  rateLimit({ maxRequests: 5 }),
  validate({
    body: z.object({
      token: z.string(),
      newPassword: z.string().min(8),
    }),
  }),
  async (ctx) => {
    const { token, newPassword } = ctx.body;
 
    // Hash the token to compare with stored hash
    const tokenHash = crypto
      .createHash('sha256')
      .update(token)
      .digest('hex');
 
    // Find user with this token
    const user = await db.users.findByResetToken(tokenHash);
 
    if (!user || !user.resetTokenExpiresAt || user.resetTokenExpiresAt < new Date()) {
      throw new HTTPError(400, 'Invalid or expired reset token');
    }
 
    // Hash new password
    const passwordHash = await passwordService.hash(newPassword);
 
    // Update password and clear reset token
    await db.users.update(user.id, {
      passwordHash,
      resetTokenHash: null,
      resetTokenExpiresAt: null,
    });
 
    ctx.json({ message: 'Password reset successful' });
  }
);

Email Verification

Send Verification Email

async function sendVerificationEmail(email: string, userId: string) {
  const token = crypto.randomBytes(32).toString('hex');
  const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
 
  await db.users.update(userId, {
    verificationTokenHash: tokenHash,
    verificationTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
  });
 
  const verificationUrl = `${process.env.APP_URL}/verify-email?token=${token}`;
 
  // Send email with verification link
  await emailService.send({
    to: email,
    subject: 'Verify your email',
    html: `Click here to verify: <a href="${verificationUrl}">${verificationUrl}</a>`,
  });
}

Verify Email Endpoint

app.post('/auth/verify-email',
  validate({
    body: z.object({
      token: z.string(),
    }),
  }),
  async (ctx) => {
    const { token } = ctx.body;
 
    const tokenHash = crypto
      .createHash('sha256')
      .update(token)
      .digest('hex');
 
    const user = await db.users.findByVerificationToken(tokenHash);
 
    if (!user || !user.verificationTokenExpiresAt || user.verificationTokenExpiresAt < new Date()) {
      throw new HTTPError(400, 'Invalid or expired verification token');
    }
 
    await db.users.update(user.id, {
      emailVerified: true,
      verificationTokenHash: null,
      verificationTokenExpiresAt: null,
    });
 
    ctx.json({ message: 'Email verified successfully' });
  }
);

Resend Verification Email

app.post('/auth/resend-verification',
  authenticate(jwtService),
  rateLimit({ maxRequests: 3 }),
  async (ctx) => {
    const user = await db.users.findById(ctx.user.sub);
 
    if (user.emailVerified) {
      throw new HTTPError(400, 'Email already verified');
    }
 
    await sendVerificationEmail(user.email, user.id);
 
    ctx.json({ message: 'Verification email sent' });
  }
);

Multi-Factor Authentication

Enable 2FA

import speakeasy from 'speakeasy';
import qrcode from 'qrcode';
 
app.post('/auth/2fa/enable',
  authenticate(jwtService),
  async (ctx) => {
    const user = await db.users.findById(ctx.user.sub);
 
    if (user.twoFactorEnabled) {
      throw new HTTPError(400, '2FA already enabled');
    }
 
    // Generate secret
    const secret = speakeasy.generateSecret({
      name: `MyApp (${user.email})`,
    });
 
    // Store secret temporarily
    await db.users.update(user.id, {
      twoFactorSecretTemp: secret.base32,
    });
 
    // Generate QR code
    const qrCodeUrl = await qrcode.toDataURL(secret.otpauth_url!);
 
    ctx.json({
      secret: secret.base32,
      qrCode: qrCodeUrl,
    });
  }
);

Verify and Confirm 2FA

app.post('/auth/2fa/verify',
  authenticate(jwtService),
  validate({
    body: z.object({
      token: z.string().length(6),
    }),
  }),
  async (ctx) => {
    const { token } = ctx.body;
    const user = await db.users.findById(ctx.user.sub);
 
    if (!user.twoFactorSecretTemp) {
      throw new HTTPError(400, '2FA setup not initiated');
    }
 
    // Verify token
    const verified = speakeasy.totp.verify({
      secret: user.twoFactorSecretTemp,
      encoding: 'base32',
      token,
    });
 
    if (!verified) {
      throw new HTTPError(400, 'Invalid 2FA token');
    }
 
    // Enable 2FA
    await db.users.update(user.id, {
      twoFactorEnabled: true,
      twoFactorSecret: user.twoFactorSecretTemp,
      twoFactorSecretTemp: null,
    });
 
    ctx.json({ message: '2FA enabled successfully' });
  }
);

Login with 2FA

app.post('/auth/login-2fa',
  validate({
    body: z.object({
      email: z.string().email(),
      password: z.string(),
      token: z.string().length(6),
    }),
  }),
  async (ctx) => {
    const { email, password, token } = ctx.body;
 
    const user = await db.users.findByEmail(email);
    if (!user || !(await passwordService.verify(password, user.passwordHash))) {
      throw new HTTPError(401, 'Invalid credentials');
    }
 
    if (user.twoFactorEnabled) {
      const verified = speakeasy.totp.verify({
        secret: user.twoFactorSecret,
        encoding: 'base32',
        token,
      });
 
      if (!verified) {
        throw new HTTPError(401, 'Invalid 2FA token');
      }
    }
 
    const authToken = jwtService.sign({
      sub: user.id,
      email: user.email,
    });
 
    ctx.json({ token: authToken });
  }
);

Session Management

Get Current User

app.get('/auth/me',
  authenticate(jwtService),
  async (ctx) => {
    const user = await db.users.findById(ctx.user.sub);
 
    if (!user) {
      throw new HTTPError(404, 'User not found');
    }
 
    ctx.json({
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
        emailVerified: user.emailVerified,
      },
    });
  }
);

Update Profile

app.put('/auth/profile',
  authenticate(jwtService),
  validate({
    body: z.object({
      name: z.string().min(1).optional(),
      bio: z.string().max(500).optional(),
    }),
  }),
  async (ctx) => {
    await db.users.update(ctx.user.sub, ctx.body);
 
    const user = await db.users.findById(ctx.user.sub);
    ctx.json({ user });
  }
);

Change Password

app.post('/auth/change-password',
  authenticate(jwtService),
  validate({
    body: z.object({
      currentPassword: z.string(),
      newPassword: z.string().min(8),
    }),
  }),
  async (ctx) => {
    const { currentPassword, newPassword } = ctx.body;
 
    const user = await db.users.findById(ctx.user.sub);
 
    // Verify current password
    if (!(await passwordService.verify(currentPassword, user.passwordHash))) {
      throw new HTTPError(401, 'Current password is incorrect');
    }
 
    // Hash new password
    const passwordHash = await passwordService.hash(newPassword);
 
    // Update password
    await db.users.update(user.id, { passwordHash });
 
    ctx.json({ message: 'Password changed successfully' });
  }
);

Complete Examples

Complete Auth API

import {
  createApp,
  JWTService,
  passwordService,
  authenticate,
  optionalAuthenticate,
  validate,
  rateLimit,
  cors,
  logger,
} from 'ramapi';
import { z } from 'zod';
 
const jwtService = new JWTService({
  secret: process.env.JWT_SECRET!,
  expiresIn: 86400,
});
 
const app = createApp();
 
app.use(logger());
app.use(cors());
 
// Registration
app.post('/auth/register',
  rateLimit({ maxRequests: 5 }),
  validate({
    body: z.object({
      email: z.string().email(),
      password: z.string().min(8),
      name: z.string().min(1),
    }),
  }),
  async (ctx) => {
    // Implementation from above
  }
);
 
// Login
app.post('/auth/login',
  rateLimit({ maxRequests: 5 }),
  validate({
    body: z.object({
      email: z.string().email(),
      password: z.string(),
    }),
  }),
  async (ctx) => {
    // Implementation from above
  }
);
 
// Get current user
app.get('/auth/me',
  authenticate(jwtService),
  async (ctx) => {
    // Implementation from above
  }
);
 
// Protected resource
app.get('/api/posts',
  optionalAuthenticate(jwtService),
  async (ctx) => {
    // Implementation from above
  }
);
 
// Admin route
app.get('/api/admin/users',
  authenticate(jwtService),
  requireRole('admin'),
  async (ctx) => {
    // Implementation from above
  }
);
 
app.listen(3000);

Next Steps


Need help? Check the Troubleshooting Guide or GitHub Issues (opens in a new tab).