MSAL - PublicClientApplication - GetAccountsAsync() doesn't return any Accounts
Asked Answered
A

3

5

I'm developing a little WPF-App that is supposed to query some data from the MS Graph API. I want to use SSO, so the user doesn't have to login to the app seperatly.

The app is run on a Azure AD joined device. The user is an AADC synchronized AD user. The AAD tenant is federated with ADFS. The user authenticates with Hello for Business (PIN) or via Password. The resulting problem is the same.

I can confirm that the user got a PRT via:

dsregcmd /status

AzureAdPrt: YES

In case it matters: The app registration in Azure AD is set to "Treat application as public client". And the following redirect URIs are configured:

Based on the examples I found, I'm using the following code to try to get an access token. However the GetAccountsAsync() method doesn't return any users nor does it throw any error or exception.

Can anyone tell me, what I'm missing here?

Any help would be much appreciated!

PS: When I try this using "Interactive Authentication" it works fine.

public GraphAuthProvider(string appId, string tenantId, string[] scopes)
{
    _scopes = scopes;

    try
    {
        _msalClient = PublicClientApplicationBuilder
                .Create(appId)
                .WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
                .WithTenantId(tenantId)
                .Build();
    }
    catch (Exception exception)
    {
        _log.Error(exception.Message);
        _log.Error(exception.StackTrace);
        throw;
    }
}

public async Task<string> GetAccessToken()
{
    _log.Info("Starting 'GetAccessToken'...");
    var accounts = await _msalClient.GetAccountsAsync();
    _userAccount = accounts.FirstOrDefault();


    // If there is no saved user account, the user must sign-in
    if (_userAccount == null)
    {
        _log.Info("No cached accounts found. Trying integrated authentication...");
        [...]
    }
    else
    {
        // If there is an account, call AcquireTokenSilent
        // By doing this, MSAL will refresh the token automatically if
        // it is expired. Otherwise it returns the cached token.

        var userAccountJson = await Task.Factory.StartNew(() => JsonConvert.SerializeObject(_userAccount));
        _log.Info($"Found cached accounts. _userAccount is: {userAccountJson}");

        var result = await _msalClient
            .AcquireTokenSilent(_scopes, _userAccount)
            .ExecuteAsync();

        return result.AccessToken;
    }
}
Anemo answered 5/6, 2020 at 8:1 Comment(0)
C
7

To be able to have IAccounts returned from MSAL (which access the cache), it must have the cache bootstrapped at some point. You are missing the starting point, which in your case is AcquireTokenInteractive.

It is recommended to use the following try/catch pattern on MSAL:

try
{
    var accounts = await _msalClient.GetAccountsAsync();

    // Try to acquire an access token from the cache. If an interaction is required, MsalUiRequiredException will be thrown.
    result = await _msalClient.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
                .ExecuteAsync();
}
catch (MsalUiRequiredException)
{
    // Acquiring an access token interactively. MSAL will cache it so you can use AcquireTokenSilent on future calls.
    result = await _msalClient.AcquireTokenInteractive(scopes)
                .ExecuteAsync();
}

Use this try/catch pattern instead of you if/else logic and you will be good to go.

For further reference, there is this msal desktop samples which covers a bunch of common scenarios.

Update

If you are instantiating a new _msalClient on every action, then this explains why the other calls are not working. You can either have _msalClient as a static/singleton instance or implement a serialized token cache. Here is a cache example

Caudal answered 5/6, 2020 at 18:31 Comment(6)
Thanks for your reply Tiago. Unfortunatly that is not helping. Even if I signed in interactively once, the later tries don't return any accounts. It just always want the interactive authentication.Anemo
If you are instantiating a new _msalClient on every action, then this explains why the other calls are not working. You can either have _msalClient as a static/singleton instance or implement a common token cache: github.com/Azure-Samples/…Caudal
Actually I was hoping that I could authenticate the user without any interaction on the users part at all. Since the user is on a Azure AD joined machine, that should be possible. For example, when I browse to portal.office.com, no authentication what so ever is required. The document: learn.microsoft.com/en-us/azure/active-directory/devices/… and especially this diagramm gives the impression, that it should be possibleAnemo
You might want to use Integrated Windows Authentication on MSAL then: learn.microsoft.com/en-us/azure/active-directory/develop/…Caudal
IWA only works when a DC is available. But I'm getting the impression, that it's not working the way I was hoping to. Anyways, caching the token works and it seems that the serialized token can be silently refreshed, when it's expired. So I'll go with that. Thank you for your support! Would you mind added the token cache comment to your answer, then I would mark it as accepted.Anemo
But if I give grant in AAD do I still need the interactive flow? The AcquireTokenSilent actually works even without interactive flow howver i need to pass an account. The account I can get only by the accountId which I cannot get for all logged in usersSheley
T
0

As there are some questions regarding non-interactive authentication in the comments, this is how I finally got this working:

Use WAM and configure the builder like this:

var builder = PublicClientApplicationBuilder.Create(ClientId)
            .WithAuthority($"{Instance}{TenantId}")
            .WithDefaultRedirectUri()
            .WithWindowsBroker(true);

Configure this redirect URI in the Azure App registration:

ms-appx-web://microsoft.aad.brokerplugin/{client_id}

A code example is available here

Teetotalism answered 13/1, 2022 at 14:10 Comment(0)
P
0

In-case anyone has a similar problem, I had an issue where both GetAccountAsync and GetAccountsAsync (the latter being now deprecated), were both sometimes returning null. All I needed to do was make sure all my authentication adjacent libraries were up to date.

I think there was an issue where the in-memory token caching wasn't always working as intended, which seems to be fixed by a simple update.

Plash answered 28/1, 2022 at 23:4 Comment(4)
Is there any link where it says GetAccountsAsync is deprecated? I am trying to generate refresh token for ConfidentialClientApplicationBuilder but getting null for accounts. I basically need a way to generate a refresh token(without interaction) which I can use in service to access web url.Casiano
To get a token without interaction I ended up using AcquireTokenForClientPlash
@Casiano Here you go. gist.github.com/seannybgoode/6d7a1fddd3597559b33d9a01a77a4539Plash
Thanks Scuba. I too ended up using acquireTokenForClient.Casiano

© 2022 - 2024 — McMap. All rights reserved.