From 18da274c05857b0e050e355d54be869064da310c Mon Sep 17 00:00:00 2001 From: fangyangci Date: Mon, 11 Dec 2023 11:28:01 +0800 Subject: [PATCH] ase channel validation --- .../Authentication/AppCredentials.cs | 2 +- .../Authentication/AseChannelValidation.cs | 209 ++++++++++++++++++ .../Authentication/AuthenticationConstants.cs | 5 + .../GovernmentAuthenticationConstants.cs | 17 ++ .../Authentication/JwtTokenValidation.cs | 5 + .../MicrosoftGovernmentAppCredentials.cs | 17 +- ...ParameterizedBotFrameworkAuthentication.cs | 5 + ...ConfigurationBotFrameworkAuthentication.cs | 2 + 8 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/AseChannelValidation.cs diff --git a/libraries/Microsoft.Bot.Connector/Authentication/AppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/AppCredentials.cs index 012bb644d6..b1f5b12c49 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/AppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/AppCredentials.cs @@ -74,7 +74,7 @@ public AppCredentials(string channelAuthTenant = null, HttpClient customHttpClie /// /// Tenant to be used for channel authentication. /// - public string ChannelAuthTenant + public virtual string ChannelAuthTenant { get => string.IsNullOrEmpty(AuthTenant) ? AuthenticationConstants.DefaultChannelAuthTenant : AuthTenant; set diff --git a/libraries/Microsoft.Bot.Connector/Authentication/AseChannelValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/AseChannelValidation.cs new file mode 100644 index 0000000000..f5d48fcbfb --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/AseChannelValidation.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.Bot.Connector.Authentication +{ + /// + /// Validates and Examines JWT tokens from the AseChannel. + /// + [Obsolete("Use `ConfigurationBotFrameworkAuthentication` instead to perform AseChannel validation.", false)] + public static class AseChannelValidation + { + /// + /// Just used for app service extension v2 (independent app service). + /// + public const string ChannelId = "AseChannel"; + + /// + /// TO BOT FROM AseChannel: Token validation parameters when connecting to a channel. + /// + public static readonly TokenValidationParameters ToBotFromAseChannelTokenValidationParameters = + new TokenValidationParameters() + { + ValidateIssuer = true, + + // Audience validation takes place manually in code. + ValidateAudience = false, // lgtm[cs/web/missing-token-validation] + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + RequireSignedTokens = true, + }; + + private static string _metadataUrl; + private static ICredentialProvider _defaultCredentialProvider; + private static IChannelProvider _defaultChannelProvider; + private static HttpClient _authHttpClient = new HttpClient(); + + /// + /// Set up user issue/metadataUrl for AseChannel validation. + /// + /// App Configurations, will GetSection MicrosoftAppId/MicrosoftAppTenantId/ChannelService/ToBotFromAseOpenIdMetadataUrl. + public static void Init(IConfiguration configuration) + { + var appId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value; + var tenantId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppTenantIdKey)?.Value; + + var channelService = configuration.GetSection("ChannelService")?.Value; + var toBotFromAseOpenIdMetadataUrl = configuration.GetSection("ToBotFromAseOpenIdMetadataUrl")?.Value; + + _defaultCredentialProvider = new SimpleCredentialProvider(appId, string.Empty); + _defaultChannelProvider = new SimpleChannelProvider(channelService); + + _metadataUrl = !string.IsNullOrEmpty(toBotFromAseOpenIdMetadataUrl) + ? toBotFromAseOpenIdMetadataUrl + : (_defaultChannelProvider.IsGovernment() + ? GovernmentAuthenticationConstants.ToBotFromAseChannelOpenIdMetadataUrl + : AuthenticationConstants.ToBotFromAseChannelOpenIdMetadataUrl); + + var tenantIds = new string[] + { + tenantId, + "f8cdef31-a31e-4b4a-93e4-5f571e91255a", // US Gov MicrosoftServices.onmicrosoft.us + "d6d49420-f39b-4df7-a1dc-d59a935871db" // Public botframework.com + }; + + var validIssuers = new HashSet(); + foreach (var tmpId in tenantIds) + { + validIssuers.Add($"https://sts.windows.net/{tmpId}/"); // Auth Public/US Gov, 1.0 token + validIssuers.Add($"https://login.microsoftonline.com/{tmpId}/v2.0"); // Auth Public, 2.0 token + validIssuers.Add($"https://login.microsoftonline.us/{tmpId}/v2.0"); // Auth for US Gov, 2.0 token + } + + ToBotFromAseChannelTokenValidationParameters.ValidIssuers = validIssuers; + } + + /// + /// Determines if a request from AseChannel. + /// + /// need to be same with ChannelId. + /// True, if the token was issued by the AseChannel. Otherwise, false. + public static bool IsAseChannel(string channelId) + { + return channelId == ChannelId; + } + + /// + /// Validate the incoming Auth Header as a token sent from the AseChannel. + /// + /// The raw HTTP header in the format: "Bearer [longString]". + /// The user defined set of valid credentials, such as the AppId. + /// Authentication of tokens requires calling out to validate Endorsements and related documents. The + /// HttpClient is used for making those calls. Those calls generally require TLS connections, which are expensive to + /// setup and teardown, so a shared HttpClient is recommended. + /// + /// A valid ClaimsIdentity. + /// + public static async Task AuthenticateAseTokenAsync( + string authHeader, + ICredentialProvider credentials = default, + HttpClient httpClient = default) + { + credentials = credentials ?? _defaultCredentialProvider; + httpClient = httpClient ?? _authHttpClient; + + return await AuthenticateAseTokenAsync(authHeader, credentials, httpClient, new AuthenticationConfiguration()).ConfigureAwait(false); + } + + /// + /// Validate the incoming Auth Header as a token sent from the AseChannel. + /// + /// The raw HTTP header in the format: "Bearer [longString]". + /// The user defined set of valid credentials, such as the AppId. + /// Authentication of tokens requires calling out to validate Endorsements and related documents. The + /// HttpClient is used for making those calls. Those calls generally require TLS connections, which are expensive to + /// setup and teardown, so a shared HttpClient is recommended. + /// The authentication configuration. + /// + /// A valid ClaimsIdentity. + /// + public static async Task AuthenticateAseTokenAsync(string authHeader, ICredentialProvider credentials, HttpClient httpClient, AuthenticationConfiguration authConfig) + { + if (authConfig == null) + { + throw new ArgumentNullException(nameof(authConfig)); + } + + var tokenExtractor = new JwtTokenExtractor( + httpClient, + ToBotFromAseChannelTokenValidationParameters, + _metadataUrl, + AuthenticationConstants.AllowedSigningAlgorithms); + + var identity = await tokenExtractor.GetIdentityAsync(authHeader, ChannelId, authConfig.RequiredEndorsements).ConfigureAwait(false); + if (identity == null) + { + // No valid identity. Not Authorized. + throw new UnauthorizedAccessException("Invalid Identity"); + } + + if (!identity.IsAuthenticated) + { + // The token is in some way invalid. Not Authorized. + throw new UnauthorizedAccessException("Token Not Authenticated"); + } + + // Now check that the AppID in the claimset matches + // what we're looking for. Note that in a multi-tenant bot, this value + // comes from developer code that may be reaching out to a service, hence the + // Async validation. + Claim versionClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.VersionClaim); + if (versionClaim == null) + { + throw new UnauthorizedAccessException("'ver' claim is required on AseChannel Tokens."); + } + + string tokenVersion = versionClaim.Value; + string appID = string.Empty; + + // The AseChannel, depending on Version, sends the AppId via either the + // appid claim (Version 1) or the Authorized Party claim (Version 2). + if (string.IsNullOrWhiteSpace(tokenVersion) || tokenVersion == "1.0") + { + // either no Version or a version of "1.0" means we should look for + // the claim in the "appid" claim. + Claim appIdClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.AppIdClaim); + if (appIdClaim == null) + { + // No claim around AppID. Not Authorized. + throw new UnauthorizedAccessException("'appid' claim is required on AseChannel Token version '1.0'."); + } + + appID = appIdClaim.Value; + } + else if (tokenVersion == "2.0") + { + // AseChannel, "2.0" puts the AppId in the "azp" claim. + Claim appZClaim = identity.Claims.FirstOrDefault(c => c.Type == AuthenticationConstants.AuthorizedParty); + if (appZClaim == null) + { + // No claim around AppID. Not Authorized. + throw new UnauthorizedAccessException("'azp' claim is required on AseChannel Token version '2.0'."); + } + + appID = appZClaim.Value; + } + else + { + // Unknown Version. Not Authorized. + throw new UnauthorizedAccessException($"Unknown AseChannel Token version '{tokenVersion}'."); + } + + if (!await credentials.IsValidAppIdAsync(appID).ConfigureAwait(false)) + { + await Console.Out.WriteLineAsync(appID).ConfigureAwait(false); + } + + return identity; + } + } +} diff --git a/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConstants.cs b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConstants.cs index 279bac6fbd..e01d571bd3 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConstants.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConstants.cs @@ -70,6 +70,11 @@ public static class AuthenticationConstants /// public const string ToBotFromEmulatorOpenIdMetadataUrl = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"; + /// + /// TO BOT FROM AseChannel: OpenID metadata document for tokens coming from MSA. + /// + public const string ToBotFromAseChannelOpenIdMetadataUrl = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"; + /// /// TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming from MSA. /// diff --git a/libraries/Microsoft.Bot.Connector/Authentication/GovernmentAuthenticationConstants.cs b/libraries/Microsoft.Bot.Connector/Authentication/GovernmentAuthenticationConstants.cs index e961808ffd..e5e8e8d185 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/GovernmentAuthenticationConstants.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/GovernmentAuthenticationConstants.cs @@ -18,6 +18,18 @@ public static class GovernmentAuthenticationConstants /// public const string ToChannelFromBotLoginUrl = "https://login.microsoftonline.us/MicrosoftServices.onmicrosoft.us"; + /// + /// TO CHANNEL FROM BOT: Login URL template string. Bot developer may specify + /// which tenant to obtain an access token from. By default, the channels only + /// accept tokens from "MicrosoftServices.onmicrosoft.us". For more details see https://aka.ms/bots/tenant-restriction. + /// + public const string ToChannelFromBotLoginUrlTemplate = "https://login.microsoftonline.us/{0}"; + + /// + /// The default tenant to acquire bot to channel token from. + /// + public const string DefaultChannelAuthTenant = "MicrosoftServices.onmicrosoft.us"; + /// /// TO GOVERNMENT CHANNEL FROM BOT: OAuth scope to request. /// @@ -42,5 +54,10 @@ public static class GovernmentAuthenticationConstants /// TO BOT FROM GOVERNMENT EMULATOR: OpenID metadata document for tokens coming from MSA. /// public const string ToBotFromEmulatorOpenIdMetadataUrl = "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration"; + + /// + /// TO BOT FROM GOVERNMENT AseChannel: OpenID metadata document for tokens coming from MSA. + /// + public const string ToBotFromAseChannelOpenIdMetadataUrl = "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration"; } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs index 927410e29f..e5f5152934 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs @@ -242,6 +242,11 @@ internal static bool IsValidTokenFormat(string authHeader) /// private static async Task AuthenticateTokenAsync(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string channelId, AuthenticationConfiguration authConfig, string serviceUrl, HttpClient httpClient) { + if (AseChannelValidation.IsAseChannel(channelId)) + { + return await AseChannelValidation.AuthenticateAseTokenAsync(authHeader, credentials, httpClient, authConfig).ConfigureAwait(false); + } + if (SkillValidation.IsSkillToken(authHeader)) { return await SkillValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, httpClient, channelId, authConfig).ConfigureAwait(false); diff --git a/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftGovernmentAppCredentials.cs b/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftGovernmentAppCredentials.cs index 7839abbc11..d46be0d738 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftGovernmentAppCredentials.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/MicrosoftGovernmentAppCredentials.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Globalization; using System.Net.Http; using Microsoft.Extensions.Logging; @@ -66,6 +67,18 @@ public MicrosoftGovernmentAppCredentials(string appId, string password, string t { } + /// + /// Gets or sets tenant to be used for channel authentication. + /// + /// + /// Tenant to be used for channel authentication. + /// + public override string ChannelAuthTenant + { + get => string.IsNullOrEmpty(AuthTenant) ? GovernmentAuthenticationConstants.DefaultChannelAuthTenant : AuthTenant; + set => base.ChannelAuthTenant = value; + } + /// /// Gets the OAuth endpoint to use. /// @@ -74,7 +87,7 @@ public MicrosoftGovernmentAppCredentials(string appId, string password, string t /// public override string OAuthEndpoint { - get { return GovernmentAuthenticationConstants.ToChannelFromBotLoginUrl; } - } + get => string.Format(CultureInfo.InvariantCulture, GovernmentAuthenticationConstants.ToChannelFromBotLoginUrlTemplate, ChannelAuthTenant); + } } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs b/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs index 0afccb78d5..bf9b016c08 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ParameterizedBotFrameworkAuthentication.cs @@ -174,6 +174,11 @@ private async Task JwtTokenValidation_ValidateClaimsAsync(IEnumerable cla private async Task JwtTokenValidation_AuthenticateTokenAsync(string authHeader, string channelId, string serviceUrl, CancellationToken cancellationToken) { + if (AseChannelValidation.IsAseChannel(channelId)) + { + return await AseChannelValidation.AuthenticateAseTokenAsync(authHeader).ConfigureAwait(false); + } + if (SkillValidation.IsSkillToken(authHeader)) { return await SkillValidation_AuthenticateChannelTokenAsync(authHeader, channelId, cancellationToken).ConfigureAwait(false); diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationBotFrameworkAuthentication.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationBotFrameworkAuthentication.cs index 84e7e26ff5..92d8fc1802 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationBotFrameworkAuthentication.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ConfigurationBotFrameworkAuthentication.cs @@ -30,6 +30,8 @@ public class ConfigurationBotFrameworkAuthentication : BotFrameworkAuthenticatio /// The ILogger instance to use. public ConfigurationBotFrameworkAuthentication(IConfiguration configuration, ServiceClientCredentialsFactory credentialsFactory = null, AuthenticationConfiguration authConfiguration = null, IHttpClientFactory httpClientFactory = null, ILogger logger = null) { + AseChannelValidation.Init(configuration); + var channelService = configuration.GetSection("ChannelService")?.Value; var validateAuthority = configuration.GetSection("ValidateAuthority")?.Value; var toChannelFromBotLoginUrl = configuration.GetSection("ToChannelFromBotLoginUrl")?.Value;