fix: retry dpop nonce errors for generic grants

This commit is contained in:
Filip Skokan
2026-04-27 17:35:51 +02:00
parent 4a19c492a2
commit 9eba32cbfd
2 changed files with 111 additions and 12 deletions
+25 -12
View File
@@ -4270,7 +4270,7 @@ export async function genericGrantRequest(
checkConfig(config)
const { as, c, auth, fetch, tlsOnly, timeout, decrypt } = int(config)
const result = await oauth
const response = await oauth
.genericTokenEndpointRequest(
as,
c,
@@ -4285,19 +4285,32 @@ export async function genericGrantRequest(
signal: signal(timeout),
},
)
.then((response) => {
let recognizedTokenTypes: oauth.RecognizedTokenTypes | undefined
if (grantType === 'urn:ietf:params:oauth:grant-type:token-exchange') {
recognizedTokenTypes = { n_a: () => {} }
}
return oauth.processGenericTokenEndpointResponse(as, c, response, {
[oauth.jweDecrypt]: decrypt,
recognizedTokenTypes,
})
})
.catch(errorHandler)
let recognizedTokenTypes: oauth.RecognizedTokenTypes | undefined
if (grantType === 'urn:ietf:params:oauth:grant-type:token-exchange') {
recognizedTokenTypes = { n_a: () => {} }
}
const p = oauth.processGenericTokenEndpointResponse(as, c, response, {
[oauth.jweDecrypt]: decrypt,
recognizedTokenTypes,
})
let result: oauth.TokenEndpointResponse
try {
result = await p
} catch (err) {
if (retryable(err, options)) {
return genericGrantRequest(config, grantType, parameters, {
...options,
flag: retry,
})
}
errorHandler(err)
}
addHelpers(result)
return result
}
@@ -1,6 +1,7 @@
import test from 'ava'
import * as client from '../src/index.js'
import * as undici from 'undici'
import * as jose from 'jose'
test('genericGrantRequest accepts n_a token type in token exchange grant response', async (t) => {
let agent = new undici.MockAgent()
@@ -123,3 +124,88 @@ test('genericGrantRequest with other grant types does not add n_a token type rec
t.notThrows(() => agent.assertNoPendingInterceptors())
})
test('genericGrantRequest retries DPoP nonce errors', async (t) => {
let agent = new undici.MockAgent()
agent.disableNetConnect()
const mockAgent = agent.get('https://as.example.com')
mockAgent
.intercept({
method: 'POST',
path: '/token',
headers({ dpop }) {
return (
typeof dpop === 'string' && jose.decodeJwt(dpop).nonce === undefined
)
},
})
.reply(
400,
{},
{
headers: {
'dpop-nonce': 'use this',
'www-authenticate': 'dpop error="use_dpop_nonce"',
},
},
)
mockAgent
.intercept({
method: 'POST',
path: '/token',
headers({ dpop }) {
return (
typeof dpop === 'string' && jose.decodeJwt(dpop).nonce === 'use this'
)
},
})
.reply(
200,
{
access_token: 'test-access-token',
token_type: 'DPoP',
},
{
headers: {
'content-type': 'application/json',
},
},
)
const config = new client.Configuration(
{
issuer: 'https://as.example.com',
token_endpoint: 'https://as.example.com/token',
},
'test-client-id',
undefined,
client.None(),
)
// @ts-ignore
config[client.customFetch] = (url, options) => {
return undici.fetch(url, { ...options, dispatcher: agent })
}
const DPoP = client.getDPoPHandle(
config,
await client.randomDPoPKeyPair('ES256'),
)
const result = await client.genericGrantRequest(
config,
'urn:ietf:params:oauth:grant-type:jwt-bearer',
{
assertion: 'jwt-assertion-value',
},
{ DPoP },
)
t.is(result.access_token, 'test-access-token')
t.is(result.token_type, 'dpop')
t.notThrows(() => agent.assertNoPendingInterceptors())
})