← Back to DevBytes

Elysia Authentication: JWT, Sessions, and OAuth Integration

Introduction to Authentication in Elysia

Authentication is the backbone of any secure web application. Elysia, the high-performance Bun web framework, offers a remarkably elegant and flexible approach to implementing authentication through its plugin ecosystem and middleware architecture. Whether you're building a stateless REST API with JWT tokens, a traditional server-rendered app with session cookies, or integrating third-party OAuth providers, Elysia provides the tools to do so with minimal boilerplate and maximum type safety.

This tutorial walks you through three fundamental authentication strategies in Elysia: JWT (JSON Web Tokens), Session-based authentication, and OAuth integration. You'll learn not just how to implement each one, but also when to choose which approach and how to combine them effectively.

Prerequisites and Setup

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before diving in, ensure you have Bun installed. Create a new Elysia project:

bun create elysia my-auth-app
cd my-auth-app

Install the core dependencies we'll use throughout this tutorial:

bun add @elysiajs/jwt @elysiajs/cookie @elysiajs/bearer
bun add -d @types/bcryptjs
bun add bcryptjs

Your project structure will look something like this:

src/
├── index.ts          # Main entry point
├── auth/
│   ├── jwt.ts        # JWT authentication module
│   ├── session.ts    # Session authentication module
│   └── oauth.ts      # OAuth integration module
├── middleware/
│   └── auth-guard.ts # Shared authentication middleware
└── models/
    └── user.ts       # User data models

Part 1: JWT Authentication in Elysia

What Are JWTs and Why Use Them?

JSON Web Tokens are compact, URL-safe tokens that carry a payload of claims — typically a user ID, permissions, and expiration metadata. They are cryptographically signed, meaning your server can verify their authenticity without consulting a database on every request. This makes JWTs ideal for stateless, horizontally scaled architectures where you want to avoid a centralized session store.

In Elysia, the @elysiajs/jwt plugin handles signing, verification, and decoding with an incredibly concise API. It automatically looks for tokens in the Authorization: Bearer <token> header, cookies, or custom extractors you define.

Setting Up the JWT Plugin

First, register the JWT plugin on your Elysia instance. You'll need a secret (or a key pair for RS256) and optionally configure token extraction behavior:

import { Elysia } from 'elysia';
import { jwt } from '@elysiajs/jwt';
import { bearer } from '@elysiajs/bearer';

const app = new Elysia()
  .use(jwt({
    name: 'jwt',
    secret: process.env.JWT_SECRET || 'super-secret-key-change-in-production',
    exp: '7d',           // Default expiration: 7 days
    schema: 'HS256',     // HMAC SHA-256 (symmetric)
    // Optional: custom token extraction
    // extract: { from: 'cookie', name: 'token' }
  }))
  .use(bearer())  // Enables Bearer token parsing
  .listen(3000);

console.log('Server running on http://localhost:3000');

Generating Tokens on Login

Create a login endpoint that validates credentials and returns a signed JWT. The jwt.sign() method accepts your payload and returns a promise resolving to the token string:

import { Elysia, t } from 'elysia';
import { jwt } from '@elysiajs/jwt';
import bcrypt from 'bcryptjs';

// Mock user database — replace with your actual DB
const users = new Map();
users.set('alice@example.com', {
  id: 'usr_001',
  email: 'alice@example.com',
  passwordHash: bcrypt.hashSync('password123', 10),
  role: 'admin'
});

const app = new Elysia()
  .use(jwt({
    name: 'jwt',
    secret: process.env.JWT_SECRET || 'dev-secret-key',
    exp: '1h'
  }))
  .post('/api/auth/login', async ({ jwt, body, set }) => {
    const { email, password } = body;

    const user = users.get(email);
    if (!user) {
      set.status = 401;
      return { error: 'Invalid credentials' };
    }

    const passwordValid = bcrypt.compareSync(password, user.passwordHash);
    if (!passwordValid) {
      set.status = 401;
      return { error: 'Invalid credentials' };
    }

    // Sign a token with user claims
    const token = await jwt.sign({
      sub: user.id,
      email: user.email,
      role: user.role,
      iat: Math.floor(Date.now() / 1000)
    });

    return {
      token,
      expiresIn: '1h',
      user: { id: user.id, email: user.email, role: user.role }
    };
  }, {
    body: t.Object({
      email: t.String({ format: 'email' }),
      password: t.String({ minLength: 6 })
    })
  })
  .listen(3000);

