Claude Code for jose: JWT Signing and Verification in JavaScript — Claude Skills 360 Blog
Blog / Backend / Claude Code for jose: JWT Signing and Verification in JavaScript
Backend

Claude Code for jose: JWT Signing and Verification in JavaScript

Published: April 28, 2027
Read time: 6 min read
By: Claude Skills 360

jose is a Web Crypto API-based JWT library that works in Node.js, browser, Deno, and Edge Runtimes — new SignJWT({ sub, role }) chains .setProtectedHeader({ alg: "HS256" }), .setIssuedAt(), .setExpirationTime("7d"), .sign(secret) to create a signed token. jwtVerify(token, secret) verifies signature and claims, returning the payload. JWTExpired and JWTClaimValidationFailed errors enable typed error handling. createRemoteJWKSet(jwksUri) verifies tokens against a JWKS endpoint like Auth0 or Clerk. generateSecret("HS256") creates cryptographically secure secrets. exportSPKI/importSPKI handle RSA public keys for asymmetric signing. jose runs on Cloudflare Workers and Vercel Edge Functions without polyfills. Claude Code generates jose JWT utilities, session token helpers, API key verification middleware, and JWKS-based third-party token validation.

CLAUDE.md for jose

## jose Stack
- Version: jose >= 5.3
- Sign: new SignJWT({ sub: userId }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("7d").sign(secret)
- Verify: const { payload } = await jwtVerify(token, secret) — throws JWTExpired, JWTClaimValidationFailed
- Secret: const secret = new TextEncoder().encode(process.env.JWT_SECRET)
- RS256: const { privateKey } = await generateKeyPair("RS256"); .sign(privateKey) + jwtVerify(token, publicKey)
- JWKS: const JWKS = createRemoteJWKSet(new URL("https://login.example.com/.well-known/jwks.json"))
- Payload: payload.sub, payload.exp, payload.iat — standard JWT claims
- Error: import { errors } from "jose" — errors.JWTExpired, errors.JWTClaimValidationFailed

JWT Utilities

// lib/auth/jwt.ts — JWT creation and verification utilities
import {
  SignJWT,
  jwtVerify,
  generateSecret,
  JWTPayload,
  errors as joseErrors,
} from "jose"

// Secret from environment — use JOSE_SECRET=base64url encoded 256-bit random key
function getSecret() {
  const secret = process.env.JWT_SECRET
  if (!secret) throw new Error("JWT_SECRET not set")
  return new TextEncoder().encode(secret)
}

// Access token — short-lived, contains minimal claims
type AccessTokenPayload = JWTPayload & {
  userId: string
  role: string
  sessionId: string
}

export async function signAccessToken(params: {
  userId: string
  role: string
  sessionId: string
}): Promise<string> {
  return new SignJWT({
    userId: params.userId,
    role: params.role,
    sessionId: params.sessionId,
  })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("15m")  // Short-lived
    .setSubject(params.userId)
    .setAudience("api")
    .setIssuer(process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000")
    .sign(getSecret())
}

export async function verifyAccessToken(token: string): Promise<AccessTokenPayload> {
  const { payload } = await jwtVerify(token, getSecret(), {
    audience: "api",
    issuer: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
    algorithms: ["HS256"],
  })
  return payload as AccessTokenPayload
}

// Refresh token — longer-lived, used to generate new access tokens
export async function signRefreshToken(userId: string, jti: string): Promise<string> {
  return new SignJWT({ type: "refresh" })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("30d")
    .setSubject(userId)
    .setJti(jti)  // Unique JWT ID for revocation
    .sign(getSecret())
}

export async function verifyRefreshToken(token: string) {
  const { payload } = await jwtVerify(token, getSecret(), {
    algorithms: ["HS256"],
  })
  if (payload.type !== "refresh") throw new Error("Not a refresh token")
  return { userId: payload.sub!, jti: payload.jti! }
}

// Email verification / password reset tokens (one-time use)
export async function signOneTimeToken(params: {
  type: "email_verify" | "password_reset"
  userId: string
  email: string
}): Promise<string> {
  return new SignJWT({
    type: params.type,
    email: params.email,
    jti: crypto.randomUUID(),
  })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("1h")
    .setSubject(params.userId)
    .sign(getSecret())
}

// Typed error handling
type VerifyResult<T> =
  | { ok: true; payload: T }
  | { ok: false; error: "expired" | "invalid" | "unknown" }

export async function safeVerify<T extends JWTPayload>(token: string): Promise<VerifyResult<T>> {
  try {
    const { payload } = await jwtVerify(token, getSecret())
    return { ok: true, payload: payload as T }
  } catch (err) {
    if (err instanceof joseErrors.JWTExpired) {
      return { ok: false, error: "expired" }
    }
    if (
      err instanceof joseErrors.JWTClaimValidationFailed ||
      err instanceof joseErrors.JWSSignatureVerificationFailed ||
      err instanceof joseErrors.JWTInvalid
    ) {
      return { ok: false, error: "invalid" }
    }
    return { ok: false, error: "unknown" }
  }
}

Middleware JWT Verification

// middleware.ts — verify JWT in Next.js Edge Middleware
import { NextRequest, NextResponse } from "next/server"
import { jwtVerify, createRemoteJWKSet } from "jose"

// jose works in Edge Runtime — no Node.js API polyfills needed
const secret = new TextEncoder().encode(process.env.JWT_SECRET!)

const PUBLIC_PATHS = ["/", "/sign-in", "/sign-up", "/api/auth", "/api/webhooks"]

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  if (PUBLIC_PATHS.some(p => pathname.startsWith(p))) {
    return NextResponse.next()
  }

  const authHeader = request.headers.get("authorization")
  const cookieToken = request.cookies.get("access_token")?.value

  const token = authHeader?.replace("Bearer ", "") ?? cookieToken

  if (!token) {
    if (pathname.startsWith("/api/")) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
    }
    return NextResponse.redirect(new URL("/sign-in", request.url))
  }

  try {
    const { payload } = await jwtVerify(token, secret, {
      audience: "api",
      algorithms: ["HS256"],
    })

    // Forward user info to handlers via headers
    const response = NextResponse.next()
    response.headers.set("x-user-id", payload.sub ?? "")
    response.headers.set("x-user-role", (payload.role as string) ?? "user")
    return response
  } catch {
    if (pathname.startsWith("/api/")) {
      return NextResponse.json({ error: "Invalid token" }, { status: 401 })
    }
    return NextResponse.redirect(new URL("/sign-in?error=session_expired", request.url))
  }
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
}

