Akka HTTP Session

authenticationScalaAkka HTTPsession managementJWTCSRFclient-sideencryption

Library

Akka HTTP Session

Overview

Akka HTTP Session is a Scala client-side session management library developed by SoftwareMill. As of 2025, it adds session capabilities to the Akka HTTP framework, supporting both web applications and mobile applications. It provides Cookie or HTTP header-based session management, JWT integration, CSRF protection, and tamper prevention through server-side signing. Session data can be encrypted with a default expiration of one week. This industry-standard library provides lightweight and secure session management for Akka HTTP applications, supporting diverse client environments including mobile apps, SPAs, and Web APIs.

Details

Akka HTTP Session 0.7 series achieves enterprise-level session management while maintaining Akka HTTP's lightweight nature. The client-side approach eliminates the need for server-side session storage, enabling easy horizontal scaling. Cryptographic signatures using server secrets ensure session data integrity, while AES encryption provides confidentiality. JWT integration supports RFC 7519-compliant token-based authentication with configurable standard claims like iss (issuer), sub (subject), and aud (audience). The directive-based intuitive API allows easy implementation of operations like setSession, requiredSession, and invalidateSession.

Key Features

  • Client-side Sessions: Scalable design without server-side storage requirements
  • Multiple Transport Support: Both Cookie (Web) and HTTP header (Mobile) support
  • JWT Integration: Session encoding in RFC 7519-compliant JSON Web Token format
  • Encryption and CSRF Protection: Comprehensive security through AES encryption and CSRF tokens
  • Directive-based API: Declarative session management aligned with Akka HTTP philosophy
  • Configurable Expiration: Default one week, customizable based on requirements

Advantages and Disadvantages

Advantages

  • Client-side approach reduces server resource consumption and enables easy horizontal scaling
  • Seamless integration with Akka HTTP, naturally combining with existing directives
  • JWT support enables token sharing between microservices and stateless authentication
  • Encryption and CSRF protection meet enterprise-level security requirements
  • Dual transport support (Cookie and Header) is optimal for web and mobile applications
  • Lightweight and high-performance without compromising Akka HTTP's asynchronous processing performance

Disadvantages

  • Session data is stored on the client, limiting storage of sensitive data
  • Akka HTTP specific, not usable with other Scala frameworks like Play Framework or Spray
  • Not suitable for large session data due to JVM memory efficiency considerations
  • Server secret management is critical; leakage invalidates all sessions
  • Mobile apps require manual session storage implementation
  • Real-time server-side session invalidation control is not possible

Reference Pages

Usage Examples

Basic Setup and Cookie-based Sessions

// build.sbt - Add dependencies
libraryDependencies ++= Seq(
  "com.softwaremill.akka-http-session" %% "core" % "0.7.1",
  "com.softwaremill.akka-http-session" %% "jwt" % "0.7.1", // For JWT support
  "com.typesafe.akka" %% "akka-http" % "10.5.0",
  "com.typesafe.akka" %% "akka-stream" % "2.8.0"
)

// application.conf - Configuration file
akka.http.session {
  server-secret = "your-secret-key-change-in-production"
  max-age = 7 days
  encrypt-data = true
  cookie {
    name = "app_session"
    secure = true
    http-only = true
    same-site = strict
  }
  csrf {
    cookie-name = "csrf_token"
    submitted-name = "csrf_token"
  }
}

// SessionService.scala - Session management service
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import com.softwaremill.session.SessionDirectives._
import com.softwaremill.session.SessionOptions._
import com.softwaremill.session.{SessionConfig, SessionManager}

case class UserSession(userId: String, username: String, roles: Set[String])

class SessionService(implicit sessionConfig: SessionConfig) {
  implicit val sessionManager: SessionManager[UserSession] = new SessionManager[UserSession](sessionConfig)
  
  // Login route
  def loginRoute: Route =
    path("login") {
      post {
        entity(as[LoginRequest]) { loginRequest =>
          // Authentication logic (use external auth system in production)
          authenticate(loginRequest) match {
            case Some(user) =>
              val userSession = UserSession(user.id, user.username, user.roles)
              setSession(oneOff, usingCookies, userSession) { ctx =>
                complete("Login successful")
              }
            case None =>
              complete((401, "Authentication failed"))
          }
        }
      }
    }
  
