authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-06-23 01:21:06+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-22 16:21:06-07:00
log92e54fb96c5e24c45fc0fa7066a3a4d38294dfc7
tree5581d1ebf6e353907c44806656f82500ebee7888
parent8cc9a6fa6b0e3b6d6528793e942982d7de040833
signature Signed by PGP key B5690EEEBB952194

[codex] fix: guard DPoP fallback URL construction (#3503)

Co-authored-by: Codex <codex@openai.com> Co-authored-by: Julius Marminge <jmarminge@gmail.com>

1 files changed, 9 insertions(+), 18 deletions(-)

apps/server/src/auth/dpop.ts+9-18
...@@ -3,7 +3,8 @@ import * as Crypto from "effect/Crypto";...@@ -3,7 +3,8 @@ import * as Crypto from "effect/Crypto";
3import * as DateTime from "effect/DateTime";3import * as DateTime from "effect/DateTime";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Encoding from "effect/Encoding";5import * as Encoding from "effect/Encoding";
6import type * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";6import * as Option from "effect/Option";
7import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
78
8import {9import {
9 ServerAuthDpopReplayKeyCalculationError,10 ServerAuthDpopReplayKeyCalculationError,
...@@ -13,22 +14,6 @@ import {...@@ -13,22 +14,6 @@ import {
13} from "./EnvironmentAuth.ts";14} from "./EnvironmentAuth.ts";
14import * as ServerSecretStore from "./ServerSecretStore.ts";15import * as ServerSecretStore from "./ServerSecretStore.ts";
1516
16function firstHeaderValue(value: string | undefined): string | undefined {
17 const first = value?.split(",")[0]?.trim();
18 return first && first.length > 0 ? first : undefined;
19}
20
21export function requestAbsoluteUrl(request: HttpServerRequest.HttpServerRequest): string {
22 try {
23 return new URL(request.originalUrl).href;
24 } catch {
25 const host = firstHeaderValue(request.headers.host) ?? "127.0.0.1";
26 const forwardedProto = firstHeaderValue(request.headers["x-forwarded-proto"]);
27 const proto = forwardedProto === "https" || forwardedProto === "http" ? forwardedProto : "http";
28 return new URL(request.originalUrl, `${proto}://${host}`).href;
29 }
30}
31
32export const mapDpopReplayStoreError = (17export const mapDpopReplayStoreError = (
33 error: ServerSecretStore.SecretStoreError,18 error: ServerSecretStore.SecretStoreError,
34): ServerAuthInvalidCredentialError | ServerAuthInternalError =>19): ServerAuthInvalidCredentialError | ServerAuthInternalError =>
...@@ -48,11 +33,17 @@ export const verifyRequestDpopProof = (input: {...@@ -48,11 +33,17 @@ export const verifyRequestDpopProof = (input: {
48}) =>33}) =>
49 Effect.gen(function* () {34 Effect.gen(function* () {
50 const proof = input.request.headers.dpop;35 const proof = input.request.headers.dpop;
36 const url = HttpServerRequest.toURL(input.request);
37 if (Option.isNone(url)) {
38 return yield* new ServerAuthInvalidCredentialError({
39 diagnostic: "Invalid DPoP request URL.",
40 });
41 }
51 const now = yield* DateTime.now;42 const now = yield* DateTime.now;
52 const result = verifyDpopProof({43 const result = verifyDpopProof({
53 proof,44 proof,
54 method: input.request.method,45 method: input.request.method,
55 url: requestAbsoluteUrl(input.request),46 url: url.value.href,
56 nowEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),47 nowEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),
57 ...(input.expectedThumbprint ? { expectedThumbprint: input.expectedThumbprint } : {}),48 ...(input.expectedThumbprint ? { expectedThumbprint: input.expectedThumbprint } : {}),
58 ...(input.expectedAccessToken ? { expectedAccessToken: input.expectedAccessToken } : {}),49 ...(input.expectedAccessToken ? { expectedAccessToken: input.expectedAccessToken } : {}),