Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.
Merged
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.

38 changes: 32 additions & 6 deletions libraries/Microsoft.Bot.Connector/Authentication/AppCredentials.cs
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