Authentication Flows
Overview
reauth supports three authentication methods:
- Magic Link — Passwordless email login (default)
- Google OAuth — Sign in with Google
- X (Twitter) OAuth — Sign in with X
Each can be used with the hosted UI (zero frontend code) or in headless mode (custom login UI).
Magic Link Flow
Hosted UI
User clicks login → Hosted login page → Enters email →
Gets email with magic link → Clicks link → Session created → Redirected to your app
const reauth = createReauthClient({ domain: 'yourdomain.com' });
// Redirects to hosted login page
reauth.login();
// After redirect, check session
const session = await reauth.getSession();
Headless Mode
Your UI collects email → requestMagicLink({ email, callbackUrl }) →
User gets email → Clicks link → Your callback URL receives token →
verifyMagicLink({ token }) → Session created
const reauth = createReauthClient({ domain: 'yourdomain.com' });
// 1. Request magic link (user enters email in your UI)
await reauth.requestMagicLink({
email: 'user@example.com',
callbackUrl: 'https://yourdomain.com/auth/verify',
});
// 2. On your callback page, extract token from URL
const token = new URLSearchParams(window.location.search).get('token');
// 3. Verify the token — session cookies are set automatically
const result = await reauth.verifyMagicLink({ token: token! });
if (result.success) {
console.log('Logged in as:', result.email);
// Redirect to dashboard
window.location.href = result.redirectUrl || '/dashboard';
}
Google OAuth Flow
Hosted UI
User clicks "Sign in with Google" on hosted page → Google consent →
Callback to reauth → Session created → Redirected to your app
Works automatically when Google OAuth is configured in the dashboard.
Headless Mode
Your UI triggers startGoogleOAuth() → User redirected to Google →
Google redirects back to reauth portal → Portal handles callback →
User redirected to your configured redirect_url with session cookies set
const reauth = createReauthClient({ domain: 'yourdomain.com' });
// Redirects to Google for authorization
const { authUrl } = await reauth.startGoogleOAuth();
window.location.href = authUrl;
// After Google callback, the reauth portal handles the exchange
// and redirects to your configured redirect_url with session cookies set
X (Twitter) OAuth Flow
Same pattern as Google OAuth:
const { authUrl } = await reauth.startTwitterOAuth();
window.location.href = authUrl;
Account Linking
When a user signs in with OAuth and their email matches an existing account:
- If the OAuth provider is already linked, they sign in normally
- If not linked, they may be prompted to confirm the link
- Once linked, they can sign in with either method
Session Lifecycle
reauth uses a dual-token system:
| Token | Purpose | Default TTL | Storage |
|---|---|---|---|
| Access token | Authenticates requests | 24 hours (configurable) | HTTP-only cookie |
| Refresh token | Renews access token | 30 days (configurable) | HTTP-only cookie |
Session Flow
Login → Access token + Refresh token set as cookies →
Requests use access token → Access token expires →
Client calls refresh() → New access token issued →
Refresh token expires → User must log in again
Auto-Refresh (React SDK)
The useAuth hook automatically refreshes sessions:
const { user, loading } = useAuth({
domain: 'yourdomain.com',
refreshInterval: 300000, // Check every 5 minutes (default)
});
If getSession() returns valid: false without an error_code, useAuth automatically calls refresh() and retries.
Manual Refresh (Browser SDK)
const session = await reauth.getSession();
if (!session.valid && !session.error_code) {
// Access token expired, try refreshing
await reauth.refresh();
const newSession = await reauth.getSession();
}
Bearer Token for API Calls
Use getToken() to get a JWT for your own API:
const tokenResponse = await reauth.getToken();
if (tokenResponse) {
const res = await fetch('/api/my-endpoint', {
headers: {
Authorization: `Bearer ${tokenResponse.accessToken}`,
},
});
}
JWT Structure
The access token is a JWT with these claims (DomainEndUserClaims):
{
sub: string; // User ID
domain_id: string; // Domain UUID
domain: string; // Root domain (e.g., "example.com")
active_org_id: string; // Active organization ID
org_role: string; // Role in active org ("owner" | "member")
roles: string[]; // User roles (e.g., ["admin"])
subscription: {
status: string; // "active" | "trialing" | "none" | ...
plan_code: string | null; // e.g., "pro"
plan_name: string | null; // e.g., "Pro Plan"
current_period_end: number | null; // Unix timestamp
cancel_at_period_end: boolean | null;
trial_ends_at: number | null;
};
exp: number; // Expiration (Unix timestamp)
iat: number; // Issued at (Unix timestamp)
}
Subscription data is embedded in the JWT, so you can check plan status without any API call.
Cookie Model
Cookies are set by the reauth API at reauth.yourdomain.com:
| Property | Value |
|---|---|
| HttpOnly | Yes (not accessible via JavaScript) |
| Secure | Yes (HTTPS only) |
| SameSite | Lax |
| Domain | Scoped to your domain |
| Cookie names | end_user_access_token, end_user_refresh_token |
The server SDK's extractToken() automatically reads the end_user_access_token cookie, or falls back to the Authorization: Bearer header.
Error Codes
The session response may include an error_code:
| Code | Meaning | Action |
|---|---|---|
ACCOUNT_SUSPENDED | Account has been frozen | Show suspended message |
SESSION_VERIFICATION_FAILED | Token is invalid | Clear session, prompt login |
| (none) | Access token expired | Call refresh() and retry |
const session = await reauth.getSession();
if (session.error_code === 'ACCOUNT_SUSPENDED') {
// Show account suspended UI
} else if (!session.valid) {
// Try refreshing
await reauth.refresh();
}