diff --git a/libraries/Microsoft.Bot.Connector/Authentication/CertificateServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/CertificateServiceClientCredentialsFactory.cs new file mode 100644 index 0000000000..0b52de3b27 --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/CertificateServiceClientCredentialsFactory.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Rest; + +namespace Microsoft.Bot.Connector.Authentication +{ + /// + /// A Managed Identity implementation of the interface. + /// + public class CertificateServiceClientCredentialsFactory : ServiceClientCredentialsFactory + { + private readonly X509Certificate2 _certificate; + private readonly string _appId; + private readonly string _tenantId; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The certificate to use for authentication. + /// Microsoft application Id related to the certificate. + /// The oauth token tenant. + /// A custom httpClient to use. + /// A logger instance to use. + public CertificateServiceClientCredentialsFactory(X509Certificate2 certificate, string appId, string tenantId = null, HttpClient httpClient = null, ILogger logger = null) + : base() + { + if (string.IsNullOrWhiteSpace(appId)) + { + throw new ArgumentNullException(nameof(appId)); + } + + _certificate = certificate ?? throw new ArgumentNullException(nameof(certificate)); + _appId = appId; + _tenantId = tenantId; + _httpClient = httpClient; + _logger = logger; + } + + /// + public override Task IsValidAppIdAsync(string appId, CancellationToken cancellationToken) + { + return Task.FromResult(appId == _appId); + } + + /// + public override Task IsAuthenticationDisabledAsync(CancellationToken cancellationToken) + { + // Auth is always enabled for Certificate. + return Task.FromResult(false); + } + + /// + public override Task CreateCredentialsAsync( + string appId, string audience, string loginEndpoint, bool validateAuthority, CancellationToken cancellationToken) + { + if (appId != _appId) + { + throw new InvalidOperationException("Invalid Managed ID."); + } + + return Task.FromResult( + new CertificateAppCredentials(_certificate, _appId, _tenantId, _httpClient, _logger)); + } + } +} diff --git a/tests/Microsoft.Bot.Connector.Tests/Authentication/CertificateServiceClientCredentialsFactoryTests.cs b/tests/Microsoft.Bot.Connector.Tests/Authentication/CertificateServiceClientCredentialsFactoryTests.cs new file mode 100644 index 0000000000..e1bf5ed6bc --- /dev/null +++ b/tests/Microsoft.Bot.Connector.Tests/Authentication/CertificateServiceClientCredentialsFactoryTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Bot.Connector.Tests.Authentication +{ + public class CertificateServiceClientCredentialsFactoryTests + { + private const string TestAppId = nameof(TestAppId); + private const string TestTenantId = nameof(TestTenantId); + private const string TestAudience = nameof(TestAudience); + private readonly Mock logger = new Mock(); + private readonly Mock certificate = new Mock(); + + [Fact] + public void ConstructorTests() + { + _ = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId); + _ = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId, tenantId: TestTenantId); + _ = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId, logger: logger.Object); + _ = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId, httpClient: new HttpClient()); + } + + [Fact] + public void CannotCreateCredentialsFactoryWithoutCertificate() + { + Assert.Throws(() => new CertificateServiceClientCredentialsFactory(null, TestAppId)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void CannotCreateCredentialsFactoryWithoutAppId(string appId) + { + Assert.Throws(() => new CertificateServiceClientCredentialsFactory(certificate.Object, appId)); + } + + [Fact] + public void IsValidAppIdTest() + { + var factory = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId); + + Assert.True(factory.IsValidAppIdAsync(TestAppId, CancellationToken.None).GetAwaiter().GetResult()); + Assert.False(factory.IsValidAppIdAsync("InvalidAppId", CancellationToken.None).GetAwaiter().GetResult()); + } + + [Fact] + public void IsAuthenticationDisabledTest() + { + var factory = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId); + + Assert.False(factory.IsAuthenticationDisabledAsync(CancellationToken.None).GetAwaiter().GetResult()); + } + + [Fact] + public async void CanCreateCredentials() + { + var factory = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId); + + var credentials = await factory.CreateCredentialsAsync( + TestAppId, TestAudience, "https://login.microsoftonline.com", true, CancellationToken.None); + + Assert.NotNull(credentials); + Assert.IsType(credentials); + } + + [Fact] + public void CannotCreateCredentialsWithInvalidAppId() + { + var factory = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId); + + Assert.ThrowsAsync(() => factory.CreateCredentialsAsync( + "InvalidAppId", TestAudience, "https://login.microsoftonline.com", true, CancellationToken.None)); + } + } +}