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:
-
Session Token (
stytch_session)- HttpOnly cookie (JavaScript cannot read it)
- Sent to Stytch backend to get JWT
- Used for token refresh
-
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:
- Clear session cookies
- 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
Cookie Security
- 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 logiclib/auth/stytch/server.ts- Server-side auth helperslib/api/api/client/token-manager.ts- Token managementlib/auth/constants.ts- Auth configurationapp/api/auth/session/refresh/route.ts- Token refresh endpointapp/api/auth/logout/route.ts- Logout endpointapp/auth/page.tsx- Login pageapp/authenticate/page.tsx- Magic link callback
Next Steps
👉 Learn about: Permissions & Roles