← Back to DevBytes

Hono Authentication: JWT, Sessions, and OAuth Integration

Introduction to Hono Authentication

Authentication is the foundation of secure web applications. It verifies user identity and grants access to protected resources. Hono, the fast, lightweight web framework for the edge, provides a rich set of built-in utilities and middleware to implement authentication seamlessly. Whether you’re building a REST API, a server-rendered app, or a full‑stack service, Hono’s authentication ecosystem covers everything from JSON Web Tokens (JWT) and session‑based cookies to third‑party OAuth providers.

In this tutorial, you’ll learn how to integrate three common authentication strategies into a Hono project: JWT tokens, session‑based authentication, and OAuth with GitHub. Each section includes complete, runnable code examples and best practices to keep your application secure.

Why Authentication Matters in Hono Apps

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Without authentication, any client can access your endpoints, leading to data breaches, unauthorized actions, and a poor user experience. Authentication enables:

Hono’s middleware‑based architecture makes it easy to plug authentication into any request pipeline without bloating your application.

Core Concepts: JWT vs Sessions vs OAuth

Before diving into code, let’s clarify the three approaches:

Hono offers dedicated middleware for all three, allowing you to mix and match as needed.

Setting Up a Hono Project

Start by creating a new Hono project and installing the required packages. You’ll need the core Hono library, the JWT helper, the session middleware, and optionally an OAuth provider package.

# Create a new Hono project (choose your preferred runtime)
npm create hono@latest my-auth-app
cd my-auth-app

# Install dependencies
npm install hono
npm install @hono/jwt
npm install @hono/session
npm install @hono/oauth-providers
# Additional utilities
npm install cookie hmac_sha1

The examples below assume a basic Hono server running on http://localhost:3000. Create an src/index.ts file (or index.js if using JavaScript) with the following skeleton:

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hono Auth Tutorial'))

export default app

Now let’s add authentication layer by layer.

JWT Authentication with Hono

JSON Web Tokens are perfect for stateless APIs and microservices. Hono’s @hono/jwt package provides helper functions to create, sign, and verify JWTs, as well as a middleware that automatically rejects requests with invalid tokens.

Creating and Signing JWTs

Typically, you issue a JWT when a user logs in successfully. Use the sign function from hono/jwt/sign to create a token with a payload and a secret.

import { sign } from 'hono/jwt/sign'
import { env } from 'hono/adapter'  // for accessing environment variables

app.post('/login', async (c) => {
  const { username, password } = await c.req.json()
  
  // Validate credentials (example: check against a database)
  if (username !== 'admin' || password !== 'secret') {
    return c.text('Invalid credentials', 401)
  }

  const secret = env(c).JWT_SECRET || 'my-secret-key'
  const payload = {
    sub: username,                // subject
    role: 'admin',
    exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour
  }

  const token = await sign(payload, secret)
  return c.json({ token })
})

Here, sub identifies the user, role carries authorization info, and exp is the expiration timestamp. The token is sent back to the client.

Verifying JWTs in Middleware

To protect routes, use the jwt middleware from hono/jwt. It checks the Authorization header (Bearer token) and verifies the signature.

import { jwt } from 'hono/jwt'

// Protect all routes under /api
app.use('/api/*', jwt({
  secret: env(c).JWT_SECRET || 'my-secret-key',
}))

If the token is missing, expired, or invalid, Hono automatically returns a 401 response with { message: "Unauthorized" }. You don’t need to write any extra logic.

Protecting Routes and Extracting Payload

Once the JWT middleware passes, the decoded payload is available via c.get('jwtPayload'). You can use it to personalize responses or enforce role‑based access.

app.get('/api/profile', jwt({ secret: env(c).JWT_SECRET }), (c) => {
  const payload = c.get('jwtPayload')
  return c.json({
    message: `Hello, ${payload.sub}!`,
    role: payload.role,
    expiresAt: new Date(payload.exp * 1000).toISOString()
  })
})

// Role-based access control
app.get('/api/admin', jwt({ secret: env(c).JWT_SECRET }), (c) => {
  const payload = c.get('jwtPayload')
  if (payload.role !== 'admin') {
    return c.text('Forbidden', 403)
  }
  return c.json({ adminData: 'sensitive info' })
})

Notice how cleanly the middleware separates concerns. The route handler only runs after successful authentication.

JWT Refresh Flow (Bonus)

For long‑lived sessions, implement a refresh token mechanism. Store refresh tokens securely (HttpOnly cookie) and issue short‑lived access tokens.

// Simplified refresh endpoint
app.post('/refresh', async (c) => {
  const refreshToken = c.req.header('X-Refresh-Token')
  // Verify refresh token (e.g., check in DB or decode)
  const newAccessToken = await sign(
    { sub: 'admin', role: 'admin', exp: Math.floor(Date.now()/1000) + 900 },
    env(c).JWT_SECRET
  )
  return c.json({ token: newAccessToken })
})

Session-Based Authentication

Sessions keep the user state on the server, identified by a session ID stored in a cookie. Hono’s @hono/session middleware manages sessions effortlessly, including cookie signing, session store (memory or custom), and helper methods on the context.

Configuring Session Middleware

Apply the session middleware globally (or to specific routes). It adds a session object to the context.

import { sessionMiddleware } from 'hono/session'
import { env } from 'hono/adapter'

app.use(sessionMiddleware({
  cookieOptions: {
    httpOnly: true,          // prevent client‑side script access
    secure: true,            // only send over HTTPS (set false for local dev)
    sameSite: 'Lax',         // CSRF protection
    path: '/',
    maxAge: 86400,           // 24 hours (in seconds)
  },
  secret: env(c).SESSION_SECRET || 'session-secret-change-me',
  // Optional: use a custom store (e.g., Redis). Default is in‑memory.
}))

