Authentication context extraction utilities for working with authenticated requests and user context in http4s applications.
Extract authentication context from authenticated requests.
/**
* Authentication context extractor for AuthedRequest
* Extracts the underlying request and authentication context
*/
object as {
def unapply[F[_], A](ar: AuthedRequest[F, A]): Option[(Request[F], A)]
}Usage Examples:
import org.http4s.AuthedRequest
import org.http4s.dsl.io._
// Define a user type for authentication context
case class User(id: Int, name: String, roles: Set[String])
// In your authenticated routes
val authedRoutes = AuthedRoutes.of[User, IO] {
// Extract request and user context
case GET -> Root / "profile" as user =>
Ok(s"Profile for user: ${user.name}")
// Access both request and user context
case req @ POST -> Root / "posts" as user =>
for {
post <- req.req.as[String] // Access underlying request
result <- createPost(post, user.id)
response <- Created(s"Post created by ${user.name}")
} yield response
// Pattern matching on user properties
case GET -> Root / "admin" as user if user.roles.contains("admin") =>
Ok("Admin panel")
case GET -> Root / "admin" as user =>
Forbidden(s"User ${user.name} does not have admin access")
}The as extractor works seamlessly with http4s authentication middleware:
import org.http4s.server.AuthMiddleware
import cats.data.{Kleisli, OptionT}
// Authentication function
def authUser(request: Request[IO]): OptionT[IO, User] = {
// Extract token from Authorization header
val tokenOpt = request.headers.get[Authorization]
.collect { case Authorization(Credentials.Token(token)) => token.value }
OptionT.fromOption[IO](tokenOpt)
.flatMap(token => OptionT(validateToken(token)))
}
// Create auth middleware
val authMiddleware: AuthMiddleware[IO, User] =
AuthMiddleware(Kleisli(authUser))
// Apply middleware to routes
val authenticatedService = authMiddleware(authedRoutes)Handle different types of authentication contexts:
// Different user types
sealed trait AuthContext
case class RegularUser(id: Int, name: String) extends AuthContext
case class ServiceAccount(apiKey: String, permissions: Set[String]) extends AuthContext
val mixedRoutes = AuthedRoutes.of[AuthContext, IO] {
case GET -> Root / "user-info" as RegularUser(id, name) =>
Ok(s"User: $name (ID: $id)")
case GET -> Root / "user-info" as ServiceAccount(apiKey, _) =>
Ok(s"Service account: $apiKey")
case POST -> Root / "admin" / "action" as ServiceAccount(_, permissions)
if permissions.contains("admin") =>
Ok("Admin action executed")
case POST -> Root / "admin" / "action" as _ =>
Forbidden("Insufficient permissions")
}Use the extractor in conditional logic:
val conditionalRoutes = AuthedRoutes.of[User, IO] {
case GET -> Root / "posts" as user =>
// Access user's own posts and public posts
getVisiblePosts(user.id).flatMap { posts =>
Ok(posts.asJson)
}
case GET -> Root / "posts" / IntVar(postId) as user =>
getPost(postId).flatMap {
case Some(post) if post.authorId == user.id || post.isPublic =>
Ok(post.asJson)
case Some(_) =>
Forbidden("Access denied to private post")
case None =>
NotFound("Post not found")
}
case req @ PUT -> Root / "posts" / IntVar(postId) as user =>
for {
updateData <- req.req.as[PostUpdate]
post <- getPost(postId)
response <- post match {
case Some(p) if p.authorId == user.id =>
updatePost(postId, updateData).flatMap(_ => Ok("Post updated"))
case Some(_) =>
Forbidden("Can only update your own posts").pure[IO]
case None =>
NotFound("Post not found").pure[IO]
}
} yield response
}Access the underlying request for additional processing:
val enhancedRoutes = AuthedRoutes.of[User, IO] {
case req @ GET -> Root / "secure-data" as user =>
val underlying = req.req // Access underlying Request[IO]
// Check additional request properties
val clientIP = underlying.remoteAddr.getOrElse("unknown")
val userAgent = underlying.headers.get[`User-Agent`].map(_.value).getOrElse("unknown")
// Log authentication event
logAccess(user.id, clientIP, userAgent) *>
Ok(s"Secure data for ${user.name}")
case req @ POST -> Root / "upload" as user =>
// Access request body and user context
req.req.decode[Multipart[IO]] { multipart =>
processUpload(multipart, user.id).flatMap { result =>
Created(s"Upload processed for user ${user.name}")
}
}
}Implement role-based access patterns:
case class User(id: Int, name: String, roles: Set[String]) {
def hasRole(role: String): Boolean = roles.contains(role)
def hasAnyRole(requiredRoles: String*): Boolean = requiredRoles.exists(roles.contains)
}
val rbacRoutes = AuthedRoutes.of[User, IO] {
// Admin-only routes
case GET -> Root / "admin" / "users" as user if user.hasRole("admin") =>
getAllUsers().flatMap(users => Ok(users.asJson))
// Manager or admin routes
case GET -> Root / "reports" as user if user.hasAnyRole("manager", "admin") =>
getReports().flatMap(reports => Ok(reports.asJson))
// User can access own data or admin can access any user's data
case GET -> Root / "users" / IntVar(userId) / "data" as user =>
if (user.id == userId || user.hasRole("admin")) {
getUserData(userId).flatMap(data => Ok(data.asJson))
} else {
Forbidden("Access denied")
}
// Default fallback for insufficient permissions
case _ as user =>
Forbidden(s"User ${user.name} does not have sufficient permissions")
}Handle authentication-related errors gracefully:
val errorHandlingRoutes = AuthedRoutes.of[User, IO] {
case req @ POST -> Root / "sensitive-action" as user =>
performSensitiveAction(user.id).attempt.flatMap {
case Right(result) =>
Ok(result.asJson)
case Left(error) => error match {
case _: UnauthorizedException =>
Unauthorized("Authentication expired")
case _: ForbiddenException =>
Forbidden("Insufficient privileges")
case _ =>
InternalServerError("An error occurred")
}
}
}
// Combine with regular routes using middleware
val allRoutes = authMiddleware(errorHandlingRoutes) <+> publicRoutes