Cookies and a simple session
A cookie is a small record the server asks the browser to keep. The browser sends it back by itself with every request to the same site. For HTML pages this is more convenient than a JWT in a header: after sign-in the browser remembers the user without extra code on the page. In Hono the cookie functions live in hono/cookie.
Setting and reading a cookie
// src/routes/pages.tsx
import { getCookie, setCookie } from 'hono/cookie'
pages.get('/theme/:name', (c) => {
setCookie(c, 'theme', c.req.param('name'), { path: '/', maxAge: 60 * 60 * 24 * 30 })
return c.redirect('/')
})
pages.get('/theme', (c) => {
const theme = getCookie(c, 'theme') ?? 'light'
return c.text(`Theme: ${theme}`)
})Open /theme/dark, then /theme — you see "Theme: dark". The cookie lives for 30 days: maxAge is in seconds.
Cookie options
| Option | What it does |
|---|---|
path: '/' | the cookie is sent for every address on the site |
maxAge | lifetime in seconds |
httpOnly: true | JavaScript on the page cannot read the cookie |
secure: true | the cookie is sent only over HTTPS |
sameSite: 'Lax' | not sent with requests from other sites, except link clicks |
A signed cookie as a session
A user can change a plain cookie in the browser. A signed cookie stores a signature made with a secret key next to the value: the server will not accept a forgery. That is enough for a simple sign-in:
// src/routes/pages.tsx
import { deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie'
import { jwtSecret } from '../auth.js'
pages.get('/login', (c) => {
return c.render(
<form method="post" action="/login">
<input name="username" placeholder="Username" />
<input name="password" type="password" placeholder="Password" />
<button>Sign in</button>
</form>,
{ title: 'Sign in' },
)
})
pages.post('/login', async (c) => {
const body = await c.req.parseBody()
if (body.username !== 'anna' || body.password !== process.env.ADMIN_PASSWORD) {
return c.redirect('/login')
}
await setSignedCookie(c, 'user', 'anna', jwtSecret, {
path: '/',
httpOnly: true,
sameSite: 'Lax',
maxAge: 60 * 60 * 8,
})
return c.redirect('/admin')
})
pages.get('/admin', async (c) => {
const user = await getSignedCookie(c, jwtSecret, 'user')
if (!user) {
return c.redirect('/login')
}
return c.render(<h1>Hello, {user}</h1>, { title: 'Panel' })
})
pages.post('/logout', (c) => {
deleteCookie(c, 'user', { path: '/' })
return c.redirect('/')
})The /login form sends the username and password, the server sets the signed user cookie and redirects to /admin. There getSignedCookie() returns the value, or false if the cookie is missing or the signature does not match. For simplicity the same secret as for JWT is used here; in a real project it is better to have a separate one.
Server-side sessions
Hono does not store sessions itself. If you need to keep more data on the server — a cart, settings — you create a sessions table, put only a random id in the cookie and look up the record in the database by it.
Common mistake
Setting the sign-in cookie without httpOnly: true. Then any script on the page, including one injected by an attacker, can read it and send it to a foreign server. Always mark cookies that only the server needs as httpOnly, and add secure: true on a site with HTTPS.