Next.js Entegrasyonu
Solidus SDK’sını kullanarak bir Next.js uygulamasına merkeziyetsiz kimlik kimlik doğrulaması ekleyin. Bu kılavuz App Router’ı kullanır ve sunucu tarafı meydan okuma oluşturmayı, istemci tarafı DID kimlik doğrulamasını, JWT oturum yönetimini ve rota korumasını kapsar.
Ön koşullar
- App Router ile Next.js 14+
- Node.js 18+
1. Bağımlılıkları Kurun
Solidus bir JWT paketi yayınlamıyor (@solidus-network/jwt npm’e hiç yayınlanmadı — bkz. SDK
genel bakış). Oturum token’ları, aşağıdaki DID imza doğrulamasıyla ilgisiz, sıradan bir
uygulama meselesidir, bu yüzden bu kılavuz birinci sınıf Edge/Next.js ara katman desteğine sahip
standart bir paket olan jose’yi kullanır.
npm install @solidus-network/sdk @solidus-network/auth jose @noble/ed25519@solidus-network/sdk— DID çözümleme, kimlik bilgisi doğrulama, zincir sorguları@solidus-network/auth— DID meydan okuma-yanıt kimlik doğrulama ilkellerijose— uygulamanın kendi oturum JWT’lerini imzalar ve doğrular (HMAC)@noble/ed25519— anahtar çifti üretimi
2. Ortam Değişkenleri
Proje kökünüzde bir .env.local dosyası oluşturun.
# Your app's Ed25519 private key (hex-encoded, 64 characters) — used by the
# SDK for any signing operations your backend performs (e.g. did.create)
SOLIDUS_PRIVATE_KEY=your_private_key_hex
# Random secret for signing this app's own session JWTs (unrelated to DID
# keys) — generate with: openssl rand -hex 32
SESSION_JWT_SECRET=your_session_secret_hex
# Auth challenge domain — must match your deployment URL
SOLIDUS_DOMAIN=localhost:30003. SDK Örneğini Oluşturun
Tüm sunucu tarafı kod tarafından kullanılan paylaşılan bir SDK örneği oluşturun.
// lib/solidus.ts
import { createSdk } from '@solidus-network/sdk'
export const sdk = createSdk({
mode: 'testnet',
rpcUrl: 'https://rpc.solidus.network',
signerPrivateKey: process.env.SOLIDUS_PRIVATE_KEY!,
})4. JWT Yardımcısı
Bu uygulamanın kendi oturum token’larını imzalamak ve doğrulamak için bir yardımcı modül oluşturun (adım 6’daki DID meydan okuma/sunum kontrolünden ayrı ve onun altında).
// lib/session.ts
import { SignJWT, jwtVerify } from 'jose'
const secret = new TextEncoder().encode(process.env.SESSION_JWT_SECRET!)
interface SessionPayload {
did: string
credentials: string[]
}
export async function createSessionToken(
payload: SessionPayload
): Promise<string> {
return new SignJWT({ credentials: payload.credentials })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(payload.did)
.setAudience(process.env.SOLIDUS_DOMAIN!)
.setIssuedAt()
.setExpirationTime('24h')
.sign(secret)
}
export async function verifySessionToken(
token: string
): Promise<SessionPayload | null> {
try {
const { payload } = await jwtVerify(token, secret, {
audience: process.env.SOLIDUS_DOMAIN!,
})
return {
did: payload.sub as string,
credentials: payload['credentials'] as string[],
}
} catch {
return null
}
}5. API Rotası: Meydan Okuma Oluşturma
Bu uç nokta, kullanıcının özel anahtarıyla imzaladığı bir DID kimlik doğrulama meydan okuması
üretir. createChallenge, düz, eşzamanlı bir fonksiyondur — meydan okumayı, meydan okunan DID’e
anahtarlanmış olarak doğrudan döndürür; onu kalıcı hale getirmek (ör. kısa ömürlü bir depoda)
rotanızın sorumluluğundadır, böylece adım 6 onu id’ye göre arayabilir.
// app/api/auth/challenge/route.ts
import { NextResponse } from 'next/server'
import { createChallenge } from '@solidus-network/auth'
import { saveChallenge } from '@/lib/challenge-store' // your own storage
export async function POST(request: Request) {
const { did } = await request.json()
const challenge = createChallenge(did)
await saveChallenge(challenge)
return NextResponse.json(challenge)
// { id, did, nonce, issuedAt, expiresAt }
}6. API Rotası: Sunumu Doğrulama
Kullanıcı meydan okumayı imzaladıktan sonra, bu uç nokta sunumu doğrular ve bir oturum JWT’si
düzenler. verifyPresentation, orijinal meydan okumayı, imzalanmış sunumu ve bir
verificationMethod kimliğini imzalayanın ham Ed25519 açık anahtarına eşleyen bir çözümleyiciyi
alır (sahibinin zincir üzerindeki DID belgesinden çözümlenir).
// app/api/auth/verify/route.ts
import { NextResponse } from 'next/server'
import { verifyPresentation } from '@solidus-network/auth'
import { sdk } from '@/lib/solidus'
import { createSessionToken } from '@/lib/session'
import { loadChallenge } from '@/lib/challenge-store'
import bs58 from 'bs58'
export async function POST(request: Request) {
const { challengeId, presentation } = await request.json()
const challenge = await loadChallenge(challengeId)
if (!challenge) {
return NextResponse.json({ error: 'Unknown or expired challenge' }, { status: 401 })
}
const verification = await verifyPresentation(
challenge,
presentation,
async (verificationMethodId: string) => {
const doc = await sdk.did.resolve(presentation.holder)
const vm = doc?.verificationMethod.find((m) => m.id === verificationMethodId)
if (!vm) throw new Error('Verification method not found')
return bs58.decode(vm.publicKeyMultibase.slice(1)) // strip 'z' multibase prefix
},
)
if (!verification.valid) {
return NextResponse.json({ error: verification.error ?? 'Invalid presentation' }, { status: 401 })
}
// Verify each credential referenced in the presentation, by ID
const credentialTypes: string[] = []
for (const credential of presentation.verifiableCredential ?? []) {
const result = await sdk.credentials.verify(credential.id)
if (!result.valid) {
return NextResponse.json(
{ error: 'Invalid credential', credentialId: credential.id },
{ status: 401 }
)
}
credentialTypes.push(...credential.type)
}
// Issue this app's own session JWT
const token = await createSessionToken({
did: presentation.holder,
credentials: credentialTypes,
})
const response = NextResponse.json({
token,
did: presentation.holder,
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. İstemci Bileşeni: Kimlik Doğrulama Düğmesi
DID kimlik doğrulama akışını tetikleyen bir istemci bileşeni.
// components/solidus-auth-button.tsx
'use client'
import { useState } from 'react'
// @solidus-network/auth exports verify-side primitives (createChallenge,
// verifyPresentation) — it does not ship a browser wallet connector. Signing
// the challenge happens wherever the user's private key lives (a browser
// extension, a deep link into the Identity app, or your own key management).
// `signChallengeWithWallet` below is a placeholder for that integration.
declare function signChallengeWithWallet(
did: string,
nonce: string,
): Promise<{ verificationMethod: string; jws: string }>
interface SolidusAuthButtonProps {
did: string
onSuccess: (session: { did: string; token: string }) => void
onError?: (error: Error) => void
}
export function SolidusAuthButton({
did,
onSuccess,
onError,
}: SolidusAuthButtonProps) {
const [loading, setLoading] = useState(false)
async function handleAuth() {
setLoading(true)
try {
// Step 1: Request a challenge from the server for this DID
const challengeRes = await fetch('/api/auth/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ did }),
})
const challenge = await challengeRes.json()
// challenge = { id, did, nonce, issuedAt, expiresAt }
// Step 2: Sign the challenge nonce with the user's DID key
const { verificationMethod, jws } = await signChallengeWithWallet(did, challenge.nonce)
// Step 3: Build the VerifiablePresentation (see the
// @solidus-network/auth reference for the exact shape)
const presentation = {
'@context': ['https://www.w3.org/2018/credentials/v1'],
type: ['VerifiablePresentation'],
holder: did,
proof: {
type: 'Ed25519Signature2020',
created: new Date().toISOString(),
challenge: challenge.nonce,
proofPurpose: 'authentication',
verificationMethod,
jws,
},
}
// Step 4: Send the challenge ID + signed presentation to the server
const verifyRes = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ challengeId: challenge.id, 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. Ara Katman: Rotaları Koruma
Kimlik doğrulaması gerektiren rotaları korumak için Next.js ara katmanını kullanın.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
const PROTECTED_PATHS = ['/dashboard', '/profile', '/settings']
const secret = new TextEncoder().encode(process.env.SESSION_JWT_SECRET!)
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 { payload } = await jwtVerify(token, secret, {
audience: process.env.SOLIDUS_DOMAIN!,
})
// Attach user info to headers for downstream use
const response = NextResponse.next()
response.headers.set('x-solidus-did', payload.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. Kullanıcı Kimliğini Görüntüleme
Oturumu okuyan ve kullanıcının DID’ini ve kimlik bilgilerini görüntüleyen bir sunucu bileşeni.
// 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>
)
}Tam Proje Yapısı
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 protectionSonraki Adımlar
- Express.js Ara Katmanı — kimlik bilgisi doğrulamasıyla bağımsız bir API oluşturun
- Kimlik Bilgisi Akışı — tam kimlik bilgisi yaşam döngüsünü anlayın
- SDK Referansı — tam API belgeleri