Protecting Routes with JWT Verification

Elysia offers multiple ways to protect routes. The most idiomatic approach uses the derive hook combined with the JWT plugin's verification. You can also create a dedicated guard middleware:

// Method 1: Using derive in a group
app.group('/api/protected', (app) => 
  app
    .derive(async ({ jwt, bearer, set }) => {
      // bearer plugin extracts the token from Authorization header
      if (!bearer) {
        set.status = 401;
        return { error: 'No token provided' };
      }
      
      const payload = await jwt.verify(bearer);
      if (!payload) {
        set.status = 401;
        return { error: 'Invalid or expired token' };
      }
      
      // Attach user info to the request context
      return { user: payload };
    })
    .get('/profile', ({ user }) => {
      return { 
        message: 'Protected data',
        user: { id: user.sub, email: user.email, role: user.role }
      };
    })
    .get('/admin-dashboard', ({ user, set }) => {
      if (user.role !== 'admin') {
        set.status = 403;
        return { error: 'Insufficient permissions' };
      }
      return { dashboard: 'Admin-only data here' };
    })
);

Creating a Reusable Auth Guard

For larger applications, extract the verification logic into a reusable guard. Elysia's plugin system lets you compose these elegantly:

// src/middleware/auth-guard.ts
import { Elysia } from 'elysia';

export const authGuard = new Elysia({ name: 'auth-guard' })
  .derive(async ({ jwt, bearer, set }) => {
    if (!bearer) {
      set.status = 401;
      throw new Error('Authorization header required');
    }
    
    const payload = await jwt.verify(bearer);
    if (!payload) {
      set.status = 401;
      throw new Error('Token invalid or expired');
    }
    
    return { 
      auth: {
        userId: payload.sub,
        email: payload.email,
        role: payload.role
      }
    };
  });

// Usage in main app
app.use(authGuard)
   .get('/api/secure/data', ({ auth }) => {
     return { 
       message: `Hello ${auth.email}!`,
       userId: auth.userId 
     };
   });

Token Refresh Pattern

A common pattern is issuing short-lived access tokens and longer-lived refresh tokens. Here's how to implement token refresh in Elysia:

// Refresh token endpoint
app.post('/api/auth/refresh', async ({ jwt, bearer, set }) => {
  if (!bearer) {
    set.status = 401;
    return { error: 'Refresh token required' };
  }
  
  // Verify the current token (even if expired for a grace period)
  // For strict refresh, you'd use a separate refresh token stored in DB
  const payload = await jwt.verify(bearer);
  if (!payload) {
    set.status = 401;
    return { error: 'Refresh token invalid' };
  }
  
  // Issue a new access token
  const newToken = await jwt.sign({
    sub: payload.sub,
    email: payload.email,
    role: payload.role,
    iat: Math.floor(Date.now() / 1000)
  });
  
  return { token: newToken, expiresIn: '1h' };
});

JWT Best Practices Summary

Part 2: Session-Based Authentication

When to Choose Sessions Over JWTs

Session-based authentication stores a session identifier in a secure, HTTP-only cookie on the client, while the server maintains the actual session data (user info, permissions) in memory, a database, or a dedicated session store like Redis. This approach shines when you need immediate revocation (deleting the session server-side instantly logs the user out), when you're building traditional server-rendered applications, or when you want to avoid the complexity of token refresh logic.

Elysia integrates seamlessly with cookie-based sessions through the @elysiajs/cookie plugin and custom session stores.

Cookie-Based Session Setup

import { Elysia, t } from 'elysia';
import { cookie } from '@elysiajs/cookie';
import { randomUUIDv7 } from 'bun';

// In-memory session store (use Redis for production)
const sessionStore = new Map();

// Session cleanup — run periodically in production
function cleanupExpiredSessions() {
  const now = Date.now();
  for (const [sessionId, session] of sessionStore) {
    if (session.expiresAt < now) {
      sessionStore.delete(sessionId);
    }
  }
}

