Why am I getting a "Key not valid for use in specified state" exception when calling AcquireTokenInteractive() for Azure MFA in Winforms App?

Viewed 55

I have .Net Winforms application that I am add Azure AD MFA to. Through the Azure portal I've"

  • Added Users through AD
  • Set up associated application

I am able to log in successfully via Azure AD MFA on my local development PC. When attempting to run the log in sequence on another machine I get an exception/error:

Key not valid for use in specified state.

I am basing my implementation on some sample code which I found by googling around.

What is causing the problem? Is it the version of .Net on the offending PC? How do I go about identifying the issue and fixing it?

Here are the relative code snippets:

public static class AzureADAuthentication
{
    private static string ClientId = "XXX";
    private static string Tenant = "XXX";
    private static string Instance = "https://login.microsoftonline.com/";
    private static IPublicClientApplication _clientApp;
    private static bool _IsADAuthenticationEnabled;
    private static IAccount CurrentSignedInAccount = (IAccount) null;

    //Set the API Endpoint to Graph 'me' endpoint. 
    // To change from Microsoft public cloud to a national cloud, use another value of graphAPIEndpoint.
    // Reference with Graph endpoints here: https://docs.microsoft.com/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints
    private static string graphAPIEndpoint = "https://graph.microsoft.com/v1.0/me";

    //Set the scope for API call to user.read
    private static string[] scopes = new string[1]
    {
        "user.read"
    };

    public static bool IsADAuthenticationEnabled => AzureADAuthentication._IsADAuthenticationEnabled;
    public static IPublicClientApplication PublicClientApp => AzureADAuthentication._clientApp;

    public static void CreateClientApplication(bool isADAuthenticationEnabled, bool useWam)
    {
        AzureADAuthentication._IsADAuthenticationEnabled = isADAuthenticationEnabled;
        if (!AzureADAuthentication.IsADAuthenticationEnabled)
        {
            return;
        }

        PublicClientApplicationBuilder builder = PublicClientApplicationBuilder
                                                    .Create(AzureADAuthentication.ClientId)
                                                    .WithAuthority(AzureADAuthentication.Instance + AzureADAuthentication.Tenant)
                                                    .WithDefaultRedirectUri();
        if (useWam)
        {
            builder.WithWindowsBroker();
            AzureADAuthentication._clientApp = builder.Build();
            TokenCacheHelper.EnableSerialization(AzureADAuthentication._clientApp.UserTokenCache);
        }
    }

    public static async Task<string> GetHttpContentWithToken(string url, string token)
    {
        HttpClient httpClient = new HttpClient();
        try
        {
            HttpResponseMessage response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, url)
            {
                Headers = 
                {
                    Authorization = new AuthenticationHeaderValue("Bearer", token)
                }
            });
            string content = await response.Content.ReadAsStringAsync();
            return content;
        }
        catch (Exception ex)
        {
            return ex.ToString();
        }
    }

    public static async Task<bool> AuthenticateUser(string loginHint, Form parentForm, bool isWindowsUser = false)
    {
        AuthenticationResult authResult = (AuthenticationResult) null;
        IAccount firstAccount = (IAccount) null;
        IEnumerable<IAccount> accounts = await AzureADAuthentication._clientApp.GetAccountsAsync();
       
        if (accounts.Any<IAccount>((Func<IAccount, bool>) (x => x.Username.Contains(loginHint))))
        {
            firstAccount = accounts.Where<IAccount>((Func<IAccount, bool>) (x => x.Username.Contains(loginHint))).FirstOrDefault<IAccount>();
            await AzureADAuthentication.PublicClientApp.RemoveAsync(firstAccount);
        }

        firstAccount = (IAccount) null;
        IAccount account = PublicClientApplication.OperatingSystemAccount;
        try
        {
            authResult = await AzureADAuthentication.PublicClientApp.AcquireTokenSilent((IEnumerable<string>) AzureADAuthentication.scopes, firstAccount).ExecuteAsync();
        }
        catch (MsalUiRequiredException ex1)
        {
            Debug.WriteLine("Azure MFA Login required => MsalUiRequiredException: " + ex1.Message);
            try
            {
                authResult = await PublicClientApp.AcquireTokenInteractive((IEnumerable<string>) scopes)
                                    .WithParentActivityOrWindow(parentForm.Handle)
                                    .WithPrompt(Prompt.NoPrompt)
                                    .WithLoginHint(loginHint)
                                    .ExecuteAsync();
            }
            catch (MsalException ex2)
            {
                // Log Exception
                return false;
            }
        }
        catch (Exception ex)
        {
            // Log Exception
            return false;
        }
        if (authResult == null)
        {
            return false;
        }

        AzureADAuthentication.CurrentSignedInAccount = authResult.Account;
        string tokeDetails = await AzureADAuthentication.GetHttpContentWithToken(AzureADAuthentication.graphAPIEndpoint, authResult.AccessToken);
        return true;
    }
}

static internal class TokenCacheHelper
{
    static TokenCacheHelper()
    {
        try
        {
            // For packaged desktop apps (MSIX packages, also called desktop bridge) the executing assembly folder is read-only. 
            // In that case we need to use Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path + "\msalcache.bin" 
            // which is a per-app read/write folder for packaged apps.
            // See https://docs.microsoft.com/windows/msix/desktop/desktop-to-uwp-behind-the-scenes
            //CacheFilePath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path, ".msalcache.bin3");

            // Fall back for an unpackaged desktop app
            CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3";
        }
        catch (Exception ex)
        {
            // log display error
            throw;
        }
    }

    public static string CacheFilePath { get; private set; }
    private static readonly object FileLock = new object();

    public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        lock (FileLock)
        {
            args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
                    ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
                                             null,
                                             DataProtectionScope.CurrentUser)
                    : null);
        }
    }

    public static void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        // if the access operation resulted in a cache update
        if (args.HasStateChanged)
        {
            lock (FileLock)
            {
                // reflect changes in the persistent store
                File.WriteAllBytes(CacheFilePath,
                                   ProtectedData.Protect(args.TokenCache.SerializeMsalV3(),
                                                         null,
                                                         DataProtectionScope.CurrentUser)
                                  );
            }
        }
    }

    internal static void EnableSerialization(ITokenCache tokenCache)
    {
        tokenCache.SetBeforeAccess(BeforeAccessNotification);
        tokenCache.SetAfterAccess(AfterAccessNotification);
    }
}

Here is the stack trace:

--STACK TRACE-- at System.Security.Cryptography.ProtectedData.Unprotect(Byte[] encryptedData, Byte[] optionalEntropy, DataProtectionScope scope)
at FooBar.Authentication.TokenCacheHelper.BeforeAccessNotification(TokenCacheNotificationArgs args) at Microsoft.Identity.Client.TokenCache.d__110.MoveNext()

--- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.Identity.Client.Cache.CacheSessionManager.d__17.MoveNext()

--- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at Microsoft.Identity.Client.Cache.CacheSessionManager.d__17.MoveNext()

--- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.Identity.Client.Cache.CacheSessionManager.d__16.MoveNext()

--- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.Identity.Client.ClientApplicationBase.d__25.MoveNext()

--- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.Identity.Client.ClientApplicationBase.d__18.MoveNext()

--- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.Identity.Client.ClientApplicationBase.d__17.MoveNext()

--- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() at FooBar.Authentication.AzureADAuthentication.d__14.MoveNext()

--- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() at FooBar.WindowsControls.Login3.d__28.MoveNext()

System.Security

Update: I've been able to verify that the exception fails on this call:

public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
    lock (FileLock)
    {
        args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
                ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
                                         null,
                                         DataProtectionScope.CurrentUser)
                : null);
    }
}
0 Answers
Related