Auth.js Configuration
The token provider, refresh, sign-out and the proxy.
Provider Setup
Auth.js is configured in lib/auth/providers.ts. Its one Credentials provider takes tokens allauth has just issued, never a password. The server actions in lib/auth/actions.ts get the tokens (the password step, then the code step) and pass them on:
Credentials({
name: "allauth",
credentials: { access_token: {}, refresh_token: {} },
async authorize(credentials) {
const tokens = tokensSchema.safeParse(credentials);
if (!tokens.success) return null;
const { access_token, refresh_token } = tokens.data;
// Accepted only if the backend accepts them; this also says whose they are.
const response = await fetch(`${getApiURL()}/auth/users/me/`, {
headers: { Authorization: bearer(access_token) },
cache: "no-store",
});
if (!response.ok) return null;
const me = userDetailsSchema.parse(await response.json());
return {
id: String(me.id),
name: me.name,
email: me.email,
accessToken: access_token,
refreshToken: refresh_token,
accessTokenExpires: expiresAt(access_token), // from the token's exp claim
};
},
}),JWT Callback
async jwt({ token, user, account }) {
// Signing in (again, after a password change): take the new tokens.
if (account && user) {
token.accessToken = user.accessToken;
token.refreshToken = user.refreshToken;
token.accessTokenExpires = user.accessTokenExpires;
token.user = { id: user.id, name: user.name, email: user.email };
}
// Refresh a minute before the access token expires.
if (Date.now() < token.accessTokenExpires - 60_000) return token;
try {
const refreshed = await refreshToken(token.refreshToken); // POST /_allauth/app/v1/tokens/refresh
token.accessToken = refreshed.access;
token.accessTokenExpires = expiresAt(refreshed.access);
token.refreshToken = refreshed.refresh; // the old one no longer works
} catch {
delete token.accessToken;
token.error = "RefreshAccessTokenError";
}
return token;
},Session Callback
The session keeps the access token for the server's own calls. What the browser receives leaves it out: the [...nextauth] route strips it from /api/auth/session, and NextAuthSesionProvider strips it before handing the session to SessionProvider.
async session({ session, token }) {
session.user = token.user;
session.accessToken = token.error ? undefined : token.accessToken; // server only
session.error = token.error;
return session;
},Signing Out
Every sign-out also ends the sign-in on the backend, so the tokens in the cookie stop working even if a copy survives somewhere:
events: {
async signOut(message) {
// DELETE /_allauth/app/v1/auth/session, refreshing first if the access token has expired
},
},The SessionErrorHandler component signs out when a refresh has failed (RefreshAccessTokenError).
The Proxy
proxy.ts runs Auth.js on the app's pages and API routes: signed-out visits go to /login, and API calls get a 401. It's also the one place a refreshed token can be saved to the cookie. When Auth.js refreshes there, the proxy applies the new cookie to the request as well, so the page or route handling it reads the new token too. Otherwise it would refresh again with a refresh token that has just been used, and sign the person out.
Session Strategy
session: {
strategy: "jwt", // no database: the session lives in an encrypted cookie
maxAge: 14 * 24 * 60 * 60, // as long as the backend's refresh token
},
pages: {
signIn: "/login",
},