123 lines · 4.1 KB · TypeScript
RawDownload
import {  type DpopVerificationFailureCode as DpopVerificationFailureCodeType,  verifyDpopProof,} from "@t3tools/shared/dpop";import type { DpopFailureReason } from "@t3tools/contracts";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as Option from "effect/Option";import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; import {  ServerAuthDpopReplayKeyCalculationError,  ServerAuthDpopReplayStateRecordError,  ServerAuthInvalidCredentialError,  type ServerAuthInternalError,} from "./EnvironmentAuth.ts";import * as ServerSecretStore from "./ServerSecretStore.ts"; export const mapDpopFailureReason = (code: DpopVerificationFailureCodeType): DpopFailureReason => {  switch (code) {    case "time_window":      return "time_window";    case "key_mismatch":      return "key_mismatch";    case "method_mismatch":    case "url_mismatch":      return "request_mismatch";    case "access_token_hash_mismatch":      return "token_mismatch";    case "missing_proof":    case "malformed_proof":    case "invalid_signature":    case "invalid_proof":      return "invalid_proof";  }}; export const mapDpopReplayStoreError = (  error: ServerSecretStore.SecretStoreError,): ServerAuthInvalidCredentialError | ServerAuthInternalError =>  ServerSecretStore.isSecretAlreadyExistsError(error)    ? new ServerAuthInvalidCredentialError({        diagnostic: "DPoP proof replayed.",        dpopFailureReason: "replay",        cause: error,      })    : new ServerAuthDpopReplayStateRecordError({        cause: error,      }); export const verifyRequestDpopProof = (input: {  readonly request: HttpServerRequest.HttpServerRequest;  readonly expectedThumbprint?: string;  readonly expectedAccessToken?: string;}) =>  Effect.gen(function* () {    const proof = input.request.headers.dpop;    const url = HttpServerRequest.toURL(input.request);    if (Option.isNone(url)) {      return yield* new ServerAuthInvalidCredentialError({        diagnostic: "Invalid DPoP request URL.",      });    }    const now = yield* DateTime.now;    const result = verifyDpopProof({      proof,      method: input.request.method,      url: url.value.href,      nowEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),      ...(input.expectedThumbprint ? { expectedThumbprint: input.expectedThumbprint } : {}),      ...(input.expectedAccessToken ? { expectedAccessToken: input.expectedAccessToken } : {}),    });    if (!result.ok) {      yield* Effect.annotateCurrentSpan({        "environment.dpop.failure_code": result.code,      });      return yield* new ServerAuthInvalidCredentialError({        diagnostic: result.reason,        dpopFailureReason: mapDpopFailureReason(result.code),      });    }    const secretStore = yield* ServerSecretStore.ServerSecretStore;    const replayKey = yield* Crypto.Crypto.pipe(      Effect.flatMap((crypto) =>        crypto.digest("SHA-256", new TextEncoder().encode(`${result.thumbprint}:${result.jti}`)),      ),      Effect.map(Encoding.encodeBase64Url),      Effect.mapError(        (cause) =>          new ServerAuthDpopReplayKeyCalculationError({            cause,          }),      ),    );    yield* secretStore      .create(        `dpop-proof-${replayKey}`,        new TextEncoder().encode(          [            `thumbprint=${result.thumbprint}`,            `jti=${result.jti}`,            `iat=${result.iat}`,            `consumedAt=${DateTime.formatIso(now)}`,          ].join("\n"),        ),      )      .pipe(        Effect.catchIf(ServerSecretStore.isSecretStoreError, (error) =>          Effect.gen(function* () {            const mapped = mapDpopReplayStoreError(error);            if (mapped._tag === "ServerAuthInvalidCredentialError") {              yield* Effect.annotateCurrentSpan({                "environment.dpop.failure_code": mapped.dpopFailureReason,              });            }            return yield* Effect.fail(mapped);          }),        ),      );    return result.thumbprint;  });