The secret is used to sign the session cookie, preventing tampering. Always use a strong, unique secret from environment variables.

Login and Logout Routes

During login, validate credentials, then store user information in the session. Hono provides c.session.put(key, value) and c.session.deleteSession().

app.post('/session-login', async (c) => {
  const { username, password } = await c.req.json()
  
  // Authenticate user (replace with real lookup + bcrypt compare)
  if (username !== 'user1' || password !== 'pass123') {
    return c.text('Invalid credentials', 401)
  }

  // Regenerate session ID to prevent fixation
  c.session.regenerate()

  // Set session values
  c.session.put('userId', username)
  c.session.put('role', 'member')
  c.session.put('loginTime', Date.now())

  return c.json({ message: 'Logged in successfully' })
})

app.post('/session-logout', async (c) => {
  c.session.deleteSession()
  return c.json({ message: 'Logged out' })
})

The regenerate() call assigns a fresh session ID, an important security measure after privilege changes.

Protecting Routes with Session Check

Write a small helper middleware or simply check c.session.get() inside your handler.

// Reusable authentication guard middleware
const sessionGuard = async (c, next) => {
  if (!c.session.get('userId')) {
    return c.text('Please log in', 401)
  }
  await next()
}

app.get('/dashboard', sessionGuard, (c) => {
  const userId = c.session.get('userId')
  return c.html(`<h1>Welcome back, ${userId}!</h1>`)
})

// Route that reads additional session data
app.get('/my-session', sessionGuard, (c) => {
  const data = {
    userId: c.session.get('userId'),
    role: c.session.get('role'),
    loginTime: new Date(c.session.get('loginTime')).toISOString()
  }
  return c.json(data)
})

Sessions are automatically persisted and the cookie is set on responses. On the next request, Hono decodes the cookie and populates c.session.

Using External Session Stores (Redis Example)

For production, replace the in‑memory store with something scalable like Redis. Install ioredis and configure the session store.

import Redis from 'ioredis'
import { SessionStore } from '@hono/session'

const redis = new Redis(env(c).REDIS_URL)

class RedisStore implements SessionStore {
  async get(id: string) {
    const data = await redis.get(`session:${id}`)
    return data ? JSON.parse(data) : null
  }
  async set(id: string, value: object, maxAge: number) {
    await redis.set(`session:${id}`, JSON.stringify(value), 'EX', maxAge)
  }
  async destroy(id: string) {
    await redis.del(`session:${id}`)
  }
}

app.use(sessionMiddleware({
  store: new RedisStore(),
  cookieOptions: { httpOnly: true, secure: true, sameSite: 'Lax' },
  secret: env(c).SESSION_SECRET,
}))

OAuth Integration (GitHub Example)

OAuth 2.0 lets users sign in with their existing GitHub (or other provider) accounts, reducing friction and improving security because you never handle passwords. Hono’s @hono/oauth-providers package provides ready‑to‑use handlers for many providers.

Registering a GitHub OAuth App

Before coding, create an OAuth app on GitHub: go to Settings → Developer settings → OAuth Apps. Register a new application with:

You’ll receive a Client ID and a Client Secret. Store them in environment variables.

Setting Up the OAuth Middleware

Import the GitHub provider and mount it on a path. It automatically handles the redirect to GitHub’s authorization page.

import { githubOAuth } from 'hono/oauth-providers/github'
import { env } from 'hono/adapter'

app.use('/auth/github/*', githubOAuth({
  client_id: env(c).GITHUB_CLIENT_ID,
  client_secret: env(c).GITHUB_CLIENT_SECRET,
  redirect_uri: 'http://localhost:3000/auth/github/callback',
  scope: ['user:email', 'read:user'],  // requested permissions
}))

Visiting /auth/github/login will now redirect the user to GitHub. After authorization, GitHub redirects back to the callback URL with a code.

Handling the Callback and Fetching User Info

Define the callback route where GitHub sends the authorization code. Use githubOAuth.callback(c) to exchange the code for an access token, then fetch the user’s profile.

app.get('/auth/github/callback', async (c) => {
  const tokens = await githubOAuth.callback(c)
  if (!tokens.access_token) {
    return c.text('OAuth failed', 400)
  }

  // Fetch user info from GitHub API
  const userResponse = await fetch('https://api.github.com/user', {
    headers: {
      Authorization: `token ${tokens.access_token}`,
      Accept: 'application/json',
    },
  })
  const githubUser = await userResponse.json()

  // Now you have user data (login, avatar_url, email, etc.)
  // Create a local session or issue a JWT
  c.session.regenerate()
  c.session.put('userId', githubUser.login)
  c.session.put('avatar', githubUser.avatar_url)
  c.session.put('provider', 'github')

  return c.redirect('/dashboard')
})

After obtaining the user identity, you can either persist a session (as above) or issue a JWT and send it to the client. The choice depends on your architecture.

OAuth with Other Providers

Hono’s @hono/oauth-providers includes Google, Twitter, Discord, and more. The pattern is identical: configure with client_id and client_secret, handle the callback, and fetch user info from the provider’s API.

Best Practices for Secure Hono Authentication

Conclusion

Hono’s authentication capabilities cover the full spectrum from simple JWT verification to full‑blown OAuth integration. By leveraging the framework’s built‑in middleware and following security best practices, you can build robust, scalable authentication layers with minimal code. Whether you choose stateless JWTs for microservices, session‑based cookies for traditional server‑rendered apps, or OAuth for social login, Hono keeps your implementation clean and performant. The patterns shown here give you a solid foundation—now adapt them to your specific needs, and always keep security at the forefront of your design.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles