From 071b122da6140ccd206a7d6a4dc2a8682cad2b07 Mon Sep 17 00:00:00 2001 From: sw-joelmut Date: Tue, 6 Dec 2022 11:03:34 -0300 Subject: [PATCH 1/2] Add UnauthorizedAccessException when the SecurityTokenExpiredException is thrown --- .../Authentication/JwtTokenExtractor.cs | 5 ++++ .../Authentication/JwtTokenExtractorTests.cs | 28 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs index 357b613806..ee791dbe8a 100644 --- a/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs +++ b/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs @@ -297,6 +297,11 @@ private async Task ValidateTokenAsync(string jwtToken, string c return principal; } + catch (SecurityTokenExpiredException err) + { + Trace.TraceError(err.Message); + throw new UnauthorizedAccessException($"The token has expired"); + } catch (SecurityTokenSignatureKeyNotFoundException) { var keys = string.Join(", ", (config?.SigningKeys ?? Enumerable.Empty()).Select(t => t.KeyId)); diff --git a/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs b/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs index c3808843b5..0a56e908e4 100644 --- a/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs +++ b/tests/Microsoft.Bot.Connector.Tests/Authentication/JwtTokenExtractorTests.cs @@ -91,7 +91,26 @@ public async Task JwtTokenExtractor_WithValidCert_ShouldNotAllowCertSigningKey() } } - private static Task BuildExtractorAndValidateToken(X509Certificate2 cert, TokenValidationParameters validationParameters = null) + [Fact] + [Trait("TestCategory", "WindowsOnly")] + public async Task JwtTokenExtractor_WithExpiredToken_ShouldThrowUnauthorizedAccessException() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var now = DateTimeOffset.UtcNow; + var cn = "test.cert.botframework.com"; + + // Create valid self-signed certificate + var cert = CreateSelfSignedCertificate(cn, from: now.AddDays(-10), to: now.AddDays(9)); + + // It will fail since the generated token is expired. + await Assert.ThrowsAnyAsync(() => BuildExtractorAndValidateToken(cert, expires: now.Date.AddDays(-5), notBefore: now.Date.AddDays(-10))); + + DeleteKeyContainer(cn); + } + } + + private static Task BuildExtractorAndValidateToken(X509Certificate2 cert, TokenValidationParameters validationParameters = null, DateTime? expires = null, DateTime? notBefore = null) { // Custom validation parameters that allow us to test the extractor logic var tokenValidationParams = validationParameters ?? CreateTokenValidationParameters(cert); @@ -103,17 +122,18 @@ private static Task BuildExtractorAndValidateToken(X509Certifica "https://login.botframework.com/v1/.well-known/openidconfiguration", AuthenticationConstants.AllowedSigningAlgorithms); - var token = CreateTokenForCertificate(cert); + var token = CreateTokenForCertificate(cert, expires, notBefore); return tokenExtractor.GetIdentityAsync($"Bearer {token}", "test"); } - private static string CreateTokenForCertificate(X509Certificate2 cert) + private static string CreateTokenForCertificate(X509Certificate2 cert, DateTime? expires = null, DateTime? notBefore = null) { var tokenHandler = new JwtSecurityTokenHandler(); var tokenDescriptor = new SecurityTokenDescriptor { - Expires = DateTime.UtcNow.AddMinutes(5), + Expires = expires ?? DateTime.UtcNow.AddMinutes(5), + NotBefore = notBefore, SigningCredentials = new SigningCredentials(new X509SecurityKey(cert), SecurityAlgorithms.RsaSha256Signature) }; From cd7fc862063c3d35448ec63f80886333689ba35a Mon Sep 17 00:00:00 2001 From: sw-joelmut Date: Tue, 6 Dec 2022 17:49:34 -0300 Subject: [PATCH 2/2] Add CloudAdapter test --- .../CloudAdapterTests.cs | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/tests/integration/Microsoft.Bot.Builder.Integration.AspNet.Core.Tests/CloudAdapterTests.cs b/tests/integration/Microsoft.Bot.Builder.Integration.AspNet.Core.Tests/CloudAdapterTests.cs index a90ca63521..d1244a3c33 100644 --- a/tests/integration/Microsoft.Bot.Builder.Integration.AspNet.Core.Tests/CloudAdapterTests.cs +++ b/tests/integration/Microsoft.Bot.Builder.Integration.AspNet.Core.Tests/CloudAdapterTests.cs @@ -805,6 +805,36 @@ public async Task CloudAdapterCreateConversation() Assert.Equal(expectedChannelId, actualChannelId); } + [Fact] + public async Task ExpiredTokenShouldThrowUnauthorizedAccessException() + { + // Arrange + var headerDictionaryMock = new Mock(); + + // Expired token with removed AppID + var token = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSIsImtpZCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSJ9.eyJhdWQiOiJodHRwczovL2FwaS5ib3RmcmFtZXdvcmsuY29tIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvZDZkNDk0MjAtZjM5Yi00ZGY3LWExZGMtZDU5YTkzNTg3MWRiLyIsImlhdCI6MTY3MDM1MDQxNSwibmJmIjoxNjcwMzUwNDE1LCJleHAiOjE2NzA0MzcxMTUsImFpbyI6IkUyWmdZTkJONEpWZmxlOTJUc2wxYjhtOHBjOWpBQT09IiwiYXBwaWQiOiI5ZGRmM2QwZS02ZDRlLTQ2MWEtYjM4Yi0zMTYzZWQ3Yjg1NmIiLCJhcHBpZGFjciI6IjEiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIvIiwicmgiOiIwLkFXNEFJSlRVMXB2ejkwMmgzTldhazFoeDIwSXpMWTBwejFsSmxYY09EcS05RnJ4dUFBQS4iLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJIWDlncld2bU1rMlhESTRkS3BHSEFBIiwidmVyIjoiMS4wIn0.PBLuja5sCcDfFjweoy-VucvbfHEyEcs1GyqXjekzBqgvK-mSc1UrEfqr5834qY6dLNsXVIMJzMFuH6WyPbnAfIfRcabdiVSOAl8N8e9Tex6vHfPi4h4P2F96VkXU80EtZX4QMjsJMDJ5eXbJlIDEAxXoJbAdHqgy-lHcVBx8XK7toJ_W7vSsFhis3C4CPCHI1cf1WuHVwfFXBiNwsOzj9cnRUKpea6UELV89q4C0L6aeSNdWYXehZmgq-wlo2wIaGgQ7rOXx4MlIrc83LBzMMc6TWvBJecK6O8pJWLe6BTwOltBI8Tmo2hWnY1OnsbOhbSSlfwLaZqKI7QpA50_2GQ"; + + headerDictionaryMock.Setup(h => h[It.Is(v => v == "Authorization")]).Returns((_) => token); + + var httpRequestMock = new Mock(); + httpRequestMock.Setup(r => r.Method).Returns(HttpMethods.Post); + httpRequestMock.Setup(r => r.Body).Returns(CreateInvokeActivityStream()); + httpRequestMock.Setup(r => r.Headers).Returns(headerDictionaryMock.Object); + + var response = new MemoryStream(); + var httpResponseMock = new Mock().SetupAllProperties(); + httpResponseMock.Setup(r => r.Body).Returns(response); + + var bot = new InvokeResponseBot(); + + // Act + var adapter = new CloudAdapter(); + await adapter.ProcessAsync(httpRequestMock.Object, httpResponseMock.Object, bot); + + // Assert + Assert.Equal((int)HttpStatusCode.Unauthorized, httpResponseMock.Object.StatusCode); + } + private static Stream CreateMessageActivityStream(string userId, string channelId, string conversationId, string recipient, string relatesToActivityId) { return CreateStream(new Activity @@ -978,11 +1008,11 @@ private class ConnectorFactoryBot : IBot public IIdentity Identity { get; private set; } public IConnectorClient ConnectorClient { get; private set; } - + public UserTokenClient UserTokenClient { get; private set; } - + public BotCallbackHandler BotCallbackHandler { get; private set; } - + public string OAuthScope { get; private set; } public AuthenticationHeaderValue Authorization { get; private set; } @@ -1036,9 +1066,9 @@ private class TestContentStream : IContentStream public Guid Id { get; set; } public string ContentType { get; set; } - + public int? Length { get; set; } - + public Stream Stream { get; set; } }