fix(auth): make diagnostics bounded and portable

This commit is contained in:
2026-08-17 12:19:19 +02:00
parent 7cfbee36fa
commit be1724890a
23 changed files with 1088 additions and 116 deletions
+18 -8
View File
@@ -1,4 +1,5 @@
import type { AuthDiagnostic, GroupCatalog } from "./group-catalog.js";
import { isUsableAuthenticationSecret } from "./secret-policy.js";
import { parseConfiguredTransportUrl } from "./url-policy.js";
const MAX_RESPONSE_BYTES = 1024 * 1024;
@@ -34,10 +35,6 @@ function ambiguous(name: string): AuthDiagnostic {
return diagnostic("oidc_mapped_group_ambiguous", "A configured authorization group is ambiguous.", name);
}
function safeApiToken(value: string): boolean {
return typeof value === "string" && value.length > 0 && value.length <= 16 * 1024 && !/\p{Cc}/u.test(value);
}
function stableCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
@@ -143,10 +140,21 @@ type GroupResult = "present" | "missing" | "ambiguous" | "unauthorized" | "unrea
function exactResult(name: string, parsed: unknown): GroupResult {
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "unreachable";
const record = parsed as { results?: unknown; pagination?: unknown };
if (!Array.isArray(record.results) || !record.pagination || typeof record.pagination !== "object"
if (!Array.isArray(record.results) || record.results.length > 2
|| !record.pagination || typeof record.pagination !== "object"
|| Array.isArray(record.pagination)) return "unreachable";
const next = (record.pagination as { next?: unknown }).next;
if (next !== null && next !== undefined) return "ambiguous";
if (!Object.prototype.hasOwnProperty.call(record.pagination, "next")) return "unreachable";
const next = (record.pagination as { next: unknown }).next;
if (next !== null) {
if (typeof next !== "string" || next.length === 0 || next.length > 2048 || /\p{Cc}/u.test(next)) return "unreachable";
try {
const continuation = new URL(next);
if (continuation.protocol !== "https:" || continuation.username || continuation.password || continuation.hash) return "unreachable";
} catch {
return "unreachable";
}
return "ambiguous";
}
const resultNames: string[] = [];
for (const result of record.results) {
if (!result || typeof result !== "object" || Array.isArray(result)
@@ -161,7 +169,9 @@ function exactResult(name: string, parsed: unknown): GroupResult {
export function createAuthentikGroupCatalog(options: AuthentikGroupCatalogOptions): GroupCatalog {
const origin = parseConfiguredTransportUrl(options.baseUrl, { allowLoopbackHttp: false, originOnly: true });
const fetchImplementation = options.fetch ?? globalThis.fetch;
const valid = origin !== undefined && safeApiToken(options.apiToken) && typeof fetchImplementation === "function";
const valid = origin !== undefined
&& isUsableAuthenticationSecret("THT_AUTHENTIK_API_TOKEN", options.apiToken)
&& typeof fetchImplementation === "function";
async function verify(name: string, signal: AbortSignal): Promise<GroupResult> {
if (!origin || !valid || signal.aborted) return "unreachable";