Skip to Content
GuidesNext.js Integration

Next.js Integration

Add decentralized identity authentication to a Next.js application using the Solidus SDK. This guide uses the App Router and covers server-side challenge creation, client-side DID auth, JWT session management, and route protection.

Prerequisites

  • Next.js 14+ with App Router
  • Node.js 18+

1. Install Dependencies

npm install @solidus-network/sdk @solidus-network/auth @solidus-network/jwt @noble/ed25519
  • @solidus-network/sdk — DID resolution, credential verification, chain queries
  • @solidus-network/auth — challenge-response authentication protocol
  • @solidus-network/jwt — sign and verify JWTs with Ed25519 keys
  • @noble/ed25519 — key pair generation

2. Environment Variables

Create a .env.local file in your project root.

# Your app's Ed25519 private key (hex-encoded, 64 characters) # Generate one with: npx @solidus-network/sdk generate-key SOLIDUS_PRIVATE_KEY=your_private_key_hex # Corresponding public key (hex-encoded, 64 characters) SOLIDUS_PUBLIC_KEY=your_public_key_hex # Your app's DID (created via sdk.did.create) SOLIDUS_APP_DID=did:solidus:testnet:your_app_did # Auth challenge domain — must match your deployment URL SOLIDUS_DOMAIN=localhost:3000

3. Create the SDK Instance

Create a shared SDK instance used by all server-side code.

// lib/solidus.ts import { createSdk } from '@solidus-network/sdk' import { createAuthVerifier } from '@solidus-network/auth' export const sdk = createSdk({ mode: 'testnet', chain: { rpcUrl: 'https://rpc.solidus.network', signerPrivateKey: process.env.SOLIDUS_PRIVATE_KEY!, network: 'testnet', }, }) export const authVerifier = createAuthVerifier({ rpcUrl: 'https://rpc.solidus.network', })

4. JWT Helper

Create a helper module for signing and verifying session tokens.

// lib/session.ts import { sign, verify } from '@solidus-network/jwt' const PRIVATE_KEY = process.env.SOLIDUS_PRIVATE_KEY! const PUBLIC_KEY = process.env.SOLIDUS_PUBLIC_KEY! interface SessionPayload { did: string credentials: string[] } export async function createSessionToken( payload: SessionPayload ): Promise<string> { return sign( { sub: payload.did, credentials: payload.credentials, iss: process.env.SOLIDUS_APP_DID!, aud: process.env.SOLIDUS_DOMAIN!, }, PRIVATE_KEY, { algorithm: 'EdDSA', expiresIn: '24h' } ) } export async function verifySessionToken( token: string ): Promise<SessionPayload | null> { try { const decoded = await verify(token, PUBLIC_KEY, { algorithms: ['EdDSA'], audience: process.env.SOLIDUS_DOMAIN!, }) return { did: decoded.sub as string, credentials: decoded.credentials as string[], } } catch { return null } }

5. API Route: Create Challenge

This endpoint generates an authentication challenge that the user’s wallet signs.

// app/api/auth/challenge/route.ts import { NextResponse } from 'next/server' import { authVerifier } from '@/lib/solidus' export async function POST(request: Request) { const body = await request.json() const { requiredCredentials } = body const challenge = await authVerifier.createChallenge({ domain: process.env.SOLIDUS_DOMAIN!, requiredCredentials: requiredCredentials || ['KYCCredential'], }) return NextResponse.json({ challengeId: challenge.id, challenge: challenge.challenge, domain: challenge.domain, requiredCredentials: challenge.requiredCredentials, expiresAt: challenge.expiresAt, }) }

6. API Route: Verify Presentation

After the user signs the challenge, this endpoint verifies the presentation, checks credentials, and issues a JWT session token.

// app/api/auth/verify/route.ts import { NextResponse } from 'next/server' import { authVerifier } from '@/lib/solidus' import { sdk } from '@/lib/solidus' import { createSessionToken } from '@/lib/session' export async function POST(request: Request) { const body = await request.json() const { presentation } = body // Verify the signed presentation const verification = await authVerifier.verifyPresentation(presentation) if (!verification.valid) { return NextResponse.json( { error: 'Invalid presentation', details: verification.errors }, { status: 401 } ) } // Verify each credential on-chain const credentialTypes: string[] = [] for (const credential of verification.credentials) { const result = await sdk.credentials.verify(credential) if (!result.valid) { return NextResponse.json( { error: 'Invalid credential', credentialId: credential.id }, { status: 401 } ) } credentialTypes.push(...credential.type) } // Issue a session JWT const token = await createSessionToken({ did: verification.subject, credentials: credentialTypes, }) const response = NextResponse.json({ token, did: verification.subject, credentials: credentialTypes, }) // Also set as HTTP-only cookie for middleware access response.cookies.set('solidus-session', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24, // 24 hours path: '/', }) return response }

