// Copyright (c) Barry Dorrans. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.IO; using System.Linq; using System.Net; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.AspNetCore.Authentication.Certificate.Test { public class ClientCertificateAuthenticationTests { [Fact] public async Task VerifySchemeDefaults() { var services = new ServiceCollection(); services.AddAuthentication().AddCertificate(); var sp = services.BuildServiceProvider(); var schemeProvider = sp.GetRequiredService(); var scheme = await schemeProvider.GetSchemeAsync(CertificateAuthenticationDefaults.AuthenticationScheme); Assert.NotNull(scheme); Assert.Equal("CertificateAuthenticationHandler", scheme.HandlerType.Name); Assert.Null(scheme.DisplayName); } [Fact] public void VerifyIsSelfSignedExtensionMethod() { Assert.True(Certificates.SelfSignedValidWithNoEku.IsSelfSigned()); } [Fact] public async Task VerifyValidSelfSignedWithClientEkuAuthenticates() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, Events = sucessfulValidationEvents }, Certificates.SelfSignedValidWithClientEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task VerifyValidSelfSignedWithNoEkuAuthenticates() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, Events = sucessfulValidationEvents }, Certificates.SelfSignedValidWithNoEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task VerifyValidSelfSignedWithClientEkuFailsWhenSelfSignedCertsNotAllowed() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.Chained }, Certificates.SelfSignedValidWithClientEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task VerifyValidSelfSignedWithNoEkuFailsWhenSelfSignedCertsNotAllowed() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.Chained, Events = sucessfulValidationEvents }, Certificates.SelfSignedValidWithNoEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task VerifyValidSelfSignedWithServerFailsEvenIfSelfSignedCertsAreAllowed() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, Events = sucessfulValidationEvents }, Certificates.SelfSignedValidWithServerEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task VerifyValidSelfSignedWithServerPassesWhenSelfSignedCertsAreAllowedAndPurposeValidationIsOff() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, ValidateCertificateUse = false, Events = sucessfulValidationEvents }, Certificates.SelfSignedValidWithServerEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task VerifyValidSelfSignedWithServerFailsPurposeValidationIsOffButSelfSignedCertsAreNotAllowed() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.Chained, ValidateCertificateUse = false, Events = sucessfulValidationEvents }, Certificates.SelfSignedValidWithServerEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task VerifyExpiredSelfSignedFails() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, ValidateCertificateUse = false, Events = sucessfulValidationEvents }, Certificates.SelfSignedExpired); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task VerifyExpiredSelfSignedPassesIfDateRangeValidationIsDisabled() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, ValidateValidityPeriod = false, Events = sucessfulValidationEvents }, Certificates.SelfSignedExpired); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task VerifyNotYetValidSelfSignedFails() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, ValidateCertificateUse = false, Events = sucessfulValidationEvents }, Certificates.SelfSignedNotYetValid); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task VerifyNotYetValidSelfSignedPassesIfDateRangeValidationIsDisabled() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, ValidateValidityPeriod = false, Events = sucessfulValidationEvents }, Certificates.SelfSignedNotYetValid); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task VerifyFailingInTheValidationEventReturnsForbidden() { var server = CreateServer( new CertificateAuthenticationOptions { ValidateCertificateUse = false, Events = failedValidationEvents }, Certificates.SelfSignedValidWithServerEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task DoingNothingInTheValidationEventReturnsOK() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, ValidateCertificateUse = false, Events = unprocessedValidationEvents }, Certificates.SelfSignedValidWithServerEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task VerifyNotSendingACertificateEndsUpInForbidden() { var server = CreateServer( new CertificateAuthenticationOptions { Events = sucessfulValidationEvents }); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task VerifyHeaderIsUsedIfCertIsNotPresent() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, Events = sucessfulValidationEvents }, wireUpHeaderMiddleware : true); var client = server.CreateClient(); client.DefaultRequestHeaders.Add("X-Client-Cert", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData)); var response = await client.GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task VerifyHeaderEncodedCertFailsOnBadEncoding() { var server = CreateServer( new CertificateAuthenticationOptions { Events = sucessfulValidationEvents }, wireUpHeaderMiddleware: true); var client = server.CreateClient(); client.DefaultRequestHeaders.Add("X-Client-Cert", "OOPS" + Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData)); var response = await client.GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task VerifySettingTheAzureHeaderOnTheForwarderOptionsWorks() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, Events = sucessfulValidationEvents }, wireUpHeaderMiddleware: true, headerName: "X-ARR-ClientCert"); var client = server.CreateClient(); client.DefaultRequestHeaders.Add("X-ARR-ClientCert", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData)); var response = await client.GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task VerifyACustomHeaderFailsIfTheHeaderIsNotPresent() { var server = CreateServer( new CertificateAuthenticationOptions { Events = sucessfulValidationEvents }, wireUpHeaderMiddleware: true, headerName: "X-ARR-ClientCert"); var client = server.CreateClient(); client.DefaultRequestHeaders.Add("random-Weird-header", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData)); var response = await client.GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task VerifyNoEventWireupWithAValidCertificateCreatesADefaultUser() { var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned }, Certificates.SelfSignedValidWithNoEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); XElement responseAsXml = null; if (response.Content != null && response.Content.Headers.ContentType != null && response.Content.Headers.ContentType.MediaType == "text/xml") { var responseContent = await response.Content.ReadAsStringAsync(); responseAsXml = XElement.Parse(responseContent); } Assert.NotNull(responseAsXml); // There should always be an Issuer and a Thumbprint. var actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "issuer"); Assert.Single(actual); Assert.Equal(Certificates.SelfSignedValidWithNoEku.Issuer, actual.First().Value); actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Thumbprint); Assert.Single(actual); Assert.Equal(Certificates.SelfSignedValidWithNoEku.Thumbprint, actual.First().Value); // Now the optional ones if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.SubjectName.Name)) { actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.X500DistinguishedName); if (actual.Count() > 0) { Assert.Single(actual); Assert.Equal(Certificates.SelfSignedValidWithNoEku.SubjectName.Name, actual.First().Value); } } if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.SerialNumber)) { actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.SerialNumber); if (actual.Count() > 0) { Assert.Single(actual); Assert.Equal(Certificates.SelfSignedValidWithNoEku.SerialNumber, actual.First().Value); } } if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.DnsName, false))) { actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Dns); if (actual.Count() > 0) { Assert.Single(actual); Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.DnsName, false), actual.First().Value); } } if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.EmailName, false))) { actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Email); if (actual.Count() > 0) { Assert.Single(actual); Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.EmailName, false), actual.First().Value); } } if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.SimpleName, false))) { actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name); if (actual.Count() > 0) { Assert.Single(actual); Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.SimpleName, false), actual.First().Value); } } if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UpnName, false))) { actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Upn); if (actual.Count() > 0) { Assert.Single(actual); Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UpnName, false), actual.First().Value); } } if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UrlName, false))) { actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Uri); if (actual.Count() > 0) { Assert.Single(actual); Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UrlName, false), actual.First().Value); } } } [Fact] public async Task VerifyValidationEventPrincipalIsPropogated() { const string Expected = "John Doe"; var server = CreateServer( new CertificateAuthenticationOptions { AllowedCertificateTypes = CertificateTypes.SelfSigned, Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { // Make sure we get the validated principal Assert.NotNull(context.Principal); var claims = new[] { new Claim(ClaimTypes.Name, Expected, ClaimValueTypes.String, context.Options.ClaimsIssuer) }; context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); return Task.CompletedTask; } } }, Certificates.SelfSignedValidWithNoEku); var response = await server.CreateClient().GetAsync("https://example.com/"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); XElement responseAsXml = null; if (response.Content != null && response.Content.Headers.ContentType != null && response.Content.Headers.ContentType.MediaType == "text/xml") { var responseContent = await response.Content.ReadAsStringAsync(); responseAsXml = XElement.Parse(responseContent); } Assert.NotNull(responseAsXml); var actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name); Assert.Single(actual); Assert.Equal(Expected, actual.First().Value); Assert.Single(responseAsXml.Elements("claim")); } private static TestServer CreateServer( CertificateAuthenticationOptions configureOptions, X509Certificate2 clientCertificate = null, Func handler = null, Uri baseAddress = null, bool wireUpHeaderMiddleware = false, string headerName = "") { var builder = new WebHostBuilder() .Configure(app => { app.Use((context, next) => { if (clientCertificate != null) { context.Connection.ClientCertificate = clientCertificate; } return next(); }); if (wireUpHeaderMiddleware) { app.UseCertificateForwarding(); } app.UseAuthentication(); app.Use(async (context, next) => { var request = context.Request; var response = context.Response; var authenticationResult = await context.AuthenticateAsync(); if (authenticationResult.Succeeded) { response.StatusCode = (int)HttpStatusCode.OK; response.ContentType = "text/xml"; await response.WriteAsync(""); foreach (Claim claim in context.User.Claims) { await response.WriteAsync($"{claim.Value}"); } await response.WriteAsync(""); } else { await context.ChallengeAsync(); } }); }) .ConfigureServices(services => { if (configureOptions != null) { services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options => { options.AllowedCertificateTypes = configureOptions.AllowedCertificateTypes; options.Events = configureOptions.Events; options.ValidateCertificateUse = configureOptions.ValidateCertificateUse; options.RevocationFlag = options.RevocationFlag; options.RevocationMode = options.RevocationMode; options.ValidateValidityPeriod = configureOptions.ValidateValidityPeriod; }); } else { services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(); } if (wireUpHeaderMiddleware && !string.IsNullOrEmpty(headerName)) { services.AddCertificateForwarding(options => { options.CertificateHeader = headerName; }); } }); var server = new TestServer(builder) { BaseAddress = baseAddress }; return server; } private CertificateAuthenticationEvents sucessfulValidationEvents = new CertificateAuthenticationEvents() { OnCertificateValidated = context => { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) }; context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); return Task.CompletedTask; } }; private CertificateAuthenticationEvents failedValidationEvents = new CertificateAuthenticationEvents() { OnCertificateValidated = context => { context.Fail("Not validated"); return Task.CompletedTask; } }; private CertificateAuthenticationEvents unprocessedValidationEvents = new CertificateAuthenticationEvents() { OnCertificateValidated = context => { return Task.CompletedTask; } }; private static class Certificates { public static X509Certificate2 SelfSignedValidWithClientEku { get; private set; } = new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedClientEkuCertificate.cer")); public static X509Certificate2 SelfSignedValidWithNoEku { get; private set; } = new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedNoEkuCertificate.cer")); public static X509Certificate2 SelfSignedValidWithServerEku { get; private set; } = new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedServerEkuCertificate.cer")); public static X509Certificate2 SelfSignedNotYetValid { get; private set; } = new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateNotValidYet.cer")); public static X509Certificate2 SelfSignedExpired { get; private set; } = new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateExpired.cer")); private static string GetFullyQualifiedFilePath(string filename) { var filePath = Path.Combine(AppContext.BaseDirectory, filename); if (!File.Exists(filePath)) { throw new FileNotFoundException(filePath); } return filePath; } } } }