Skip to content

Commit

Permalink
[#6628] port [#4449] CloudAdapter always builds Connector with Micros…
Browse files Browse the repository at this point in the history
…oftAppCredentials (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>
  • Loading branch information
sw-joelmut and ceciliaavila authored May 16, 2023
1 parent 68e0c42 commit 0cda584
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A Managed Identity implementation of the <see cref="ServiceClientCredentialsFactory"/> interface.
/// </summary>
public class CertificateServiceClientCredentialsFactory : ServiceClientCredentialsFactory
{
private readonly X509Certificate2 _certificate;
private readonly string _appId;
private readonly string _tenantId;
private readonly HttpClient _httpClient;
private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="CertificateServiceClientCredentialsFactory"/> class.
/// </summary>
/// <param name="certificate">The certificate to use for authentication.</param>
/// <param name="appId">Microsoft application Id related to the certificate.</param>
/// <param name="tenantId">The oauth token tenant.</param>
/// <param name="httpClient">A custom httpClient to use.</param>
/// <param name="logger">A logger instance to use.</param>
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;
}

/// <inheritdoc />
public override Task<bool> IsValidAppIdAsync(string appId, CancellationToken cancellationToken)
{
return Task.FromResult(appId == _appId);
}

/// <inheritdoc />
public override Task<bool> IsAuthenticationDisabledAsync(CancellationToken cancellationToken)
{
// Auth is always enabled for Certificate.
return Task.FromResult(false);
}

/// <inheritdoc />
public override Task<ServiceClientCredentials> CreateCredentialsAsync(
string appId, string audience, string loginEndpoint, bool validateAuthority, CancellationToken cancellationToken)
{
if (appId != _appId)
{
throw new InvalidOperationException("Invalid Managed ID.");
}

return Task.FromResult<ServiceClientCredentials>(
new CertificateAppCredentials(_certificate, _appId, _tenantId, _httpClient, _logger));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ILogger> logger = new Mock<ILogger>();
private readonly Mock<X509Certificate2> certificate = new Mock<X509Certificate2>();

[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<ArgumentNullException>(() => new CertificateServiceClientCredentialsFactory(null, TestAppId));
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void CannotCreateCredentialsFactoryWithoutAppId(string appId)
{
Assert.Throws<ArgumentNullException>(() => 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<CertificateAppCredentials>(credentials);
}

[Fact]
public void CannotCreateCredentialsWithInvalidAppId()
{
var factory = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId);

Assert.ThrowsAsync<InvalidOperationException>(() => factory.CreateCredentialsAsync(
"InvalidAppId", TestAudience, "https://login.microsoftonline.com", true, CancellationToken.None));
}
}
}

0 comments on commit 0cda584

Please sign in to comment.