From 2381ec7ed3a9e86309e2654c74008d81124d9cae Mon Sep 17 00:00:00 2001 From: carlosscastro Date: Wed, 16 Jun 2021 15:05:23 -0700 Subject: [PATCH 01/12] Managed service Identity: rough structure / classes (does not compile yet!) --- .../ManagedIdentityAppCredentials.cs | 55 ++++++++++++++++ .../ManagedIdentityAuthenticator.cs | 66 +++++++++++++++++++ .../Microsoft.Bot.Connector.csproj | 1 + 3 files changed, 122 insertions(+) create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs new file mode 100644 index 0000000000..9beb9cb51a --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Clients.ActiveDirectory; + +namespace Microsoft.Bot.Connector.Authentication +{ + /// + /// Managed Service Identity auth implementation. + /// + public class ManagedIdentityAppCredentials : AppCredentials + { + public ManagedIdentityAppCredentials() + : base(null, null, null) + { + } + + /// + /// Gets or sets the ManagedIdentity tenant id. + /// + /// + /// The ManagedIdentity tenant id. + /// + public string ManagedIdentityTenantId { get; set; } + + /// + /// Gets or sets the ManagedIdentity client id. + /// + /// + /// The ManagedIdentity client id. + /// + public string ManagedIdentityClientId { get; set; } + + /// + protected override Lazy BuildAuthenticator() + { + // Should not be called, legacy + throw new NotImplementedException(); + } + + /// + protected override Lazy BuildIAuthenticator() + { + // TODOS: constructor, test oauth scope for skills and channels, enable httpclient factory, logging, etc + return new Lazy( + () => new ManagedIdentityAuthenticator(ManagedIdentityTenantId, ManagedIdentityClientId, OAuthScope), + LazyThreadSafetyMode.ExecutionAndPublication); + } + } +} diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs new file mode 100644 index 0000000000..286ac26642 --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Services.AppAuthentication; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Clients.ActiveDirectory; + +namespace Microsoft.Bot.Connector.Authentication +{ + /// + /// Abstraction to acquire tokens from a Managed Service Identity. + /// + public class ManagedIdentityAuthenticator : IAuthenticator + { + private readonly AzureServiceTokenProvider _tokenProvider; + private readonly string _resource; + private readonly string _tenantId; + + /// + /// Initializes a new instance of the class. + /// + /// Tenant id for the managed identity to be used for acquiring tokens. + /// Client id for the managed identity to be used for acquiring tokens. + /// Resource for which to acquire the token. + public ManagedIdentityAuthenticator(string tenantId, string managedIdentityClientId, string resource) + { + if (string.IsNullOrEmpty(managedIdentityClientId)) + { + throw new ArgumentNullException(nameof(managedIdentityClientId)); + } + + if (string.IsNullOrEmpty(tenantId)) + { + throw new ArgumentNullException(nameof(tenantId)); + } + + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentNullException(nameof(resource)); + } + + //https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=dotnet + // "RunAs=App;AppId=" for user-assigned managed identities + // Production TODOS: does AzureServiceTokenProvider cache? how does this behave under load? + _tokenProvider = new AzureServiceTokenProvider($"RunAs=App;AppId={managedIdentityClientId}"); + _resource = resource; + _tenantId = tenantId; + } + + /// + public async Task GetTokenAsync(bool forceRefresh = false) + { + var authResult = await _tokenProvider.GetAuthenticationResultAsync(_resource, _tenantId, forceRefresh).ConfigureAwait(false); + + return new AuthenticatorResult() + { + AccessToken = authResult.AccessToken, + ExpiresOn = authResult.ExpiresOn + }; + } + } +} diff --git a/libraries/Microsoft.Bot.Connector/Microsoft.Bot.Connector.csproj b/libraries/Microsoft.Bot.Connector/Microsoft.Bot.Connector.csproj index 20edbbbe2e..5ff836a63a 100644 --- a/libraries/Microsoft.Bot.Connector/Microsoft.Bot.Connector.csproj +++ b/libraries/Microsoft.Bot.Connector/Microsoft.Bot.Connector.csproj @@ -26,6 +26,7 @@ + From 982b33c202f91bbeb429c504ecf1d84ba53ac911 Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Tue, 13 Jul 2021 12:41:20 -0700 Subject: [PATCH 02/12] Fix build break. --- .../Authentication/ManagedIdentityAppCredentials.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs index 9beb9cb51a..d99f95393d 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs @@ -15,6 +15,10 @@ namespace Microsoft.Bot.Connector.Authentication /// public class ManagedIdentityAppCredentials : AppCredentials { + /// + /// Initializes a new instance of the class. + /// Managed Identity for AAD credentials auth and caching. + /// public ManagedIdentityAppCredentials() : base(null, null, null) { From bc9bbf0217dc05e2be3c08adda8d6d84f50d019b Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Tue, 3 Aug 2021 13:29:23 -0700 Subject: [PATCH 03/12] Add support for MSI. --- .../DelegatingCredentialProvider.cs | 5 ++ .../Authentication/ICredentialProvider.cs | 6 ++ .../ManagedIdentityAppCredentials.cs | 41 ++++++------- .../ManagedIdentityAuthenticator.cs | 29 +++------- ...IdentityServiceClientCredentialsFactory.cs | 58 +++++++++++++++++++ .../MsalServiceClientCredentialsFactory.cs | 6 ++ ...ParameterizedBotFrameworkAuthentication.cs | 21 +++++++ .../PasswordServiceClientCredentialFactory.cs | 7 +++ .../ServiceClientCredentialsFactory.cs | 8 +++ .../SimpleCredentialProvider.cs | 6 ++ .../Authentication/SkillValidation.cs | 13 +++++ ...igurationServiceClientCredentialFactory.cs | 53 +++++++++++++++-- 12 files changed, 203 insertions(+), 50 deletions(-) create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs diff --git a/libraries/Microsoft.Bot.Connector/Authentication/DelegatingCredentialProvider.cs b/libraries/Microsoft.Bot.Connector/Authentication/DelegatingCredentialProvider.cs index ca8fec276e..9d41a4a87d 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/DelegatingCredentialProvider.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/DelegatingCredentialProvider.cs @@ -29,6 +29,11 @@ public Task IsAuthenticationDisabledAsync() return _credentialFactory.IsAuthenticationDisabledAsync(CancellationToken.None); } + public Task GetAuthTenantAsync() + { + return _credentialFactory.GetAuthTenantAsync(CancellationToken.None); + } + public Task IsValidAppIdAsync(string appId) { return _credentialFactory.IsValidAppIdAsync(appId, CancellationToken.None); diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ICredentialProvider.cs b/libraries/Microsoft.Bot.Connector/Authentication/ICredentialProvider.cs index 08e4775877..4928bd9eb3 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ICredentialProvider.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ICredentialProvider.cs @@ -54,5 +54,11 @@ public interface ICredentialProvider /// that may need to call out to serviced to validate the appId / password pair. /// Task IsAuthenticationDisabledAsync(); + + /// + /// Gets the Tenant ID of the Azure AD tenant where the bot is registered. + /// + /// The Tenant ID of the Azure AD tenant where the bot is registered. + Task GetAuthTenantAsync(); } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs index d99f95393d..d76f05d8d3 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs @@ -2,11 +2,7 @@ // Licensed under the MIT License. using System; -using System.Net.Http; using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Clients.ActiveDirectory; namespace Microsoft.Bot.Connector.Authentication { @@ -16,29 +12,28 @@ namespace Microsoft.Bot.Connector.Authentication public class ManagedIdentityAppCredentials : AppCredentials { /// - /// Initializes a new instance of the class. - /// Managed Identity for AAD credentials auth and caching. + /// The configuration property for Client ID of the Managed Identity. /// - public ManagedIdentityAppCredentials() - : base(null, null, null) - { - } + public const string ManagedIdKey = "ManagedId"; /// - /// Gets or sets the ManagedIdentity tenant id. + /// The configuration property for Tenant ID of the Azure AD tenant. /// - /// - /// The ManagedIdentity tenant id. - /// - public string ManagedIdentityTenantId { get; set; } + public const string TenantIdKey = "TenantId"; /// - /// Gets or sets the ManagedIdentity client id. + /// Initializes a new instance of the class. + /// Managed Identity for AAD credentials auth and caching. /// - /// - /// The ManagedIdentity client id. - /// - public string ManagedIdentityClientId { get; set; } + /// Client ID for the managed identity assigned to the bot. + /// Tenant ID of the Azure AD tenant where the bot is created. + /// The id of the resource that is being accessed by the bot. + public ManagedIdentityAppCredentials(string appId, string tenantId, string audience) + : base(null, null, null, audience) + { + MicrosoftAppId = appId ?? throw new ArgumentNullException(nameof(appId)); + AuthTenant = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); + } /// protected override Lazy BuildAuthenticator() @@ -50,9 +45,9 @@ protected override Lazy BuildAuthenticator() /// protected override Lazy BuildIAuthenticator() { - // TODOS: constructor, test oauth scope for skills and channels, enable httpclient factory, logging, etc - return new Lazy( - () => new ManagedIdentityAuthenticator(ManagedIdentityTenantId, ManagedIdentityClientId, OAuthScope), + // TODO: constructor, test oauth scope for skills and channels, enable httpclient factory, logging, etc + return new ( + () => new ManagedIdentityAuthenticator(MicrosoftAppId, OAuthScope), LazyThreadSafetyMode.ExecutionAndPublication); } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs index 286ac26642..bb130f6546 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs @@ -2,12 +2,8 @@ // Licensed under the MIT License. using System; -using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Services.AppAuthentication; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Clients.ActiveDirectory; namespace Microsoft.Bot.Connector.Authentication { @@ -18,24 +14,17 @@ public class ManagedIdentityAuthenticator : IAuthenticator { private readonly AzureServiceTokenProvider _tokenProvider; private readonly string _resource; - private readonly string _tenantId; /// /// Initializes a new instance of the class. /// - /// Tenant id for the managed identity to be used for acquiring tokens. - /// Client id for the managed identity to be used for acquiring tokens. + /// Client id for the managed identity to be used for acquiring tokens. /// Resource for which to acquire the token. - public ManagedIdentityAuthenticator(string tenantId, string managedIdentityClientId, string resource) + public ManagedIdentityAuthenticator(string appId, string resource) { - if (string.IsNullOrEmpty(managedIdentityClientId)) + if (string.IsNullOrEmpty(appId)) { - throw new ArgumentNullException(nameof(managedIdentityClientId)); - } - - if (string.IsNullOrEmpty(tenantId)) - { - throw new ArgumentNullException(nameof(tenantId)); + throw new ArgumentNullException(nameof(appId)); } if (string.IsNullOrEmpty(resource)) @@ -43,20 +32,18 @@ public ManagedIdentityAuthenticator(string tenantId, string managedIdentityClien throw new ArgumentNullException(nameof(resource)); } - //https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=dotnet + // https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=dotnet // "RunAs=App;AppId=" for user-assigned managed identities // Production TODOS: does AzureServiceTokenProvider cache? how does this behave under load? - _tokenProvider = new AzureServiceTokenProvider($"RunAs=App;AppId={managedIdentityClientId}"); + _tokenProvider = new AzureServiceTokenProvider($"RunAs=App;AppId={appId}"); _resource = resource; - _tenantId = tenantId; } /// public async Task GetTokenAsync(bool forceRefresh = false) { - var authResult = await _tokenProvider.GetAuthenticationResultAsync(_resource, _tenantId, forceRefresh).ConfigureAwait(false); - - return new AuthenticatorResult() + var authResult = await _tokenProvider.GetAuthenticationResultAsync(_resource, forceRefresh).ConfigureAwait(false); + return new AuthenticatorResult { AccessToken = authResult.AccessToken, ExpiresOn = authResult.ExpiresOn diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs new file mode 100644 index 0000000000..ea94ddbe3d --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Rest; + +namespace Microsoft.Bot.Connector.Authentication +{ + /// + /// A Managed Identity implementation of the interface. + /// + public class ManagedIdentityServiceClientCredentialsFactory : ServiceClientCredentialsFactory + { + private readonly string _appId; + private readonly string _tenantId; + + /// + /// Initializes a new instance of the class. + /// + /// Client ID for the managed identity assigned to the bot. + /// Tenant ID of the Azure AD tenant where the bot is created. + public ManagedIdentityServiceClientCredentialsFactory(string appId, string tenantId) + { + _appId = appId ?? throw new ArgumentNullException(nameof(appId)); + _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); + } + + /// + public override Task IsValidAppIdAsync(string appId, CancellationToken cancellationToken) + { + return Task.FromResult(appId == _appId); + } + + /// + public override Task IsAuthenticationDisabledAsync(CancellationToken cancellationToken) + { + // Auth is always enabled for MSI + 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 ManagedIdentityAppCredentials(_appId, _tenantId, audience)); + } + + /// + public override Task GetAuthTenantAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_tenantId); + } + } +} diff --git a/libraries/Microsoft.Bot.Connector/Authentication/MsalServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/MsalServiceClientCredentialsFactory.cs index 986917c0ea..24ae660b61 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/MsalServiceClientCredentialsFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/MsalServiceClientCredentialsFactory.cs @@ -84,6 +84,12 @@ public override Task CreateCredentialsAsync(string app new MsalAppCredentials(_clientApplication, appId, authority: loginEndpoint, scope: audience, validateAuthority: validateAuthority, logger: _logger)); } + /// + public override Task GetAuthTenantAsync(CancellationToken cancellationToken) + { + return Task.FromResult(string.Empty); + } + /// public override Task IsAuthenticationDisabledAsync(CancellationToken cancellationToken) { diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs b/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs index 29bdf9651f..c1327e4488 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs @@ -210,6 +210,19 @@ private async Task SkillValidation_AuthenticateChannelTokenAsync RequireSignedTokens = true }; + // Add tenant id from settings (if present) as a valid token issuer + var tenantId = await _credentialsFactory.GetAuthTenantAsync(CancellationToken.None).ConfigureAwait(false); + if (!string.IsNullOrWhiteSpace(tenantId)) + { + var validIssuers = tokenValidationParameters.ValidIssuers.ToList(); + validIssuers.AddRange(new[] + { + $"https://sts.windows.net/{tenantId}/", // MSI auth, 1.0 token + $"https://login.microsoftonline.com/{tenantId}/v2.0" // MSI auth, 2.0 token + }); + tokenValidationParameters.ValidIssuers = validIssuers; + } + // TODO: what should the openIdMetadataUrl be here? var tokenExtractor = new JwtTokenExtractor( _authHttpClient, @@ -290,6 +303,14 @@ private async Task EmulatorValidation_AuthenticateEmulatorTokenA RequireSignedTokens = true, }; + // Add tenant id from settings (if present) as a valid token issuer + var tenantId = await _credentialsFactory.GetAuthTenantAsync(CancellationToken.None).ConfigureAwait(false); + if (!string.IsNullOrWhiteSpace(tenantId)) + { + _ = toBotFromEmulatorTokenValidationParameters.ValidIssuers.Append($"https://sts.windows.net/{tenantId}/"); // MSI auth, 1.0 token + _ = toBotFromEmulatorTokenValidationParameters.ValidIssuers.Append($"https://login.microsoftonline.com/{tenantId}/v2.0"); // MSI auth, 2.0 token + } + var tokenExtractor = new JwtTokenExtractor( _authHttpClient, toBotFromEmulatorTokenValidationParameters, diff --git a/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs index 1ca095fe97..6c8563d1aa 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs @@ -112,6 +112,13 @@ public override Task CreateCredentialsAsync(string app } } + /// + public override Task GetAuthTenantAsync(CancellationToken cancellationToken) + { + // Tenant is not required for Password auth + return Task.FromResult(string.Empty); + } + private class PrivateCloudAppCredentials : MicrosoftAppCredentials { private readonly string _oauthEndpoint; diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs index 5930f63125..234b06e5c6 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Rest; @@ -50,5 +51,12 @@ public abstract class ServiceClientCredentialsFactory /// A cancellation token. /// A representing the result of the asynchronous operation. public abstract Task CreateCredentialsAsync(string appId, string audience, string loginEndpoint, bool validateAuthority, CancellationToken cancellationToken); + + /// + /// Gets the Tenant ID of the Azure AD tenant where the bot is registered. + /// + /// A cancellation token. + /// The Tenant ID of the Azure AD tenant where the bot is registered. + public abstract Task GetAuthTenantAsync(CancellationToken cancellationToken); } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/SimpleCredentialProvider.cs b/libraries/Microsoft.Bot.Connector/Authentication/SimpleCredentialProvider.cs index 0fd274fce3..b0a422c67c 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/SimpleCredentialProvider.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/SimpleCredentialProvider.cs @@ -82,5 +82,11 @@ public Task IsAuthenticationDisabledAsync() { return Task.FromResult(string.IsNullOrEmpty(AppId)); } + + /// + public Task GetAuthTenantAsync() + { + return Task.FromResult(string.Empty); + } } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs index 42127c5aae..e1d202f921 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs @@ -144,6 +144,19 @@ public static async Task AuthenticateChannelToken(string authHea GovernmentAuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl : AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl; + // Add tenant id from settings (if present) as a valid token issuer + var tenantId = await credentials.GetAuthTenantAsync().ConfigureAwait(false); + if (!string.IsNullOrWhiteSpace(tenantId)) + { + var validIssuers = _tokenValidationParameters.ValidIssuers.ToList(); + validIssuers.AddRange(new[] + { + $"https://sts.windows.net/{tenantId}/", // MSI auth, 1.0 token + $"https://login.microsoftonline.com/{tenantId}/v2.0" // MSI auth, 2.0 token + }); + _tokenValidationParameters.ValidIssuers = validIssuers; + } + var tokenExtractor = new JwtTokenExtractor( httpClient, _tokenValidationParameters, diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs index 01b9e12245..c01859ad26 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs @@ -2,9 +2,12 @@ // Licensed under the MIT License. using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Bot.Connector.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Rest; namespace Microsoft.Bot.Builder.Integration.AspNet.Core { @@ -17,8 +20,10 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core /// /// NOTE: if the keys are not present, a null value will be used. /// - public class ConfigurationServiceClientCredentialFactory : PasswordServiceClientCredentialFactory + public class ConfigurationServiceClientCredentialFactory : ServiceClientCredentialsFactory { + private readonly ServiceClientCredentialsFactory _inner; + /// /// Initializes a new instance of the class. /// @@ -26,12 +31,48 @@ public class ConfigurationServiceClientCredentialFactory : PasswordServiceClient /// A httpClient to use. /// A logger to use. public ConfigurationServiceClientCredentialFactory(IConfiguration configuration, HttpClient httpClient = null, ILogger logger = null) - : base( - configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value, - configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppPasswordKey)?.Value, - httpClient, - logger) { + string appId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value; + string password = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppPasswordKey)?.Value; + string managedId = configuration.GetSection(ManagedIdentityAppCredentials.ManagedIdKey)?.Value; + string tenantId = configuration.GetSection(ManagedIdentityAppCredentials.TenantIdKey)?.Value; + + if (!string.IsNullOrWhiteSpace(managedId) && !string.IsNullOrWhiteSpace(tenantId)) + { + // Both ManagedId and TenantId are present -- Use MSI auth + _inner = new ManagedIdentityServiceClientCredentialsFactory(managedId, tenantId); + } + else + { + // Default to Password Auth + _inner = new PasswordServiceClientCredentialFactory(appId, password, httpClient, logger); + } + } + + /// + public override Task IsValidAppIdAsync(string appId, CancellationToken cancellationToken) + { + return _inner.IsValidAppIdAsync(appId, cancellationToken); + } + + /// + public override Task IsAuthenticationDisabledAsync(CancellationToken cancellationToken) + { + return _inner.IsAuthenticationDisabledAsync(cancellationToken); + } + + /// + public override Task CreateCredentialsAsync( + string appId, string audience, string loginEndpoint, bool validateAuthority, CancellationToken cancellationToken) + { + return _inner.CreateCredentialsAsync( + appId, audience, loginEndpoint, validateAuthority, cancellationToken); + } + + /// + public override Task GetAuthTenantAsync(CancellationToken cancellationToken) + { + return _inner.GetAuthTenantAsync(cancellationToken); } } } From 1339460d5c6b468701af389601e1a3939717479d Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Thu, 12 Aug 2021 01:54:18 -0700 Subject: [PATCH 04/12] Add support for SingleTenant and remove tenant id injection from ICredentialProvider. --- .../AuthenticationConfiguration.cs | 9 +++ .../DelegatingCredentialProvider.cs | 5 -- .../Authentication/ICredentialProvider.cs | 6 -- .../ManagedIdentityAppCredentials.cs | 14 +--- ...IdentityServiceClientCredentialsFactory.cs | 13 +--- .../Authentication/MicrosoftAppCredentials.cs | 10 +++ .../MicrosoftGovernmentAppCredentials.cs | 16 ++++- .../MsalServiceClientCredentialsFactory.cs | 6 -- ...ParameterizedBotFrameworkAuthentication.cs | 21 +++--- .../PasswordServiceClientCredentialFactory.cs | 64 +++++++++++-------- .../ServiceClientCredentialsFactory.cs | 7 -- .../SimpleCredentialProvider.cs | 6 -- .../Authentication/SkillValidation.cs | 11 +--- ...igurationServiceClientCredentialFactory.cs | 42 +++++------- 14 files changed, 102 insertions(+), 128 deletions(-) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs index fe88ba77da..256cb1164c 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; namespace Microsoft.Bot.Connector.Authentication { @@ -31,5 +32,13 @@ public class AuthenticationConfiguration /// An instance used to validate the identity claims. /// public virtual ClaimsValidator ClaimsValidator { get; set; } = null; + + /// + /// Gets or sets a collection of valid JWT token issuers. + /// + /// + /// A collection of valid JWT token issuers. + /// + public IEnumerable ValidTokenIssuers { get; set; } } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/DelegatingCredentialProvider.cs b/libraries/Microsoft.Bot.Connector/Authentication/DelegatingCredentialProvider.cs index 9d41a4a87d..ca8fec276e 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/DelegatingCredentialProvider.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/DelegatingCredentialProvider.cs @@ -29,11 +29,6 @@ public Task IsAuthenticationDisabledAsync() return _credentialFactory.IsAuthenticationDisabledAsync(CancellationToken.None); } - public Task GetAuthTenantAsync() - { - return _credentialFactory.GetAuthTenantAsync(CancellationToken.None); - } - public Task IsValidAppIdAsync(string appId) { return _credentialFactory.IsValidAppIdAsync(appId, CancellationToken.None); diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ICredentialProvider.cs b/libraries/Microsoft.Bot.Connector/Authentication/ICredentialProvider.cs index 4928bd9eb3..08e4775877 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ICredentialProvider.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ICredentialProvider.cs @@ -54,11 +54,5 @@ public interface ICredentialProvider /// that may need to call out to serviced to validate the appId / password pair. /// Task IsAuthenticationDisabledAsync(); - - /// - /// Gets the Tenant ID of the Azure AD tenant where the bot is registered. - /// - /// The Tenant ID of the Azure AD tenant where the bot is registered. - Task GetAuthTenantAsync(); } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs index d76f05d8d3..2823b8652f 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs @@ -11,28 +11,16 @@ namespace Microsoft.Bot.Connector.Authentication /// public class ManagedIdentityAppCredentials : AppCredentials { - /// - /// The configuration property for Client ID of the Managed Identity. - /// - public const string ManagedIdKey = "ManagedId"; - - /// - /// The configuration property for Tenant ID of the Azure AD tenant. - /// - public const string TenantIdKey = "TenantId"; - /// /// Initializes a new instance of the class. /// Managed Identity for AAD credentials auth and caching. /// /// Client ID for the managed identity assigned to the bot. - /// Tenant ID of the Azure AD tenant where the bot is created. /// The id of the resource that is being accessed by the bot. - public ManagedIdentityAppCredentials(string appId, string tenantId, string audience) + public ManagedIdentityAppCredentials(string appId, string audience) : base(null, null, null, audience) { MicrosoftAppId = appId ?? throw new ArgumentNullException(nameof(appId)); - AuthTenant = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); } /// diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs index ea94ddbe3d..fa166978a4 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs @@ -11,17 +11,14 @@ namespace Microsoft.Bot.Connector.Authentication public class ManagedIdentityServiceClientCredentialsFactory : ServiceClientCredentialsFactory { private readonly string _appId; - private readonly string _tenantId; /// /// Initializes a new instance of the class. /// /// Client ID for the managed identity assigned to the bot. - /// Tenant ID of the Azure AD tenant where the bot is created. - public ManagedIdentityServiceClientCredentialsFactory(string appId, string tenantId) + public ManagedIdentityServiceClientCredentialsFactory(string appId) { _appId = appId ?? throw new ArgumentNullException(nameof(appId)); - _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); } /// @@ -46,13 +43,7 @@ public override Task CreateCredentialsAsync( throw new InvalidOperationException("Invalid Managed ID."); } - return Task.FromResult(new ManagedIdentityAppCredentials(_appId, _tenantId, audience)); - } - - /// - public override Task GetAuthTenantAsync(CancellationToken cancellationToken) - { - return Task.FromResult(_tenantId); + return Task.FromResult(new ManagedIdentityAppCredentials(_appId, audience)); } } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftAppCredentials.cs index 9a7c92ffff..93782590de 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftAppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftAppCredentials.cs @@ -21,6 +21,11 @@ namespace Microsoft.Bot.Connector.Authentication /// public class MicrosoftAppCredentials : AppCredentials { + /// + /// The configuration property for the App type of the bot -- MultiTenant, SingleTenant, or, MSI. + /// + public const string MicrosoftAppTypeKey = "MicrosoftAppType"; + /// /// The configuration property for the Microsoft app Password. /// @@ -31,6 +36,11 @@ public class MicrosoftAppCredentials : AppCredentials /// public const string MicrosoftAppIdKey = "MicrosoftAppId"; + /// + /// The configuration property for Tenant ID of the Azure AD tenant. + /// + public const string MicrosoftAppTenantIdKey = "MicrosoftAppTenantId"; + /// /// An empty set of credentials. /// diff --git a/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftGovernmentAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftGovernmentAppCredentials.cs index d492cfc13a..7839abbc11 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftGovernmentAppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftGovernmentAppCredentials.cs @@ -48,7 +48,21 @@ public MicrosoftGovernmentAppCredentials(string appId, string password, HttpClie /// Optional to gather telemetry data while acquiring and managing credentials. /// The scope for the token (defaults to if null). public MicrosoftGovernmentAppCredentials(string appId, string password, HttpClient customHttpClient, ILogger logger, string oAuthScope = null) - : base(appId, password, customHttpClient, logger, oAuthScope ?? GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope) + : this(appId, password, tenantId: string.Empty, customHttpClient, logger, oAuthScope) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Microsoft app ID. + /// The Microsoft app password. + /// Tenant ID of the Azure AD tenant where the bot is created. + /// Optional to be used when acquiring tokens. + /// Optional to gather telemetry data while acquiring and managing credentials. + /// The scope for the token (defaults to if null). + public MicrosoftGovernmentAppCredentials(string appId, string password, string tenantId, HttpClient customHttpClient, ILogger logger, string oAuthScope = null) + : base(appId, password, tenantId, customHttpClient, logger, oAuthScope ?? GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope) { } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/MsalServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/MsalServiceClientCredentialsFactory.cs index 24ae660b61..986917c0ea 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/MsalServiceClientCredentialsFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/MsalServiceClientCredentialsFactory.cs @@ -84,12 +84,6 @@ public override Task CreateCredentialsAsync(string app new MsalAppCredentials(_clientApplication, appId, authority: loginEndpoint, scope: audience, validateAuthority: validateAuthority, logger: _logger)); } - /// - public override Task GetAuthTenantAsync(CancellationToken cancellationToken) - { - return Task.FromResult(string.Empty); - } - /// public override Task IsAuthenticationDisabledAsync(CancellationToken cancellationToken) { diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs b/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs index c1327e4488..715d3b192b 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs @@ -210,16 +210,11 @@ private async Task SkillValidation_AuthenticateChannelTokenAsync RequireSignedTokens = true }; - // Add tenant id from settings (if present) as a valid token issuer - var tenantId = await _credentialsFactory.GetAuthTenantAsync(CancellationToken.None).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(tenantId)) + // Add allowed token issuers from configuration (if present) + if (_authConfiguration.ValidTokenIssuers != null && _authConfiguration.ValidTokenIssuers.Any()) { var validIssuers = tokenValidationParameters.ValidIssuers.ToList(); - validIssuers.AddRange(new[] - { - $"https://sts.windows.net/{tenantId}/", // MSI auth, 1.0 token - $"https://login.microsoftonline.com/{tenantId}/v2.0" // MSI auth, 2.0 token - }); + validIssuers.AddRange(_authConfiguration.ValidTokenIssuers); tokenValidationParameters.ValidIssuers = validIssuers; } @@ -303,12 +298,12 @@ private async Task EmulatorValidation_AuthenticateEmulatorTokenA RequireSignedTokens = true, }; - // Add tenant id from settings (if present) as a valid token issuer - var tenantId = await _credentialsFactory.GetAuthTenantAsync(CancellationToken.None).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(tenantId)) + // Add allowed token issuers from configuration (if present) + if (_authConfiguration.ValidTokenIssuers != null && _authConfiguration.ValidTokenIssuers.Any()) { - _ = toBotFromEmulatorTokenValidationParameters.ValidIssuers.Append($"https://sts.windows.net/{tenantId}/"); // MSI auth, 1.0 token - _ = toBotFromEmulatorTokenValidationParameters.ValidIssuers.Append($"https://login.microsoftonline.com/{tenantId}/v2.0"); // MSI auth, 2.0 token + var validIssuers = toBotFromEmulatorTokenValidationParameters.ValidIssuers.ToList(); + validIssuers.AddRange(_authConfiguration.ValidTokenIssuers); + toBotFromEmulatorTokenValidationParameters.ValidIssuers = validIssuers; } var tokenExtractor = new JwtTokenExtractor( diff --git a/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs index 6c8563d1aa..c871aed79e 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs @@ -35,9 +35,24 @@ public PasswordServiceClientCredentialFactory() /// A custom httpClient to use. /// A logger instance to use. public PasswordServiceClientCredentialFactory(string appId, string password, HttpClient httpClient, ILogger logger) + : this(appId, password, tenantId: string.Empty, httpClient, logger) + { + } + + /// + /// Initializes a new instance of the class. + /// with the provided credentials. + /// + /// The app ID. + /// The app password. + /// Tenant ID of the Azure AD tenant where the bot is created. + /// A custom httpClient to use. + /// A logger instance to use. + public PasswordServiceClientCredentialFactory(string appId, string password, string tenantId, HttpClient httpClient, ILogger logger) { AppId = appId; Password = password; + TenantId = tenantId; _httpClient = httpClient; _logger = logger; } @@ -58,6 +73,14 @@ public PasswordServiceClientCredentialFactory(string appId, string password, Htt /// public string Password { get; set; } + /// + /// Gets the Tenant ID of the Azure AD tenant where the bot is created. + /// + /// + /// The Tenant ID of the Azure AD tenant where the bot is created. + /// + public string TenantId { get; } + /// public override Task IsValidAppIdAsync(string appId, CancellationToken cancellationToken) { @@ -85,47 +108,36 @@ public override Task CreateCredentialsAsync(string app if (loginEndpoint.StartsWith(AuthenticationConstants.ToChannelFromBotLoginUrlTemplate, StringComparison.OrdinalIgnoreCase)) { - return Task.FromResult( - AppId == null - ? - MicrosoftAppCredentials.Empty - : - new MicrosoftAppCredentials(appId, Password, _httpClient, _logger, oauthScope)); + return Task.FromResult(string.IsNullOrWhiteSpace(TenantId) + ? new MicrosoftAppCredentials(appId, Password, _httpClient, _logger, oauthScope) + : new MicrosoftAppCredentials(appId, Password, TenantId, _httpClient, _logger, oauthScope)); } else if (loginEndpoint.Equals(GovernmentAuthenticationConstants.ToChannelFromBotLoginUrl, StringComparison.OrdinalIgnoreCase)) { - return Task.FromResult( - AppId == null - ? - MicrosoftGovernmentAppCredentials.Empty - : - new MicrosoftGovernmentAppCredentials(appId, Password, _httpClient, _logger, oauthScope)); + return Task.FromResult(string.IsNullOrWhiteSpace(TenantId) + ? new MicrosoftGovernmentAppCredentials(appId, Password, _httpClient, _logger, oauthScope) + : new MicrosoftGovernmentAppCredentials(appId, Password, TenantId, _httpClient, _logger, oauthScope)); } else { - return Task.FromResult( - AppId == null - ? - new PrivateCloudAppCredentials(null, null, null, null, null, loginEndpoint, validateAuthority) - : - new PrivateCloudAppCredentials(AppId, Password, _httpClient, _logger, oauthScope, loginEndpoint, validateAuthority)); + return Task.FromResult(string.IsNullOrWhiteSpace(TenantId) + ? new PrivateCloudAppCredentials(AppId, Password, _httpClient, _logger, oauthScope, loginEndpoint, validateAuthority) + : new PrivateCloudAppCredentials(AppId, Password, TenantId, _httpClient, _logger, oauthScope, loginEndpoint, validateAuthority)); } } - /// - public override Task GetAuthTenantAsync(CancellationToken cancellationToken) - { - // Tenant is not required for Password auth - return Task.FromResult(string.Empty); - } - private class PrivateCloudAppCredentials : MicrosoftAppCredentials { private readonly string _oauthEndpoint; private readonly bool _validateAuthority; public PrivateCloudAppCredentials(string appId, string password, HttpClient customHttpClient, ILogger logger, string oAuthScope, string oauthEndpoint, bool validateAuthority) - : base(appId, password, customHttpClient, logger, oAuthScope) + : this(appId, password, tenantId: string.Empty, customHttpClient, logger, oAuthScope, oauthEndpoint, validateAuthority) + { + } + + public PrivateCloudAppCredentials(string appId, string password, string tenantId, HttpClient customHttpClient, ILogger logger, string oAuthScope, string oauthEndpoint, bool validateAuthority) + : base(appId, password, tenantId, customHttpClient, logger, oAuthScope) { _oauthEndpoint = oauthEndpoint; _validateAuthority = validateAuthority; diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs index 234b06e5c6..8c9bec99b4 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs @@ -51,12 +51,5 @@ public abstract class ServiceClientCredentialsFactory /// A cancellation token. /// A representing the result of the asynchronous operation. public abstract Task CreateCredentialsAsync(string appId, string audience, string loginEndpoint, bool validateAuthority, CancellationToken cancellationToken); - - /// - /// Gets the Tenant ID of the Azure AD tenant where the bot is registered. - /// - /// A cancellation token. - /// The Tenant ID of the Azure AD tenant where the bot is registered. - public abstract Task GetAuthTenantAsync(CancellationToken cancellationToken); } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/SimpleCredentialProvider.cs b/libraries/Microsoft.Bot.Connector/Authentication/SimpleCredentialProvider.cs index b0a422c67c..0fd274fce3 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/SimpleCredentialProvider.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/SimpleCredentialProvider.cs @@ -82,11 +82,5 @@ public Task IsAuthenticationDisabledAsync() { return Task.FromResult(string.IsNullOrEmpty(AppId)); } - - /// - public Task GetAuthTenantAsync() - { - return Task.FromResult(string.Empty); - } } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs index e1d202f921..2cbff32a9b 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs @@ -144,16 +144,11 @@ public static async Task AuthenticateChannelToken(string authHea GovernmentAuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl : AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl; - // Add tenant id from settings (if present) as a valid token issuer - var tenantId = await credentials.GetAuthTenantAsync().ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(tenantId)) + // Add allowed token issuers from configuration (if present) + if (authConfig.ValidTokenIssuers != null && authConfig.ValidTokenIssuers.Any()) { var validIssuers = _tokenValidationParameters.ValidIssuers.ToList(); - validIssuers.AddRange(new[] - { - $"https://sts.windows.net/{tenantId}/", // MSI auth, 1.0 token - $"https://login.microsoftonline.com/{tenantId}/v2.0" // MSI auth, 2.0 token - }); + validIssuers.AddRange(authConfig.ValidTokenIssuers); _tokenValidationParameters.ValidIssuers = validIssuers; } diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs index c01859ad26..79903e26b3 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs @@ -12,14 +12,8 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core { /// - /// Credential provider which uses to lookup appId and password. + /// Credential provider which uses to lookup app credentials. /// - /// - /// This will populate the from an configuration entry with the key of - /// and the from a configuration entry with the key of . - /// - /// NOTE: if the keys are not present, a null value will be used. - /// public class ConfigurationServiceClientCredentialFactory : ServiceClientCredentialsFactory { private readonly ServiceClientCredentialsFactory _inner; @@ -32,21 +26,23 @@ public class ConfigurationServiceClientCredentialFactory : ServiceClientCredenti /// A logger to use. public ConfigurationServiceClientCredentialFactory(IConfiguration configuration, HttpClient httpClient = null, ILogger logger = null) { - string appId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value; - string password = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppPasswordKey)?.Value; - string managedId = configuration.GetSection(ManagedIdentityAppCredentials.ManagedIdKey)?.Value; - string tenantId = configuration.GetSection(ManagedIdentityAppCredentials.TenantIdKey)?.Value; + var appType = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppTypeKey)?.Value; + var appId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value; + var password = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppPasswordKey)?.Value; + var tenantId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value; - if (!string.IsNullOrWhiteSpace(managedId) && !string.IsNullOrWhiteSpace(tenantId)) - { - // Both ManagedId and TenantId are present -- Use MSI auth - _inner = new ManagedIdentityServiceClientCredentialsFactory(managedId, tenantId); - } - else + // TODO: Config Validations + // 1. AppType can only be one of 3 values (if present) -- If not specified, default is MultiTenant. + // 2. TenantId can be specified at anytime -- If specified, it will be added to allowed token issuers. + // 3. For MSI -- AppId is required, and, Password must not be specified. + // 4. For SingleTenant -- TenantId is required. + + _inner = appType switch { - // Default to Password Auth - _inner = new PasswordServiceClientCredentialFactory(appId, password, httpClient, logger); - } + "UserAssignedMSI" => new ManagedIdentityServiceClientCredentialsFactory(appId), + "SingleTenant" => new PasswordServiceClientCredentialFactory(appId, password, tenantId, httpClient, logger), + _ => new PasswordServiceClientCredentialFactory(appId, password, httpClient, logger) // MultiTenant + }; } /// @@ -68,11 +64,5 @@ public override Task CreateCredentialsAsync( return _inner.CreateCredentialsAsync( appId, audience, loginEndpoint, validateAuthority, cancellationToken); } - - /// - public override Task GetAuthTenantAsync(CancellationToken cancellationToken) - { - return _inner.GetAuthTenantAsync(cancellationToken); - } } } From ebd5a0a6fcc5836b19bed4a75d8d4cc175b39dee Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Thu, 12 Aug 2021 02:05:00 -0700 Subject: [PATCH 05/12] Add the bot tenant as a valid JWT token issuer. --- .../Extensions/ServiceCollectionExtensions.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime/Extensions/ServiceCollectionExtensions.cs b/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime/Extensions/ServiceCollectionExtensions.cs index 5b83370de2..6523b3d74e 100644 --- a/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime/Extensions/ServiceCollectionExtensions.cs +++ b/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime/Extensions/ServiceCollectionExtensions.cs @@ -114,6 +114,16 @@ public static void AddBotRuntime(this IServiceCollection services, IConfiguratio internal static void AddBotRuntimeSkills(this IServiceCollection services, IConfiguration configuration) { + // If TenantId is specified in config, add the tenant as a valid JWT token issuer for Bot to Skill conversation. + // The token issuer for MSI and single tenant scenarios will be the tenant where the bot is registered. + var validTokenIssuers = new List(); + var tenantId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value; + if (!string.IsNullOrWhiteSpace(tenantId)) + { + validTokenIssuers.Add($"https://sts.windows.net/{tenantId}/"); // For MSI auth, 1.0 token + validTokenIssuers.Add($"https://login.microsoftonline.com/{tenantId}/v2.0"); // For MSI auth, 2.0 token + } + // We only support being a skill or a skill consumer currently (not both). // See https://github.com/microsoft/botbuilder-dotnet/issues/5738 for feature request to allow both in the future. var skillSettings = configuration.GetSection(SkillSettings.SkillSettingsKey).Get(); @@ -122,13 +132,21 @@ internal static void AddBotRuntimeSkills(this IServiceCollection services, IConf { // If the config entry for SkillConfigurationEntry.SkillSettingsKey is present then we are a consumer // and the entries under SkillSettings.SkillSettingsKey are ignored - services.AddSingleton(sp => new AuthenticationConfiguration { ClaimsValidator = new AllowedSkillsClaimsValidator(settings.Select(x => x.MsAppId).ToList()) }); + services.AddSingleton(sp => new AuthenticationConfiguration + { + ClaimsValidator = new AllowedSkillsClaimsValidator(settings.Select(x => x.MsAppId).ToList()), + ValidTokenIssuers = validTokenIssuers + }); } else { // If the config entry for SkillSettings.SkillSettingsKey contains entries, then we are a skill // and we validate caller against this list - services.AddSingleton(sp => new AuthenticationConfiguration { ClaimsValidator = new AllowedCallersClaimsValidator(skillSettings?.AllowedCallers) }); + services.AddSingleton(sp => new AuthenticationConfiguration + { + ClaimsValidator = new AllowedCallersClaimsValidator(skillSettings?.AllowedCallers), + ValidTokenIssuers = validTokenIssuers + }); } services.TryAddSingleton(); From 22b2c1480ba6feb3801e79c8b2b8f57099393506 Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Thu, 12 Aug 2021 02:19:08 -0700 Subject: [PATCH 06/12] Revert change to ServiceClientCredentialsFactory. --- .../Authentication/ServiceClientCredentialsFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs index 8c9bec99b4..5930f63125 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ServiceClientCredentialsFactory.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Rest; From 38c4f61e969a2e7c89fddf96e3f76e58f22a7886 Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Thu, 12 Aug 2021 17:21:42 -0700 Subject: [PATCH 07/12] Add custom HttpClient and Logger to MSI authenticator. --- .../ManagedIdentityAppCredentials.cs | 18 +++++--- .../ManagedIdentityAuthenticator.cs | 45 +++++++++++++++---- ...IdentityServiceClientCredentialsFactory.cs | 13 +++++- ...igurationServiceClientCredentialFactory.cs | 2 +- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs index 2823b8652f..dcb297c82f 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System; +using System.Net.Http; using System.Threading; +using Microsoft.Extensions.Logging; namespace Microsoft.Bot.Connector.Authentication { @@ -16,10 +18,17 @@ public class ManagedIdentityAppCredentials : AppCredentials /// Managed Identity for AAD credentials auth and caching. /// /// Client ID for the managed identity assigned to the bot. - /// The id of the resource that is being accessed by the bot. - public ManagedIdentityAppCredentials(string appId, string audience) - : base(null, null, null, audience) + /// The scope for the token. + /// Optional to be used when acquiring tokens. + /// Optional to gather telemetry data while acquiring and managing credentials. + public ManagedIdentityAppCredentials(string appId, string oAuthScope, HttpClient customHttpClient = null, ILogger logger = null) + : base(channelAuthTenant: null, customHttpClient, logger, oAuthScope) { + if (oAuthScope == null) + { + throw new ArgumentNullException(nameof(oAuthScope)); + } + MicrosoftAppId = appId ?? throw new ArgumentNullException(nameof(appId)); } @@ -33,9 +42,8 @@ protected override Lazy BuildAuthenticator() /// protected override Lazy BuildIAuthenticator() { - // TODO: constructor, test oauth scope for skills and channels, enable httpclient factory, logging, etc return new ( - () => new ManagedIdentityAuthenticator(MicrosoftAppId, OAuthScope), + () => new ManagedIdentityAuthenticator(MicrosoftAppId, OAuthScope, CustomHttpClient, Logger), LazyThreadSafetyMode.ExecutionAndPublication); } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs index bb130f6546..7ec150e055 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs @@ -2,8 +2,12 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.Azure.Services.AppAuthentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Bot.Connector.Authentication { @@ -14,33 +18,49 @@ public class ManagedIdentityAuthenticator : IAuthenticator { private readonly AzureServiceTokenProvider _tokenProvider; private readonly string _resource; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// Client id for the managed identity to be used for acquiring tokens. /// Resource for which to acquire the token. - public ManagedIdentityAuthenticator(string appId, string resource) + /// A customized instance of the HttpClient class. + /// The type used to perform logging. + public ManagedIdentityAuthenticator(string appId, string resource, HttpClient customHttpClient = null, ILogger logger = null) { if (string.IsNullOrEmpty(appId)) { throw new ArgumentNullException(nameof(appId)); } - if (string.IsNullOrEmpty(resource)) - { - throw new ArgumentNullException(nameof(resource)); - } + _resource = resource ?? throw new ArgumentNullException(nameof(resource)); // https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=dotnet // "RunAs=App;AppId=" for user-assigned managed identities - // Production TODOS: does AzureServiceTokenProvider cache? how does this behave under load? - _tokenProvider = new AzureServiceTokenProvider($"RunAs=App;AppId={appId}"); - _resource = resource; + _tokenProvider = customHttpClient == null + ? new AzureServiceTokenProvider($"RunAs=App;AppId={appId}") + : new AzureServiceTokenProvider($"RunAs=App;AppId={appId}", httpClientFactory: new ConstantHttpClientFactory(customHttpClient)); + + _logger = logger ?? NullLogger.Instance; } /// public async Task GetTokenAsync(bool forceRefresh = false) + { + var watch = Stopwatch.StartNew(); + + var result = await Retry + .Run(() => AcquireTokenAsync(forceRefresh), HandleTokenProviderException) + .ConfigureAwait(false); + + watch.Stop(); + _logger.LogInformation($"GetTokenAsync: Acquired token using MSI in {watch.ElapsedMilliseconds}."); + + return result; + } + + private async Task AcquireTokenAsync(bool forceRefresh) { var authResult = await _tokenProvider.GetAuthenticationResultAsync(_resource, forceRefresh).ConfigureAwait(false); return new AuthenticatorResult @@ -49,5 +69,14 @@ public async Task GetTokenAsync(bool forceRefresh = false) ExpiresOn = authResult.ExpiresOn }; } + + private RetryParams HandleTokenProviderException(Exception e, int retryCount) + { + _logger.LogError(e, "Exception when trying to acquire token using MSI!"); + + return e is AzureServiceTokenProviderException // BadRequest + ? RetryParams.StopRetrying + : RetryParams.DefaultBackOff(retryCount); + } } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs index fa166978a4..6414000171 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs @@ -1,6 +1,8 @@ using System; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Microsoft.Rest; namespace Microsoft.Bot.Connector.Authentication @@ -11,14 +13,20 @@ namespace Microsoft.Bot.Connector.Authentication public class ManagedIdentityServiceClientCredentialsFactory : ServiceClientCredentialsFactory { private readonly string _appId; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// Client ID for the managed identity assigned to the bot. - public ManagedIdentityServiceClientCredentialsFactory(string appId) + /// A custom httpClient to use. + /// A logger instance to use. + public ManagedIdentityServiceClientCredentialsFactory(string appId, HttpClient httpClient = null, ILogger logger = null) { _appId = appId ?? throw new ArgumentNullException(nameof(appId)); + _httpClient = httpClient; + _logger = logger; } /// @@ -43,7 +51,8 @@ public override Task CreateCredentialsAsync( throw new InvalidOperationException("Invalid Managed ID."); } - return Task.FromResult(new ManagedIdentityAppCredentials(_appId, audience)); + return Task.FromResult( + new ManagedIdentityAppCredentials(_appId, audience, _httpClient, _logger)); } } } diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs index 79903e26b3..6a2d18a7b1 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs @@ -39,7 +39,7 @@ public ConfigurationServiceClientCredentialFactory(IConfiguration configuration, _inner = appType switch { - "UserAssignedMSI" => new ManagedIdentityServiceClientCredentialsFactory(appId), + "UserAssignedMSI" => new ManagedIdentityServiceClientCredentialsFactory(appId, httpClient, logger), "SingleTenant" => new PasswordServiceClientCredentialFactory(appId, password, tenantId, httpClient, logger), _ => new PasswordServiceClientCredentialFactory(appId, password, httpClient, logger) // MultiTenant }; From bee20af6ec376de02d46e8d7861edd7344357450 Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Thu, 12 Aug 2021 18:01:10 -0700 Subject: [PATCH 08/12] Config validations. --- ...igurationServiceClientCredentialFactory.cs | 75 ++++++++++++++++--- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs index 6a2d18a7b1..8f4128c48c 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,24 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core { + internal enum MicrosoftAppType + { + /// + /// MultiTenant app which uses botframework.com tenant to acquire tokens. + /// + MultiTenant, + + /// + /// SingleTenant app which uses the bot's host tenant to acquire tokens. + /// + SingleTenant, + + /// + /// App with a user assigned Managed Identity (MSI), which will be used as the AppId for token acquisition. + /// + UserAssignedMsi + } + /// /// Credential provider which uses to lookup app credentials. /// @@ -31,18 +50,54 @@ public ConfigurationServiceClientCredentialFactory(IConfiguration configuration, var password = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppPasswordKey)?.Value; var tenantId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value; - // TODO: Config Validations - // 1. AppType can only be one of 3 values (if present) -- If not specified, default is MultiTenant. - // 2. TenantId can be specified at anytime -- If specified, it will be added to allowed token issuers. - // 3. For MSI -- AppId is required, and, Password must not be specified. - // 4. For SingleTenant -- TenantId is required. + var parsedAppType = Enum.TryParse(appType, ignoreCase: true, out MicrosoftAppType parsed) + ? parsed + : MicrosoftAppType.MultiTenant; // default - _inner = appType switch + switch (parsedAppType) { - "UserAssignedMSI" => new ManagedIdentityServiceClientCredentialsFactory(appId, httpClient, logger), - "SingleTenant" => new PasswordServiceClientCredentialFactory(appId, password, tenantId, httpClient, logger), - _ => new PasswordServiceClientCredentialFactory(appId, password, httpClient, logger) // MultiTenant - }; + case MicrosoftAppType.UserAssignedMsi: + if (string.IsNullOrWhiteSpace(appId)) + { + throw new ArgumentException($"{MicrosoftAppCredentials.MicrosoftAppIdKey} is required for MSI in configuration."); + } + + if (string.IsNullOrWhiteSpace(tenantId)) + { + throw new ArgumentException($"{MicrosoftAppCredentials.MicrosoftAppTenantIdKey} is required for MSI in configuration."); + } + + if (!string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentException($"{MicrosoftAppCredentials.MicrosoftAppPasswordKey} must not be set for MSI in configuration."); + } + + _inner = new ManagedIdentityServiceClientCredentialsFactory(appId, httpClient, logger); + break; + + case MicrosoftAppType.SingleTenant: + if (string.IsNullOrWhiteSpace(appId)) + { + throw new ArgumentException($"{MicrosoftAppCredentials.MicrosoftAppIdKey} is required for SingleTenant in configuration."); + } + + if (string.IsNullOrWhiteSpace(tenantId)) + { + throw new ArgumentException($"{MicrosoftAppCredentials.MicrosoftAppTenantIdKey} is required for SingleTenant in configuration."); + } + + if (string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentException($"{MicrosoftAppCredentials.MicrosoftAppPasswordKey} is required for SingleTenant in configuration."); + } + + _inner = new PasswordServiceClientCredentialFactory(appId, password, tenantId, httpClient, logger); + break; + + default: // MultiTenant + _inner = new PasswordServiceClientCredentialFactory(appId, password, httpClient, logger); + break; + } } /// From a07adeec52819d1024a3a4359d9922a786575002 Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Thu, 19 Aug 2021 16:21:11 -0700 Subject: [PATCH 09/12] unit tests --- .../IJwtTokenProviderFactory.cs | 22 ++ .../Authentication/JwtTokenProviderFactory.cs | 29 ++ .../ManagedIdentityAppCredentials.cs | 15 +- .../ManagedIdentityAuthenticator.cs | 21 +- ...IdentityServiceClientCredentialsFactory.cs | 19 +- ...igurationServiceClientCredentialFactory.cs | 2 +- .../JwtTokenProviderFactoryTests.cs | 47 ++++ .../ManagedIdentityAppCredentialsTests.cs | 79 ++++++ .../ManagedIdentityAuthenticatorTests.cs | 182 ++++++++++++ ...ityServiceClientCredentialsFactoryTests.cs | 102 +++++++ ...tionServiceClientCredentialFactoryTests.cs | 259 ++++++++++++++++++ 11 files changed, 759 insertions(+), 18 deletions(-) create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/IJwtTokenProviderFactory.cs create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/JwtTokenProviderFactory.cs create mode 100644 tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenProviderFactoryTests.cs create mode 100644 tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityAppCredentialsTests.cs create mode 100644 tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityAuthenticatorTests.cs create mode 100644 tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityServiceClientCredentialsFactoryTests.cs create mode 100644 tests/integration/Microsoft.Bot.Builder.Integration.AspNet.Core.Tests/ConfigurationServiceClientCredentialFactoryTests.cs diff --git a/libraries/Microsoft.Bot.Connector/Authentication/IJwtTokenProviderFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/IJwtTokenProviderFactory.cs new file mode 100644 index 0000000000..0b3362692c --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/IJwtTokenProviderFactory.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Net.Http; +using Microsoft.Azure.Services.AppAuthentication; + +namespace Microsoft.Bot.Connector.Authentication +{ + /// + /// A factory that can create OAuth token providers for generating JWT auth tokens. + /// + public interface IJwtTokenProviderFactory + { + /// + /// Creates a new instance of the class. + /// + /// Client id for the managed identity to be used for acquiring tokens. + /// A customized instance of the HttpClient class. + /// A new instance of the class. + AzureServiceTokenProvider CreateAzureServiceTokenProvider(string appId, HttpClient customHttpClient = null); + } +} diff --git a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenProviderFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenProviderFactory.cs new file mode 100644 index 0000000000..9f348165ed --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenProviderFactory.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using Microsoft.Azure.Services.AppAuthentication; + +namespace Microsoft.Bot.Connector.Authentication +{ + /// + public class JwtTokenProviderFactory : IJwtTokenProviderFactory + { + /// + public AzureServiceTokenProvider CreateAzureServiceTokenProvider(string appId, HttpClient customHttpClient = null) + { + if (string.IsNullOrWhiteSpace(appId)) + { + throw new ArgumentNullException(nameof(appId)); + } + + // https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=dotnet + // "RunAs=App;AppId=" for user-assigned managed identities + var connectionString = $"RunAs=App;AppId={appId}"; + return customHttpClient == null + ? new AzureServiceTokenProvider(connectionString) + : new AzureServiceTokenProvider(connectionString, httpClientFactory: new ConstantHttpClientFactory(customHttpClient)); + } + } +} diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs index dcb297c82f..224914ec59 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs @@ -13,23 +13,28 @@ namespace Microsoft.Bot.Connector.Authentication /// public class ManagedIdentityAppCredentials : AppCredentials { + private readonly IJwtTokenProviderFactory _tokenProviderFactory; + /// /// Initializes a new instance of the class. /// Managed Identity for AAD credentials auth and caching. /// /// Client ID for the managed identity assigned to the bot. /// The scope for the token. + /// The JWT token provider factory to use. /// Optional to be used when acquiring tokens. /// Optional to gather telemetry data while acquiring and managing credentials. - public ManagedIdentityAppCredentials(string appId, string oAuthScope, HttpClient customHttpClient = null, ILogger logger = null) + public ManagedIdentityAppCredentials(string appId, string oAuthScope, IJwtTokenProviderFactory tokenProviderFactory, HttpClient customHttpClient = null, ILogger logger = null) : base(channelAuthTenant: null, customHttpClient, logger, oAuthScope) { - if (oAuthScope == null) + if (string.IsNullOrWhiteSpace(appId)) { - throw new ArgumentNullException(nameof(oAuthScope)); + throw new ArgumentNullException(nameof(appId)); } - MicrosoftAppId = appId ?? throw new ArgumentNullException(nameof(appId)); + _tokenProviderFactory = tokenProviderFactory ?? throw new ArgumentNullException(nameof(tokenProviderFactory)); + + MicrosoftAppId = appId; } /// @@ -43,7 +48,7 @@ protected override Lazy BuildAuthenticator() protected override Lazy BuildIAuthenticator() { return new ( - () => new ManagedIdentityAuthenticator(MicrosoftAppId, OAuthScope, CustomHttpClient, Logger), + () => new ManagedIdentityAuthenticator(MicrosoftAppId, OAuthScope, _tokenProviderFactory, CustomHttpClient, Logger), LazyThreadSafetyMode.ExecutionAndPublication); } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs index 7ec150e055..06ac34640c 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAuthenticator.cs @@ -25,23 +25,28 @@ public class ManagedIdentityAuthenticator : IAuthenticator /// /// Client id for the managed identity to be used for acquiring tokens. /// Resource for which to acquire the token. + /// The JWT token provider factory to use. /// A customized instance of the HttpClient class. /// The type used to perform logging. - public ManagedIdentityAuthenticator(string appId, string resource, HttpClient customHttpClient = null, ILogger logger = null) + public ManagedIdentityAuthenticator(string appId, string resource, IJwtTokenProviderFactory tokenProviderFactory, HttpClient customHttpClient = null, ILogger logger = null) { - if (string.IsNullOrEmpty(appId)) + if (string.IsNullOrWhiteSpace(appId)) { throw new ArgumentNullException(nameof(appId)); } - _resource = resource ?? throw new ArgumentNullException(nameof(resource)); + if (string.IsNullOrWhiteSpace(resource)) + { + throw new ArgumentNullException(nameof(resource)); + } - // https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=dotnet - // "RunAs=App;AppId=" for user-assigned managed identities - _tokenProvider = customHttpClient == null - ? new AzureServiceTokenProvider($"RunAs=App;AppId={appId}") - : new AzureServiceTokenProvider($"RunAs=App;AppId={appId}", httpClientFactory: new ConstantHttpClientFactory(customHttpClient)); + if (tokenProviderFactory == null) + { + throw new ArgumentNullException(nameof(tokenProviderFactory)); + } + _resource = resource; + _tokenProvider = tokenProviderFactory.CreateAzureServiceTokenProvider(appId, customHttpClient); _logger = logger ?? NullLogger.Instance; } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs index 6414000171..0cb18fd9ec 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityServiceClientCredentialsFactory.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -13,6 +16,7 @@ namespace Microsoft.Bot.Connector.Authentication public class ManagedIdentityServiceClientCredentialsFactory : ServiceClientCredentialsFactory { private readonly string _appId; + private readonly IJwtTokenProviderFactory _tokenProviderFactory; private readonly HttpClient _httpClient; private readonly ILogger _logger; @@ -20,11 +24,18 @@ public class ManagedIdentityServiceClientCredentialsFactory : ServiceClientCrede /// Initializes a new instance of the class. /// /// Client ID for the managed identity assigned to the bot. + /// The JWT token provider factory to use. /// A custom httpClient to use. /// A logger instance to use. - public ManagedIdentityServiceClientCredentialsFactory(string appId, HttpClient httpClient = null, ILogger logger = null) + public ManagedIdentityServiceClientCredentialsFactory(string appId, IJwtTokenProviderFactory tokenProviderFactory, HttpClient httpClient = null, ILogger logger = null) { - _appId = appId ?? throw new ArgumentNullException(nameof(appId)); + if (string.IsNullOrWhiteSpace(appId)) + { + throw new ArgumentNullException(nameof(appId)); + } + + _appId = appId; + _tokenProviderFactory = tokenProviderFactory ?? throw new ArgumentNullException(nameof(tokenProviderFactory)); _httpClient = httpClient; _logger = logger; } @@ -52,7 +63,7 @@ public override Task CreateCredentialsAsync( } return Task.FromResult( - new ManagedIdentityAppCredentials(_appId, audience, _httpClient, _logger)); + new ManagedIdentityAppCredentials(_appId, audience, _tokenProviderFactory, _httpClient, _logger)); } } } diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs index 8f4128c48c..a188aa4015 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs @@ -72,7 +72,7 @@ public ConfigurationServiceClientCredentialFactory(IConfiguration configuration, throw new ArgumentException($"{MicrosoftAppCredentials.MicrosoftAppPasswordKey} must not be set for MSI in configuration."); } - _inner = new ManagedIdentityServiceClientCredentialsFactory(appId, httpClient, logger); + _inner = new ManagedIdentityServiceClientCredentialsFactory(appId, new JwtTokenProviderFactory(), httpClient, logger); break; case MicrosoftAppType.SingleTenant: diff --git a/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenProviderFactoryTests.cs b/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenProviderFactoryTests.cs new file mode 100644 index 0000000000..b59ce06275 --- /dev/null +++ b/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenProviderFactoryTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using Microsoft.Bot.Connector.Authentication; +using Xunit; + +namespace Microsoft.Bot.Connector.Tests.Authentication +{ + public class JwtTokenProviderFactoryTests + { + private const string TestAppId = "foo"; + + [Fact] + public void CanCreateAzureServiceTokenProvider() + { + var sut = new JwtTokenProviderFactory(); + var tokenProvider = sut.CreateAzureServiceTokenProvider(TestAppId); + Assert.NotNull(tokenProvider); + } + + [Fact] + public void CanCreateAzureServiceTokenProviderWithCustomHttpClient() + { + using (var customHttpClient = new HttpClient()) + { + var sut = new JwtTokenProviderFactory(); + var tokenProvider = sut.CreateAzureServiceTokenProvider(TestAppId, customHttpClient); + Assert.NotNull(tokenProvider); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void CannotCreateAzureServiceTokenProviderWithoutAppId(string appId) + { + Assert.Throws(() => + { + var sut = new JwtTokenProviderFactory(); + _ = sut.CreateAzureServiceTokenProvider(appId); + }); + } + } +} diff --git a/tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityAppCredentialsTests.cs b/tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityAppCredentialsTests.cs new file mode 100644 index 0000000000..177866bdc0 --- /dev/null +++ b/tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityAppCredentialsTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Bot.Connector.Tests.Authentication +{ + public class ManagedIdentityAppCredentialsTests + { + private const string TestAppId = "foo"; + private const string TestAudience = "bar"; + + [Fact] + public void ConstructorTests() + { + var tokenProviderFactory = new Mock(); + + var sut1 = new ManagedIdentityAppCredentials(TestAppId, TestAudience, tokenProviderFactory.Object); + Assert.Equal(TestAppId, sut1.MicrosoftAppId); + Assert.Equal(TestAudience, sut1.OAuthScope); + + using (var customHttpClient = new HttpClient()) + { + var sut2 = new ManagedIdentityAppCredentials(TestAppId, TestAudience, tokenProviderFactory.Object, customHttpClient); + Assert.Equal(TestAppId, sut2.MicrosoftAppId); + Assert.Equal(TestAudience, sut2.OAuthScope); + + var logger = new Mock().Object; + var sut3 = new ManagedIdentityAppCredentials(TestAppId, TestAudience, tokenProviderFactory.Object, null, logger); + Assert.Equal(TestAppId, sut3.MicrosoftAppId); + Assert.Equal(TestAudience, sut3.OAuthScope); + + var sut4 = new ManagedIdentityAppCredentials(TestAppId, TestAudience, tokenProviderFactory.Object, customHttpClient, logger); + Assert.Equal(TestAppId, sut4.MicrosoftAppId); + Assert.Equal(TestAudience, sut4.OAuthScope); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void CanCreateCredentialsWithoutAudience(string audience) + { + var tokenProviderFactory = new Mock(); + + var sut = new ManagedIdentityAppCredentials(TestAppId, audience, tokenProviderFactory.Object); + Assert.Equal(TestAppId, sut.MicrosoftAppId); + Assert.Equal(AuthenticationConstants.ToChannelFromBotOAuthScope, sut.OAuthScope); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void CannotCreateCredentialsWithoutAppId(string appId) + { + Assert.Throws(() => + { + var tokenProviderFactory = new Mock(); + _ = new ManagedIdentityAppCredentials(appId, TestAudience, tokenProviderFactory.Object); + }); + } + + [Fact] + public void CannotCreateCredentialsWithoutTokenProviderFactory() + { + Assert.Throws(() => + { + _ = new ManagedIdentityAppCredentials(TestAppId, TestAudience, tokenProviderFactory: null); + }); + } + } +} diff --git a/tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityAuthenticatorTests.cs b/tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityAuthenticatorTests.cs new file mode 100644 index 0000000000..ec719e895d --- /dev/null +++ b/tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityAuthenticatorTests.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Services.AppAuthentication; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Bot.Connector.Tests.Authentication +{ + public class ManagedIdentityAuthenticatorTests + { + private const string TestAppId = "foo"; + private const string TestAudience = "bar"; + private const string TestConnectionString = "RunAs=App;AppId=foo"; + private const string TestAzureAdInstance = "https://login.microsoftonline.com/"; + + [Fact] + public void ConstructorTests() + { + var callsToCreateTokenProvider = 0; + + var tokenProvider = new Mock(TestConnectionString, TestAzureAdInstance); + + var tokenProviderFactory = new Mock(); + tokenProviderFactory + .Setup(f => f.CreateAzureServiceTokenProvider(It.IsAny(), It.IsAny())) + .Returns((appId, customHttpClient) => + { + callsToCreateTokenProvider++; + Assert.Equal(TestAppId, appId); + + return tokenProvider.Object; + }); + + _ = new ManagedIdentityAuthenticator(TestAppId, TestAudience, tokenProviderFactory.Object); + + using (var customHttpClient = new HttpClient()) + { + _ = new ManagedIdentityAuthenticator(TestAppId, TestAudience, tokenProviderFactory.Object, customHttpClient); + + var logger = new Mock(); + _ = new ManagedIdentityAuthenticator(TestAppId, TestAudience, tokenProviderFactory.Object, null, logger.Object); + + _ = new ManagedIdentityAuthenticator(TestAppId, TestAudience, tokenProviderFactory.Object, customHttpClient, logger.Object); + } + + Assert.Equal(4, callsToCreateTokenProvider); + } + + [Fact] + public void CanGetJwtToken() + { + var authResult = new AppAuthenticationResult(); + var tokenProvider = new Mock(TestConnectionString, TestAzureAdInstance); + tokenProvider + .Setup(p => p.GetAuthenticationResultAsync( + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((resource, forceRefresh, cancellationToken) => + { + Assert.False(forceRefresh); + return Task.FromResult(authResult); + }); + + var tokenProviderFactory = new Mock(); + tokenProviderFactory + .Setup(f => f.CreateAzureServiceTokenProvider(It.IsAny(), It.IsAny())) + .Returns((appId, customHttpClient) => tokenProvider.Object); + + var sut = new ManagedIdentityAuthenticator(TestAppId, TestAudience, tokenProviderFactory.Object); + var token = sut.GetTokenAsync().GetAwaiter().GetResult(); + + Assert.Equal(authResult.AccessToken, token.AccessToken); + Assert.Equal(authResult.ExpiresOn, token.ExpiresOn); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CanGetJwtTokenWithForceRefresh(bool forceRefreshInput) + { + var authResult = new AppAuthenticationResult(); + var tokenProvider = new Mock(TestConnectionString, TestAzureAdInstance); + tokenProvider + .Setup(p => p.GetAuthenticationResultAsync( + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((resource, forceRefresh, cancellationToken) => + { + Assert.Equal(forceRefreshInput, forceRefresh); + return Task.FromResult(authResult); + }); + + var tokenProviderFactory = new Mock(); + tokenProviderFactory + .Setup(f => f.CreateAzureServiceTokenProvider(It.IsAny(), It.IsAny())) + .Returns((appId, customHttpClient) => tokenProvider.Object); + + var sut = new ManagedIdentityAuthenticator(TestAppId, TestAudience, tokenProviderFactory.Object); + var token = sut.GetTokenAsync(forceRefreshInput).GetAwaiter().GetResult(); + + Assert.Equal(authResult.AccessToken, token.AccessToken); + Assert.Equal(authResult.ExpiresOn, token.ExpiresOn); + } + + [Fact] + public void DefaultRetryOnException() + { + var maxRetries = 10; + var callsToAcquireToken = 0; + + var tokenProvider = new Mock(TestConnectionString, TestAzureAdInstance); + tokenProvider + .Setup(p => p.GetAuthenticationResultAsync( + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((resource, forceRefresh, cancellationToken) => + { + callsToAcquireToken++; + throw new Exception(); + }); + + var tokenProviderFactory = new Mock(); + tokenProviderFactory + .Setup(f => f.CreateAzureServiceTokenProvider(It.IsAny(), It.IsAny())) + .Returns((appId, customHttpClient) => tokenProvider.Object); + + var sut = new ManagedIdentityAuthenticator(TestAppId, TestAudience, tokenProviderFactory.Object); + + try + { + _ = sut.GetTokenAsync().GetAwaiter().GetResult(); + } + catch (AggregateException e) + { + Assert.Equal(maxRetries + 1, e.InnerExceptions.Count); + } + finally + { + Assert.Equal(maxRetries + 1, callsToAcquireToken); + } + } + + [Fact] + public void CanRetryAndAcquireToken() + { + var callsToAcquireToken = 0; + + var authResult = new AppAuthenticationResult(); + var tokenProvider = new Mock(TestConnectionString, TestAzureAdInstance); + tokenProvider + .Setup(p => p.GetAuthenticationResultAsync( + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((resource, forceRefresh, cancellationToken) => + { + callsToAcquireToken++; + if (callsToAcquireToken == 1) + { + throw new Exception(); + } + + return Task.FromResult(authResult); + }); + + var tokenProviderFactory = new Mock(); + tokenProviderFactory + .Setup(f => f.CreateAzureServiceTokenProvider(It.IsAny(), It.IsAny())) + .Returns((appId, customHttpClient) => tokenProvider.Object); + + var sut = new ManagedIdentityAuthenticator(TestAppId, TestAudience, tokenProviderFactory.Object); + var token = sut.GetTokenAsync().GetAwaiter().GetResult(); + + Assert.Equal(authResult.AccessToken, token.AccessToken); + Assert.Equal(authResult.ExpiresOn, token.ExpiresOn); + + Assert.Equal(2, callsToAcquireToken); + } + } +} diff --git a/tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityServiceClientCredentialsFactoryTests.cs b/tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityServiceClientCredentialsFactoryTests.cs new file mode 100644 index 0000000000..e5b7140c0d --- /dev/null +++ b/tests/Microsoft.Bot.Connector.Tests/Authentication/ManagedIdentityServiceClientCredentialsFactoryTests.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Threading; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.Bot.Connector.Tests.Authentication +{ + public class ManagedIdentityServiceClientCredentialsFactoryTests + { + private const string TestAppId = "foo"; + private const string TestAudience = "bar"; + + [Fact] + public void ConstructorTests() + { + var tokenProviderFactory = new Mock(); + + _ = new ManagedIdentityServiceClientCredentialsFactory(TestAppId, tokenProviderFactory.Object); + + using (var customHttpClient = new HttpClient()) + { + _ = new ManagedIdentityServiceClientCredentialsFactory(TestAppId, tokenProviderFactory.Object, customHttpClient); + + var logger = new Mock(); + _ = new ManagedIdentityServiceClientCredentialsFactory(TestAppId, tokenProviderFactory.Object, null, logger.Object); + + _ = new ManagedIdentityServiceClientCredentialsFactory(TestAppId, tokenProviderFactory.Object, customHttpClient, logger.Object); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void CannotCreateCredentialsFactoryWithoutAppId(string appId) + { + Assert.Throws(() => + { + var tokenProviderFactory = new Mock(); + _ = new ManagedIdentityServiceClientCredentialsFactory(appId, tokenProviderFactory.Object); + }); + } + + [Fact] + public void CannotCreateCredentialsFactoryWithoutTokenProviderFactory() + { + Assert.Throws(() => + { + _ = new ManagedIdentityServiceClientCredentialsFactory(TestAppId, tokenProviderFactory: null); + }); + } + + [Fact] + public void IsValidAppIdTest() + { + var tokenProviderFactory = new Mock(); + var sut = new ManagedIdentityServiceClientCredentialsFactory(TestAppId, tokenProviderFactory.Object); + + Assert.True(sut.IsValidAppIdAsync(TestAppId, CancellationToken.None).GetAwaiter().GetResult()); + Assert.False(sut.IsValidAppIdAsync("InvalidAppId", CancellationToken.None).GetAwaiter().GetResult()); + } + + [Fact] + public void IsAuthenticationDisabledTest() + { + var tokenProviderFactory = new Mock(); + var sut = new ManagedIdentityServiceClientCredentialsFactory(TestAppId, tokenProviderFactory.Object); + + Assert.False(sut.IsAuthenticationDisabledAsync(CancellationToken.None).GetAwaiter().GetResult()); + } + + [Fact] + public void CanCreateCredentials() + { + var tokenProviderFactory = new Mock(); + var sut = new ManagedIdentityServiceClientCredentialsFactory(TestAppId, tokenProviderFactory.Object); + + var credentials = sut.CreateCredentialsAsync( + TestAppId, TestAudience, "https://login.microsoftonline.com", true, CancellationToken.None); + Assert.NotNull(credentials); + } + + [Fact] + public void CannotCreateCredentialsWithInvalidAppId() + { + var tokenProviderFactory = new Mock(); + var sut = new ManagedIdentityServiceClientCredentialsFactory(TestAppId, tokenProviderFactory.Object); + + Assert.Throws(() => + { + _ = sut.CreateCredentialsAsync( + "InvalidAppId", TestAudience, "https://login.microsoftonline.com", true, CancellationToken.None); + }); + } + } +} diff --git a/tests/integration/Microsoft.Bot.Builder.Integration.AspNet.Core.Tests/ConfigurationServiceClientCredentialFactoryTests.cs b/tests/integration/Microsoft.Bot.Builder.Integration.AspNet.Core.Tests/ConfigurationServiceClientCredentialFactoryTests.cs new file mode 100644 index 0000000000..2b440ff80e --- /dev/null +++ b/tests/integration/Microsoft.Bot.Builder.Integration.AspNet.Core.Tests/ConfigurationServiceClientCredentialFactoryTests.cs @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Xunit; + +namespace Microsoft.Bot.Builder.Integration.AspNet.Core.Tests +{ + public class ConfigurationServiceClientCredentialFactoryTests + { + private const string TestAppId = "foo"; + private const string TestAppPassword = "bar"; + private const string TestAppTenantId = "test"; + + [Fact] + public void CanCreateMultiTenantAppWithoutCredentials() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource()) + }); + + _ = new ConfigurationServiceClientCredentialFactory(config); + } + + [Fact] + public void CanCreateMultiTenantAppWithEmptyCredentials() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppIdKey, string.Empty }, + { MicrosoftAppCredentials.MicrosoftAppPasswordKey, string.Empty } + } + }) + }); + + _ = new ConfigurationServiceClientCredentialFactory(config); + } + + [Fact] + public void CanCreateMultiTenantAppWithCredentials() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppIdKey, TestAppId }, + { MicrosoftAppCredentials.MicrosoftAppPasswordKey, TestAppPassword } + } + }) + }); + + _ = new ConfigurationServiceClientCredentialFactory(config); + } + + [Fact] + public void CanCreateMultiTenantAppWithAppTypeAndTenantId() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppTypeKey, "MultiTenant" }, + { MicrosoftAppCredentials.MicrosoftAppIdKey, TestAppId }, + { MicrosoftAppCredentials.MicrosoftAppPasswordKey, TestAppPassword }, + { MicrosoftAppCredentials.MicrosoftAppTenantIdKey, TestAppTenantId } + } + }) + }); + + _ = new ConfigurationServiceClientCredentialFactory(config); + } + + [Fact] + public void CanCreateSingleTenantApp() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppTypeKey, "SingleTenant" }, + { MicrosoftAppCredentials.MicrosoftAppIdKey, TestAppId }, + { MicrosoftAppCredentials.MicrosoftAppPasswordKey, TestAppPassword }, + { MicrosoftAppCredentials.MicrosoftAppTenantIdKey, TestAppTenantId } + } + }) + }); + + _ = new ConfigurationServiceClientCredentialFactory(config); + } + + [Fact] + public void CannotCreateSingleTenantAppWithoutTenantId() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppTypeKey, "SingleTenant" }, + { MicrosoftAppCredentials.MicrosoftAppIdKey, TestAppId }, + { MicrosoftAppCredentials.MicrosoftAppPasswordKey, TestAppPassword } + } + }) + }); + + Assert.Throws(() => + { + _ = new ConfigurationServiceClientCredentialFactory(config); + }); + } + + [Fact] + public void CannotCreateSingleTenantAppWithoutAppId() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppTypeKey, "SingleTenant" }, + { MicrosoftAppCredentials.MicrosoftAppIdKey, string.Empty }, + { MicrosoftAppCredentials.MicrosoftAppPasswordKey, TestAppPassword }, + { MicrosoftAppCredentials.MicrosoftAppTenantIdKey, TestAppTenantId } + } + }) + }); + + Assert.Throws(() => + { + _ = new ConfigurationServiceClientCredentialFactory(config); + }); + } + + [Fact] + public void CannotCreateSingleTenantAppWithoutPassword() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppTypeKey, "SingleTenant" }, + { MicrosoftAppCredentials.MicrosoftAppIdKey, TestAppId }, + { MicrosoftAppCredentials.MicrosoftAppPasswordKey, string.Empty }, + { MicrosoftAppCredentials.MicrosoftAppTenantIdKey, TestAppTenantId } + } + }) + }); + + Assert.Throws(() => + { + _ = new ConfigurationServiceClientCredentialFactory(config); + }); + } + + [Fact] + public void CanCreateManagedIdentityApp() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppTypeKey, "UserAssignedMSI" }, + { MicrosoftAppCredentials.MicrosoftAppIdKey, TestAppId }, + { MicrosoftAppCredentials.MicrosoftAppTenantIdKey, TestAppTenantId } + } + }) + }); + + _ = new ConfigurationServiceClientCredentialFactory(config); + } + + [Fact] + public void CannotCreateManagedIdentityAppWithoutTenantId() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppTypeKey, "UserAssignedMSI" }, + { MicrosoftAppCredentials.MicrosoftAppIdKey, TestAppId } + } + }) + }); + + Assert.Throws(() => + { + _ = new ConfigurationServiceClientCredentialFactory(config); + }); + } + + [Fact] + public void CannotCreateManagedIdentityAppWithoutAppId() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppTypeKey, "UserAssignedMSI" }, + { MicrosoftAppCredentials.MicrosoftAppIdKey, string.Empty }, + { MicrosoftAppCredentials.MicrosoftAppTenantIdKey, TestAppTenantId } + } + }) + }); + + Assert.Throws(() => + { + _ = new ConfigurationServiceClientCredentialFactory(config); + }); + } + + [Fact] + public void CannotCreateManagedIdentityAppWithPassword() + { + var config = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + { MicrosoftAppCredentials.MicrosoftAppTypeKey, "UserAssignedMSI" }, + { MicrosoftAppCredentials.MicrosoftAppIdKey, TestAppId }, + { MicrosoftAppCredentials.MicrosoftAppPasswordKey, TestAppPassword }, + { MicrosoftAppCredentials.MicrosoftAppTenantIdKey, TestAppTenantId } + } + }) + }); + + Assert.Throws(() => + { + _ = new ConfigurationServiceClientCredentialFactory(config); + }); + } + } +} From b8ae0441397d02e607313e3645c0f9162c0dadb9 Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Thu, 19 Aug 2021 17:05:24 -0700 Subject: [PATCH 10/12] Fix build break. --- .../Authentication/ManagedIdentityAppCredentials.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs index 224914ec59..bab0c92879 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ManagedIdentityAppCredentials.cs @@ -47,7 +47,7 @@ protected override Lazy BuildAuthenticator() /// protected override Lazy BuildIAuthenticator() { - return new ( + return new Lazy( () => new ManagedIdentityAuthenticator(MicrosoftAppId, OAuthScope, _tokenProviderFactory, CustomHttpClient, Logger), LazyThreadSafetyMode.ExecutionAndPublication); } From 2e6ec05f4feb6267cee2ba41dc88d90b4bc6919d Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Thu, 19 Aug 2021 18:05:22 -0700 Subject: [PATCH 11/12] Addressed PR feedback. --- .../Extensions/ServiceCollectionExtensions.cs | 9 ++++-- .../Authentication/AuthenticationConstants.cs | 20 +++++++++++++ .../PasswordServiceClientCredentialFactory.cs | 28 ++++--------------- ...igurationServiceClientCredentialFactory.cs | 2 +- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime/Extensions/ServiceCollectionExtensions.cs b/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime/Extensions/ServiceCollectionExtensions.cs index 6523b3d74e..b0ec9fab9b 100644 --- a/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime/Extensions/ServiceCollectionExtensions.cs +++ b/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime/Extensions/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Runtime.Loader; using Microsoft.ApplicationInsights.Extensibility; @@ -120,8 +121,12 @@ internal static void AddBotRuntimeSkills(this IServiceCollection services, IConf var tenantId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value; if (!string.IsNullOrWhiteSpace(tenantId)) { - validTokenIssuers.Add($"https://sts.windows.net/{tenantId}/"); // For MSI auth, 1.0 token - validTokenIssuers.Add($"https://login.microsoftonline.com/{tenantId}/v2.0"); // For MSI auth, 2.0 token + // For SingleTenant/MSI auth, the JWT tokens will be issued from the bot's home tenant. + // So, these issuers need to be added to the list of valid token issuers for authenticating activity requests. + validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId)); + validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, tenantId)); + validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV1, tenantId)); + validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2, tenantId)); } // We only support being a skill or a skill consumer currently (not both). diff --git a/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConstants.cs b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConstants.cs index 36bf02e00f..279bac6fbd 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConstants.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConstants.cs @@ -75,6 +75,26 @@ public static class AuthenticationConstants /// public const string ToBotFromEnterpriseChannelOpenIdMetadataUrlFormat = "https://{0}.enterprisechannel.botframework.com/v1/.well-known/openidconfiguration"; + /// + /// The V1 Azure AD token issuer URL template that will contain the tenant id where the token was issued from. + /// + public const string ValidTokenIssuerUrlTemplateV1 = "https://sts.windows.net/{0}/"; + + /// + /// The V2 Azure AD token issuer URL template that will contain the tenant id where the token was issued from. + /// + public const string ValidTokenIssuerUrlTemplateV2 = "https://login.microsoftonline.com/{0}/v2.0"; + + /// + /// The Government V1 Azure AD token issuer URL template that will contain the tenant id where the token was issued from. + /// + public const string ValidGovernmentTokenIssuerUrlTemplateV1 = "https://login.microsoftonline.us/{0}/"; + + /// + /// The Government V2 Azure AD token issuer URL template that will contain the tenant id where the token was issued from. + /// + public const string ValidGovernmentTokenIssuerUrlTemplateV2 = "https://login.microsoftonline.us/{0}/v2.0"; + /// /// "azp" Claim. /// Authorized party - the party to which the ID Token was issued. diff --git a/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs b/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs index c871aed79e..f8740d71ec 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/PasswordServiceClientCredentialFactory.cs @@ -26,19 +26,6 @@ public PasswordServiceClientCredentialFactory() { } - /// - /// Initializes a new instance of the class. - /// with the provided credentials. - /// - /// The app ID. - /// The app password. - /// A custom httpClient to use. - /// A logger instance to use. - public PasswordServiceClientCredentialFactory(string appId, string password, HttpClient httpClient, ILogger logger) - : this(appId, password, tenantId: string.Empty, httpClient, logger) - { - } - /// /// Initializes a new instance of the class. /// with the provided credentials. @@ -108,21 +95,18 @@ public override Task CreateCredentialsAsync(string app if (loginEndpoint.StartsWith(AuthenticationConstants.ToChannelFromBotLoginUrlTemplate, StringComparison.OrdinalIgnoreCase)) { - return Task.FromResult(string.IsNullOrWhiteSpace(TenantId) - ? new MicrosoftAppCredentials(appId, Password, _httpClient, _logger, oauthScope) - : new MicrosoftAppCredentials(appId, Password, TenantId, _httpClient, _logger, oauthScope)); + return Task.FromResult(new MicrosoftAppCredentials( + appId, Password, TenantId, _httpClient, _logger, oauthScope)); } else if (loginEndpoint.Equals(GovernmentAuthenticationConstants.ToChannelFromBotLoginUrl, StringComparison.OrdinalIgnoreCase)) { - return Task.FromResult(string.IsNullOrWhiteSpace(TenantId) - ? new MicrosoftGovernmentAppCredentials(appId, Password, _httpClient, _logger, oauthScope) - : new MicrosoftGovernmentAppCredentials(appId, Password, TenantId, _httpClient, _logger, oauthScope)); + return Task.FromResult(new MicrosoftGovernmentAppCredentials( + appId, Password, TenantId, _httpClient, _logger, oauthScope)); } else { - return Task.FromResult(string.IsNullOrWhiteSpace(TenantId) - ? new PrivateCloudAppCredentials(AppId, Password, _httpClient, _logger, oauthScope, loginEndpoint, validateAuthority) - : new PrivateCloudAppCredentials(AppId, Password, TenantId, _httpClient, _logger, oauthScope, loginEndpoint, validateAuthority)); + return Task.FromResult(new PrivateCloudAppCredentials( + AppId, Password, TenantId, _httpClient, _logger, oauthScope, loginEndpoint, validateAuthority)); } } diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs index a188aa4015..9768f37d2b 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationServiceClientCredentialFactory.cs @@ -95,7 +95,7 @@ public ConfigurationServiceClientCredentialFactory(IConfiguration configuration, break; default: // MultiTenant - _inner = new PasswordServiceClientCredentialFactory(appId, password, httpClient, logger); + _inner = new PasswordServiceClientCredentialFactory(appId, password, tenantId: string.Empty, httpClient, logger); break; } } From d84c6a1f76a56dbee0d18a16adf5d678e5b30035 Mon Sep 17 00:00:00 2001 From: Muthuveer Somanathan Date: Fri, 20 Aug 2021 11:51:54 -0700 Subject: [PATCH 12/12] Remove private static field "_tokenValidationParameters" and made it to be a local variable where it is being used. --- .../Authentication/SkillValidation.cs | 50 +++++++++---------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs index 2cbff32a9b..b586ecbefc 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/SkillValidation.cs @@ -19,30 +19,6 @@ namespace Microsoft.Bot.Connector.Authentication public class SkillValidation #pragma warning restore CA1052 // Static holder types should be Static or NotInheritable { - /// - /// TO SKILL FROM BOT and TO BOT FROM SKILL: Token validation parameters when connecting a bot to a skill. - /// - private static readonly TokenValidationParameters _tokenValidationParameters = - new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuers = new[] - { - "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", // Auth v3.1, 1.0 token - "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", // Auth v3.1, 2.0 token - "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Auth v3.2, 1.0 token - "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Auth v3.2, 2.0 token - "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", // Auth for US Gov, 1.0 token - "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0", // Auth for US Gov, 2.0 token - "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Auth for US Gov, 1.0 token - "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Auth for US Gov, 2.0 token - }, - ValidateAudience = false, // Audience validation takes place manually in code. - ValidateLifetime = true, - ClockSkew = TimeSpan.FromMinutes(5), - RequireSignedTokens = true - }; - /// /// Determines if a given Auth header is from from a skill to bot or bot to skill request. /// @@ -144,17 +120,37 @@ public static async Task AuthenticateChannelToken(string authHea GovernmentAuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl : AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl; + var tokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuers = new[] + { + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", // Auth v3.1, 1.0 token + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", // Auth v3.1, 2.0 token + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Auth v3.2, 1.0 token + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Auth v3.2, 2.0 token + "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", // Auth for US Gov, 1.0 token + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0", // Auth for US Gov, 2.0 token + "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Auth for US Gov, 1.0 token + "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Auth for US Gov, 2.0 token + }, + ValidateAudience = false, // Audience validation takes place manually in code. + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + RequireSignedTokens = true + }; + // Add allowed token issuers from configuration (if present) if (authConfig.ValidTokenIssuers != null && authConfig.ValidTokenIssuers.Any()) { - var validIssuers = _tokenValidationParameters.ValidIssuers.ToList(); + var validIssuers = tokenValidationParameters.ValidIssuers.ToList(); validIssuers.AddRange(authConfig.ValidTokenIssuers); - _tokenValidationParameters.ValidIssuers = validIssuers; + tokenValidationParameters.ValidIssuers = validIssuers; } var tokenExtractor = new JwtTokenExtractor( httpClient, - _tokenValidationParameters, + tokenValidationParameters, openIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms);