Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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.

// 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.

OptionWhat it does
path: '/'the cookie is sent for every address on the site
maxAgelifetime in seconds
httpOnly: trueJavaScript on the page cannot read the cookie
secure: truethe cookie is sent only over HTTPS
sameSite: 'Lax'not sent with requests from other sites, except link clicks

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.

Thanks for reading!

Donate to the site — we work hard for you.