azure-functions-core-tools icon indicating copy to clipboard operation
azure-functions-core-tools copied to clipboard

Incorrect results from `ClaimsPrincipal` when running locally

Open aaronpowell opened this issue 2 years ago • 0 comments

We had https://github.com/Azure/static-web-apps-cli/issues/390 reported with the Static Web Apps CLI, in which using a ClaimsPrincipal in a C# Function doesn't contain the roles that you would expect and IsInRole returns false when it shouldn't, resulting in a different experience with auth locally compared to when it's deployed.

Context

The SWA CLI provides a mock of EasyAuth, in which you can simulate a flow through and then a token is generated for you. This token is then handled by the proxy in the CLI and serialized before being injected into the x-ms-client-principal header. The header can be parsed like so:

public static class StaticWebAppsAuth
{
  private class ClientPrincipal
  {
      public string IdentityProvider { get; set; }
      public string UserId { get; set; }
      public string UserDetails { get; set; }
      public IEnumerable<string> UserRoles { get; set; }
  }

  public static ClaimsPrincipal Parse(HttpRequest req)
  {
      var principal = new ClientPrincipal();

      if (req.Headers.TryGetValue("x-ms-client-principal", out var header))
      {
          var data = header[0];
          var decoded = Convert.FromBase64String(data);
          var json = Encoding.UTF8.GetString(decoded);
          principal = JsonSerializer.Deserialize<ClientPrincipal>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
      }

      principal.UserRoles = principal.UserRoles?.Except(new string[] { "anonymous" }, StringComparer.CurrentCultureIgnoreCase);

      if (!principal.UserRoles?.Any() ?? true)
      {
          return new ClaimsPrincipal();
      }

      var identity = new ClaimsIdentity(principal.IdentityProvider);
      identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, principal.UserId));
      identity.AddClaim(new Claim(ClaimTypes.Name, principal.UserDetails));
      identity.AddClaims(principal.UserRoles.Select(r => new Claim(ClaimTypes.Role, r)));

      return new ClaimsPrincipal(identity);
  }
}

Reference

But when deployed to Azure, the ClaimsPrincipal is automatically populated with the claims information from that header and you are not required to do manual parsing. You can see a sample app here https://github.com/aaronpowell/swa-blazor-cli-bug, which is deployed to https://thankful-desert-0d90ef310.1.azurestaticapps.net/secured (yeah, it's an ugly dumping string of claims).

Given that the x-ms-claims-principal header that is injected via the SWA CLI can be decoded it's expected that the ClaimsPrincipal would contain the same information.

Is it possible to inject a header that the Functions runtime (locally) will be able to create a ClaimsPrincipal correctly, or at least one that responds to the IsInRole call correctly? (so, mapping the roles from the header payload correctly)

Aside - I've tried to debug this myself and see where the problem is coming from but I'm unable to find docs on how to do end-to-end debugging of Azure Functions locally. I tried to load symbols from SourceLink but didn't managed to get the debugging working competely (I'm unable to hit the point where it creates the ClaimsPrincipal), and when my debugging chain consisted of SWA CLI -> My Functions App -> Core Tools -> WebJobs Script Host -> WebJobs SDK -> WebJobs SDK Extensions I gave up 🤣. I'm yet to find where the ClaimsPrincipal is created.

aaronpowell avatar Mar 16 '22 01:03 aaronpowell