Files
TREK/server/tests/unit/nest/oidc-ssrf.test.ts
T
Maurice 0b8ddd7dcb test(server): day order tests follow the dateless-day rule and the OIDC hop check reads the formatted call
DAY-SVC-048 and DAY-DEL-011 pinned the old reorder, which handed a day
without a date the first slot's date; they now expect it to keep none and
the day it passes to keep its own. OIDC-SSRF-003 reads oidc.service.ts as
text and missed the token call once Prettier put a trailing comma after its
hop budget of 0; the pattern takes the comma.
2026-10-10 22:53:59 +02:00

50 lines
2.2 KiB
TypeScript

/**
* The four OIDC provider calls go through the SSRF guard.
*
* Three of them are worse than they look: `discover` fetches a URL an admin
* configured, but the token, userinfo and JWKS URLs come out of the RESPONSE to
* that first call. They are chosen by the other end, not by anyone here, and
* until now all four went to a bare `fetch`.
*
* Deliberately a separate file. oidc.service.test.ts stubs global fetch and
* mocks the guard away so it can test OIDC logic; that is the right trade there
* and it is also exactly why the guard needs pinning somewhere it is not mocked.
*
* safeFetchAdminConfigured rather than safeFetch: a self-hoster running Authentik
* on http://localhost:9000 must keep working, so loopback and LAN stay allowed
* while link-local and the cloud-metadata range never are.
*/
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, it, expect } from 'vitest';
const SOURCE = readFileSync(join(__dirname, '../../../src/nest/oidc/oidc.service.ts'), 'utf8');
describe('OIDC outbound calls', () => {
it('OIDC-SSRF-001: no bare fetch( is left in the service', () => {
// A grep, and that is the point: the failure this prevents is somebody adding
// a fifth provider call next year with the same `await fetch(...)` the other
// four used to have. A behavioural test would only cover the four that exist.
const bare = SOURCE.split('\n')
.map((line, i) => ({ line: line.trim(), n: i + 1 }))
.filter(({ line }) => /\bawait fetch\(|[^.\w]fetch\(/.test(line) && !line.startsWith('*'));
expect(bare.map((b) => `${b.n}: ${b.line.slice(0, 70)}`)).toEqual([]);
});
it('OIDC-SSRF-002: all four calls name the guard', () => {
const guarded = SOURCE.match(/safeFetchAdminConfigured\(/g) ?? [];
// discover, exchangeCodeForToken, getUserInfo, fetchJwks.
expect(guarded).toHaveLength(4);
});
it('OIDC-SSRF-003: the token exchange follows no redirect at all', () => {
// A redirect there would hand client_secret to a second host, and the
// platform default is to follow. The third argument is the hop budget.
const tokenCall = /safeFetchAdminConfigured\(\s*doc\.token_endpoint[\s\S]*?\}\s*,\s*0\s*,?\s*\)/.test(SOURCE);
expect(tokenCall).toBe(true);
});
});