History

Preserve token exchange diagnostics

Julius Marminge committed 4 months ago

Commit 39441ec1 parent e9914a92 files · +76 −6Browse files

Unified · Split

2 files
  1. infra/relay/src/http/Api.test.ts +52 −0
  2. infra/relay/src/http/Api.ts +24 −6
infra/relay/src/http/Api.test.ts+52−0
@@ -21,6 +21,7 @@ import { · 20 unmodified lines
2121 relayEnvironmentAuthLayer,
2222 relayNotFoundRoute,
2323 traceRelayHttpRequestWith,
24+ verifyRelayDpopTokenExchangeSubject,
2425 verifyRelayClientBearerToken,
2526 withoutCapturedParentSpan,
2627 } from "./Api.ts";
@@ -188,6 +189,57 @@ describe("relay client authentication", () => { · 161 unmodified lines
188189 );
189190 });
190191
192+describe("relay DPoP token exchange authentication", () => {
193+ it.effect("records safe Clerk verification diagnostics before returning invalid bearer", () => {
194+ const verificationCause = Object.assign(new Error("private token verification detail"), {
195+ reason: "session_invalid",
196+ });
197+
198+ return Effect.gen(function* () {
199+ const spans: Array<Tracer.NativeSpan> = [];
200+ const tracer = Tracer.make({
201+ span: (options) => {
202+ const span = new Tracer.NativeSpan(options);
203+ spans.push(span);
204+ return span;
205+ },
206+ });
207+ vi.mocked(verifyToken).mockRejectedValue(verificationCause);
208+
209+ const error = yield* Effect.flip(
210+ verifyRelayDpopTokenExchangeSubject(relaySettings, "private-subject-token").pipe(
211+ Effect.provideService(Tracer.Tracer, tracer),
212+ ),
213+ );
214+
215+ expect(Predicate.isTagged(error, "RelayAuthInvalidError")).toBe(true);
216+ if (Predicate.isTagged(error, "RelayAuthInvalidError")) {
217+ expect(error.reason).toBe("invalid_bearer");
218+ }
219+ const exchangeSpan = spans.find(
220+ (span) => span.name === "relay.auth.dpop_token_exchange_subject",
221+ );
222+ expect(exchangeSpan?.attributes.get("relay.auth.clerk_verification_failure")).toBe(
223+ "session_invalid",
224+ );
225+ expect(exchangeSpan?.attributes.get("relay.auth.clerk_verification_stage")).toBe(
226+ "session-token-verification",
227+ );
228+ for (const attribute of exchangeSpan?.attributes.values() ?? []) {
229+ expect(String(attribute)).not.toContain("private-subject-token");
230+ expect(String(attribute)).not.toContain("private token verification detail");
231+ }
232+ }).pipe(
233+ Effect.ensuring(
234+ Effect.sync(() => {
235+ vi.mocked(verifyToken).mockReset();
236+ vi.mocked(createClerkClient).mockReset();
237+ }),
238+ ),
239+ );
240+ });
241+});
242+
191243 describe("relay environment authentication", () => {
192244 it.effect("preserves credential lookup persistence failures as internal errors", () => {
193245 const failure = new EnvironmentCredentials.EnvironmentCredentialAuthenticatePersistenceError({
infra/relay/src/http/Api.ts+24−6
@@ -586,12 +586,10 @@ export const tokenApi = HttpApiBuilder.group( · 585 unmodified lines
586586 return yield* new HttpApiError.Unauthorized({});
587587 }
588588
589− const verified = yield* verifyClerkBearerToken(config, args.payload.subject_token).pipe(
590− Effect.catch(() => relayAuthInvalidError("invalid_bearer")),
589+ const userId = yield* verifyRelayDpopTokenExchangeSubject(
590+ config,
591+ args.payload.subject_token,
591592 );
592− if (!verified.sub || !hasExpectedClerkAudience(verified.aud, config.clerkJwtAudience)) {
593− return yield* relayAuthInvalidError("invalid_bearer");
594− }
595593 const proofKeyThumbprint = yield* requireDpopProof().pipe(
596594 Effect.provideService(DpopProofs.DpopProofReplay, dpopProofs),
597595 );
@@ -603,7 +601,7 @@ export const tokenApi = HttpApiBuilder.group( · 5 unmodified lines
603601 return {
604602 access_token: yield* relayTokens
605603 .issueDpopAccessToken({
606− userId: verified.sub,
604+ userId,
607605 proofKeyThumbprint,
608606 jti,
609607 issuedAtEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),
@@ -1062,6 +1060,26 @@ function hasExpectedClerkAudience(audience: unknown, expectedAudience: string): · 452 unmodified lines
10621060 audience.some((entry) => typeof entry === "string" && entry === expectedAudience);
10631061 }
10641062
1063+export const verifyRelayDpopTokenExchangeSubject = Effect.fn(
1064+ "relay.auth.dpop_token_exchange_subject",
1065+)(function* (config: RelayConfiguration.RelayConfiguration["Service"], token: string) {
1066+ const verified = yield* verifyClerkBearerToken(config, token).pipe(
1067+ Effect.tapErrorTag("ClerkTokenVerificationError", (error) =>
1068+ Effect.annotateCurrentSpan({
1069+ "relay.auth.clerk_verification_failure": error.reason,
1070+ "relay.auth.clerk_verification_stage": error.stage,
1071+ }),
1072+ ),
1073+ Effect.catchTags({
1074+ ClerkTokenVerificationError: () => relayAuthInvalidError("invalid_bearer"),
1075+ }),
1076+ );
1077+ if (!verified.sub || !hasExpectedClerkAudience(verified.aud, config.clerkJwtAudience)) {
1078+ return yield* relayAuthInvalidError("invalid_bearer");
1079+ }
1080+ return verified.sub;
1081+});
1082+
10651083 function verifyClerkBearerToken(
10661084 config: RelayConfiguration.RelayConfiguration["Service"],
10671085 token: string,

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t