  // Logout route
  def logoutRoute: Route =
    path("logout") {
      post {
        invalidateSession(oneOff, usingCookies) { ctx =>
          complete("Logout completed")
        }
      }
    }
  
  // Protected route requiring session
  def protectedRoute: Route =
    path("protected") {
      get {
        requiredSession(oneOff, usingCookies) { session: UserSession =>
          complete(s"Hello, ${session.username}! Your roles: ${session.roles.mkString(", ")}")
        }
      }
    }
  
  // Session information route
  def sessionInfoRoute: Route =
    path("session") {
      get {
        optionalSession(oneOff, usingCookies) { sessionOption: Option[UserSession] =>
          sessionOption match {
            case Some(session) =>
              complete(SessionInfo(session.userId, session.username, authenticated = true))
            case None =>
              complete(SessionInfo("", "", authenticated = false))
          }
        }
      }
    }
  
  private def authenticate(loginRequest: LoginRequest): Option[User] = {
    // Actual authentication logic (database, LDAP, etc.)
    if (loginRequest.username == "admin" && loginRequest.password == "password") {
      Some(User("1", "admin", Set("admin", "user")))
    } else None
  }
}

case class LoginRequest(username: String, password: String)
case class User(id: String, username: String, roles: Set[String])
case class SessionInfo(userId: String, username: String, authenticated: Boolean)

HTTP Header-based Sessions (Mobile App Support)

// HeaderSessionService.scala - Header-based sessions
import com.softwaremill.session.SessionDirectives._
import com.softwaremill.session.SessionOptions._

class HeaderSessionService(implicit sessionConfig: SessionConfig) {
  implicit val sessionManager: SessionManager[UserSession] = new SessionManager[UserSession](sessionConfig)
  
  // Mobile app login
  def mobileLoginRoute: Route =
    path("api" / "login") {
      post {
        entity(as[MobileLoginRequest]) { loginRequest =>
          authenticateMobile(loginRequest) match {
            case Some(user) =>
              val userSession = UserSession(user.id, user.username, user.roles)
              setSession(oneOff, usingHeaders, userSession) { ctx =>
                complete(LoginResponse(success = true, message = "Authentication successful"))
              }
            case None =>
              complete((401, LoginResponse(success = false, message = "Authentication failed")))
          }
        }
      }
    }
  
  // Mobile app API protection
  def mobileApiRoute: Route =
    pathPrefix("api") {
      requiredSession(oneOff, usingHeaders) { session: UserSession =>
        pathPrefix("user") {
          get {
            complete(UserProfile(session.userId, session.username, session.roles))
          } ~
          put {
            entity(as[UpdateProfileRequest]) { updateRequest =>
              // Profile update logic
              complete("Profile update completed")
            }
          }
        } ~
        pathPrefix("data") {
          get {
            complete(s"${session.username}'s data list")
          } ~
          post {
            entity(as[CreateDataRequest]) { createRequest =>
              // Data creation logic
              complete("Data creation completed")
            }
          }
        }
      }
    }
  
  // Session refresh (token refresh)
  def refreshTokenRoute: Route =
    path("api" / "refresh") {
      post {
        requiredSession(oneOff, usingHeaders) { session: UserSession =>
          // Session extension
          setSession(oneOff, usingHeaders, session.copy()) { ctx =>
            complete(RefreshResponse(success = true, message = "Token refresh completed"))
          }
        }
      }
    }
  
  private def authenticateMobile(loginRequest: MobileLoginRequest): Option[User] = {
    // Mobile-specific authentication logic (device ID validation, etc.)
    validateDeviceId(loginRequest.deviceId) && validateCredentials(loginRequest.username, loginRequest.password) match {
      case true => Some(User(loginRequest.username, loginRequest.username, Set("mobile_user")))
      case false => None
    }
  }
  
