(opts: {
tokenEndpoint: string
assertion: string
clientId: string
clientSecret: string
authMethod?: 'client_secret_basic' | 'client_secret_post'
scope?: string
fetchFn?: FetchLike
})
| 335 | * `client_secret_post` if the AS explicitly requires it. |
| 336 | */ |
| 337 | export async function exchangeJwtAuthGrant(opts: { |
| 338 | tokenEndpoint: string |
| 339 | assertion: string |
| 340 | clientId: string |
| 341 | clientSecret: string |
| 342 | authMethod?: 'client_secret_basic' | 'client_secret_post' |
| 343 | scope?: string |
| 344 | fetchFn?: FetchLike |
| 345 | }): Promise<XaaTokenResult> { |
| 346 | const fetchFn = opts.fetchFn ?? defaultFetch |
| 347 | const authMethod = opts.authMethod ?? 'client_secret_basic' |
| 348 | |
| 349 | const params = new URLSearchParams({ |
| 350 | grant_type: JWT_BEARER_GRANT, |
| 351 | assertion: opts.assertion, |
| 352 | }) |
| 353 | if (opts.scope) { |
| 354 | params.set('scope', opts.scope) |
| 355 | } |
| 356 | |
| 357 | const headers: Record<string, string> = { |
| 358 | 'Content-Type': 'application/x-www-form-urlencoded', |
| 359 | } |
| 360 | if (authMethod === 'client_secret_basic') { |
| 361 | const basicAuth = Buffer.from( |
| 362 | `${encodeURIComponent(opts.clientId)}:${encodeURIComponent(opts.clientSecret)}`, |
| 363 | ).toString('base64') |
| 364 | headers.Authorization = `Basic ${basicAuth}` |
| 365 | } else { |
| 366 | params.set('client_id', opts.clientId) |
| 367 | params.set('client_secret', opts.clientSecret) |
| 368 | } |
| 369 | |
| 370 | const res = await fetchFn(opts.tokenEndpoint, { |
| 371 | method: 'POST', |
| 372 | headers, |
| 373 | body: params, |
| 374 | }) |
| 375 | if (!res.ok) { |
| 376 | const body = redactTokens(await res.text()).slice(0, 200) |
| 377 | throw new Error(`XAA: jwt-bearer grant failed: HTTP ${res.status}: ${body}`) |
| 378 | } |
| 379 | let rawTokens: unknown |
| 380 | try { |
| 381 | rawTokens = await res.json() |
| 382 | } catch { |
| 383 | throw new Error( |
| 384 | `XAA: jwt-bearer grant returned non-JSON (captive portal?) at ${opts.tokenEndpoint}`, |
| 385 | ) |
| 386 | } |
| 387 | const tokensParsed = JwtBearerResponseSchema().safeParse(rawTokens) |
| 388 | if (!tokensParsed.success) { |
| 389 | throw new Error( |
| 390 | `XAA: jwt-bearer response did not match expected shape: ${redactTokens(rawTokens)}`, |
| 391 | ) |
| 392 | } |
| 393 | return tokensParsed.data |
| 394 | } |
no test coverage detected