const app = new Elysia()
  .use(cookie({
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 3600  // 1 hour in seconds
  }))
  .post('/api/auth/session-login', async ({ body, set, cookie }) => {
    const { email, password } = body;
    
    // Validate credentials (same as before)
    const user = users.get(email);
    if (!user || !bcrypt.compareSync(password, user.passwordHash)) {
      set.status = 401;
      return { error: 'Invalid credentials' };
    }
    
    // Create a session
    const sessionId = randomUUIDv7();
    const expiresAt = Date.now() + 3600 * 1000; // 1 hour
    
    sessionStore.set(sessionId, {
      userId: user.id,
      email: user.email,
      role: user.role,
      createdAt: Date.now(),
      expiresAt
    });
    
    // Set the session cookie
    cookie.session_id.value = sessionId;
    cookie.session_id.maxAge = 3600;
    cookie.session_id.httpOnly = true;
    cookie.session_id.secure = process.env.NODE_ENV === 'production';
    
    return { 
      message: 'Logged in successfully',
      user: { id: user.id, email: user.email }
    };
  }, {
    body: t.Object({
      email: t.String({ format: 'email' }),
      password: t.String({ minLength: 6 })
    })
  });

Session Verification Middleware

// Session auth guard
const sessionGuard = new Elysia({ name: 'session-guard' })
  .derive(({ cookie, set }) => {
    const sessionId = cookie.session_id?.value;
    
    if (!sessionId) {
      set.status = 401;
      throw new Error('No session cookie found');
    }
    
    const session = sessionStore.get(sessionId);
    if (!session) {
      set.status = 401;
      throw new Error('Session expired or invalid');
    }
    
    if (session.expiresAt < Date.now()) {
      sessionStore.delete(sessionId);
      set.status = 401;
      throw new Error('Session expired');
    }
    
    // Optionally extend session on activity
    session.expiresAt = Date.now() + 3600 * 1000;
    sessionStore.set(sessionId, session);
    
    return { 
      session: {
        userId: session.userId,
        email: session.email,
        role: session.role
      }
    };
  });

app.use(sessionGuard)
   .get('/api/session/profile', ({ session }) => {
     return { 
       message: 'Session-authenticated endpoint',
       user: session 
     };
   })
   .post('/api/auth/logout', ({ cookie, set, session }) => {
     const sessionId = cookie.session_id?.value;
     if (sessionId) {
       sessionStore.delete(sessionId);
     }
     // Clear the cookie
     cookie.session_id.value = '';
     cookie.session_id.maxAge = 0;
     return { message: 'Logged out successfully' };
   });

Using Redis as a Session Store

For production applications running multiple server instances, an in-memory Map won't work — you need a shared session store. Here's how to integrate Redis:

import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

// Redis-based session functions
async function createSession(userData: { userId: string; email: string; role: string }) {
  const sessionId = randomUUIDv7();
  const sessionKey = `session:${sessionId}`;
  
  await redis.hSet(sessionKey, {
    userId: userData.userId,
    email: userData.email,
    role: userData.role,
    createdAt: Date.now().toString()
  });
  await redis.expire(sessionKey, 3600); // 1 hour TTL
  
  return sessionId;
}

async function getSession(sessionId: string) {
  const sessionKey = `session:${sessionId}`;
  const exists = await redis.exists(sessionKey);
  if (!exists) return null;
  
  const session = await redis.hGetAll(sessionKey);
  return {
    userId: session.userId,
    email: session.email,
    role: session.role,
    createdAt: parseInt(session.createdAt)
  };
}

async function deleteSession(sessionId: string) {
  await redis.del(`session:${sessionId}`);
}

Session Best Practices

Part 3: OAuth Integration in Elysia

Understanding OAuth 2.0 Flow

OAuth 2.0 allows users to authenticate via third-party providers like Google, GitHub, or Discord without sharing their credentials with your application. The flow involves redirecting the user to the provider's authorization page, receiving an authorization code, exchanging that code for an access token, and then using that token to fetch the user's profile information.

Elysia's lightweight nature makes it perfect for implementing OAuth flows without heavy framework abstractions. You'll handle the redirect dance manually, giving you full control over every step.

Google OAuth Integration Example

First, set up your Google Cloud Console credentials (OAuth 2.0 Client ID) with authorized redirect URIs pointing to your Elysia server. Then implement the flow:

