JWT Authentication
How to use the JWT authentication system for secure API access.
This document explains how to use the JWT authentication system in the Lawallet NWC application.
Overview
The JWT system provides secure authentication using JSON Web Tokens, similar to the NIP98 implementation. It includes:
- Server-side JWT library (
lib/jwt.ts) - Core JWT functions - Authentication middleware (
lib/jwt-auth.ts) - Route protection utilities - Client-side utilities (
lib/jwt-client.ts) - Token management on the frontend - API endpoints (
/api/jwt/*) - Token creation and validation
Setup
1. Environment Variables
Add the following to your .env.local file:
JWT_SECRET=your-super-secret-jwt-key-hereImportant: Use a strong, random secret key in production.
2. Dependencies
The required packages are already installed:
jsonwebtoken- JWT creation and verification@types/jsonwebtoken- TypeScript types
Server-Side Usage
Creating JWT Tokens
import { createJwtToken } from '@/lib/jwt'
const token = createJwtToken(
{
userId: 'user123',
pubkey: 'pubkey123',
role: 'admin',
permissions: ['read', 'write']
},
process.env.JWT_SECRET!,
{
expiresIn: '24h',
issuer: 'lawallet-nwc',
audience: 'lawallet-users'
}
)Verifying JWT Tokens
import { verifyJwtToken } from '@/lib/jwt'
try {
const result = verifyJwtToken(token, process.env.JWT_SECRET!)
console.log('User ID:', result.payload.sub)
console.log('Role:', result.payload.role)
} catch (error) {
console.error('Token verification failed:', error.message)
}Protecting API Routes
Option 1: Using the middleware function
import { authenticateJwt } from '@/lib/jwt-auth'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
// Authenticate the request
const authResult = await authenticateJwt(request, {
requiredClaims: ['role']
})
if (authResult) {
return authResult // Returns error response if auth fails
}
// Request is authenticated, proceed with your logic
const userId = request.jwt?.payload.sub
return NextResponse.json({ message: `Hello user ${userId}` })
}Option 2: Using the higher-order function (Recommended)
import { withJwtAuth, getUserIdFromRequest } from '@/lib/jwt-auth'
import { NextResponse } from 'next/server'
import type { AuthenticatedRequest } from '@/lib/jwt-auth'
async function protectedHandler(request: AuthenticatedRequest) {
const userId = getUserIdFromRequest(request)
return NextResponse.json({
message: `Hello user ${userId}`,
timestamp: new Date().toISOString()
})
}
// Wrap with JWT authentication
export const GET = withJwtAuth(protectedHandler, {
requiredClaims: ['role', 'permissions']
})Working with Authenticated Requests
import {
getUserIdFromRequest,
getClaimFromRequest,
hasClaim
} from '@/lib/jwt-auth'
async function handler(request: AuthenticatedRequest) {
// Get user ID
const userId = getUserIdFromRequest(request)
// Get specific claims
const role = getClaimFromRequest<string>(request, 'role')
const permissions = getClaimFromRequest<string[]>(request, 'permissions')
// Check claims
if (hasClaim(request, 'role', 'admin')) {
// Admin-only logic
}
if (hasClaim(request, 'permissions', 'write')) {
// Write permission logic
}
}Client-Side Usage
Basic Token Management
import { jwtClient } from '@/lib/jwt-client'
// Request a new token
const tokenData = await jwtClient.requestToken(
'user123',
{ role: 'user', permissions: ['read'] },
'24h'
)
// Check if token exists
if (jwtClient.hasStoredToken()) {
console.log('User is authenticated')
}
// Get auth header for requests
const authHeader = jwtClient.getAuthHeader()
// Returns: "Bearer eyJhbGciOiJIUzI1NiIs..."
// Logout
jwtClient.logout()Making Authenticated Requests
import { jwtClient } from '@/lib/jwt-client'
// Simple authenticated request
const response = await jwtClient.authenticatedRequest('/api/protected-endpoint')
// With custom options
const response = await jwtClient.authenticatedRequest('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'John Doe' })
})Token Validation
import { jwtClient } from '@/lib/jwt-client'
// Validate stored token
const isValid = await jwtClient.validateStoredToken()
if (!isValid) {
// Token is invalid, redirect to login
jwtClient.logout()
// redirect to login page
}API Endpoints
POST /api/jwt
Request a new JWT token.
Request Body:
{
"userId": "user123",
"expiresIn": "24h",
"additionalClaims": {
"role": "admin",
"permissions": ["read", "write"]
}
}Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": "24h",
"type": "Bearer"
}GET /api/jwt
Validate an existing JWT token.
Headers:
Authorization: Bearer <token>Response:
{
"valid": true,
"userId": "user123",
"issuedAt": "2024-01-01T00:00:00.000Z",
"expiresAt": "2024-01-02T00:00:00.000Z",
"additionalClaims": {
"role": "admin",
"permissions": ["read", "write"]
}
}Security Considerations
- Secret Key: Use a strong, random secret key and keep it secure
- Token Expiration: Set reasonable expiration times
- HTTPS: Always use HTTPS in production
- Token Storage: Store tokens securely (localStorage for client-side)
- Claims: Only include necessary claims in tokens
- Validation: Always validate tokens on the server side
Error Handling
The system provides detailed error messages for common issues:
Authorization header is required- Missing auth headerAuthorization header must start with "Bearer "- Invalid header formatToken has expired- Token is past expirationInvalid token- Malformed or corrupted tokenMissing required claim: <claim>- Required claim not present
Interactive Demo: JWT Auth Flow
Try the complete authentication flow below. Click "Login" to simulate the NIP-98 to JWT exchange, then make authenticated API calls:
import { useState, useCallback } from "react"; // Simulated JWT utilities function base64url(str: string) { return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); } function createMockJWT(claims: Record<string, any>) { const header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" })); const payload = base64url(JSON.stringify({ sub: claims.userId, role: claims.role, permissions: claims.permissions, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 86400, iss: "lawallet-nwc", })); return header + "." + payload + ".mock-signature"; } function decodeJWT(token: string) { const [, payload] = token.split("."); return JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/"))); } export default function App() { const [token, setToken] = useState<string | null>(null); const [apiResponse, setApiResponse] = useState<string>(""); const [step, setStep] = useState(0); const login = useCallback(async () => { setStep(1); await new Promise(r => setTimeout(r, 500)); setStep(2); await new Promise(r => setTimeout(r, 500)); const jwt = createMockJWT({ userId: "user_abc123", role: "admin", permissions: ["read", "write"], }); setToken(jwt); setStep(3); }, []); const callAPI = useCallback(async () => { if (!token) return; setApiResponse("Loading..."); await new Promise(r => setTimeout(r, 600)); const claims = decodeJWT(token); setApiResponse(JSON.stringify({ status: 200, message: "Hello " + claims.sub, role: claims.role, authorized: true, }, null, 2)); }, [token]); const logout = () => { setToken(null); setApiResponse(""); setStep(0); }; return ( <div style={{ fontFamily: "system-ui", padding: 20, maxWidth: 500 }}> <h2>JWT Authentication Flow</h2> <div style={{ display: "flex", gap: 8, marginBottom: 16 }}> {["NIP-98 Event", "POST /api/jwt", "JWT Issued"].map((label, i) => ( <div key={i} style={{ padding: "6px 12px", borderRadius: 6, fontSize: 12, fontWeight: 600, background: step > i ? "#22c55e" : step === i ? "#eab308" : "#e5e7eb", color: step > i ? "white" : step === i ? "white" : "#6b7280", transition: "all 0.3s", }}> {label} </div> ))} </div> {!token ? ( <button onClick={login} style={{ padding: "10px 20px", borderRadius: 8, background: "#7c3aed", color: "white", border: "none", cursor: "pointer", fontSize: 14, }}> Login with Nostr (NIP-98) </button> ) : ( <div> <div style={{ background: "#f0fdf4", padding: 12, borderRadius: 8, marginBottom: 12, fontSize: 13, }}> <strong>Token:</strong> <code style={{ fontSize: 11, wordBreak: "break-all", display: "block", marginTop: 4 }}> {token.slice(0, 60)}... </code> </div> <div style={{ background: "#eff6ff", padding: 12, borderRadius: 8, marginBottom: 12, fontSize: 13, }}> <strong>Decoded Claims:</strong> <pre style={{ margin: 0, fontSize: 11 }}> {JSON.stringify(decodeJWT(token), null, 2)} </pre> </div> <div style={{ display: "flex", gap: 8 }}> <button onClick={callAPI} style={{ padding: "8px 16px", borderRadius: 6, background: "#2563eb", color: "white", border: "none", cursor: "pointer", }}> Call Protected API </button> <button onClick={logout} style={{ padding: "8px 16px", borderRadius: 6, background: "#ef4444", color: "white", border: "none", cursor: "pointer", }}> Logout </button> </div> {apiResponse && ( <pre style={{ background: "#1e1e2e", color: "#a6e3a1", padding: 12, borderRadius: 8, marginTop: 12, fontSize: 12, }}> {apiResponse} </pre> )} </div> )} </div> ); }
Source Files
See the following files for complete examples:
app/api/jwt/protected/route.ts- Protected endpoint examplelib/jwt-auth.ts- Authentication utilitieslib/jwt-client.ts- Client-side token management
Migration from NIP98
If you're migrating from NIP98 to JWT:
- Replace
createNip98Tokencalls withjwtClient.requestToken - Replace
validateNip98calls withwithJwtAuthwrapper - Update client-side code to use
jwtClient.authenticatedRequest - Update environment variables (add
JWT_SECRET)
The JWT system provides similar functionality with better performance and broader compatibility.