11import * as NodeCrypto from "node:crypto" ;
22
3- import { describe , expect , it } from "@effect/vitest" ;
3+ import { assert , describe , it } from "@effect/vitest" ;
44
55import {
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