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:

TokenPurposeDefault TTLStorage
Access tokenAuthenticates requests24 hours (configurable)HTTP-only cookie
Refresh tokenRenews access token30 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:

PropertyValue
HttpOnlyYes (not accessible via JavaScript)
SecureYes (HTTPS only)
SameSiteLax
DomainScoped to your domain
Cookie namesend_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:

CodeMeaningAction
ACCOUNT_SUSPENDEDAccount has been frozenShow suspended message
SESSION_VERIFICATION_FAILEDToken is invalidClear session, prompt login
(none)Access token expiredCall 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();
}