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:30003. 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 protectionNext Steps
- Express.js Middleware — build a standalone API with credential verification
- Credential Flow — understand the full credential lifecycle
- SDK Reference — complete API documentation