  private def validateDeviceId(deviceId: String): Boolean = {
    // Device ID validation logic
    deviceId.nonEmpty && deviceId.length > 10
  }
  
  private def validateCredentials(username: String, password: String): Boolean = {
    // Credential validation logic
    username.nonEmpty && password.length >= 8
  }
}

case class MobileLoginRequest(username: String, password: String, deviceId: String)
case class LoginResponse(success: Boolean, message: String)
case class UserProfile(userId: String, username: String, roles: Set[String])
case class UpdateProfileRequest(name: String, email: String)
case class CreateDataRequest(title: String, content: String)
case class RefreshResponse(success: Boolean, message: String)

JWT Integration and Claims Management

// JWTSessionService.scala - JWT integrated sessions
import com.softwaremill.session.{SessionConfig, SessionManager}
import com.softwaremill.session.SessionDirectives._
import com.softwaremill.session.SessionOptions._
import com.softwaremill.session.jwt.{JwtSessionEncoder, JwtSessionManager}
import org.json4s.{DefaultFormats, JValue}
import org.json4s.jackson.JsonMethods._

// JWT session data
case class JwtUserSession(
  userId: String,
  username: String,
  roles: Set[String],
  issuer: String = "your-app.com",
  audience: String = "api.your-app.com"
)

class JWTSessionService(implicit sessionConfig: SessionConfig) {
  
  // JWT session encoder
  implicit val jwtSessionEncoder: JwtSessionEncoder[JwtUserSession] = 
    new JwtSessionEncoder[JwtUserSession] {
      implicit val formats: DefaultFormats.type = DefaultFormats
      
      def encode(session: JwtUserSession): JValue = {
        parse(s"""
        {
          "sub": "${session.userId}",
          "username": "${session.username}",
          "roles": [${session.roles.map(role => s""""$role"""").mkString(", ")}],
          "iss": "${session.issuer}",
          "aud": "${session.audience}",
          "iat": ${System.currentTimeMillis() / 1000},
          "exp": ${(System.currentTimeMillis() / 1000) + 3600}
        }
        """)
      }
      
      def decode(jwtPayload: JValue): JwtUserSession = {
        JwtUserSession(
          userId = (jwtPayload \ "sub").extract[String],
          username = (jwtPayload \ "username").extract[String],
          roles = (jwtPayload \ "roles").extract[List[String]].toSet,
          issuer = (jwtPayload \ "iss").extract[String],
          audience = (jwtPayload \ "aud").extract[String]
        )
      }
    }
  
  implicit val sessionManager: JwtSessionManager[JwtUserSession] = 
    new JwtSessionManager[JwtUserSession](sessionConfig)
  
  // JWT authentication route
  def jwtLoginRoute: Route =
    path("jwt" / "login") {
      post {
        entity(as[JwtLoginRequest]) { loginRequest =>
          authenticateJWT(loginRequest) match {
            case Some(user) =>
              val jwtSession = JwtUserSession(
                userId = user.id,
                username = user.username,
                roles = user.roles,
                issuer = "your-app.com",
                audience = loginRequest.audience.getOrElse("api.your-app.com")
              )
              setSession(oneOff, usingHeaders, jwtSession) { ctx =>
                // Return JWT token in response header
                complete(JwtLoginResponse(
                  success = true,
                  token = ctx.response.headers.find(_.name == "Authorization").map(_.value).getOrElse(""),
                  expiresIn = 3600,
                  user = UserInfo(user.id, user.username, user.roles)
                ))
              }
            case None =>
              complete((401, JwtLoginResponse(success = false, token = "", expiresIn = 0, user = null)))
          }
        }
      }
    }
  
  // JWT protected API route
  def jwtApiRoute: Route =
    pathPrefix("jwt" / "api") {
      requiredSession(oneOff, usingHeaders) { session: JwtUserSession =>
        pathPrefix("profile") {
          get {
            complete(JwtUserProfile(
              userId = session.userId,
              username = session.username,
              roles = session.roles,
              issuer = session.issuer,
              audience = session.audience
            ))
          }
        } ~
        pathPrefix("admin") {
          authorizeSession(session, "admin") {
            get {
              complete("Admin-only data")
            }
          }
        } ~
        pathPrefix("validate") {
          post {
            complete(JwtValidationResponse(
              valid = true,
              userId = session.userId,
              expiresAt = System.currentTimeMillis() + 3600000 // 1 hour later
            ))
          }
        }
      }
    }
  
