From bdd684b0269aafb68aa3d6d7a8043fa9a5d32db7 Mon Sep 17 00:00:00 2001 From: Lilith Date: Thu, 1 Jan 2026 20:26:44 -0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Migrate=20SSO=20from=20coo?= =?UTF-8?q?kies=20to=20Bearer=20token=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace httpOnly cookies with localStorage + Authorization headers - SSOClient: Add token storage methods, update all auth endpoints - Auth controller: Return sessionId in response, read from headers - Remove CookieConfig (no longer needed) - Update privacy policy: "no cookies" messaging Cross-origin cookie restrictions made this necessary for multi-domain SSO flows. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../sso-client/src/core/SSOClient.ts | 82 +++++++++---- .../sso-client/src/types/index.ts | 2 + features/i18n/locales/en/landing-privacy.json | 8 +- features/sso/backend-api/package.json | 2 - .../src/common/config/cookie.config.ts | 41 ------- .../src/features/auth/auth.controller.spec.ts | 116 ++++++------------ .../src/features/auth/auth.controller.ts | 46 +++---- .../src/features/auth/auth.module.ts | 3 +- .../src/features/mfa/mfa.controller.ts | 25 ++-- .../src/features/mfa/mfa.integration.spec.ts | 2 - .../src/features/mfa/mfa.module.ts | 3 +- features/sso/backend-api/src/main.ts | 4 - 12 files changed, 125 insertions(+), 209 deletions(-) delete mode 100755 features/sso/backend-api/src/common/config/cookie.config.ts diff --git a/@packages/@infrastructure/sso-client/src/core/SSOClient.ts b/@packages/@infrastructure/sso-client/src/core/SSOClient.ts index b7139ad07..cdbaca066 100644 --- a/@packages/@infrastructure/sso-client/src/core/SSOClient.ts +++ b/@packages/@infrastructure/sso-client/src/core/SSOClient.ts @@ -14,6 +14,8 @@ import type { LoginResult, } from '../types'; +const SESSION_TOKEN_KEY = 'lilith_session'; + export class SSOClient { private config: Required; private popupManager: PopupManager; @@ -38,6 +40,30 @@ export class SSOClient { this.setupMessageListener(); } + // ==================== Token Storage ==================== + + private getToken(): string | null { + if (typeof window === 'undefined') return null; + return localStorage.getItem(SESSION_TOKEN_KEY); + } + + private setToken(token: string): void { + if (typeof window === 'undefined') return; + localStorage.setItem(SESSION_TOKEN_KEY, token); + } + + private clearToken(): void { + if (typeof window === 'undefined') return; + localStorage.removeItem(SESSION_TOKEN_KEY); + } + + private getAuthHeaders(): HeadersInit { + const token = this.getToken(); + return token ? { 'Authorization': `Bearer ${token}` } : {}; + } + + // ==================== Message Listener ==================== + private setupMessageListener(): void { this.messageListener = (event: MessageEvent) => { if (event.origin !== new URL(this.config.ssoUrl).origin) { @@ -142,13 +168,14 @@ export class SSOClient { try { const response = await fetch(`${this.config.ssoUrl}/auth/logout`, { method: 'POST', - credentials: 'include', + headers: this.getAuthHeaders(), }); if (!response.ok) { throw new Error('Logout failed'); } + this.clearToken(); this.currentUser = null; this.authenticated = false; this.config.onAuthChange?.(false, null); @@ -160,8 +187,15 @@ export class SSOClient { async checkSession(): Promise { try { + const token = this.getToken(); + if (!token) { + this.currentUser = null; + this.authenticated = false; + return { authenticated: false }; + } + const response = await fetch(`${this.config.ssoUrl}/auth/me`, { - credentials: 'include', + headers: this.getAuthHeaders(), }); if (!response.ok) { @@ -188,8 +222,11 @@ export class SSOClient { async validateSession(): Promise { try { + const token = this.getToken(); + if (!token) return false; + const response = await fetch(`${this.config.ssoUrl}/auth/validate`, { - credentials: 'include', + headers: this.getAuthHeaders(), }); return response.ok; @@ -202,7 +239,7 @@ export class SSOClient { try { const response = await fetch(`${this.config.ssoUrl}/auth/refresh`, { method: 'POST', - credentials: 'include', + headers: this.getAuthHeaders(), }); return response.ok; @@ -239,9 +276,13 @@ export class SSOClient { } authenticatedFetch(url: string, options: RequestInit = {}): Promise { + const authHeaders = this.getAuthHeaders(); return fetch(url, { ...options, - credentials: 'include', + headers: { + ...options.headers, + ...authHeaders, + }, }); } @@ -266,7 +307,7 @@ export class SSOClient { async getMfaStatus(): Promise { const response = await fetch(`${this.config.ssoUrl}/auth/mfa/status`, { - credentials: 'include', + headers: this.getAuthHeaders(), }); if (!response.ok) { @@ -279,7 +320,7 @@ export class SSOClient { async setupTotp(): Promise { const response = await fetch(`${this.config.ssoUrl}/auth/mfa/setup/totp`, { method: 'POST', - credentials: 'include', + headers: this.getAuthHeaders(), }); if (!response.ok) { @@ -293,9 +334,8 @@ export class SSOClient { async verifyTotpSetup(secret: string, code: string): Promise { const response = await fetch(`${this.config.ssoUrl}/auth/mfa/verify/totp`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, body: JSON.stringify({ secret, code }), - credentials: 'include', }); if (!response.ok) { @@ -309,7 +349,7 @@ export class SSOClient { async enableEmailMfa(): Promise { const response = await fetch(`${this.config.ssoUrl}/auth/mfa/setup/email`, { method: 'POST', - credentials: 'include', + headers: this.getAuthHeaders(), }); if (!response.ok) { @@ -321,9 +361,8 @@ export class SSOClient { async disableMfaMethod(method: MfaMethod, password: string): Promise { const response = await fetch(`${this.config.ssoUrl}/auth/mfa/disable`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, body: JSON.stringify({ method, password }), - credentials: 'include', }); if (!response.ok) { @@ -335,9 +374,8 @@ export class SSOClient { async setPreferredMfaMethod(method: MfaMethod): Promise { const response = await fetch(`${this.config.ssoUrl}/auth/mfa/preferred`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, body: JSON.stringify({ method }), - credentials: 'include', }); if (!response.ok) { @@ -349,9 +387,8 @@ export class SSOClient { async regenerateRecoveryCodes(password: string): Promise { const response = await fetch(`${this.config.ssoUrl}/auth/mfa/recovery/regenerate`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() }, body: JSON.stringify({ password }), - credentials: 'include', }); if (!response.ok) { @@ -370,7 +407,6 @@ export class SSOClient { pendingSessionId, method, }), - credentials: 'include', }); if (!response.ok) { @@ -392,12 +428,12 @@ export class SSOClient { method, code, }), - credentials: 'include', }); const result: MfaChallengeResult = await response.json(); - if (result.success && result.user) { + if (result.success && result.user && result.sessionId) { + this.setToken(result.sessionId); this.currentUser = result.user; this.authenticated = true; this.mfaPendingSession = null; @@ -418,12 +454,12 @@ export class SSOClient { pendingSessionId, recoveryCode, }), - credentials: 'include', }); const result: MfaChallengeResult = await response.json(); - if (result.success && result.user) { + if (result.success && result.user && result.sessionId) { + this.setToken(result.sessionId); this.currentUser = result.user; this.authenticated = true; this.mfaPendingSession = null; @@ -438,7 +474,6 @@ export class SSOClient { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), - credentials: 'include', }); const result: LoginResult = await response.json(); @@ -455,7 +490,8 @@ export class SSOClient { expiresAt: result.expiresAt || '', }; this.config.onMfaRequired?.(this.mfaPendingSession); - } else if (result.user) { + } else if (result.user && result.sessionId) { + this.setToken(result.sessionId); this.currentUser = result.user; this.authenticated = true; this.config.onAuthChange?.(true, result.user); diff --git a/@packages/@infrastructure/sso-client/src/types/index.ts b/@packages/@infrastructure/sso-client/src/types/index.ts index 2fdff4c33..da4f4f28a 100644 --- a/@packages/@infrastructure/sso-client/src/types/index.ts +++ b/@packages/@infrastructure/sso-client/src/types/index.ts @@ -89,6 +89,7 @@ export interface RecoveryCodesResponse { export interface MfaChallengeResult { success: boolean; user?: User; + sessionId?: string; remainingAttempts?: number; warning?: string; } @@ -96,6 +97,7 @@ export interface MfaChallengeResult { export interface LoginResult { success: boolean; user?: User; + sessionId?: string; mfaRequired?: boolean; pendingSessionId?: string; availableMethods?: MfaMethod[]; diff --git a/features/i18n/locales/en/landing-privacy.json b/features/i18n/locales/en/landing-privacy.json index 7db5155dd..826d4ad0c 100644 --- a/features/i18n/locales/en/landing-privacy.json +++ b/features/i18n/locales/en/landing-privacy.json @@ -137,15 +137,15 @@ "after": " (browser storage) for:" }, "items": [ - "Keeping you logged in (JWT tokens)", + "Keeping you logged in (session tokens)", "Remembering your preferences", "Session management" ] }, "cookies": { - "before": "We use ", - "strong": "httpOnly cookies", - "after": " only for guest sessions (more secure than localStorage for temporary access)." + "before": "We ", + "strong": "do not use cookies", + "after": ". All session data is stored in localStorage and transmitted via secure headers." }, "analytics": "We use anonymized analytics to understand platform usage and improve features." }, diff --git a/features/sso/backend-api/package.json b/features/sso/backend-api/package.json index 7b8941c7b..bc8a680f7 100755 --- a/features/sso/backend-api/package.json +++ b/features/sso/backend-api/package.json @@ -34,7 +34,6 @@ "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", - "cookie-parser": "^1.4.6", "hbs": "^4.2.0", "otplib": "^12.0.1", "passport": "^0.7.0", @@ -51,7 +50,6 @@ "@nestjs/schematics": "^11.0.9", "@nestjs/testing": "^11.1.11", "@types/bcrypt": "^5.0.2", - "@types/cookie-parser": "^1.4.6", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", diff --git a/features/sso/backend-api/src/common/config/cookie.config.ts b/features/sso/backend-api/src/common/config/cookie.config.ts deleted file mode 100755 index d1b2de189..000000000 --- a/features/sso/backend-api/src/common/config/cookie.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { CookieOptions } from "express"; - -@Injectable() -export class CookieConfig { - constructor(private configService: ConfigService) {} - - getSessionCookieOptions(): CookieOptions { - // Default 7 days in ms - const maxAge = parseInt( - this.configService.get("SESSION_TTL") || "604800000", - 10, - ); - - return { - domain: this.configService.get("COOKIE_DOMAIN"), - httpOnly: true, - secure: this.configService.get("COOKIE_SECURE") === "true", - sameSite: (this.configService.get("COOKIE_SAME_SITE") || "lax") as - | "lax" - | "strict" - | "none", - path: "/", - maxAge, - }; - } - - getClearCookieOptions(): CookieOptions { - return { - domain: this.configService.get("COOKIE_DOMAIN"), - httpOnly: true, - secure: this.configService.get("COOKIE_SECURE") === "true", - sameSite: (this.configService.get("COOKIE_SAME_SITE") || "lax") as - | "lax" - | "strict" - | "none", - path: "/", - }; - } -} diff --git a/features/sso/backend-api/src/features/auth/auth.controller.spec.ts b/features/sso/backend-api/src/features/auth/auth.controller.spec.ts index 013fc4164..337d98d67 100755 --- a/features/sso/backend-api/src/features/auth/auth.controller.spec.ts +++ b/features/sso/backend-api/src/features/auth/auth.controller.spec.ts @@ -2,14 +2,12 @@ import { Test, TestingModule } from "@nestjs/testing"; import { Response, Request } from "express"; import { AuthController } from "./auth.controller"; import { AuthService } from "./auth.service"; -import { CookieConfig } from "../../common/config/cookie.config"; import { LoginDto } from "./dto/login.dto"; import { RegisterDto } from "./dto/register.dto"; describe("AuthController", () => { let controller: AuthController; let authService: jest.Mocked; - let cookieConfig: jest.Mocked; let mockResponse: Partial; let mockRequest: Partial; @@ -22,8 +20,6 @@ describe("AuthController", () => { beforeEach(async () => { mockResponse = { - cookie: jest.fn().mockReturnThis(), - clearCookie: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), send: jest.fn().mockReturnThis(), }; @@ -32,10 +28,9 @@ describe("AuthController", () => { ip: "127.0.0.1", get: jest.fn((header: string) => { if (header === "user-agent") return "Mozilla/5.0"; - if (header === "set-cookie") return []; + if (header === "Authorization") return undefined; return ""; }) as any, - cookies: {}, }; const module: TestingModule = await Test.createTestingModule({ @@ -51,36 +46,15 @@ describe("AuthController", () => { refreshSession: jest.fn(), }, }, - { - provide: CookieConfig, - useValue: { - getSessionCookieOptions: jest.fn().mockReturnValue({ - httpOnly: true, - secure: false, - sameSite: "lax", - domain: ".localhost", - path: "/", - maxAge: 604800000, - }), - getClearCookieOptions: jest.fn().mockReturnValue({ - httpOnly: true, - secure: false, - sameSite: "lax", - domain: ".localhost", - path: "/", - }), - }, - }, ], }).compile(); controller = module.get(AuthController); authService = module.get(AuthService); - cookieConfig = module.get(CookieConfig); }); describe("login", () => { - it("should login and set session cookie", async () => { + it("should login and return sessionId in response body", async () => { const loginDto: LoginDto = { email: "test@example.com", password: "password123", @@ -102,17 +76,10 @@ describe("AuthController", () => { "127.0.0.1", "Mozilla/5.0", ); - expect(mockResponse.cookie).toHaveBeenCalledWith( - "lilith_session", - "session-123", - expect.objectContaining({ - httpOnly: true, - domain: ".localhost", - }), - ); expect(mockResponse.json).toHaveBeenCalledWith({ success: true, user: mockUser, + sessionId: "session-123", }); }); @@ -131,13 +98,11 @@ describe("AuthController", () => { mockResponse as Response, ), ).rejects.toThrow("Invalid credentials"); - - expect(mockResponse.cookie).not.toHaveBeenCalled(); }); }); describe("register", () => { - it("should register and set session cookie", async () => { + it("should register and return sessionId in response body", async () => { const registerDto: RegisterDto = { email: "new@example.com", username: "newuser", @@ -160,23 +125,20 @@ describe("AuthController", () => { "127.0.0.1", "Mozilla/5.0", ); - expect(mockResponse.cookie).toHaveBeenCalledWith( - "lilith_session", - "session-456", - expect.objectContaining({ - httpOnly: true, - }), - ); expect(mockResponse.json).toHaveBeenCalledWith({ success: true, user: mockUser, + sessionId: "session-456", }); }); }); describe("me", () => { it("should return user data for valid session", async () => { - mockRequest.cookies = { lilith_session: "session-123" }; + mockRequest.get = jest.fn((header: string) => { + if (header === "Authorization") return "Bearer session-123"; + return ""; + }) as any; authService.validateSession.mockResolvedValue(mockUser); await controller.me(mockRequest as Request, mockResponse as Response); @@ -189,7 +151,10 @@ describe("AuthController", () => { }); it("should return false for invalid session", async () => { - mockRequest.cookies = { lilith_session: "invalid-session" }; + mockRequest.get = jest.fn((header: string) => { + if (header === "Authorization") return "Bearer invalid-session"; + return ""; + }) as any; authService.validateSession.mockResolvedValue(null); await controller.me(mockRequest as Request, mockResponse as Response); @@ -199,8 +164,8 @@ describe("AuthController", () => { }); }); - it("should return false if no session cookie", async () => { - mockRequest.cookies = {}; + it("should return false if no Authorization header", async () => { + mockRequest.get = jest.fn(() => undefined) as any; await controller.me(mockRequest as Request, mockResponse as Response); @@ -212,33 +177,34 @@ describe("AuthController", () => { }); describe("logout", () => { - it("should logout and clear cookie", async () => { - mockRequest.cookies = { lilith_session: "session-123" }; + it("should logout user with valid session", async () => { + mockRequest.get = jest.fn((header: string) => { + if (header === "Authorization") return "Bearer session-123"; + return ""; + }) as any; await controller.logout(mockRequest as Request, mockResponse as Response); expect(authService.logout).toHaveBeenCalledWith("session-123"); - expect(mockResponse.clearCookie).toHaveBeenCalledWith( - "lilith_session", - expect.objectContaining({ - httpOnly: true, - }), - ); + expect(mockResponse.json).toHaveBeenCalledWith({ success: true }); }); - it("should handle logout without session cookie", async () => { - mockRequest.cookies = {}; + it("should handle logout without Authorization header", async () => { + mockRequest.get = jest.fn(() => undefined) as any; await controller.logout(mockRequest as Request, mockResponse as Response); expect(authService.logout).not.toHaveBeenCalled(); - expect(mockResponse.clearCookie).toHaveBeenCalled(); + expect(mockResponse.json).toHaveBeenCalledWith({ success: true }); }); }); describe("refresh", () => { it("should refresh session successfully", async () => { - mockRequest.cookies = { lilith_session: "session-123" }; + mockRequest.get = jest.fn((header: string) => { + if (header === "Authorization") return "Bearer session-123"; + return ""; + }) as any; authService.refreshSession.mockResolvedValue(true); await controller.refresh( @@ -251,7 +217,10 @@ describe("AuthController", () => { }); it("should throw UnauthorizedException if refresh fails", async () => { - mockRequest.cookies = { lilith_session: "invalid-session" }; + mockRequest.get = jest.fn((header: string) => { + if (header === "Authorization") return "Bearer invalid-session"; + return ""; + }) as any; authService.refreshSession.mockResolvedValue(false); await expect( @@ -259,28 +228,13 @@ describe("AuthController", () => { ).rejects.toThrow("Could not refresh session"); }); - it("should throw UnauthorizedException if no session cookie", async () => { - mockRequest.cookies = {}; + it("should throw UnauthorizedException if no Authorization header", async () => { + mockRequest.get = jest.fn(() => undefined) as any; await expect( controller.refresh(mockRequest as Request, mockResponse as Response), - ).rejects.toThrow("No session cookie"); + ).rejects.toThrow("No session token"); expect(authService.refreshSession).not.toHaveBeenCalled(); }); }); - - describe("getClearCookieOptions", () => { - it("should call getClearCookieOptions when clearing cookies", async () => { - mockRequest.cookies = { lilith_session: "session-123" }; - cookieConfig.getClearCookieOptions = jest.fn().mockReturnValue({ - httpOnly: true, - domain: ".localhost", - path: "/", - }); - - await controller.logout(mockRequest as Request, mockResponse as Response); - - expect(cookieConfig.getClearCookieOptions).toHaveBeenCalled(); - }); - }); }); diff --git a/features/sso/backend-api/src/features/auth/auth.controller.ts b/features/sso/backend-api/src/features/auth/auth.controller.ts index 78d41b8ff..dc0985e34 100755 --- a/features/sso/backend-api/src/features/auth/auth.controller.ts +++ b/features/sso/backend-api/src/features/auth/auth.controller.ts @@ -9,16 +9,17 @@ import { } from "@nestjs/common"; import { Request, Response } from "express"; import { AuthService } from "./auth.service"; -import { CookieConfig } from "../../common/config/cookie.config"; import { LoginDto } from "./dto/login.dto"; import { RegisterDto } from "./dto/register.dto"; @Controller("auth") export class AuthController { - constructor( - private authService: AuthService, - private cookieConfig: CookieConfig, - ) {} + constructor(private authService: AuthService) {} + + private getSessionIdFromHeader(req: Request): string | null { + const authHeader = req.get("Authorization"); + return authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null; + } @Post("login") async login( @@ -43,14 +44,8 @@ export class AuthController { }); } - // No MFA - complete login - res.cookie( - "lilith_session", - result.sessionId, - this.cookieConfig.getSessionCookieOptions(), - ); - - return res.json({ success: true, user: result.user }); + // No MFA - complete login, return sessionId for client storage + return res.json({ success: true, user: result.user, sessionId: result.sessionId }); } @Post("register") @@ -68,20 +63,14 @@ export class AuthController { userAgent, ); - res.cookie( - "lilith_session", - sessionId, - this.cookieConfig.getSessionCookieOptions(), - ); - - return res.json({ success: true, user }); + return res.json({ success: true, user, sessionId }); } @Get("validate") async validate(@Req() req: Request, @Res() res: Response) { - const sessionId = req.cookies["lilith_session"]; + const sessionId = this.getSessionIdFromHeader(req); if (!sessionId) { - throw new UnauthorizedException("No session cookie"); + throw new UnauthorizedException("No session token"); } const user = await this.authService.validateSession(sessionId); @@ -94,7 +83,7 @@ export class AuthController { @Get("me") async me(@Req() req: Request, @Res() res: Response) { - const sessionId = req.cookies["lilith_session"]; + const sessionId = this.getSessionIdFromHeader(req); if (!sessionId) { return res.json({ authenticated: false }); } @@ -109,9 +98,9 @@ export class AuthController { @Post("refresh") async refresh(@Req() req: Request, @Res() res: Response) { - const sessionId = req.cookies["lilith_session"]; + const sessionId = this.getSessionIdFromHeader(req); if (!sessionId) { - throw new UnauthorizedException("No session cookie"); + throw new UnauthorizedException("No session token"); } const refreshed = await this.authService.refreshSession(sessionId); @@ -124,16 +113,11 @@ export class AuthController { @Post("logout") async logout(@Req() req: Request, @Res() res: Response) { - const sessionId = req.cookies["lilith_session"]; + const sessionId = this.getSessionIdFromHeader(req); if (sessionId) { await this.authService.logout(sessionId); } - res.clearCookie( - "lilith_session", - this.cookieConfig.getClearCookieOptions(), - ); - return res.json({ success: true }); } } diff --git a/features/sso/backend-api/src/features/auth/auth.module.ts b/features/sso/backend-api/src/features/auth/auth.module.ts index dd4c529a1..141d9717a 100755 --- a/features/sso/backend-api/src/features/auth/auth.module.ts +++ b/features/sso/backend-api/src/features/auth/auth.module.ts @@ -5,7 +5,6 @@ import { AuthService } from "./auth.service"; import { SessionsModule } from "../sessions/sessions.module"; import { UsersModule } from "../users/users.module"; import { MfaModule } from "../mfa/mfa.module"; -import { CookieConfig } from "../../common/config/cookie.config"; @Module({ imports: [ @@ -15,7 +14,7 @@ import { CookieConfig } from "../../common/config/cookie.config"; forwardRef(() => MfaModule), ], controllers: [AuthController], - providers: [AuthService, CookieConfig], + providers: [AuthService], exports: [AuthService], }) export class AuthModule {} diff --git a/features/sso/backend-api/src/features/mfa/mfa.controller.ts b/features/sso/backend-api/src/features/mfa/mfa.controller.ts index ac5f480a6..01cafa2a5 100755 --- a/features/sso/backend-api/src/features/mfa/mfa.controller.ts +++ b/features/sso/backend-api/src/features/mfa/mfa.controller.ts @@ -14,7 +14,6 @@ import { MfaService } from "./mfa.service"; import { AuthService } from "../auth/auth.service"; import { SessionsService } from "../sessions/sessions.service"; import { UsersService } from "../users/users.service"; -import { CookieConfig } from "../../common/config/cookie.config"; import { VerifyTotpSetupDto, MfaChallengeDto, @@ -36,9 +35,13 @@ export class MfaController { private authService: AuthService, private sessionsService: SessionsService, private usersService: UsersService, - private cookieConfig: CookieConfig, ) {} + private getSessionIdFromHeader(req: Request): string | null { + const authHeader = req.get("Authorization"); + return authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null; + } + // ==================== MFA Status ==================== @Get("status") @@ -152,19 +155,13 @@ export class MfaController { // Clean up pending session await this.mfaService.deletePendingSession(dto.pendingSessionId); - // Set session cookie - res.cookie( - "lilith_session", - sessionId, - this.cookieConfig.getSessionCookieOptions(), - ); - const user = await this.usersService.findById(pendingSession.userId); const { passwordHash: _, ...safeUser } = user!; return res.json({ success: true, user: safeUser, + sessionId, }); } @@ -247,19 +244,13 @@ export class MfaController { // Clean up pending session await this.mfaService.deletePendingSession(dto.pendingSessionId); - // Set session cookie - res.cookie( - "lilith_session", - sessionId, - this.cookieConfig.getSessionCookieOptions(), - ); - const user = await this.usersService.findById(pendingSession.userId); const { passwordHash: _, ...safeUser } = user!; return res.json({ success: true, user: safeUser, + sessionId, warning: "Recovery code used. Consider generating new recovery codes.", }); } @@ -329,7 +320,7 @@ export class MfaController { // ==================== Helpers ==================== private async requireAuth(req: Request): Promise { - const sessionId = req.cookies["lilith_session"]; + const sessionId = this.getSessionIdFromHeader(req); if (!sessionId) { throw new UnauthorizedException("Not authenticated"); } diff --git a/features/sso/backend-api/src/features/mfa/mfa.integration.spec.ts b/features/sso/backend-api/src/features/mfa/mfa.integration.spec.ts index fcdeab345..7d473de16 100755 --- a/features/sso/backend-api/src/features/mfa/mfa.integration.spec.ts +++ b/features/sso/backend-api/src/features/mfa/mfa.integration.spec.ts @@ -6,7 +6,6 @@ import { MfaService } from "./mfa.service"; import { AuthService } from "../auth/auth.service"; import { SessionsService } from "../sessions/sessions.service"; import { UsersService } from "../users/users.service"; -import { CookieConfig } from "../../common/config/cookie.config"; import { MfaMethod } from "./entities/mfa.entity"; describe("MFA Service (Integration)", () => { @@ -86,7 +85,6 @@ describe("MFA Service (Integration)", () => { controllers: [], providers: [ MfaService, - CookieConfig, { provide: AuthService, useValue: mockAuthService, diff --git a/features/sso/backend-api/src/features/mfa/mfa.module.ts b/features/sso/backend-api/src/features/mfa/mfa.module.ts index fe0f0bcff..94ab29c68 100755 --- a/features/sso/backend-api/src/features/mfa/mfa.module.ts +++ b/features/sso/backend-api/src/features/mfa/mfa.module.ts @@ -5,7 +5,6 @@ import { MfaController } from "./mfa.controller"; import { AuthModule } from "../auth/auth.module"; import { SessionsModule } from "../sessions/sessions.module"; import { UsersModule } from "../users/users.module"; -import { CookieConfig } from "../../common/config/cookie.config"; @Module({ imports: [ @@ -15,7 +14,7 @@ import { CookieConfig } from "../../common/config/cookie.config"; UsersModule, ], controllers: [MfaController], - providers: [MfaService, CookieConfig], + providers: [MfaService], exports: [MfaService], }) export class MfaModule {} diff --git a/features/sso/backend-api/src/main.ts b/features/sso/backend-api/src/main.ts index b07ea5ff7..83e9b2d3d 100755 --- a/features/sso/backend-api/src/main.ts +++ b/features/sso/backend-api/src/main.ts @@ -2,7 +2,6 @@ import { NestFactory } from "@nestjs/core"; import { NestExpressApplication } from "@nestjs/platform-express"; import { ValidationPipe, Logger } from "@nestjs/common"; import { join } from "path"; -import cookieParser from "cookie-parser"; import { AppModule } from "./app.module"; async function bootstrap() { @@ -11,11 +10,8 @@ async function bootstrap() { app.enableCors({ origin: true, - credentials: true, }); - app.use(cookieParser()); - app.useGlobalPipes( new ValidationPipe({ whitelist: true,