- Package Name:
@azure/identity
- Package Version: 4.13.1 (latest stable; same code on
main at 4.14.0-beta.4)
- Operating system: any
- nodejs version: any
- Is the bug related to documentation? no
When OnBehalfOfCredential.getToken() fails because Entra ID applies a Conditional Access policy at the OBO /token exchange, MSAL throws InteractionRequiredAuthError with a populated .claims field (the JSON the upstream client must send on its next /authorize to satisfy the policy). @azure/identity catches that error in msalClient.ts and runs it through handleMsalError, which wraps it in a fresh AuthenticationRequiredError({ scopes, getTokenOptions, message: error.message }). The .claims field is dropped and there is no cause chain, so the caller has no way to recover the directive.
This blocks the documented OBO Conditional Access pattern (docs): the middle-tier service is supposed to surface claims back to the calling client so the client can re-run interactive auth with ?claims=.... Without .claims on the thrown error, a middle-tier built on OnBehalfOfCredential can only show a generic "authentication required" failure.
To Reproduce
import { OnBehalfOfCredential } from "@azure/identity";
const cred = new OnBehalfOfCredential({
tenantId, clientId, clientSecret,
userAssertionToken: incomingAccessToken,
});
try {
await cred.getToken(["https://graph.microsoft.com/.default"], { enableCae: true });
} catch (e) {
// Tenant has "Require MFA for Office 365" CA policy.
// MSAL threw InteractionRequiredAuthError with
// .claims = '{"access_token":{"capolids":{"essential":true,"values":["..."]}}}'
// but e is AuthenticationRequiredError with only { scopes, getTokenOptions, message }.
console.log((e as any).claims); // undefined
console.log((e as any).cause); // undefined
}
Expected behavior
AuthenticationRequiredError should carry claims (or chain the original MSAL error via cause) so a middle-tier service can propagate the directive to its caller.
Relevant code
msalClient.ts getTokenOnBehalfOf catch: catch (err: any) { throw handleMsalError(scopes, err, options); }
utils.ts handleMsalError: InteractionRequiredAuthError has .name === "InteractionRequiredAuthError", which matches none of the checked names, so it falls through to return new AuthenticationRequiredError({ scopes, getTokenOptions, message: error.message }); claims and cause are not passed
errors.ts AuthenticationRequiredError: options interface is { scopes, getTokenOptions?, message?, cause? }; cause is already supported but no claims field exists
Proposed fix
Either of these would work:
- Add
claims?: string to AuthenticationRequiredErrorOptions and AuthenticationRequiredError, and have handleMsalError pass claims: (error as InteractionRequiredAuthError).claims when present.
- Pass
cause: error to the AuthenticationRequiredError constructor (the class already accepts options.cause, handleMsalError just doesn't supply it). Callers can then read (e.cause as InteractionRequiredAuthError).claims.
Additional context
The SDK already supports the resource-server side of CAE (#31501 in bearerTokenAuthenticationPolicy, and getToken({ claims, enableCae }) forwarding in msalClient.ts). This issue is specifically the authorization-server side: surfacing claims when the credential's own /token call fails. Workaround today is to bypass @azure/identity and use @azure/msal-node ConfidentialClientApplication.acquireTokenOnBehalfOf directly, which throws the InteractionRequiredAuthError raw, but that loses the IdentityClient retry policy.
@azure/identitymainat 4.14.0-beta.4)When
OnBehalfOfCredential.getToken()fails because Entra ID applies a Conditional Access policy at the OBO/tokenexchange, MSAL throwsInteractionRequiredAuthErrorwith a populated.claimsfield (the JSON the upstream client must send on its next/authorizeto satisfy the policy).@azure/identitycatches that error inmsalClient.tsand runs it throughhandleMsalError, which wraps it in a freshAuthenticationRequiredError({ scopes, getTokenOptions, message: error.message }). The.claimsfield is dropped and there is nocausechain, so the caller has no way to recover the directive.This blocks the documented OBO Conditional Access pattern (docs): the middle-tier service is supposed to surface
claimsback to the calling client so the client can re-run interactive auth with?claims=.... Without.claimson the thrown error, a middle-tier built onOnBehalfOfCredentialcan only show a generic "authentication required" failure.To Reproduce
Expected behavior
AuthenticationRequiredErrorshould carryclaims(or chain the original MSAL error viacause) so a middle-tier service can propagate the directive to its caller.Relevant code
msalClient.tsgetTokenOnBehalfOfcatch:catch (err: any) { throw handleMsalError(scopes, err, options); }utils.tshandleMsalError:InteractionRequiredAuthErrorhas.name === "InteractionRequiredAuthError", which matches none of the checked names, so it falls through toreturn new AuthenticationRequiredError({ scopes, getTokenOptions, message: error.message });claimsandcauseare not passederrors.tsAuthenticationRequiredError: options interface is{ scopes, getTokenOptions?, message?, cause? };causeis already supported but noclaimsfield existsProposed fix
Either of these would work:
claims?: stringtoAuthenticationRequiredErrorOptionsandAuthenticationRequiredError, and havehandleMsalErrorpassclaims: (error as InteractionRequiredAuthError).claimswhen present.cause: errorto theAuthenticationRequiredErrorconstructor (the class already acceptsoptions.cause,handleMsalErrorjust doesn't supply it). Callers can then read(e.cause as InteractionRequiredAuthError).claims.Additional context
The SDK already supports the resource-server side of CAE (#31501 in
bearerTokenAuthenticationPolicy, andgetToken({ claims, enableCae })forwarding inmsalClient.ts). This issue is specifically the authorization-server side: surfacingclaimswhen the credential's own/tokencall fails. Workaround today is to bypass@azure/identityand use@azure/msal-nodeConfidentialClientApplication.acquireTokenOnBehalfOfdirectly, which throws theInteractionRequiredAuthErrorraw, but that loses theIdentityClientretry policy.