From c94e292337a8ba98084d17ae86f84b93ee9e60e0 Mon Sep 17 00:00:00 2001 From: Carlos Castro Date: Wed, 5 Jun 2019 13:48:37 -0700 Subject: [PATCH 1/5] Auth: Support required endorsements --- .../BotFrameworkAdapter.cs | 8 +- .../AuthenticationConfiguration.cs | 18 ++++ .../Authentication/ChannelValidation.cs | 10 +- .../Authentication/EmulatorValidation.cs | 4 +- .../Authentication/EndorsementsValidator.cs | 8 +- .../EnterpriseChannelValidation.cs | 4 +- .../GovernmentChannelValidation.cs | 4 +- .../Authentication/JwtTokenExtractor.cs | 43 ++++++-- .../Authentication/JwtTokenValidation.cs | 16 +-- .../Authentication/JwtTokenExtractorTests.cs | 101 ++++++++++++++++++ 10 files changed, 185 insertions(+), 31 deletions(-) create mode 100644 libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs create mode 100644 tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs diff --git a/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs b/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs index 8433a51ce0..ca481c25cb 100644 --- a/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs +++ b/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs @@ -54,6 +54,7 @@ public class BotFrameworkAdapter : BotAdapter, IAdapterIntegration, IUserTokenPr private readonly RetryPolicy _connectorClientRetryPolicy; private readonly ILogger _logger; private readonly ConcurrentDictionary _appCredentialMap = new ConcurrentDictionary(); + private readonly AuthenticationConfiguration _authConfiguration; // There is a significant boost in throughput if we reuse a connectorClient // _connectorClients is a cache using [serviceUrl + appId]. @@ -69,6 +70,7 @@ public class BotFrameworkAdapter : BotAdapter, IAdapterIntegration, IUserTokenPr /// The HTTP client. /// The middleware to initially add to the adapter. /// The ILogger implementation this adapter should use. + /// The optional authentication configuration. /// /// is null. /// Use a object to add multiple middleware @@ -81,13 +83,15 @@ public BotFrameworkAdapter( RetryPolicy connectorClientRetryPolicy = null, HttpClient customHttpClient = null, IMiddleware middleware = null, - ILogger logger = null) + ILogger logger = null, + AuthenticationConfiguration authConfig = null) { _credentialProvider = credentialProvider ?? throw new ArgumentNullException(nameof(credentialProvider)); _channelProvider = channelProvider; _httpClient = customHttpClient ?? _defaultHttpClient; _connectorClientRetryPolicy = connectorClientRetryPolicy; _logger = logger ?? NullLogger.Instance; + _authConfiguration = authConfig; if (middleware != null) { @@ -211,7 +215,7 @@ public async Task ProcessActivityAsync(string authHeader, Activi { BotAssert.ActivityNotNull(activity); - var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _channelProvider, _httpClient).ConfigureAwait(false); + var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _channelProvider, _httpClient, _authConfiguration).ConfigureAwait(false); return await ProcessActivityAsync(claimsIdentity, activity, callback, cancellationToken).ConfigureAwait(false); } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs new file mode 100644 index 0000000000..77fd661f64 --- /dev/null +++ b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Bot.Connector.Authentication +{ + /// + /// General configuration settings for authentication. + /// + /// + /// Note that this is explicitly a class and not an interface, + /// since interfaces don't support default values, after the initial release any change would break backwards compatibility. + /// + public class AuthenticationConfiguration + { + public string[] RequiredEndorsements { get; set; } + } +} diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ChannelValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/ChannelValidation.cs index 8f368fa64c..78d17cad8e 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ChannelValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ChannelValidation.cs @@ -48,10 +48,11 @@ public static class ChannelValidation /// 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 ID of the channel to validate. + /// The optional authentication configuration. /// /// A valid ClaimsIdentity. /// - public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, HttpClient httpClient, string channelId) + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) { var tokenExtractor = new JwtTokenExtractor( httpClient, @@ -59,7 +60,7 @@ public static async Task AuthenticateChannelToken(string authHea OpenIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); - var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId); + var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig?.RequiredEndorsements); if (identity == null) { // No valid identity. Not Authorized. @@ -115,10 +116,11 @@ public static async Task AuthenticateChannelToken(string authHea /// 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 ID of the channel to validate. + /// The optional authentication configuration. /// ClaimsIdentity. - public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId) + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) { - var identity = await AuthenticateChannelToken(authHeader, credentials, httpClient, channelId); + var identity = await AuthenticateChannelToken(authHeader, credentials, httpClient, channelId, authConfig); var serviceUrlClaim = identity.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.ServiceUrlClaim)?.Value; if (string.IsNullOrWhiteSpace(serviceUrlClaim)) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/EmulatorValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/EmulatorValidation.cs index a800e403e1..a24eb470ba 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/EmulatorValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/EmulatorValidation.cs @@ -109,7 +109,7 @@ public static bool IsTokenFromEmulator(string authHeader) /// /// A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. /// - public static async Task AuthenticateEmulatorToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, HttpClient httpClient, string channelId) + public static async Task AuthenticateEmulatorToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) { var openIdMetadataUrl = (channelProvider != null && channelProvider.IsGovernment()) ? GovernmentAuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl : @@ -121,7 +121,7 @@ public static async Task AuthenticateEmulatorToken(string authHe openIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); - var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId); + var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig?.RequiredEndorsements); if (identity == null) { // No valid identity. Not Authorized. diff --git a/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs b/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs index ce2572a289..466e7b09b9 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs @@ -17,18 +17,18 @@ public static class EndorsementsValidator /// property is set to "webchat" and the signing party /// of the JWT token must have a corresponding endorsement of “Webchat”. /// - /// The ID of the channel to validate, typically extracted from the activity's + /// The expected endorsement. Generally the ID of the channel to validate, typically extracted from the activity's /// property, that to which the Activity is affinitized. /// The JWT token’s signing party is permitted to send activities only for /// specific channels. That list, the set of channels the service can sign for, is called the the endorsement list. /// The activity’s MUST be found in the endorsement list, or the incoming /// activity is not considered valid. /// True if the channel ID is found in the endorsements list; otherwise, false. - public static bool Validate(string channelId, HashSet endorsements) + public static bool Validate(string expectedEndorsement, HashSet endorsements) { // If the Activity came in and doesn't have a channel ID then it's making no // assertions as to who endorses it. This means it should pass. - if (string.IsNullOrEmpty(channelId)) + if (string.IsNullOrEmpty(expectedEndorsement)) { return true; } @@ -48,7 +48,7 @@ public static bool Validate(string channelId, HashSet endorsements) // JWTTokenExtractor // Does the set of endorsements match the channelId that was passed in? - var endorsementPresent = endorsements.Contains(channelId); + var endorsementPresent = endorsements.Contains(expectedEndorsement); return endorsementPresent; } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/EnterpriseChannelValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/EnterpriseChannelValidation.cs index 38d52dc327..e1a2713ba1 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/EnterpriseChannelValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/EnterpriseChannelValidation.cs @@ -40,7 +40,7 @@ public sealed class EnterpriseChannelValidation /// setup and teardown, so a shared HttpClient is recommended. /// The ID of the channel to validate. /// ClaimsIdentity. - public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string serviceUrl, HttpClient httpClient, string channelId) + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string serviceUrl, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) { var channelService = await channelProvider.GetChannelServiceAsync().ConfigureAwait(false); @@ -50,7 +50,7 @@ public static async Task AuthenticateChannelToken(string authHea string.Format(AuthenticationConstants.ToBotFromEnterpriseChannelOpenIdMetadataUrlFormat, channelService), AuthenticationConstants.AllowedSigningAlgorithms); - var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId).ConfigureAwait(false); + var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig?.RequiredEndorsements).ConfigureAwait(false); await ValidateIdentity(identity, credentials, serviceUrl).ConfigureAwait(false); diff --git a/libraries/Microsoft.Bot.Connector/Authentication/GovernmentChannelValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/GovernmentChannelValidation.cs index 50d76b70d1..bb885340e4 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/GovernmentChannelValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/GovernmentChannelValidation.cs @@ -41,7 +41,7 @@ public sealed class GovernmentChannelValidation /// setup and teardown, so a shared HttpClient is recommended. /// The ID of the channel to validate. /// ClaimsIdentity. - public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId) + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) { var tokenExtractor = new JwtTokenExtractor( httpClient, @@ -49,7 +49,7 @@ public static async Task AuthenticateChannelToken(string authHea OpenIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); - var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId).ConfigureAwait(false); + var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig?.RequiredEndorsements).ConfigureAwait(false); await ValidateIdentity(identity, credentials, serviceUrl).ConfigureAwait(false); diff --git a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs index d506d0a1e6..4f037a4518 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs @@ -61,26 +61,34 @@ public class JwtTokenExtractor /// tokenValidationParameters. /// metadataUrl. /// allowedSigningAlgorithms. - public JwtTokenExtractor(HttpClient httpClient, TokenValidationParameters tokenValidationParameters, string metadataUrl, HashSet allowedSigningAlgorithms) + /// Custom endorsement configuration to be used by the JwtTokenExtractor. + /// Custom OpenId connect configuration to be used by the JwtTokenExtractor. + public JwtTokenExtractor( + HttpClient httpClient, + TokenValidationParameters tokenValidationParameters, + string metadataUrl, + HashSet allowedSigningAlgorithms, + ConfigurationManager>> customEndorsementsConfig = null, + ConfigurationManager customOpenIdConnectConfig = null) { // Make our own copy so we can edit it _tokenValidationParameters = tokenValidationParameters.Clone(); _tokenValidationParameters.RequireSignedTokens = true; _allowedSigningAlgorithms = allowedSigningAlgorithms; - _openIdMetadata = _openIdMetadataCache.GetOrAdd(metadataUrl, key => + _openIdMetadata = customOpenIdConnectConfig ?? _openIdMetadataCache.GetOrAdd(metadataUrl, key => { return new ConfigurationManager(metadataUrl, new OpenIdConnectConfigurationRetriever(), httpClient); }); - _endorsementsData = _endorsementsCache.GetOrAdd(metadataUrl, key => + _endorsementsData = customEndorsementsConfig ?? _endorsementsCache.GetOrAdd(metadataUrl, key => { var retriever = new EndorsementsRetriever(httpClient); return new ConfigurationManager>>(metadataUrl, retriever, retriever); }); } - public async Task GetIdentityAsync(string authorizationHeader, string channelId) + public async Task GetIdentityAsync(string authorizationHeader, string channelId, string[] requiredEndorsements = null) { if (authorizationHeader == null) { @@ -90,13 +98,13 @@ public async Task GetIdentityAsync(string authorizationHeader, s string[] parts = authorizationHeader?.Split(' '); if (parts.Length == 2) { - return await GetIdentityAsync(parts[0], parts[1], channelId).ConfigureAwait(false); + return await GetIdentityAsync(parts[0], parts[1], channelId, requiredEndorsements).ConfigureAwait(false); } return null; } - public async Task GetIdentityAsync(string scheme, string parameter, string channelId) + public async Task GetIdentityAsync(string scheme, string parameter, string channelId, string[] requiredEndorsements = null) { // No header in correct scheme or no token if (scheme != "Bearer" || string.IsNullOrEmpty(parameter)) @@ -112,7 +120,7 @@ public async Task GetIdentityAsync(string scheme, string paramet try { - var claimsPrincipal = await ValidateTokenAsync(parameter, channelId).ConfigureAwait(false); + var claimsPrincipal = await ValidateTokenAsync(parameter, channelId, requiredEndorsements).ConfigureAwait(false); return claimsPrincipal.Identities.OfType().FirstOrDefault(); } catch (Exception e) @@ -124,7 +132,13 @@ public async Task GetIdentityAsync(string scheme, string paramet private bool HasAllowedIssuer(string jwtToken) { + if (!_tokenValidationParameters.ValidateIssuer) + { + return true; + } + JwtSecurityToken token = new JwtSecurityToken(jwtToken); + if (_tokenValidationParameters.ValidIssuer != null && _tokenValidationParameters.ValidIssuer == token.Issuer) { return true; @@ -138,7 +152,7 @@ private bool HasAllowedIssuer(string jwtToken) return false; } - private async Task ValidateTokenAsync(string jwtToken, string channelId) + private async Task ValidateTokenAsync(string jwtToken, string channelId, string[] requiredEndorsements = null) { // _openIdMetadata only does a full refresh when the cache expires every 5 days OpenIdConnectConfiguration config = null; @@ -161,6 +175,7 @@ private async Task ValidateTokenAsync(string jwtToken, string c _tokenValidationParameters.IssuerSigningKeys = config.SigningKeys; var tokenHandler = new JwtSecurityTokenHandler(); + try { var principal = tokenHandler.ValidateToken(jwtToken, _tokenValidationParameters, out SecurityToken parsedToken); @@ -175,11 +190,23 @@ private async Task ValidateTokenAsync(string jwtToken, string c // below won't run. This is normal. if (!string.IsNullOrEmpty(keyId) && endorsements.TryGetValue(keyId, out var endorsementsForKey)) { + // Verify that channelId is included in endorsements var isEndorsed = EndorsementsValidator.Validate(channelId, endorsementsForKey); + if (!isEndorsed) { throw new UnauthorizedAccessException($"Could not validate endorsement for key: {keyId} with endorsements: {string.Join(",", endorsementsForKey)}"); } + + // Verify that additional endorsements are satisfied. If no additional endorsements are expected, the requirement is satisfied as well + var additionalEndorsementsSatisfied = requiredEndorsements == null ? true : + requiredEndorsements.All( + endorsement => EndorsementsValidator.Validate(endorsement, endorsementsForKey)); + + if (!additionalEndorsementsSatisfied) + { + throw new UnauthorizedAccessException($"Could not validate additional endorsement for key: {keyId} with endorsements: {string.Join(",", endorsementsForKey)}. Expected endorsements: {string.Join(",", requiredEndorsements)}"); + } } if (_allowedSigningAlgorithms != null) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs index c52157cac7..a48002b510 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs @@ -27,10 +27,11 @@ public static class JwtTokenValidation /// The bot's credential provider. /// The bot's channel service provider. /// The HTTP client. + /// The optional authentication configuration. /// A task that represents the work queued to execute. /// If the task completes successfully, the result contains the claims-based /// identity for the request. - public static async Task AuthenticateRequest(IActivity activity, string authHeader, ICredentialProvider credentials, IChannelProvider provider, HttpClient httpClient = null) + public static async Task AuthenticateRequest(IActivity activity, string authHeader, ICredentialProvider credentials, IChannelProvider provider, HttpClient httpClient = null, AuthenticationConfiguration authConfig = null) { if (string.IsNullOrWhiteSpace(authHeader)) { @@ -47,7 +48,7 @@ public static async Task AuthenticateRequest(IActivity activity, throw new UnauthorizedAccessException(); } - var claimsIdentity = await ValidateAuthHeader(authHeader, credentials, provider, activity.ChannelId, activity.ServiceUrl, httpClient ?? _httpClient); + var claimsIdentity = await ValidateAuthHeader(authHeader, credentials, provider, activity.ChannelId, activity.ServiceUrl, httpClient ?? _httpClient, authConfig); MicrosoftAppCredentials.TrustServiceUrl(activity.ServiceUrl); @@ -63,10 +64,11 @@ public static async Task AuthenticateRequest(IActivity activity, /// The ID of the channel that sent the request. /// The service URL for the activity. /// The HTTP client. + /// The optional authentication configuration. /// A task that represents the work queued to execute. /// If the task completes successfully, the result contains the claims-based /// identity for the request. - public static async Task ValidateAuthHeader(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string channelId, string serviceUrl = null, HttpClient httpClient = null) + public static async Task ValidateAuthHeader(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string channelId, string serviceUrl = null, HttpClient httpClient = null, AuthenticationConfiguration authConfig = null) { if (string.IsNullOrEmpty(authHeader)) { @@ -84,20 +86,20 @@ public static async Task ValidateAuthHeader(string authHeader, I // No empty or null check. Empty can point to issues. Null checks only. if (serviceUrl != null) { - return await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient ?? _httpClient, channelId); + return await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient ?? _httpClient, channelId, authConfig); } else { - return await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, httpClient ?? _httpClient, channelId); + return await ChannelValidation.AuthenticateChannelToken(authHeader, credentials, httpClient ?? _httpClient, channelId, authConfig); } } else if (channelProvider.IsGovernment()) { - return await GovernmentChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient ?? _httpClient, channelId).ConfigureAwait(false); + return await GovernmentChannelValidation.AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient ?? _httpClient, channelId, authConfig).ConfigureAwait(false); } else { - return await EnterpriseChannelValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, serviceUrl, httpClient ?? _httpClient, channelId).ConfigureAwait(false); + return await EnterpriseChannelValidation.AuthenticateChannelToken(authHeader, credentials, channelProvider, serviceUrl, httpClient ?? _httpClient, channelId, authConfig).ConfigureAwait(false); } } } diff --git a/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs b/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs new file mode 100644 index 0000000000..595a20fede --- /dev/null +++ b/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Schema; +using Microsoft.IdentityModel.Protocols; +using Xunit; + +namespace Microsoft.Bot.Connector.Authentication.Tests +{ + public class TestConfigurationRetriever : IConfigurationRetriever>> + { + public readonly Dictionary> EndorsementTable = new Dictionary>(); + + public async Task>> GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel) + { + return EndorsementTable; + } + } + + public class JwtTokenExtractorTests + { + private readonly HttpClient client; + private readonly HttpClient emptyClient; + + private const string keyId = "CtfQC8Le-8NsC7oC2zQkZpcrfOc"; + private const string testChannelName = "testChannel"; + private const string complianceEndorsement = "o365Compliant"; + private const string randomEndorsement = "2112121212"; + + public JwtTokenExtractorTests() + { + ChannelValidation.ToBotFromChannelTokenValidationParameters.ValidateLifetime = false; + ChannelValidation.ToBotFromChannelTokenValidationParameters.ValidateIssuer = false; + + client = new HttpClient + { + BaseAddress = new Uri("https://webchat.botframework.com/"), + }; + emptyClient = new HttpClient(); + } + + [Fact] + public async Task Connector_TokenExtractor_NullRequiredEndorsements_ShouldValidate() + { + var configRetriever = new TestConfigurationRetriever(); + + configRetriever.EndorsementTable.Add(keyId, new HashSet() { randomEndorsement, complianceEndorsement, testChannelName}); + var claimsIdentity = await RunTestCase(configRetriever); + Assert.True(claimsIdentity.IsAuthenticated); + } + + [Fact] + public async Task Connector_TokenExtractor_EmptyRequireEndorsements_ShouldValidate() + { + var configRetriever = new TestConfigurationRetriever(); + + configRetriever.EndorsementTable.Add(keyId, new HashSet() { randomEndorsement, complianceEndorsement, testChannelName }); + var claimsIdentity = await RunTestCase(configRetriever, new string[] { }); + Assert.True(claimsIdentity.IsAuthenticated); + } + + [Fact] + public async Task Connector_TokenExtractor_RequiredEndorsementsPresent_ShouldValidate() + { + var configRetriever = new TestConfigurationRetriever(); + + configRetriever.EndorsementTable.Add(keyId, new HashSet() { randomEndorsement, complianceEndorsement, testChannelName }); + var claimsIdentity = await RunTestCase(configRetriever, new string[] { complianceEndorsement }); + Assert.True(claimsIdentity.IsAuthenticated); + } + + [Fact] + public async Task Connector_TokenExtractor_RequiredEndorsementsPartiallyPresent_ShouldNotValidate() + { + var configRetriever = new TestConfigurationRetriever(); + + configRetriever.EndorsementTable.Add(keyId, new HashSet() { randomEndorsement, complianceEndorsement, testChannelName }); + await Assert.ThrowsAsync(async () => await RunTestCase(configRetriever, new string[] { complianceEndorsement, "notSatisfiedEndorsement" })); + } + + private async Task RunTestCase(IConfigurationRetriever>> configRetriever, string[] requiredEndorsements = null) + { + var tokenExtractor = new JwtTokenExtractor( + emptyClient, + EmulatorValidation.ToBotFromEmulatorTokenValidationParameters, + AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl, + AuthenticationConstants.AllowedSigningAlgorithms, + new ConfigurationManager>>("http://test", configRetriever)); + + string header = $"Bearer {await new MicrosoftAppCredentials("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F").GetTokenAsync()}"; + + return await tokenExtractor.GetIdentityAsync(header, "testChannel", requiredEndorsements); + } + } +} From dda6adcd97874231b4ceacd9203069ef06621443 Mon Sep 17 00:00:00 2001 From: Carlos Castro Date: Thu, 6 Jun 2019 15:15:30 -0700 Subject: [PATCH 2/5] Endorsement Validation: Update comments to reflet the current behavior. --- .../Authentication/EndorsementsValidator.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs b/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs index 466e7b09b9..4aa81eab2d 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs @@ -12,11 +12,16 @@ namespace Microsoft.Bot.Connector.Authentication public static class EndorsementsValidator { /// - /// Verify that a channel matches the endorsements found on the JWT token. + /// Verify that the specified endorsement exists on the JWT token. Call this method multiple times to validate multiple endorsements. /// For example, if an comes from WebChat, that activity's /// property is set to "webchat" and the signing party /// of the JWT token must have a corresponding endorsement of “Webchat”. /// + /// + /// JWT token signing keys contain endorsements matching the IDs of the channels they are approved to sign for. + /// They also contain keywords representing compliance certifications. This code ensures that a channel ID or compliance + /// certification is present on the signing key used for the request's token. + /// /// The expected endorsement. Generally the ID of the channel to validate, typically extracted from the activity's /// property, that to which the Activity is affinitized. /// The JWT token’s signing party is permitted to send activities only for From 5ddeb2c8d8dea4d16d10ebe33cb6a68310c60184 Mon Sep 17 00:00:00 2001 From: Carlos Castro Date: Thu, 6 Jun 2019 15:15:42 -0700 Subject: [PATCH 3/5] Endorsements: Improve comments --- .../Authentication/EndorsementsValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs b/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs index 4aa81eab2d..aaf695698e 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/EndorsementsValidator.cs @@ -23,7 +23,7 @@ public static class EndorsementsValidator /// certification is present on the signing key used for the request's token. /// /// The expected endorsement. Generally the ID of the channel to validate, typically extracted from the activity's - /// property, that to which the Activity is affinitized. + /// property, that to which the Activity is affinitized. Alternatively, it could represent a compliance certification that is required. /// The JWT token’s signing party is permitted to send activities only for /// specific channels. That list, the set of channels the service can sign for, is called the the endorsement list. /// The activity’s MUST be found in the endorsement list, or the incoming From 491aa73759b2b1cc8be6960419eea8dab81423a2 Mon Sep 17 00:00:00 2001 From: Carlos Castro Date: Fri, 7 Jun 2019 10:11:16 -0700 Subject: [PATCH 4/5] Auth Endorsement validation: Binary compatibility and Pr comments --- .../BotFrameworkAdapter.cs | 37 +++++++-- .../AuthenticationConfiguration.cs | 7 +- .../Authentication/ChannelValidation.cs | 56 ++++++++++++-- .../Authentication/EmulatorValidation.cs | 31 +++++++- .../EnterpriseChannelValidation.cs | 27 ++++++- .../GovernmentChannelValidation.cs | 26 ++++++- .../Authentication/JwtTokenExtractor.cs | 76 ++++++++++++++++--- .../Authentication/JwtTokenValidation.cs | 52 ++++++++++++- .../Authentication/JwtTokenExtractorTests.cs | 6 +- 9 files changed, 279 insertions(+), 39 deletions(-) diff --git a/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs b/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs index ca481c25cb..2e1806859a 100644 --- a/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs +++ b/libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs @@ -70,7 +70,6 @@ public class BotFrameworkAdapter : BotAdapter, IAdapterIntegration, IUserTokenPr /// The HTTP client. /// The middleware to initially add to the adapter. /// The ILogger implementation this adapter should use. - /// The optional authentication configuration. /// /// is null. /// Use a object to add multiple middleware @@ -83,15 +82,43 @@ public BotFrameworkAdapter( RetryPolicy connectorClientRetryPolicy = null, HttpClient customHttpClient = null, IMiddleware middleware = null, - ILogger logger = null, - AuthenticationConfiguration authConfig = null) + ILogger logger = null) + : this(credentialProvider, new AuthenticationConfiguration(), channelProvider, connectorClientRetryPolicy, customHttpClient, middleware, logger) + { + } + + /// + /// Initializes a new instance of the class, + /// using a credential provider. + /// + /// The credential provider. + /// The authentication configuration. + /// The channel provider. + /// Retry policy for retrying HTTP operations. + /// The HTTP client. + /// The middleware to initially add to the adapter. + /// The ILogger implementation this adapter should use. + /// + /// is null. + /// Use a object to add multiple middleware + /// components in the constructor. Use the method to + /// add additional middleware to the adapter after construction. + /// + public BotFrameworkAdapter( + ICredentialProvider credentialProvider, + AuthenticationConfiguration authConfig, + IChannelProvider channelProvider = null, + RetryPolicy connectorClientRetryPolicy = null, + HttpClient customHttpClient = null, + IMiddleware middleware = null, + ILogger logger = null) { _credentialProvider = credentialProvider ?? throw new ArgumentNullException(nameof(credentialProvider)); _channelProvider = channelProvider; _httpClient = customHttpClient ?? _defaultHttpClient; _connectorClientRetryPolicy = connectorClientRetryPolicy; _logger = logger ?? NullLogger.Instance; - _authConfiguration = authConfig; + _authConfiguration = authConfig ?? throw new ArgumentNullException(nameof(authConfig)); if (middleware != null) { @@ -215,7 +242,7 @@ public async Task ProcessActivityAsync(string authHeader, Activi { BotAssert.ActivityNotNull(activity); - var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _channelProvider, _httpClient, _authConfiguration).ConfigureAwait(false); + var claimsIdentity = await JwtTokenValidation.AuthenticateRequest(activity, authHeader, _credentialProvider, _channelProvider, _authConfiguration, _httpClient).ConfigureAwait(false); return await ProcessActivityAsync(claimsIdentity, activity, callback, cancellationToken).ConfigureAwait(false); } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs index 77fd661f64..a54131481d 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/AuthenticationConfiguration.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. namespace Microsoft.Bot.Connector.Authentication { @@ -13,6 +12,6 @@ namespace Microsoft.Bot.Connector.Authentication /// public class AuthenticationConfiguration { - public string[] RequiredEndorsements { get; set; } + public string[] RequiredEndorsements { get; set; } = new string[] { }; } } diff --git a/libraries/Microsoft.Bot.Connector/Authentication/ChannelValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/ChannelValidation.cs index 78d17cad8e..596025d858 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/ChannelValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/ChannelValidation.cs @@ -48,19 +48,44 @@ public static class ChannelValidation /// 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 ID of the channel to validate. - /// The optional authentication configuration. /// /// A valid ClaimsIdentity. /// - public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, HttpClient httpClient, string channelId) { + return await AuthenticateChannelToken(authHeader, credentials, httpClient, channelId, new AuthenticationConfiguration()).ConfigureAwait(false); + } + + /// + /// Validate the incoming Auth Header as a token sent from the Bot Framework Service. + /// + /// + /// A token issued by the Bot Framework emulator will FAIL this check. + /// + /// 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 ID of the channel to validate. + /// The authentication configuration. + /// + /// A valid ClaimsIdentity. + /// + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig) + { + if (authConfig == null) + { + throw new ArgumentNullException(nameof(authConfig)); + } + var tokenExtractor = new JwtTokenExtractor( httpClient, ToBotFromChannelTokenValidationParameters, OpenIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); - var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig?.RequiredEndorsements); + var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig.RequiredEndorsements); if (identity == null) { // No valid identity. Not Authorized. @@ -116,10 +141,31 @@ public static async Task AuthenticateChannelToken(string authHea /// 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 ID of the channel to validate. - /// The optional authentication configuration. /// ClaimsIdentity. - public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId) { + return await AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient, channelId, new AuthenticationConfiguration()).ConfigureAwait(false); + } + + /// + /// Validate the incoming Auth Header as a token sent from the Bot Framework Service. + /// + /// The raw HTTP header in the format: "Bearer [longString]". + /// The user defined set of valid credentials, such as the AppId. + /// Service url. + /// 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 ID of the channel to validate. + /// The authentication configuration. + /// ClaimsIdentity. + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig) + { + if (authConfig == null) + { + throw new ArgumentNullException(nameof(authConfig)); + } + var identity = await AuthenticateChannelToken(authHeader, credentials, httpClient, channelId, authConfig); var serviceUrlClaim = identity.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.ServiceUrlClaim)?.Value; diff --git a/libraries/Microsoft.Bot.Connector/Authentication/EmulatorValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/EmulatorValidation.cs index a24eb470ba..7e75663269 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/EmulatorValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/EmulatorValidation.cs @@ -109,8 +109,35 @@ public static bool IsTokenFromEmulator(string authHeader) /// /// A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. /// - public static async Task AuthenticateEmulatorToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) + public static async Task AuthenticateEmulatorToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, HttpClient httpClient, string channelId) { + return await AuthenticateEmulatorToken(authHeader, credentials, channelProvider, httpClient, channelId, new AuthenticationConfiguration()).ConfigureAwait(false); + } + + /// + /// Validate the incoming Auth Header as a token sent from the Bot Framework Emulator. + /// + /// The raw HTTP header in the format: "Bearer [longString]". + /// The user defined set of valid credentials, such as the AppId. + /// The channelService value that distinguishes public Azure from US Government Azure. + /// 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 ID of the channel to validate. + /// The authentication configuration. + /// + /// A valid ClaimsIdentity. + /// + /// + /// A token issued by the Bot Framework will FAIL this check. Only Emulator tokens will pass. + /// + public static async Task AuthenticateEmulatorToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig) + { + if (authConfig == null) + { + throw new ArgumentNullException(nameof(authConfig)); + } + var openIdMetadataUrl = (channelProvider != null && channelProvider.IsGovernment()) ? GovernmentAuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl : AuthenticationConstants.ToBotFromEmulatorOpenIdMetadataUrl; @@ -121,7 +148,7 @@ public static async Task AuthenticateEmulatorToken(string authHe openIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); - var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig?.RequiredEndorsements); + var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig.RequiredEndorsements); if (identity == null) { // No valid identity. Not Authorized. diff --git a/libraries/Microsoft.Bot.Connector/Authentication/EnterpriseChannelValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/EnterpriseChannelValidation.cs index e1a2713ba1..b5bd04de21 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/EnterpriseChannelValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/EnterpriseChannelValidation.cs @@ -40,8 +40,31 @@ public sealed class EnterpriseChannelValidation /// setup and teardown, so a shared HttpClient is recommended. /// The ID of the channel to validate. /// ClaimsIdentity. - public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string serviceUrl, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string serviceUrl, HttpClient httpClient, string channelId) { + return await AuthenticateChannelToken(authHeader, credentials, channelProvider, serviceUrl, httpClient, channelId, new AuthenticationConfiguration()).ConfigureAwait(false); + } + + /// + /// Validate the incoming Auth Header as a token sent from a Bot Framework Channel Service. + /// + /// The raw HTTP header in the format: "Bearer [longString]". + /// The user defined set of valid credentials, such as the AppId. + /// The user defined configuration for the channel. + /// The service url from the request. + /// 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 ID of the channel to validate. + /// The authentication configuration. + /// ClaimsIdentity. + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string serviceUrl, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig) + { + if (authConfig == null) + { + throw new ArgumentNullException(nameof(authConfig)); + } + var channelService = await channelProvider.GetChannelServiceAsync().ConfigureAwait(false); var tokenExtractor = new JwtTokenExtractor( @@ -50,7 +73,7 @@ public static async Task AuthenticateChannelToken(string authHea string.Format(AuthenticationConstants.ToBotFromEnterpriseChannelOpenIdMetadataUrlFormat, channelService), AuthenticationConstants.AllowedSigningAlgorithms); - var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig?.RequiredEndorsements).ConfigureAwait(false); + var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig.RequiredEndorsements).ConfigureAwait(false); await ValidateIdentity(identity, credentials, serviceUrl).ConfigureAwait(false); diff --git a/libraries/Microsoft.Bot.Connector/Authentication/GovernmentChannelValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/GovernmentChannelValidation.cs index bb885340e4..70e77d6540 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/GovernmentChannelValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/GovernmentChannelValidation.cs @@ -41,15 +41,37 @@ public sealed class GovernmentChannelValidation /// setup and teardown, so a shared HttpClient is recommended. /// The ID of the channel to validate. /// ClaimsIdentity. - public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig = null) + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId) { + return await AuthenticateChannelToken(authHeader, credentials, serviceUrl, httpClient, channelId, new AuthenticationConfiguration()).ConfigureAwait(false); + } + + /// + /// Validate the incoming Auth Header as a token sent from a Bot Framework Government Channel Service. + /// + /// The raw HTTP header in the format: "Bearer [longString]". + /// The user defined set of valid credentials, such as the AppId. + /// The service url from the request. + /// 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 ID of the channel to validate. + /// The authentication configuration. + /// ClaimsIdentity. + public static async Task AuthenticateChannelToken(string authHeader, ICredentialProvider credentials, string serviceUrl, HttpClient httpClient, string channelId, AuthenticationConfiguration authConfig) + { + if (authConfig == null) + { + throw new ArgumentNullException(nameof(authConfig)); + } + var tokenExtractor = new JwtTokenExtractor( httpClient, ToBotFromGovernmentChannelTokenValidationParameters, OpenIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms); - var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig?.RequiredEndorsements).ConfigureAwait(false); + var identity = await tokenExtractor.GetIdentityAsync(authHeader, channelId, authConfig.RequiredEndorsements).ConfigureAwait(false); await ValidateIdentity(identity, credentials, serviceUrl).ConfigureAwait(false); diff --git a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs index 4f037a4518..01baec684c 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs @@ -61,34 +61,67 @@ public class JwtTokenExtractor /// tokenValidationParameters. /// metadataUrl. /// allowedSigningAlgorithms. - /// Custom endorsement configuration to be used by the JwtTokenExtractor. - /// Custom OpenId connect configuration to be used by the JwtTokenExtractor. public JwtTokenExtractor( HttpClient httpClient, TokenValidationParameters tokenValidationParameters, string metadataUrl, - HashSet allowedSigningAlgorithms, - ConfigurationManager>> customEndorsementsConfig = null, - ConfigurationManager customOpenIdConnectConfig = null) + HashSet allowedSigningAlgorithms) { // Make our own copy so we can edit it _tokenValidationParameters = tokenValidationParameters.Clone(); _tokenValidationParameters.RequireSignedTokens = true; _allowedSigningAlgorithms = allowedSigningAlgorithms; - _openIdMetadata = customOpenIdConnectConfig ?? _openIdMetadataCache.GetOrAdd(metadataUrl, key => + _openIdMetadata = _openIdMetadataCache.GetOrAdd(metadataUrl, key => { return new ConfigurationManager(metadataUrl, new OpenIdConnectConfigurationRetriever(), httpClient); }); - _endorsementsData = customEndorsementsConfig ?? _endorsementsCache.GetOrAdd(metadataUrl, key => + _endorsementsData = _endorsementsCache.GetOrAdd(metadataUrl, key => { var retriever = new EndorsementsRetriever(httpClient); return new ConfigurationManager>>(metadataUrl, retriever, retriever); }); } - public async Task GetIdentityAsync(string authorizationHeader, string channelId, string[] requiredEndorsements = null) + /// + /// Initializes a new instance of the class. + /// Extracts relevant data from JWT Tokens. + /// + /// As part of validating JWT Tokens, endorsements need to be feteched from + /// sources specified by the relevant security URLs. This HttpClient is used to allow for resource + /// pooling around those retrievals. As those resources require TLS sharing the HttpClient is + /// important to overall perfomance. + /// tokenValidationParameters. + /// metadataUrl. + /// allowedSigningAlgorithms. + /// Custom endorsement configuration to be used by the JwtTokenExtractor. + public JwtTokenExtractor( + HttpClient httpClient, + TokenValidationParameters tokenValidationParameters, + string metadataUrl, + HashSet allowedSigningAlgorithms, + ConfigurationManager>> customEndorsementsConfig) + { + // Make our own copy so we can edit it + _tokenValidationParameters = tokenValidationParameters.Clone(); + _tokenValidationParameters.RequireSignedTokens = true; + _allowedSigningAlgorithms = allowedSigningAlgorithms; + + _openIdMetadata = _openIdMetadataCache.GetOrAdd(metadataUrl, key => + { + return new ConfigurationManager(metadataUrl, new OpenIdConnectConfigurationRetriever(), httpClient); + }); + + _endorsementsData = customEndorsementsConfig ?? throw new ArgumentNullException(nameof(customEndorsementsConfig)); + } + + public async Task GetIdentityAsync(string authorizationHeader, string channelId) + { + return await GetIdentityAsync(authorizationHeader, channelId, new string[] { }).ConfigureAwait(false); + } + + public async Task GetIdentityAsync(string authorizationHeader, string channelId, string[] requiredEndorsements) { if (authorizationHeader == null) { @@ -104,8 +137,18 @@ public async Task GetIdentityAsync(string authorizationHeader, s return null; } - public async Task GetIdentityAsync(string scheme, string parameter, string channelId, string[] requiredEndorsements = null) + public async Task GetIdentityAsync(string scheme, string parameter, string channelId) + { + return await GetIdentityAsync(scheme, parameter, channelId, new string[] { }).ConfigureAwait(false); + } + + public async Task GetIdentityAsync(string scheme, string parameter, string channelId, string[] requiredEndorsements) { + if (requiredEndorsements == null) + { + throw new ArgumentNullException(nameof(requiredEndorsements)); + } + // No header in correct scheme or no token if (scheme != "Bearer" || string.IsNullOrEmpty(parameter)) { @@ -152,8 +195,18 @@ private bool HasAllowedIssuer(string jwtToken) return false; } - private async Task ValidateTokenAsync(string jwtToken, string channelId, string[] requiredEndorsements = null) + private async Task ValidateTokenAsync(string jwtToken, string channelId) { + return await ValidateTokenAsync(jwtToken, channelId, new string[] { }).ConfigureAwait(false); + } + + private async Task ValidateTokenAsync(string jwtToken, string channelId, string[] requiredEndorsements) + { + if (requiredEndorsements == null) + { + throw new ArgumentNullException(nameof(requiredEndorsements)); + } + // _openIdMetadata only does a full refresh when the cache expires every 5 days OpenIdConnectConfiguration config = null; try @@ -199,8 +252,7 @@ private async Task ValidateTokenAsync(string jwtToken, string c } // Verify that additional endorsements are satisfied. If no additional endorsements are expected, the requirement is satisfied as well - var additionalEndorsementsSatisfied = requiredEndorsements == null ? true : - requiredEndorsements.All( + var additionalEndorsementsSatisfied = requiredEndorsements.All( endorsement => EndorsementsValidator.Validate(endorsement, endorsementsForKey)); if (!additionalEndorsementsSatisfied) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs index a48002b510..63d40d79f1 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenValidation.cs @@ -27,12 +27,34 @@ public static class JwtTokenValidation /// The bot's credential provider. /// The bot's channel service provider. /// The HTTP client. + /// A task that represents the work queued to execute. + /// If the task completes successfully, the result contains the claims-based + /// identity for the request. + public static async Task AuthenticateRequest(IActivity activity, string authHeader, ICredentialProvider credentials, IChannelProvider provider, HttpClient httpClient = null) + { + return await AuthenticateRequest(activity, authHeader, credentials, provider, new AuthenticationConfiguration(), httpClient); + } + + /// + /// Authenticates the request and add's the activity's + /// to the set of trusted URLs. + /// + /// The activity. + /// The authentication header. + /// The bot's credential provider. + /// The bot's channel service provider. /// The optional authentication configuration. + /// The HTTP client. /// A task that represents the work queued to execute. /// If the task completes successfully, the result contains the claims-based /// identity for the request. - public static async Task AuthenticateRequest(IActivity activity, string authHeader, ICredentialProvider credentials, IChannelProvider provider, HttpClient httpClient = null, AuthenticationConfiguration authConfig = null) + public static async Task AuthenticateRequest(IActivity activity, string authHeader, ICredentialProvider credentials, IChannelProvider provider, AuthenticationConfiguration authConfig, HttpClient httpClient = null) { + if (authConfig == null) + { + throw new ArgumentNullException(nameof(authConfig)); + } + if (string.IsNullOrWhiteSpace(authHeader)) { bool isAuthDisabled = await credentials.IsAuthenticationDisabledAsync().ConfigureAwait(false); @@ -48,7 +70,7 @@ public static async Task AuthenticateRequest(IActivity activity, throw new UnauthorizedAccessException(); } - var claimsIdentity = await ValidateAuthHeader(authHeader, credentials, provider, activity.ChannelId, activity.ServiceUrl, httpClient ?? _httpClient, authConfig); + var claimsIdentity = await ValidateAuthHeader(authHeader, credentials, provider, activity.ChannelId, authConfig, activity.ServiceUrl, httpClient ?? _httpClient); MicrosoftAppCredentials.TrustServiceUrl(activity.ServiceUrl); @@ -64,17 +86,39 @@ public static async Task AuthenticateRequest(IActivity activity, /// The ID of the channel that sent the request. /// The service URL for the activity. /// The HTTP client. - /// The optional authentication configuration. /// A task that represents the work queued to execute. /// If the task completes successfully, the result contains the claims-based /// identity for the request. - public static async Task ValidateAuthHeader(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string channelId, string serviceUrl = null, HttpClient httpClient = null, AuthenticationConfiguration authConfig = null) + public static async Task ValidateAuthHeader(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string channelId, string serviceUrl = null, HttpClient httpClient = null) + { + return await ValidateAuthHeader(authHeader, credentials, channelProvider, channelId, new AuthenticationConfiguration(), serviceUrl, httpClient).ConfigureAwait(false); + } + + /// + /// Validates the authentication header of an incoming request. + /// + /// The authentication header to validate. + /// The bot's credential provider. + /// The bot's channel service provider. + /// The ID of the channel that sent the request. + /// The authentication configuration. + /// The service URL for the activity. + /// The HTTP client. + /// A task that represents the work queued to execute. + /// If the task completes successfully, the result contains the claims-based + /// identity for the request. + public static async Task ValidateAuthHeader(string authHeader, ICredentialProvider credentials, IChannelProvider channelProvider, string channelId, AuthenticationConfiguration authConfig, string serviceUrl = null, HttpClient httpClient = null) { if (string.IsNullOrEmpty(authHeader)) { throw new ArgumentNullException(nameof(authHeader)); } + if (authConfig == null) + { + throw new ArgumentNullException(nameof(authConfig)); + } + bool usingEmulator = EmulatorValidation.IsTokenFromEmulator(authHeader); if (usingEmulator) diff --git a/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs b/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs index 595a20fede..54b8e1bca0 100644 --- a/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs +++ b/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.Bot.Schema; using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Xunit; namespace Microsoft.Bot.Connector.Authentication.Tests @@ -46,13 +47,12 @@ public JwtTokenExtractorTests() } [Fact] - public async Task Connector_TokenExtractor_NullRequiredEndorsements_ShouldValidate() + public async Task Connector_TokenExtractor_NullRequiredEndorsements_ShouldFail() { var configRetriever = new TestConfigurationRetriever(); configRetriever.EndorsementTable.Add(keyId, new HashSet() { randomEndorsement, complianceEndorsement, testChannelName}); - var claimsIdentity = await RunTestCase(configRetriever); - Assert.True(claimsIdentity.IsAuthenticated); + await Assert.ThrowsAsync(async () => await RunTestCase(configRetriever)); } [Fact] From 0f66b75b360b562fa588c5d0df895967b6910402 Mon Sep 17 00:00:00 2001 From: Carlos Castro Date: Fri, 7 Jun 2019 10:54:35 -0700 Subject: [PATCH 5/5] Updating publish to coveralls for build --- build/PublishToCoveralls.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/PublishToCoveralls.ps1 b/build/PublishToCoveralls.ps1 index 788cb76af4..8f599aef27 100644 --- a/build/PublishToCoveralls.ps1 +++ b/build/PublishToCoveralls.ps1 @@ -9,7 +9,7 @@ Param( Write-Host Install tools $basePath = (get-item $pathToCoverageFiles ).parent.FullName -$coverageAnalyzer = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Team Tools\Dynamic Code Coverage Tools\CodeCoverage.exe" +$coverageAnalyzer = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Team Tools\Dynamic Code Coverage Tools\CodeCoverage.exe" dotnet tool install coveralls.net --version 1.0.0 --tool-path tools $coverageUploader = ".\tools\csmacnz.Coveralls.exe"