JWT Authentication
Current JWT flow in LaWallet NWC, including NIP-98 exchange and server-side helpers.
This document explains the JWT flow that currently exists in lawallet-nwc.
Overview
The current auth flow works like this:
- Server-side JWT library (
lib/jwt.ts) - Core JWT functions - JWT route helpers (
lib/jwt-auth.ts) - Route protection utilities for JWT-only routes - Unified auth helpers (
lib/auth/unified-auth.ts) - Accept either NIP-98 or JWT - API endpoints (
/api/jwtand/api/jwt/protected) - Token creation, validation, and protected examples
There is no lib/jwt-client.ts helper in the current repo. Frontend callers should manage token storage themselves.
Setup
1. Environment Variables
Add the following to your .env 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',
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.userId)
console.log('Role:', result.payload.role)
} catch (error) {
console.error('Token verification failed:', error.message)
}Protecting API Routes
Option 1: JWT-only protection
import { authenticateJwt } from '@/lib/jwt-auth'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const auth = await authenticateJwt(request, {
requiredClaims: ['role'],
})
return NextResponse.json({
userId: auth.payload.userId,
role: auth.payload.role,
})
}Option 2: Higher-order JWT wrapper
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()
})
}
export const GET = withJwtAuth(protectedHandler, {
requiredClaims: ['role', 'permissions']
})Option 3: Accept NIP-98 or JWT
import { withAuth } from '@/lib/auth/unified-auth'
import { NextResponse } from 'next/server'
export const GET = withAuth(async (_request, auth) => {
return NextResponse.json({
pubkey: auth.pubkey,
role: auth.role,
method: auth.method,
})
})Working with Authenticated JWT Requests
import {
getUserIdFromRequest,
getClaimFromRequest,
hasClaim,
} from '@/lib/jwt-auth'
async function handler(request: AuthenticatedRequest) {
const userId = getUserIdFromRequest(request)
const role = getClaimFromRequest<string>(request, 'role')
const permissions = getClaimFromRequest<string[]>(request, 'permissions')
if (hasClaim(request, 'role', 'admin')) {
// Admin-only logic
}
if (hasClaim(request, 'permissions', 'write')) {
// Write permission logic
}
}Current API Contract
POST /api/jwt
Exchange a NIP-98 signed request for a JWT session token.
Request Body:
{
"expiresIn": "24h"
}Headers:
Authorization: Nostr <base64-encoded-nip98-event>Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": "24h",
"type": "Bearer"
}GET /api/jwt
Validate an existing JWT token.
Headers:
Authorization: Bearer <token>Response:
{
"valid": true,
"pubkey": "npub-or-hex-pubkey",
"role": "USER",
"permissions": ["read"],
"issuedAt": "2024-01-01T00:00:00.000Z",
"expiresAt": "2024-01-02T00:00:00.000Z"
}Frontend Note
If you are building a frontend against this repo today, store the JWT however your app prefers and send it as a standard bearer token:
Authorization: Bearer <token>There is no first-party browser token manager or refresh flow in the current repository.