Token Flow
Signing in, the code step, refreshing tokens, changing the password and signing out.
Token Lifecycle
Sign in (email + password, then a code if two-factor is on)
│
▼
allauth issues {access_token, refresh_token}
│
▼
Auth.js keeps both in its encrypted cookie
│
▼
Server code sends the access token: Authorization: Bearer <token>
│
├── Token valid → Request succeeds
│
└── Token expiring (< 60s left) → Refresh
│
▼
POST /_allauth/app/v1/tokens/refresh → {access_token, refresh_token}
│
├── Success → Keep both new tokens, continue
└── Failure → Session ends → /login| Token | Lifetime |
|---|---|
| Access | 15 minutes |
| Refresh | 14 days, and each one works once |
Every token belongs to a sign-in on the backend, checked on every request. Signing out, changing the password, or a password reset ends a sign-in's tokens straight away.
Refreshing saves the new tokens to the cookie only when it happens in the proxy, which runs on the app's pages and API routes and passes the new cookie on to the request it's handling.
Server-Side Token Access
Server components, route handlers and server actions get the access token with getAccessToken():
// lib/auth/utils.ts
export async function getAccessToken(): Promise<string | undefined> {
const session = await auth();
return session?.accessToken;
}Most code doesn't call it directly: backendFetch, relayToBackend (lib/api/backend.ts) and relayToAllauth (lib/auth/allauth.ts) send it for you.
export default async function Page() {
const workspaces = await backendJson<Workspace[]>("/api/workspaces/");
// ...
}The browser never gets the token. Client components call the app's own /api/... routes, and the session cookie does the rest.
Login Flow
The form calls server actions in lib/auth/actions.ts:
const result = await loginAction({ email, password });
if (result.success) router.push(returnUrl);
else if (result.step === "code") showCodeStep(); // two-factor is on
else toast.error(result.error);
// The code step
const result = await verifyCodeAction(code); // a code from the app, or a recovery codeloginAction calls allauth's auth/login. With two-factor on, allauth answers with a session token for the code step, which waits in an httpOnly cookie for five minutes. verifyCodeAction sends it with the code to auth/2fa/authenticate. Either way, the tokens go to signIn("credentials", tokens), which checks them with /auth/users/me/ and starts the session.
The actions pass the browser's IP and User-Agent on to allauth (X-Client-IP), for its rate limits and the security emails it sends.
Changing the Password
changePasswordAction calls allauth's account/password/change. Other devices are signed out. The sign-in that made the change gets a new key on the backend, which ends its tokens too, so the action trades allauth's new session token for new tokens (/auth/tokens/from-session/) and starts the Auth.js session again with them. You stay signed in.
Signing Out
signOut() from anywhere (the user menu, a failed refresh, deleting the account) also ends the sign-in on the backend, through Auth.js's events.signOut.
Registration Flow
Registration goes directly to the Django backend's djoser endpoints (no Auth.js involvement):
1. POST /auth/users/ → Create user account
2. Backend sends activation email
3. User clicks activation link → /user-activation/[uid]/[token]
4. POST /auth/users/activation/ → Activate account
5. User can now sign inClient-Side Session Access
Client components read who's signed in with useSession:
"use client";
import { useSession } from "next-auth/react";
function UserMenu() {
const { data: session, status } = useSession();
if (status === "loading") return <Skeleton />;
if (!session) return <LoginButton />;
return <span>{session.user.email}</span>;
}