| ... | @@ -3,7 +3,8 @@ import * as Crypto from "effect/Crypto"; | ... | @@ -3,7 +3,8 @@ import * as Crypto from "effect/Crypto"; |
| 3 | import * as DateTime from "effect/DateTime"; | 3 | import * as DateTime from "effect/DateTime"; |
| 4 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Encoding from "effect/Encoding"; | 5 | import * as Encoding from "effect/Encoding"; |
| 6 | import type * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; | 6 | import * as Option from "effect/Option"; |
| | 7 | import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; |
| 7 | | 8 | |
| 8 | import { | 9 | import { |
| 9 | ServerAuthDpopReplayKeyCalculationError, | 10 | ServerAuthDpopReplayKeyCalculationError, |
| ... | @@ -13,22 +14,6 @@ import { | ... | @@ -13,22 +14,6 @@ import { |
| 13 | } from "./EnvironmentAuth.ts"; | 14 | } from "./EnvironmentAuth.ts"; |
| 14 | import * as ServerSecretStore from "./ServerSecretStore.ts"; | 15 | import * as ServerSecretStore from "./ServerSecretStore.ts"; |
| 15 | | 16 | |
| 16 | function firstHeaderValue(value: string | undefined): string | undefined { | | |
| 17 | const first = value?.split(",")[0]?.trim(); | | |
| 18 | return first && first.length > 0 ? first : undefined; | | |
| 19 | } | | |
| 20 | | | |
| 21 | export 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 | | | |
| 32 | export const mapDpopReplayStoreError = ( | 17 | export 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 } : {}), |