This commit is contained in:
Bastian Wagner
2026-07-17 14:52:20 +02:00
parent d30d9cfdde
commit e89ac1481e
3 changed files with 132 additions and 1 deletions

View File

@@ -0,0 +1,52 @@
import { normalizeOidcForwardedProto } from './oidc-forwarded-proto';
describe('normalizeOidcForwardedProto', () => {
it('corrects an OIDC request when the trusted public issuer uses HTTPS', () => {
const request = {
method: 'GET',
originalUrl: '/oidc/auth?client_id=test',
headers: { 'x-forwarded-proto': 'http' },
};
const result = normalizeOidcForwardedProto(request, 'https://auth.example.com', true);
expect(result).toEqual({ corrected: true, path: '/oidc/auth', previousProto: 'http' });
expect(request.headers['x-forwarded-proto']).toBe('https');
});
it('also corrects interaction routes used to finish login and consent', () => {
const request = { originalUrl: '/interaction/uid/login', headers: {} as Record<string, string> };
const result = normalizeOidcForwardedProto(request, 'https://auth.example.com', true);
expect(result.corrected).toBe(true);
expect(request.headers['x-forwarded-proto']).toBe('https');
});
it('does not alter unrelated API requests', () => {
const request = { originalUrl: '/api/account', headers: { 'x-forwarded-proto': 'http' } };
const result = normalizeOidcForwardedProto(request, 'https://auth.example.com', true);
expect(result.corrected).toBe(false);
expect(request.headers['x-forwarded-proto']).toBe('http');
});
it('does not override the protocol when proxy trust is disabled', () => {
const request = { originalUrl: '/oidc/auth', headers: { 'x-forwarded-proto': 'http' } };
const result = normalizeOidcForwardedProto(request, 'https://auth.example.com', false);
expect(result.corrected).toBe(false);
expect(request.headers['x-forwarded-proto']).toBe('http');
});
it('does not force HTTPS for a configured HTTP issuer', () => {
const request = { originalUrl: '/oidc/auth', headers: { 'x-forwarded-proto': 'http' } };
const result = normalizeOidcForwardedProto(request, 'http://localhost:8080', true);
expect(result.corrected).toBe(false);
expect(request.headers['x-forwarded-proto']).toBe('http');
});
});

View File

@@ -0,0 +1,58 @@
import type { IncomingHttpHeaders } from 'node:http';
interface OidcProxyRequest {
method?: string;
originalUrl?: string;
url?: string;
headers: IncomingHttpHeaders;
}
export interface OidcForwardedProtoResult {
corrected: boolean;
path: string;
previousProto?: string;
}
export function normalizeOidcForwardedProto(
request: OidcProxyRequest,
issuer: string | undefined,
trustProxy: boolean,
): OidcForwardedProtoResult {
const path = (request.originalUrl ?? request.url ?? '/').split('?', 1)[0];
const previousProto = headerValue(request.headers['x-forwarded-proto']);
if (!trustProxy || !isOidcPath(path) || !isHttpsUrl(issuer) || previousProto === 'https') {
return { corrected: false, path, previousProto };
}
request.headers['x-forwarded-proto'] = 'https';
return { corrected: true, path, previousProto };
}
function isOidcPath(path: string): boolean {
return (
path === '/oidc' ||
path.startsWith('/oidc/') ||
path === '/interaction' ||
path.startsWith('/interaction/') ||
path === '/.well-known' ||
path.startsWith('/.well-known/')
);
}
function isHttpsUrl(value?: string): boolean {
if (!value) {
return false;
}
try {
return new URL(value).protocol === 'https:';
} catch {
return false;
}
}
function headerValue(value: string | string[] | undefined): string | undefined {
const firstValue = Array.isArray(value) ? value[0] : value;
return firstValue?.split(',', 1)[0].trim().toLowerCase() || undefined;
}

View File

@@ -1,8 +1,9 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { ValidationPipe } from '@nestjs/common'; import { Logger, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import express, { NextFunction, Request, Response } from 'express'; import express, { NextFunction, Request, Response } from 'express';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { normalizeOidcForwardedProto } from './common/oidc-forwarded-proto';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { bodyParser: false }); const app = await NestFactory.create(AppModule, { bodyParser: false });
@@ -12,6 +13,26 @@ async function bootstrap() {
credentials: true, credentials: true,
}); });
const oidcProxyLogger = new Logger('OidcProxyProtocol');
app.use((request: Request, _response: Response, next: NextFunction) => {
const result = normalizeOidcForwardedProto(
request,
process.env.OIDC_ISSUER,
process.env.OIDC_TRUST_PROXY === 'true',
);
if (result.corrected) {
oidcProxyLogger.warn(
`[OIDC_FORWARDED_PROTO_CORRECTED] ${JSON.stringify({
method: request.method,
path: result.path,
receivedProto: result.previousProto,
effectiveProto: 'https',
})}`,
);
}
next();
});
const jsonParser = express.json(); const jsonParser = express.json();
const formParser = express.urlencoded({ extended: false }); const formParser = express.urlencoded({ extended: false });
app.use((request: Request, response: Response, next: NextFunction) => { app.use((request: Request, response: Response, next: NextFunction) => {