Skip to content

Commit c08b968

Browse files
cursor[bot]cursoragentjuliusmarmingecodex
authored
Use Effect schema decoders for JSON parsing (#3060)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com> Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>
1 parent d9f59be commit c08b968

2 files changed

Lines changed: 156 additions & 122 deletions

File tree

packages/shared/src/dpop.test.ts

Lines changed: 107 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as NodeCrypto from "node:crypto";
22

3-
import { describe, expect, it } from "@effect/vitest";
3+
import { assert, describe, it } from "@effect/vitest";
44

55
import {
66
computeDpopAccessTokenHash,
@@ -56,59 +56,93 @@ describe("verifyDpopProof", () => {
5656

5757
it("verifies an ES256 DPoP proof and returns the RFC 7638 thumbprint", () => {
5858
const thumbprint = computeDpopJwkThumbprint(publicJwk);
59-
expect(
60-
verifyDpopProof({
61-
proof,
62-
method: "POST",
63-
url: "https://example.com/oauth/token",
64-
nowEpochSeconds: 101,
65-
expectedThumbprint: thumbprint,
66-
}),
67-
).toMatchObject({
68-
ok: true,
69-
thumbprint,
70-
jti: "proof-1",
59+
const result = verifyDpopProof({
60+
proof,
61+
method: "POST",
62+
url: "https://example.com/oauth/token",
63+
nowEpochSeconds: 101,
64+
expectedThumbprint: thumbprint,
65+
});
66+
67+
if (!result.ok) {
68+
assert.fail(result.reason);
69+
}
70+
assert.equal(result.thumbprint, thumbprint);
71+
assert.equal(result.jti, "proof-1");
72+
});
73+
74+
it("rejects malformed DPoP header and payload JSON", () => {
75+
const [header, payload, signature] = proof.split(".");
76+
if (!header || !payload || !signature) {
77+
assert.fail("Expected the test DPoP proof to use compact JWT format.");
78+
}
79+
const malformedJson = Buffer.from("{").toString("base64url");
80+
81+
const malformedHeader = verifyDpopProof({
82+
proof: `${malformedJson}.${payload}.${signature}`,
83+
method: "POST",
84+
url: "https://example.com/oauth/token",
85+
nowEpochSeconds: 101,
7186
});
87+
if (malformedHeader.ok) {
88+
assert.fail("Expected malformed DPoP header JSON to fail.");
89+
}
90+
assert.equal(malformedHeader.reason, "Invalid DPoP JWT header.");
91+
92+
const malformedPayload = verifyDpopProof({
93+
proof: `${header}.${malformedJson}.${signature}`,
94+
method: "POST",
95+
url: "https://example.com/oauth/token",
96+
nowEpochSeconds: 101,
97+
});
98+
if (malformedPayload.ok) {
99+
assert.fail("Expected malformed DPoP payload JSON to fail.");
100+
}
101+
assert.equal(malformedPayload.reason, "Invalid DPoP JWT payload.");
72102
});
73103

74104
it("rejects method, URL, thumbprint, and time-window mismatches", () => {
75105
const thumbprint = computeDpopJwkThumbprint(publicJwk);
76-
expect(
106+
assert.equal(
77107
verifyDpopProof({
78108
proof,
79109
method: "GET",
80110
url: "https://example.com/oauth/token",
81111
nowEpochSeconds: 101,
82112
expectedThumbprint: thumbprint,
83-
}),
84-
).toMatchObject({ ok: false });
85-
expect(
113+
}).ok,
114+
false,
115+
);
116+
assert.equal(
86117
verifyDpopProof({
87118
proof,
88119
method: "POST",
89120
url: "https://example.com/other",
90121
nowEpochSeconds: 101,
91122
expectedThumbprint: thumbprint,
92-
}),
93-
).toMatchObject({ ok: false });
94-
expect(
123+
}).ok,
124+
false,
125+
);
126+
assert.equal(
95127
verifyDpopProof({
96128
proof,
97129
method: "POST",
98130
url: "https://example.com/oauth/token",
99131
nowEpochSeconds: 101,
100132
expectedThumbprint: "other-thumbprint",
101-
}),
102-
).toMatchObject({ ok: false });
103-
expect(
133+
}).ok,
134+
false,
135+
);
136+
assert.equal(
104137
verifyDpopProof({
105138
proof,
106139
method: "POST",
107140
url: "https://example.com/oauth/token",
108141
nowEpochSeconds: 1_000,
109142
expectedThumbprint: thumbprint,
110-
}),
111-
).toMatchObject({ ok: false });
143+
}).ok,
144+
false,
145+
);
112146
});
113147

