mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-13 12:33:07 +02:00
Session-bound tokens broke re-login after logout: logoutUser destroys the session, but the SPA navigated to the login page without a document reload, so the surviving _csrf/XSRF-TOKEN cookies stayed bound to the destroyed session id and the next login POST failed with 403 until a manual refresh. Hit both manual logout and the idle-timer auto-logout. Two coordinated fixes: 1. Frontend: the logout effect now performs a full document navigation to the login page (after the server logout completes, so the request is not aborted by the reload), which re-runs the handshake and mints a token bound to the fresh session. The logout reason previously travelled on the NgRx action stream, which cannot survive a reload - it is now handed over via sessionStorage (set after clearAll) and picked up and cleared by the login component. The SSO branch is unchanged (it already left the document). 2. Backend: the EBADCSRFTOKEN error path now re-mints the token for the current session before responding 403, so any client holding a stale token (e.g. after a server restart rotates the boot secret) self-heals on retry instead of looping on 403. Verified on the fixture: reviewer's repro now shows login 200 -> logout 200 -> stale-token login 403 (binding intact) with re-minted cookies on the 403 -> retry 200; and the reload path (fresh GET / after logout, what the full navigation does) logs in on the first attempt. Both API suites, the CSRF battery, rtl.effects specs and the full frontend suite pass; frontend and backend artifacts rebuilt.
45 lines
2.1 KiB
TypeScript
45 lines
2.1 KiB
TypeScript
import { doubleCsrf } from 'csrf-csrf';
|
|
import { Application } from 'express';
|
|
import { Logger, LoggerService } from './logger.js';
|
|
import { Common, CommonService } from './common.js';
|
|
|
|
class CSRF {
|
|
|
|
public logger: LoggerService = Logger;
|
|
public common: CommonService = Common;
|
|
|
|
// Signed double-submit-cookie protection (replaces the deprecated csurf).
|
|
// The signed token lives in the httpOnly '_csrf' cookie; the client echoes
|
|
// the same token (read from the XSRF-TOKEN cookie set in app.ts) in a
|
|
// header. The cookie is not secure-only because RTL commonly serves plain
|
|
// HTTP (matching the session cookie); token sources match what csurf
|
|
// accepted. The error code EBADCSRFTOKEN is handled in app.ts.
|
|
private doubleCsrfUtilities = doubleCsrf({
|
|
getSecret: () => this.common.secret_key,
|
|
getSessionIdentifier: (req: any) => (req.session ? req.session.id : ''),
|
|
cookieName: '_csrf',
|
|
cookieOptions: { sameSite: 'strict', path: '/', secure: false, httpOnly: true },
|
|
getCsrfTokenFromRequest: (req: any) => (req.body && req.body._csrf) || (req.query && req.query._csrf) ||
|
|
req.headers['csrf-token'] || req.headers['xsrf-token'] ||
|
|
req.headers['x-csrf-token'] || req.headers['x-xsrf-token']
|
|
});
|
|
|
|
public csrfProtection = this.doubleCsrfUtilities.doubleCsrfProtection;
|
|
|
|
// Force-mints a fresh token for the current session, discarding any token
|
|
// cookie bound to a previous session or boot secret (used by the
|
|
// EBADCSRFTOKEN error path in app.ts so a client retry succeeds).
|
|
public reMintToken = (req, res) => this.doubleCsrfUtilities.generateCsrfToken(req, res, { overwrite: true });
|
|
|
|
public mount(app: Application): Application {
|
|
this.logger.log({ selectedNode: this.common.selectedNode, level: 'INFO', fileName: 'CSRF', msg: 'Setting up CSRF..' });
|
|
if (process.env.NODE_ENV !== 'development') {
|
|
app.use((req, res, next) => this.csrfProtection(req, res, next));
|
|
}
|
|
this.logger.log({ selectedNode: this.common.selectedNode, level: 'INFO', fileName: 'CSRF', msg: 'CSRF Set' });
|
|
return app;
|
|
};
|
|
|
|
}
|
|
|
|
export default new CSRF;
|