SessionKit is a session access and route protection library. It does NOT handle:
- Session creation/authentication (but provides helpers to register sessions)
- Session storage (cookies, Redis, database)
- Session expiration checking
- CSRF protection
SessionKit DOES provide:
- β
setSession()- Register session after authentication - β
clearSession()- Clear session during logout - β
updateSession()- Update session data - β Session access helpers throughout your app
- β Route protection based on roles/permissions
These are your responsibility as the developer. This guide explains what you must implement.
β
Session structure validation - Prevents crashes from malformed data
β
DoS protection - Limits array sizes and pattern complexity
β
Safe session access - AsyncLocalStorage-based session context
β
Route protection - Declarative guards based on roles/permissions
SessionKit provides setSession() to register sessions, but you must store them securely.
// INSECURE - Anyone can modify this!
import { setSession } from 'astro-sessionkit/server';
export const POST: APIRoute = async (context) => {
const user = await authenticateUser(credentials);
// Register with SessionKit
setSession(context, { userId: user.id, role: user.role });
// DANGEROUS - Plain cookie, no encryption!
context.cookies.set('session', JSON.stringify({
userId: user.id,
role: 'admin'
}));
};// Use a library like iron-session, lucia-auth, or @auth/astro
import { setSession } from 'astro-sessionkit/server';
import { encrypt } from 'iron-session';
export const POST: APIRoute = async (context) => {
const user = await authenticateUser(credentials);
// 1. Register with SessionKit
setSession(context, {
userId: user.id,
email: user.email,
role: user.role,
permissions: user.permissions
});
// 2. Store encrypted session ID
const sessionId = crypto.randomUUID();
await db.createSession({
id: sessionId,
userId: user.id,
expiresAt: Date.now() + 3600000 // 1 hour
});
// 3. Set secure cookie
const encryptedId = await encrypt(sessionId, {
password: process.env.SESSION_SECRET!,
ttl: 3600
});
context.cookies.set('session_id', encryptedId, {
httpOnly: true, // Prevent JavaScript access
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 3600, // 1 hour
path: '/'
});
};- lucia-auth - Modern, type-safe auth
- @auth/astro - Popular auth solution
- iron-session - Encrypted cookies
- better-auth - Full-featured auth
SessionKit does not check expiration. You must implement this:
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { verifySession } from './auth'; // Your auth logic
export const onRequest = defineMiddleware(async (context, next) => {
const sessionCookie = context.cookies.get('session')?.value;
if (sessionCookie) {
try {
const session = await verifySession(sessionCookie);
// Check expiration
if (session.expiresAt && session.expiresAt < Date.now()) {
context.cookies.delete('session');
return next();
}
// Set for SessionKit to read
context.session.set('__session__', {
userId: session.userId,
email: session.email,
role: session.role,
permissions: session.permissions
});
} catch (error) {
// Invalid session - delete cookie
context.cookies.delete('session');
}
}
return next();
});For state-changing operations (POST, PUT, DELETE), implement CSRF tokens:
// Generate CSRF token (in your auth middleware)
import { randomBytes } from 'crypto';
const csrfToken = randomBytes(32).toString('hex');
context.cookies.set('csrf_token', csrfToken, {
httpOnly: false, // Must be readable by JavaScript
sameSite: 'strict'
});
// Store in session for validation
context.locals.csrfToken = csrfToken;// Validate CSRF token (in API routes)
export const POST: APIRoute = async ({ request, cookies, locals }) => {
const token = request.headers.get('x-csrf-token');
const expected = locals.csrfToken;
if (!token || token !== expected) {
return new Response('CSRF validation failed', { status: 403 });
}
// Process request...
};<!-- Include in forms -->
<form method="POST">
<input type="hidden" name="csrf_token" value={locals.csrfToken} />
<!-- ... -->
</form>Regenerate session IDs after authentication:
// After successful login
export const POST: APIRoute = async ({ request, cookies }) => {
const { email, password } = await request.json();
// Verify credentials
const user = await verifyCredentials(email, password);
if (user) {
// Delete old session if it exists
const oldSession = cookies.get('session')?.value;
if (oldSession) {
await deleteSession(oldSession); // Clean up server-side
}
// Generate NEW session ID
const newSessionId = crypto.randomUUID();
const session = await createSession(newSessionId, user.id);
// Set new cookie
cookies.set('session', await encryptSession(session), {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
}
};Protect authentication endpoints from brute force:
import { RateLimiter } from 'rate-limiter-flexible';
import { Redis } from 'ioredis';
const redis = new Redis();
const limiter = new RateLimiterRedis({
storeClient: redis,
points: 5, // 5 attempts
duration: 900, // per 15 minutes
blockDuration: 900 // block for 15 minutes after
});
export const POST: APIRoute = async ({ request, clientAddress }) => {
try {
// Check rate limit
await limiter.consume(clientAddress);
} catch {
return new Response('Too many login attempts', {
status: 429,
headers: { 'Retry-After': '900' }
});
}
// Process login...
};Never trust session data in HTML contexts:
---
import { getSession } from 'astro-sessionkit/server';
const session = getSession();
---
<!-- Astro automatically escapes variables -->
<p>Welcome, {session?.email}</p> <!-- β
Safe -->
<!-- Be careful with set:html -->
<div set:html={session?.bio}></div> <!-- β Dangerous if bio contains HTML -->
<!-- Sanitize user-generated HTML -->
<div set:html={sanitizeHtml(session?.bio)}></div> <!-- β
Safe -->Always use the server-side helpers to validate user roles and permissions. Do not rely on client-side state or hidden inputs.
import { hasRole, hasPermission, hasRolePermission } from 'astro-sessionkit/server';
// β
SAFE: Validation happens on the server using the trusted session
if (hasRole('admin')) {
// Perform admin action
}
// β
SAFE: Complex permission checks
if (hasRolePermission('editor', 'publish:posts')) {
// Perform action
}Before deploying to production:
- Sessions are encrypted/signed (using iron-session, lucia, etc.)
- Cookies have security flags (HttpOnly, Secure, SameSite)
- Session expiration is enforced (both client and server)
- CSRF protection on all state-changing operations
- Session IDs regenerated after login/logout
- Rate limiting on authentication endpoints
- HTTPS enforced in production
- Security headers configured:
headers: { 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block', 'Content-Security-Policy': "default-src 'self'" }
- Password hashing with bcrypt/argon2 (min 10 rounds)
- Audit logging for authentication events
- Regular security updates for dependencies
// NEVER store sensitive data in plain cookies
cookies.set('user', JSON.stringify({ role: 'admin' }));// NEVER trust data from forms/headers without validation
const role = request.headers.get('x-user-role'); // β Attacker controlled!// Sessions should expire!
cookies.set('session', token); // β No maxAge = lives forever// NEVER use weak or hardcoded secrets
const SECRET = 'password123'; // β Use crypto.randomBytes(32)If you're unsure about any security aspect:
- Read the documentation for your auth library
- Use established libraries instead of rolling your own
- Consult OWASP guidelines: https://owasp.org/
- Get a security audit before handling sensitive data
If you discover a security vulnerability in SessionKit itself, please email: oa.mora [at] hotmail [dot] com (π Do not open public issues)
We'll respond within 48 hours and work with you on a fix.