JWKS Verification for Third-Party Tokens

// lib/auth/jwks.ts — verify Clerk/Auth0/Okta tokens
import { createRemoteJWKSet, jwtVerify } from "jose"

// Cache JWKS — auto-refreshes periodically
const ClerkJWKS = createRemoteJWKSet(
  new URL(`https://${process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY?.split("_")[1]}.clerk.accounts.dev/.well-known/jwks.json`),
  { cacheMaxAge: 600_000 },  // Cache 10 minutes
)

export async function verifyClerkToken(sessionToken: string) {
  const { payload } = await jwtVerify(sessionToken, ClerkJWKS, {
    issuer: `https://clerk.${process.env.NEXT_PUBLIC_APP_URL}`,
    algorithms: ["RS256"],
  })
  return {
    userId: payload.sub!,
    sessionId: payload.sid as string,
    role: (payload as any).metadata?.role as string | undefined,
  }
}

// Generate secure secret for scripts
export async function generateJWTSecret(): Promise<string> {
  const key = crypto.getRandomValues(new Uint8Array(32))
  return Buffer.from(key).toString("base64url")
}

For the jsonwebtoken alternative when a simpler, synchronous Node.js-only JWT library without Web Crypto API is preferred — jsonwebtoken.sign/verify have a callback/synchronous API without async/await, but don’t work in Edge Runtimes or browsers, making jose preferable for modern full-stack apps, see the jsonwebtoken guide. For the iron-session alternative when encrypted, opaque session cookies are preferred over signed JWTs — iron-session uses Iron symmetric encryption to store session data in a cookie without a separate token store, suitable when you don’t need stateless token verification across services, see the iron-session guide. The Claude Skills 360 bundle includes jose skill sets covering JWT signing, verification, and JWKS. Start with the free tier to try token authentication generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free