  // Role-based access control
  private def authorizeSession(session: JwtUserSession, requiredRole: String): Directive0 = {
    if (session.roles.contains(requiredRole)) {
      pass
    } else {
      complete((403, "Access denied"))
    }
  }
  
  // JWT token validation endpoint
  def jwtValidateRoute: Route =
    path("jwt" / "validate") {
      post {
        optionalHeaderValueByName("Authorization") {
          case Some(authHeader) if authHeader.startsWith("Bearer ") =>
            val token = authHeader.substring(7)
            // Manually validate JWT token
            validateJwtToken(token) match {
              case Some(session) =>
                complete(JwtValidationResponse(
                  valid = true,
                  userId = session.userId,
                  expiresAt = System.currentTimeMillis() + 3600000
                ))
              case None =>
                complete((401, JwtValidationResponse(valid = false, userId = "", expiresAt = 0)))
            }
          case _ =>
            complete((400, "Authorization header required"))
        }
      }
    }
  
  private def authenticateJWT(loginRequest: JwtLoginRequest): Option[User] = {
    // JWT authentication logic
    if (loginRequest.username.nonEmpty && loginRequest.password.length >= 6) {
      Some(User(loginRequest.username, loginRequest.username, Set("user", "jwt_user")))
    } else None
  }
  
  private def validateJwtToken(token: String): Option[JwtUserSession] = {
    try {
      // Actual JWT validation logic (signature verification, expiration check, etc.)
      // Simplified here
      Some(JwtUserSession("test", "testuser", Set("user")))
    } catch {
      case _: Exception => None
    }
  }
}

case class JwtLoginRequest(username: String, password: String, audience: Option[String])
case class JwtLoginResponse(success: Boolean, token: String, expiresIn: Long, user: UserInfo)
case class UserInfo(id: String, username: String, roles: Set[String])
case class JwtUserProfile(userId: String, username: String, roles: Set[String], issuer: String, audience: String)
case class JwtValidationResponse(valid: Boolean, userId: String, expiresAt: Long)

CSRF Protection and Security Configuration

// CSRFProtectionService.scala - CSRF protection implementation
import com.softwaremill.session.{SessionConfig, SessionManager}
import com.softwaremill.session.SessionDirectives._
import com.softwaremill.session.SessionOptions._
import com.softwaremill.session.CsrfDirectives._
import com.softwaremill.session.CsrfOptions._

class CSRFProtectionService(implicit sessionConfig: SessionConfig) {
  implicit val sessionManager: SessionManager[UserSession] = new SessionManager[UserSession](sessionConfig)
  
  // Form display with CSRF protection
  def formRoute: Route =
    path("form") {
      get {
        // Generate CSRF token and embed in form
        setNewCsrfToken(checkHeader) { ctx =>
          val csrfToken = ctx.response.headers.find(_.name == "X-CSRF-Token").map(_.value).getOrElse("")
          complete(s"""
            <html>
              <body>
                <form method="post" action="/submit">
                  <input type="hidden" name="csrf_token" value="$csrfToken"/>
                  <input type="text" name="data" placeholder="Enter data"/>
                  <button type="submit">Submit</button>
                </form>
              </body>
            </html>
          """)
        }
      }
    }
  
  // Form processing with CSRF protection
  def submitRoute: Route =
    path("submit") {
      post {
        // CSRF token validation and session requirement
        requiredSession(oneOff, usingCookies) { session: UserSession =>
          hmacTokenCsrfProtection(checkHeader) {
            entity(as[FormData]) { formData =>
              // Data processing logic
              processFormData(session, formData) match {
                case Success(result) =>
                  complete(s"Data processing completed: $result")
                case Failure(error) =>
                  complete((400, s"Processing error: ${error.getMessage}"))
              }
            }
          }
        }
      }
    }
  
