diff --git a/conformance/runner.ts b/conformance/runner.ts index 27506de8..7f65604a 100644 --- a/conformance/runner.ts +++ b/conformance/runner.ts @@ -337,11 +337,7 @@ export const flow = (options?: MacroOptions) => { try { result = await oauth.processPushedAuthorizationResponse(as, client, par) } catch (err) { - if ( - DPoPKeyPair && - err instanceof oauth.ResponseBodyError && - err.error === 'use_dpop_nonce' - ) { + if (DPoPKeyPair && oauth.isDPoPNonceError(err)) { t.log('error', inspect(err, { depth: Infinity })) t.log('retrying with a newly obtained dpop nonce') par = await request() @@ -413,11 +409,7 @@ export const flow = (options?: MacroOptions) => { result = await oauth.processAuthorizationCodeResponse(as, client, response) } } catch (err) { - if ( - DPoPKeyPair && - err instanceof oauth.ResponseBodyError && - err.error === 'use_dpop_nonce' - ) { + if (DPoPKeyPair && oauth.isDPoPNonceError(err)) { t.log('error', inspect(err, { depth: Infinity })) t.log('retrying with a newly obtained dpop nonce') response = await request() @@ -461,18 +453,9 @@ export const flow = (options?: MacroOptions) => { t.log('userinfo endpoint response', { ...result }) } catch (err) { t.log('error', inspect(err, { depth: Infinity })) - if (DPoPKeyPair && err instanceof oauth.WWWAuthenticateChallengeError) { - const { 0: challenge, length } = err.cause - if ( - length === 1 && - challenge.scheme === 'dpop' && - challenge.parameters.error === 'use_dpop_nonce' - ) { - t.log('retrying with a newly obtained dpop nonce') - response = await request() - } else { - throw err - } + if (DPoPKeyPair && oauth.isDPoPNonceError(err)) { + t.log('retrying with a newly obtained dpop nonce') + response = await request() } else { throw err } @@ -501,18 +484,9 @@ export const flow = (options?: MacroOptions) => { accounts = await request() } catch (err) { t.log('error', inspect(err, { depth: Infinity })) - if (DPoPKeyPair && err instanceof oauth.WWWAuthenticateChallengeError) { - const { 0: challenge, length } = err.cause - if ( - length === 1 && - challenge.scheme === 'dpop' && - challenge.parameters.error === 'use_dpop_nonce' - ) { - t.log('retrying with a newly obtained dpop nonce') - accounts = await request() - } else { - throw err - } + if (DPoPKeyPair && oauth.isDPoPNonceError(err)) { + t.log('retrying with a newly obtained dpop nonce') + accounts = await request() } else { throw err } diff --git a/examples/dpop.diff b/examples/dpop.diff index 6334071f..d029925b 100644 --- a/examples/dpop.diff +++ b/examples/dpop.diff @@ -1,5 +1,5 @@ diff --git a/examples/oauth.ts b/examples/dpop.ts -index d55e62d..29b3c7f 100644 +index d55e62d..4cb7a95 100644 --- a/examples/oauth.ts +++ b/examples/dpop.ts @@ -15,6 +15,12 @@ let client_secret!: string @@ -23,7 +23,7 @@ index d55e62d..29b3c7f 100644 const code_challenge_method = 'S256' /** -@@ -64,16 +71,32 @@ let access_token: string +@@ -64,16 +71,29 @@ let access_token: string const currentUrl: URL = getCurrentUrl() const params = oauth.validateAuthResponse(as, client, currentUrl, state) @@ -53,19 +53,16 @@ index d55e62d..29b3c7f 100644 - const result = await oauth.processAuthorizationCodeResponse(as, client, response) + let result = await processAuthorizationCodeResponse().catch(async (err) => { -+ if (err instanceof oauth.ResponseBodyError) { -+ if (result.error === 'use_dpop_nonce') { -+ // the AS-signalled nonce is now cached, retrying -+ response = await authorizationCodeGrantRequest() -+ return processAuthorizationCodeResponse() -+ } ++ if (oauth.isDPoPNonceError(err)) { ++ response = await authorizationCodeGrantRequest() ++ return processAuthorizationCodeResponse() + } + throw err + }) console.log('Access Token Response', result) ;({ access_token } = result) -@@ -81,11 +104,29 @@ let access_token: string +@@ -81,11 +101,22 @@ let access_token: string // Protected Resource Request { @@ -84,16 +81,9 @@ index d55e62d..29b3c7f 100644 + { DPoP }, + ) + let response = await protectedResourceRequest().catch((err) => { -+ if (err instanceof oauth.WWWAuthenticateChallengeError) { -+ const { 0: challenge, length } = err.cause -+ if ( -+ length === 1 && -+ challenge.scheme === 'dpop' && -+ challenge.parameters.error === 'use_dpop_nonce' -+ ) { -+ // the AS-signalled nonce is now cached, retrying -+ return protectedResourceRequest() -+ } ++ if (oauth.isDPoPNonceError(err)) { ++ // the RS-signalled nonce is now cached, retrying ++ return protectedResourceRequest() + } + throw err + }) diff --git a/examples/dpop.ts b/examples/dpop.ts index 29b3c7f9..4cb7a954 100644 --- a/examples/dpop.ts +++ b/examples/dpop.ts @@ -88,12 +88,9 @@ let access_token: string oauth.processAuthorizationCodeResponse(as, client, response) let result = await processAuthorizationCodeResponse().catch(async (err) => { - if (err instanceof oauth.ResponseBodyError) { - if (result.error === 'use_dpop_nonce') { - // the AS-signalled nonce is now cached, retrying - response = await authorizationCodeGrantRequest() - return processAuthorizationCodeResponse() - } + if (oauth.isDPoPNonceError(err)) { + response = await authorizationCodeGrantRequest() + return processAuthorizationCodeResponse() } throw err }) @@ -114,16 +111,9 @@ let access_token: string { DPoP }, ) let response = await protectedResourceRequest().catch((err) => { - if (err instanceof oauth.WWWAuthenticateChallengeError) { - const { 0: challenge, length } = err.cause - if ( - length === 1 && - challenge.scheme === 'dpop' && - challenge.parameters.error === 'use_dpop_nonce' - ) { - // the AS-signalled nonce is now cached, retrying - return protectedResourceRequest() - } + if (oauth.isDPoPNonceError(err)) { + // the RS-signalled nonce is now cached, retrying + return protectedResourceRequest() } throw err }) diff --git a/examples/fapi2-message-signing.diff b/examples/fapi2-message-signing.diff index 6b035817..2a9fa5b9 100644 --- a/examples/fapi2-message-signing.diff +++ b/examples/fapi2-message-signing.diff @@ -1,5 +1,5 @@ diff --git a/examples/fapi2.ts b/examples/fapi2-message-signing.ts -index e49247d..5857c0c 100644 +index bba1f24..b16f16f 100644 --- a/examples/fapi2.ts +++ b/examples/fapi2-message-signing.ts @@ -25,6 +25,11 @@ let DPoPKeys!: oauth.CryptoKeyPair @@ -45,7 +45,7 @@ index e49247d..5857c0c 100644 const pushedAuthorizationRequest = () => oauth.pushedAuthorizationRequest(as, client, clientAuth, params, { -@@ -92,7 +108,7 @@ let request_uri: string +@@ -90,7 +106,7 @@ let request_uri: string let access_token: string { const currentUrl: URL = getCurrentUrl() @@ -54,7 +54,7 @@ index e49247d..5857c0c 100644 const authorizationCodeGrantRequest = () => oauth.authorizationCodeGrantRequest( -@@ -108,7 +124,7 @@ let access_token: string +@@ -106,7 +122,7 @@ let access_token: string let response = await authorizationCodeGrantRequest() const processAuthorizationCodeResponse = () => @@ -62,8 +62,8 @@ index e49247d..5857c0c 100644 + oauth.processAuthorizationCodeResponse(as, client, response, true) let result = await processAuthorizationCodeResponse().catch(async (err) => { - if (err instanceof oauth.ResponseBodyError) { -@@ -121,6 +137,9 @@ let access_token: string + if (oauth.isDPoPNonceError(err)) { +@@ -117,6 +133,9 @@ let access_token: string throw err }) diff --git a/examples/fapi2-message-signing.ts b/examples/fapi2-message-signing.ts index 5857c0c6..b16f16fb 100644 --- a/examples/fapi2-message-signing.ts +++ b/examples/fapi2-message-signing.ts @@ -81,12 +81,10 @@ let request_uri: string const processPushedAuthorizationResponse = () => oauth.processPushedAuthorizationResponse(as, client, response) let result = await processPushedAuthorizationResponse().catch(async (err) => { - if (err instanceof oauth.ResponseBodyError) { - if (result.error === 'use_dpop_nonce') { - // the AS-signalled nonce is now cached, retrying - response = await pushedAuthorizationRequest() - return processPushedAuthorizationResponse() - } + if (oauth.isDPoPNonceError(err)) { + // the AS-signalled nonce is now cached, retrying + response = await pushedAuthorizationRequest() + return processPushedAuthorizationResponse() } throw err }) @@ -127,12 +125,10 @@ let access_token: string oauth.processAuthorizationCodeResponse(as, client, response, true) let result = await processAuthorizationCodeResponse().catch(async (err) => { - if (err instanceof oauth.ResponseBodyError) { - if (result.error === 'use_dpop_nonce') { - // the AS-signalled nonce is now cached, retrying - response = await authorizationCodeGrantRequest() - return processAuthorizationCodeResponse() - } + if (oauth.isDPoPNonceError(err)) { + // the AS-signalled nonce is now cached, retrying + response = await authorizationCodeGrantRequest() + return processAuthorizationCodeResponse() } throw err }) @@ -156,16 +152,9 @@ let access_token: string { DPoP }, ) let response = await protectedResourceRequest().catch((err) => { - if (err instanceof oauth.WWWAuthenticateChallengeError) { - const { 0: challenge, length } = err.cause - if ( - length === 1 && - challenge.scheme === 'dpop' && - challenge.parameters.error === 'use_dpop_nonce' - ) { - // the AS-signalled nonce is now cached, retrying - return protectedResourceRequest() - } + if (oauth.isDPoPNonceError(err)) { + // the RS-signalled nonce is now cached, retrying + return protectedResourceRequest() } throw err }) diff --git a/examples/fapi2.diff b/examples/fapi2.diff index acedf7c2..55553661 100644 --- a/examples/fapi2.diff +++ b/examples/fapi2.diff @@ -1,5 +1,5 @@ diff --git a/examples/oauth.ts b/examples/fapi2.ts -index d55e62d..e49247d 100644 +index d55e62d..bba1f24 100644 --- a/examples/oauth.ts +++ b/examples/fapi2.ts @@ -9,12 +9,22 @@ let algorithm!: @@ -26,7 +26,7 @@ index d55e62d..e49247d 100644 // End of prerequisites -@@ -23,36 +33,56 @@ const as = await oauth +@@ -23,36 +33,54 @@ const as = await oauth .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { client_id } @@ -66,12 +66,10 @@ index d55e62d..e49247d 100644 + const processPushedAuthorizationResponse = () => + oauth.processPushedAuthorizationResponse(as, client, response) + let result = await processPushedAuthorizationResponse().catch(async (err) => { -+ if (err instanceof oauth.ResponseBodyError) { -+ if (result.error === 'use_dpop_nonce') { -+ // the AS-signalled nonce is now cached, retrying -+ response = await pushedAuthorizationRequest() -+ return processPushedAuthorizationResponse() -+ } ++ if (oauth.isDPoPNonceError(err)) { ++ // the AS-signalled nonce is now cached, retrying ++ response = await pushedAuthorizationRequest() ++ return processPushedAuthorizationResponse() + } + throw err + }) @@ -101,7 +99,7 @@ index d55e62d..e49247d 100644 // now redirect the user to authorizationUrl.href } -@@ -62,18 +92,34 @@ let state: string | undefined +@@ -62,18 +90,32 @@ let state: string | undefined let access_token: string { const currentUrl: URL = getCurrentUrl() @@ -134,19 +132,17 @@ index d55e62d..e49247d 100644 - const result = await oauth.processAuthorizationCodeResponse(as, client, response) + let result = await processAuthorizationCodeResponse().catch(async (err) => { -+ if (err instanceof oauth.ResponseBodyError) { -+ if (result.error === 'use_dpop_nonce') { -+ // the AS-signalled nonce is now cached, retrying -+ response = await authorizationCodeGrantRequest() -+ return processAuthorizationCodeResponse() -+ } ++ if (oauth.isDPoPNonceError(err)) { ++ // the AS-signalled nonce is now cached, retrying ++ response = await authorizationCodeGrantRequest() ++ return processAuthorizationCodeResponse() + } + throw err + }) console.log('Access Token Response', result) ;({ access_token } = result) -@@ -81,11 +127,29 @@ let access_token: string +@@ -81,11 +123,22 @@ let access_token: string // Protected Resource Request { @@ -165,16 +161,9 @@ index d55e62d..e49247d 100644 + { DPoP }, + ) + let response = await protectedResourceRequest().catch((err) => { -+ if (err instanceof oauth.WWWAuthenticateChallengeError) { -+ const { 0: challenge, length } = err.cause -+ if ( -+ length === 1 && -+ challenge.scheme === 'dpop' && -+ challenge.parameters.error === 'use_dpop_nonce' -+ ) { -+ // the AS-signalled nonce is now cached, retrying -+ return protectedResourceRequest() -+ } ++ if (oauth.isDPoPNonceError(err)) { ++ // the RS-signalled nonce is now cached, retrying ++ return protectedResourceRequest() + } + throw err + }) diff --git a/examples/fapi2.ts b/examples/fapi2.ts index e49247d2..bba1f248 100644 --- a/examples/fapi2.ts +++ b/examples/fapi2.ts @@ -65,12 +65,10 @@ let request_uri: string const processPushedAuthorizationResponse = () => oauth.processPushedAuthorizationResponse(as, client, response) let result = await processPushedAuthorizationResponse().catch(async (err) => { - if (err instanceof oauth.ResponseBodyError) { - if (result.error === 'use_dpop_nonce') { - // the AS-signalled nonce is now cached, retrying - response = await pushedAuthorizationRequest() - return processPushedAuthorizationResponse() - } + if (oauth.isDPoPNonceError(err)) { + // the AS-signalled nonce is now cached, retrying + response = await pushedAuthorizationRequest() + return processPushedAuthorizationResponse() } throw err }) @@ -111,12 +109,10 @@ let access_token: string oauth.processAuthorizationCodeResponse(as, client, response) let result = await processAuthorizationCodeResponse().catch(async (err) => { - if (err instanceof oauth.ResponseBodyError) { - if (result.error === 'use_dpop_nonce') { - // the AS-signalled nonce is now cached, retrying - response = await authorizationCodeGrantRequest() - return processAuthorizationCodeResponse() - } + if (oauth.isDPoPNonceError(err)) { + // the AS-signalled nonce is now cached, retrying + response = await authorizationCodeGrantRequest() + return processAuthorizationCodeResponse() } throw err }) @@ -137,16 +133,9 @@ let access_token: string { DPoP }, ) let response = await protectedResourceRequest().catch((err) => { - if (err instanceof oauth.WWWAuthenticateChallengeError) { - const { 0: challenge, length } = err.cause - if ( - length === 1 && - challenge.scheme === 'dpop' && - challenge.parameters.error === 'use_dpop_nonce' - ) { - // the AS-signalled nonce is now cached, retrying - return protectedResourceRequest() - } + if (oauth.isDPoPNonceError(err)) { + // the RS-signalled nonce is now cached, retrying + return protectedResourceRequest() } throw err }) diff --git a/src/index.ts b/src/index.ts index f76cc16a..a6f3c621 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2156,13 +2156,36 @@ class DPoPHandler implements DPoPHandle { } } +/** + * Used to determine if a rejected error indicates the need to retry the request due to an + * expired/missing nonce. + * + * @group DPoP + */ +export function isDPoPNonceError(err: unknown): boolean { + if (err instanceof WWWAuthenticateChallengeError) { + const { 0: challenge, length } = err.cause + return ( + length === 1 && challenge.scheme === 'dpop' && challenge.parameters.error === 'use_dpop_nonce' + ) + } + + if (err instanceof ResponseBodyError) { + return err.error === 'use_dpop_nonce' + } + + return false +} + /** * Returns a wrapper / handle around a {@link !CryptoKeyPair} that is used for negotiating and * proving proof-of-possession to sender-constrain OAuth 2.0 tokens via DPoP at the Authorization * Server and Resource Server. * - * This wrapper / handle also keeps track of server-issued nonces, allowing this module to - * automatically retry requests with a fresh nonce when the server indicates the need to use one. + * This wrapper / handle also keeps track of server-issued nonces, allowing requests to be retried + * with a fresh nonce when the server indicates the need to use one. {@link isDPoPNonceError} can be + * used to determine if a rejected error indicates the need to retry the request due to an + * expired/missing nonce. * * @example * diff --git a/tap/helper.ts b/tap/helper.ts index 35a2e548..72640a52 100644 --- a/tap/helper.ts +++ b/tap/helper.ts @@ -4,18 +4,13 @@ import * as jose from 'jose' export function isDpopNonceError(t: Assert, err: unknown) { if (err instanceof lib.ResponseBodyError) { t.true(err.response.bodyUsed) - return err.error === 'use_dpop_nonce' } if (err instanceof lib.WWWAuthenticateChallengeError) { t.false(err.response.bodyUsed) - const { 0: challenge, length } = err.cause - return ( - length === 1 && challenge.scheme === 'dpop' && challenge.parameters.error === 'use_dpop_nonce' - ) } - return false + return lib.isDPoPNonceError(err) } export function random() {