Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.
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
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Microsoft.Bot.Connector.Authentication
{
/// <summary>
Expand All @@ -12,6 +14,22 @@ namespace Microsoft.Bot.Connector.Authentication
/// </remarks>
public class AuthenticationConfiguration
{
public string[] RequiredEndorsements { get; set; } = new string[] { };
/// <summary>
/// Gets or sets an array of JWT endorsements.
/// </summary>
/// <value>
/// An array of JWT endorsements.
/// </value>
#pragma warning disable CA1819 // Properties should not return arrays (we can't change this without breaking binary compat)
public string[] RequiredEndorsements { get; set; } = Array.Empty<string>();
#pragma warning restore CA1819 // Properties should not return arrays

/// <summary>
/// Gets or sets an <see cref="ClaimsValidator"/> instance used to validate the identity claims.
/// </summary>
/// <value>
/// An <see cref="ClaimsValidator"/> instance used to validate the identity claims.
/// </value>
public virtual ClaimsValidator ClaimsValidator { get; set; } = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Microsoft.Bot.Connector.Authentication
{
/// <summary>
/// An interface used to validate identity <see cref="Claim"/>.
/// </summary>
public abstract class ClaimsValidator
{
/// <summary>
/// Validates a list of <see cref="Claim"/> and should throw an exception if the validation fails.
/// </summary>
/// <param name="claims">The list of claims to validate.</param>
/// <returns>true if the validation is successful, false if not.</returns>
/// <exception cref="UnauthorizedAccessException">Throw this exception if the validation fails.</exception>
public abstract Task ValidateClaimsAsync(IList<Claim> claims);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,33 +121,11 @@ public static async Task<ClaimsIdentity> ValidateAuthHeader(string authHeader, I

httpClient = httpClient ?? _httpClient;

if (SkillValidation.IsSkillToken(authHeader))
{
return await SkillValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, httpClient, channelId, authConfig).ConfigureAwait(false);
}

if (EmulatorValidation.IsTokenFromEmulator(authHeader))
{
return await EmulatorValidation.AuthenticateEmulatorToken(authHeader, credentials, channelProvider, httpClient, channelId, authConfig).ConfigureAwait(false);
}

if (channelProvider == null || channelProvider.IsPublicAzure())
{
// No empty or null check. Empty can point to issues. Null checks only.
if (serviceUrl != null)
{
return await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient, channelId, authConfig).ConfigureAwait(false);
}

return await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, httpClient, channelId, authConfig).ConfigureAwait(false);
}
var identity = await AuthenticateTokenAsync(authHeader, credentials, channelProvider, channelId, authConfig, serviceUrl, httpClient).ConfigureAwait(false);

if (channelProvider.IsGovernment())
{
return await GovernmentChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient, channelId, authConfig).ConfigureAwait(false);
}
await ValidateClaimsAsync(authConfig, identity.Claims).ConfigureAwait(false);

return await EnterpriseChannelValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, serviceUrl, httpClient, channelId, authConfig).ConfigureAwait(false);
return identity;
}

/// <summary>
Expand Down Expand Up @@ -186,6 +164,27 @@ public static string GetAppIdFromClaims(IEnumerable<Claim> claims)
return appId;
}

/// <summary>
/// Validates the identity claims against the <see cref="ClaimsValidator"/> in <see cref="AuthenticationConfiguration"/> if present.
/// </summary>
/// <param name="authConfig">An <see cref="AuthenticationConfiguration"/> instance.</param>
/// <param name="claims">The list of claims to validate.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="UnauthorizedAccessException">If the validation returns false, or ClaimsValidator is null and this is a skill claim.</exception>
internal static async Task ValidateClaimsAsync(AuthenticationConfiguration authConfig, IEnumerable<Claim> claims)
{
if (authConfig.ClaimsValidator != null)
{
// Call the validation method if defined (it should throw an exception if the validation fails)
var claimsList = claims as IList<Claim> ?? claims.ToList();
await authConfig.ClaimsValidator.ValidateClaimsAsync(claimsList).ConfigureAwait(false);
}
else if (SkillValidation.IsSkillClaim(claims))
{
throw new UnauthorizedAccessException("ClaimsValidator is required for validation of Skill Host calls.");
}
}

/// <summary>
/// Internal helper to check if the token has the shape we expect "Bearer [big long string]".
/// </summary>
Expand Down Expand Up @@ -218,5 +217,36 @@ internal static bool IsValidTokenFormat(string authHeader)

return true;
}

private static async Task<ClaimsIdentity> AuthenticateTokenAsync(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string channelId, AuthenticationConfiguration authConfig, string serviceUrl, HttpClient httpClient)
{
if (SkillValidation.IsSkillToken(authHeader))
{
return await SkillValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, httpClient, channelId, authConfig).ConfigureAwait(false);
}

if (EmulatorValidation.IsTokenFromEmulator(authHeader))
{
return await EmulatorValidation.AuthenticateEmulatorToken(authHeader, credentials, channelProvider, httpClient, channelId, authConfig).ConfigureAwait(false);
}

if (channelProvider == null || channelProvider.IsPublicAzure())
{
// No empty or null check. Empty can point to issues. Null checks only.
if (serviceUrl != null)
{
return await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient, channelId, authConfig).ConfigureAwait(false);
}

return await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, httpClient, channelId, authConfig).ConfigureAwait(false);
}

if (channelProvider.IsGovernment())
{
return await GovernmentChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient, channelId, authConfig).ConfigureAwait(false);
}

return await EnterpriseChannelValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, serviceUrl, httpClient, channelId, authConfig).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,32 @@ public void GetAppIdFromClaimsTests()
Assert.Equal(appId, JwtTokenValidation.GetAppIdFromClaims(v2Claims));
}

[Fact]
public async Task ValidateClaimsTest_ThrowsOnSkillClaim_WithNullValidator()
{
var claims = new List<Claim>();
claims.Add(new Claim(AuthenticationConstants.VersionClaim, "2.0"));
claims.Add(new Claim(AuthenticationConstants.AudienceClaim, "SkillBotId"));
claims.Add(new Claim(AuthenticationConstants.AuthorizedParty, "BotId")); // Skill claims aud!=azp

// AuthenticationConfiguration with no ClaimsValidator and a Skill Claim, should throw UnauthorizedAccessException
// Skill calls MUST be validated with a ClaimsValidator
await Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await JwtTokenValidation.ValidateClaimsAsync(new AuthenticationConfiguration(), claims));
}

[Fact]
public async Task ValidateClaimsTest_DoesNotThrow_WhenNotSkillClaim_WithNullValidator()
{
var claims = new List<Claim>();
claims.Add(new Claim(AuthenticationConstants.VersionClaim, "2.0"));
claims.Add(new Claim(AuthenticationConstants.AudienceClaim, "BotId"));
claims.Add(new Claim(AuthenticationConstants.AuthorizedParty, "BotId")); // Skill claims aud!=azp

// AuthenticationConfiguration with no ClaimsValidator and a none Skill Claim, should NOT throw UnauthorizedAccessException
// None Skill do not need a ClaimsValidator.
await JwtTokenValidation.ValidateClaimsAsync(new AuthenticationConfiguration(), claims);
}

private async Task JwtTokenValidation_ValidateAuthHeader_WithChannelService_Succeeds(string appId, string pwd, string channelService)
{
string header = $"Bearer {await new MicrosoftAppCredentials(appId, pwd).GetTokenAsync()}";
Expand Down