Wiring PalmyraStoreFactory for session-based auth#
The default PalmyraStoreFactory assumes Basic auth via an Authorization header. When your backend uses HTTP-session login (POST /auth/login → JSESSIONID cookie) with CSRF protection, three settings in the axios customizer make it work. This page is the recipe.
The full factory setup#
import { IEndPoint, PalmyraStoreFactory } from '@palmyralabs/palmyra-wire';
import { toast } from 'react-toastify';
export const ACL_ACCESS_KEY = 'aclAccess';
const errorHandler = () => (error: any) => {
const s = error?.response?.status;
if (s === 401) {
localStorage.removeItem(ACL_ACCESS_KEY);
window.location.assign('/app/login');
return true;
}
if (s === 403) { toast.error("You don't have permission."); return true; }
if (s >= 500) { toast.error('Server error.'); return true; }
return false;
};
const AppStoreFactory = new PalmyraStoreFactory({
baseUrl: '/api/palmyra',
errorHandlerFactory: errorHandler,
storeOptions: {
axiosCustomizer: (ax) => {
ax.defaults.withCredentials = true;
// CSRF defaults match Spring Security:
// ax.defaults.xsrfCookieName = 'XSRF-TOKEN'; // (axios default)
// ax.defaults.xsrfHeaderName = 'X-XSRF-TOKEN'; // (axios default)
},
},
});
export const useFormstore = (ep: IEndPoint, opts = {}, idKey?: string) =>
AppStoreFactory.getFormStore(opts, ep, idKey);
export const useGridstore = (ep: IEndPoint, opts = {}, idKey?: string) =>
AppStoreFactory.getGridStore(opts, ep, idKey);
export const useTreestore = (ep: IEndPoint, opts = {}) =>
AppStoreFactory.getTreeStore(opts, ep);
export default AppStoreFactory;Three behaviours this unlocks:
withCredentials: true— the browser attachesJSESSIONID(and other cookies) to every request. Non-negotiable; without this, requests go out cookie-less and the server sees no session.- Axios’s built-in CSRF handling — axios defaults read the
XSRF-TOKENcookie and echo it on every mutating request asX-XSRF-TOKEN. Those names match Spring Security’sCookieCsrfTokenRepository.withHttpOnlyFalse()defaults exactly — no additional config. - 401 → redirect — expired session triggers
errorHandler, which clears transient state and redirects to/app/login. The user logs in, the new session cookie overwrites the old, the SPA continues.
What you don’t do anymore#
- Don’t attach an
Authorizationheader. The session cookie is the credential. - Don’t cache the logged-in user in
localStorage. Fetch/user/abouton Topbar mount instead — the server is the source of truth. See the Security Configuration Recipes for the backend/user/abouthandler. - Don’t store the CSRF token yourself. Axios reads the cookie and writes the header for you.
Login flow on the client#
// pages/login/LoginPage.tsx
async function submit(e: FormEvent) {
e.preventDefault();
try {
await axios.post(
'/api/auth/login',
{ userName: user, password },
{ withCredentials: true },
);
// Session cookie now set; any subsequent palmyra-wire call authenticates via it.
nav('/');
} catch (x: any) {
setErr(x?.response?.status === 401 ? 'Invalid credentials' : 'Login failed');
}
}Note the explicit withCredentials: true on the login call — it’s not going through AppStoreFactory, so axios’s default (false) applies unless overridden.
Logout#
async function signOut() {
try { await axios.post('/api/auth/logout', null, { withCredentials: true }); }
catch { /* best-effort — still clear local state and redirect */ }
localStorage.removeItem(ACL_ACCESS_KEY);
nav('/login');
}Debugging cookie / CSRF round-trips#
Open devtools → Network → pick a POST:
- Request Headers should show
Cookie: JSESSIONID=…; XSRF-TOKEN=…andX-XSRF-TOKEN: <same token>. - Response Headers on the login call should show
Set-Cookie: JSESSIONID=…; Path=/api; HttpOnly; SameSite=Lax. - First GET to any authenticated endpoint after login should trigger
Set-Cookie: XSRF-TOKEN=<uuid>; Path=/api— if the cookie never lands, see Security Configuration Recipes § 3 (eager token resolution).
Dev vs prod#
In dev (Vite proxy at localhost:3000 → backend localhost:6060), SameSite=Lax + Secure=false lets the cookie flow across the proxy. In prod behind HTTPS:
server:
servlet:
session:
cookie:
secure: true
same-site: lax # or strict if you don't need cross-site GETsNo axios change on the frontend — browsers handle the attribute bump transparently.
See also#
- Security Configuration Recipes — the backend half of this wiring
PalmyraStoreFactory— the factory referenceAuthDecorator— when you need more than axios interceptors- Custom backend — pattern when the backend isn’t Palmyra-shaped