Owner: Engineering Team | Last Updated: 2026-01-30 | Status: Current
Warning: OAuth client IDs differ per environment (local, staging, production). Never use production keys for local development. See Environment Variables for the correct values.
Authentication is handled by NextAuth.js 4.24.11 configured in server/auth.ts. The app supports three authentication providers: email/password credentials, Google OAuth, and Facebook OAuth. JWT tokens are used for session management with automatic refresh. TOTP-based 2FA is supported for the credentials provider.
POST /api/user/login/ on the Django backenddata.result.totp === true, the login response includes jwt_credentials for the 2FA challengeX-Forwarded-For and X-Client-IP headers are set from getClientIp()GoogleProvider with GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRETPOST /api/user/login/google/FacebookProvider with FACEBOOK_CLIENT_ID and FACEBOOK_CLIENT_SECRETPOST /api/user/login/facebook/From server/auth.ts:
// Check for 2FA requirement
if (data.result.totp === true && data.result.jwt_credentials) {
return {
jwt_credentials: data.result.jwt_credentials,
user: data.result.user,
totp: true,
}
}
When totp: true is returned, the frontend redirects to a 2FA verification page where the user enters their TOTP code along with the jwt_credentials token.
When a user successfully authenticates, the JWT callback stores:
token.user = user.user // UserObject
token.access_token = user.access // JWT access token
token.refresh_token = user.refresh // JWT refresh token
token.extension_token = user.extension_token // Browser extension token
token.accessTokenExpires = Date.now() + 3600000 // 1 hour from now
Tokens are refreshed 5 minutes before expiry:
const shouldRefreshToken = Date.now() >= expires - 5 * 60 * 1000
if (shouldRefreshToken && token.refresh_token) {
const response = await api.post('/api/user/refresh/', {
refresh: token.refresh_token,
email: token.user.email,
extension: hasExtensionFlag ? true : undefined,
})
// Update tokens from response
}
The session callback refreshes the user data on every session access:
POST /api/user/refresh/GET /api/user/account/If refresh fails with 401 or 500+, throws RefreshAccessTokenError.
From next-auth.d.ts — the complete user data structure:
interface UserObject {
id: string | null
first_name: string | null
last_name: string | null
email: string | null
avatar: string | null
plan: 'free' | 'starter' | 'pro' | 'unlimited'
period: 'Y' | 'M' | null | '3Y'
credit: number
is_staff: boolean
isLoggedIn: boolean
is_trial_end: boolean
stripe_customer_id: string | null
is_unlocked: boolean
next_billing_date: string | null
detector_credits: number
referral: Referral
limit_monthly: number
limit_per_request: number
ai_detector_monthly_limit: number
ai_detector_per_request_limit: number
total_word_used_current_period: number
email_confirmed: boolean
login_provider: 'google' | 'facebook' | 'email' | null
downgraded_to_free: boolean
previous_paid_plan_details: PreviousPaidPlanDetails | null
trial_ended_without_purchase: boolean
subscription_paused: boolean
can_pause_subscription: boolean
subscription_resumes_at: number | null
subscription_status: 'active' | 'trailing' | 'canceled'
plan_monthly_limit: number
plan_per_request_limit: number
// ... additional fields
}
plan: 'free' | 'starter' | 'pro' | 'unlimited'
period: 'Y' | 'M' | null | '3Y' // Yearly, Monthly, null (free), 3-Year
interface Session extends DefaultSession {
user: UserObject & DefaultSession['user']
access_token: string // JWT access token for API calls
extension_token: string // Browser extension auth token
extension?: boolean // Has extension cookie
error?: string // Auth error (e.g., "RefreshAccessTokenError")
accessTokenExpires?: number // Token expiry timestamp
}
interface JWT {
user: UserObject
email: string
access_token: string
refresh_token: string
extension_token: string
hasExtensionFlag?: boolean
accessTokenExpires?: number
error?: string
}
From server/auth.ts — errors are extracted from multiple possible fields:
throw new Error(
axiosError.response?.data?.error ||
axiosError.response?.data?.email?.[0] ||
axiosError.response?.data?.detail ||
axiosError.response?.data?.message ||
'Authentication failed'
)
Thrown when token refresh fails (401, 500+). The frontend should catch this and redirect to login:
throw new Error('RefreshAccessTokenError')
pages: {
signIn: '/login',
error: '/login',
}
Both sign-in and error routes redirect to /login.
The auth system supports a browser extension flow:
extension cookie via hasExtensionCookie()extension: true in refresh requestsextension_token (from browser_extension_auth_token) in the sessionsession.extension = true flag| Variable | Purpose |
|---|---|
NEXTAUTH_SECRET |
NextAuth encryption secret |
NEXTAUTH_URL |
NextAuth base URL |
GOOGLE_CLIENT_ID |
Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret |
FACEBOOK_CLIENT_ID |
Facebook OAuth client ID |
FACEBOOK_CLIENT_SECRET |
Facebook OAuth client secret |
NEXT_PUBLIC_BACKEND_URL |
Django backend URL (used by api instance) |
WALTER_INTERNAL_KEY |
Internal API key (server-side only) |
| Date | Author | Change |
|---|---|---|
| 2026-01-30 | Admin | Initial creation |
| 2026-01-30 | Docs team | Rewritten with real code: 3 providers, TOTP/2FA flow, JWT lifecycle, UserObject interface, session/JWT types, error handling, extension support |
Prev: Web App - Document Management | Next: Web App - Payments | Up: WalterWrites