Sessions

Learn how to manage user sessions and maintain state in Suave applications.

Cookie-Based Sessions

Use cookies to maintain session state:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.Writers

let app =
  choose [
    path "/set-session" >=>
      setHeader "Set-Cookie" "session=user123; Path=/; HttpOnly" >=>
      OK "Session set"
    
    path "/get-session" >=> fun ctx ->
      match ctx.request.header "Cookie" with
      | Choice1Of2 cookies ->
          OK (sprintf "Cookies: %s" cookies) ctx
      | Choice2Of2 _ ->
          OK "No session" ctx
  ]

Session Storage Patterns

Store session data with in-memory or database backends:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.Writers
open System.Collections.Concurrent
open Suave.RequestErrors

type SessionData = { userId : int; username : string }

// In-memory session store
let sessions = new ConcurrentDictionary<string, SessionData>()

let app =
  choose [
    POST >=> path "/login" >=> fun ctx ->
      async {
        let sessionId = System.Guid.NewGuid().ToString()
        let sessionData = { userId = 1; username = "john" }
        sessions.TryAdd(sessionId, sessionData) |> ignore
        
        return!
          (setHeader "Set-Cookie"
            (sprintf "sessionId=%s; Path=/; HttpOnly" sessionId) >=>
           OK "Session created") ctx
      }
    
    path "/profile" >=> fun ctx ->
      match ctx.request.header "cookie" with
      | Choice1Of2 cookie ->
          let sessionId = cookie.Split('=').[1]
          match sessions.TryGetValue(sessionId) with
          | true, sessionData ->
              OK (sprintf "Session data: %A" sessionData) ctx
          | false, _ ->
              RequestErrors.UNAUTHORIZED "Invalid session" ctx
      | Choice2Of2 _ ->
          RequestErrors.UNAUTHORIZED "No session" ctx
  ]

Session Timeout

Implement session expiration:

open Suave
open Suave.Operators
open System
open Suave.Filters
open Suave.Successful
open Suave.Writers

let sessionTimeout = TimeSpan.FromMinutes(30.0)

let app =
  choose [
    path "/login" >=>
      setHeader "Set-Cookie" 
        (sprintf "session=xyz; Path=/; Max-Age=%d; HttpOnly" 
          (int sessionTimeout.TotalSeconds)) >=>
      OK "Session created with 30 minute timeout"
  ]

Secure Session Cookies

Create secure session cookies for production:

open Suave
open Suave.Operators
open Suave.Writers
open Suave.Filters
open Suave.Successful

let createSecureSessionCookie (sessionId: string) =
  sprintf "session=%s; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=1800" sessionId

let app =
  choose [
    path "/secure-login" >=>
      setHeader "Set-Cookie" (createSecureSessionCookie "session-xyz") >=>
      OK "Secure session created"
  ]

Session Cleanup

Clean up expired sessions:

open Suave
open System
open System.Collections.Concurrent
open Suave.Successful

let sessions = new ConcurrentDictionary<string, (obj * DateTime)>()

let cleanupExpiredSessions () =
  async {
    while true do
      do! Async.Sleep(60000) // Every minute
      let now = DateTime.UtcNow
      let expired = 
        sessions 
        |> Seq.filter (fun kvp -> (snd kvp.Value - now).TotalMinutes > 30.0)
        |> Seq.map (fun kvp -> kvp.Key)
        |> Seq.toList
      
      expired |> List.iter (fun key -> sessions.TryRemove(key) |> ignore)
      printfn "Cleaned up %d expired sessions" expired.Length
  }

[<EntryPoint>]
let main argv =
  // Start cleanup task
  Async.Start (cleanupExpiredSessions())
  
  let app = OK "Sessions with cleanup"
  startWebServer defaultConfig app
  0

CSRF Token Sessions

Protect forms with CSRF tokens in sessions:

open Suave
open Suave.Operators
open System.Collections.Concurrent
open Suave.Filters
open Suave.Successful
open Suave.RequestErrors

let csrfTokens = new ConcurrentDictionary<string, string>()

let generateCSRFToken sessionId =
  let token = System.Guid.NewGuid().ToString()
  csrfTokens.AddOrUpdate(sessionId, token, fun _ _ -> token) |> ignore
  token

let validateCSRFToken sessionId token =
  match csrfTokens.TryGetValue(sessionId) with
  | true, storedToken -> storedToken = token
  | false, _ -> false

let app =
  choose [
    GET >=> path "/form" >=> fun ctx ->
      let sessionId = "session-123"
      let token = generateCSRFToken sessionId
      let html = sprintf "<input type='hidden' name='csrf_token' value='%s'>" token
      OK html ctx
    
    POST >=> path "/form" >=> fun ctx ->
      match ctx.request.formData "csrf_token" with
      | Choice1Of2 token when validateCSRFToken "session-123" token ->
          OK "Form submitted safely" ctx
      | _ ->
          RequestErrors.FORBIDDEN "CSRF token invalid" ctx
  ]