web-production-saas-starter/next_b2b_starter/docs/02-authentication.md
2025-12-16 18:54:41 +04:00

6.4 KiB

Authentication

This guide explains how authentication works and how to check if a user is logged in.

How Authentication Works

The app uses Stytch B2B for authentication with magic link emails. No passwords required.

Authentication Flow

graph TD
    A[User visits /dashboard] --> B{Has session cookie?}
    B -->|No| C[Redirect to /auth]
    B -->|Yes| D{JWT token valid?}
    D -->|No| E[Call /api/auth/session/refresh]
    D -->|Yes| F[Render /dashboard]
    E --> G{Refresh successful?}
    G -->|Yes| F
    G -->|No| C

    C --> H[User enters email]
    H --> I[Send magic link]
    I --> J[User clicks link in email]
    J --> K[Redirect to /authenticate]
    K --> L[Exchange token for session]
    L --> M[Set session cookies]
    M --> F

Token System

The app uses two cookies for authentication:

  1. Session Token (stytch_session)

    • HttpOnly cookie (JavaScript cannot read it)
    • Sent to Stytch backend to get JWT
    • Used for token refresh
  2. JWT Token (stytch_session_jwt)

    • Readable by JavaScript
    • Attached to API requests as Authorization: Bearer <token>
    • Contains user info and expiration

Token Lifecycle

  • Duration: 8 hours (480 minutes)
  • Auto-Refresh: Happens automatically when expired
  • Grace Period: 60 seconds (handles clock skew)

Checking if User is Authenticated

In Server Components

Use getMemberSession() to check authentication.

File: lib/auth/stytch/server.ts

import { getMemberSession } from '@/lib/auth/stytch/server';

export default async function DashboardPage() {
  const session = await getMemberSession();

  // Not authenticated
  if (!session) {
    return <div>Please log in</div>;
  }

  // Authenticated
  return <div>Welcome, {session.member.email}</div>;
}

Requiring Authentication

Use requireMemberSession() to enforce authentication. It automatically redirects if not logged in.

import { requireMemberSession } from '@/lib/auth/stytch/server';

export default async function ProtectedPage() {
  // Will redirect to /auth if not logged in
  const session = await requireMemberSession();

  return <div>Protected content for {session.member.email}</div>;
}

In Client Components

Use useStytchMember() hook from Stytch SDK.

'use client';
import { useStytchMember } from '@stytch/nextjs/b2b';

export function UserProfile() {
  const { member, isInitialized } = useStytchMember();

  if (!isInitialized) {
    return <div>Loading...</div>;
  }

  if (!member) {
    return <div>Not logged in</div>;
  }

  return <div>Hello, {member.email_address}</div>;
}

Protected vs Public Routes

Protected Routes

These routes require authentication. Defined in middleware.ts:

  • /dashboard
  • /dashboard/settings
  • Any route starting with /dashboard/

If you visit a protected route without being logged in, you'll be redirected to /auth.

Public Routes

These routes don't require authentication:

  • / (homepage)
  • /auth (login page)
  • /authenticate (magic link callback)
  • /signup (signup page)
  • /api/auth/* (auth API routes)

Login Flow

sequenceDiagram
    participant User
    participant Browser
    participant Frontend
    participant Stytch

    User->>Browser: Click "Sign In"
    Browser->>Frontend: Navigate to /auth
    Frontend-->>Browser: Show email form
    User->>Frontend: Enter email
    Frontend->>Stytch: Send magic link request
    Stytch-->>User: Email with magic link
    User->>Stytch: Click link in email
    Stytch->>Frontend: Redirect to /authenticate?token=xxx
    Frontend->>Stytch: Exchange token for session
    Stytch-->>Frontend: Return session tokens
    Frontend->>Browser: Set cookies (session + JWT)
    Frontend->>Browser: Redirect to /dashboard

Logout Flow

To log out a user, redirect them to the logout API:

// In a client component
'use client';

export function LogoutButton() {
  const handleLogout = () => {
    window.location.href = '/api/auth/logout';
  };

  return <button onClick={handleLogout}>Log Out</button>;
}

The logout API will:

  1. Clear session cookies
  2. Redirect to login page

Token Refresh

When a JWT expires, the app automatically refreshes it.

Token Refresh Flow

graph LR
    A[API Request] --> B{Token expired?}
    B -->|No| C[Make request]
    B -->|Yes| D[Call /api/auth/session/refresh]
    D --> E{Has valid session?}
    E -->|Yes| F[Get new JWT]
    E -->|No| G[Logout user]
    F --> C
    G --> H[Redirect to /auth]

This happens automatically in lib/api/api/client/token-manager.ts.

Retry Logic

If token refresh fails:

  • Retry 3 times
  • Wait 1s, then 2s, then 4s (exponential backoff)
  • Logout if all retries fail

Security Features

  • HttpOnly: Session token cannot be read by JavaScript
  • Secure: Cookies only sent over HTTPS (in production)
  • SameSite: Lax (protects against CSRF)
  • Path: / (available to all routes)

Token Storage

  • Session token: Server-side only (secure)
  • JWT token: Client + server (needed for API calls)
  • In-memory cache: Browser caches JWT to reduce cookie reads

Common Patterns

Check Auth in API Route

// app/api/data/route.ts
import { getMemberSession } from '@/lib/auth/stytch/server';

export async function GET() {
  const session = await getMemberSession();

  if (!session) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  // Authenticated logic
  return NextResponse.json({ data: 'secret data' });
}

Conditional Rendering Based on Auth

'use client';

export function ConditionalContent() {
  const { member } = useStytchMember();

  return (
    <div>
      {member ? (
        <div>Logged in as {member.email_address}</div>
      ) : (
        <a href="/auth">Please log in</a>
      )}
    </div>
  );
}

Key Files

  • middleware.ts - Route protection logic
  • lib/auth/stytch/server.ts - Server-side auth helpers
  • lib/api/api/client/token-manager.ts - Token management
  • lib/auth/constants.ts - Auth configuration
  • app/api/auth/session/refresh/route.ts - Token refresh endpoint
  • app/api/auth/logout/route.ts - Logout endpoint
  • app/auth/page.tsx - Login page
  • app/authenticate/page.tsx - Magic link callback

Next Steps

👉 Learn about: Permissions & Roles