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
21 relayEnvironmentAuthLayer,21 relayEnvironmentAuthLayer,
22 relayNotFoundRoute,22 relayNotFoundRoute,
23 traceRelayHttpRequestWith,23 traceRelayHttpRequestWith,
24 verifyRelayDpopTokenExchangeSubject,
24 verifyRelayClientBearerToken,25 verifyRelayClientBearerToken,
25 withoutCapturedParentSpan,26 withoutCapturedParentSpan,
26} from "./Api.ts";27} from "./Api.ts";
@@ -188,6 +189,57 @@ describe("relay client authentication", () => { · 161 unmodified lines
188 );189 );
189});190});
190 191
192describe("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
191describe("relay environment authentication", () => {243describe("relay environment authentication", () => {
192 it.effect("preserves credential lookup persistence failures as internal errors", () => {244 it.effect("preserves credential lookup persistence failures as internal errors", () => {
193 const failure = new EnvironmentCredentials.EnvironmentCredentialAuthenticatePersistenceError({245 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
586 return yield* new HttpApiError.Unauthorized({});586 return yield* new HttpApiError.Unauthorized({});
587 }587 }
588 588
589 const verified = yield* verifyClerkBearerToken(config, args.payload.subject_token).pipe(589 const userId = yield* verifyRelayDpopTokenExchangeSubject(
590 Effect.catch(() => relayAuthInvalidError("invalid_bearer")),590 config,
591 args.payload.subject_token,
591 );592 );
592 if (!verified.sub || !hasExpectedClerkAudience(verified.aud, config.clerkJwtAudience)) {
593 return yield* relayAuthInvalidError("invalid_bearer");
594 }
595 const proofKeyThumbprint = yield* requireDpopProof().pipe(593 const proofKeyThumbprint = yield* requireDpopProof().pipe(
596 Effect.provideService(DpopProofs.DpopProofReplay, dpopProofs),594 Effect.provideService(DpopProofs.DpopProofReplay, dpopProofs),
597 );595 );
@@ -603,7 +601,7 @@ export const tokenApi = HttpApiBuilder.group( · 5 unmodified lines
603 return {601 return {
604 access_token: yield* relayTokens602 access_token: yield* relayTokens
605 .issueDpopAccessToken({603 .issueDpopAccessToken({
606 userId: verified.sub,604 userId,
607 proofKeyThumbprint,605 proofKeyThumbprint,
608 jti,606 jti,
609 issuedAtEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),607 issuedAtEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),
@@ -1062,6 +1060,26 @@ function hasExpectedClerkAudience(audience: unknown, expectedAudience: string): · 452 unmodified lines
1062 audience.some((entry) => typeof entry === "string" && entry === expectedAudience);1060 audience.some((entry) => typeof entry === "string" && entry === expectedAudience);
1063}1061}
1064 1062
1063export 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
1065function verifyClerkBearerToken(1083function verifyClerkBearerToken(
1066 config: RelayConfiguration.RelayConfiguration["Service"],1084 config: RelayConfiguration.RelayConfiguration["Service"],
1067 token: string,1085 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