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 '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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user