Authentication
This document explains the authentication system used in the Rhesis frontend application.
Authentication Architecture
The Rhesis frontend uses NextAuth.js (src/auth.ts) to manage authentication sessions. The backend provides a native authentication system with support for:
- Email/password login and registration
- OAuth login (Google, GitHub) — the OAuth handshake is owned entirely by the backend
- Magic link (passwordless) login
- Automatic access token refresh via refresh token rotation
All sensitive tokens (access token, refresh token) are stored inside NextAuth’s httpOnly, encrypted session cookie and are never exposed to client-side JavaScript or passed through a NextAuth client call directly.
Authentication Flow
Every login path — email/password, OAuth, and magic link — ends the same way: the backend hands the frontend a short-lived, single-use auth code (60-second TTL), which the frontend exchanges server-side for the real tokens. Raw tokens never appear in a signIn() call or a redirect URL.
Email/Password Login
- User submits credentials via the
AuthFormcomponent - Frontend calls
POST /auth/login/email, receiving anauth_code - Frontend calls
signIn('credentials', { code: auth_code, redirect: false }) - NextAuth’s
authorize()callback exchanges the code server-side viaPOST /auth/exchange-code, gets back{ session_token, refresh_token }, and decodes the JWT locally to build the session - NextAuth stores both tokens in its httpOnly session cookie
OAuth Login (Google, GitHub)
- User clicks “Sign in with Google” or “Sign in with GitHub”
- Frontend redirects to
GET /auth/login/{provider} - Backend redirects to the OAuth provider, then handles the callback at
GET /auth/callback - Backend wraps the tokens in the same short-lived auth code and redirects to the frontend
- Frontend calls
signIn('credentials', { code: auth_code }), which exchanges the code exactly as in the email/password flow
Magic Link Login
- User enters their email address
- Frontend calls
POST /auth/magic-link - Backend sends a single-use login link via email
- User clicks the link; frontend calls
POST /auth/magic-link/verify, receiving anauth_code - Frontend calls
signIn('credentials', { code: auth_code })
Session Management
Credentials Provider
NextAuth’s CredentialsProvider takes only an opaque code, never raw tokens:
httpOnly Cookie Security
Automatic Token Refresh
- On every request, the token is refreshed if it’s within 60 seconds of expiry
- If so,
POST /auth/refreshis called with the stored refresh token - The new access token and rotated refresh token replace the old values in the cookie
- This is transparent to the user — access tokens last 15 minutes, sessions up to 7 days
If the refresh fails, token.error = 'RefreshTokenError' is set on the JWT and propagated to the session object; src/proxy.ts reacts to it by clearing the session and redirecting home with force_logout=true.
Route Protection
Next.js 16 renamed the edge middleware convention from middleware.ts to src/proxy.ts:
- Public paths: driven by
isPublicPath()insrc/constants/paths.ts(/,/auth/*, static assets) - Protected routes: everything under
(protected)/requires a session - Onboarding redirect: if the decoded token has no organization, the user is redirected to
/onboarding
proxy.ts decodes the session JWT locally rather than calling the backend on every request — it only makes a network call (POST /auth/refresh) when the token is close to expiry. POST /auth/verify is a real backend endpoint, but it isn’t called on this request path.
Authentication Components
AuthForm
components/auth/AuthForm.tsx handles both login and registration:
- Email/password form with validation
- OAuth provider buttons (Google, GitHub) — shown only when the provider is enabled, per
GET /auth/providers(fetched via the same-origin/api/auth-configroute on mount, not literally at app startup) - Magic link option
- Toggle between login and registration modes
Session Establishment Pattern
Every place that establishes a session calls signIn('credentials', { code }) with the auth code, never with raw tokens:
Files that use this pattern:
components/auth/AuthForm.tsx— Email/password login and registrationapp/auth/signin/page.tsx— OAuth callback (exchanges auth code for a session)app/auth/magic-link/page.tsx— Magic link verificationapp/auth/verify-email/page.tsx— Email verification
Environment Variables
Google/GitHub OAuth client credentials (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GH_CLIENT_ID, GH_CLIENT_SECRET) are backend environment variables — the frontend never talks to the OAuth providers itself, it only redirects the browser to the backend’s GET /auth/login/{provider}, which owns the whole handshake.
Security Considerations
- httpOnly cookies: tokens are never exposed to client-side JavaScript
- Auth-code exchange: OAuth/email/magic-link flows hand off a short-lived (60s), single-use code instead of raw tokens, so tokens never appear in a redirect URL or client-side
signIn()call - Short-lived access tokens: access tokens expire in 15 minutes, limiting the impact of XSS
- HTTPS: always use HTTPS in production to protect cookies in transit
- Token refresh: automatic refresh (within 60s of expiry) maintains sessions without long-lived tokens
- CORS: backend CORS configuration restricts which origins can make API requests