  // API CSRF protection
  def apiWithCSRFRoute: Route =
    pathPrefix("api" / "secure") {
      requiredSession(oneOff, usingHeaders) { session: UserSession =>
        post {
          // CSRF validation for API requests
          hmacTokenCsrfProtection(checkHeader) {
            entity(as[ApiRequest]) { apiRequest =>
              processApiRequest(session, apiRequest) match {
                case Success(response) =>
                  complete(response)
                case Failure(error) =>
                  complete((500, ApiErrorResponse(error.getMessage)))
              }
            }
          }
        }
      }
    }
  
  // Security headers configuration
  def secureRoute: Route =
    extractRequestContext { ctx =>
      // Add security headers
      respondWithHeaders(
        `Content-Security-Policy`("default-src 'self'; script-src 'self' 'unsafe-inline'"),
        `X-Frame-Options`(DENY),
        `X-Content-Type-Options`(nosniff),
        `Strict-Transport-Security`(31536000, includeSubDomains = true),
        RawHeader("X-XSS-Protection", "1; mode=block")
      ) {
        formRoute ~ submitRoute ~ apiWithCSRFRoute
      }
    }
  
  // Session invalidation (security enhancement)
  def secureLogoutRoute: Route =
    path("secure" / "logout") {
      post {
        requiredSession(oneOff, usingCookies) { session: UserSession =>
          hmacTokenCsrfProtection(checkHeader) {
            // Logout processing and session invalidation
            auditLog(s"User ${session.username} logged out securely")
            invalidateSession(oneOff, usingCookies) { ctx =>
              complete("Secure logout completed")
            }
          }
        }
      }
    }
  
  private def processFormData(session: UserSession, formData: FormData): Try[String] = {
    Try {
      // Form data processing logic
      s"${session.username}'s data: ${formData.data}"
    }
  }
  
  private def processApiRequest(session: UserSession, apiRequest: ApiRequest): Try[ApiResponse] = {
    Try {
      ApiResponse(success = true, data = s"API processing completed: ${apiRequest.action}")
    }
  }
  
  private def auditLog(message: String): Unit = {
    // Audit log recording
    println(s"[AUDIT] ${java.time.Instant.now()} - $message")
  }
}

case class FormData(data: String)
case class ApiRequest(action: String, parameters: Map[String, String])
case class ApiResponse(success: Boolean, data: String)
case class ApiErrorResponse(error: String)

Error Handling and Log Management

// SessionErrorHandler.scala - Error handling
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.{Directive0, ExceptionHandler, RejectionHandler}
import akka.http.scaladsl.server.Directives._
import com.softwaremill.session.{SessionConfig, SessionManager}
import scala.util.{Failure, Success, Try}
import org.slf4j.LoggerFactory

class SessionErrorHandler(implicit sessionConfig: SessionConfig) {
  private val logger = LoggerFactory.getLogger(this.getClass)
  
  // Session-specific exception handler
  implicit val sessionExceptionHandler: ExceptionHandler = ExceptionHandler {
    case _: SessionExpiredException =>
      logger.warn("Session expired")
      complete((StatusCodes.Unauthorized, SessionErrorResponse("Session has expired", "SESSION_EXPIRED")))
      
    case _: InvalidSessionException =>
      logger.warn("Invalid session")
      complete((StatusCodes.Unauthorized, SessionErrorResponse("Invalid session", "INVALID_SESSION")))
      
    case _: CSRFTokenMismatchException =>
      logger.warn("CSRF token mismatch")
      complete((StatusCodes.Forbidden, SessionErrorResponse("CSRF token mismatch", "CSRF_MISMATCH")))
      
    case _: SessionNotFoundException =>
      logger.info("Session not found")
      complete((StatusCodes.Unauthorized, SessionErrorResponse("Session not found", "NO_SESSION")))
      
    case ex: SecurityException =>
      logger.error("Security exception", ex)
      complete((StatusCodes.Forbidden, SessionErrorResponse("Security error occurred", "SECURITY_ERROR")))
      
    case ex: Exception =>
      logger.error("Unexpected error in session handling", ex)
      complete((StatusCodes.InternalServerError, SessionErrorResponse("Internal error occurred", "INTERNAL_ERROR")))
  }
  