// src/auth/oauth.ts
import { Elysia, t } from 'elysia';
import { jwt } from '@elysiajs/jwt';
import { cookie } from '@elysiajs/cookie';

const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID!;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET!;
const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/api/auth/google/callback';

// Store state parameter to prevent CSRF
const stateStore = new Map();

export const oauthRoutes = new Elysia({ prefix: '/api/auth' })
  .use(jwt({ name: 'jwt', secret: process.env.JWT_SECRET! }))
  .use(cookie())

  // Step 1: Redirect to Google's authorization page
  .get('/google', ({ set, cookie }) => {
    const state = randomUUIDv7();
    stateStore.set(state, { createdAt: Date.now() });
    
    // Store state in a short-lived cookie for verification on callback
    cookie.oauth_state.value = state;
    cookie.oauth_state.maxAge = 600; // 10 minutes
    cookie.oauth_state.httpOnly = true;
    cookie.oauth_state.secure = process.env.NODE_ENV === 'production';
    
    const params = new URLSearchParams({
      client_id: GOOGLE_CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      response_type: 'code',
      scope: 'openid email profile',
      state,
      access_type: 'offline',
      prompt: 'consent'
    });
    
    set.redirect = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
    set.status = 302;
  })

  // Step 2: Handle the callback from Google
  .get('/google/callback', async ({ query, set, jwt, cookie }) => {
    const { code, state, error } = query;
    
    if (error) {
      set.status = 400;
      return { error: `OAuth error: ${error}` };
    }
    
    // Verify state parameter to prevent CSRF
    const storedState = cookie.oauth_state?.value;
    if (!state || !storedState || state !== storedState) {
      set.status = 403;
      return { error: 'State mismatch — possible CSRF attack' };
    }
    
    // Clear the state cookie
    cookie.oauth_state.value = '';
    cookie.oauth_state.maxAge = 0;
    stateStore.delete(state);
    
    // Step 3: Exchange authorization code for access token
    const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        code,
        client_id: GOOGLE_CLIENT_ID,
        client_secret: GOOGLE_CLIENT_SECRET,
        redirect_uri: REDIRECT_URI,
        grant_type: 'authorization_code'
      })
    });
    
    const tokenData = await tokenResponse.json() as {
      access_token: string;
      refresh_token?: string;
      expires_in: number;
      id_token: string;
    };
    
    if (!tokenData.access_token) {
      set.status = 400;
      return { error: 'Failed to exchange code for token' };
    }
    
    // Step 4: Fetch user profile from Google
    const userResponse = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
      headers: { Authorization: `Bearer ${tokenData.access_token}` }
    });
    
    const googleUser = await userResponse.json() as {
      sub: string;
      email: string;
      email_verified: boolean;
      name: string;
      picture: string;
    };
    
    // Step 5: Find or create user in your database
    // This is where you'd upsert the user based on googleUser.sub
    const user = {
      id: `usr_${googleUser.sub}`,
      email: googleUser.email,
      name: googleUser.name,
      picture: googleUser.picture,
      provider: 'google' as const
    };
    
    // Save user to your database here...
    
    // Step 6: Issue your own JWT or session
    const appToken = await jwt.sign({
      sub: user.id,
      email: user.email,
      name: user.name,
      provider: 'google'
    });
    
    // Redirect to frontend with token (or set cookie)
    // Option A: Redirect with token in URL fragment
    const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
    set.redirect = `${frontendUrl}/auth/callback?token=${appToken}`;
    set.status = 302;
    
    return { token: appToken, user };
  });

GitHub OAuth Integration

The pattern is nearly identical for GitHub. Here's a compact implementation:

// GitHub OAuth endpoints
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID!;
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET!;
const GITHUB_REDIRECT = 'http://localhost:3000/api/auth/github/callback';

app.get('/api/auth/github', ({ set, cookie }) => {
  const state = randomUUIDv7();
  cookie.gh_state.value = state;
  cookie.gh_state.maxAge = 600;
  
  const params = new URLSearchParams({
    client_id: GITHUB_CLIENT_ID,
    redirect_uri: GITHUB_REDIRECT,
    scope: 'user:email read:user',
    state
  });
  
  set.redirect = `https://github.com/login/oauth/authorize?${params.toString()}`;
  set.status = 302;
});

