Skip to content

Latest commit

 

History

History
256 lines (191 loc) · 8.46 KB

File metadata and controls

256 lines (191 loc) · 8.46 KB

Server Authentication for @ouim/logto-authkit

This module provides JWT verification helpers for Node.js applications using Logto authentication. The verification is done manually by fetching public keys from Logto's JWKS endpoint and verifying JWT claims locally, providing better control and performance with built-in caching.

Features

  • ✅ Manual JWT verification with JWKS caching
  • ✅ Express.js middleware support
  • ✅ Next.js API route support
  • ✅ Custom claim validation
  • ✅ Scope-based authorization
  • ✅ Multiple token sources (cookies + Authorization header)
  • ✅ TypeScript support

Installation

The backend helpers are included with the main package:

npm install @ouim/logto-authkit

Usage

SSR And Runtime Boundary

Use the backend subpath anywhere you need request-time authorization in SSR frameworks. This module is safe for Node.js server contexts and should be the source of truth for access control in:

  • Next.js route handlers
  • Next.js middleware
  • server-rendered loaders/actions
  • Express or custom Node servers

Do not try to infer authoritative server auth state from the frontend package during SSR. The frontend entrypoint is browser-first and hydrates auth state on the client; the backend verifier should own request authentication on the server.

Express.js Middleware

createExpressAuthMiddleware automatically parses cookies for you so there's no need to install or app.use a separate cookie-parser middleware. Just import and mount the helper as shown below.

import { createExpressAuthMiddleware } from '@ouim/logto-authkit/server'

const authMiddleware = createExpressAuthMiddleware({
  logtoUrl: 'https://your-logto-domain.com',
  audience: ['your-api-resource-identifier', 'your-secondary-resource'], // string or string[]
  cookieName: 'logto_authtoken', // optional, defaults to 'logto_authtoken'
  requiredScope: 'some_scope', // optional
})

// Use in your Express routes
app.get('/protected', authMiddleware, (req, res) => {
  // req.auth contains the authenticated user info
  res.json({
    message: 'Hello authenticated user!',
    userId: req.auth.userId,
    isAuthenticated: req.auth.isAuthenticated,
  })
})

Next.js API Routes

// pages/api/protected.js or app/api/protected/route.js
import { verifyNextAuth } from '@ouim/logto-authkit/server'

export async function GET(request) {
  const authResult = await verifyNextAuth(request, {
    logtoUrl: 'https://your-logto-domain.com',
    audience: 'your-api-resource-identifier', // or ['api-audience', 'fallback-audience']
    cookieName: 'logto_authtoken', // optional
    requiredScope: 'some_scope', // optional
  })

  if (!authResult.success) {
    return Response.json({ error: authResult.error }, { status: 401 })
  }

  return Response.json({
    message: 'Hello authenticated user!',
    userId: authResult.auth.userId,
    payload: authResult.auth.payload,
  })
}

Next.js Middleware

// middleware.js
import { verifyNextAuth } from '@ouim/logto-authkit/server'
import { NextResponse } from 'next/server'

export async function middleware(request) {
  // Only apply to API routes that need authentication
  if (request.nextUrl.pathname.startsWith('/api/protected')) {
    const authResult = await verifyNextAuth(request, {
      logtoUrl: process.env.LOGTO_URL,
      audience: process.env.LOGTO_AUDIENCE,
    })

    if (!authResult.success) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    // Add auth info to headers for the API route
    const response = NextResponse.next()
    response.headers.set('x-user-id', authResult.auth.userId)
    return response
  }
}

export const config = {
  matcher: '/api/protected/:path*',
}

Pairing Next.js server checks with client auth UI

Use @ouim/logto-authkit only in client components such as app/signin/page.tsx, app/callback/page.tsx, or a client-side providers wrapper. Use @ouim/logto-authkit/server in route handlers and middleware. That split avoids hydration confusion and keeps browser-only logic out of the server bundle.

Generic Usage

import { verifyAuth } from '@ouim/logto-authkit/server'

// Verify with token string
try {
  const auth = await verifyAuth('your-jwt-token', {
    logtoUrl: 'https://your-logto-domain.com',
    audience: 'your-api-resource-identifier',
  })

  console.log('User ID:', auth.userId)
} catch (error) {
  console.error('Auth failed:', error.message)
}

