Skip to content

API Routes

The ByteAuth Next.js SDK provides route handlers for QR code generation, webhook processing, and status polling.

Create app/api/byteauth/[...byteauth]/route.ts:

import { ByteAuthHandler } from '@bytefederal/byteauth-nextjs';
export const { GET, POST } = ByteAuthHandler({
callbacks: {
async onLogin(user) {
console.log('User logged in:', user.publicKey);
return user;
},
async onRegister(user) {
console.log('New user registered:', user.publicKey);
return user;
},
},
});

Create pages/api/byteauth/[...byteauth].ts:

import { ByteAuthHandler } from '@bytefederal/byteauth-nextjs';
export default ByteAuthHandler({
callbacks: {
async onLogin(user) {
return user;
},
async onRegister(user) {
return user;
},
},
});

The handler creates these endpoints:

EndpointMethodDescription
/api/byteauth/qrGETGenerate QR code
/api/byteauth/webhook/loginPOSTLogin webhook
/api/byteauth/webhook/registerPOSTRegistration webhook
/api/byteauth/checkGETPoll authentication status

Generates a new session with QR code data.

Query Parameters:

  • mode - login or register (default: login)
  • sid - Existing session ID to reuse (optional)

Response:

{
"qr": "https://yourapp.com/webhook?session=abc123&challenge=xyz...",
"sid": "sess_abc123",
"status": 0,
"expiresAt": 1699876573
}

Override the default implementation:

app/api/byteauth/qr/route.ts
import { generateQRSession } from '@bytefederal/byteauth-nextjs';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const mode = searchParams.get('mode') || 'login';
const session = await generateQRSession({
mode,
domain: process.env.BYTEAUTH_DOMAIN!,
challengeLifetime: 30,
metadata: {
// Custom data to include
returnUrl: searchParams.get('returnUrl'),
},
});
return NextResponse.json({
qr: session.qrData,
sid: session.id,
status: 0,
expiresAt: session.expiresAt,
});
}

Handles login authentication from ByteVault.

Request Body:

{
"public_key": "04a1b2c3...",
"signature": "3045022100...",
"challenge": "Sign this to login...",
"timestamp": 1699876545
}

Response:

{
"status": "authenticated",
"message": "Login successful"
}
app/api/byteauth/webhook/login/route.ts
import { verifySignature, findSession, type WebhookPayload } from '@bytefederal/byteauth-nextjs';
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function POST(request: NextRequest) {
const payload: WebhookPayload = await request.json();
// 1. Verify timestamp
if (Math.abs(Date.now() / 1000 - payload.timestamp) > 30) {
return NextResponse.json(
{ error: 'Challenge expired' },
{ status: 408 }
);
}
// 2. Find session by challenge
const session = await findSession(payload.challenge);
if (!session) {
return NextResponse.json(
{ error: 'Challenge not found' },
{ status: 404 }
);
}
// 3. Verify signature
const isValid = await verifySignature(
payload.public_key,
payload.signature,
payload.challenge
);
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 406 }
);
}
// 4. Find user
const user = await prisma.user.findUnique({
where: { publicKey: payload.public_key },
});
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
// 5. Mark session as authenticated
await prisma.byteAuthSession.update({
where: { id: session.id },
data: {
userId: user.id,
authenticatedAt: new Date(),
},
});
// 6. Custom logic
await prisma.user.update({
where: { id: user.id },
data: { lastLoginAt: new Date() },
});
return NextResponse.json({
status: 'authenticated',
message: 'Login successful',
});
}

Client polls this endpoint to check authentication status.

Query Parameters:

  • sid - Session ID

Response (pending):

{
"status": "pending",
"sid": "sess_abc123"
}

Response (authenticated):

{
"status": "authenticated",
"user": {
"id": "user_123",
"publicKey": "04a1b2c3..."
},
"redirect": "/dashboard"
}
app/api/byteauth/check/route.ts
import { getSession } from '@bytefederal/byteauth-nextjs';
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const sessionId = searchParams.get('sid');
if (!sessionId) {
return NextResponse.json(
{ error: 'Session ID required' },
{ status: 400 }
);
}
const session = await getSession(sessionId);
if (!session) {
return NextResponse.json(
{ status: 'not_found' },
{ status: 404 }
);
}
if (session.authenticatedAt && session.user) {
// Set auth cookie
const cookieStore = cookies();
cookieStore.set('auth-token', session.user.id, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30, // 30 days
});
return NextResponse.json({
status: 'authenticated',
user: {
id: session.user.id,
publicKey: session.user.publicKey,
},
redirect: '/dashboard',
});
}
return NextResponse.json({
status: 'pending',
sid: sessionId,
});
}

Verify that webhooks come from ByteAuth:

import { verifyWebhookSignature } from '@bytefederal/byteauth-nextjs';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const signature = request.headers.get('X-ByteAuth-Signature');
const body = await request.text();
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
);
}
const isValid = verifyWebhookSignature(
body,
signature,
process.env.BYTEAUTH_WEBHOOK_SECRET!
);
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Process webhook...
}

Handle errors gracefully:

import { ByteAuthError } from '@bytefederal/byteauth-nextjs';
export async function POST(request: NextRequest) {
try {
// ... handle webhook
} catch (error) {
if (error instanceof ByteAuthError) {
return NextResponse.json(
{ error: error.message },
{ status: error.statusCode }
);
}
console.error('Unexpected error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

Protect endpoints from abuse:

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { NextRequest, NextResponse } from 'next/server';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
});
export async function GET(request: NextRequest) {
const ip = request.ip ?? '127.0.0.1';
const { success, limit, remaining } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
},
}
);
}
// Process request...
}