Owner: Engineering Team | Last Updated: 2026-01-30 | Status: Current
Scope: This page covers the cross-platform authentication architecture. For backend implementation details, see Backend - Authentication. For the Chrome Extension auth flow, see Chrome Extension - Authentication.
WWAI uses a JWT-based authentication system that spans all client applications. Authentication is implemented via NextAuth.js on the Next.js web app, with the Django backend serving as the source of truth for user credentials, token issuance, and token refresh. All login flows -- credentials, Google OAuth, and Facebook OAuth -- ultimately resolve through Django API endpoints that return JWT tokens (access, refresh, and extension).
This page is the definitive reference for how authentication works in WWAI. It covers provider configuration, token lifecycle, 2FA/TOTP, session structure, middleware behavior, error handling, and browser extension support.
┌──────────────────────────────────────────────────────────┐
│ Client Applications │
│ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌────────────────┐ │
│ │ Web App │ │Mobile App│ │Chrome │ │ Shopify/WP │ │
│ │(NextAuth)│ │(Flutter) │ │Plugin │ │ Plugins │ │
│ └────┬─────┘ └────┬─────┘ └───┬────┘ └───────┬────────┘ │
└───────┼─────────────┼───────────┼──────────────┼──────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────────────────┐
│ Django Backend │
│ ┌──────────────────────────────────────────┐ │
│ │ Authentication Service │ │
│ │ • POST /api/user/login/ │ │
│ │ • POST /api/user/signup/ │ │
│ │ • POST /api/user/refresh/ │ │
│ │ • POST /api/user/login/google/ │ │
│ │ • POST /api/user/login/facebook/ │ │
│ │ • POST /api/user/set-extension-cookie/ │ │
│ │ • GET /api/user/account/ │ │
│ └──────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
All three providers are configured in server/auth.ts and delegate to Django backend endpoints.
The credentials provider handles traditional email and password login.
| Property | Value |
|---|---|
| Backend Endpoint | POST /api/user/login/ |
| Request Body | { email, password, fingerprint?, captcha? } |
| Request Headers | x-forwarded-for, x-client-ip, user-agent, Content-Type: application/json |
Request details:
fingerprint field is included in the request body when a device fingerprint is available (via FingerprintJS).captcha field is included when a Cloudflare Turnstile token has been obtained.x-forwarded-for, x-client-ip) are forwarded from the incoming request for server-side audit logging and rate limiting.Response on success:
The Django backend returns JWT tokens (access_token, refresh_token, extension_token) and user data -- unless 2FA is enabled (see TOTP/2FA Flow below).
| Property | Value |
|---|---|
| NextAuth Provider | GoogleProvider |
| Environment Variables | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
| Backend Endpoint | POST /api/user/login/google/ |
| Payload Sent to Backend | { access_token: account.access_token, id_token: account.id_token } |
After Google completes the OAuth flow and returns tokens to NextAuth, the signIn callback forwards both the access_token and id_token to the Django backend for verification and session creation.
| Property | Value |
|---|---|
| NextAuth Provider | FacebookProvider |
| Environment Variables | FACEBOOK_CLIENT_ID, FACEBOOK_CLIENT_SECRET |
| Backend Endpoint | POST /api/user/login/facebook/ |
| Payload Sent to Backend | { access_token: account.access_token } |
Facebook login sends only the access_token (no id_token) to the Django backend.
| Method | Provider | Backend Endpoint | Tokens Sent |
|---|---|---|---|
| Email/Password | Credentials | POST /api/user/login/ |
N/A (sends email + password) |
| OAuth 2.0 | POST /api/user/login/google/ |
access_token + id_token |
|
| OAuth | POST /api/user/login/facebook/ |
access_token only |
| Token | Purpose | Lifetime | Storage |
|---|---|---|---|
| access_token | API authorization bearer token | 1 hour | NextAuth JWT (server-side cookie) |
| refresh_token | Obtain new access tokens | Longer-lived (backend-controlled) | NextAuth JWT (server-side cookie) |
| extension_token | Browser extension authentication | Backend-controlled | Separate cookie + NextAuth JWT |
On first login (any provider), the JWT callback stores:
token.access_token = backend response access token
token.refresh_token = backend response refresh token
token.extension_token = backend response extension token
token.user = user object from backend
token.accessTokenExpires = Date.now() + (60 * 60 * 1000) // now + 1 hour
┌─────────────┐ Token valid? ┌───────────────┐
│ API Request │────────────────────► │ Return cached │
│ (any page) │ Yes (> 5min left) │ token as-is │
└──────┬───────┘ └───────────────┘
│
│ No (within 5 minutes of expiry)
▼
┌──────────────────┐ Success ┌───────────────────────┐
│ POST /api/user/ │─────────────►│ Update token: │
│ refresh/ │ │ • new access_token │
│ { refresh: ... } │ │ • new refresh_token │
└──────┬───────────┘ │ • reset expiry to │
│ │ now + 1 hour │
│ Failure └───────────────────────┘
▼
┌──────────────────────────┐
│ token.error = │
│ 'RefreshAccessTokenError'│
│ → session invalidated │
└──────────────────────────┘
Refresh trigger condition:
Date.now() > token.accessTokenExpires - (5 * 60 * 1000)
This means the token is refreshed 5 minutes before it actually expires, preventing edge cases where a valid token expires mid-request.
Refresh endpoint:
POST /api/user/refresh/
Content-Type: application/json
{ "refresh": "<refresh_token>" }
On refresh success: The response provides a new access_token and refresh_token. The accessTokenExpires value is reset to Date.now() + 60 * 60 * 1000 (one hour from now).
On refresh failure: The token object is tagged with token.error = 'RefreshAccessTokenError'. This propagates to the session callback, which returns an empty or error session, forcing the user to re-authenticate.
The JWT callback in server/auth.ts handles five scenarios:
trigger === 'signIn'): Stores all tokens and user data from the backend response. Sets accessTokenExpires.trigger === 'update'): Merges the incoming session update data into the existing token. This is used when client-side code calls update() to push new user data into the session.token.error is already set, the error propagates on every subsequent call until the user signs in again.Two-factor authentication uses Time-based One-Time Passwords (TOTP). The flow is triggered during credentials login when the user has 2FA enabled.
┌──────────┐ email/password ┌───────────────┐
│ Login │──────────────────► │ Django Backend │
│ Form │ │ /api/user/ │
└──────────┘ │ login/ │
└───────┬────────┘
│
┌────────────┴────────────┐
│ │
2FA disabled 2FA enabled
(totp: false) (totp: true)
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ Return full │ │ Return: │
│ session tokens │ │ • totp: true │
│ (access, │ │ • jwt_credentials │
│ refresh, │ │ (temporary token) │
│ extension) │ └──────────┬───────────┘
└─────────────────┘ │
▼
┌───────────────────┐
│ Frontend shows │
│ TOTP code input │
└────────┬──────────┘
│
User enters 6-digit code
│
▼
┌───────────────────┐
│ Verify TOTP code │
│ + jwt_credentials │
│ with backend │
└────────┬──────────┘
│
┌────────┴────────┐
│ │
Valid Invalid
│ │
▼ ▼
┌──────────────┐ ┌────────────┐
│ Return full │ │ Show error │
│ session │ │ message │
│ tokens │ └────────────┘
└──────────────┘
totp is enabled for the account.data.result.totp === true, the backend returns a temporary jwt_credentials token instead of full session tokens.totp: true and returns a partial result to the frontend.jwt_credentials token to the backend verification endpoint.access_token, refresh_token, and extension_token.The jwt_credentials token is a short-lived, scoped token that can only be used for TOTP verification. It cannot be used to access any other API endpoint. This prevents a compromised first factor from granting API access without the second factor.
The NextAuth session and JWT types are extended in next-auth.d.ts.
Session interface:
interface Session {
user: UserObject & {
name?: string
email?: string
image?: string
}
access_token: string
refresh_token: string
extension_token: string
error?: string // Set to 'RefreshAccessTokenError' on token refresh failure
}
UserObject interface (key fields):
interface UserObject {
id: number
email: string
first_name: string
last_name: string
// Subscription & plan
plan: 'free' | 'starter' | 'pro' | 'unlimited'
period: 'Y' | 'M' | null | '3Y'
subscription_status: 'active' | 'trailing' | 'canceled'
subscription_paused: boolean
stripe_customer_id: string
// Usage & limits
credit: number
detector_credits: number
limit_monthly: number
limit_per_request: number
plan_monthly_limit: number
plan_per_request_limit: number
// Security
totp: boolean // Whether 2FA is enabled
has_password: boolean // false for social-only accounts (Google/Facebook sign-up)
// Profile
avatar: string
// Additional fields: referral tracking, usage counters, etc.
}
The session callback runs on every session access (every page load, every getSession() or useSession() call). It performs the following:
POST /api/user/refresh/ to get fresh tokens.access_token to call GET /api/user/account/ to fetch the latest user data.session.user -- this ensures the session always reflects the current plan, credit balance, subscription status, and other mutable fields.token.error === 'RefreshAccessTokenError', returns an empty/error session, which triggers re-authentication on the frontend.This design ensures that subscription changes, credit purchases, and plan upgrades are reflected immediately without requiring the user to log out and back in.
middleware.tsnext-auth/middleware via withAuthThe following routes bypass authentication and are accessible without a session:
| Route Pattern | Purpose |
|---|---|
/ |
Landing page |
/login |
Login page |
/register |
Registration page |
/checkout |
Checkout flow |
/pricing |
Pricing page |
/forgot-password |
Password reset request |
/reset-password |
Password reset form |
/verify |
Email verification |
/account-deleted |
Account deletion confirmation |
/embed* |
Embedded widget pages |
/trial-started-email-signup |
Trial flow |
/register-verification |
Registration verification |
/payment-failed-redirect/.+ |
Payment failure redirects |
/wp-plugin-callback |
WordPress plugin OAuth callback |
/account-setup |
Initial account setup |
All routes not listed above require authentication. If no valid session exists, the middleware redirects the user to /login.
extension=true as a query parameter, the middleware preserves this flag through any redirects. This ensures the browser extension login flow completes correctly after authentication.The Chrome browser extension authenticates through the same flow as the web app but uses a dedicated extension token and cookie mechanism.
hasExtensionCookie() checks whether the browser extension cookie is present in the request.extension_token is stored as a separate field in the NextAuth JWT and exposed via session.extension_token.signIn callback sends a request to POST /api/user/set-extension-cookie/ to establish the extension authentication cookie.The extension token exists separately from the access token because:
When a login request fails, the Django backend may return error messages in several different response shapes. The auth configuration checks the following fields in order:
response.data.error -- general error stringresponse.data.email[0] -- email validation error (first element of array)response.data.detail -- DRF detail messageresponse.data.message -- generic message field| Scenario | Behavior |
|---|---|
| Invalid credentials | authorize() returns null; NextAuth redirects to login page |
| Backend unreachable | Caught by try/catch; logged to console.error; returns null |
| Token refresh failure | token.error set to 'RefreshAccessTokenError'; session callback returns error session |
| OAuth provider failure | NextAuth built-in error handling; redirects to error page |
| TOTP verification failure | Backend returns error; frontend displays error message on TOTP form |
| Rate limited | Backend returns 429; error extracted and displayed to user |
All authentication operations are wrapped in try/catch blocks. Errors are logged to console.error but never thrown in a way that would crash the server-side rendering process. This ensures that auth failures degrade gracefully rather than causing 500 errors.
| Layer | Implementation | Purpose |
|---|---|---|
| CAPTCHA | Cloudflare Turnstile | Prevents automated bot registration and login attempts |
| Device Fingerprinting | FingerprintJS | Identifies devices for fraud detection and suspicious login alerts |
| IP Tracking | X-Forwarded-For / X-Client-IP headers |
Audit logging, geo-based rate limiting, suspicious activity detection |
| TOTP 2FA | RFC 6238 time-based one-time passwords | Second authentication factor for accounts with 2FA enabled |
| Rate Limiting | Backend API rate limits | Prevents brute-force attacks on auth endpoints |
| JWT Expiry | 1-hour access tokens with 5-minute early refresh | Limits window of exposure for compromised tokens |
| Scoped TOTP Token | jwt_credentials temporary token |
Ensures first-factor-only compromise cannot access API |
The authentication system implements multiple overlapping security mechanisms:
Request Flow:
Client → Cloudflare Turnstile (CAPTCHA)
→ FingerprintJS (device ID)
→ IP headers (audit trail)
→ Credentials validation (Django)
→ TOTP verification (if enabled)
→ JWT issuance (scoped, time-limited)
→ Token refresh (rolling, pre-expiry)
Each layer operates independently, so the failure of any single layer does not compromise the overall security posture.
| File | Purpose |
|---|---|
server/auth.ts |
NextAuth configuration: providers, JWT callback, session callback, sign-in callback |
next-auth.d.ts |
TypeScript type extensions for Session, JWT, UserObject |
middleware.ts |
Route protection, public page allowlist, extension parameter handling |
| Date | Author | Change |
|---|---|---|
| 2026-01-30 | Admin | Major rewrite: added real implementation details from server/auth.ts including JWT lifecycle, TOTP/2FA flow, session structure, middleware behavior, error handling, and browser extension support |
| 2026-01-30 | Admin | Initial creation |
Next: JWT Token Flow | Up: WalterWrites