1

I have an App Service Web API that I want to support both Azure Active Directory auth, as well as Client Certificate Auth.

I've followed these guides to get to where I am:

Here's the setup I have so far:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddAuthentication()
        .AddAzureADBearer(options => Configuration.Bind("AzureAd", options))
        .AddCertificate();

    services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
    {
        options.TokenValidationParameters.ValidAudiences = new[]
        {
            options.Audience,
        };
    });

    services
        .AddAuthorization(options =>
        {
            options.DefaultPolicy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .AddAuthenticationSchemes(
                    CertificateAuthenticationDefaults.AuthenticationScheme,
                    AzureADDefaults.JwtBearerAuthenticationScheme)
                .Build();
        });

    services.AddSingleton<IAuthorizationHandler, MyAuthorizationHandler>();

    services.AddControllers().AddControllersAsServices();
}

public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseAuthentication();
    app.UseAuthorization();
    ...

MyAuthorizationHandler.cs

public class MyAuthorizationHandler : IAuthorizationHandler
{
    private const string AppIdClaimType = "appid";
    private const string AppIdACRClaimType = "appidacr";

    private readonly HashSet<string> allowedCertificateSubjects;
    private readonly HashSet<string> allowedAadClients;

    private readonly IWebHostEnvironment env;
    private readonly IHttpContextAccessor httpContextAccessor;
    public MyAuthorizationHandler(
        IWebHostEnvironment env,
        IHttpContextAccessor httpContextAccessor,
        IUnityContainer unityContainer)
    {
        this.env = env;
        this.httpContextAccessor = httpContextAccessor;
        allowedCertificateSubjects = // Get from DI;
        allowedAadClients = // Get from DI;
    }

    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        bool isAuthorized = false;

        // Check for Certificate First
        string certificateSubjectName = null;
        if (env.IsDevelopment())
        {
            // Is Local environment, the cert is pasded through the Claims
            Claim subjectNameClaim = context.User.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Name);

            if (subjectNameClaim != null)
            {
                certificateSubjectName = subjectNameClaim.Value;
            }
        }
        else
        {
            // https://learn.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth
            // App Service by default captures the client certificate, and passes it through
            // in the Header X-ARR-ClientCert. We have to read it from there to verify.
            string certHeader = httpContextAccessor.HttpContext.Request.Headers["X-ARR-ClientCert"];
            if (!string.IsNullOrEmpty(certHeader))
            {
                try
                {
                    var certificate = new X509Certificate2(Convert.FromBase64String(certHeader));
                    certificateSubjectName = certificate.GetNameInfo(X509NameType.SimpleName, forIssuer: false);
                }
                catch (Exception)
                {
                    // If there is an error parsing the value (e.g. fake value passed in header),
                    // we should not error, but just ignore the header value.
                }
            }
        }

        // Validate Certificate
        if (allowedCertificateSubjects.Contains(certificateSubjectName, StringComparer.OrdinalIgnoreCase))
        {
            isAuthorized = true;
        }
        else
        {
            // If no cert found or not valid, check for AAD Bearer Token
            Claim authTypeClaim = context.User.Claims.FirstOrDefault(claim => claim.Type == AppIdACRClaimType);
            Claim claimAppId = context.User.Claims.FirstOrDefault(claim => claim.Type == AppIdClaimType);

            if (authTypeClaim != null && claimAppId != null)
            {
                // We only support Client/Secret and Cert AAD auth, not user auth.
                bool isValidAuthType = authTypeClaim.Value == "1" || authTypeClaim.Value == "2";
                bool isValidAppId = allowedAadClients.Contains(claimAppId.Value, StringComparer.OrdinalIgnoreCase);

                if (isValidAuthType && isValidAppId)
                {
                    isAuthorized = true;
                }
            }
        }

        if (!isAuthorized)
        {
            context.Fail();
        }

        return Task.CompletedTask;
    }
}

Application Settings has WEBSITE_LOAD_CERTIFICATES set to *

App Service Require Client Certificate setup:

enter image description here

I've excluded all paths from Require Incoming Certificate, since I want either Aad or Cert auth to be available.

Notes:

  • When running my API locally, the certificate is correctly picked up, and passed through the claims. When running in my App Service, it seems like the cert gets removed by App Service. That's why I have that if (env.IsDevelopment()) statement there to choose between Claims and X-ARR-ClientCert header.
  • When I exclude all paths in my "Incoming client certificates", the X-ARR-ClientCert header does not get passed. When I remove the exclusion, it passes the header correctly.

Is there any way for me to either:

  1. Get the Client Certificate to be passed through the User Claims in my Product App Service app?
  2. Get App Service to pass the X-ARR-ClientCert header without mandating that a client cert be present?
  3. Is there something I'm missing / a better way to do this?
justindao
  • 2,273
  • 4
  • 18
  • 34

1 Answers1

1

As you already found, Azure App Services won't set the X-ARR-ClientCert request header for excluded paths since (server-level) authentication has been disabled for them.

Disable Web app client certificates and attach and get the certificate from a custom header using options.CertificateHeader = "value".

justindao
  • 2,273
  • 4
  • 18
  • 34
AlfredoRevilla-MSFT
  • 3,171
  • 1
  • 12
  • 18