Four Alternative Methods to Get User Identity and Claims in a .NET Azure Functions App

A comprehensive guide on how to obtain user identity and authentication claims in a .NET Core Azure Functions App



Photo by Alex Knight on Unsplash

Azure App Service provides built-in identity and authentication support, making user identification and authentication a hassle-free, low-effort task.

For ASP.NET 4.6 (and above) apps, ClaimsPrincipal.Current is populated with authenticated user’s identities and claims, enabling developers to follow the standard .NET code patterns. However, in .NET Core-based Azure Functions, ClaimsPrincipal.Current is not populated automatically and Claims must be obtained by different means.

This article will present four methods to access user identity and claims information in the .NET Core (C#) code.


Prerequisites

  1. An active Microsoft Azure subscription
  2. Azure Functions App

When App Service Authentication is enabled, every incoming HTTP request passes through the identity provider’s authentication and authorization module before being handled by the application code. The information regarding authenticated clients is available as a ClaimsPrincipal object and in special headers.

Hmm… How do I enable App Service Authentication?

ClaimsPrincipal as a Binding Parameter

The most straightforward method is to obtain the ClaimsPrincipal object from the Function’s binding parameters. Include the ClaimsPrincipal as an additional parameter in the function signature. The object will be automatically injected, similar to how ILogger is injected.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;

// DEMO: Get ClaimsPrincipal as a binding parameter
namespace ClaimsDemo.Function
{
    public static class ClaimsDemo
    {
    	// Pass ClaimsPrincipal parameter in the function signature
        [FunctionName("ClaimsDemo")]
        public static IActionResult Run
            ([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
             HttpRequest req, ILogger log, ClaimsPrincipal claimIdentity)
        {
            log.LogInformation("User ID: " + claimIdentity.Identity.Name);
            
            log.LogInformation("Claim Type : Claim Value");

            foreach (Claim claim in claimIdentity.Claims)
            {
                log.LogInformation(claim.Type + " : " + claim.Value + "\n");
            }

            return new OkObjectResult("Success");
        }
    }
}
ClaimsPrincipal: User identity and claims (Image by author)

ClaimsPrincipal from the Request Context

The ClaimsPrincipal object is also available as part of the request context and can be extracted from the HttpRequest.HttpContext.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;

// DEMO: Get ClaimsPrincipal from the request context
namespace ClaimsDemo.Function
{
    public static class ClaimsDemo
    {
        [FunctionName("ClaimsDemo")]
        public static IActionResult Run
            ([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
             HttpRequest req, ILogger log)
        {
            ClaimsPrincipal claimIdentity = req.HttpContext.User;

            log.LogInformation("User ID: " + claimIdentity.Identity.Name);
            
            log.LogInformation("Claim Type : Claim Value");

            foreach (Claim claim in claimIdentity.Claims)
            {
                log.LogInformation(claim.Type + " : " + claim.Value + "\n");
            }

            return new OkObjectResult("Success");
        }
    }
}

The advantage of the ClaimsPrincipal is the ease of referring and working with the individual claims presented by the identity allowing for a quick validation or design/decision logic implementation.

User Claims from the Request Headers

App Service passes user claims to the app by using special request headers. External requests aren’t allowed to set these headers, so they are present only if set by the App Service. Few example headers:

X-MS-CLIENT-PRINCIPAL-ID --User ID
X-MS-CLIENT-PRINCIPAL-NAME --User Name
X-MS-CLIENT-PRINCIPAL-IDP --Identity Provider's ID
X-MS-CLIENT-PRINCIPAL --Claims
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.Linq;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;

// DEMO: Get user Claims from the Request Headers
// Adopted from https://github.com/MaximRouiller/MaximeRouiller.Azure.AppService.EasyAuth
namespace ClaimsDemo.Function
{
    public static class ClaimsDemo
    {
        [FunctionName("ClaimsDemo")]
        public static IActionResult Run
        ([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
        HttpRequest req, ILogger log)
        {

            //Extract User ID and Claims from the request headers
            var principal_name = req.Headers["X-MS-CLIENT-PRINCIPAL-NAME"].FirstOrDefault();
            var principal_Id = req.Headers["X-MS-CLIENT-PRINCIPAL-ID"].FirstOrDefault();
            string easyAuthProvider = req.Headers["X-MS-CLIENT-PRINCIPAL-IDP"].FirstOrDefault();
            string clientPrincipalEncoded = req.Headers["X-MS-CLIENT-PRINCIPAL"].FirstOrDefault();

            log.LogInformation("User ID: " + principal_name);
            log.LogInformation("User Principal ID: " + principal_Id);
            log.LogInformation("EasyAuth Provider: " + easyAuthProvider);
            log.LogInformation("Encoded Client Principal: " + clientPrincipalEncoded);

            //Decode the Client Principal
            byte[] decodedBytes = Convert.FromBase64String(clientPrincipalEncoded);
            string clientPrincipalDecoded = System.Text.Encoding.Default.GetString(decodedBytes);
            // log.LogInformation("Decoded Client Principal: " + clientPrincipalDecoded);

            ClientPrincipal clientPrincipal = JsonConvert.DeserializeObject<ClientPrincipal>(clientPrincipalDecoded);
            IEnumerable<Claim> claims = clientPrincipal.Claims.Select(x => new Claim(x.Type, x.Value));

            log.LogInformation("Claim Type : Claim Value");

            foreach (Claim claim in claims)
            {
                log.LogInformation(claim.Type + " : " + claim.Value + "\n");
            }

            return new OkObjectResult("Success");
        }
    }

    public class ClientPrincipal
    {
        [JsonProperty("auth_typ")]
        public string AuthenticationType { get; set; }
        [JsonProperty("claims")]
        public IEnumerable<UserClaim> Claims { get; set; }
        [JsonProperty("name_typ")]
        public string NameType { get; set; }
        [JsonProperty("role_typ")]
        public string RoleType { get; set; }
    }

    public class UserClaim
    {
        [JsonProperty("typ")]
        public string Type { get; set; }
        [JsonProperty("val")]
        public string Value { get; set; }
    }
}
Claims from the Request Headers (Image by author)

User Claims from the Authentication Tokens

The authentication servers inject provider-specific tokens into the request headers with the authenticated user’s details and the claims. These tokens can also be used to get access to the user information and claims in the code.

Token headers are JSON web tokens (JWTs). JWTs are encrypted and must be decrypted before any reference.

In our demo code, we will obtain the claims from the Azure AD ID token header: X-MS-TOKEN-AAD-ID-TOKEN

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;

// DEMO: Get user Claims from the Authentication Tokens
namespace ClaimsDemo.Function
{
    public static class ClaimsDemo
    {
        [FunctionName("ClaimsDemo")]
        public static IActionResult Run
        ([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
        HttpRequest req, ILogger log)
        {

            //Extract AAD ID Token from the request headers
            var userIDToken = req.Headers["X-MS-TOKEN-AAD-ID-TOKEN"];

            log.LogInformation("Encrypted JWT: " + userIDToken);

            //Read and decrypt the JWT
            var jwttoken = new JwtSecurityTokenHandler().ReadJwtToken(userIDToken) as JwtSecurityToken;

            log.LogInformation("Decrypted JWT:");
            log.LogInformation("Claim Type : Claim Value");

            //Extract the claims from the JWT
            foreach (Claim claim in jwttoken.Claims)
            {
                log.LogInformation(claim.Type + " : " + claim.Value + "\n");
            }

            return new OkObjectResult("Success");
        }
    }
}
Claims from Authentication Tokens (Image by author)

What about token headers from other providers?

Want to peek inside your JWT without any code?

Pro Tip

If you encounter a token can’t be null error, ensure that the Token Store is set to On in the App’s Authentication/Authorization settings.

The token is null: Token Store must be enabled (Image by author)

3rd Party

Just for the sake of the completion of the conversation, there are some 3rd party open-source middleware components out there to make things easy for us.

Now we’re equipped with the apt tool for the job.

Conclusion

We explored different methods of obtaining user identity and claims in a .NET Core app. We demonstrated the ease, simplicity, and complexities of working with claims with demo programs.


Leave a comment