Files
webmail/lib/__tests__/request-protocol.test.ts
T
lucamzanon a307859194 fix: derive the admin cookie's Secure flag from the request
The setup wizard and the admin sign-in set their session cookies by two
different rules. The wizard matches Secure to the request - X-Forwarded-
Proto from the proxy, else the URL scheme - because browsers drop Secure
cookies on plain HTTP and an unconditional Secure broke setup over HTTP.
The admin cookie kept `secure: NODE_ENV === 'production'`, so on the same
HTTP deployment the wizard worked and admin sign-in failed silently: the
cookie was set, the browser threw it away, every call came back 401.

Both now share one helper, lib/security/request-protocol.ts, and the admin
route passes its request in. Behind a TLS proxy that forwards the proto
nothing changes: the cookie is Secure as before. Callers that pass no
request keep the production default.
2026-09-19 11:19:35 +02:00

23 lines
1.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { isHttpsRequest } from '@/lib/security/request-protocol';
const req = (url: string, headers: Record<string, string> = {}) => new Request(url, { headers });
describe('isHttpsRequest', () => {
it('trusts the proxy header over the scheme the app itself sees', () => {
// The app always sees plain HTTP behind a TLS-terminating proxy.
expect(isHttpsRequest(req('http://webmail.internal:3000/api/admin/auth', { 'x-forwarded-proto': 'https' }))).toBe(true);
expect(isHttpsRequest(req('https://webmail.internal/api/admin/auth', { 'x-forwarded-proto': 'http' }))).toBe(false);
});
it('reads the first hop of a comma-separated forwarded list, case-insensitively', () => {
expect(isHttpsRequest(req('http://x/', { 'x-forwarded-proto': 'HTTPS, http' }))).toBe(true);
expect(isHttpsRequest(req('http://x/', { 'x-forwarded-proto': ' http ,https' }))).toBe(false);
});
it('falls back to the request URL without a proxy', () => {
expect(isHttpsRequest(req('https://localhost:3000/'))).toBe(true);
expect(isHttpsRequest(req('http://localhost:3000/'))).toBe(false);
});
});