fix
This commit is contained in:
52
apps/api/src/common/oidc-forwarded-proto.spec.ts
Normal file
52
apps/api/src/common/oidc-forwarded-proto.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
58
apps/api/src/common/oidc-forwarded-proto.ts
Normal file
58
apps/api/src/common/oidc-forwarded-proto.ts
Normal 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;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'reflect-metadata';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import { AppModule } from './app.module';
|
||||
import { normalizeOidcForwardedProto } from './common/oidc-forwarded-proto';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { bodyParser: false });
|
||||
@@ -12,6 +13,26 @@ async function bootstrap() {
|
||||
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 formParser = express.urlencoded({ extended: false });
|
||||
app.use((request: Request, response: Response, next: NextFunction) => {
|
||||
|
||||
Reference in New Issue
Block a user