Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Sign-in with JWT

Right now anyone can create or delete a post. APIs usually rely on JWT — a signed token, a string in which the server records who the user is and adds a signature made with a secret key. The client receives the token on sign-in and sends it with every request; the server checks the signature and keeps no sessions.

The auth module

The secret comes from the JWT_SECRET variable in the .env file. Create src/auth.ts:

// src/auth.ts
import { jwt } from 'hono/jwt'
 
const secret = process.env.JWT_SECRET
if (!secret) {
  throw new Error('JWT_SECRET is not set')
}
 
export const jwtSecret = secret
export const requireAuth = jwt({ secret, alg: 'HS256' })

If the variable is missing, the server refuses to start and says so. requireAuth is middleware that lets a request through only with a valid token. HS256 is the algorithm that signs with a shared secret.

Issuing a token

Create src/routes/auth.ts with a sign-in route. For simplicity the admin password also lives in .env as ADMIN_PASSWORD; in a real project users are stored in the database with password hashes:

// src/routes/auth.ts
import { Hono } from 'hono'
import { sign } from 'hono/jwt'
import { jwtSecret } from '../auth.js'
 
const auth = new Hono()
 
auth.post('/login', async (c) => {
  const { username, password } = await c.req.json<{ username: string; password: string }>()
 
  if (username !== 'anna' || password !== process.env.ADMIN_PASSWORD) {
    return c.json({ error: 'Wrong username or password' }, 401)
  }
 
  const token = await sign(
    { sub: username, exp: Math.floor(Date.now() / 1000) + 60 * 60 },
    jwtSecret,
  )
  return c.json({ token })
})
 
export default auth

Mount the group in src/index.ts: app.route('/auth', auth). The exp field is the token's expiry time in seconds: after an hour the token is no longer accepted.

Protecting routes

Add requireAuth to the routes that change data in src/routes/posts.ts:

// src/routes/posts.ts
import { requireAuth } from '../auth.js'
 
posts.post('/', requireAuth, zValidator('json', postSchema), (c) => {
  const user = c.get('jwtPayload')
  console.log(`Post created by ${user.sub}`)
  // ...create the post as in the previous lesson
  return c.json({ author: user.sub }, 201)
})

Do the same for put and delete. c.get('jwtPayload') returns the data from the token. Reading the list stays open.

Checking

curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "anna", "password": "password-from-env"}'

The response is {"token":"eyJ..."}. A request without the token gets 401; with the token it goes through:

curl -X POST http://localhost:3000/posts \
  -H "Authorization: Bearer eyJ..." \
  -H "Content-Type: application/json" \
  -d '{"title": "A protected post", "body": "Text"}'

Replace eyJ... with the whole token you received.

Common mistake

Putting secret data into the token, such as a password or an address. The signature protects the token from forgery but does not hide its content: any part of it can be read by decoding base64. Keep only the user id, the role and the expiry time in the token.

Thanks for reading!

Donate to the site — we work hard for you.