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:
maxAge
updates the max-age of the cookie:
responseCookie.copy(maxAge = Some(5.days))
domain
updates the host to which the cookie will be sent:
responseCookie.copy(domain = Some("example.com"))
path
updates the path of the cookie:
responseCookie.copy(path = Some(Path.root / "cookie"))
isSecure
enables cookie only on https server:
responseCookie.copy(isSecure = true)
isHttpOnly
forbids JavaScript from accessing the cookie:
responseCookie.copy(isHttpOnly = true)
sameSite
updates 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
signCookies
middleware:
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
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
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)
}