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

feat: Add Ase channel validation. #6718

Merged
merged 10 commits into from
Dec 19, 2023
11 changes: 10 additions & 1 deletion ApiCompatBaseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@ TypesMustExist : Type 'Microsoft.Bot.Builder.Azure.CosmosDbCustomClientOptions'
TypesMustExist : Type 'Microsoft.Bot.Builder.Azure.CosmosDbStorage' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Microsoft.Bot.Builder.Azure.CosmosDbStorageOptions' does not exist in the implementation but it does exist in the contract.

MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.MicrosoftAppCredentials..ctor(System.String, System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.MicrosoftAppCredentials..ctor(System.String, System.String, System.Net.Http.HttpClient)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.MicrosoftAppCredentials..ctor(System.String, System.String, System.Net.Http.HttpClient, Microsoft.Extensions.Logging.ILogger)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.MicrosoftAppCredentials..ctor(System.String, System.String, System.String, System.Net.Http.HttpClient)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.MicrosoftAppCredentials..ctor(System.String, System.String, System.String, System.Net.Http.HttpClient, Microsoft.Extensions.Logging.ILogger)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.MicrosoftGovernmentAppCredentials..ctor(System.String, System.String, System.Net.Http.HttpClient)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.MicrosoftGovernmentAppCredentials..ctor(System.String, System.String, System.Net.Http.HttpClient, Microsoft.Extensions.Logging.ILogger)' does not exist in the implementation but it does exist in the contract.

TypesMustExist : Type 'Microsoft.Bot.Connector.Authentication.AdalAuthenticator' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.AppCredentials.BuildAuthenticator()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'Microsoft.Bot.Connector.Authentication.AppCredentials.BuildIAuthenticator()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.CertificateAppCredentials..ctor(Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate, System.String, System.Net.Http.HttpClient, Microsoft.Extensions.Logging.ILogger)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.CertificateAppCredentials.BuildAuthenticator()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.MicrosoftAppCredentials.BuildAuthenticator()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'Microsoft.Bot.Connector.Authentication.MicrosoftAppCredentials.BuildAuthenticator()' does not exist in the implementation but it does exist in the contract.

Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public abstract class AppCredentials : ServiceClientCredentials
/// </summary>
private Lazy<IAuthenticator> _authenticator;

private string _oAuthScope;

/// <summary>
/// Initializes a new instance of the <see cref="AppCredentials"/> class.
/// </summary>
Expand All @@ -54,7 +56,7 @@ public AppCredentials(string channelAuthTenant = null, HttpClient customHttpClie
/// <param name="oAuthScope">The scope for the token.</param>
public AppCredentials(string channelAuthTenant = null, HttpClient customHttpClient = null, ILogger logger = null, string oAuthScope = null)
{
OAuthScope = string.IsNullOrWhiteSpace(oAuthScope) ? AuthenticationConstants.ToChannelFromBotOAuthScope : oAuthScope;
_oAuthScope = oAuthScope;
ChannelAuthTenant = channelAuthTenant;
CustomHttpClient = customHttpClient;
Logger = logger ?? NullLogger.Instance;
Expand All @@ -74,13 +76,15 @@ public AppCredentials(string channelAuthTenant = null, HttpClient customHttpClie
/// <value>
/// Tenant to be used for channel authentication.
/// </value>
public string ChannelAuthTenant
public virtual string ChannelAuthTenant
{
get => string.IsNullOrEmpty(AuthTenant) ? AuthenticationConstants.DefaultChannelAuthTenant : AuthTenant;
get => string.IsNullOrEmpty(AuthTenant)
? DefaultChannelAuthTenant
: AuthTenant;
set
{
// Advanced user only, see https://aka.ms/bots/tenant-restriction
var endpointUrl = string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ToChannelFromBotLoginUrlTemplate, value);
var endpointUrl = string.Format(CultureInfo.InvariantCulture, ToChannelFromBotLoginUrlTemplate, value);

if (Uri.TryCreate(endpointUrl, UriKind.Absolute, out _))
{
Expand All @@ -99,7 +103,7 @@ public string ChannelAuthTenant
/// <value>
/// The OAuth endpoint to use.
/// </value>
public virtual string OAuthEndpoint => string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ToChannelFromBotLoginUrlTemplate, ChannelAuthTenant);
public virtual string OAuthEndpoint => string.Format(CultureInfo.InvariantCulture, ToChannelFromBotLoginUrlTemplate, ChannelAuthTenant);

/// <summary>
/// Gets a value indicating whether to validate the Authority.
Expand All @@ -115,7 +119,9 @@ public string ChannelAuthTenant
/// <value>
/// The OAuth scope to use.
/// </value>
public virtual string OAuthScope { get; }
public virtual string OAuthScope => string.IsNullOrEmpty(_oAuthScope)
? ToChannelFromBotOAuthScope
: _oAuthScope;

/// <summary>
/// Gets or sets the channel auth token tenant for this credential.
Expand All @@ -141,6 +147,26 @@ public string ChannelAuthTenant
/// </value>
protected ILogger Logger { get; set; }

/// <summary>
/// Gets DefaultChannelAuthTenant.
/// </summary>
/// <value>DefaultChannelAuthTenant.</value>
protected virtual string DefaultChannelAuthTenant => AuthenticationConstants.DefaultChannelAuthTenant;

/// <summary>
/// Gets ToChannelFromBotOAuthScope.
/// </summary>
/// <value>ToChannelFromBotOAuthScope.</value>
protected virtual string ToChannelFromBotOAuthScope => AuthenticationConstants.ToChannelFromBotOAuthScope;

/// <summary>
/// Gets ToChannelFromBotLoginUrlTemplate.
/// </summary>
/// <value>ToChannelFromBotLoginUrlTemplate.</value>
#pragma warning disable CA1056 // Uri properties should not be strings
protected virtual string ToChannelFromBotLoginUrlTemplate => AuthenticationConstants.ToChannelFromBotLoginUrlTemplate;
#pragma warning restore CA1056 // Uri properties should not be strings

/// <summary>
/// Adds the host of service url to <see cref="MicrosoftAppCredentials"/> trusted hosts.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// 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
{
/// <summary>
/// Validates and Examines JWT tokens from the AseChannel.
/// </summary>
[Obsolete("Use `ConfigurationBotFrameworkAuthentication` instead to perform AseChannel validation.", false)]
public static class AseChannelValidation
{
/// <summary>
/// Just used for app service extension v2 (independent app service).
/// </summary>
public const string ChannelId = "AseChannel";

/// <summary>
/// TO BOT FROM AseChannel: Token validation parameters when connecting to a channel.
/// </summary>
public static readonly TokenValidationParameters BetweenBotAndAseChannelTokenValidationParameters =
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 _credentialProvider;
private static HttpClient _authHttpClient = new HttpClient();

/// <summary>
/// Set up user issue/metadataUrl for AseChannel validation.
/// </summary>
/// <param name="configuration">App Configurations, will GetSection MicrosoftAppId/MicrosoftAppTenantId/ChannelService.</param>
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;

_credentialProvider = new SimpleCredentialProvider(appId, string.Empty);
_metadataUrl = new SimpleChannelProvider(channelService).IsGovernment()
? GovernmentAuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl
: AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl;

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<string>();
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
}

BetweenBotAndAseChannelTokenValidationParameters.ValidIssuers = validIssuers;
}

/// <summary>
/// Determines if a request from AseChannel.
/// </summary>
/// <param name="channelId">need to be same with ChannelId.</param>
/// <returns>True, if the token was issued by the AseChannel. Otherwise, false.</returns>
public static bool IsAseChannel(string channelId)
{
return channelId == ChannelId;
}

/// <summary>
/// Validate the incoming Auth Header as a token sent from the AseChannel.
/// </summary>
/// <param name="authHeader">The raw HTTP header in the format: "Bearer [longString]".</param>
/// <param name="httpClient">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.</param>
/// <returns>
/// A valid ClaimsIdentity.
/// </returns>
public static async Task<ClaimsIdentity> AuthenticateAseTokenAsync(
string authHeader,
HttpClient httpClient = default)
{
httpClient = httpClient ?? _authHttpClient;

return await AuthenticateAseTokenAsync(authHeader, httpClient, new AuthenticationConfiguration()).ConfigureAwait(false);
}

/// <summary>
/// Validate the incoming Auth Header as a token sent from the AseChannel.
/// </summary>
/// <param name="authHeader">The raw HTTP header in the format: "Bearer [longString]".</param>
/// <param name="httpClient">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.</param>
/// <param name="authConfig">The authentication configuration.</param>
/// <returns>
/// A valid ClaimsIdentity.
/// </returns>
public static async Task<ClaimsIdentity> AuthenticateAseTokenAsync(string authHeader, HttpClient httpClient, AuthenticationConfiguration authConfig)
{
if (authConfig == null)
{
throw new ArgumentNullException(nameof(authConfig));
}

var tokenExtractor = new JwtTokenExtractor(
httpClient,
BetweenBotAndAseChannelTokenValidationParameters,
_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 _credentialProvider.IsValidAppIdAsync(appID).ConfigureAwait(false))
{
await Console.Out.WriteLineAsync(appID).ConfigureAwait(false);
}

return identity;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public static class AuthenticationConstants
/// </summary>
public const string ToBotFromEmulatorOpenIdMetadataUrl = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";

/// <summary>
/// TO BOT FROM AseChannel: OpenID metadata document for tokens coming from MSA.
/// </summary>
public const string ToBotFromAseChannelOpenIdMetadataUrl = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";

/// <summary>
/// TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming from MSA.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,24 @@ public static class GovernmentAuthenticationConstants
public const string ChannelService = "https://botframework.azure.us";

/// <summary>
/// TO GOVERNMENT CHANNEL FROM BOT: Login URL.
/// TO CHANNEL FROM BOT: Login URL.
///
/// DEPRECATED. For binary compat only.
/// </summary>
public const string ToChannelFromBotLoginUrl = "https://login.microsoftonline.us/MicrosoftServices.onmicrosoft.us";

/// <summary>
/// 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.
/// </summary>
public const string ToChannelFromBotLoginUrlTemplate = "https://login.microsoftonline.us/{0}";

/// <summary>
/// The default tenant to acquire bot to channel token from.
/// </summary>
public const string DefaultChannelAuthTenant = "MicrosoftServices.onmicrosoft.us";

/// <summary>
/// TO GOVERNMENT CHANNEL FROM BOT: OAuth scope to request.
/// </summary>
Expand All @@ -42,5 +56,10 @@ public static class GovernmentAuthenticationConstants
/// TO BOT FROM GOVERNMENT EMULATOR: OpenID metadata document for tokens coming from MSA.
/// </summary>
public const string ToBotFromEmulatorOpenIdMetadataUrl = "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration";

/// <summary>
/// TO BOT FROM GOVERNMENT AseChannel: OpenID metadata document for tokens coming from MSA.
/// </summary>
public const string ToBotFromAseChannelOpenIdMetadataUrl = "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration";
}
}
Loading