Sigma Auth
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

package-install.sh
npm install next-auth
npm install bitcoin-auth

Configuration

app/api/auth/[...nextauth]/route.ts
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

.env.local
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

app/auth/signin/page.tsx
'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

app/providers.tsx
'use client';

import { SessionProvider } from 'next-auth/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}
app/layout.tsx
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

lib/auth-context.tsx
'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.