Cookies and Sessions
Spider provides comprehensive support for HTTP cookies and session management.
Cookies
Reading Cookies
Access cookies from requests:
import dev.alteration.branch.spider.server.Cookie
case class MyHandler() extends RequestHandler[Unit, String] {
override def handle(request: Request[Unit]): Response[String] = {
// Get all cookies as a map
val cookies = Cookie.fromHeaders(request.headers)
// Get a specific cookie
val sessionId = cookies.get("session_id")
// Or use the helper method
val userId = request.cookie("user_id")
Response(200, s"Session: ${sessionId.getOrElse("none")}")
}
}
Setting Cookies
Add cookies to responses:
import dev.alteration.branch.spider.server.Cookie
import dev.alteration.branch.spider.server.Cookie.SameSite
case class MyHandler() extends RequestHandler[Unit, String] {
override def handle(request: Request[Unit]): Response[String] = {
val cookie = Cookie(
name = "session_id",
value = "abc123",
path = Some("/"),
domain = Some("example.com"),
maxAge = Some(3600), // 1 hour in seconds
secure = true, // HTTPS only
httpOnly = true, // Not accessible via JavaScript
sameSite = Some(SameSite.Strict)
)
Response(200, "Cookie set").withCookie(cookie)
}
}
Cookie Builder Pattern
Build cookies fluently:
val cookie = Cookie("session_id", "abc123")
.withPath("/app")
.withDomain("example.com")
.withMaxAge(3600)
.withSecure
.withHttpOnly
.withSameSite(Cookie.SameSite.Lax)
Response(200, "OK").withCookie(cookie)
SameSite Attribute
Control cross-site request behavior:
// Strict: Cookie only sent in first-party context
Cookie("id", "123").withSameSite(Cookie.SameSite.Strict)
// Lax: Cookie sent with top-level navigations (default for most browsers)
Cookie("id", "123").withSameSite(Cookie.SameSite.Lax)
// None: Cookie sent in all contexts (requires Secure flag)
Cookie("id", "123")
.withSameSite(Cookie.SameSite.None)
.withSecure
Signed Cookies
Prevent cookie tampering with HMAC signatures:
import dev.alteration.branch.spider.server.SignedCookie
val secret = "your-secret-key"
// Sign a cookie
val signed = SignedCookie.sign("session_id", "abc123", secret)
// Returns: "abc123.signature"
// Verify and decode
val verified = SignedCookie.verify("session_id", signed, secret)
// Returns: Some("abc123") if valid, None if tampered
// Use in handlers
case class MyHandler() extends RequestHandler[Unit, String] {
override def handle(request: Request[Unit]): Response[String] = {
// Set signed cookie
val value = "user123"
val signedValue = SignedCookie.sign("user_id", value, secret)
val cookie = Cookie("user_id", signedValue).withHttpOnly
// Verify signed cookie
val cookieValue = request.cookie("user_id")
val userId = cookieValue.flatMap(SignedCookie.verify("user_id", _, secret))
Response(200, s"User: ${userId.getOrElse("invalid")}")
.withCookie(cookie)
}
}
Deleting Cookies
Delete cookies by setting maxAge to 0:
val deleteCookie = Cookie("session_id", "")
.withMaxAge(0)
.withPath("/")
Response(200, "Logged out").withCookie(deleteCookie)
Sessions
Sessions provide server-side state management using cookies to store session IDs.
Basic Session Usage
Use SessionMiddleware to enable sessions:
import dev.alteration.branch.spider.server.middleware.*
// Apply session middleware
val handler = MyHandler()
.withMiddleware(SessionMiddleware.default)
Access sessions in handlers:
import dev.alteration.branch.spider.server.middleware.SessionExtensions.*
case class MyHandler() extends RequestHandler[Unit, String] {
override def handle(request: Request[Unit]): Response[String] = {
// Get session value
val username = request.sessionGet("username")
// Set session value
request.sessionSet("username", "alice")
// Remove session value
request.sessionRemove("temp_data")
// Clear all session data
request.sessionClear()
Response(200, s"Hello, ${username.getOrElse("guest")}!")
}
}
Session Configuration
Customize session behavior:
import dev.alteration.branch.spider.server.middleware.*
val sessionConfig = SessionConfig(
cookieName = "SESSION",
maxAge = 3600, // Session duration in seconds (1 hour)
path = "/",
domain = Some("example.com"),
secure = true, // HTTPS only
httpOnly = true, // Not accessible via JavaScript
sameSite = Cookie.SameSite.Lax,
slidingExpiration = true // Extend session on each request
)
val store = InMemorySessionStore()
val middleware = SessionMiddleware(sessionConfig, store)
Preset Configurations
// Development: Less restrictive, longer sessions
SessionMiddleware.development
// Default: Balanced settings
SessionMiddleware.default
// Strict: Short sessions, strict security
SessionMiddleware.strict
Session Storage
InMemorySessionStore
Default in-memory storage (data lost on restart):
val store = InMemorySessionStore()
Features:
- Fast access
- Automatic cleanup of expired sessions
- Thread-safe
- No persistence
Custom Session Store
Implement your own session storage:
import dev.alteration.branch.spider.server.middleware.*
class DatabaseSessionStore(db: Database) extends SessionStore {
override def get(sessionId: String): Option[Session] = {
db.query("SELECT * FROM sessions WHERE id = ?", sessionId)
.map(row => Session(
id = row.getString("id"),
data = parseJson(row.getString("data")),
createdAt = row.getTimestamp("created_at"),
lastAccessedAt = row.getTimestamp("last_accessed_at"),
expiresAt = row.getTimestamp("expires_at")
))
}
override def save(session: Session): Unit = {
db.execute(
"INSERT INTO sessions (id, data, created_at, last_accessed_at, expires_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET ...",
session.id, toJson(session.data), session.createdAt, session.lastAccessedAt, session.expiresAt
)
}
override def delete(sessionId: String): Unit = {
db.execute("DELETE FROM sessions WHERE id = ?", sessionId)
}
override def cleanup(): Unit = {
db.execute("DELETE FROM sessions WHERE expires_at < ?", Instant.now())
}
}
Session Lifecycle
Creating Sessions
Sessions are created automatically when you first set a value:
// No session exists yet
request.sessionGet("key") // None
// Setting a value creates the session
request.sessionSet("username", "alice")
// Now the session exists
request.sessionGet("username") // Some("alice")
Or explicitly create a session:
val session = request.getOrCreateSession(sessionConfig)
Session Expiration
Sessions expire based on maxAge:
val config = SessionConfig(
maxAge = 3600, // Absolute expiration: 1 hour from creation
slidingExpiration = false
)
With sliding expiration, sessions extend on each request:
val config = SessionConfig(
maxAge = 3600, // 1 hour from last access
slidingExpiration = true // Reset expiration on each request
)
Destroying Sessions
Explicitly destroy a session:
request.sessionDestroy()
This:
- Clears all session data
- Removes the session from the store
- Deletes the session cookie
Session ID Regeneration
Regenerate the session ID (important after login):
// After successful login
request.sessionSet("user_id", userId)
request.sessionRegenerateId() // New session ID, same data
This prevents session fixation attacks.
Session Security
Best Practices
- Use HTTPS: Always set
secure = truein production - HttpOnly cookies: Set
httpOnly = trueto prevent XSS attacks - SameSite attribute: Use
StrictorLaxto prevent CSRF - Regenerate ID after login: Prevent session fixation
- Short expiration: Use reasonable
maxAgevalues - Signed cookies: Consider using signed cookies for sensitive data
Secure Session Configuration
val secureConfig = SessionConfig(
cookieName = "SESSION",
maxAge = 900, // 15 minutes
secure = true, // HTTPS only
httpOnly = true, // XSS protection
sameSite = Cookie.SameSite.Strict,// CSRF protection
slidingExpiration = true
)
Example: Login Flow
import dev.alteration.branch.spider.server.middleware.SessionExtensions.*
case class LoginHandler() extends RequestHandler[Unit, String] {
override def handle(request: Request[Unit]): Response[String] = {
val username = request.formParam("username")
val password = request.formParam("password")
if (authenticate(username, password)) {
// Set session data
request.sessionSet("user_id", username.get)
request.sessionSet("authenticated", "true")
// Regenerate session ID for security
request.sessionRegenerateId()
Response(302, "")
.withHeader("Location" -> "/dashboard")
} else {
Response(401, "Invalid credentials")
}
}
}
case class LogoutHandler() extends RequestHandler[Unit, String] {
override def handle(request: Request[Unit]): Response[String] = {
// Destroy the session
request.sessionDestroy()
Response(302, "")
.withHeader("Location" -> "/login")
}
}
case class DashboardHandler() extends RequestHandler[Unit, String] {
override def handle(request: Request[Unit]): Response[String] = {
val userId = request.sessionGet("user_id")
userId match {
case Some(user) =>
Response(200, s"Welcome, $user!")
case None =>
Response(302, "")
.withHeader("Location" -> "/login")
}
}
}
Flash Messages
Flash messages are session values that persist for only one request:
// Set a flash message
request.sessionSet("flash_message", "Login successful!")
// Read and remove flash message
val flash = request.sessionGet("flash_message")
request.sessionRemove("flash_message")
Response(200, flash.getOrElse(""))
You can wrap this in a helper:
object FlashHelper {
def setFlash(request: Request[?], message: String): Unit = {
request.sessionSet("_flash", message)
}
def getFlash(request: Request[?]): Option[String] = {
val flash = request.sessionGet("_flash")
request.sessionRemove("_flash")
flash
}
}
Next Steps
- Learn about Middleware
- Explore Request/Response Parsing
- Return to HTTP Server