114148
it("requires the RFC 9449 access token hash when an access token is expected", () => {
@@ -122,40 +156,48 @@ describe("verifyDpopProof", () => {
122156
accessToken: "clerk-access-token",
123157
});
124158

125-
expect(
159+
assert.equal(
126160
verifyDpopProof({
127161
proof: accessTokenProof,
128162
method: "POST",
129163
url: "https://example.com/v1/environments/env/connect",
130164
nowEpochSeconds: 101,
131165
expectedThumbprint: thumbprint,
132166
expectedAccessToken: "clerk-access-token",
133-
}),
134-
).toMatchObject({ ok: true });
135-
expect(
136-
verifyDpopProof({
137-
proof,
138-
method: "POST",
139-
url: "https://example.com/oauth/token",
140-
nowEpochSeconds: 101,
141-
expectedThumbprint: thumbprint,
142-
expectedAccessToken: "clerk-access-token",
143-
}),
144-
).toMatchObject({ ok: false, reason: "DPoP access token hash mismatch." });
145-
expect(
146-
verifyDpopProof({
147-
proof: accessTokenProof,
148-
method: "POST",
149-
url: "https://example.com/v1/environments/env/connect",
150-
nowEpochSeconds: 101,
151-
expectedThumbprint: thumbprint,
152-
expectedAccessToken: "other-access-token",
153-
}),
154-
).toMatchObject({ ok: false, reason: "DPoP access token hash mismatch." });
167+
}).ok,
168+
true,
169+
);
170+
171+
const missingHash = verifyDpopProof({
172+
proof,
173+
method: "POST",
174+
url: "https://example.com/oauth/token",
175+
nowEpochSeconds: 101,
176+
expectedThumbprint: thumbprint,
177+
expectedAccessToken: "clerk-access-token",
178+
});
179+
if (missingHash.ok) {
180+
assert.fail("Expected DPoP proof without an access token hash to fail.");
181+
}
182+
assert.equal(missingHash.reason, "DPoP access token hash mismatch.");
183+
184+
const mismatchedHash = verifyDpopProof({
185+
proof: accessTokenProof,
186+
method: "POST",
187+
url: "https://example.com/v1/environments/env/connect",
188+
nowEpochSeconds: 101,
189+
expectedThumbprint: thumbprint,
190+
expectedAccessToken: "other-access-token",
191+
});
192+
if (mismatchedHash.ok) {
193+
assert.fail("Expected DPoP proof with a mismatched access token hash to fail.");
194+
}
195+
assert.equal(mismatchedHash.reason, "DPoP access token hash mismatch.");
155196
});
156197

157198
it("normalizes htu by excluding query and fragment components per RFC 9449", () => {
158-
expect(normalizeDpopHtu("https://example.com/v1/environments/env/connect?foo=bar#frag")).toBe(
199+
assert.equal(
200+
normalizeDpopHtu("https://example.com/v1/environments/env/connect?foo=bar#frag"),
159201
"https://example.com/v1/environments/env/connect",
160202
);
161203

@@ -168,15 +210,16 @@ describe("verifyDpopProof", () => {
168210
publicJwk,
169211
});
170212

171-
expect(
213+
assert.equal(
172214
verifyDpopProof({
173215
proof: queryProof,
174216
method: "POST",
175217
url: "https://example.com/v1/environments/env/connect?foo=bar#frag",
176218
nowEpochSeconds: 101,
177219
expectedThumbprint: thumbprint,
178-
}),
179-
).toMatchObject({ ok: true });
220+
}).ok,
221+
true,
222+
);
180223
});
181224

182225
it("rejects DPoP public JWK headers that expose private key material", () => {
@@ -192,14 +235,17 @@ describe("verifyDpopProof", () => {
192235
publicJwk: privateJwk,
193236
});
194237

195-
expect(
196-
verifyDpopProof({
197-
proof: proofWithPrivateJwk,
198-
method: "POST",
199-
url: "https://example.com/oauth/token",
200-
nowEpochSeconds: 101,
201-
expectedThumbprint: thumbprint,
202-
}),
203-
).toMatchObject({ ok: false, reason: "Invalid DPoP JWT header." });
238+
const result = verifyDpopProof({
239+
proof: proofWithPrivateJwk,
240+
method: "POST",
241+
url: "https://example.com/oauth/token",
242+
nowEpochSeconds: 101,
243+
expectedThumbprint: thumbprint,
244+
});
245+
246+
if (result.ok) {
247+
assert.fail("Expected DPoP proof with private JWK material to fail.");
248+
}
249+
assert.equal(result.reason, "Invalid DPoP JWT header.");
204250
});
205251
});

0 commit comments

Comments
 (0)