Simple session access and route protection for Astro applications.
- ✅ Simple API - Access session data anywhere in your app
- 🛡️ Route Protection - Declarative route guards with roles/permissions
- 🔒 Global Protection - Protect all routes by default with a single flag
- 🚀 Type Safe - Full TypeScript support
- 🎯 Flexible - Works with any session storage (cookies, Redis, DB, etc.)
- ⚡ Fast - Uses AsyncLocalStorage for zero-overhead access
npm install astro-sessionkit// astro.config.mjs
import { defineConfig } from 'astro/config';
import sessionkit from 'astro-sessionkit';
export default defineConfig({
integrations: [
sessionkit({
loginPath: '/login',
protect: [
// Protect admin routes
{ pattern: '/admin/**', role: 'admin' },
// Protect dashboard for authenticated users
{ pattern: '/dashboard', roles: ['user', 'admin'] },
// Protect by permission
{ pattern: '/settings', permission: 'settings:write' },
// Custom logic
{
pattern: '/premium/**',
allow: (session) => session?.subscription === 'premium'
}
],
globalProtect: false, // Set to true to protect all routes by default
exclude: ['/public/**', '/about'], // Routes to ignore if globalProtect is true
debug: false // Enable debug logging
})
]
});SessionKit reads from context.session.get('__session__'). You set it up in your middleware:
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
// Get session from wherever you store it
// (cookies, Redis, database, etc.)
const sessionId = context.cookies.get('session_id')?.value;
if (sessionId) {
const user = await db.getUserBySessionId(sessionId);
// Set session for SessionKit to read
context.session.set('__session__', {
userId: user.id,
email: user.email,
role: user.role,
permissions: user.permissions
});
}
return next();
});Use the provided helpers to register sessions after successful authentication:
// src/pages/api/login.ts
import type { APIRoute } from 'astro';
import { setSession } from 'astro-sessionkit/server';
export const POST: APIRoute = async (context) => {
const { email, password } = await context.request.json();
// Verify credentials (YOUR authentication logic)
const user = await verifyCredentials(email, password);
if (user) {
// Register session with SessionKit
setSession(context, {
userId: user.id,
email: user.email,
role: user.role,
permissions: user.permissions
});
// Store session ID (YOUR storage logic)
const sessionId = await createSessionInDatabase(user.id);
context.cookies.set('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7 days
});
return new Response(JSON.stringify({ success: true }));
}
return new Response(JSON.stringify({ error: 'Invalid credentials' }), {
status: 401
});
};---
// src/pages/dashboard.astro
import { getSession, requireSession } from 'astro-sessionkit/server';
// Get session (returns null if not authenticated)
const session = getSession();
// Or require authentication (throws 401 if not authenticated)
const session = requireSession();
---
<h1>Welcome, {session.email}</h1>Register a session after successful authentication.
Parameters:
context: APIContext- Astro API contextsession: Session- Session data to register
Throws: Error if session structure is invalid
import { setSession } from 'astro-sessionkit/server';
export const POST: APIRoute = async (context) => {
const user = await authenticateUser(credentials);
setSession(context, {
userId: user.id,
email: user.email,
role: user.role,
permissions: user.permissions
});
// Also store in cookies/database
context.cookies.set('session_id', sessionId);
};Clear the session during logout.
import { clearSession } from 'astro-sessionkit/server';
export const POST: APIRoute = async (context) => {
clearSession(context);
// Also delete from cookies/database
context.cookies.delete('session_id');
await db.deleteSession(sessionId);
return context.redirect('/');
};Update specific fields in the current session.
Parameters:
context: APIContext- Astro API contextupdates: Partial<Session>- Fields to update
Throws: Error if no session exists or updated session is invalid
import { updateSession } from 'astro-sessionkit/server';
export const POST: APIRoute = async (context) => {
// Update user's role
updateSession(context, {
role: 'admin',
permissions: ['admin:read', 'admin:write']
});
// Also update in your storage
await db.updateSession(sessionId, updates);
};All functions are imported from astro-sessionkit/server:
Get the current session (returns null if not authenticated).
import { getSession } from 'astro-sessionkit/server';
const session = getSession();
if (session) {
console.log('User ID:', session.userId);
}Get the current session or throw 401 if not authenticated.
import { requireSession } from 'astro-sessionkit/server';
const session = requireSession();
// TypeScript knows session is not null hereCheck if the user is authenticated.
import { isAuthenticated } from 'astro-sessionkit/server';
if (isAuthenticated()) {
// User is logged in
}Check if user has a specific role.
import { hasRole } from 'astro-sessionkit/server';
if (hasRole('admin')) {
// User is an admin
}Check if user has a specific permission.
import { hasPermission } from 'astro-sessionkit/server';
if (hasPermission('posts:delete')) {
// User can delete posts
}Check if user has ALL specified permissions.
import { hasAllPermissions } from 'astro-sessionkit/server';
if (hasAllPermissions('posts:read', 'posts:write')) {
// User has both permissions
}Check if user has ANY of the specified permissions.
import { hasAnyPermission } from 'astro-sessionkit/server';
if (hasAnyPermission('posts:delete', 'admin:panel')) {
// User has at least one permission
}Check if user has a specific role AND a specific permission.
import { hasRolePermission } from 'astro-sessionkit/server';
if (hasRolePermission('admin', 'delete users')) {
// User is admin AND has 'delete users' permission
}Require a specific role:
const rule = { pattern: '/admin/**', role: 'admin' };User must have ONE of these roles:
const rule = { pattern: '/dashboard', roles: ['user', 'admin', 'moderator'] };Require a specific permission:
const rule = { pattern: '/settings', permission: 'settings:write' };User must have ALL of these permissions:
{ pattern: '/admin/users', permissions: ['users:read', 'users:write'] }Use a custom function for complex logic:
{
pattern: '/premium/**',
allow: (session) => {
return session?.subscription === 'premium' && !session?.banned;
}
}Override the default login path per rule:
{
pattern: '/admin/**',
role: 'admin',
redirectTo: '/unauthorized'
}Patterns support glob syntax:
/admin- Exact match/admin/*- One or more segments (/admin/users,/admin/users/123)/admin/**- Any path under admin (/admin,/admin/users,/admin/x/y/z)
You can protect all routes in your application by default using the globalProtect option. When enabled, any route that doesn't match an explicit rule in protect or isn't listed in exclude will require an active session.
sessionkit({
loginPath: '/login',
globalProtect: true,
exclude: [
'/', // Public landing page
'/login', // Login page (automatically excluded but good to be explicit)
'/about', // Public about page
'/public/**', // All public assets/pages
'/api/health' // Health check endpoint
]
})- If
globalProtectistrue: All routes require authentication unless explicitly excluded. - If
globalProtectisfalse(default): Only routes matching patterns in theprotectarray are guarded. - Exclusion rules: The
excludearray accepts the same glob patterns as theprotectrules. - Auto-exclusion: The
loginPathis automatically excluded from global protection to prevent redirect loops.
Enable debug logging to troubleshoot configuration issues or pattern matching:
sessionkit({
debug: true
})When enabled, SessionKit will log detailed information about matching rules and access decisions to the console.
The session object must have this shape:
interface Session {
userId: string; // Required
email?: string;
role?: string;
roles?: string[];
permissions?: string[];
[key: string]: unknown; // Add any custom fields
}You control what goes in the session - SessionKit just reads it.
Override how roles/permissions are extracted:
sessionkit({
access: {
// Custom role extraction
getRole: (session) => session?.primaryRole ?? null,
// Custom permissions extraction
getPermissions: (session) => {
return [...session?.permissions ?? [], ...session?.dynamicPerms ?? []];
},
// Override all built-in checks
check: (rule, session) => {
// Your custom logic
return session?.customField === 'allowed';
}
}
})By default, SessionKit uses Node's AsyncLocalStorage to manage the session context. In some environments (like certain edge runtimes), you might need to provide your own context management.
sessionkit({
// Provide a custom way to run code within a context
runWithContext: (context, fn) => {
// Your custom context implementation
return myCustomStorage.run(context, fn);
},
// Provide a custom way to retrieve the current context
getContextStore: () => {
return myCustomStorage.getStore();
}
})// src/pages/api/logout.ts
import type { APIRoute } from 'astro';
import { clearSession } from 'astro-sessionkit/server';
export const POST: APIRoute = async (context) => {
const sessionId = context.cookies.get('session_id')?.value;
// Clear from SessionKit
clearSession(context);
// Delete from storage
if (sessionId) {
await db.deleteSession(sessionId);
}
context.cookies.delete('session_id');
return context.redirect('/login');
};// src/pages/api/update-role.ts
import type { APIRoute } from 'astro';
import { updateSession } from 'astro-sessionkit/server';
export const POST: APIRoute = async (context) => {
const { newRole } = await context.request.json();
// Update in SessionKit
updateSession(context, { role: newRole });
// Also update in your database
const sessionId = context.cookies.get('session_id')?.value;
await db.updateUserRole(sessionId, newRole);
return new Response(JSON.stringify({ success: true }));
};// src/pages/api/auth/login.ts
import type { APIRoute } from 'astro';
import { setSession } from 'astro-sessionkit/server';
import { hashPassword, generateSessionId } from './utils';
export const POST: APIRoute = async (context) => {
const { email, password } = await context.request.json();
// 1. Verify credentials
const user = await db.findUserByEmail(email);
if (!user || !await hashPassword.verify(password, user.hashedPassword)) {
return new Response(JSON.stringify({ error: 'Invalid credentials' }), {
status: 401
});
}
// 2. Create session ID
const sessionId = generateSessionId();
await db.createSession({
id: sessionId,
userId: user.id,
expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000) // 7 days
});
// 3. Register with SessionKit
setSession(context, {
userId: user.id,
email: user.email,
role: user.role,
permissions: user.permissions
});
// 4. Set secure cookie
context.cookies.set('session_id', sessionId, {
httpOnly: true,
secure: import.meta.env.PROD,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/'
});
return new Response(JSON.stringify({
success: true,
user: { email: user.email, role: user.role }
}));
};sessionkit({
protect: [
// Admin section
{ pattern: '/admin/**', role: 'admin', redirectTo: '/unauthorized' },
// User dashboard
{ pattern: '/dashboard', roles: ['user', 'admin'] },
// Settings require specific permission
{ pattern: '/settings/**', permission: 'settings:access' },
// Premium content
{
pattern: '/premium/**',
allow: (session) => session?.tier === 'premium'
}
]
})---
import { hasRole, hasPermission } from 'astro-sessionkit/server';
---
{hasRole('admin') && (
<a href="/admin">Admin Panel</a>
)}
{hasPermission('posts:create') && (
<button>Create Post</button>
)}// src/pages/api/admin.ts
import type { APIRoute } from 'astro';
import { requireSession, hasRole } from 'astro-sessionkit/server';
export const GET: APIRoute = async () => {
const session = requireSession();
if (!hasRole('admin')) {
return new Response('Forbidden', { status: 403 });
}
// Admin logic here
return new Response(JSON.stringify({ data: 'secret' }));
};- You set the session in
context.locals.sessionvia your own middleware - SessionKit reads it and makes it available via AsyncLocalStorage
- Route guards automatically protect paths based on your rules
- Helper functions provide easy access throughout your app
- Session creation/storage
- Authentication
- Session expiration
- CSRF protection
These are your responsibility. See SECURITY.md for a complete security guide.
Before production:
- ✅ Encrypt/sign your sessions (use lucia-auth, @auth/astro, or iron-session)
- ✅ Set secure cookie flags (HttpOnly, Secure, SameSite)
- ✅ Implement session expiration
- ✅ Add CSRF protection for state-changing operations
- ✅ Use HTTPS in production
MIT License © Alex Mora