Validation and errors
Use cookies and authorization safely
Set restrictive cookies and authorize resource access on the server for every protected operation.
Cookie helpers parse and serialize headers. They do not replace session design, CSRF protection, or ownership checks on the server.
Login sets an opaque session cookie with restrictive attributes:
import { setCookie, getCookie } from 'hono/cookie'
app.post('/session', async (c) => {
const sessionId = await sessions.create('user_42')
setCookie(c, 'session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'Lax',
path: '/',
maxAge: 60 * 60 * 24 * 7
})
return c.body(null, 204)
})
Update and delete check the bookmark owner on every request, not just at login:
app.delete('/bookmarks/:id', async (c) => {
const sessionId = getCookie(c, 'session')
const user = await sessions.findUser(sessionId)
if (!user) {
return c.json({ error: 'Unauthorized' }, 401)
}
const bookmark = await bookmarks.find(c.req.param('id'))
if (!bookmark || bookmark.userId !== user.id) {
return c.json({ error: 'Not found' }, 404)
}
await bookmarks.remove(bookmark.id)
return c.body(null, 204)
})
Create a session, then delete with curl:
curl -s -c /tmp/jar -X POST http://localhost:3000/session
curl -s -b /tmp/jar -X DELETE http://localhost:3000/bookmarks/bk_01 -w '\n%{http_code}\n'
With a valid session and owned bookmark you get 204. Without the cookie:
curl -s -X DELETE http://localhost:3000/bookmarks/bk_01 -w '\n%{http_code}\n'
You should see 401 and {"error":"Unauthorized"}.
A realistic failure on Workers: you set secure: false because localhost dev worked without HTTPS. Production runs on HTTPS and the browser drops the cookie, so every mutating request returns 401. Fix by setting secure: true in production or toggling from env while keeping httpOnly and sameSite strict.
Never store trusted roles in a JavaScript-readable cookie. Keep authority server-side.
Return 404 instead of 403 when a user probes another owner’s bookmark id. That avoids leaking whether the resource exists.
Logout clears the cookie with deleteCookie(c, 'session', { path: '/' }) and invalidates the server record.
Vitest can simulate cookies with a helper that sets Cookie on app.request(). Assert 401 without it and 204 with a valid session jar entry.
Cross-site POST tests belong in the suite too. A form post from another origin should not delete a bookmark when CSRF protection is enabled.
Another user’s bookmark should look like a missing resource:
curl -s -b /tmp/jar -X DELETE http://localhost:3000/bookmarks/other-users-id -w '\n%{http_code}\n'
Expect 404 {"error":"Not found"}, not 403 that confirms the id exists.
Session cookies are opaque keys, not JWTs with embedded roles. Parse the session server-side on every mutating route.
Login over HTTP in local dev may omit secure: true; production on HTTPS must set it or browsers drop the cookie silently.
Try this on your own project: grep for getCookie without a matching ownership check on mutating routes.
Lesson completed