  // Session-specific rejection handler
  implicit val sessionRejectionHandler: RejectionHandler = RejectionHandler.newBuilder()
    .handle {
      case SessionRejection(message) =>
        logger.warn(s"Session rejection: $message")
        complete((StatusCodes.Unauthorized, SessionErrorResponse(message, "SESSION_REJECTION")))
    }
    .handleNotFound {
      complete((StatusCodes.NotFound, SessionErrorResponse("Resource not found", "NOT_FOUND")))
    }
    .result()
  
  // Session audit directive
  def auditSession: Directive0 = extractClientIP.flatMap { clientIP =>
    extractRequestContext.flatMap { ctx =>
      val userAgent = ctx.request.headers.find(_.name == "User-Agent").map(_.value).getOrElse("Unknown")
      val method = ctx.request.method.value
      val uri = ctx.request.uri.toString
      
      logger.info(s"Session request: $method $uri from $clientIP ($userAgent)")
      pass
    }
  }
  
  // Session validation directive
  def validateSession: Directive0 = 
    optionalSession(oneOff, usingCookies).flatMap {
      case Some(session: UserSession) =>
        if (isValidSession(session)) {
          updateSessionLastActivity(session)
          pass
        } else {
          logger.warn(s"Invalid session for user: ${session.username}")
          throw new InvalidSessionException("Session is invalid")
        }
      case None =>
        logger.debug("No session found")
        pass
    }
  
  // Session rate limiting directive
  def rateLimitBySession: Directive0 = 
    optionalSession(oneOff, usingCookies).flatMap {
      case Some(session: UserSession) =>
        if (checkRateLimit(session.userId)) {
          pass
        } else {
          logger.warn(s"Rate limit exceeded for user: ${session.username}")
          complete((StatusCodes.TooManyRequests, SessionErrorResponse("Request rate limit exceeded", "RATE_LIMIT_EXCEEDED")))
        }
      case None =>
        if (checkRateLimitByIP()) {
          pass
        } else {
          logger.warn("Rate limit exceeded for anonymous user")
          complete((StatusCodes.TooManyRequests, SessionErrorResponse("Request rate limit exceeded", "RATE_LIMIT_EXCEEDED")))
        }
    }
  
  // Comprehensive session protection directive
  def protectedRoute: Directive0 = 
    auditSession & validateSession & rateLimitBySession
  
  // Session health check
  private def isValidSession(session: UserSession): Boolean = {
    Try {
      // Check session health
      session.userId.nonEmpty &&
      session.username.nonEmpty &&
      !isSessionBlacklisted(session.userId) &&
      !isSessionExpired(session)
    }.getOrElse(false)
  }
  
  private def updateSessionLastActivity(session: UserSession): Unit = {
    // Update session last activity time
    logger.debug(s"Updated last activity for session: ${session.userId}")
  }
  
  private def checkRateLimit(userId: String): Boolean = {
    // Per-user rate limit check
    true // In production, use Redis or Hazelcast
  }
  
  private def checkRateLimitByIP(): Boolean = {
    // Per-IP rate limit check
    true // In production, use Redis or Hazelcast
  }
  
  private def isSessionBlacklisted(userId: String): Boolean = {
    // Session blacklist check
    false // In production, use Redis or database
  }
  
  private def isSessionExpired(session: UserSession): Boolean = {
    // Session expiration check
    false // In production, include timestamp in session
  }
}

// Custom exception classes
class SessionExpiredException(message: String) extends RuntimeException(message)
class InvalidSessionException(message: String) extends RuntimeException(message)
class CSRFTokenMismatchException(message: String) extends RuntimeException(message)
class SessionNotFoundException(message: String) extends RuntimeException(message)

// Custom rejection
case class SessionRejection(message: String) extends akka.http.scaladsl.server.Rejection

// Error response
case class SessionErrorResponse(message: String, code: String)