MSAL - PublicClientApplication - GetAccountsAsync() doesn't return any Accounts
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:
- https://login.microsoftonline.com/common/oauth2/nativeclient
- https://login.live.com/oauth20_desktop.srf
- msalxxxxxxx(appId)://auth
- urn:ietf:wg:oauth:2.0:oob
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;
}
}
Solution 1:
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