LaWallet NWC
Guides

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-here

Important: 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}` })
}
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

  1. Secret Key: Use a strong, random secret key and keep it secure
  2. Token Expiration: Set reasonable expiration times
  3. HTTPS: Always use HTTPS in production
  4. Token Storage: Store tokens securely (localStorage for client-side)
  5. Claims: Only include necessary claims in tokens
  6. 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 header
  • Authorization header must start with "Bearer " - Invalid header format
  • Token has expired - Token is past expiration
  • Invalid token - Malformed or corrupted token
  • Missing 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 example
  • lib/jwt-auth.ts - Authentication utilities
  • lib/jwt-client.ts - Client-side token management

Migration from NIP98

If you're migrating from NIP98 to JWT:

  1. Replace createNip98Token calls with jwtClient.requestToken
  2. Replace validateNip98 calls with withJwtAuth wrapper
  3. Update client-side code to use jwtClient.authenticatedRequest
  4. Update environment variables (add JWT_SECRET)

The JWT system provides similar functionality with better performance and broader compatibility.

On this page