@@ -8,6 +8,10 @@ import {
88} from '@openid4vc/utils'
99import { ValidationError } from '../../../utils/src/error/ValidationError'
1010import type { CallbackContext } from '../callbacks'
11+ import {
12+ authorizationServerRequestWithClientAttestationChallengeRetry ,
13+ extractClientAttestationChallengeFromHeaders ,
14+ } from '../client-attestation/client-attestation-challenge'
1115import { createDpopHeadersForRequest , extractDpopNonceFromHeaders , type RequestDpopOptions } from '../dpop/dpop'
1216import { authorizationServerRequestWithDpopRetry } from '../dpop/dpop-retry'
1317import { Oauth2ClientErrorResponseError } from '../error/Oauth2ClientErrorResponseError'
@@ -31,6 +35,13 @@ import {
3135export interface RetrieveAccessTokenReturn {
3236 accessTokenResponse : AccessTokenResponse
3337 dpop ?: RequestDpopOptions
38+
39+ /**
40+ * A fresh Client Attestation challenge provided by the authorization server in the
41+ * `OAuth-Client-Attestation-Challenge` response header (draft 09 §6.2). If present, the client
42+ * should use this challenge for the next Client Attestation PoP JWT.
43+ */
44+ attestationChallenge ?: string
3445}
3546
3647interface RetrieveAccessTokenBaseOptions {
@@ -231,83 +242,88 @@ async function retrieveAccessToken(options: RetrieveAccessTokenOptions): Promise
231242 accessTokenRequest . user_pin = accessTokenRequest . tx_code
232243 }
233244
234- return await authorizationServerRequestWithDpopRetry ( {
235- dpop : options . dpop ,
236- request : async ( dpop ) => {
237- const dpopHeaders = dpop
238- ? await createDpopHeadersForRequest ( {
239- request : {
240- method : 'POST' ,
241- url : options . authorizationServerMetadata . token_endpoint ,
242- } ,
243- signer : dpop . signer ,
244- callbacks : options . callbacks ,
245- nonce : dpop . nonce ,
245+ return await authorizationServerRequestWithClientAttestationChallengeRetry ( {
246+ request : ( attestationChallenge ) =>
247+ authorizationServerRequestWithDpopRetry ( {
248+ dpop : options . dpop ,
249+ request : async ( dpop ) => {
250+ const dpopHeaders = dpop
251+ ? await createDpopHeadersForRequest ( {
252+ request : {
253+ method : 'POST' ,
254+ url : options . authorizationServerMetadata . token_endpoint ,
255+ } ,
256+ signer : dpop . signer ,
257+ callbacks : options . callbacks ,
258+ nonce : dpop . nonce ,
259+ } )
260+ : undefined
261+
262+ const headers = new Headers ( {
263+ 'Content-Type' : ContentType . XWwwFormUrlencoded ,
264+ ...dpopHeaders ,
246265 } )
247- : undefined
248-
249- const headers = new Headers ( {
250- 'Content-Type' : ContentType . XWwwFormUrlencoded ,
251- ...dpopHeaders ,
252- } )
253-
254- // Apply client authentication
255- await options . callbacks . clientAuthentication ( {
256- url : options . authorizationServerMetadata . token_endpoint ,
257- method : 'POST' ,
258- authorizationServerMetadata : options . authorizationServerMetadata ,
259- body : accessTokenRequest ,
260- contentType : ContentType . XWwwFormUrlencoded ,
261- headers,
262- } )
263-
264- const { response, result } = await fetchWithZod (
265- zAccessTokenResponse ,
266- ContentType . Json ,
267- options . authorizationServerMetadata . token_endpoint ,
268- {
269- body : objectToQueryParams ( accessTokenRequest ) . toString ( ) ,
270- method : 'POST' ,
271- headers,
272- }
273- )
274-
275- if ( ! response . ok || ! result ) {
276- const tokenErrorResponse = zAccessTokenErrorResponse . safeParse (
277- await response
278- . clone ( )
279- . json ( )
280- . catch ( ( ) => null )
281- )
282- if ( tokenErrorResponse . success ) {
283- throw new Oauth2ClientErrorResponseError (
284- `Unable to retrieve access token from '${ options . authorizationServerMetadata . token_endpoint } '. Received token error response with status ${ response . status } ` ,
285- tokenErrorResponse . data ,
286- response
266+
267+ // Apply client authentication
268+ await options . callbacks . clientAuthentication ( {
269+ url : options . authorizationServerMetadata . token_endpoint ,
270+ method : 'POST' ,
271+ authorizationServerMetadata : options . authorizationServerMetadata ,
272+ body : accessTokenRequest ,
273+ contentType : ContentType . XWwwFormUrlencoded ,
274+ headers,
275+ attestationChallenge,
276+ } )
277+
278+ const { response, result } = await fetchWithZod (
279+ zAccessTokenResponse ,
280+ ContentType . Json ,
281+ options . authorizationServerMetadata . token_endpoint ,
282+ {
283+ body : objectToQueryParams ( accessTokenRequest ) . toString ( ) ,
284+ method : 'POST' ,
285+ headers,
286+ }
287287 )
288- }
289-
290- throw new InvalidFetchResponseError (
291- `Unable to retrieve access token from '${ options . authorizationServerMetadata . token_endpoint } '. Received response with status ${ response . status } ` ,
292- await response . clone ( ) . text ( ) ,
293- response
294- )
295- }
296-
297- if ( ! result . success ) {
298- throw new ValidationError ( 'Error validating access token response' , result . error )
299- }
300-
301- const dpopNonce = extractDpopNonceFromHeaders ( response . headers ) ?? undefined
302- return {
303- dpop : dpop
304- ? {
305- ...dpop ,
306- nonce : dpopNonce ,
288+
289+ if ( ! response . ok || ! result ) {
290+ const tokenErrorResponse = zAccessTokenErrorResponse . safeParse (
291+ await response
292+ . clone ( )
293+ . json ( )
294+ . catch ( ( ) => null )
295+ )
296+ if ( tokenErrorResponse . success ) {
297+ throw new Oauth2ClientErrorResponseError (
298+ `Unable to retrieve access token from '${ options . authorizationServerMetadata . token_endpoint } '. Received token error response with status ${ response . status } ` ,
299+ tokenErrorResponse . data ,
300+ response
301+ )
307302 }
308- : undefined ,
309- accessTokenResponse : result . data ,
310- }
311- } ,
303+
304+ throw new InvalidFetchResponseError (
305+ `Unable to retrieve access token from '${ options . authorizationServerMetadata . token_endpoint } '. Received response with status ${ response . status } ` ,
306+ await response . clone ( ) . text ( ) ,
307+ response
308+ )
309+ }
310+
311+ if ( ! result . success ) {
312+ throw new ValidationError ( 'Error validating access token response' , result . error )
313+ }
314+
315+ const dpopNonce = extractDpopNonceFromHeaders ( response . headers ) ?? undefined
316+ return {
317+ dpop : dpop
318+ ? {
319+ ...dpop ,
320+ nonce : dpopNonce ,
321+ }
322+ : undefined ,
323+ attestationChallenge : extractClientAttestationChallengeFromHeaders ( response . headers ) ?? undefined ,
324+ accessTokenResponse : result . data ,
325+ }
326+ } ,
327+ } ) ,
312328 } )
313329}
0 commit comments