2025-06-14 04:48:00 +08:00
# Stytch Auth Middleware for Hono
[](https://codecov.io/github/honojs/middleware)
A third-party [Stytch ](https://stytch.com ) authentication middleware for [Hono ](https://github.com/honojs/hono ). Supports both [Consumer ](https://stytch.com/b2c ) and [B2B ](https://stytch.com/b2b ) authentication with flexible configuration options.
> 💡 This package works with [Stytch Frontend SDKs](https://stytch.com/docs/guides/sessions/frontend-guide) and validates sessions they create. By default, it reads JWTs from the `stytch_session_jwt` cookie. See the [Session JWTs guide](https://stytch.com/docs/guides/sessions/using-jwts) for more details.
## Quick Start
The fastest way to get started with Consumer authentication:
```ts
import { Hono } from 'hono'
import { Consumer } from '@hono/stytch-auth'
const app = new Hono()
// Authenticate all routes
app.use('*', Consumer.authenticateSessionLocal())
app.get('/', (c) => {
const session = Consumer.getStytchSession(c)
return c.json({ message: `Hello ${session.user_id}!` })
})
export default app
```
## Installation
```bash
npm install hono @hono/stytch -auth stytch
```
## Configuration
Set these environment variables before using the middleware:
```bash
STYTCH_PROJECT_ID=project-live-xxx-xxx-xxx
STYTCH_PROJECT_SECRET=secret-live-xxx-xxx-xxx
```
## Table of Contents
- [Consumer Authentication ](#consumer-authentication )
- [Basic Session Auth ](#basic-consumer-session-auth )
- [OAuth Bearer Token Auth ](#consumer-oauth-auth )
- [B2B Authentication ](#b2b-authentication )
- [Basic Session Auth ](#basic-b2b-session-auth )
- [OAuth Bearer Token Auth ](#b2b-oauth-auth )
- [API Reference ](#api-reference )
- [Advanced Usage ](#advanced-usage )
- [Custom Configuration ](#custom-configuration )
- [Error Handling ](#custom-error-handling )
- [Troubleshooting ](#troubleshooting )
## Consumer Authentication
### Basic Consumer Session Auth
**Local Authentication** (JWT validation only - fastest):
```ts
import { Consumer } from '@hono/stytch-auth'
// Validates JWT locally using cached JWKS
app.use('*', Consumer.authenticateSessionLocal())
app.get('/profile', (c) => {
const session = Consumer.getStytchSession(c)
return c.json({ sessionId: session.session_id })
})
```
**Remote Authentication** (validates with Stytch servers - more data):
```ts
import { Consumer } from '@hono/stytch-auth'
// Always calls Stytch servers, returns user data
app.use('*', Consumer.authenticateSessionRemote())
app.get('/profile', (c) => {
const session = Consumer.getStytchSession(c)
const user = Consumer.getStytchUser(c)
return c.json({
sessionId: session.session_id,
userId: user.user_id,
})
})
```
### Consumer OAuth Auth
```ts
import { Consumer } from '@hono/stytch-auth'
app.use('*', Consumer.authenticateOAuthToken())
app.get('/api/data', (c) => {
const { claims, token } = Consumer.getOAuthData(c)
return c.json({
subject: claims.subject,
hasValidToken: !!token,
})
})
```
## B2B Authentication
### Basic B2B Session Auth
**Local Authentication:**
```ts
import { B2B } from '@hono/stytch-auth'
app.use('*', B2B.authenticateSessionLocal())
app.get('/dashboard', (c) => {
const session = B2B.getStytchSession(c)
return c.json({
sessionId: session.member_session_id,
orgId: session.organization_id,
})
})
```
**Remote Authentication:**
```ts
import { B2B } from '@hono/stytch-auth'
app.use('*', B2B.authenticateSessionRemote())
app.get('/dashboard', (c) => {
const session = B2B.getStytchSession(c)
const member = B2B.getStytchMember(c)
const organization = B2B.getStytchOrganization(c)
return c.json({
sessionId: session.member_session_id,
memberEmail: member.email_address,
orgName: organization.organization_name,
})
})
```
### B2B OAuth Auth
```ts
import { B2B } from '@hono/stytch-auth'
app.use('*', B2B.authenticateOAuthToken())
app.get('/api/org-data', (c) => {
const { claims, token } = B2B.getOAuthData(c)
return c.json({
subject: claims.subject,
hasValidToken: !!token,
})
})
```
### B2B Organization Access
After B2B remote authentication, you get access to organization data:
```ts
app.use('*', B2B.authenticateSessionRemote())
app.get('/org-settings', (c) => {
const organization = B2B.getStytchOrganization(c)
return c.json({
orgId: organization.organization_id,
orgName: organization.organization_name,
// ... other organization fields
})
})
```
## API Reference
### Consumer Methods
| Method | Description | Returns |
| ------------------------------------------- | ------------------------------- | ------------------------------------------------ |
| `Consumer.getClient(c)` | Get Consumer Stytch client | `Client` |
| `Consumer.authenticateSessionLocal(opts?)` | JWT-only auth middleware | `MiddlewareHandler` |
| `Consumer.authenticateSessionRemote(opts?)` | Remote session auth middleware | `MiddlewareHandler` |
| `Consumer.authenticateOAuthToken(opts?)` | OAuth bearer token middleware | `MiddlewareHandler` |
| `Consumer.getStytchSession(c)` | Get session from context | `Session` |
| `Consumer.getStytchUser(c)` | Get user from context\* | `User` |
| `Consumer.getOAuthData(c)` | Get OAuth data from context\*\* | `{ claims: ConsumerTokenClaims, token: string }` |
\*Only available after `authenticateSessionRemote`
\*\*Only available after `authenticateOAuthToken`
### B2B Methods
| Method | Description | Returns |
| -------------------------------------- | ----------------------------------- | ------------------------------------------- |
| `B2B.getClient(c)` | Get B2B Stytch client | `B2BClient` |
| `B2B.authenticateSessionLocal(opts?)` | JWT-only auth middleware | `MiddlewareHandler` |
| `B2B.authenticateSessionRemote(opts?)` | Remote session auth middleware | `MiddlewareHandler` |
| `B2B.authenticateOAuthToken(opts?)` | B2B OAuth bearer token middleware | `MiddlewareHandler` |
| `B2B.getStytchSession(c)` | Get B2B session from context | `MemberSession` |
| `B2B.getStytchMember(c)` | Get member from context\* | `Member` |
| `B2B.getStytchOrganization(c)` | Get organization from context\* | `Organization` |
| `B2B.getOAuthData(c)` | Get B2B OAuth data from context\*\* | `{ claims: B2BTokenClaims, token: string }` |
\*Only available after `authenticateSessionRemote`
\*\*Only available after `authenticateOAuthToken`
### Configuration Options
#### Session Authentication Options
```ts
{
getCredential?: (c: Context) => { session_jwt: string } | { session_token: string },
maxTokenAgeSeconds?: number, // For local auth only
onError?: (c: Context, error: Error) => Response | void
}
```
#### OAuth Authentication Options
```ts
{
getCredential?: (c: Context) => { access_token: string },
onError?: (c: Context, error: Error) => void
}
```
## Advanced Usage
### Accessing Raw Stytch Clients
For advanced operations, access the underlying Stytch clients:
```ts
// Consumer operations
app.get('/advanced-user-ops', async (c) => {
const stytchClient = Consumer.getClient(c)
const { user } = await stytchClient.users.get({ user_id: 'user-123' })
return c.json({ user })
})
// B2B operations
app.get('/advanced-org-ops', async (c) => {
const stytchClient = B2B.getClient(c)
const { organization } = await stytchClient.organizations.get({
organization_id: 'org-123',
})
return c.json({ organization })
})
```
### Custom Configuration
2025-06-27 05:27:26 +08:00
**Custom Base URL:**
Set `STYTCH_DOMAIN` as an environment variable to instruct the SDK to use your [custom domain ](https://stytch.com/docs/guides/custom-domains/overview ) for all API calls.
```bash
STYTCH_DOMAIN=https://login.example.com
```
2025-06-14 04:48:00 +08:00
**Custom Cookie Name:**
```ts
import { getCookie } from 'hono/cookie'
app.use(
'*',
Consumer.authenticateSessionLocal({
getCredential: (c) => ({
session_jwt: getCookie(c, 'my_session_cookie') ?? '',
}),
})
)
```
**Custom Token Age:**
```ts
app.use(
'*',
Consumer.authenticateSessionLocal({
maxTokenAgeSeconds: 60, // 1 minute
})
)
```
### Custom Error Handling
**Redirect to Login:**
```ts
app.use(
'*',
Consumer.authenticateSessionLocal({
onError: (c, error) => {
return c.redirect('/login')
},
})
)
```
**Custom Error Response:**
```ts
import { HTTPException } from 'hono/http-exception'
app.use(
'*',
Consumer.authenticateOAuthToken({
onError: (c, error) => {
const errorResponse = new Response('Unauthorized', {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer realm="api", error="invalid_token"',
},
})
throw new HTTPException(401, { res: errorResponse })
},
})
)
```
### Multiple Authentication Methods
You can use different auth methods for different routes:
```ts
const app = new Hono()
// Public routes (no auth)
app.get('/health', (c) => c.json({ status: 'ok' }))
// Less-sensitive routes use local authentication against cached JWT / JWKS
app.get('/profile', Consumer.authenticateSessionLocal(), (c) => {
const session = Consumer.getStytchSession(c)
return c.json({ session })
})
// More sensitive routes use remote authentication
app.get('/paymentinfo', Consumer.authenticateSessionRemote(), (c) => {
const member = B2B.getStytchMember(c)
const organization = B2B.getStytchOrganization(c)
return c.json({ member, organization })
})
// OAuth API routes
app.use('/api/*', Consumer.authenticateOAuthToken())
app.get('/api/data', (c) => {
const { claims } = Consumer.getOAuthData(c)
return c.json({ subject: claims.subject })
})
```
### Custom Credential Extraction Examples
**From Authorization Header:**
```ts
getCredential: (c) => ({
session_jwt: c.req.header('Authorization')?.replace('Bearer ', '') ?? '',
})
```
**From Query Parameter:**
```ts
getCredential: (c) => ({
session_jwt: c.req.query('token') ?? '',
})
```
**From Custom Header:**
```ts
getCredential: (c) => ({
access_token: c.req.header('X-API-Token') ?? '',
})
```
## Troubleshooting
### Cloudflare Worker Cache Field issues
```
The 'cache' field on 'RequestInitializerDict' is not implemented.
```
In Cloudflare Workers the worker `compatibility_date` must be set to `2024-11-11` or later in the `wranger.jsonc` .
Workers with earlier compatibility dates can set the `cache_option_enabled` flag detailed
[here ](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-cache-no-store-http-standard-api ).
---
For more information, visit the [Stytch documentation ](https://stytch.com/docs ) or the [Hono documentation ](https://hono.dev/ ).