Setup
Next.js App Router Integration
Complete examples for integrating Sigma Auth with Next.js 13+ App Router.
NextAuth.js Integration
Important: The NextAuth.js integration runs in your Next.js frontend application, not on the Sigma Auth server. The /api/auth/*
routes are served by your Next.js app via NextAuth.js, which then communicates with the Sigma Auth OAuth 2.0 endpoints.
Coming Soon: Pre-built NextAuth providers and components will be available through BigBlocks - installable via the shadcn/ui CLI. Until then, use the manual configuration below.
Installation
npm install next-auth
npm install bitcoin-auth
Configuration
import NextAuth from 'next-auth';
import type { NextAuthOptions } from 'next-auth';
const authOptions: NextAuthOptions = {
providers: [
{
id: 'sigma',
name: 'Sigma Auth',
type: 'oauth',
authorization: {
url: 'https://auth.sigmaidentity.com/authorize',
params: {
scope: 'openid profile email',
provider: 'sigma'
}
},
token: 'https://auth.sigmaidentity.com/token',
userinfo: 'https://auth.sigmaidentity.com/userinfo',
profile: (profile) => ({
id: profile.sub,
name: profile.name || profile.address,
email: profile.email,
image: profile.picture,
address: profile.address,
publicKey: profile.publicKey
}),
clientId: process.env.SIGMA_CLIENT_ID,
clientSecret: process.env.SIGMA_CLIENT_SECRET, // Optional for public clients
},
{
id: 'sigma-google',
name: 'Google via Sigma',
type: 'oauth',
authorization: {
url: 'https://auth.sigmaidentity.com/authorize',
params: {
scope: 'openid profile email',
provider: 'google'
}
},
token: 'https://auth.sigmaidentity.com/token',
userinfo: 'https://auth.sigmaidentity.com/userinfo',
profile: (profile) => ({
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
}),
clientId: process.env.SIGMA_CLIENT_ID,
}
],
callbacks: {
async jwt({ token, account, profile }) {
if (account) {
token.accessToken = account.access_token;
token.provider = account.provider;
// Store Bitcoin-specific data
if (profile?.address) {
token.bitcoinAddress = profile.address;
token.publicKey = profile.publicKey;
}
}
return token;
},
async session({ session, token }) {
return {
...session,
accessToken: token.accessToken as string,
provider: token.provider as string,
bitcoinAddress: token.bitcoinAddress as string,
publicKey: token.publicKey as string,
};
},
},
pages: {
signIn: '/auth/signin',
error: '/auth/error',
}
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Environment Variables
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-nextauth-secret
SIGMA_CLIENT_ID=your-app-name
SIGMA_CLIENT_SECRET=your-client-secret # Optional
Custom Sign In Page
'use client';
import { signIn, getProviders } from 'next-auth/react';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
export default function SignIn() {
const [providers, setProviders] = useState<any>(null);
const [loading, setLoading] = useState<string | null>(null);
useEffect(() => {
getProviders().then(setProviders);
}, []);
const handleSignIn = async (providerId: string) => {
setLoading(providerId);
try {
await signIn(providerId, { callbackUrl: '/' });
} catch (error) {
console.error('Sign in error:', error);
} finally {
setLoading(null);
}
};
if (!providers) {
return <div>Loading...</div>;
}
return (
<div className="flex min-h-screen items-center justify-center">
<Card className="w-[400px]">
<CardHeader>
<CardTitle>Sign In</CardTitle>
<CardDescription>
Choose your preferred authentication method
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{Object.values(providers).map((provider: any) => (
<Button
key={provider.id}
onClick={() => handleSignIn(provider.id)}
disabled={loading === provider.id}
className="w-full"
variant={provider.id === 'sigma' ? 'default' : 'outline'}
>
{loading === provider.id ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
) : null}
{provider.name}
</Button>
))}
</CardContent>
</Card>
</div>
);
}
Session Provider Setup
'use client';
import { SessionProvider } from 'next-auth/react';
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
import { Providers } from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
Direct API Integration
For applications that prefer direct API integration without NextAuth.js:
Auth Context
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
interface User {
id: string;
name: string;
email?: string;
image?: string;
address: string;
publicKey: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
signIn: (provider?: string) => Promise<void>;
signOut: () => void;
error: string | null;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
checkAuthStatus();
}, []);
const checkAuthStatus = async () => {
try {
const token = localStorage.getItem('access_token');
if (!token) {
setLoading(false);
return;
}
const response = await fetch('https://auth.sigmaidentity.com/userinfo', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const userData = await response.json();
setUser({
id: userData.sub,
name: userData.name,
email: userData.email,
image: userData.picture,
address: userData.address,
publicKey: userData.publicKey
});
} else {
localStorage.removeItem('access_token');
}
} catch (error) {
console.error('Auth check failed:', error);
localStorage.removeItem('access_token');
} finally {
setLoading(false);
}
};
const signIn = async (provider = 'bitcoin') => {
try {
setError(null);
// Generate PKCE parameters
const codeVerifier = generateRandomString(128);
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store for later use
sessionStorage.setItem('code_verifier', codeVerifier);
// Generate state
const state = generateRandomString(32);
sessionStorage.setItem('oauth_state', state);
// Build authorization URL
const params = new URLSearchParams({
client_id: process.env.NEXT_PUBLIC_SIGMA_CLIENT_ID!,
redirect_uri: `${window.location.origin}/auth/callback`,
response_type: 'code',
provider,
scope: 'openid profile email',
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `https://auth.sigmaidentity.com/authorize?${params}`;
} catch (error) {
setError('Failed to initiate sign in');
console.error('Sign in error:', error);
}
};
const signOut = () => {
localStorage.removeItem('access_token');
sessionStorage.clear();
setUser(null);
};
const value = {
user,
loading,
signIn,
signOut,
error
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Utility functions
function generateRandomString(length: number): string {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
OAuth Callback Handler
// app/auth/callback/page.tsx
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
export default function AuthCallback() {
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [error, setError] = useState<string>('');
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
handleCallback();
}, []);
const handleCallback = async () => {
try {
const code = searchParams.get('code');
const state = searchParams.get('state');
const error = searchParams.get('error');
if (error) {
throw new Error(`Authentication failed: ${error}`);
}
if (!code) {
throw new Error('No authorization code received');
}
// Verify state
const storedState = sessionStorage.getItem('oauth_state');
if (state !== storedState) {
throw new Error('Invalid state parameter');
}
// Exchange code for token
const codeVerifier = sessionStorage.getItem('code_verifier');
const response = await fetch('https://auth.sigmaidentity.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: process.env.NEXT_PUBLIC_SIGMA_CLIENT_ID!,
redirect_uri: `${window.location.origin}/auth/callback`,
code_verifier: codeVerifier!
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_description || 'Token exchange failed');
}
const tokens = await response.json();
// Store access token
localStorage.setItem('access_token', tokens.access_token);
// Clean up
sessionStorage.removeItem('code_verifier');
sessionStorage.removeItem('oauth_state');
setStatus('success');
// Redirect to home page
setTimeout(() => {
router.push('/');
}, 1000);
} catch (error) {
console.error('Callback error:', error);
setError(error instanceof Error ? error.message : 'Authentication failed');
setStatus('error');
}
};
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
{status === 'loading' && (
<div>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4"></div>
<p>Completing authentication...</p>
</div>
)}
{status === 'success' && (
<div>
<div className="text-green-600 text-xl mb-4">✓</div>
<p>Authentication successful! Redirecting...</p>
</div>
)}
{status === 'error' && (
<div>
<div className="text-red-600 text-xl mb-4">✗</div>
<p className="text-red-600">Authentication failed</p>
<p className="text-sm text-gray-600 mt-2">{error}</p>
<button
onClick={() => router.push('/auth/signin')}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Try Again
</button>
</div>
)}
</div>
</div>
);
}
Server-Side Components
Protected Route Wrapper
// components/protected-route.tsx
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: string;
}
export default async function ProtectedRoute({
children,
requiredRole
}: ProtectedRouteProps) {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/auth/signin');
}
// Role-based access control
if (requiredRole && session.user.role !== requiredRole) {
redirect('/unauthorized');
}
return <>{children}</>;
}
Server Action with Authentication
// app/actions/user-actions.ts
'use server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { revalidatePath } from 'next/cache';
export async function updateUserProfile(formData: FormData) {
const session = await getServerSession(authOptions);
if (!session) {
throw new Error('Unauthorized');
}
const name = formData.get('name') as string;
const bio = formData.get('bio') as string;
try {
// Make authenticated request to your API
const response = await fetch('https://your-api.example.com/user/profile', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${session.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, bio }),
});
if (!response.ok) {
throw new Error('Failed to update profile');
}
// Revalidate the profile page
revalidatePath('/profile');
return { success: true };
} catch (error) {
console.error('Profile update error:', error);
throw error;
}
}
Client Components
User Profile Component
// components/user-profile.tsx
'use client';
import { useSession, signOut } from 'next-auth/react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
export default function UserProfile() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <div>Loading...</div>;
}
if (!session) {
return <div>Please sign in to view your profile.</div>;
}
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="flex items-center gap-4">
<Avatar>
<AvatarImage src={session.user.image} />
<AvatarFallback>
{session.user.name?.charAt(0) || 'U'}
</AvatarFallback>
</Avatar>
<div>
<h3 className="text-lg font-semibold">{session.user.name}</h3>
<p className="text-sm text-muted-foreground">{session.user.email}</p>
</div>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{session.bitcoinAddress && (
<div>
<label className="text-sm font-medium">Bitcoin Address:</label>
<p className="font-mono text-sm break-all">{session.bitcoinAddress}</p>
</div>
)}
<div>
<label className="text-sm font-medium">Authentication Provider:</label>
<p className="text-sm">{session.provider}</p>
</div>
<Button
onClick={() => signOut()}
variant="outline"
className="w-full"
>
Sign Out
</Button>
</CardContent>
</Card>
);
}
Authentication Button
// components/auth-button.tsx
'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
export default function AuthButton() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <Button disabled>Loading...</Button>;
}
if (!session) {
return (
<Button onClick={() => signIn()}>
Sign In
</Button>
);
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src={session.user.image} alt={session.user.name} />
<AvatarFallback>{session.user.name?.charAt(0)}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuItem className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{session.user.name}</p>
<p className="text-xs leading-none text-muted-foreground">
{session.user.email}
</p>
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => signOut()}>
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
API Routes
Protected API Route
// app/api/protected/route.ts
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Your protected logic here
return NextResponse.json({
message: 'This is protected data',
user: session.user,
bitcoinAddress: session.bitcoinAddress,
});
}
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
try {
const body = await req.json();
// Validate input
if (!body.name) {
return NextResponse.json(
{ error: 'Name is required' },
{ status: 400 }
);
}
// Process the request...
return NextResponse.json({ success: true });
} catch (error) {
console.error('API error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Middleware
Authentication Middleware
// middleware.ts
import { withAuth } from 'next-auth/middleware';
export default withAuth(
function middleware(req) {
// Additional middleware logic here
console.log('Authenticated user:', req.nextauth.token?.email);
},
{
callbacks: {
authorized: ({ token, req }) => {
// Define which routes require authentication
const { pathname } = req.nextUrl;
// Allow public routes
if (pathname.startsWith('/auth/') || pathname === '/') {
return true;
}
// Require authentication for all other routes
return !!token;
},
},
}
);
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api/auth (NextAuth.js)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
],
};
TypeScript Types
// types/next-auth.d.ts
import 'next-auth';
declare module 'next-auth' {
interface Session {
accessToken: string;
provider: string;
bitcoinAddress?: string;
publicKey?: string;
}
interface User {
address?: string;
publicKey?: string;
}
}
declare module 'next-auth/jwt' {
interface JWT {
accessToken: string;
provider: string;
bitcoinAddress?: string;
publicKey?: string;
}
}
This comprehensive Next.js integration provides a robust foundation for building applications with Sigma Auth, supporting both NextAuth.js integration and direct API approaches.