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
- Use short expiration times for access tokens (15–60 minutes) and implement refresh tokens for longer sessions.
- Store secrets securely — never hardcode them. Use environment variables or a secrets manager.
- Consider RS256 (asymmetric) for microservices where multiple services need to verify tokens without sharing a secret.
- Include only necessary claims — keep the payload small to reduce request overhead.
- Implement token revocation by maintaining a blacklist or using a token version (
jti) stored in your database. - Use HTTPS everywhere — tokens sent over plain HTTP are vulnerable to interception.
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
- Always use HttpOnly cookies — this prevents JavaScript from reading the session ID, mitigating XSS attacks.
- Set the Secure flag in production to ensure cookies are only sent over HTTPS.
- Use SameSite=Lax or Strict to protect against CSRF attacks.
- Implement session rotation — regenerate the session ID after sensitive operations like login or password changes to prevent session fixation.
- Set reasonable expiration and implement idle timeout logic to automatically expire inactive sessions.
- Use a dedicated session store (Redis, Memcached) in multi-instance deployments.
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
- Always validate the state parameter — this is your primary defense against CSRF attacks in OAuth flows.
- Store client secrets securely — never expose them in client-side code or version control. Use environment variables.
- Use PKCE (Proof Key for Code Exchange) for public clients like mobile apps or SPAs that cannot safely store a client secret.
- Verify email ownership — not all providers guarantee email verification. Check the
email_verifiedclaim from Google or verify via your own flow. - Implement account linking — allow users to connect multiple OAuth providers to the same account by matching verified email addresses.
- Handle token refresh for OAuth tokens — if you need ongoing access to provider APIs, store and refresh their access tokens using refresh tokens.
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