Signed Cookies
Learning objectives
- You know what signed cookies are and how to use them.
By default, as we've seen, the cookies are sent as text. This means that we, as the user, can adjust the cookies in any way we want. This is not a problem if the cookie is used to store information about the user's preferences, but it is a problem if the cookie is used to store information about the user's identity. If the cookie is used to store information about the user's identity, then the user can impersonate another user by changing the cookie value.
One to the problem of the user changing the cookie content is to create an encrypted signature of the cookie data, and passing that along with the cookie. When the user responds with the cookie (and the encrypted signature), the server can check whether the signature is valid. If the signature is valid, then the server can trust the cookie data. If the signature is not valid, then the server can discard the cookie data.
With Hono, we can use the getSignedCookie
and setSignedCookie
functions to create and read encrypted cookies. The getSignedCookie
function takes a Context
object, a secret, and a cookie name as arguments, and returns the cookie value. The setSignedCookie
function takes a Context
object, a cookie name, a cookie value, and a secret as arguments, signs the cookie, and sets the signed cookie on the Context
object. Both functions are asynchronous, and need to be used with the await
keyword.
The below application is an implementation of the earlier count application using the getSignedCookie
and setSignedCookie
functions. The secret is set to secret
in the example, but in a real application, the secret should be a long, random string.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
import {
getSignedCookie,
setSignedCookie,
} from "https://deno.land/x/hono@v3.12.11/helper.ts";
const app = new Hono();
const secret = "secret";
app.get("/", async (c) => {
let count = await getSignedCookie(c, secret, "count") ?? 0;
count = Number(count) + 1;
await setSignedCookie(c, "count", `${count}`, secret, {
path: "/",
});
return c.text(`Hello cookies! -- ${count}`);
});
Deno.serve(app.fetch);
Now, when we make a request to the server, we can see that the cookie value is encrypted.
curl -v localhost:8000
...
< HTTP/1.1 200 OK
< set-cookie: count=1.vSjuFCyltGJZ9uJ%2Fw6Qhb0R71YQ8QG5jIZz%2FMOc7E1s%3D
...
Hello cookies -- 1!
Similar to earlier, we can use the Cookie
header to send the cookie back to the server.
curl -v -H "Cookie: count=1.vSjuFCyltGJZ9uJ%2Fw6Qhb0R71YQ8QG5jIZz%2FMOc7E1s%3D" localhost:8000
...
< HTTP/1.1 200 OK
< set-cookie: count=2.G0bBon7tcv7PLnbyKfn%2F1H0yfj75oTrTD0a9JH4H%2B%2B4%3D
...
Hello cookies -- 2!
curl -v -H "Cookie: count=2.G0bBon7tcv7PLnbyKfn%2F1H0yfj75oTrTD0a9JH4H%2B%2B4%3D" localhost:8000
...
< HTTP/1.1 200 OK
< set-cookie: count=3.iKQ7G4720ukAeXNj2iH9K7KTUdMCI4n%2Bsx%2BxNAqU2uI%3D
...
Hello cookies -- 3!
If we would try to change the cookie value, we would see that the server would not accept the cookie and the count would start from zero.
curl -v -H "Cookie: count=42.G0bBon7tcv7PLnbyKfn%2F1H0yfj75oTrTD0a9JH4H%2B%2B4%3D" localhost:8000
...
< HTTP/1.1 200 OK
< set-cookie: count=1.vSjuFCyltGJZ9uJ%2Fw6Qhb0R71YQ8QG5jIZz%2FMOc7E1s%3D
...
Hello cookies -- 1!
Path and setSignedCookie
If you wonder why we added the path: "/"
option to the setSignedCookie
function, it is because the default path for cookies is the path of the request. This means that if we would not have added the path: '/'
option, the cookie would only be sent to the server if the request path would be /
. If the request path would be /foo
, then the cookie would not be sent to the server.