app.get('/api/auth/github/callback', async ({ query, set, jwt, cookie }) => {
  const { code, state } = query;
  const storedState = cookie.gh_state?.value;
  
  if (!state || state !== storedState) {
    set.status = 403;
    return { error: 'State mismatch' };
  }
  
  // Exchange code for token
  const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
    method: 'POST',
    headers: { 
      'Content-Type': 'application/json',
      'Accept': 'application/json' 
    },
    body: JSON.stringify({
      client_id: GITHUB_CLIENT_ID,
      client_secret: GITHUB_CLIENT_SECRET,
      code,
      redirect_uri: GITHUB_REDIRECT
    })
  });
  
  const { access_token } = await tokenRes.json() as { access_token: string };
  
  // Fetch GitHub user profile
  const userRes = await fetch('https://api.github.com/user', {
    headers: { Authorization: `Bearer ${access_token}` }
  });
  const ghUser = await userRes.json() as {
    id: number;
    login: string;
    email: string;
    avatar_url: string;
    name: string;
  };
  
  // Fetch emails if not public
  let email = ghUser.email;
  if (!email) {
    const emailsRes = await fetch('https://api.github.com/user/emails', {
      headers: { Authorization: `Bearer ${access_token}` }
    });
    const emails = await emailsRes.json() as Array<{ email: string; primary: boolean }>;
    email = emails.find(e => e.primary)?.email || emails[0]?.email;
  }
  
  const user = {
    id: `usr_gh_${ghUser.id}`,
    email,
    name: ghUser.name || ghUser.login,
    avatar: ghUser.avatar_url,
    provider: 'github' as const
  };
  
  // Issue app token
  const appToken = await jwt.sign({
    sub: user.id,
    email: user.email,
    name: user.name,
    provider: 'github'
  });
  
  const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
  set.redirect = `${frontendUrl}/auth/callback?token=${appToken}`;
  set.status = 302;
});

Generic OAuth Helper with Multiple Providers

For applications supporting multiple OAuth providers, abstract the common pattern into a reusable helper:

// src/auth/oauth-providers.ts
interface OAuthProviderConfig {
  name: string;
  authorizationUrl: string;
  tokenUrl: string;
  userInfoUrl: string;
  clientId: string;
  clientSecret: string;
  redirectUri: string;
  scopes: string[];
  mapUser: (providerUser: Record) => {
    id: string;
    email: string;
    name: string;
    avatar?: string;
  };
}

const providers: Record = {
  google: {
    name: 'google',
    authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
    tokenUrl: 'https://oauth2.googleapis.com/token',
    userInfoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo',
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    redirectUri: 'http://localhost:3000/api/auth/google/callback',
    scopes: ['openid', 'email', 'profile'],
    mapUser: (u: any) => ({
      id: `usr_go_${u.sub}`,
      email: u.email,
      name: u.name,
      avatar: u.picture
    })
  },
  github: {
    name: 'github',
    authorizationUrl: 'https://github.com/login/oauth/authorize',
    tokenUrl: 'https://github.com/login/oauth/access_token',
    userInfoUrl: 'https://api.github.com/user',
    clientId: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    redirectUri: 'http://localhost:3000/api/auth/github/callback',
    scopes: ['user:email', 'read:user'],
    mapUser: (u: any) => ({
      id: `usr_gh_${u.id}`,
      email: u.email,
      name: u.name || u.login,
      avatar: u.avatar_url
    })
  }
};

