Session Module
The Session module provides secure session management with support for multiple storage backends, CSRF protection, and secure cookie handling.
Overview
zig
const session = @import("api").session;Quick Start
zig
const api = @import("api");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var app = try api.App.init(allocator, .{});
defer app.deinit();
// Enable sessions
try app.enableSessions(.{
.secret = "your-secret-key-at-least-32-characters",
.cookie_name = "session_id",
.max_age_seconds = 86400, // 24 hours
});
app.router.get("/profile", profileHandler);
app.router.post("/login", loginHandler);
try app.listen(.{ .port = 8080 });
}Session Configuration
SessionConfig
zig
const SessionConfig = struct {
// Secret key for signing session IDs (min 32 chars)
secret: []const u8,
// Cookie name for session ID
cookie_name: []const u8 = "session_id",
// Session lifetime in seconds
max_age_seconds: u32 = 86400,
// Cookie path
path: []const u8 = "/",
// Cookie domain (null = current domain)
domain: ?[]const u8 = null,
// Secure flag (HTTPS only)
secure: bool = true,
// HttpOnly flag (no JavaScript access)
http_only: bool = true,
// SameSite attribute
same_site: SameSite = .lax,
// Session ID length in bytes
id_length: u8 = 32,
// Cleanup interval for expired sessions
cleanup_interval_seconds: u32 = 3600,
};
const SameSite = enum {
strict, // Most restrictive
lax, // Default, good balance
none, // Required for cross-site
};Session Management
Session Structure
zig
const Session = struct {
id: []const u8,
data: std.StringHashMap([]const u8),
created_at: i64,
last_accessed: i64,
expires_at: i64,
is_new: bool,
modified: bool,
/// Gets a session value.
pub fn get(self: *Session, key: []const u8) ?[]const u8;
/// Sets a session value.
pub fn set(self: *Session, key: []const u8, value: []const u8) !void;
/// Removes a session value.
pub fn remove(self: *Session, key: []const u8) void;
/// Checks if session has a key.
pub fn has(self: *Session, key: []const u8) bool;
/// Clears all session data.
pub fn clear(self: *Session) void;
/// Invalidates the session (logout).
pub fn invalidate(self: *Session) void;
/// Regenerates session ID (security).
pub fn regenerate(self: *Session) !void;
/// Gets flash message (auto-deleted after read).
pub fn getFlash(self: *Session, key: []const u8) ?[]const u8;
/// Sets flash message.
pub fn setFlash(self: *Session, key: []const u8, value: []const u8) !void;
};Working with Sessions
zig
fn profileHandler(ctx: *api.Context) !void {
// Get or create session
const sess = try ctx.session() orelse {
ctx.response.setStatus(.unauthorized);
return;
};
// Get user ID from session
const user_id = sess.get("user_id") orelse {
return ctx.response.redirect("/login");
};
// Get user data
const user = try db.getUserById(user_id);
try ctx.response.json(user);
}
fn loginHandler(ctx: *api.Context) !void {
const body = try ctx.request.json(LoginRequest);
// Validate credentials
const user = try auth.authenticate(body.username, body.password);
// Get session
var sess = try ctx.session();
// Regenerate session ID after login (security)
try sess.regenerate();
// Store user data
try sess.set("user_id", user.id);
try sess.set("username", user.username);
try sess.set("role", user.role);
// Set flash message
try sess.setFlash("message", "Welcome back!");
try ctx.response.json(.{ .success = true });
}
fn logoutHandler(ctx: *api.Context) !void {
if (try ctx.session()) |sess| {
sess.invalidate();
}
ctx.response.redirect("/");
}Session Storage
Memory Store (Default)
zig
const MemoryStore = struct {
allocator: std.mem.Allocator,
sessions: std.StringHashMap(*Session),
mutex: std.Thread.Mutex,
config: SessionConfig,
pub fn init(allocator: std.mem.Allocator, config: SessionConfig) MemoryStore;
pub fn deinit(self: *MemoryStore) void;
/// Gets session by ID.
pub fn get(self: *MemoryStore, id: []const u8) ?*Session;
/// Creates a new session.
pub fn create(self: *MemoryStore) !*Session;
/// Saves session changes.
pub fn save(self: *MemoryStore, sess: *Session) !void;
/// Destroys a session.
pub fn destroy(self: *MemoryStore, id: []const u8) void;
/// Removes expired sessions.
pub fn cleanup(self: *MemoryStore) void;
};Using Memory Store
zig
var store = session.MemoryStore.init(allocator, .{
.secret = "your-secret-key",
.max_age_seconds = 86400,
});
defer store.deinit();
// Create session
var sess = try store.create();
// Set data
try sess.set("key", "value");
// Save changes
try store.save(sess);
// Retrieve session
if (store.get(sess.id)) |s| {
const value = s.get("key");
}
// Cleanup expired
store.cleanup();Session Manager
Manager Structure
zig
const Manager = struct {
allocator: std.mem.Allocator,
store: *MemoryStore,
config: SessionConfig,
/// Gets existing session or creates new one.
pub fn getOrCreate(self: *Manager, ctx: *Context) !*Session;
/// Gets existing session only.
pub fn get(self: *Manager, ctx: *Context) ?*Session;
/// Saves session and sets cookie.
pub fn save(self: *Manager, sess: *Session, response: *Response) !void;
/// Destroys session.
pub fn destroy(self: *Manager, sess: *Session, response: *Response) void;
/// Starts background cleanup task.
pub fn startCleanupTask(self: *Manager) !void;
};Using Session Manager
zig
fn middleware(ctx: *api.Context, next: api.NextFn) !void {
const manager = ctx.app.session_manager orelse return next(ctx);
// Load session
var sess = try manager.getOrCreate(ctx);
// Call handler
try next(ctx);
// Save session if modified
if (sess.modified) {
try manager.save(sess, ctx.response);
}
}CSRF Protection
CSRF Module
zig
const CSRF = struct {
pub const TOKEN_LENGTH = 32;
/// Generates a CSRF token and stores it in session.
pub fn generate(sess: *Session) ![TOKEN_LENGTH * 2]u8;
/// Validates a CSRF token from request.
pub fn validate(sess: *Session, token: []const u8) bool;
/// Middleware for CSRF protection.
pub fn middleware(ctx: *api.Context, next: api.NextFn) !void {
// Skip safe methods
if (isSafeMethod(ctx.request.method)) {
return next(ctx);
}
// Get session
const sess = try ctx.session() orelse return error.NoSession;
// Get token from request
const token = ctx.request.getHeader("X-CSRF-Token") orelse
ctx.request.formValue("_csrf") orelse
return error.MissingCSRFToken;
// Validate
if (!validate(sess, token)) {
ctx.response.setStatus(.forbidden);
try ctx.response.send("Invalid CSRF token");
return;
}
try next(ctx);
}
};Using CSRF Protection
zig
fn formHandler(ctx: *api.Context) !void {
const sess = try ctx.session();
// Generate CSRF token
const token = try session.CSRF.generate(sess);
// Include in form
const html = try std.fmt.allocPrint(ctx.allocator,
\\<form method="POST" action="/submit">
\\ <input type="hidden" name="_csrf" value="{s}">
\\ <input type="text" name="data">
\\ <button type="submit">Submit</button>
\\</form>
, .{token});
ctx.response.setHeader("Content-Type", "text/html");
try ctx.response.send(html);
}
fn submitHandler(ctx: *api.Context) !void {
// CSRF validation happens in middleware
const body = try ctx.request.formData();
// Process form...
}
// Enable CSRF middleware
app.use(session.CSRF.middleware);Secure Cookie Handling
Setting Session Cookie
zig
fn setCookie(self: *Manager, sess: *Session, response: *Response) void {
var cookie = std.ArrayList(u8).init(self.allocator);
defer cookie.deinit();
// Cookie name and value
try cookie.writer().print("{s}={s}", .{self.config.cookie_name, sess.id});
// Path
try cookie.writer().print("; Path={s}", .{self.config.path});
// Domain
if (self.config.domain) |domain| {
try cookie.writer().print("; Domain={s}", .{domain});
}
// Max-Age
try cookie.writer().print("; Max-Age={d}", .{self.config.max_age_seconds});
// Secure
if (self.config.secure) {
try cookie.writer().writeAll("; Secure");
}
// HttpOnly
if (self.config.http_only) {
try cookie.writer().writeAll("; HttpOnly");
}
// SameSite
switch (self.config.same_site) {
.strict => try cookie.writer().writeAll("; SameSite=Strict"),
.lax => try cookie.writer().writeAll("; SameSite=Lax"),
.none => try cookie.writer().writeAll("; SameSite=None"),
}
response.setHeader("Set-Cookie", cookie.items);
}Cookie Security Best Practices
zig
const secure_config = session.SessionConfig{
.secret = std.crypto.random.bytes(32), // Strong random secret
.secure = true, // HTTPS only
.http_only = true, // No JS access
.same_site = .strict, // CSRF protection
.max_age_seconds = 3600, // 1 hour
};Flash Messages
Flash messages are one-time messages that are automatically deleted after being read.
zig
fn loginHandler(ctx: *api.Context) !void {
var sess = try ctx.session();
if (auth.login(ctx)) {
try sess.setFlash("success", "Login successful!");
return ctx.response.redirect("/dashboard");
} else {
try sess.setFlash("error", "Invalid credentials");
return ctx.response.redirect("/login");
}
}
fn dashboardHandler(ctx: *api.Context) !void {
var sess = try ctx.session();
// Get and auto-delete flash message
const success_msg = sess.getFlash("success");
const error_msg = sess.getFlash("error");
// Render page with messages
try ctx.response.render("dashboard", .{
.success = success_msg,
.error = error_msg,
});
}Session ID Generation
zig
/// Generates a cryptographically secure session ID.
pub fn generateSessionId() [SESSION_ID_LENGTH * 2]u8 {
var random_bytes: [SESSION_ID_LENGTH]u8 = undefined;
std.crypto.random.bytes(&random_bytes);
var hex_id: [SESSION_ID_LENGTH * 2]u8 = undefined;
bytesToHex(&random_bytes, &hex_id);
return hex_id;
}App Integration
Enabling Sessions
zig
var app = try api.App.init(allocator, .{});
try app.enableSessions(.{
.secret = "your-very-long-secret-key-for-signing",
.cookie_name = "app_session",
.max_age_seconds = 86400,
.secure = true,
.http_only = true,
.same_site = .lax,
});
// Access session manager
if (app.session_manager) |manager| {
// Use manager
}Example: Complete Auth System
zig
const std = @import("std");
const api = @import("api");
const session = api.session;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var app = try api.App.init(allocator, .{});
defer app.deinit();
// Enable sessions with secure settings
try app.enableSessions(.{
.secret = "production-secret-key-minimum-32-chars!",
.cookie_name = "sid",
.max_age_seconds = 7200, // 2 hours
.secure = true,
.http_only = true,
.same_site = .strict,
});
// CSRF protection for mutations
app.use(csrfMiddleware);
// Auth routes
app.router.get("/login", loginPageHandler);
app.router.post("/login", loginHandler);
app.router.post("/logout", logoutHandler);
// Protected routes
app.router.get("/dashboard", requireAuth(dashboardHandler));
app.router.get("/profile", requireAuth(profileHandler));
app.router.post("/profile", requireAuth(updateProfileHandler));
try app.listen(.{ .port = 8080 });
}
fn csrfMiddleware(ctx: *api.Context, next: api.NextFn) !void {
// Skip GET, HEAD, OPTIONS
if (ctx.request.method == .GET or
ctx.request.method == .HEAD or
ctx.request.method == .OPTIONS) {
return next(ctx);
}
var sess = try ctx.session() orelse return error.NoSession;
const token = ctx.request.getHeader("X-CSRF-Token") orelse
ctx.request.formValue("_csrf") orelse {
ctx.response.setStatus(.forbidden);
return;
};
if (!session.CSRF.validate(sess, token)) {
ctx.response.setStatus(.forbidden);
try ctx.response.json(.{ .error = "Invalid CSRF token" });
return;
}
try next(ctx);
}
fn requireAuth(comptime handler: api.HandlerFn) api.HandlerFn {
return struct {
fn handle(ctx: *api.Context) !void {
const sess = try ctx.session() orelse {
return ctx.response.redirect("/login");
};
if (sess.get("user_id") == null) {
try sess.setFlash("error", "Please login to continue");
return ctx.response.redirect("/login");
}
// Check session expiration
if (std.time.timestamp() > sess.expires_at) {
sess.invalidate();
try sess.setFlash("error", "Session expired");
return ctx.response.redirect("/login");
}
// Update last accessed
sess.last_accessed = std.time.timestamp();
try handler(ctx);
}
}.handle;
}
fn loginPageHandler(ctx: *api.Context) !void {
var sess = try ctx.session();
const csrf_token = try session.CSRF.generate(sess);
const error_msg = sess.getFlash("error");
const html = try std.fmt.allocPrint(ctx.allocator,
\\<!DOCTYPE html>
\\<html>
\\<body>
\\ {s}
\\ <form method="POST" action="/login">
\\ <input type="hidden" name="_csrf" value="{s}">
\\ <input type="text" name="username" placeholder="Username">
\\ <input type="password" name="password" placeholder="Password">
\\ <button type="submit">Login</button>
\\ </form>
\\</body>
\\</html>
, .{
if (error_msg) |msg| std.fmt.allocPrint(ctx.allocator, "<p style='color:red'>{s}</p>", .{msg}) catch "" else "",
&csrf_token,
});
ctx.response.setHeader("Content-Type", "text/html");
try ctx.response.send(html);
}
fn loginHandler(ctx: *api.Context) !void {
const form = try ctx.request.formData();
const username = form.get("username") orelse "";
const password = form.get("password") orelse "";
// Authenticate
const user = auth.authenticate(username, password) catch |err| {
var sess = try ctx.session();
try sess.setFlash("error", "Invalid credentials");
return ctx.response.redirect("/login");
};
var sess = try ctx.session();
// Regenerate session ID after successful login
try sess.regenerate();
// Store user info
try sess.set("user_id", user.id);
try sess.set("username", user.username);
try sess.set("role", user.role);
try sess.set("login_time", std.fmt.allocPrint(ctx.allocator, "{d}", .{std.time.timestamp()}) catch "");
try sess.setFlash("success", "Welcome back!");
ctx.response.redirect("/dashboard");
}
fn logoutHandler(ctx: *api.Context) !void {
if (try ctx.session()) |sess| {
sess.invalidate();
}
ctx.response.redirect("/login");
}
fn dashboardHandler(ctx: *api.Context) !void {
var sess = (try ctx.session()).?;
const username = sess.get("username") orelse "User";
const success = sess.getFlash("success");
try ctx.response.json(.{
.message = if (success) |msg| msg else "Dashboard",
.user = username,
});
}Session Security Checklist
- [ ] Use strong, random secret key (32+ characters)
- [ ] Enable
secureflag in production (HTTPS only) - [ ] Enable
http_onlyflag (prevent XSS) - [ ] Set appropriate
same_siteattribute - [ ] Regenerate session ID after login
- [ ] Implement session timeout
- [ ] Use CSRF protection for mutations
- [ ] Store minimal data in session
- [ ] Implement proper logout (invalidate session)
- [ ] Regular cleanup of expired sessions
See Also
- Context Module - For request context
- Middleware Module - For authentication middleware
- Response Module - For cookie handling
