From 0cda5846d2eb5f8e958ee11309c6b08b6ee9f7ca Mon Sep 17 00:00:00 2001
From: Joel Mut <62260472+sw-joelmut@users.noreply.github.com>
Date: Tue, 16 May 2023 13:23:23 -0300
Subject: [PATCH] [#6628] port [#4449] CloudAdapter always builds Connector
with MicrosoftAppCredentials (never CertificateAppCredentials) -- certificate
auth flow broken (#6631)
* Add functionality to auth with a certificate
* Add unit tests
* Apply Visual Studio suggestion
Co-authored-by: Cecilia Avila <44245136+ceciliaavila@users.noreply.github.com>
* Fix indentation
---------
Co-authored-by: Cecilia Avila <44245136+ceciliaavila@users.noreply.github.com>
---
...tificateServiceClientCredentialsFactory.cs | 74 ++++++++++++++++
...ateServiceClientCredentialsFactoryTests.cs | 85 +++++++++++++++++++
2 files changed, 159 insertions(+)
create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/CertificateServiceClientCredentialsFactory.cs
create mode 100644 tests/Microsoft.Bot.Connector.Tests/Authentication/CertificateServiceClientCredentialsFactoryTests.cs
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));
+ }
+ }
+}