async function exchangeCodeForToken(
  provider: OAuthProviderConfig, 
  code: string
): Promise {
  const isGitHub = provider.name === 'github';
  
  const response = await fetch(provider.tokenUrl, {
    method: 'POST',
    headers: isGitHub 
      ? { 'Content-Type': 'application/json', 'Accept': 'application/json' }
      : { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: isGitHub
      ? JSON.stringify({
          client_id: provider.clientId,
          client_secret: provider.clientSecret,
          code,
          redirect_uri: provider.redirectUri
        })
      : new URLSearchParams({
          client_id: provider.clientId,
          client_secret: provider.clientSecret,
          code,
          redirect_uri: provider.redirectUri,
          grant_type: 'authorization_code'
        })
  });
  
  const data = await response.json() as { access_token: string };
  return data.access_token;
}

async function fetchProviderUser(
  provider: OAuthProviderConfig,
  accessToken: string
): Promise> {
  const response = await fetch(provider.userInfoUrl, {
    headers: { Authorization: `Bearer ${accessToken}` }
  });
  return response.json() as Record;
}

OAuth Best Practices

Combining Authentication Strategies

Real-world applications often need to support multiple authentication methods simultaneously. Elysia's plugin architecture makes this remarkably clean. You can combine JWT, sessions, and OAuth in a single application, letting users choose their preferred login method:

// Complete combined auth setup
import { Elysia, t } from 'elysia';
import { jwt } from '@elysiajs/jwt';
import { bearer } from '@elysiajs/bearer';
import { cookie } from '@elysiajs/cookie';

const app = new Elysia()
  .use(jwt({ 
    name: 'jwt', 
    secret: process.env.JWT_SECRET!,
    exp: '1h'
  }))
  .use(bearer())
  .use(cookie({
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax'
  }))

  // Flexible auth middleware — tries JWT first, falls back to session
  .derive(async ({ jwt, bearer, cookie, set }) => {
    let user: { sub: string; email: string; role: string } | null = null;
    
    // Try JWT bearer token first
    if (bearer) {
      const payload = await jwt.verify(bearer);
      if (payload) {
        user = { sub: payload.sub as string, email: payload.email as string, role: payload.role as string };
      }
    }
    
    // Fall back to session cookie
    if (!user) {
      const sessionId = cookie.session_id?.value;
      if (sessionId) {
        const session = sessionStore.get(sessionId);
        if (session && session.expiresAt > Date.now()) {
          user = { sub: session.userId, email: session.email, role: session.role };
        }
      }
    }
    
    if (!user) {
      // Don't fail here — let individual routes decide
      return { auth: null };
    }
    
    return { auth: user };
  })

  // Public routes
  .get('/api/public/status', () => ({ status: 'ok', timestamp: Date.now() }))

  // Semi-protected: works with or without auth
  .get('/api/me', ({ auth }) => {
    if (!auth) {
      return { authenticated: false, message: 'Guest user' };
    }
    return { authenticated: true, user: auth };
  })

  // Strictly protected routes
  .guard(app => 
    app.derive(({ auth, set }) => {
      if (!auth) {
        set.status = 401;
        throw new Error('Authentication required');
      }
      return { auth: auth! };  // Narrow type to non-null
    })
    .get('/api/dashboard', ({ auth }) => {
      return { 
        message: `Welcome back, ${auth.email}!`,
        role: auth.role 
      };
    })
  )

  .listen(3000);

Security Considerations and Hardening

CSRF Protection

For session-based auth, implement CSRF tokens for state-changing operations. Elysia doesn't include CSRF protection out of the box, but you can add it with a custom middleware:

// Simple double-submit cookie CSRF pattern
app.derive(({ request, cookie, set }) => {
  // Only for mutation methods
  if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method)) {
    const headerToken = request.headers.get('x-csrf-token');
    const cookieToken = cookie.csrf_token?.value;
    
    if (!headerToken || !cookieToken || headerToken !== cookieToken) {
      set.status = 403;
      throw new Error('CSRF validation failed');
    }
  }
});

// Generate CSRF token on login
app.post('/api/auth/login', ({ cookie }) => {
  const csrfToken = randomUUIDv7();
  cookie.csrf_token.value = csrfToken;
  cookie.csrf_token.httpOnly = false;  // Must be readable by JS
  cookie.csrf_token.sameSite = 'strict';
  // ... rest of login logic
});

Rate Limiting Authentication Endpoints

Protect login and OAuth callback endpoints from brute force attacks:

// Simple in-memory rate limiter
const rateLimits = new Map();

function checkRateLimit(key: string, maxAttempts: number, windowMs: number): boolean {
  const now = Date.now();
  const entry = rateLimits.get(key);
  
  if (!entry || entry.resetAt < now) {
    rateLimits.set(key, { count: 1, resetAt: now + windowMs });
    return true;
  }
  
  if (entry.count >= maxAttempts) {
    return false

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles