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);
}
}