@flyingsquirrel0419/jwt-refresh
v0.1.0
Published
JWT refresh token rotation, race-condition prevention, reuse detection, and session management for Node.js.
Maintainers
Readme
The Problem
Every hand-rolled JWT stack eventually hits the same refresh wall:
jsonwebtoken only --> signs and verifies tokens, but leaves refresh flows to app code
custom refresh endpoint --> works until rotation, replay detection, and logout rules pile up
multiple browser tabs --> all try to refresh at once, then users get logged out by accident
stolen refresh token --> gets replayed without a clear security responseThe failure mode is almost always the same: a team solves "issue access tokens" and underestimates "manage the refresh lifecycle safely".
The Solution
jwt-refresh packages the refresh lifecycle as a first-class system:
client request
-> access token verification
-> refresh cookie extraction
-> refresh token verification
-> session lookup
-> race-condition guard
-> rotation or replay decision
-> blacklist / revoke / event emission
-> new access token + refresh tokenInstead of sprinkling refresh logic across routes, services, and middleware, you get one manager that owns:
- refresh token rotation
- reuse detection
- legitimate race handling
- access-token blacklisting
- session revocation
- framework-friendly handlers
Quick Start
Install
Install the published package from npm:
npm install @flyingsquirrel0419/jwt-refresh jsonwebtokenMinimal example
import express from 'express'
import { JwtManager, MemoryTokenStore } from '@flyingsquirrel0419/jwt-refresh'
const app = express()
app.use(express.json())
const jwt = new JwtManager({
access: {
secret: process.env.JWT_SECRET!,
ttl: '15m',
},
refresh: {
secret: process.env.REFRESH_SECRET!,
ttl: '7d',
rotation: true,
reuseDetection: true,
absoluteExpiry: '90d',
},
cookie: {
name: 'refreshToken',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/auth',
},
refreshBuffer: 120,
store: new MemoryTokenStore(),
})
app.post('/auth/login', async (_req, res) => {
const { accessToken } = await jwt.issueTokens(
res,
{
userId: 'user-1',
email: '[email protected]',
roles: ['member'],
},
{
deviceId: 'browser',
},
)
res.json({ accessToken })
})
app.post('/auth/refresh', jwt.refreshHandler())
app.get('/api/me', jwt.authenticate(), (req, res) => res.json(req.user))What refresh handling looks like
POST /auth/refresh
-> reads refresh cookie
-> verifies JWT signature and claims
-> checks stored session state
-> returns 409 retry for a legitimate in-flight race
-> revokes all sessions on replayed rotated tokens
-> rotates refresh token and returns a fresh access tokenFeatures
Token lifecycle
| Feature | What it does | |---|---| | Refresh token rotation | Issues a new refresh token on successful refresh and revokes the previous session | | Absolute expiry | Stops endless refresh chains and forces re-authentication after a fixed window | | Access-token blacklisting | Rejects still-valid access tokens immediately after sensitive events | | Cookie-aware refresh flow | Reads and writes refresh cookies without route boilerplate |
Security
| Feature | What it does |
|---|---|
| Reuse detection | Detects replayed rotated refresh tokens and revokes all active sessions for the user |
| Race-condition protection | Distinguishes a real replay from a legitimate multi-request refresh burst |
| Session-level revocation | Revokes one device, the current device, or all devices |
| Security events | Emits token:reuse-detected, token:revoked, and refresh-related events |
Developer experience
| Feature | What it does |
|---|---|
| Typed payloads | JwtManager<TPayload> preserves your access-token payload type |
| Framework helpers | Express-first handlers plus Fastify, Next.js, and NestJS adapters |
| Multiple store adapters | Memory store for tests, plus Redis, Prisma, and Drizzle adapter surfaces |
| ESM + CJS | Ships generated declaration files and dual module output |
Quality
| Feature | What it does | |---|---| | 64 passing tests | Integration, security, and deep unit coverage across core and adapter branches | | 99.05% line coverage | High confidence in the refresh, security, and adapter control flow | | GitHub Actions CI | Lint, test, coverage, and build on push and pull request |
Integrations
jwt-refresh is designed to sit inside the web stack you already use:
| Framework | Integration |
|---|---|
| Express | jwt.authenticate() and jwt.refreshHandler() as route-ready handlers |
| Fastify | createFastifyHandler(jwt, 'authenticate' | 'refresh') bridge |
| Next.js App Router | verifyNextRequest() and createNextRefreshHandler() helpers |
| NestJS | createNestGuard() and getJwtUser() for guard-style integration |
import express from 'express'
import { JwtManager, MemoryTokenStore } from '@flyingsquirrel0419/jwt-refresh'
const app = express()
const jwt = new JwtManager({
access: { secret: process.env.JWT_SECRET!, ttl: '15m' },
refresh: { secret: process.env.REFRESH_SECRET!, ttl: '7d', rotation: true, reuseDetection: true },
store: new MemoryTokenStore(),
})
app.post('/auth/refresh', jwt.refreshHandler())
app.get('/api/me', jwt.authenticate(), (req, res) => res.json(req.user))import { createNextRefreshHandler } from '@flyingsquirrel0419/jwt-refresh/integrations/nextjs'
export const POST = createNextRefreshHandler(jwt)import { createNestGuard } from '@flyingsquirrel0419/jwt-refresh/integrations/nestjs'
const JwtAuthGuard = createNestGuard(jwt)API
The public API is intentionally compact:
class JwtManager<TPayload extends AccessTokenPayload = AccessTokenPayload> {
issueTokens(res, payload, sessionMeta?)
signAccessToken(payload)
signRefreshToken(userId, sessionMeta?)
verifyAccessToken(token)
verifyRefreshToken(token)
authenticate(options?)
refreshHandler(options?)
revokeCurrentSession(req, res)
revokeSession(userId, sessionId)
revokeAllSessions(userId)
getSessions(userId, currentSessionId?)
blacklistToken(token, options?)
}See API Reference for the full option and method surface.
Security Defaults
Recommended production defaults:
refresh.rotation: truerefresh.reuseDetection: truerefresh.absoluteExpiry: '90d'cookie.httpOnly: truecookie.sameSite: 'strict'cookie.secure: truein production
See the full security checklist.
Comparison
jwt-refresh is not trying to replace low-level JWT libraries. It sits above them and solves the lifecycle problems they intentionally leave to application code.
| Package | Signs JWTs | Rotation | Reuse detection | Race handling | Session revoke | Access blacklist |
|---|---:|---:|---:|---:|---:|---:|
| jwt-refresh | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| jsonwebtoken | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| jose | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| passport-jwt | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| @nestjs/jwt | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
More detail: Comparison
Development
npm install
npm run lint
npm test
npm run coverage
npm run build