Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Managed Identity (MSI) + Single Tenant support for Bot apps #5829

Merged
merged 16 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.Loader;
using Microsoft.ApplicationInsights.Extensibility;
Expand Down Expand Up @@ -114,6 +115,20 @@ 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<string>();
var tenantId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value;
if (!string.IsNullOrWhiteSpace(tenantId))
{
// 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).
// 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<SkillSettings>();
Expand All @@ -122,13 +137,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<ChannelServiceHandlerBase, CloudSkillHandler>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;

namespace Microsoft.Bot.Connector.Authentication
{
Expand Down Expand Up @@ -31,5 +32,13 @@ public class AuthenticationConfiguration
/// An <see cref="ClaimsValidator"/> instance used to validate the identity claims.
/// </value>
public virtual ClaimsValidator ClaimsValidator { get; set; } = null;

/// <summary>
/// Gets or sets a collection of valid JWT token issuers.
/// </summary>
/// <value>
/// A collection of valid JWT token issuers.
/// </value>
public IEnumerable<string> ValidTokenIssuers { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,26 @@ public static class AuthenticationConstants
/// </summary>
public const string ToBotFromEnterpriseChannelOpenIdMetadataUrlFormat = "https://{0}.enterprisechannel.botframework.com/v1/.well-known/openidconfiguration";

/// <summary>
/// The V1 Azure AD token issuer URL template that will contain the tenant id where the token was issued from.
/// </summary>
public const string ValidTokenIssuerUrlTemplateV1 = "https://sts.windows.net/{0}/";

/// <summary>
/// The V2 Azure AD token issuer URL template that will contain the tenant id where the token was issued from.
/// </summary>
public const string ValidTokenIssuerUrlTemplateV2 = "https://login.microsoftonline.com/{0}/v2.0";

/// <summary>
/// The Government V1 Azure AD token issuer URL template that will contain the tenant id where the token was issued from.
/// </summary>
public const string ValidGovernmentTokenIssuerUrlTemplateV1 = "https://login.microsoftonline.us/{0}/";

/// <summary>
/// The Government V2 Azure AD token issuer URL template that will contain the tenant id where the token was issued from.
/// </summary>
public const string ValidGovernmentTokenIssuerUrlTemplateV2 = "https://login.microsoftonline.us/{0}/v2.0";

/// <summary>
/// "azp" Claim.
/// Authorized party - the party to which the ID Token was issued.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A factory that can create OAuth token providers for generating JWT auth tokens.
/// </summary>
public interface IJwtTokenProviderFactory
{
/// <summary>
/// Creates a new instance of the <see cref="AzureServiceTokenProvider"/> class.
/// </summary>
/// <param name="appId">Client id for the managed identity to be used for acquiring tokens.</param>
/// <param name="customHttpClient">A customized instance of the HttpClient class.</param>
/// <returns>A new instance of the <see cref="AzureServiceTokenProvider"/> class.</returns>
AzureServiceTokenProvider CreateAzureServiceTokenProvider(string appId, HttpClient customHttpClient = null);
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <inheritdoc />
public class JwtTokenProviderFactory : IJwtTokenProviderFactory
{
/// <inheritdoc />
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=<client-id-guid>" for user-assigned managed identities
var connectionString = $"RunAs=App;AppId={appId}";
return customHttpClient == null
? new AzureServiceTokenProvider(connectionString)
: new AzureServiceTokenProvider(connectionString, httpClientFactory: new ConstantHttpClientFactory(customHttpClient));
}
}
}
Original file line number Diff line number Diff line change
@@ -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 Microsoft.Extensions.Logging;

namespace Microsoft.Bot.Connector.Authentication
{
/// <summary>
/// Managed Service Identity auth implementation.
/// </summary>
public class ManagedIdentityAppCredentials : AppCredentials
EricDahlvang marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly IJwtTokenProviderFactory _tokenProviderFactory;

/// <summary>
/// Initializes a new instance of the <see cref="ManagedIdentityAppCredentials"/> class.
/// Managed Identity for AAD credentials auth and caching.
/// </summary>
/// <param name="appId">Client ID for the managed identity assigned to the bot.</param>
/// <param name="oAuthScope">The scope for the token.</param>
/// <param name="tokenProviderFactory">The JWT token provider factory to use.</param>
/// <param name="customHttpClient">Optional <see cref="HttpClient"/> to be used when acquiring tokens.</param>
/// <param name="logger">Optional <see cref="ILogger"/> to gather telemetry data while acquiring and managing credentials.</param>
public ManagedIdentityAppCredentials(string appId, string oAuthScope, IJwtTokenProviderFactory tokenProviderFactory, HttpClient customHttpClient = null, ILogger logger = null)
: base(channelAuthTenant: null, customHttpClient, logger, oAuthScope)
{
if (string.IsNullOrWhiteSpace(appId))
{
throw new ArgumentNullException(nameof(appId));
}

_tokenProviderFactory = tokenProviderFactory ?? throw new ArgumentNullException(nameof(tokenProviderFactory));

MicrosoftAppId = appId;
}

/// <inheritdoc/>
protected override Lazy<AdalAuthenticator> BuildAuthenticator()
{
// Should not be called, legacy
throw new NotImplementedException();
}

/// <inheritdoc/>
protected override Lazy<IAuthenticator> BuildIAuthenticator()
{
return new Lazy<IAuthenticator>(
() => new ManagedIdentityAuthenticator(MicrosoftAppId, OAuthScope, _tokenProviderFactory, CustomHttpClient, Logger),
LazyThreadSafetyMode.ExecutionAndPublication);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// 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
{
/// <summary>
/// Abstraction to acquire tokens from a Managed Service Identity.
/// </summary>
public class ManagedIdentityAuthenticator : IAuthenticator
{
private readonly AzureServiceTokenProvider _tokenProvider;
private readonly string _resource;
private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="ManagedIdentityAuthenticator"/> class.
/// </summary>
/// <param name="appId">Client id for the managed identity to be used for acquiring tokens.</param>
/// <param name="resource">Resource for which to acquire the token.</param>
/// <param name="tokenProviderFactory">The JWT token provider factory to use.</param>
/// <param name="customHttpClient">A customized instance of the HttpClient class.</param>
/// <param name="logger">The type used to perform logging.</param>
public ManagedIdentityAuthenticator(string appId, string resource, IJwtTokenProviderFactory tokenProviderFactory, HttpClient customHttpClient = null, ILogger logger = null)
{
if (string.IsNullOrWhiteSpace(appId))
{
throw new ArgumentNullException(nameof(appId));
}

if (string.IsNullOrWhiteSpace(resource))
{
throw new ArgumentNullException(nameof(resource));
}

if (tokenProviderFactory == null)
{
throw new ArgumentNullException(nameof(tokenProviderFactory));
}

_resource = resource;
_tokenProvider = tokenProviderFactory.CreateAzureServiceTokenProvider(appId, customHttpClient);
_logger = logger ?? NullLogger.Instance;
}

/// <inheritdoc/>
public async Task<AuthenticatorResult> 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<AuthenticatorResult> AcquireTokenAsync(bool forceRefresh)
{
var authResult = await _tokenProvider.GetAuthenticationResultAsync(_resource, forceRefresh).ConfigureAwait(false);
return new AuthenticatorResult
{
AccessToken = authResult.AccessToken,
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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.Rest;

namespace Microsoft.Bot.Connector.Authentication
{
/// <summary>
/// A Managed Identity implementation of the <see cref="ServiceClientCredentialsFactory"/> interface.
/// </summary>
public class ManagedIdentityServiceClientCredentialsFactory : ServiceClientCredentialsFactory
EricDahlvang marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly string _appId;
private readonly IJwtTokenProviderFactory _tokenProviderFactory;
private readonly HttpClient _httpClient;
private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="ManagedIdentityServiceClientCredentialsFactory"/> class.
/// </summary>
/// <param name="appId">Client ID for the managed identity assigned to the bot.</param>
/// <param name="tokenProviderFactory">The JWT token provider factory to use.</param>
/// <param name="httpClient">A custom httpClient to use.</param>
/// <param name="logger">A logger instance to use.</param>
public ManagedIdentityServiceClientCredentialsFactory(string appId, IJwtTokenProviderFactory tokenProviderFactory, HttpClient httpClient = null, ILogger logger = null)
{
if (string.IsNullOrWhiteSpace(appId))
{
throw new ArgumentNullException(nameof(appId));
}

_appId = appId;
_tokenProviderFactory = tokenProviderFactory ?? throw new ArgumentNullException(nameof(tokenProviderFactory));
_httpClient = httpClient;
_logger = logger;
}

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

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

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

return Task.FromResult<ServiceClientCredentials>(
new ManagedIdentityAppCredentials(_appId, audience, _tokenProviderFactory, _httpClient, _logger));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ namespace Microsoft.Bot.Connector.Authentication
/// </summary>
public class MicrosoftAppCredentials : AppCredentials
{
/// <summary>
/// The configuration property for the App type of the bot -- MultiTenant, SingleTenant, or, MSI.
/// </summary>
public const string MicrosoftAppTypeKey = "MicrosoftAppType";

/// <summary>
/// The configuration property for the Microsoft app Password.
/// </summary>
Expand All @@ -31,6 +36,11 @@ public class MicrosoftAppCredentials : AppCredentials
/// </summary>
public const string MicrosoftAppIdKey = "MicrosoftAppId";

/// <summary>
/// The configuration property for Tenant ID of the Azure AD tenant.
/// </summary>
public const string MicrosoftAppTenantIdKey = "MicrosoftAppTenantId";

/// <summary>
/// An empty set of credentials.
/// </summary>
Expand Down
Loading