AzureAD / microsoft-authentication-library-for-java

Microsoft Authentication Library (MSAL) for Java http://aka.ms/aadv2
MIT License
285 stars 143 forks source link

[Bug] Error when acquiring token silently #869

Open q-benwillis opened 1 week ago

q-benwillis commented 1 week ago

Library version used

1.15.1

Java version

openjdk version "17.0.11" 2024-04-16 LTS

Scenario

ConfidentialClient - web api (AcquireTokenOnBehalfOf)

Is this a new or an existing app?

This is a new app or experiment

Issue description and reproduction steps

Seeing the following NPE when attempting to acquire a token silently:

2024-10-08T11:43:08.732+01:00 DEBUG 33188 --- [app] [onPool-worker-2] c.m.aad.msal4j.TokenRequestExecutor      : Sending token request to: https://login.microsoftonline.com/<tenant-id>/
2024-10-08T11:43:08.939+01:00 DEBUG 33188 --- [app] [onPool-worker-2] c.m.a.m.ConfidentialClientApplication    : [Correlation ID: 98cfdbb2-3240-425c-a7c2-6622960f493b] Access Token and Refresh Token were returned
2024-10-08T11:43:11.024+01:00  WARN 33188 --- [app] [onPool-worker-2] c.m.a.m.ConfidentialClientApplication    : [Correlation ID: 779911f4-83a1-4940-97fe-2b676dc6c5c1] Execution of class com.microsoft.aad.msal4j.AcquireTokenSilentSupplier failed: Cannot invoke "String.equals(Object)" because "accessToken.homeAccountId" is null
2024-10-08T11:43:11.024+01:00  WARN 33188 --- [app] [  io-compute-15] c.q.f.s.FabricAuthenticationClientImpl   : Failed to get token silently using session ID, getting OBO token using subject token

java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "accessToken.homeAccountId" is null
        at com.microsoft.aad.msal4j.TokenCache.lambda$getAccessTokenCacheEntity$3(TokenCache.java:481) ~[msal4j-1.15.1.jar:1.15.1]
        at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:178) ~[na:na]
        at java.base/java.util.Spliterators$IteratorSpliterator.tryAdvance(Spliterators.java:1856) ~[na:na]
        at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:129) ~[na:na]
        at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:527) ~[na:na]
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513) ~[na:na]
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
        at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150) ~[na:na]
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
        at java.base/java.util.stream.ReferencePipeline.findAny(ReferencePipeline.java:652) ~[na:na]
        at com.microsoft.aad.msal4j.TokenCache.getAccessTokenCacheEntity(TokenCache.java:487) ~[msal4j-1.15.1.jar:1.15.1]
        at com.microsoft.aad.msal4j.TokenCache.getCachedAuthenticationResult(TokenCache.java:617) ~[msal4j-1.15.1.jar:1.15.1]
        at com.microsoft.aad.msal4j.AcquireTokenSilentSupplier.execute(AcquireTokenSilentSupplier.java:29) ~[msal4j-1.15.1.jar:1.15.1]
        at com.microsoft.aad.msal4j.AuthenticationResultSupplier.get(AuthenticationResultSupplier.java:69) ~[msal4j-1.15.1.jar:1.15.1]
        at com.microsoft.aad.msal4j.AuthenticationResultSupplier.get(AuthenticationResultSupplier.java:18) ~[msal4j-1.15.1.jar:1.15.1]
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[na:na]
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1760) ~[na:na]
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[na:na]
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[na:na]
        at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[na:na]
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[na:na]
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[na:na]

I've acquired an Authentication Result using the OnBehalfOfParameters and am now trying to get a token silently using the account from the first Authentication Result.

I've seen this working in some of my test scenarios but in the deployed version of my application I get this warning and null pointer exception.

Relevant code snippets

No response

Expected behavior

Retrieve token silently using account

Identity provider

Microsoft Entra ID (Work and School accounts and Personal Microsoft accounts)

Regression

No response

Solution and workarounds

I feel like here we could just check that homeAccountId isn't null for each access token?

I think it's because I have previously requested an app token which is stored in the same cache, I could also perhaps use two different confidential clients.

Avery-Dunn commented 1 week ago

Hello @q-benwillis, could you clarify a few things:

The line in TokenCache that you linked is where the homeAccountId value is potentially set in the cache, but it's here where the null is being found and failing: https://github.com/AzureAD/microsoft-authentication-library-for-java/blob/081341c8d0afe8ca5d024f60047d45c5f440a5c7/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenCache.java#L480C37-L480C57

However, for that to fail it means it's being passed a non-null Account object that has a null homeAccountId field. But homeAccountId is essentially the tenant ID plus the account ID in that tenant, and I can't imagine a scenario where that would be null.

q-benwillis commented 6 days ago

Hi, thanks for taking a look at this

Are you explicitly calling the acquireTokenSilently(SilentParameters) API? If so, are you passing in the account object you got in the result of the original acquireToken(OnBehalfOfParameters), and does that account object have a value for homeAccountId?

Yes I am explicitly calling acquireTokenSilently(SilentParameters) and passing in the account object I received from a previous call. I've used the debugger to validate that the account definitely does have a homeAccountId

so you could try simply calling acquireToken(OnBehalfOfParameters) twice and seeing if you get the same error (and if not, confirm you get the same token)

I didn't think I could call acquireToken(OnBehalfOfParameters) because the user assertion I was using would have expired and I want to use the refresh token which I believe is stored in the cache?

When you say it's working in "some of my test scenarios", does that mean in some test scenarios it is failing just like in the deployed version? And in the deployed version is it also only failing in some scenarios or all of them?

Apologies this was a little vague. If I just call acquireToken(OnBehalfOfParameters) and then call acquireTokenSilently(SilentParameters) with the account I can get a token succesfully.

However if I call acquireToken(OnBehalfOfParameters), then acquireToken(ClientCredentialParameters) and then acquireTokenSilently(SilentParameters) using the account from the first OBO response I get the NPE.