Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.

Commit 0cda584

Browse files
[#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>
1 parent 68e0c42 commit 0cda584

2 files changed

Lines changed: 159 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Net.Http;
6+
using System.Security.Cryptography.X509Certificates;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Rest;
11+
12+
namespace Microsoft.Bot.Connector.Authentication
13+
{
14+
/// <summary>
15+
/// A Managed Identity implementation of the <see cref="ServiceClientCredentialsFactory"/> interface.
16+
/// </summary>
17+
public class CertificateServiceClientCredentialsFactory : ServiceClientCredentialsFactory
18+
{
19+
private readonly X509Certificate2 _certificate;
20+
private readonly string _appId;
21+
private readonly string _tenantId;
22+
private readonly HttpClient _httpClient;
23+
private readonly ILogger _logger;
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="CertificateServiceClientCredentialsFactory"/> class.
27+
/// </summary>
28+
/// <param name="certificate">The certificate to use for authentication.</param>
29+
/// <param name="appId">Microsoft application Id related to the certificate.</param>
30+
/// <param name="tenantId">The oauth token tenant.</param>
31+
/// <param name="httpClient">A custom httpClient to use.</param>
32+
/// <param name="logger">A logger instance to use.</param>
33+
public CertificateServiceClientCredentialsFactory(X509Certificate2 certificate, string appId, string tenantId = null, HttpClient httpClient = null, ILogger logger = null)
34+
: base()
35+
{
36+
if (string.IsNullOrWhiteSpace(appId))
37+
{
38+
throw new ArgumentNullException(nameof(appId));
39+
}
40+
41+
_certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));
42+
_appId = appId;
43+
_tenantId = tenantId;
44+
_httpClient = httpClient;
45+
_logger = logger;
46+
}
47+
48+
/// <inheritdoc />
49+
public override Task<bool> IsValidAppIdAsync(string appId, CancellationToken cancellationToken)
50+
{
51+
return Task.FromResult(appId == _appId);
52+
}
53+
54+
/// <inheritdoc />
55+
public override Task<bool> IsAuthenticationDisabledAsync(CancellationToken cancellationToken)
56+
{
57+
// Auth is always enabled for Certificate.
58+
return Task.FromResult(false);
59+
}
60+
61+
/// <inheritdoc />
62+
public override Task<ServiceClientCredentials> CreateCredentialsAsync(
63+
string appId, string audience, string loginEndpoint, bool validateAuthority, CancellationToken cancellationToken)
64+
{
65+
if (appId != _appId)
66+
{
67+
throw new InvalidOperationException("Invalid Managed ID.");
68+
}
69+
70+
return Task.FromResult<ServiceClientCredentials>(
71+
new CertificateAppCredentials(_certificate, _appId, _tenantId, _httpClient, _logger));
72+
}
73+
}
74+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Net.Http;
6+
using System.Security.Cryptography.X509Certificates;
7+
using System.Threading;
8+
using Microsoft.Bot.Connector.Authentication;
9+
using Microsoft.Extensions.Logging;
10+
using Moq;
11+
using Xunit;
12+
13+
namespace Microsoft.Bot.Connector.Tests.Authentication
14+
{
15+
public class CertificateServiceClientCredentialsFactoryTests
16+
{
17+
private const string TestAppId = nameof(TestAppId);
18+
private const string TestTenantId = nameof(TestTenantId);
19+
private const string TestAudience = nameof(TestAudience);
20+
private readonly Mock<ILogger> logger = new Mock<ILogger>();
21+
private readonly Mock<X509Certificate2> certificate = new Mock<X509Certificate2>();
22+
23+
[Fact]
24+
public void ConstructorTests()
25+
{
26+
_ = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId);
27+
_ = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId, tenantId: TestTenantId);
28+
_ = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId, logger: logger.Object);
29+
_ = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId, httpClient: new HttpClient());
30+
}
31+
32+
[Fact]
33+
public void CannotCreateCredentialsFactoryWithoutCertificate()
34+
{
35+
Assert.Throws<ArgumentNullException>(() => new CertificateServiceClientCredentialsFactory(null, TestAppId));
36+
}
37+
38+
[Theory]
39+
[InlineData(null)]
40+
[InlineData("")]
41+
[InlineData(" ")]
42+
public void CannotCreateCredentialsFactoryWithoutAppId(string appId)
43+
{
44+
Assert.Throws<ArgumentNullException>(() => new CertificateServiceClientCredentialsFactory(certificate.Object, appId));
45+
}
46+
47+
[Fact]
48+
public void IsValidAppIdTest()
49+
{
50+
var factory = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId);
51+
52+
Assert.True(factory.IsValidAppIdAsync(TestAppId, CancellationToken.None).GetAwaiter().GetResult());
53+
Assert.False(factory.IsValidAppIdAsync("InvalidAppId", CancellationToken.None).GetAwaiter().GetResult());
54+
}
55+
56+
[Fact]
57+
public void IsAuthenticationDisabledTest()
58+
{
59+
var factory = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId);
60+
61+
Assert.False(factory.IsAuthenticationDisabledAsync(CancellationToken.None).GetAwaiter().GetResult());
62+
}
63+
64+
[Fact]
65+
public async void CanCreateCredentials()
66+
{
67+
var factory = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId);
68+
69+
var credentials = await factory.CreateCredentialsAsync(
70+
TestAppId, TestAudience, "https://login.microsoftonline.com", true, CancellationToken.None);
71+
72+
Assert.NotNull(credentials);
73+
Assert.IsType<CertificateAppCredentials>(credentials);
74+
}
75+
76+
[Fact]
77+
public void CannotCreateCredentialsWithInvalidAppId()
78+
{
79+
var factory = new CertificateServiceClientCredentialsFactory(certificate.Object, TestAppId);
80+
81+
Assert.ThrowsAsync<InvalidOperationException>(() => factory.CreateCredentialsAsync(
82+
"InvalidAppId", TestAudience, "https://login.microsoftonline.com", true, CancellationToken.None));
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)