// Verify with request object
try {
  const auth = await verifyAuth(requestObject, {
    logtoUrl: 'https://your-logto-domain.com',
    audience: 'your-api-resource-identifier',
  })
} catch (error) {
  console.error('Auth failed:', error.message)
}

Token Sources

The verification helpers will look for the JWT token in the following order:

  1. Cookie: logto_authtoken (or custom name specified in cookieName)
  2. Authorization Header: Bearer <token>

Configuration Options

  • logtoUrl: Your Logto server URL (required)
  • audience: Your API resource identifier or allowed identifiers (required for protected resource APIs, accepts string | string[])
  • cookieName: Custom cookie name (optional, defaults to 'logto_authtoken')
  • requiredScope: Required scope for access (optional)
  • jwksCacheTtlMs: Override the per-process JWKS cache TTL in milliseconds (optional, defaults to 5 minutes)
  • skipJwksCache: Force a fresh JWKS fetch for this verification call instead of reading/writing the in-memory cache (optional)

Multi-scope Authorization Helpers

requiredScope is intentionally narrow. When you need more than one scope, verify the token first and then use the exported helpers:

import { hasScopes, requireScopes, verifyAuth } from '@ouim/logto-authkit/server'

const auth = await verifyAuth(request, {
  logtoUrl: 'https://your-logto-domain.com',
  audience: 'your-api-resource-identifier',
})

if (!hasScopes(auth, ['read:reports', 'write:reports'], { mode: 'any' })) {
  throw new Error('Forbidden')
}

requireScopes(auth, ['read:reports', 'write:reports'])
  • hasScopes(subject, scopes, { mode }) returns true / false
  • requireScopes(subject, scopes, { mode }) throws a descriptive authorization error
  • mode defaults to 'all'; set 'any' to accept any matching scope
  • Both helpers accept either a raw AuthPayload or a full AuthContext

Role-based Authorization Helpers

Use the exported role helpers when your access control is role-oriented instead of scope-oriented:

import { hasRole, requireRole, verifyAuth } from '@ouim/logto-authkit/server'

const auth = await verifyAuth(request, {
  logtoUrl: 'https://your-logto-domain.com',
  audience: 'your-api-resource-identifier',
})

if (!hasRole(auth, 'admin')) {
  throw new Error('Forbidden')
}

requireRole(auth, 'admin')
  • hasRole(subject, role, { claimKeys }) returns true / false
  • requireRole(subject, role, { claimKeys }) throws a descriptive authorization error
  • Default role claim lookup is roles, then role
  • If your Logto tenant emits roles under a custom namespaced claim, pass claimKeys explicitly
  • The helpers accept either a raw AuthPayload or a full AuthContext

JWKS Cache Controls

The backend verifier keeps a per-process in-memory JWKS cache by default. That is usually the right tradeoff, but you can now control it explicitly when needed:

  • Pass jwksCacheTtlMs to shorten or extend the cache lifetime for a specific verifier/middleware instance.
  • Pass skipJwksCache: true to force a fresh JWKS fetch for a specific verification call.
  • Call invalidateJwksCache(logtoUrl) to drop one tenant's cached JWKS entry.
  • Call clearJwksCache() to flush the entire in-memory JWKS cache for the current process.
import { clearJwksCache, invalidateJwksCache, verifyAuth } from '@ouim/logto-authkit/server'

const auth = await verifyAuth(token, {
  logtoUrl: 'https://your-logto-domain.com',
  audience: 'your-api-resource-identifier',
  jwksCacheTtlMs: 60_000,
})

invalidateJwksCache('https://your-logto-domain.com')
clearJwksCache()

Auth Context

When authentication is successful, you'll get an AuthContext object:

interface AuthContext {
  userId: string // User ID from token
  isAuthenticated: boolean // Always true when verification succeeds
  payload: AuthPayload // Full JWT payload
}

Error Handling

All verification functions will throw errors or return error responses when:

  • No token is found
  • Token is invalid or expired
  • Token doesn't contain required scope
  • JWKS verification fails