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:
- Azure Active Directory with ASP.NET Core
- Configure TLS mutual authentication for Azure App Service
- Policy-based authorization in ASP.NET Core
- Use multiple JWT Bearer Authentication
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:
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 andX-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:
- Get the Client Certificate to be passed through the User Claims in my Product App Service app?
- Get App Service to pass the
X-ARR-ClientCert
header without mandating that a client cert be present? - Is there something I'm missing / a better way to do this?