Earlier this week I spent more time than I wanted for trying to figure out why I was losing the Thread.CurrentPrincipal value after having set it inside an async method. My history goes a lot like the one described here. But it took me sometime to find that website on Google, which was retrieving many results for web applications but very few for desktop. I also found similar problem described by this post.
In summary, I was working on authentication for a desktop application, using Open ID Connect and Identity Server. My calls to the user store are all based on WCF services, and I wanted to have the authentication done asynchronously to prevent my UI from locking up.
According to my first reference above, the Thread.CurrentPrincipal is associated with the current thread and the logical call context, and despite the async method executes in the same thread, any changes in the logical call context are discarded when the method completes.
At the end of the day, I “solved” it by not setting the Thread.CurrentPrincipal in the async method, but instead having the async method return the ClaimPrincipal and only set the Thread.CurrentPrincipal in the main thread, regular synchronous method. This sounds more like a workaround than a solutions, but in the Agile world I couldn’t spend endless time trying to make it work right. Maybe I will come back to this sometime in the future.
Here’s how I have it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
public class AuthenticationProvider { private readonly IMyService _service; /// <summary> /// Default constructor /// </summary> public AuthenticationProvider() { _service = new MyServiceProxy(); } /// <summary> /// Authenticate user using credentials and set the /// Thread.ClaimPrincipal. This method must be called from the /// UI main thread synchronously. /// <returns></returns> public bool TryAuthenticate(string username, string password) { ClaimsPrincipal p = GetClaimsPrincipal(username, password); if (p == null || !p.Claims.Any() || !p.Identity.IsAuthenticated) return false; Thread.CurrentPrincipal = p; return true; } /// <summary> /// Retrieve authenticated claims principal /// </summary> /// <returns>Null on invalid username/password, /// or authenicated claims principal otherwise</returns> public ClaimsPrincipal GetClaimsPrincipal(string username, string password) { var response = _service.Authenticate(username, password); if (response.HasErrors) return null; // the ClaimsPrincipal object is generated inside the service call, // but only associated with Thread.CurrentPrincipal in the method // caller return response.ClaimsPrincipal; } /// <summary> /// Retrieve authenticated claims principal asynchronously /// </summary> /// <returns>Null on invalid username/password, /// or authenicated claims principal otherwise</returns> public async Task<ClaimsPrincipal> GetClaimsPrincipalAsync(string username, string password) { return await Task.Run(() => GetClaimsPrincipal(username, password)); } } |
And in my client application, I have:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// this call locks the UI up until the service responds and execution completes private void LoginClick(object sender, RoutedEventArgs e) { var authProvider = new AuthenticationProvider(); var result = authProvider.TryAuthenticate(UsernameBox.Text, PasswordBox.Password); if (!result) { MessageBox.Show("Login failed"); return; } PrintClaims(); } // this call doesn't lock the UI up private async void LoginClick(object sender, RoutedEventArgs e) { var authProvider = new AuthenticationProvider(); var result = await authProvider.GetClaimsPrincipalAsync(UsernameBox.Text, PasswordBox.Password); if (!result.Claims.Any() || !result.Identity.IsAuthenticated) { MessageBox.Show("Login failed"); return; } // setting CurrentPrincipal here, on main thread Thread.CurrentPrincipal = result; PrintClaims(); } // print claims private void PrintClaims() { var claims = ClaimsPrincipal.Current.Identities.FirstOrDefault(); if (claims == null) { Console.WriteLine = "Null claims"; return; } IPrincipal currentPrincipal = Thread.CurrentPrincipal; Console.WriteLine(currentPrincipal.Identity.Name); Console.WriteLine(currentPrincipal.IsInRole("IT")); Console.WriteLine = string.Join(Environment.NewLine, claims.Claims); } |