7. Client Component: Auth Button

A client component that triggers the DID authentication flow.

// components/solidus-auth-button.tsx 'use client' import { useState } from 'react' import { createAuthClient } from '@solidus-network/auth' interface SolidusAuthButtonProps { onSuccess: (session: { did: string; token: string }) => void onError?: (error: Error) => void requiredCredentials?: string[] } export function SolidusAuthButton({ onSuccess, onError, requiredCredentials = ['KYCCredential'], }: SolidusAuthButtonProps) { const [loading, setLoading] = useState(false) async function handleAuth() { setLoading(true) try { // Step 1: Request a challenge from the server const challengeRes = await fetch('/api/auth/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requiredCredentials }), }) const challenge = await challengeRes.json() // Step 2: Connect to the user's Solidus wallet // The SDK prompts the user to approve in their wallet const authClient = createAuthClient({ walletAdapter: 'browser', // Uses browser extension or Identity app }) // Step 3: Sign the challenge with the user's DID const presentation = await authClient.signPresentation({ challenge: challenge.challengeId, requiredCredentials: challenge.requiredCredentials, }) // Step 4: Send the signed presentation to the server const verifyRes = await fetch('/api/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ presentation }), }) if (!verifyRes.ok) { throw new Error('Verification failed') } const session = await verifyRes.json() onSuccess({ did: session.did, token: session.token }) } catch (err) { onError?.(err instanceof Error ? err : new Error(String(err))) } finally { setLoading(false) } } return ( <button onClick={handleAuth} disabled={loading} style={{ padding: '12px 24px', borderRadius: '8px', border: 'none', backgroundColor: '#0A0A0A', color: '#FFFFFF', fontSize: '16px', cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? 0.7 : 1, }} > {loading ? 'Connecting...' : 'Sign in with Solidus'} </button> ) }

8. Middleware: Protect Routes

Use Next.js middleware to protect routes that require authentication.

// middleware.ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import { verify } from '@solidus-network/jwt' const PROTECTED_PATHS = ['/dashboard', '/profile', '/settings'] export async function middleware(request: NextRequest) { const isProtected = PROTECTED_PATHS.some((path) => request.nextUrl.pathname.startsWith(path) ) if (!isProtected) { return NextResponse.next() } const token = request.cookies.get('solidus-session')?.value if (!token) { return NextResponse.redirect(new URL('/login', request.url)) } try { const decoded = await verify(token, process.env.SOLIDUS_PUBLIC_KEY!, { algorithms: ['EdDSA'], }) // Attach user info to headers for downstream use const response = NextResponse.next() response.headers.set('x-solidus-did', decoded.sub as string) return response } catch { // Invalid or expired token — clear cookie and redirect const response = NextResponse.redirect(new URL('/login', request.url)) response.cookies.delete('solidus-session') return response } } export const config = { matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*'], }

9. Display User Identity

A server component that reads the session and displays the user’s DID and credentials.

// app/dashboard/page.tsx import { cookies } from 'next/headers' import { verifySessionToken } from '@/lib/session' import { sdk } from '@/lib/solidus' import { redirect } from 'next/navigation' export default async function DashboardPage() { const cookieStore = await cookies() const token = cookieStore.get('solidus-session')?.value if (!token) { redirect('/login') } const session = await verifySessionToken(token) if (!session) { redirect('/login') } // Resolve the user's DID document from the chain const didDocument = await sdk.did.resolve(session.did) return ( <div> <h1>Dashboard</h1> <section> <h2>Your Identity</h2> <dl> <dt>DID</dt> <dd>{session.did}</dd> <dt>Credentials</dt> <dd>{session.credentials.join(', ')}</dd> <dt>Verification Methods</dt> <dd>{didDocument.verificationMethod?.length ?? 0}</dd> </dl> </section> </div> ) }

Complete Project Structure

my-nextjs-app/ .env.local lib/ solidus.ts # SDK instance session.ts # JWT sign/verify helpers app/ api/ auth/ challenge/ route.ts # POST — create auth challenge verify/ route.ts # POST — verify presentation, issue JWT dashboard/ page.tsx # Protected page showing user identity login/ page.tsx # Login page with SolidusAuthButton components/ solidus-auth-button.tsx middleware.ts # Route protection

Next Steps

Last updated on