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

Add ase channel validation #6712

Closed
wants to merge 1 commit into from
Closed
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 @@ -74,7 +74,7 @@ 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;
set
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <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 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();

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

ToBotFromAseChannelTokenValidationParameters.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="credentials">The user defined set of valid credentials, such as the AppId.</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,
ICredentialProvider credentials = default,
HttpClient httpClient = default)
{
credentials = credentials ?? _defaultCredentialProvider;
httpClient = httpClient ?? _authHttpClient;

return await AuthenticateAseTokenAsync(authHeader, credentials, 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="credentials">The user defined set of valid credentials, such as the AppId.</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, 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;
}
}
}
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 @@ -18,6 +18,18 @@ public static class GovernmentAuthenticationConstants
/// </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 +54,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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ internal static bool IsValidTokenFormat(string authHeader)
/// </summary>
private static async Task<ClaimsIdentity> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -66,6 +67,18 @@ public MicrosoftGovernmentAppCredentials(string appId, string password, string t
{
}

/// <summary>
/// Gets or sets tenant to be used for channel authentication.
/// </summary>
/// <value>
/// Tenant to be used for channel authentication.
/// </value>
public override string ChannelAuthTenant
{
get => string.IsNullOrEmpty(AuthTenant) ? GovernmentAuthenticationConstants.DefaultChannelAuthTenant : AuthTenant;
set => base.ChannelAuthTenant = value;
}

/// <summary>
/// Gets the OAuth endpoint to use.
/// </summary>
Expand All @@ -74,7 +87,7 @@ public MicrosoftGovernmentAppCredentials(string appId, string password, string t
/// </value>
public override string OAuthEndpoint
{
get { return GovernmentAuthenticationConstants.ToChannelFromBotLoginUrl; }
}
get => string.Format(CultureInfo.InvariantCulture, GovernmentAuthenticationConstants.ToChannelFromBotLoginUrlTemplate, ChannelAuthTenant);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ private async Task JwtTokenValidation_ValidateClaimsAsync(IEnumerable<Claim> cla

private async Task<ClaimsIdentity> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class ConfigurationBotFrameworkAuthentication : BotFrameworkAuthenticatio
/// <param name="logger">The ILogger instance to use.</param>
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;
Expand Down
Loading