Authentication
User context — Cache & Invalidation
When to call /me, React caching strategy, invalidation after token refresh and best practices.
Version v1.04 min read
User context — Cache & Invalidation
This chapter covers the caching strategy for the
/mepayload and invalidation patterns. Read /me payload structure first.
When to call /me
| Situation | Call /me? |
|---|---|
| At login (after enrichment) | Yes — store in the context |
| After a token refresh | Yes — rights may have changed |
| On every navigation | No — use the cached data |
| After a profile change (if an admin modified rights) | Yes — invalidate the cache and re-fetch |
| After a partial logout or organization change | Yes |
Recommended React pattern — AuthContext
typescript// lib/auth-context.tsx import { createContext, useContext, useState, useCallback } from "react"; interface MeUser { id: string; wid: string; email: string; firstName: string; lastName: string; adminLevel: string; appRights: string[]; customer: { id: string; wid: string; name: string }; // ... other fields } interface AuthContextValue { user: MeUser | null; loading: boolean; refresh: () => Promise<void>; } const AuthContext = createContext<AuthContextValue>({ user: null, loading: false, refresh: async () => {}, }); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<MeUser | null>(null); const [loading, setLoading] = useState(false); const refresh = useCallback(async () => { setLoading(true); try { const res = await fetch("/api/proxy/me"); // via your server proxy if (res.ok) setUser(await res.json()); } finally { setLoading(false); } }, []); return ( <AuthContext.Provider value={{ user, loading, refresh }}> {children} </AuthContext.Provider> ); } export const useAuth = () => useContext(AuthContext);
Caching and recommended TTL
/me is called once at login. The response remains stable as long as:
- The user's rights haven't been modified by an admin.
- The token hasn't been refreshed.
Recommended strategy:
- Store it in a React context (in memory, not in localStorage).
- Invalidate the cache after every token refresh (rights may have changed).
- Expose a
useAuth().refresh()hook to force a reload if the admin signals a change.
Synchronization after token refresh
typescript// app/api/auth/refresh/route.ts export async function POST(req: NextRequest) { const cookieStore = await cookies(); const refreshToken = cookieStore.get("refresh_token")?.value; if (!refreshToken) { return NextResponse.json({ error: "No refresh token" }, { status: 401 }); } // 1. Refresh the Keycloak tokens const refreshResponse = await fetch(`${BFF_URL}/api/auth/token/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }), }); if (!refreshResponse.ok) { // Delete cookies and force re-login cookieStore.delete("keycloak_token"); cookieStore.delete("wakastart_token"); cookieStore.delete("refresh_token"); return NextResponse.json({ error: "Refresh failed" }, { status: 401 }); } const { access_token, refresh_token: newRefreshToken } = await refreshResponse.json(); // 2. Re-enrich the new token const enrichResponse = await fetch(`${BFF_URL}/api/auth/enrich`, { method: "POST", headers: { Authorization: `Bearer ${access_token}` }, }); const { token: wakaToken } = await enrichResponse.json(); // 3. Update cookies cookieStore.set("keycloak_token", access_token, { httpOnly: true, sameSite: "lax" }); cookieStore.set("wakastart_token", wakaToken, { httpOnly: true, sameSite: "lax" }); cookieStore.set("refresh_token", newRefreshToken, { httpOnly: true, sameSite: "lax" }); // 4. Signal the client to re-fetch /me return NextResponse.json({ refreshed: true }); }
On the client side, after a successful refresh:
typescriptconst { refresh } = useAuth(); // After every successful token refresh await refresh(); // re-fetch /me with the new rights
Best practices
- Do not store the
/mepayload inlocalStorage— it contains sensitive rights. - Use
appRightsonly for conditional display on the front end. The backend is the source of authority. - If
adminLevel === "None"andappRights.length === 0, the user has no rights — show a dedicated page. - Check
isActiveon load: iffalse, redirect to an "Account disabled" page. - Check feature activation via
GET /api/config/app-features/check/{customerWid}/{featureCode}to hide entire sections (e.g. if theinvitationsfeature is disabled, hide the whole module) — this is not a precomputed field of/me.
Common pitfalls
- Stale rights: rights are modified by an admin but the
/mecache isn't invalidated. Add a lightweight polling mechanism or a WebSocket notification. - Empty
appRightsbut active user: the user is active but has no profile assigned. Contact the Customer's admin. - Misinterpreted
adminLevel: the level is a string, not an integer — compare by exact value or by inclusion in an ordered array.
Go further
- Access control: how to use
appRightsin your guards - Using the API: how to pass tokens in your requests