Cookies
Cookies are small pieces of data that websites store on a user's browser. They are sent between the client (browser) and server in HTTP requests and responses. Cookies serve various purposes, including session management, user authentication, personalization, and tracking.
When a user visits a website, the server can send one or more cookies to the browser, which stores them locally. The browser then includes these cookies in subsequent requests to the same website, allowing the server to retrieve and utilize the stored information.
In ZIO HTTP, cookies are represented by the Cookie data type, which encompasses both request cookies and response cookies:
We can think of a Cookie as an immutable and type-safe representation of HTTP cookies that contains the name, content:
sealed trait Cookie {
def name: String
def content: String
}
object Cookie {
case class Request(name: String, content: String) extends Cookie { self =>
// Request Cookie methods
}
case class Response(
name: String,
content: String,
domain: Option[String] = None,
path: Option[Path] = None,
isSecure: Boolean = false,
isHttpOnly: Boolean = false,
maxAge: Option[Duration] = None,
sameSite: Option[SameSite] = None,
) extends Cookie { self =>
// Response Cookie methods
}
}
Request cookies (Cookie.Request) are sent by the client to the server, while response cookies (Cookie.Response) are sent by the server to the client.
Response Cookie
Creating a Response Cookie
A Response Cookie can be created with params name, content, expires, domain, path, isSecure, isHttpOnly, maxAge, sameSite and secret according to HTTP Set-Cookie
import zio._
import zio.http._
val responseCookie = Cookie.Response("user_id", "user123", maxAge = Some(5.days))
// responseCookie: Cookie.Response = Response(
// name = "user_id",
// content = "user123",
// domain = None,
// path = None,
// isSecure = false,
// isHttpOnly = false,
// maxAge = Some(value = PT120H),
// sameSite = None
// )
Adding Cookie in a Response
The cookies can be added in Response headers:
val res = Response.ok.addCookie(responseCookie)
It updates the response header Set-Cookie as Set-Cookie: <cookie-name>=<cookie-value>
By adding the above cookie to a Response, it will add a Set-Cookie header with the respective cookie name and value and other optional attributes.
Let's write a simple example to see how it works:
import zio.http._
object ResponseCookieExample extends ZIOAppDefault {
val routes = Routes(
Method.GET / "cookie" -> handler {
Response.ok.addCookie(
Cookie.Response(name = "user_id", content = "user123", maxAge = Some(5.days))
)
},
)
def run = Server.serve(routes).provide(Server.default)
}
When we call the /cookie endpoint, it will return a response with a Set-Cookie header:
~> curl -X GET http://127.0.0.1:8080/cookie -i
HTTP/1.1 200 OK
set-cookie: user_id=user123; Max-Age=432000; Expires=Fri, 08 Mar 2024 10:41:52 GMT
content-length: 0
To convert a request cookie to a response cookie, use the toResponse method:
import zio.http._
val requestCookie = Cookie.Request("id", "abc")
val responseCookie = requestCookie.toResponse
Updating a Response Cookie
Cookie.Response is a case class, so it can be updated by its copy method:
maxAgeupdates the max-age of the cookie:
responseCookie.copy(maxAge = Some(5.days))
domainupdates the host to which the cookie will be sent:
responseCookie.copy(domain = Some("example.com"))
pathupdates the path of the cookie:
responseCookie.copy(path = Some(Path.root / "cookie"))
isSecureenables cookie only on https server:
responseCookie.copy(isSecure = true)
isHttpOnlyforbids JavaScript from accessing the cookie:
responseCookie.copy(isHttpOnly = true)
sameSiteupdates whether or not a cookie is sent with cross-origin requests:
responseCookie.copy(sameSite = Some(Cookie.SameSite.Strict))
Signing a Cookie
Signing a cookie involves appending a cryptographic signature to the cookie data before it is transmitted to the client. This signature is generated using a secret key known only to the server. When the client sends the cookie back to the server in subsequent requests, the server can verify the signature to ensure the integrity and authenticity of the cookie data.
The cookies can be signed with a signature:
- Using
Response#sign:
val cookie = Cookie.Response("key", "hello", maxAge = Some(5.days))
val app =
Routes(
Method.GET / "cookie" -> handler {
Response.ok.addCookie(cookie.sign("secret"))
}
)
- Using
signCookiesmiddleware:
To sign all the cookies in your routes, we can use signCookies middleware:
import Middleware.signCookies
val app = Routes(
Method.GET / "cookie" -> handler(Response.ok.addCookie(cookie)),
Method.GET / "secure-cookie" -> handler(Response.ok.addCookie(cookie.copy(isSecure = true)))
)
// Run it like any simple app
def run(args: List[String]): ZIO[Any, Throwable, Nothing] =
Server.serve(app @@ signCookies("secret"))
.provide(Server.default)
Request Cookie
Creating a Request Cookie
A request cookie consists of name and content and can be created with Cookie.Request:
val cookie: Cookie = Cookie.Request("user_id", "user123")
// cookie: Cookie = Request(name = "user_id", content = "user123")
Updating a Request Cookie
The Cookie#name method updates the name of cookie:
cookie.name("session_id")
// res8: Cookie = Request(name = "session_id", content = "user123")
The Cookie#content method updates the content of the cookie:
cookie.content("abc123xyz789")
// res9: Cookie = Request(name = "user_id", content = "abc123xyz789")
Getting Cookie from a Request
From HTTP requests, a single cookie can be retrieved with Request#cookie:
private val app4 =
Routes(
Method.GET / "cookie" -> handler { (req: Request) =>
val cookieContent = req.cookie("sessionId").map(_.content)
Response.text(s"cookie content: $cookieContent")
}
)
Getting Cookie from a Header
In HTTP requests, cookies are stored in the Header.cookie header:
private val app3 =
Routes(
Method.GET / "cookie" -> handler { (req: Request) =>
Response.text(
req.header(Header.Cookie)
.map(_.value.toChunk)
.getOrElse(Chunk.empty)
.mkString("")
)
}
)
Examples
Here are some simple examples of using cookies in a ZIO HTTP application.
Server Side Example
//> using dep "dev.zio::zio-http:3.4.0"
package example
import zio._
import zio.http._
/**
* Example to make app using cookies
*/
object CookieServerSide extends ZIOAppDefault {
// Setting cookies with an expiry of 5 days
private val cookie = Cookie.Response("key", "value", maxAge = Some(5 days))
val res = Response.ok.addCookie(cookie)
private val app = Routes(
Method.GET / "cookie" ->
handler(Response.ok.addCookie(cookie.copy(path = Some(Path.root / "cookie"), isHttpOnly = true))),
Method.GET / "secure-cookie" ->
handler(Response.ok.addCookie(cookie.copy(isSecure = true, path = Some(Path.root / "secure-cookie")))),
Method.GET / "cookie" / "remove" ->
handler(res.addCookie(Cookie.clear("key"))),
)
// Run it like any simple app
val run =
Server.serve(app).provide(Server.default)
}
Signed Cookies
//> using dep "dev.zio::zio-http:3.4.0"
package example
import zio._
import zio.http._
/**
* Example to make app using signed-cookies
*/
object SignCookies extends ZIOAppDefault {
// Setting cookies with an expiry of 5 days
private val cookie = Cookie.Response("key", "hello", maxAge = Some(5 days))
private val app = Routes(
Method.GET / "cookie" ->
handler(Response.ok.addCookie(cookie.sign("secret"))),
)
// Run it like any simple app
val run = Server.serve(app).provide(Server.default)
}