From 2260e574ed7682576521e68767803682c748aa2a Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Fri, 23 Jun 2023 10:22:41 -0700 Subject: [PATCH 01/19] Option to use JsonWebTokenHandler in OpenIdConnectHandler --- eng/Versions.props | 8 +- .../OpenIdConnect/src/LoggingExtensions.cs | 6 ++ .../OpenIdConnect/src/OpenIdConnectHandler.cs | 81 ++++++++++++++++++- .../OpenIdConnect/src/OpenIdConnectOptions.cs | 31 +++++++ .../OpenIdConnect/src/PublicAPI.Shipped.txt | 6 ++ .../OpenIdConnect/src/Resources.resx | 6 ++ 6 files changed, 132 insertions(+), 6 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 2fed627a089c..bd2c5625cc9d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -248,15 +248,15 @@ 1.1.2-beta1.22531.1 1.1.2-beta1.22531.1 1.0.0-20230414.1 - 6.15.1 - 6.15.1 - 6.15.1 + 6.31.0 + 6.31.0 + 6.31.0 2.2.1 1.0.1 3.0.1 3.0.1 11.1.0 - 6.21.0 + 6.31.0 5.0.0 5.0.0-alpha.20560.6 5.0.0 diff --git a/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs b/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs index c0cc19bbf12a..79f0d2e63861 100644 --- a/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs +++ b/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs @@ -164,4 +164,10 @@ internal static partial class LoggingExtensions [LoggerMessage(55, LogLevel.Error, "The remote signout request was ignored because the 'iss' parameter didn't match " + "the expected value, which may indicate an unsolicited logout.", EventName = "RemoteSignOutIssuerInvalid")] public static partial void RemoteSignOutIssuerInvalid(this ILogger logger); + + [LoggerMessage(56, LogLevel.Error, "Unable to validate the 'id_token', no suitable TokenHandler was found for: '{IdToken}'.", EventName = "UnableToValidateIdTokenFromHandler")] + public static partial void UnableToValidateIdTokenFromHandler(this ILogger logger, string idToken); + + [LoggerMessage(57, LogLevel.Error, "The Validated Security Token must be of type JsonWebToken, but instead its type is: '{SecurityTokenType}'", EventName = "InvalidSecurityTokenTypeFromHandler")] + public static partial void InvalidSecurityTokenTypeFromHandler(this ILogger logger, string? securityTokenType); } diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs index 1f2c620def33..c6abec03ec18 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs @@ -17,8 +17,10 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; +using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; namespace Microsoft.AspNetCore.Authentication.OpenIdConnect; @@ -649,7 +651,18 @@ protected override async Task HandleRemoteAuthenticateAsync if (!string.IsNullOrEmpty(authorizationResponse.IdToken)) { Logger.ReceivedIdToken(); - user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); + + if (Options.UseTokenHandler) + { + var tokenValidationResult = await ValidateTokenUsingHandlerAsync(authorizationResponse.IdToken, properties, validationParameters); + user = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); + // todo: need to use converter in 7x branch here + jwt = new JwtSecurityToken(); + } + else + { + user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); + } nonce = jwt.Payload.Nonce; if (!string.IsNullOrEmpty(nonce)) @@ -717,7 +730,20 @@ protected override async Task HandleRemoteAuthenticateAsync // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response. // And we'll want to validate the new JWT in ValidateTokenResponse. - var tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out var tokenEndpointJwt); + ClaimsPrincipal tokenEndpointUser; + JwtSecurityToken tokenEndpointJwt; + + if (Options.UseTokenHandler) + { + var tokenValidationResult = await ValidateTokenUsingHandlerAsync(tokenEndpointResponse.IdToken, properties, validationParameters); + tokenEndpointUser = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); + // TODO: need to use converter to create jwt + tokenEndpointJwt = new JwtSecurityToken(); + } + else + { + tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out tokenEndpointJwt); + } // Avoid reading & deleting the nonce cookie, running the event, etc, if it was already done as part of the authorization response validation. if (user == null) @@ -1294,6 +1320,57 @@ private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties p return principal; } + // Note this modifies properties if Options.UseTokenLifetime + private async Task ValidateTokenUsingHandlerAsync(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters) + { + if (_configuration != null) + { + var issuer = new[] { _configuration.Issuer }; + validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuer) ?? issuer; + + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) + ?? _configuration.SigningKeys; + } + + var validationResult = await Options.TokenHandler.ValidateTokenAsync(idToken, validationParameters); + + if (validationResult.Exception != null) + { + throw validationResult.Exception; + } + + var validatedToken = validationResult.SecurityToken; + + if (!validationResult.IsValid || validatedToken == null) + { + Logger.UnableToValidateIdTokenFromHandler(idToken); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateTokenFromHandler, idToken)); + } + + if (validatedToken is not JsonWebToken) + { + Logger.InvalidSecurityTokenTypeFromHandler(validatedToken?.GetType().ToString()); + throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJsonWebToken, validatedToken?.GetType())); + } + + if (Options.UseTokenLifetime) + { + var issued = validatedToken.ValidFrom; + if (issued != DateTime.MinValue) + { + properties.IssuedUtc = issued; + } + + var expires = validatedToken.ValidTo; + if (expires != DateTime.MinValue) + { + properties.ExpiresUtc = expires; + } + } + + return validationResult; + } + /// /// Build a redirect path if the given path is a relative path. /// diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs index 7fada340c927..1491c680e322 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs @@ -4,6 +4,7 @@ using System.IdentityModel.Tokens.Jwt; using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -17,6 +18,7 @@ public class OpenIdConnectOptions : RemoteAuthenticationOptions { private CookieBuilder _nonceCookieBuilder; private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler(); + private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler(); /// /// Initializes a new @@ -38,6 +40,10 @@ public OpenIdConnectOptions() SignedOutCallbackPath = new PathString("/signout-callback-oidc"); RemoteSignOutPath = new PathString("/signout-oidc"); SecurityTokenValidator = _defaultHandler; + TokenHandler = _defaultTokenHandler; + + // reconcile the difference between default values. + _defaultTokenHandler.MapInboundClaims = _defaultHandler.MapInboundClaims; Events = new OpenIdConnectEvents(); Scope.Add("openid"); @@ -255,6 +261,14 @@ public override void Validate() /// public ISecurityTokenValidator SecurityTokenValidator { get; set; } + /// + /// Gets or sets the used to validate identity tokens. + /// + /// This will be used instead of if is + /// + /// + public TokenHandler TokenHandler { get; set; } + /// /// Gets or sets the parameters used to validate identity tokens. /// @@ -363,4 +377,21 @@ public bool MapInboundClaims get => _defaultHandler.MapInboundClaims; set => _defaultHandler.MapInboundClaims = value; } + + /// + /// Gets or sets the property on the default instance of in TokenHandler, which is used when determining + /// whether or not to map claim types that are extracted when validating a . + /// If this is set to true, the Claim Type is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs. + /// The default value is true. + /// + public bool MapInboundClaimsTokenHandler + { + get => _defaultTokenHandler.MapInboundClaims; + set => _defaultTokenHandler.MapInboundClaims = value; + } + + /// + /// Gets or sets whether to use the or the for validating identity tokens. + /// + public bool UseTokenHandler { get; set; } = true; } diff --git a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Shipped.txt b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Shipped.txt index 443f7f561a9d..3196b868dc99 100644 --- a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Shipped.txt +++ b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Shipped.txt @@ -210,3 +210,9 @@ virtual Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.H virtual Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleSignOutCallbackAsync() -> System.Threading.Tasks.Task! virtual Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RedeemAuthorizationCodeAsync(Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage! tokenEndpointRequest) -> System.Threading.Tasks.Task! virtual Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.SignOutAsync(Microsoft.AspNetCore.Authentication.AuthenticationProperties? properties) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaimsTokenHandler.get -> bool +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaimsTokenHandler.set -> void +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.get -> Microsoft.IdentityModel.Tokens.TokenHandler! +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.set -> void +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseTokenHandler.get -> bool +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseTokenHandler.set -> void diff --git a/src/Security/Authentication/OpenIdConnect/src/Resources.resx b/src/Security/Authentication/OpenIdConnect/src/Resources.resx index 7f790fef43f5..fec68b579f86 100644 --- a/src/Security/Authentication/OpenIdConnect/src/Resources.resx +++ b/src/Security/Authentication/OpenIdConnect/src/Resources.resx @@ -135,4 +135,10 @@ Cannot process the message. Both id_token and code are missing. + + The Validated Security Token must be of type JsonWebToken, but instead its tye is '{0}'. + + + Unable to validate the 'id_token', no suitable TokenHandler was found for: '{0}'." + \ No newline at end of file From 61323b83e5bffa0381591986e2b42a795eb19ad4 Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Thu, 29 Jun 2023 12:17:28 -0700 Subject: [PATCH 02/19] fix sample --- .../OpenIdConnect/samples/OpenIdConnectSample/Startup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs b/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs index 3c063b244100..7c53f799c443 100644 --- a/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs +++ b/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs @@ -266,7 +266,7 @@ await WriteHtmlAsync(response, async res => // Persist the new acess token props.UpdateTokenValue("access_token", payload.RootElement.GetString("access_token")); props.UpdateTokenValue("refresh_token", payload.RootElement.GetString("refresh_token")); - if (payload.RootElement.TryGetProperty("expires_in", out var property) && property.TryGetInt32(out var seconds)) + if (payload.RootElement.TryGetProperty("expires_in", out var property) && int.TryParse(property.GetString(), out var seconds)) { var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds); props.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); @@ -283,7 +283,7 @@ await WriteHtmlAsync(response, async res => await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value })); await res.WriteAsync("

Payload:

"); - await res.WriteAsync(HtmlEncoder.Default.Encode(payload.ToString()).Replace(",", ",
") + "
"); + await res.WriteAsync(HtmlEncoder.Default.Encode(payload.RootElement.ToString()).Replace(",", ",
") + "
"); }); } From 7f45829cee07997603dc05e581731f9e63623fa2 Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Thu, 29 Jun 2023 12:18:32 -0700 Subject: [PATCH 03/19] split event tests --- .../OpenIdConnect/OpenIdConnectEventTests.cs | 2 + .../OpenIdConnectEventTests_Handler.cs | 1404 +++++++++++++++++ 2 files changed, 1406 insertions(+) create mode 100644 src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests_Handler.cs diff --git a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs index 3da118f7e03e..230f862626e4 100644 --- a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs +++ b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; @@ -1286,6 +1287,7 @@ private TestServer CreateServer(OpenIdConnectEvents events, RequestDelegate appC }; o.StateDataFormat = new TestStateDataFormat(); o.SecurityTokenValidator = new TestTokenValidator(); + o.UseTokenHandler = false; o.ProtocolValidator = new TestProtocolValidator(); o.BackchannelHttpHandler = new TestBackchannel(); }); diff --git a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests_Handler.cs b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests_Handler.cs new file mode 100644 index 000000000000..0749108b219c --- /dev/null +++ b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests_Handler.cs @@ -0,0 +1,1404 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect; + +public class OpenIdConnectEventTests_Handlers +{ + private readonly RequestDelegate AppWritePath = context => context.Response.WriteAsync(context.Request.Path); + private readonly RequestDelegate AppNotImpl = context => { throw new NotImplementedException("App"); }; + + [Fact] + public async Task OnMessageReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + }; + events.OnMessageReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", ""); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnMessageReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectRemoteFailure = true, + }; + events.OnMessageReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", ""); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnMessageReceived_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + }; + events.OnMessageReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", ""); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + }; + events.OnTokenValidated = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectRemoteFailure = true, + }; + events.OnTokenValidated = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + }; + events.OnTokenValidated = context => + { + context.HandleResponse(); + context.Principal = null; + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectTicketReceived = true, + }; + events.OnTokenValidated = context => + { + context.HandleResponse(); + context.Principal = null; + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + events.OnTokenValidated = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectRemoteFailure = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.HandleResponse(); + context.Principal = null; + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTicketReceived = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenResponseReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectRemoteFailure = true, + }; + events.OnTokenResponseReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenResponseReceived = context => + { + context.Principal = null; + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectTicketReceived = true, + }; + events.OnTokenResponseReceived = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenValidated = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectRemoteFailure = true, + }; + events.OnTokenValidated = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenValidated = context => + { + context.Principal = null; + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectTicketReceived = true, + }; + events.OnTokenValidated = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + }; + events.OnUserInformationReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + }; + events.OnUserInformationReceived = context => + { + context.Principal = null; + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectTicketReceived = true, + }; + events.OnUserInformationReceived = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + Assert.Null(context.Principal); + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectTicketReceived = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + Assert.Null(context.Principal); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAccessDenied_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectAccessDenied = true + }; + events.OnAccessDenied = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "error=access_denied&state=protected_state"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAccessDenied_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectAccessDenied = true + }; + events.OnAccessDenied = context => + { + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "error=access_denied&state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRemoteFailure_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + return Task.FromResult(0); + }; + events.OnRemoteFailure = context => + { + Assert.Equal("TestException", context.Failure.Message); + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRemoteFailure_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnRemoteFailure = context => + { + Assert.Equal("TestException", context.Failure.Message); + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTicketReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectTicketReceived = true, + }; + events.OnTicketReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTicketReceived_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectTicketReceived = true, + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToIdentityProviderForSignOut_Invoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectForSignOut = true, + }; + var server = CreateServer(events, + context => + { + return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/"); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + Assert.Equal("http://testhost/end", response.Headers.Location.GetLeftPart(UriPartial.Path)); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToIdentityProviderForSignOut_Handled_RedirectNotInvoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectForSignOut = true, + }; + events.OnRedirectToIdentityProviderForSignOut = context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + context.HandleResponse(); + return Task.CompletedTask; + }; + var server = CreateServer(events, + context => + { + return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Null(response.Headers.Location); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRemoteSignOut_Invoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRemoteSignOut = true, + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-oidc"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + events.ValidateExpectations(); + Assert.True(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values)); + Assert.True(SetCookieHeaderValue.TryParseStrictList(values.ToList(), out var parsedValues)); + Assert.Equal(1, parsedValues.Count); + Assert.True(StringSegment.IsNullOrEmpty(parsedValues.Single().Value)); + } + + [Fact] + public async Task OnRemoteSignOut_Handled_NoSignout() + { + var events = new ExpectedOidcEvents() + { + ExpectRemoteSignOut = true, + }; + events.OnRemoteSignOut = context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + context.HandleResponse(); + return Task.CompletedTask; + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-oidc"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + events.ValidateExpectations(); + Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values)); + } + + [Fact] + public async Task OnRemoteSignOut_Skip_NoSignout() + { + var events = new ExpectedOidcEvents() + { + ExpectRemoteSignOut = true, + }; + events.OnRemoteSignOut = context => + { + context.SkipHandler(); + return Task.CompletedTask; + }; + var server = CreateServer(events, context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.CompletedTask; + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-oidc"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + events.ValidateExpectations(); + Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values)); + } + + [Fact] + public async Task OnRedirectToSignedOutRedirectUri_Invoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectToSignedOut = true, + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-callback-oidc?state=protected_state"); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + Assert.Equal("http://testhost/redirect", response.Headers.Location.AbsoluteUri); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToSignedOutRedirectUri_Handled_NoRedirect() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectToSignedOut = true, + }; + events.OnSignedOutCallbackRedirect = context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + context.HandleResponse(); + return Task.CompletedTask; + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-callback-oidc?state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Null(response.Headers.Location); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToSignedOutRedirectUri_Skipped_NoRedirect() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectToSignedOut = true, + }; + events.OnSignedOutCallbackRedirect = context => + { + context.SkipHandler(); + return Task.CompletedTask; + }; + var server = CreateServer(events, + context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.CompletedTask; + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-callback-oidc?state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Null(response.Headers.Location); + events.ValidateExpectations(); + } + + private class ExpectedOidcEvents : OpenIdConnectEvents + { + public bool ExpectMessageReceived { get; set; } + public bool InvokedMessageReceived { get; set; } + + public bool ExpectTokenValidated { get; set; } + public bool InvokedTokenValidated { get; set; } + + public bool ExpectAccessDenied { get; set; } + public bool InvokedAccessDenied { get; set; } + + public bool ExpectRemoteFailure { get; set; } + public bool InvokedRemoteFailure { get; set; } + + public bool ExpectTicketReceived { get; set; } + public bool InvokedTicketReceived { get; set; } + + public bool ExpectAuthorizationCodeReceived { get; set; } + public bool InvokedAuthorizationCodeReceived { get; set; } + + public bool ExpectTokenResponseReceived { get; set; } + public bool InvokedTokenResponseReceived { get; set; } + + public bool ExpectUserInfoReceived { get; set; } + public bool InvokedUserInfoReceived { get; set; } + + public bool ExpectAuthenticationFailed { get; set; } + public bool InvokeAuthenticationFailed { get; set; } + + public bool ExpectRedirectForSignOut { get; set; } + public bool InvokedRedirectForSignOut { get; set; } + + public bool ExpectRemoteSignOut { get; set; } + public bool InvokedRemoteSignOut { get; set; } + + public bool ExpectRedirectToSignedOut { get; set; } + public bool InvokedRedirectToSignedOut { get; set; } + + public override Task MessageReceived(MessageReceivedContext context) + { + InvokedMessageReceived = true; + return base.MessageReceived(context); + } + + public override Task TokenValidated(TokenValidatedContext context) + { + InvokedTokenValidated = true; + return base.TokenValidated(context); + } + + public override Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context) + { + InvokedAuthorizationCodeReceived = true; + return base.AuthorizationCodeReceived(context); + } + + public override Task TokenResponseReceived(TokenResponseReceivedContext context) + { + InvokedTokenResponseReceived = true; + return base.TokenResponseReceived(context); + } + + public override Task UserInformationReceived(UserInformationReceivedContext context) + { + InvokedUserInfoReceived = true; + return base.UserInformationReceived(context); + } + + public override Task AuthenticationFailed(AuthenticationFailedContext context) + { + InvokeAuthenticationFailed = true; + return base.AuthenticationFailed(context); + } + + public override Task TicketReceived(TicketReceivedContext context) + { + InvokedTicketReceived = true; + return base.TicketReceived(context); + } + + public override Task AccessDenied(AccessDeniedContext context) + { + InvokedAccessDenied = true; + return base.AccessDenied(context); + } + + public override Task RemoteFailure(RemoteFailureContext context) + { + InvokedRemoteFailure = true; + return base.RemoteFailure(context); + } + + public override Task RedirectToIdentityProviderForSignOut(RedirectContext context) + { + InvokedRedirectForSignOut = true; + return base.RedirectToIdentityProviderForSignOut(context); + } + + public override Task RemoteSignOut(RemoteSignOutContext context) + { + InvokedRemoteSignOut = true; + return base.RemoteSignOut(context); + } + + public override Task SignedOutCallbackRedirect(RemoteSignOutContext context) + { + InvokedRedirectToSignedOut = true; + return base.SignedOutCallbackRedirect(context); + } + + public void ValidateExpectations() + { + Assert.Equal(ExpectMessageReceived, InvokedMessageReceived); + Assert.Equal(ExpectTokenValidated, InvokedTokenValidated); + Assert.Equal(ExpectAuthorizationCodeReceived, InvokedAuthorizationCodeReceived); + Assert.Equal(ExpectTokenResponseReceived, InvokedTokenResponseReceived); + Assert.Equal(ExpectUserInfoReceived, InvokedUserInfoReceived); + Assert.Equal(ExpectAuthenticationFailed, InvokeAuthenticationFailed); + Assert.Equal(ExpectTicketReceived, InvokedTicketReceived); + Assert.Equal(ExpectAccessDenied, InvokedAccessDenied); + Assert.Equal(ExpectRemoteFailure, InvokedRemoteFailure); + Assert.Equal(ExpectRedirectForSignOut, InvokedRedirectForSignOut); + Assert.Equal(ExpectRemoteSignOut, InvokedRemoteSignOut); + Assert.Equal(ExpectRedirectToSignedOut, InvokedRedirectToSignedOut); + } + } + + private TestServer CreateServer(OpenIdConnectEvents events, RequestDelegate appCode) + { + var host = new HostBuilder() + .ConfigureWebHost(builder => + builder.UseTestServer() + .ConfigureServices(services => + { + services.AddAuthentication(auth => + { + auth.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(o => + { + o.Events = events; + o.ClientId = "ClientId"; + o.GetClaimsFromUserInfoEndpoint = true; + o.Configuration = new OpenIdConnectConfiguration() + { + TokenEndpoint = "http://testhost/tokens", + UserInfoEndpoint = "http://testhost/user", + EndSessionEndpoint = "http://testhost/end" + }; + o.StateDataFormat = new TestStateDataFormat(); + o.UseTokenHandler = true; + o.TokenHandler = new TestTokenHandler(); + o.ProtocolValidator = new TestProtocolValidator(); + o.BackchannelHttpHandler = new TestBackchannel(); + }); + }) + .Configure(app => + { + app.UseAuthentication(); + app.Run(appCode); + })) + .Build(); + + host.Start(); + return host.GetTestServer(); + } + + private Task PostAsync(TestServer server, string path, string form) + { + var client = server.CreateClient(); + var cookie = ".AspNetCore.Correlation.correlationId=N"; + client.DefaultRequestHeaders.Add("Cookie", cookie); + return client.PostAsync("signin-oidc", + new StringContent(form, Encoding.ASCII, "application/x-www-form-urlencoded")); + } + + private class TestStateDataFormat : ISecureDataFormat + { + private AuthenticationProperties Data { get; set; } + + public string Protect(AuthenticationProperties data) + { + return "protected_state"; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + throw new NotImplementedException(); + } + + public AuthenticationProperties Unprotect(string protectedText) + { + Assert.Equal("protected_state", protectedText); + var properties = new AuthenticationProperties(new Dictionary() + { + { ".xsrf", "correlationId" }, + { OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, "redirect_uri" }, + { "testkey", "testvalue" } + }); + properties.RedirectUri = "http://testhost/redirect"; + return properties; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + throw new NotImplementedException(); + } + } + + private class TestTokenHandler : TokenHandler + { + public override Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) + { + Assert.Equal("my_id_token", token); + var jwt = new JwtSecurityToken(); + return Task.FromResult(new TokenValidationResult() + { + SecurityToken = new JsonWebToken(jwt.EncodedHeader + "." + jwt.EncodedPayload + "."), + ClaimsIdentity = new ClaimsIdentity("customAuthType"), + IsValid = true + }); + } + + public override SecurityToken ReadToken(string token) + { + Assert.Equal("my_id_token", token); + return new JsonWebToken(token); + } + } + + private class TestProtocolValidator : OpenIdConnectProtocolValidator + { + public override void ValidateAuthenticationResponse(OpenIdConnectProtocolValidationContext validationContext) + { + } + + public override void ValidateTokenResponse(OpenIdConnectProtocolValidationContext validationContext) + { + } + + public override void ValidateUserInfoResponse(OpenIdConnectProtocolValidationContext validationContext) + { + } + } + + private class TestBackchannel : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (string.Equals("/tokens", request.RequestUri.AbsolutePath, StringComparison.Ordinal)) + { + return Task.FromResult(new HttpResponseMessage() + { + Content = + new StringContent("{ \"id_token\": \"my_id_token\", \"access_token\": \"my_access_token\" }", Encoding.ASCII, "application/json") + }); + } + if (string.Equals("/user", request.RequestUri.AbsolutePath, StringComparison.Ordinal)) + { + return Task.FromResult(new HttpResponseMessage() { Content = new StringContent("{ }", Encoding.ASCII, "application/json") }); + } + + throw new NotImplementedException(request.RequestUri.ToString()); + } + } +} From 507d24137b37c43ddf9c18bdeb7510e70c84bcf1 Mon Sep 17 00:00:00 2001 From: Brent Schmaltz Date: Wed, 14 Jun 2023 10:21:09 -0700 Subject: [PATCH 04/19] update IdentityModel to 6.31.0 --- eng/Versions.props | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index fdeab081a838..72af5327b996 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -11,6 +11,7 @@ 0 7 true + 6.31.0 @@ -254,15 +255,15 @@ 1.1.2-beta1.22531.1 1.1.2-beta1.22531.1 1.0.0-20230414.1 - 6.15.1 - 6.15.1 - 6.15.1 + $(IdentityModelVersion) + $(IdentityModelVersion) + $(IdentityModelVersion) 2.2.1 1.0.1 3.0.1 3.0.1 11.1.0 - 6.21.0 + $(IdentityModelVersion) 5.0.0 5.0.0-alpha.20560.6 5.0.0 From 47ffe3239a44e2229792fa3ace4ff5fa09369ab6 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Fri, 16 Jun 2023 11:04:22 -0700 Subject: [PATCH 05/19] Added JsonWebTokenHandler and TokenHandlers (#48857) Co-authored-by: Brent Schmaltz --- .../JwtBearer/src/AuthenticateResults.cs | 1 + .../JwtBearer/src/JwtBearerHandler.cs | 158 ++++++++++++------ .../JwtBearer/src/JwtBearerOptions.cs | 18 ++ .../JwtBearer/src/PublicAPI.Shipped.txt | 3 + .../WsFederation/src/PublicAPI.Shipped.txt | 4 + .../WsFederation/src/WsFederationHandler.cs | 132 ++++++++++++--- .../WsFederation/src/WsFederationOptions.cs | 33 ++++ 7 files changed, 271 insertions(+), 78 deletions(-) diff --git a/src/Security/Authentication/JwtBearer/src/AuthenticateResults.cs b/src/Security/Authentication/JwtBearer/src/AuthenticateResults.cs index 5f0e8bb6b914..c66ea3304f1d 100644 --- a/src/Security/Authentication/JwtBearer/src/AuthenticateResults.cs +++ b/src/Security/Authentication/JwtBearer/src/AuthenticateResults.cs @@ -6,4 +6,5 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer; internal static class AuthenticateResults { internal static AuthenticateResult ValidatorNotFound = AuthenticateResult.Fail("No SecurityTokenValidator available for token."); + internal static AuthenticateResult TokenHandlerUnableToValidate = AuthenticateResult.Fail("No TokenHandler was able to validate the token."); } diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs index ff4767430d57..5be6ada7682f 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Globalization; using System.Linq; using System.Security.Claims; @@ -96,79 +97,86 @@ protected override async Task HandleAuthenticateAsync() } } - if (_configuration == null && Options.ConfigurationManager != null) - { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); - } - - var validationParameters = Options.TokenValidationParameters.Clone(); - if (_configuration != null) - { - var issuers = new[] { _configuration.Issuer }; - validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers; - - validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) - ?? _configuration.SigningKeys; - } - + var tvp = await SetupTokenValidationParameters(); List? validationFailures = null; SecurityToken? validatedToken = null; - foreach (var validator in Options.SecurityTokenValidators) + ClaimsPrincipal? principal = null; + + if (Options.UseTokenHandlers) { - if (validator.CanReadToken(token)) + foreach (var tokenHandler in Options.TokenHandlers) { - ClaimsPrincipal principal; try { - principal = validator.ValidateToken(token, validationParameters, out validatedToken); + var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tvp); + if (tokenValidationResult.IsValid) + { + principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); + validatedToken = tokenValidationResult.SecurityToken; + break; + } + else + { + validationFailures ??= new List(1); + RecordTokenValidationError(tokenValidationResult.Exception ?? new SecurityTokenValidationException($"The TokenHandler: '{tokenHandler}', was unable to validate the Token."), validationFailures); + } } catch (Exception ex) { - Logger.TokenValidationFailed(ex); - - // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. - if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null - && ex is SecurityTokenSignatureKeyNotFoundException) + validationFailures ??= new List(1); + RecordTokenValidationError(new SecurityTokenValidationException($"TokenHandler: '{tokenHandler}', threw an exception (see inner exception).", ex), validationFailures); + } + } + } + else + { + foreach (var validator in Options.SecurityTokenValidators) + { + if (validator.CanReadToken(token)) + { + try { - Options.ConfigurationManager.RequestRefresh(); + principal = validator.ValidateToken(token, tvp, out validatedToken); } - - if (validationFailures == null) + catch (Exception ex) { - validationFailures = new List(1); + validationFailures ??= new List(1); + RecordTokenValidationError(ex, validationFailures); + continue; } - validationFailures.Add(ex); - continue; } + } + } - Logger.TokenValidationSucceeded(); + if (principal != null && validatedToken != null) + { + Logger.TokenValidationSucceeded(); - var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options) - { - Principal = principal, - SecurityToken = validatedToken - }; + var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options) + { + Principal = principal + }; - tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo); - tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom); + tokenValidatedContext.SecurityToken = validatedToken; + tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo); + tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom); - await Events.TokenValidated(tokenValidatedContext); - if (tokenValidatedContext.Result != null) - { - return tokenValidatedContext.Result; - } + await Events.TokenValidated(tokenValidatedContext); + if (tokenValidatedContext.Result != null) + { + return tokenValidatedContext.Result; + } - if (Options.SaveToken) + if (Options.SaveToken) + { + tokenValidatedContext.Properties.StoreTokens(new[] { - tokenValidatedContext.Properties.StoreTokens(new[] - { - new AuthenticationToken { Name = "access_token", Value = token } - }); - } - - tokenValidatedContext.Success(); - return tokenValidatedContext.Result!; + new AuthenticationToken { Name = "access_token", Value = token } + }); } + + tokenValidatedContext.Success(); + return tokenValidatedContext.Result!; } if (validationFailures != null) @@ -187,6 +195,11 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Fail(authenticationFailedContext.Exception); } + if (Options.UseTokenHandlers) + { + return AuthenticateResults.TokenHandlerUnableToValidate; + } + return AuthenticateResults.ValidatorNotFound; } catch (Exception ex) @@ -208,6 +221,47 @@ protected override async Task HandleAuthenticateAsync() } } + private void RecordTokenValidationError(Exception exception, List exceptions) + { + if (exception != null) + { + Logger.TokenValidationFailed(exception); + exceptions.Add(exception); + } + + // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. + // Refreshing on SecurityTokenSignatureKeyNotFound may be redundant if Last-Known-Good is enabled, it won't do much harm, most likely will be a nop. + if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null + && exception is SecurityTokenSignatureKeyNotFoundException) + { + Options.ConfigurationManager.RequestRefresh(); + } + } + + private async Task SetupTokenValidationParameters() + { + // Clone to avoid cross request race conditions for updated configurations. + var tokenValidationParameters = Options.TokenValidationParameters.Clone(); + + if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager) + { + tokenValidationParameters.ConfigurationManager = baseConfigurationManager; + } + else + { + if (Options.ConfigurationManager != null) + { + // GetConfigurationAsync has a time interval that must pass before new http request will be issued. + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + var issuers = new[] { _configuration.Issuer }; + tokenValidationParameters.ValidIssuers = (tokenValidationParameters.ValidIssuers == null ? issuers : tokenValidationParameters.ValidIssuers.Concat(issuers)); + tokenValidationParameters.IssuerSigningKeys = (tokenValidationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : tokenValidationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + } + } + + return tokenValidationParameters; + } + private static DateTime? GetSafeDateTime(DateTime dateTime) { // Assigning DateTime.MinValue or default(DateTime) to a DateTimeOffset when in a UTC+X timezone will throw diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs index db007691f3b9..f18401d20b78 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs @@ -5,6 +5,7 @@ using System.Net.Http; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; namespace Microsoft.AspNetCore.Authentication.JwtBearer; @@ -15,6 +16,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer; public class JwtBearerOptions : AuthenticationSchemeOptions { private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler(); + private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler(); /// /// Initializes a new instance of . @@ -22,6 +24,8 @@ public class JwtBearerOptions : AuthenticationSchemeOptions public JwtBearerOptions() { SecurityTokenValidators = new List { _defaultHandler }; + // TODO - communicate to IdentityModel team to see if ITokenValidator interface can be created. + TokenHandlers = new List { _defaultTokenHandler }; } /// @@ -105,6 +109,11 @@ public JwtBearerOptions() /// public IList SecurityTokenValidators { get; private set; } + /// + /// Gets the ordered list of used to validate access tokens. + /// + public IList TokenHandlers { get; private set; } + /// /// Gets or sets the parameters used to validate identity tokens. /// @@ -152,4 +161,13 @@ public bool MapInboundClaims /// Defaults to . /// public TimeSpan RefreshInterval { get; set; } = ConfigurationManager.DefaultRefreshInterval; + + /// + /// Gets of sets the property to control if or will be used to validate the inbound token. + /// The advantage of using the TokenHandlers is: + /// There is an Async model. + /// The default token handler is a which is 30 % faster when validating. + /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. + /// + public bool UseTokenHandlers { get; set; } } diff --git a/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt b/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt index c9aa49918342..e97b1aba38ad 100644 --- a/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt +++ b/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt @@ -72,8 +72,11 @@ Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.RequireHttpsMetad Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SaveToken.get -> bool Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SaveToken.set -> void Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SecurityTokenValidators.get -> System.Collections.Generic.IList! +Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenHandlers.get -> System.Collections.Generic.IList! Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters! Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenValidationParameters.set -> void +Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.get -> bool +Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.set -> void Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.JwtBearerPostConfigureOptions() -> void Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void diff --git a/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt b/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt index 3af5868ef305..b5c4afeada66 100644 --- a/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt +++ b/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt @@ -106,10 +106,14 @@ Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.SkipUnrecog Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.SkipUnrecognizedRequests.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.StateDataFormat.get -> Microsoft.AspNetCore.Authentication.ISecureDataFormat! Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.StateDataFormat.set -> void +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.get -> System.Collections.Generic.ICollection! +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters! Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenValidationParameters.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenLifetime.get -> bool Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenLifetime.set -> void +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.get -> bool +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.Wreply.get -> string? Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.Wreply.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.WsFederationOptions() -> void diff --git a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs index ca52664613ba..1e641084a609 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; @@ -176,6 +177,7 @@ protected override async Task HandleRemoteAuthenticateAsync return HandleRequestResults.NoMessage; } + List? validationFailures = null; try { // Retrieve our cached redirect uri @@ -241,42 +243,86 @@ protected override async Task HandleRemoteAuthenticateAsync wsFederationMessage = securityTokenReceivedContext.ProtocolMessage; properties = messageReceivedContext.Properties!; - if (_configuration == null) + var tvp = await SetupTokenValidationParameters(); + ClaimsPrincipal? principal = null; + SecurityToken? validatedToken = null; + if (Options.UseTokenHandlers) { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + foreach (var tokenHandler in Options.TokenHandlers) + { + try + { + var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tvp); + if (tokenValidationResult.IsValid) + { + principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); + validatedToken = tokenValidationResult.SecurityToken; + break; + } + else + { + validationFailures ??= new List(1); + Exception exception = tokenValidationResult.Exception ?? new SecurityTokenValidationException($"The TokenHandler: '{tokenHandler}', was unable to validate the Token."); + validationFailures.Add(exception); + RequestRefresh(exception); + } + } + catch (Exception ex) + { + validationFailures ??= new List(1); + validationFailures.Add(new SecurityTokenValidationException($"TokenHandler: '{tokenHandler}', threw an exception (see inner exception).", ex)); + RequestRefresh(ex); + } + } } - - // Copy and augment to avoid cross request race conditions for updated configurations. - var tvp = Options.TokenValidationParameters.Clone(); - var issuers = new[] { _configuration.Issuer }; - tvp.ValidIssuers = (tvp.ValidIssuers == null ? issuers : tvp.ValidIssuers.Concat(issuers)); - tvp.IssuerSigningKeys = (tvp.IssuerSigningKeys == null ? _configuration.SigningKeys : tvp.IssuerSigningKeys.Concat(_configuration.SigningKeys)); - - ClaimsPrincipal? principal = null; - SecurityToken? parsedToken = null; - foreach (var validator in Options.SecurityTokenHandlers) + else { - if (validator.CanReadToken(token)) + + foreach (var validator in Options.SecurityTokenHandlers) { - principal = validator.ValidateToken(token, tvp, out parsedToken); - break; + if (validator.CanReadToken(token)) + { + try + { + principal = validator.ValidateToken(token, tvp, out validatedToken); + } + catch (Exception ex) + { + validationFailures ??= new List(1); + validationFailures.Add(ex); + continue; + } + break; + } } } if (principal == null) { - throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound); + // TODO - need new string for TokenHandler + if (validationFailures == null || validationFailures.Count == 0) + { + throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound); + } + else if (validationFailures.Count == 1) + { + throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound, validationFailures[0]); + } + else + { + throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound, new AggregateException(validationFailures)); + } } - if (Options.UseTokenLifetime && parsedToken != null) + if (Options.UseTokenLifetime && validatedToken != null) { // Override any session persistence to match the token lifetime. - var issued = parsedToken.ValidFrom; + var issued = validatedToken.ValidFrom; if (issued != DateTime.MinValue) { properties.IssuedUtc = issued.ToUniversalTime(); } - var expires = parsedToken.ValidTo; + var expires = validatedToken.ValidTo; if (expires != DateTime.MinValue) { properties.ExpiresUtc = expires.ToUniversalTime(); @@ -287,7 +333,7 @@ protected override async Task HandleRemoteAuthenticateAsync var securityTokenValidatedContext = new SecurityTokenValidatedContext(Context, Scheme, Options, principal, properties) { ProtocolMessage = wsFederationMessage, - SecurityToken = parsedToken, + SecurityToken = validatedToken, }; await Events.SecurityTokenValidated(securityTokenValidatedContext); @@ -306,17 +352,13 @@ protected override async Task HandleRemoteAuthenticateAsync { Logger.ExceptionProcessingMessage(exception); - // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification. - if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException) - { - Options.ConfigurationManager.RequestRefresh(); - } - + RequestRefresh(exception); var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) { ProtocolMessage = wsFederationMessage, Exception = exception }; + await Events.AuthenticationFailed(authenticationFailedContext); if (authenticationFailedContext.Result != null) { @@ -327,6 +369,44 @@ protected override async Task HandleRemoteAuthenticateAsync } } + // TODO - this method could be shared across OIDC, WsFed, JwtBearer to ensure consistency + private async Task SetupTokenValidationParameters() + { + // Clone to avoid cross request race conditions for updated configurations. + var tokenValidationParameters = Options.TokenValidationParameters.Clone(); + + if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager) + { + // TODO - we need to add a parameter to TokenValidationParameters for the CancellationToken. + tokenValidationParameters.ConfigurationManager = baseConfigurationManager; + } + else + { + if (Options.ConfigurationManager != null) + { + // GetConfigurationAsync has a time interval that must pass before new http request will be issued. + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + + var issuers = new[] { _configuration.Issuer }; + tokenValidationParameters.ValidIssuers = (tokenValidationParameters.ValidIssuers == null ? issuers : tokenValidationParameters.ValidIssuers.Concat(issuers)); + tokenValidationParameters.IssuerSigningKeys = (tokenValidationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : tokenValidationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + } + } + + return tokenValidationParameters; + } + + private void RequestRefresh(Exception exception) + { + // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification. + // Refreshing on SecurityTokenSignatureKeyNotFound may be redundant if Last-Known-Good is enabled, it won't do much harm, most likely will be a nop. + if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException) + { + Options.ConfigurationManager.RequestRefresh(); + } + + } + /// /// Handles Signout /// diff --git a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs index 81ec1f385bd5..944dd75b9552 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.WsFederation; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens.Saml; using Microsoft.IdentityModel.Tokens.Saml2; @@ -24,6 +25,14 @@ public class WsFederationOptions : RemoteAuthenticationOptions new SamlSecurityTokenHandler(), new JwtSecurityTokenHandler() }; + + private ICollection _tokenHandlers = new Collection() + { + new Saml2SecurityTokenHandler(), + new SamlSecurityTokenHandler(), + new JsonWebTokenHandler() + }; + private TokenValidationParameters _tokenValidationParameters = new TokenValidationParameters(); /// @@ -108,6 +117,21 @@ public ICollection SecurityTokenHandlers } } + /// + /// Gets or sets the collection of used to read and validate the s. + /// + public ICollection TokenHandlers + { + get + { + return _tokenHandlers; + } + set + { + _tokenHandlers = value ?? throw new ArgumentNullException(nameof(TokenHandlers)); + } + } + /// /// Gets or sets the type used to secure data handled by the middleware. /// @@ -181,4 +205,13 @@ public TokenValidationParameters TokenValidationParameters /// [EditorBrowsable(EditorBrowsableState.Never)] public new bool SaveTokens { get; set; } + + /// + /// Gets of sets the property to control if or will be used to validate the inbound token. + /// The advantage of using the TokenHandlers is: + /// There is an Async model. + /// The default token handler is a which is 30 % faster when validating. + /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. + /// + public bool UseTokenHandlers { get; set; } } From 4153b06e3369fe0cf0982ee309b97ba32d41636b Mon Sep 17 00:00:00 2001 From: Brent Schmaltz Date: Thu, 22 Jun 2023 10:09:52 -0700 Subject: [PATCH 06/19] adjust for claims mapping remove using System --- .../JwtBearer/src/JwtBearerHandler.cs | 1 - .../JwtBearer/src/JwtBearerOptions.cs | 14 ++++++++++---- .../WsFederation/src/WsFederationHandler.cs | 1 - .../WsFederation/src/WsFederationOptions.cs | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs index 5be6ada7682f..48be47fa36d8 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Globalization; using System.Linq; using System.Security.Claims; diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs index f18401d20b78..b36cd2e9da32 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs @@ -17,6 +17,7 @@ public class JwtBearerOptions : AuthenticationSchemeOptions { private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler(); private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler(); + private bool _mapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims; /// /// Initializes a new instance of . @@ -135,15 +136,20 @@ public JwtBearerOptions() public bool IncludeErrorDetails { get; set; } = true; /// - /// Gets or sets the property on the default instance of in SecurityTokenValidators, which is used when determining - /// whether or not to map claim types that are extracted when validating a . + /// Gets or sets the property on the default instance of in SecurityTokenValidators, or which is used when determining + /// whether or not to map claim types that are extracted when validating a or . /// If this is set to true, the Claim Type is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs. /// The default value is true. /// public bool MapInboundClaims { - get => _defaultHandler.MapInboundClaims; - set => _defaultHandler.MapInboundClaims = value; + get => _mapInboundClaims; + set + { + _mapInboundClaims = value; + _defaultHandler.MapInboundClaims = value; + _defaultTokenHandler.MapInboundClaims = value; + } } /// diff --git a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs index 1e641084a609..33b3cad3c7c2 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; diff --git a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs index 944dd75b9552..41133c60ba4a 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs @@ -30,7 +30,7 @@ public class WsFederationOptions : RemoteAuthenticationOptions { new Saml2SecurityTokenHandler(), new SamlSecurityTokenHandler(), - new JsonWebTokenHandler() + new JsonWebTokenHandler(){ MapInboundClaims = true } }; private TokenValidationParameters _tokenValidationParameters = new TokenValidationParameters(); From 7fa2f968fea3918956ad9bd405e9b20572b833a0 Mon Sep 17 00:00:00 2001 From: Brent Schmaltz Date: Thu, 22 Jun 2023 10:54:21 -0700 Subject: [PATCH 07/19] moved api's to unshipped --- .../Authentication/JwtBearer/src/PublicAPI.Shipped.txt | 3 --- .../Authentication/JwtBearer/src/PublicAPI.Unshipped.txt | 3 +++ .../Authentication/WsFederation/src/PublicAPI.Shipped.txt | 4 ---- .../Authentication/WsFederation/src/PublicAPI.Unshipped.txt | 4 ++++ 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt b/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt index e97b1aba38ad..c9aa49918342 100644 --- a/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt +++ b/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt @@ -72,11 +72,8 @@ Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.RequireHttpsMetad Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SaveToken.get -> bool Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SaveToken.set -> void Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SecurityTokenValidators.get -> System.Collections.Generic.IList! -Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenHandlers.get -> System.Collections.Generic.IList! Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters! Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenValidationParameters.set -> void -Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.get -> bool -Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.set -> void Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.JwtBearerPostConfigureOptions() -> void Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void diff --git a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt index d9d3a043b473..0e7f7787af03 100644 --- a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt @@ -1,2 +1,5 @@ #nullable enable Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.JwtBearerHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.UrlEncoder! encoder) -> void +Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenHandlers.get -> System.Collections.Generic.IList! +Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.get -> bool +Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.set -> void diff --git a/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt b/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt index b5c4afeada66..3af5868ef305 100644 --- a/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt +++ b/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt @@ -106,14 +106,10 @@ Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.SkipUnrecog Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.SkipUnrecognizedRequests.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.StateDataFormat.get -> Microsoft.AspNetCore.Authentication.ISecureDataFormat! Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.StateDataFormat.set -> void -Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.get -> System.Collections.Generic.ICollection! -Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters! Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenValidationParameters.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenLifetime.get -> bool Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenLifetime.set -> void -Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.get -> bool -Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.Wreply.get -> string? Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.Wreply.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.WsFederationOptions() -> void diff --git a/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt index 6586d9439638..717284fbda1b 100644 --- a/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt @@ -1,2 +1,6 @@ #nullable enable Microsoft.AspNetCore.Authentication.WsFederation.WsFederationHandler.WsFederationHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.UrlEncoder! encoder) -> void +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.get -> System.Collections.Generic.ICollection! +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.set -> void +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.get -> bool +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.set -> void From aca45dd5e1162952edb8784a0236945ba3237cef Mon Sep 17 00:00:00 2001 From: Brent Schmaltz Date: Sat, 8 Jul 2023 10:10:01 -0700 Subject: [PATCH 08/19] Addressed PR comments --- .../JwtBearer/src/JwtBearerHandler.cs | 4 +-- .../JwtBearer/src/JwtBearerOptions.cs | 23 +++++++----- .../WsFederation/src/WsFederationHandler.cs | 5 ++- .../WsFederation/src/WsFederationOptions.cs | 35 ++++++++----------- .../Authentication/test/JwtBearerTests.cs | 17 +++++++++ 5 files changed, 50 insertions(+), 34 deletions(-) diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs index 48be47fa36d8..88da59dfa116 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs @@ -96,7 +96,7 @@ protected override async Task HandleAuthenticateAsync() } } - var tvp = await SetupTokenValidationParameters(); + var tvp = await SetupTokenValidationParametersAsync(); List? validationFailures = null; SecurityToken? validatedToken = null; ClaimsPrincipal? principal = null; @@ -237,7 +237,7 @@ private void RecordTokenValidationError(Exception exception, List exc } } - private async Task SetupTokenValidationParameters() + private async Task SetupTokenValidationParametersAsync() { // Clone to avoid cross request race conditions for updated configurations. var tokenValidationParameters = Options.TokenValidationParameters.Clone(); diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs index b36cd2e9da32..0c53c2adb692 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs @@ -16,7 +16,11 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer; public class JwtBearerOptions : AuthenticationSchemeOptions { private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler(); - private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler(); + private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler + { + MapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims + }; + private bool _mapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims; /// @@ -25,7 +29,6 @@ public class JwtBearerOptions : AuthenticationSchemeOptions public JwtBearerOptions() { SecurityTokenValidators = new List { _defaultHandler }; - // TODO - communicate to IdentityModel team to see if ITokenValidator interface can be created. TokenHandlers = new List { _defaultTokenHandler }; } @@ -136,8 +139,8 @@ public JwtBearerOptions() public bool IncludeErrorDetails { get; set; } = true; /// - /// Gets or sets the property on the default instance of in SecurityTokenValidators, or which is used when determining - /// whether or not to map claim types that are extracted when validating a or . + /// Gets or sets the property on the default instance of in SecurityTokenValidators, or in TokenHandlers which is used when determining + /// whether or not to map claim types that are extracted when validating a or a . /// If this is set to true, the Claim Type is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs. /// The default value is true. /// @@ -169,11 +172,13 @@ public bool MapInboundClaims public TimeSpan RefreshInterval { get; set; } = ConfigurationManager.DefaultRefreshInterval; /// - /// Gets of sets the property to control if or will be used to validate the inbound token. - /// The advantage of using the TokenHandlers is: + /// Gets or sets whether or will be used to validate the inbound token. + /// + /// + /// The advantage of using TokenHandlers are: /// There is an Async model. - /// The default token handler is a which is 30 % faster when validating. + /// The default token handler is a which is faster than a . /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. - /// - public bool UseTokenHandlers { get; set; } + /// + public bool UseTokenHandlers { get; set; } = true; } diff --git a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs index 33b3cad3c7c2..0be2de5334f0 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs @@ -242,7 +242,7 @@ protected override async Task HandleRemoteAuthenticateAsync wsFederationMessage = securityTokenReceivedContext.ProtocolMessage; properties = messageReceivedContext.Properties!; - var tvp = await SetupTokenValidationParameters(); + var tvp = await SetupTokenValidationParametersAsync(); ClaimsPrincipal? principal = null; SecurityToken? validatedToken = null; if (Options.UseTokenHandlers) @@ -368,8 +368,7 @@ protected override async Task HandleRemoteAuthenticateAsync } } - // TODO - this method could be shared across OIDC, WsFed, JwtBearer to ensure consistency - private async Task SetupTokenValidationParameters() + private async Task SetupTokenValidationParametersAsync() { // Clone to avoid cross request race conditions for updated configurations. var tokenValidationParameters = Options.TokenValidationParameters.Clone(); diff --git a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs index 41133c60ba4a..7e9d3ffa4bea 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs @@ -26,13 +26,6 @@ public class WsFederationOptions : RemoteAuthenticationOptions new JwtSecurityTokenHandler() }; - private ICollection _tokenHandlers = new Collection() - { - new Saml2SecurityTokenHandler(), - new SamlSecurityTokenHandler(), - new JsonWebTokenHandler(){ MapInboundClaims = true } - }; - private TokenValidationParameters _tokenValidationParameters = new TokenValidationParameters(); /// @@ -46,6 +39,13 @@ public WsFederationOptions() // If you manage to get it configured, then you can set RemoteSignOutPath accordingly. RemoteSignOutPath = "/signin-wsfed"; Events = new WsFederationEvents(); + + TokenHandlers = new Collection() + { + new Saml2SecurityTokenHandler(), + new SamlSecurityTokenHandler(), + new JsonWebTokenHandler{ MapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims } + }; } /// @@ -122,14 +122,7 @@ public ICollection SecurityTokenHandlers /// public ICollection TokenHandlers { - get - { - return _tokenHandlers; - } - set - { - _tokenHandlers = value ?? throw new ArgumentNullException(nameof(TokenHandlers)); - } + get; private set; } /// @@ -207,11 +200,13 @@ public TokenValidationParameters TokenValidationParameters public new bool SaveTokens { get; set; } /// - /// Gets of sets the property to control if or will be used to validate the inbound token. - /// The advantage of using the TokenHandlers is: + /// Gets or sets whether or will be used to validate the inbound token. + /// + /// + /// The advantage of using the TokenHandlers are: /// There is an Async model. - /// The default token handler is a which is 30 % faster when validating. + /// The default token handler for JsonWebTokens is a which is faster than a . /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. - /// - public bool UseTokenHandlers { get; set; } + /// + public bool UseTokenHandlers { get; set; } = true; } diff --git a/src/Security/Authentication/test/JwtBearerTests.cs b/src/Security/Authentication/test/JwtBearerTests.cs index c6c9268cca26..10a8f2dd5947 100755 --- a/src/Security/Authentication/test/JwtBearerTests.cs +++ b/src/Security/Authentication/test/JwtBearerTests.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; namespace Microsoft.AspNetCore.Authentication.JwtBearer; @@ -42,6 +43,13 @@ private void ConfigureDefaults(JwtBearerOptions o) { } + [Fact] + public void MapInboundClaimsDefaults() + { + JwtBearerOptions jwtBearerOptions = new JwtBearerOptions(); + Assert() + } + [Fact] public async Task BearerTokenValidation() { @@ -121,9 +129,18 @@ public void MapInboundClaimsDefaultsToTrue() { var options = new JwtBearerOptions(); Assert.True(options.MapInboundClaims); + var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler; Assert.NotNull(jwtHandler); Assert.True(jwtHandler.MapInboundClaims); + + var tokenHandler = options.TokenHandlers.First() as JsonWebTokenHandler; + Assert.NotNull(tokenHandler); + Assert.True(tokenHandler.MapInboundClaims); + + options.MapInboundClaims = false; + Assert.False(jwtHandler.MapInboundClaims); + Assert.False(tokenHandler.MapInboundClaims); } [Fact] From 0402406d3357034cf11f57e42495634141a533f5 Mon Sep 17 00:00:00 2001 From: Brent Schmaltz Date: Sat, 8 Jul 2023 10:24:00 -0700 Subject: [PATCH 09/19] Removed setter. --- .../Authentication/WsFederation/src/PublicAPI.Unshipped.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt index 717284fbda1b..fa17d772ba12 100644 --- a/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt @@ -1,6 +1,5 @@ #nullable enable Microsoft.AspNetCore.Authentication.WsFederation.WsFederationHandler.WsFederationHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.UrlEncoder! encoder) -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.get -> System.Collections.Generic.ICollection! -Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.set -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.get -> bool Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.set -> void From b208837500902d310b151f5439dff947be35653d Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Tue, 11 Jul 2023 09:47:10 -0700 Subject: [PATCH 10/19] Use 7.0.0-preview for identity model libraries --- eng/Versions.props | 8 ++++---- .../OpenIdConnect/src/OpenIdConnectHandler.cs | 12 +++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index bd2c5625cc9d..ee64a1108827 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -248,15 +248,15 @@ 1.1.2-beta1.22531.1 1.1.2-beta1.22531.1 1.0.0-20230414.1 - 6.31.0 - 6.31.0 - 6.31.0 + 7.0.0-preview + 7.0.0-preview + 7.0.0-preview 2.2.1 1.0.1 3.0.1 3.0.1 11.1.0 - 6.31.0 + 7.0.0-preview 5.0.0 5.0.0-alpha.20560.6 5.0.0 diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs index c6abec03ec18..f234f2059b24 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs @@ -656,8 +656,7 @@ protected override async Task HandleRemoteAuthenticateAsync { var tokenValidationResult = await ValidateTokenUsingHandlerAsync(authorizationResponse.IdToken, properties, validationParameters); user = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); - // todo: need to use converter in 7x branch here - jwt = new JwtSecurityToken(); + jwt = JwtSecurityTokenConverter.Convert(tokenValidationResult.SecurityToken as JsonWebToken); } else { @@ -737,8 +736,7 @@ protected override async Task HandleRemoteAuthenticateAsync { var tokenValidationResult = await ValidateTokenUsingHandlerAsync(tokenEndpointResponse.IdToken, properties, validationParameters); tokenEndpointUser = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); - // TODO: need to use converter to create jwt - tokenEndpointJwt = new JwtSecurityToken(); + tokenEndpointJwt = JwtSecurityTokenConverter.Convert(tokenValidationResult.SecurityToken as JsonWebToken); } else { @@ -1323,7 +1321,11 @@ private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties p // Note this modifies properties if Options.UseTokenLifetime private async Task ValidateTokenUsingHandlerAsync(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters) { - if (_configuration != null) + if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager) + { + validationParameters.ConfigurationManager = baseConfigurationManager; + } + else if (_configuration != null) { var issuer = new[] { _configuration.Issuer }; validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuer) ?? issuer; From df64e724c8554b60efe78c73c3dab5b31a70b58d Mon Sep 17 00:00:00 2001 From: Brent Schmaltz Date: Tue, 11 Jul 2023 10:48:31 -0700 Subject: [PATCH 11/19] fix bild break, useTokenHanlders default false. --- .../Authentication/JwtBearer/src/JwtBearerOptions.cs | 2 +- .../Authentication/WsFederation/src/WsFederationOptions.cs | 2 +- src/Security/Authentication/test/JwtBearerTests.cs | 7 ------- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs index 0c53c2adb692..e274790b5f4a 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs @@ -180,5 +180,5 @@ public bool MapInboundClaims /// The default token handler is a which is faster than a . /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. /// - public bool UseTokenHandlers { get; set; } = true; + public bool UseTokenHandlers { get; set; } } diff --git a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs index 7e9d3ffa4bea..a3b4f4512f82 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs @@ -208,5 +208,5 @@ public TokenValidationParameters TokenValidationParameters /// The default token handler for JsonWebTokens is a which is faster than a . /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. /// - public bool UseTokenHandlers { get; set; } = true; + public bool UseTokenHandlers { get; set; } } diff --git a/src/Security/Authentication/test/JwtBearerTests.cs b/src/Security/Authentication/test/JwtBearerTests.cs index 10a8f2dd5947..30eedcd9ee19 100755 --- a/src/Security/Authentication/test/JwtBearerTests.cs +++ b/src/Security/Authentication/test/JwtBearerTests.cs @@ -43,13 +43,6 @@ private void ConfigureDefaults(JwtBearerOptions o) { } - [Fact] - public void MapInboundClaimsDefaults() - { - JwtBearerOptions jwtBearerOptions = new JwtBearerOptions(); - Assert() - } - [Fact] public async Task BearerTokenValidation() { From 9a4cc563d0a0af39c312fb13a87f2da5c6c7af1f Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Tue, 11 Jul 2023 16:39:09 -0700 Subject: [PATCH 12/19] use var for identitymodel versions --- eng/Versions.props | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index ee64a1108827..e6fc50fbd089 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -11,6 +11,7 @@ 0 6 true + 7.0.0-preview @@ -248,15 +249,15 @@ 1.1.2-beta1.22531.1 1.1.2-beta1.22531.1 1.0.0-20230414.1 - 7.0.0-preview - 7.0.0-preview - 7.0.0-preview + $(IdentityModelVersion) + $(IdentityModelVersion) + $(IdentityModelVersion) 2.2.1 1.0.1 3.0.1 3.0.1 11.1.0 - 7.0.0-preview + $(IdentityModelVersion) 5.0.0 5.0.0-alpha.20560.6 5.0.0 From a4c116c18f62ad9e38d9dfe9d570775e2b54a288 Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Tue, 11 Jul 2023 16:43:30 -0700 Subject: [PATCH 13/19] Move new apis to unshipped --- .../Authentication/OpenIdConnect/src/PublicAPI.Shipped.txt | 6 ------ .../OpenIdConnect/src/PublicAPI.Unshipped.txt | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Shipped.txt b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Shipped.txt index 3196b868dc99..443f7f561a9d 100644 --- a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Shipped.txt +++ b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Shipped.txt @@ -210,9 +210,3 @@ virtual Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.H virtual Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleSignOutCallbackAsync() -> System.Threading.Tasks.Task! virtual Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RedeemAuthorizationCodeAsync(Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage! tokenEndpointRequest) -> System.Threading.Tasks.Task! virtual Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.SignOutAsync(Microsoft.AspNetCore.Authentication.AuthenticationProperties? properties) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaimsTokenHandler.get -> bool -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaimsTokenHandler.set -> void -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.get -> Microsoft.IdentityModel.Tokens.TokenHandler! -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.set -> void -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseTokenHandler.get -> bool -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseTokenHandler.set -> void diff --git a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt index 8ff5e3305e99..0c5388da6f06 100644 --- a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt @@ -1,2 +1,8 @@ #nullable enable Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.OpenIdConnectHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.HtmlEncoder! htmlEncoder, System.Text.Encodings.Web.UrlEncoder! encoder) -> void +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaimsTokenHandler.get -> bool +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaimsTokenHandler.set -> void +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.get -> Microsoft.IdentityModel.Tokens.TokenHandler! +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.set -> void +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseTokenHandler.get -> bool +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseTokenHandler.set -> void From e63b5ceb275c6c0463c3b183034837729f39c095 Mon Sep 17 00:00:00 2001 From: Brent Schmaltz Date: Tue, 11 Jul 2023 20:21:57 -0700 Subject: [PATCH 14/19] Increase key size to 256 bits or HMAC will fail. --- src/SignalR/server/SignalR/test/Startup.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SignalR/server/SignalR/test/Startup.cs b/src/SignalR/server/SignalR/test/Startup.cs index a567a0d4e36e..a9b7254160ff 100644 --- a/src/SignalR/server/SignalR/test/Startup.cs +++ b/src/SignalR/server/SignalR/test/Startup.cs @@ -5,6 +5,7 @@ using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Security.Claims; +using System.Security.Cryptography; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; @@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests; public class Startup { - private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); + private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(SHA256.HashData(Guid.NewGuid().ToByteArray())); private readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); public void ConfigureServices(IServiceCollection services) From 4501b82c05bc64c4d7bfc5a8f6705eee4e155c74 Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Mon, 17 Jul 2023 09:28:25 -0700 Subject: [PATCH 15/19] update version of identity model libraries (#49349) Co-authored-by: Keegan Caruso --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index 72af5327b996..c6f87d65fcc7 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -11,7 +11,7 @@ 0 7 true - 6.31.0 + 7.0.0-preview From 583728ea481f09ddd5a9ffa3b00e2f4ec87785da Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 18 Jul 2023 08:43:17 -0700 Subject: [PATCH 16/19] Update Wilson7 branch (#49491) * SymmetricSecurityKey needs 32 bytes * Update source-build-externals dependencies --------- Co-authored-by: Keegan Caruso --- eng/Version.Details.xml | 4 ++-- eng/Versions.props | 2 +- .../clients/csharp/Client/test/FunctionalTests/Startup.cs | 3 ++- src/SignalR/clients/ts/FunctionalTests/Startup.cs | 3 ++- .../Http.Connections/test/HttpConnectionDispatcherTests.cs | 5 +++-- src/SignalR/samples/JwtSample/Startup.cs | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1e062867fae0..5794d7ea0681 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -185,9 +185,9 @@ https://github.com/dotnet/runtime 5d54b08d5fc40d0b1c156f430a487a94c1e34f79 - + https://github.com/dotnet/source-build-externals - ac076c101e6fe5e8fbfbd0a0ab878bd3313d9138 + 844e2cd86e7525d7eb32358e63a0c554187eb26b diff --git a/eng/Versions.props b/eng/Versions.props index c6f87d65fcc7..b005e112e16c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -163,7 +163,7 @@ 8.0.0-beta.23328.1 - 8.0.0-alpha.1.23329.1 + 8.0.0-alpha.1.23368.1 8.0.0-alpha.1.23356.4 diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs index 91286fa5378d..b20a20325aa2 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs @@ -5,6 +5,7 @@ using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Security.Claims; +using System.Security.Cryptography; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.Negotiate; using Microsoft.AspNetCore.Authorization; @@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests; public class Startup { - private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); + private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(SHA256.HashData(Guid.NewGuid().ToByteArray())); private readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); public void ConfigureServices(IServiceCollection services) diff --git a/src/SignalR/clients/ts/FunctionalTests/Startup.cs b/src/SignalR/clients/ts/FunctionalTests/Startup.cs index 9182753d6a2e..2dda5dc55538 100644 --- a/src/SignalR/clients/ts/FunctionalTests/Startup.cs +++ b/src/SignalR/clients/ts/FunctionalTests/Startup.cs @@ -5,6 +5,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Reflection; using System.Security.Claims; +using System.Security.Cryptography; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http.Connections; @@ -19,7 +20,7 @@ namespace FunctionalTests; public class Startup { - private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); + private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(SHA256.HashData(Guid.NewGuid().ToByteArray())); private readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); private int _numRedirects; diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs index f083e9b6b897..d0ecdcf21560 100644 --- a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs +++ b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs @@ -10,6 +10,7 @@ using System.Net.Http; using System.Net.WebSockets; using System.Security.Claims; +using System.Security.Cryptography; using System.Security.Principal; using System.Text; using Microsoft.AspNetCore.Authentication; @@ -2988,7 +2989,7 @@ public async Task ConnectionClosedRequestedTriggeredOnAuthExpiration() [InlineData(HttpTransportType.WebSockets)] public async Task AuthenticationExpirationSetOnAuthenticatedConnectionWithJWT(HttpTransportType transportType) { - SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); + SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(SHA256.HashData(Guid.NewGuid().ToByteArray())); JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); using var host = CreateHost(services => @@ -3150,7 +3151,7 @@ public async Task AuthenticationExpirationSetOnAuthenticatedConnectionWithCookie [InlineData(HttpTransportType.WebSockets)] public async Task AuthenticationExpirationUsesCorrectScheme(HttpTransportType transportType) { - var SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); + var SecurityKey = new SymmetricSecurityKey(SHA256.HashData(Guid.NewGuid().ToByteArray())); var JwtTokenHandler = new JwtSecurityTokenHandler(); using var host = CreateHost(services => diff --git a/src/SignalR/samples/JwtSample/Startup.cs b/src/SignalR/samples/JwtSample/Startup.cs index 18b1ae21ea35..cefde00a4cd0 100644 --- a/src/SignalR/samples/JwtSample/Startup.cs +++ b/src/SignalR/samples/JwtSample/Startup.cs @@ -11,7 +11,7 @@ namespace JwtSample; public class Startup { - private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(RandomNumberGenerator.GetBytes(16)); + private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(RandomNumberGenerator.GetBytes(32)); private readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); public void ConfigureServices(IServiceCollection services) From f22eba991ab10e433f4a209ce8cadbf5a7c40f9f Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Wed, 19 Jul 2023 19:30:40 -0700 Subject: [PATCH 17/19] Update Wilson7 branch (#49524) * Update Wilson7 branch Default to using new handlers Changes from API review * Handle obsolete members * Handle obsolete members in tests * Add aka.ms link --------- Co-authored-by: Keegan Caruso --- .../JwtBearer/src/JwtBearerHandler.cs | 17 +- .../JwtBearer/src/JwtBearerOptions.cs | 5 +- .../JwtBearer/src/PublicAPI.Unshipped.txt | 4 +- .../WsFederation/src/PublicAPI.Unshipped.txt | 4 +- .../WsFederation/src/Resources.resx | 2 +- .../WsFederation/src/WsFederationHandler.cs | 6 +- .../WsFederation/src/WsFederationOptions.cs | 5 +- .../Authentication/test/JwtBearerTests.cs | 69 +- .../test/JwtBearerTests_Handler.cs | 1297 +++++++++++++++++ .../test/WsFederation/TestTokenHandler.cs | 30 + .../test/WsFederation/WsFederationTest.cs | 3 + .../WsFederation/WsFederationTest_Handler.cs | 453 ++++++ 12 files changed, 1867 insertions(+), 28 deletions(-) create mode 100644 src/Security/Authentication/test/JwtBearerTests_Handler.cs create mode 100644 src/Security/Authentication/test/WsFederation/TestTokenHandler.cs create mode 100644 src/Security/Authentication/test/WsFederation/WsFederationTest_Handler.cs diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs index 88da59dfa116..c880cb555380 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; @@ -20,8 +19,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer; /// public class JwtBearerHandler : AuthenticationHandler { - private OpenIdConnectConfiguration? _configuration; - /// /// Initializes a new instance of . /// @@ -101,7 +98,7 @@ protected override async Task HandleAuthenticateAsync() SecurityToken? validatedToken = null; ClaimsPrincipal? principal = null; - if (Options.UseTokenHandlers) + if (!Options.UseSecurityTokenValidators) { foreach (var tokenHandler in Options.TokenHandlers) { @@ -123,12 +120,13 @@ protected override async Task HandleAuthenticateAsync() catch (Exception ex) { validationFailures ??= new List(1); - RecordTokenValidationError(new SecurityTokenValidationException($"TokenHandler: '{tokenHandler}', threw an exception (see inner exception).", ex), validationFailures); + RecordTokenValidationError(ex, validationFailures); } } } else { +#pragma warning disable CS0618 // Type or member is obsolete foreach (var validator in Options.SecurityTokenValidators) { if (validator.CanReadToken(token)) @@ -145,6 +143,7 @@ protected override async Task HandleAuthenticateAsync() } } } +#pragma warning restore CS0618 // Type or member is obsolete } if (principal != null && validatedToken != null) @@ -194,7 +193,7 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Fail(authenticationFailedContext.Exception); } - if (Options.UseTokenHandlers) + if (!Options.UseSecurityTokenValidators) { return AuthenticateResults.TokenHandlerUnableToValidate; } @@ -251,10 +250,10 @@ private async Task SetupTokenValidationParametersAsyn if (Options.ConfigurationManager != null) { // GetConfigurationAsync has a time interval that must pass before new http request will be issued. - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); - var issuers = new[] { _configuration.Issuer }; + var configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + var issuers = new[] { configuration.Issuer }; tokenValidationParameters.ValidIssuers = (tokenValidationParameters.ValidIssuers == null ? issuers : tokenValidationParameters.ValidIssuers.Concat(issuers)); - tokenValidationParameters.IssuerSigningKeys = (tokenValidationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : tokenValidationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + tokenValidationParameters.IssuerSigningKeys = (tokenValidationParameters.IssuerSigningKeys == null ? configuration.SigningKeys : tokenValidationParameters.IssuerSigningKeys.Concat(configuration.SigningKeys)); } } diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs index e274790b5f4a..3ef2893b6fef 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs @@ -28,7 +28,9 @@ public class JwtBearerOptions : AuthenticationSchemeOptions /// public JwtBearerOptions() { +#pragma warning disable CS0618 // Type or member is obsolete SecurityTokenValidators = new List { _defaultHandler }; +#pragma warning restore CS0618 // Type or member is obsolete TokenHandlers = new List { _defaultTokenHandler }; } @@ -111,6 +113,7 @@ public JwtBearerOptions() /// /// Gets the ordered list of used to validate access tokens. /// + [Obsolete("SecurityTokenValidators is no longer used by default. Use TokenHandlers instead. To continue using SecurityTokenValidators, set UseSecurityTokenValidators to true. See https://aka.ms/aspnetcore8/security-token-changes")] public IList SecurityTokenValidators { get; private set; } /// @@ -180,5 +183,5 @@ public bool MapInboundClaims /// The default token handler is a which is faster than a . /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. /// - public bool UseTokenHandlers { get; set; } + public bool UseSecurityTokenValidators { get; set; } } diff --git a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt index 0e7f7787af03..4626d5e95af8 100644 --- a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.JwtBearerHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.UrlEncoder! encoder) -> void Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenHandlers.get -> System.Collections.Generic.IList! -Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.get -> bool -Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.set -> void +Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseSecurityTokenValidators.get -> bool +Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseSecurityTokenValidators.set -> void diff --git a/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt index fa17d772ba12..473f251c9c8a 100644 --- a/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable Microsoft.AspNetCore.Authentication.WsFederation.WsFederationHandler.WsFederationHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.UrlEncoder! encoder) -> void Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.get -> System.Collections.Generic.ICollection! -Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.get -> bool -Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.set -> void +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseSecurityTokenHandlers.get -> bool +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseSecurityTokenHandlers.set -> void diff --git a/src/Security/Authentication/WsFederation/src/Resources.resx b/src/Security/Authentication/WsFederation/src/Resources.resx index e2edafb671bd..d006fa4ea32e 100644 --- a/src/Security/Authentication/WsFederation/src/Resources.resx +++ b/src/Security/Authentication/WsFederation/src/Resources.resx @@ -121,7 +121,7 @@ The service descriptor is missing. - No token validator was found for the given token. + No token validator or token handler was found for the given token. The '{0}' option must be provided. diff --git a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs index 0be2de5334f0..3b2594b8137a 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs @@ -245,7 +245,7 @@ protected override async Task HandleRemoteAuthenticateAsync var tvp = await SetupTokenValidationParametersAsync(); ClaimsPrincipal? principal = null; SecurityToken? validatedToken = null; - if (Options.UseTokenHandlers) + if (!Options.UseSecurityTokenHandlers) { foreach (var tokenHandler in Options.TokenHandlers) { @@ -277,6 +277,7 @@ protected override async Task HandleRemoteAuthenticateAsync else { +#pragma warning disable CS0618 // Type or member is obsolete foreach (var validator in Options.SecurityTokenHandlers) { if (validator.CanReadToken(token)) @@ -294,11 +295,11 @@ protected override async Task HandleRemoteAuthenticateAsync break; } } +#pragma warning restore CS0618 // Type or member is obsolete } if (principal == null) { - // TODO - need new string for TokenHandler if (validationFailures == null || validationFailures.Count == 0) { throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound); @@ -375,7 +376,6 @@ private async Task SetupTokenValidationParametersAsyn if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager) { - // TODO - we need to add a parameter to TokenValidationParameters for the CancellationToken. tokenValidationParameters.ConfigurationManager = baseConfigurationManager; } else diff --git a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs index a3b4f4512f82..bfb8de3b36c7 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs @@ -105,6 +105,7 @@ public override void Validate() /// /// Gets or sets the collection of used to read and validate the s. /// + [Obsolete("SecurityTokenHandlers is no longer used by default. Use TokenHandlers instead. To continue using SecurityTokenHandlers, set UseSecurityTokenHandlers to true. See https://aka.ms/aspnetcore8/security-token-changes")] public ICollection SecurityTokenHandlers { get @@ -118,7 +119,7 @@ public ICollection SecurityTokenHandlers } /// - /// Gets or sets the collection of used to read and validate the s. + /// Gets the collection of used to read and validate the s. /// public ICollection TokenHandlers { @@ -208,5 +209,5 @@ public TokenValidationParameters TokenValidationParameters /// The default token handler for JsonWebTokens is a which is faster than a . /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. /// - public bool UseTokenHandlers { get; set; } + public bool UseSecurityTokenHandlers { get; set; } } diff --git a/src/Security/Authentication/test/JwtBearerTests.cs b/src/Security/Authentication/test/JwtBearerTests.cs index 30eedcd9ee19..502afab2db02 100755 --- a/src/Security/Authentication/test/JwtBearerTests.cs +++ b/src/Security/Authentication/test/JwtBearerTests.cs @@ -71,6 +71,7 @@ public async Task BearerTokenValidation() ValidAudience = "audience.contoso.com", IssuerSigningKey = key, }; + o.UseSecurityTokenValidators = true; }); var newBearerToken = "Bearer " + tokenText; @@ -108,6 +109,7 @@ public async Task SaveBearerToken() ValidAudience = "audience.contoso.com", IssuerSigningKey = key, }; + o.UseSecurityTokenValidators = true; }); var newBearerToken = "Bearer " + tokenText; @@ -123,7 +125,9 @@ public void MapInboundClaimsDefaultsToTrue() var options = new JwtBearerOptions(); Assert.True(options.MapInboundClaims); +#pragma warning disable CS0618 // Type or member is obsolete var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler; +#pragma warning restore CS0618 // Type or member is obsolete Assert.NotNull(jwtHandler); Assert.True(jwtHandler.MapInboundClaims); @@ -142,7 +146,9 @@ public void MapInboundClaimsCanBeSetToFalse() var options = new JwtBearerOptions(); options.MapInboundClaims = false; Assert.False(options.MapInboundClaims); +#pragma warning disable CS0618 // Type or member is obsolete var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler; +#pragma warning restore CS0618 // Type or member is obsolete Assert.NotNull(jwtHandler); Assert.False(jwtHandler.MapInboundClaims); } @@ -183,8 +189,11 @@ public async Task ThrowAtAuthenticationFailedEvent() return Task.FromResult(0); } }; + o.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete o.SecurityTokenValidators.Clear(); o.SecurityTokenValidators.Insert(0, new InvalidTokenValidator()); +#pragma warning restore CS0618 // Type or member is obsolete }, async (context, next) => { @@ -228,6 +237,7 @@ public async Task CustomHeaderReceived() return Task.FromResult(null); } }; + o.UseSecurityTokenValidators = true; }); using var server = host.GetTestServer(); @@ -239,7 +249,7 @@ public async Task CustomHeaderReceived() [Fact] public async Task NoHeaderReceived() { - using var host = await CreateHost(); + using var host = await CreateHost(o => o.UseSecurityTokenValidators = true); using var server = host.GetTestServer(); var response = await SendAsync(server, "http://example.com/oauth"); Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); @@ -248,7 +258,7 @@ public async Task NoHeaderReceived() [Fact] public async Task HeaderWithoutBearerReceived() { - using var host = await CreateHost(); + using var host = await CreateHost(o => o.UseSecurityTokenValidators = true); using var server = host.GetTestServer(); var response = await SendAsync(server, "http://example.com/oauth", "Token"); Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); @@ -257,7 +267,7 @@ public async Task HeaderWithoutBearerReceived() [Fact] public async Task UnrecognizedTokenReceived() { - using var host = await CreateHost(); + using var host = await CreateHost(o => o.UseSecurityTokenValidators = true); using var server = host.GetTestServer(); var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); @@ -269,8 +279,11 @@ public async Task InvalidTokenReceived() { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new InvalidTokenValidator()); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -293,8 +306,11 @@ public async Task ExceptionReportedInHeaderForAuthenticationFailures(Type errorT { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType)); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -314,8 +330,11 @@ public async Task ExceptionReportedInHeaderWithDetailsForAuthenticationFailures( { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new DetailedInvalidTokenValidator(errorType)); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -331,8 +350,11 @@ public async Task ExceptionNotReportedInHeaderForOtherFailures(Type errorType) { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType)); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -347,9 +369,12 @@ public async Task ExceptionsReportedInHeaderForMultipleAuthenticationFailures() { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenInvalidAudienceException))); options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenSignatureKeyNotFoundException))); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -382,6 +407,7 @@ public async Task ExceptionsReportedInHeaderExposesUserDefinedError(string error return Task.FromResult(0); } }; + options.UseSecurityTokenValidators = true; }); using var server = host.GetTestServer(); @@ -430,6 +456,7 @@ public async Task ExceptionNotReportedInHeaderWhenIncludeErrorDetailsIsFalse() using var host = await CreateHost(o => { o.IncludeErrorDetails = false; + o.UseSecurityTokenValidators = true; }); using var server = host.GetTestServer(); @@ -442,7 +469,7 @@ public async Task ExceptionNotReportedInHeaderWhenIncludeErrorDetailsIsFalse() [Fact] public async Task ExceptionNotReportedInHeaderWhenTokenWasMissing() { - using var host = await CreateHost(); + using var host = await CreateHost(o => o.UseSecurityTokenValidators = true); using var server = host.GetTestServer(); var response = await SendAsync(server, "http://example.com/oauth"); @@ -477,8 +504,11 @@ public async Task CustomTokenValidated() return Task.FromResult(null); } }; + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator(JwtBearerDefaults.AuthenticationScheme)); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -500,11 +530,14 @@ public async Task RetrievingTokenFromAlternateLocation() return Task.FromResult(null); } }; + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT", token => { Assert.Equal("CustomToken", token); })); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -518,6 +551,7 @@ public async Task EventOnMessageReceivedSkip_NoMoreEventsExecuted() { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; options.Events = new JwtBearerEvents() { OnMessageReceived = context => @@ -551,6 +585,7 @@ public async Task EventOnMessageReceivedReject_NoMoreEventsExecuted() { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; options.Events = new JwtBearerEvents() { OnMessageReceived = context => @@ -604,8 +639,11 @@ public async Task EventOnTokenValidatedSkip_NoMoreEventsExecuted() throw new NotImplementedException(); }, }; + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -636,8 +674,11 @@ public async Task EventOnTokenValidatedReject_NoMoreEventsExecuted() throw new NotImplementedException(); }, }; + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -670,8 +711,11 @@ public async Task EventOnAuthenticationFailedSkip_NoMoreEventsExecuted() throw new NotImplementedException(); }, }; + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -702,8 +746,11 @@ public async Task EventOnAuthenticationFailedReject_NoMoreEventsExecuted() throw new NotImplementedException(); }, }; + options.UseSecurityTokenValidators = true; +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); +#pragma warning restore CS0618 // Type or member is obsolete }); using var server = host.GetTestServer(); @@ -728,6 +775,7 @@ public async Task EventOnChallengeSkip_ResponseNotModified() return Task.FromResult(0); }, }; + o.UseSecurityTokenValidators = true; }); using var server = host.GetTestServer(); @@ -750,6 +798,7 @@ public async Task EventOnForbidden_ResponseNotModified() ValidAudience = "audience.contoso.com", IssuerSigningKey = tokenData.key, }; + o.UseSecurityTokenValidators = true; }); var newBearerToken = "Bearer " + tokenData.tokenText; using var server = host.GetTestServer(); @@ -776,6 +825,7 @@ public async Task EventOnForbiddenSkip_ResponseNotModified() return Task.FromResult(0); } }; + o.UseSecurityTokenValidators = true; }); var newBearerToken = "Bearer " + tokenData.tokenText; using var server = host.GetTestServer(); @@ -803,6 +853,7 @@ public async Task EventOnForbidden_ResponseModified() return context.Response.WriteAsync("You Shall Not Pass"); } }; + o.UseSecurityTokenValidators = true; }); var newBearerToken = "Bearer " + tokenData.tokenText; using var server = host.GetTestServer(); @@ -846,8 +897,8 @@ public async Task EventOnForbidden_ResponseForMultipleAuthenticationSchemas() .ConfigureServices(services => { services.AddAuthentication() - .AddJwtBearer("JwtAuthSchemaOne", o => { o.Events = jwtBearerEvents; }) - .AddJwtBearer("JwtAuthSchemaTwo", o => { o.Events = jwtBearerEvents; }); + .AddJwtBearer("JwtAuthSchemaOne", o => { o.Events = jwtBearerEvents; o.UseSecurityTokenValidators = true; }) + .AddJwtBearer("JwtAuthSchemaTwo", o => { o.Events = jwtBearerEvents; o.UseSecurityTokenValidators = true; }); })) .Build(); @@ -891,6 +942,7 @@ public async Task ExpirationAndIssuedSetOnAuthenticateResult() ValidAudience = "audience.contoso.com", IssuerSigningKey = key, }; + o.UseSecurityTokenValidators = true; }); var newBearerToken = "Bearer " + tokenText; @@ -934,6 +986,7 @@ public async Task ExpirationAndIssuedNullWhenMinOrMaxValue() ValidAudience = "audience.contoso.com", IssuerSigningKey = key, }; + o.UseSecurityTokenValidators = true; }); var newBearerToken = "Bearer " + tokenText; @@ -966,7 +1019,7 @@ public void CanReadJwtBearerOptionsFromConfig() { o.AddScheme("Bearer", "Bearer"); }); - builder.AddJwtBearer("Bearer"); + builder.AddJwtBearer("Bearer", o => o.UseSecurityTokenValidators = true); RegisterAuth(builder, _ => { }); var sp = services.BuildServiceProvider(); @@ -1004,7 +1057,7 @@ public void CanReadMultipleIssuersFromConfig() { o.AddScheme("Bearer", "Bearer"); }); - builder.AddJwtBearer("Bearer"); + builder.AddJwtBearer("Bearer", o => o.UseSecurityTokenValidators = true); RegisterAuth(builder, _ => { }); var sp = services.BuildServiceProvider(); diff --git a/src/Security/Authentication/test/JwtBearerTests_Handler.cs b/src/Security/Authentication/test/JwtBearerTests_Handler.cs new file mode 100644 index 000000000000..daf20680b687 --- /dev/null +++ b/src/Security/Authentication/test/JwtBearerTests_Handler.cs @@ -0,0 +1,1297 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using System.Xml.Linq; +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer; + +public class JwtBearerTests_Handler : SharedAuthenticationTests +{ + protected override string DefaultScheme => JwtBearerDefaults.AuthenticationScheme; + protected override Type HandlerType => typeof(JwtBearerHandler); + protected override bool SupportsSignIn { get => false; } + protected override bool SupportsSignOut { get => false; } + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) + { + services.AddJwtBearer(o => + { + ConfigureDefaults(o); + configure.Invoke(o); + }); + } + + private void ConfigureDefaults(JwtBearerOptions o) + { + } + + [Fact] + public async Task BearerTokenValidation() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob") + }; + + var token = new JwtSecurityToken( + issuer: "issuer.contoso.com", + audience: "audience.contoso.com", + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: creds); + + var tokenText = new JwtSecurityTokenHandler().WriteToken(token); + + using var host = await CreateHost(o => + { + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = key, + }; + }); + + var newBearerToken = "Bearer " + tokenText; + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", newBearerToken); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + } + + [Fact] + public async Task SaveBearerToken() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob") + }; + + var token = new JwtSecurityToken( + issuer: "issuer.contoso.com", + audience: "audience.contoso.com", + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: creds); + + var tokenText = new JwtSecurityTokenHandler().WriteToken(token); + + using var host = await CreateHost(o => + { + o.SaveToken = true; + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = key, + }; + }); + + var newBearerToken = "Bearer " + tokenText; + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/token", newBearerToken); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(tokenText, await response.Response.Content.ReadAsStringAsync()); + } + + [Fact] + public void MapInboundClaimsDefaultsToTrue() + { + var options = new JwtBearerOptions(); + Assert.True(options.MapInboundClaims); + +#pragma warning disable CS0618 // Type or member is obsolete + var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler; +#pragma warning restore CS0618 // Type or member is obsolete + Assert.NotNull(jwtHandler); + Assert.True(jwtHandler.MapInboundClaims); + + var tokenHandler = options.TokenHandlers.First() as JsonWebTokenHandler; + Assert.NotNull(tokenHandler); + Assert.True(tokenHandler.MapInboundClaims); + + options.MapInboundClaims = false; + Assert.False(jwtHandler.MapInboundClaims); + Assert.False(tokenHandler.MapInboundClaims); + } + + [Fact] + public void MapInboundClaimsCanBeSetToFalse() + { + var options = new JwtBearerOptions(); + options.MapInboundClaims = false; + Assert.False(options.MapInboundClaims); +#pragma warning disable CS0618 // Type or member is obsolete + var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler; +#pragma warning restore CS0618 // Type or member is obsolete + Assert.NotNull(jwtHandler); + Assert.False(jwtHandler.MapInboundClaims); + } + + [Fact] + public async Task SignInThrows() + { + using var host = await CreateHost(); + using var server = host.GetTestServer(); + var transaction = await server.SendAsync("https://example.com/signIn"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + using var host = await CreateHost(); + using var server = host.GetTestServer(); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ThrowAtAuthenticationFailedEvent() + { + using var host = await CreateHost(o => + { + o.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + context.Response.StatusCode = 401; + throw new Exception(); + }, + OnMessageReceived = context => + { + context.Token = "something"; + return Task.FromResult(0); + } + }; + o.TokenHandlers.Clear(); + o.TokenHandlers.Insert(0, new InvalidTokenValidator()); + }, + async (context, next) => + { + try + { + await next(); + Assert.False(true, "Expected exception is not thrown"); + } + catch (Exception) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("i got this"); + } + }); + + using var server = host.GetTestServer(); + var transaction = await server.SendAsync("https://example.com/signIn"); + + Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); + } + + [Fact] + public async Task CustomHeaderReceived() + { + using var host = await CreateHost(o => + { + o.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + + return Task.FromResult(null); + } + }; + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "someHeader someblob"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal("Bob le Magnifique", response.ResponseText); + } + + [Fact] + public async Task NoHeaderReceived() + { + using var host = await CreateHost(); + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + } + + [Fact] + public async Task HeaderWithoutBearerReceived() + { + using var host = await CreateHost(); + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Token"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + } + + [Fact] + public async Task UnrecognizedTokenReceived() + { + using var host = await CreateHost(); + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task InvalidTokenReceived() + { + using var host = await CreateHost(options => + { + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new InvalidTokenValidator()); + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData(typeof(SecurityTokenInvalidAudienceException), "The audience '(null)' is invalid")] + [InlineData(typeof(SecurityTokenInvalidIssuerException), "The issuer '(null)' is invalid")] + [InlineData(typeof(SecurityTokenNoExpirationException), "The token has no expiration")] + [InlineData(typeof(SecurityTokenInvalidLifetimeException), "The token lifetime is invalid; NotBefore: '(null)', Expires: '(null)'")] + [InlineData(typeof(SecurityTokenNotYetValidException), "The token is not valid before '01/01/0001 00:00:00'")] + [InlineData(typeof(SecurityTokenExpiredException), "The token expired at '01/01/0001 00:00:00'")] + [InlineData(typeof(SecurityTokenInvalidSignatureException), "The signature is invalid")] + [InlineData(typeof(SecurityTokenSignatureKeyNotFoundException), "The signature key was not found")] + public async Task ExceptionReportedInHeaderForAuthenticationFailures(Type errorType, string message) + { + using var host = await CreateHost(options => + { + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new InvalidTokenValidator(errorType)); + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal($"Bearer error=\"invalid_token\", error_description=\"{message}\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData(typeof(SecurityTokenInvalidAudienceException), "The audience 'Bad Audience' is invalid")] + [InlineData(typeof(SecurityTokenInvalidIssuerException), "The issuer 'Bad Issuer' is invalid")] + [InlineData(typeof(SecurityTokenInvalidLifetimeException), "The token lifetime is invalid; NotBefore: '01/15/2001 00:00:00', Expires: '02/20/2000 00:00:00'")] + [InlineData(typeof(SecurityTokenNotYetValidException), "The token is not valid before '01/15/2045 00:00:00'")] + [InlineData(typeof(SecurityTokenExpiredException), "The token expired at '02/20/2000 00:00:00'")] + public async Task ExceptionReportedInHeaderWithDetailsForAuthenticationFailures(Type errorType, string message) + { + using var host = await CreateHost(options => + { + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new DetailedInvalidTokenValidator(errorType)); + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal($"Bearer error=\"invalid_token\", error_description=\"{message}\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData(typeof(ArgumentException))] + public async Task ExceptionNotReportedInHeaderForOtherFailures(Type errorType) + { + using var host = await CreateHost(options => + { + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new InvalidTokenValidator(errorType)); + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task ExceptionsReportedInHeaderForMultipleAuthenticationFailures() + { + using var host = await CreateHost(options => + { + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new InvalidTokenValidator(typeof(SecurityTokenInvalidAudienceException))); + options.TokenHandlers.Add(new InvalidTokenValidator(typeof(SecurityTokenSignatureKeyNotFoundException))); + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer error=\"invalid_token\", error_description=\"The audience '(null)' is invalid; The signature key was not found\"", + response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData("custom_error", "custom_description", "custom_uri")] + [InlineData("custom_error", "custom_description", null)] + [InlineData("custom_error", null, null)] + [InlineData(null, "custom_description", "custom_uri")] + [InlineData(null, "custom_description", null)] + [InlineData(null, null, "custom_uri")] + public async Task ExceptionsReportedInHeaderExposesUserDefinedError(string error, string description, string uri) + { + using var host = await CreateHost(options => + { + options.Events = new JwtBearerEvents + { + OnChallenge = context => + { + context.Error = error; + context.ErrorDescription = description; + context.ErrorUri = uri; + + return Task.FromResult(0); + } + }; + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("", response.ResponseText); + + var builder = new StringBuilder(JwtBearerDefaults.AuthenticationScheme); + + if (!string.IsNullOrEmpty(error)) + { + builder.Append(" error=\""); + builder.Append(error); + builder.Append("\""); + } + if (!string.IsNullOrEmpty(description)) + { + if (!string.IsNullOrEmpty(error)) + { + builder.Append(","); + } + + builder.Append(" error_description=\""); + builder.Append(description); + builder.Append('\"'); + } + if (!string.IsNullOrEmpty(uri)) + { + if (!string.IsNullOrEmpty(error) || + !string.IsNullOrEmpty(description)) + { + builder.Append(","); + } + + builder.Append(" error_uri=\""); + builder.Append(uri); + builder.Append('\"'); + } + + Assert.Equal(builder.ToString(), response.Response.Headers.WwwAuthenticate.First().ToString()); + } + + [Fact] + public async Task ExceptionNotReportedInHeaderWhenIncludeErrorDetailsIsFalse() + { + using var host = await CreateHost(o => + { + o.IncludeErrorDetails = false; + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task ExceptionNotReportedInHeaderWhenTokenWasMissing() + { + using var host = await CreateHost(); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task CustomTokenValidated() + { + using var host = await CreateHost(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + // Retrieve the NameIdentifier claim from the identity + // returned by the custom security token validator. + var identity = (ClaimsIdentity)context.Principal.Identity; + var identifier = identity.FindFirst(ClaimTypes.NameIdentifier); + + Assert.Equal("Bob le Tout Puissant", identifier.Value); + + // Remove the existing NameIdentifier claim and replace it + // with a new one containing a different value. + identity.RemoveClaim(identifier); + // Make sure to use a different name identifier + // than the one defined by BlobTokenValidator. + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique")); + + return Task.FromResult(null); + } + }; + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new BlobTokenValidator(JwtBearerDefaults.AuthenticationScheme)); + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal("Bob le Magnifique", response.ResponseText); + } + + [Fact] + public async Task RetrievingTokenFromAlternateLocation() + { + using var host = await CreateHost(options => + { + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.Token = "CustomToken"; + return Task.FromResult(null); + } + }; + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new BlobTokenValidator("JWT", token => + { + Assert.Equal("CustomToken", token); + })); + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal("Bob le Tout Puissant", response.ResponseText); + } + + [Fact] + public async Task EventOnMessageReceivedSkip_NoMoreEventsExecuted() + { + using var host = await CreateHost(options => + { + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.NoResult(); + return Task.FromResult(0); + }, + OnTokenValidated = context => + { + throw new NotImplementedException(); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnMessageReceivedReject_NoMoreEventsExecuted() + { + using var host = await CreateHost(options => + { + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.Fail("Authentication was aborted from user code."); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }, + OnTokenValidated = context => + { + throw new NotImplementedException(); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + using var server = host.GetTestServer(); + var exception = await Assert.ThrowsAsync(delegate + { + return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + } + + [Fact] + public async Task EventOnTokenValidatedSkip_NoMoreEventsExecuted() + { + using var host = await CreateHost(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + context.NoResult(); + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new BlobTokenValidator("JWT")); + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnTokenValidatedReject_NoMoreEventsExecuted() + { + using var host = await CreateHost(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + context.Fail("Authentication was aborted from user code."); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new BlobTokenValidator("JWT")); + }); + + using var server = host.GetTestServer(); + var exception = await Assert.ThrowsAsync(delegate + { + return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + } + + [Fact] + public async Task EventOnAuthenticationFailedSkip_NoMoreEventsExecuted() + { + using var host = await CreateHost(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + throw new Exception("Test Exception"); + }, + OnAuthenticationFailed = context => + { + context.NoResult(); + return Task.FromResult(0); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new BlobTokenValidator("JWT")); + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnAuthenticationFailedReject_NoMoreEventsExecuted() + { + using var host = await CreateHost(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + throw new Exception("Test Exception"); + }, + OnAuthenticationFailed = context => + { + context.Fail("Authentication was aborted from user code."); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new BlobTokenValidator("JWT")); + }); + + using var server = host.GetTestServer(); + var exception = await Assert.ThrowsAsync(delegate + { + return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + } + + [Fact] + public async Task EventOnChallengeSkip_ResponseNotModified() + { + using var host = await CreateHost(o => + { + o.Events = new JwtBearerEvents() + { + OnChallenge = context => + { + context.HandleResponse(); + return Task.FromResult(0); + }, + }; + }); + + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Empty(response.Response.Headers.WwwAuthenticate); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnForbidden_ResponseNotModified() + { + var tokenData = CreateStandardTokenAndKey(); + + using var host = await CreateHost(o => + { + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = tokenData.key, + }; + }); + var newBearerToken = "Bearer " + tokenData.tokenText; + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/forbidden", newBearerToken); + Assert.Equal(HttpStatusCode.Forbidden, response.Response.StatusCode); + } + + [Fact] + public async Task EventOnForbiddenSkip_ResponseNotModified() + { + var tokenData = CreateStandardTokenAndKey(); + using var host = await CreateHost(o => + { + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = tokenData.key, + }; + o.Events = new JwtBearerEvents() + { + OnForbidden = context => + { + return Task.FromResult(0); + } + }; + }); + var newBearerToken = "Bearer " + tokenData.tokenText; + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/forbidden", newBearerToken); + Assert.Equal(HttpStatusCode.Forbidden, response.Response.StatusCode); + } + + [Fact] + public async Task EventOnForbidden_ResponseModified() + { + var tokenData = CreateStandardTokenAndKey(); + using var host = await CreateHost(o => + { + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = tokenData.key, + }; + o.Events = new JwtBearerEvents() + { + OnForbidden = context => + { + context.Response.StatusCode = 418; + return context.Response.WriteAsync("You Shall Not Pass"); + } + }; + }); + var newBearerToken = "Bearer " + tokenData.tokenText; + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/forbidden", newBearerToken); + Assert.Equal(418, (int)response.Response.StatusCode); + Assert.Equal("You Shall Not Pass", await response.Response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task EventOnForbidden_ResponseForMultipleAuthenticationSchemas() + { + var onForbiddenCallCount = 0; + var jwtBearerEvents = new JwtBearerEvents() + { + OnForbidden = context => + { + onForbiddenCallCount++; + + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 418; + return context.Response.WriteAsync("You Shall Not Pass"); + } + return Task.CompletedTask; + } + }; + + using var host = new HostBuilder() + .ConfigureWebHost(builder => + builder.UseTestServer() + .Configure(app => + { + app.UseAuthentication(); + app.Run(async (context) => + { + // Simulate Forbidden By Multiple Authentication Schemas + await context.ForbidAsync("JwtAuthSchemaOne"); + await context.ForbidAsync("JwtAuthSchemaTwo"); + }); + }) + .ConfigureServices(services => + { + services.AddAuthentication() + .AddJwtBearer("JwtAuthSchemaOne", o => { o.Events = jwtBearerEvents; }) + .AddJwtBearer("JwtAuthSchemaTwo", o => { o.Events = jwtBearerEvents; }); + })) + .Build(); + + await host.StartAsync(); + + using var server = host.GetTestServer(); + var response = await server.CreateClient().SendAsync(new HttpRequestMessage(HttpMethod.Get, string.Empty)); + + Assert.Equal(418, (int)response.StatusCode); + Assert.Equal("You Shall Not Pass", await response.Content.ReadAsStringAsync()); + Assert.Equal(2, onForbiddenCallCount); + } + + [Fact] + public async Task ExpirationAndIssuedSetOnAuthenticateResult() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob") + }; + + var token = new JwtSecurityToken( + issuer: "issuer.contoso.com", + audience: "audience.contoso.com", + claims: claims, + notBefore: DateTime.Now.AddMinutes(-10), + expires: DateTime.Now.AddMinutes(30), + signingCredentials: creds); + + var tokenText = new JwtSecurityTokenHandler().WriteToken(token); + + using var host = await CreateHost(o => + { + o.SaveToken = true; + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = key, + }; + }); + + var newBearerToken = "Bearer " + tokenText; + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/expiration", newBearerToken); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + var responseBody = await response.Response.Content.ReadAsStringAsync(); + using var dom = JsonDocument.Parse(responseBody); + Assert.NotEqual(DateTimeOffset.MinValue, token.ValidTo); + Assert.NotEqual(DateTimeOffset.MinValue, token.ValidFrom); + Assert.Equal(token.ValidTo, dom.RootElement.GetProperty("expires").GetDateTimeOffset()); + Assert.Equal(token.ValidFrom, dom.RootElement.GetProperty("issued").GetDateTimeOffset()); + } + + [Fact] + public async Task ExpirationAndIssuedWhenMinOrMaxValue() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob") + }; + + var token = new JwtSecurityToken( + issuer: "issuer.contoso.com", + audience: "audience.contoso.com", + claims: claims, + expires: DateTime.MaxValue, + signingCredentials: creds); + + var tokenText = new JwtSecurityTokenHandler().WriteToken(token); + + using var host = await CreateHost(o => + { + o.SaveToken = true; + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = key, + }; + }); + + var newBearerToken = "Bearer " + tokenText; + using var server = host.GetTestServer(); + var response = await SendAsync(server, "http://example.com/expiration", newBearerToken); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + var responseBody = await response.Response.Content.ReadAsStringAsync(); + using var dom = JsonDocument.Parse(responseBody); + Assert.Equal(JsonValueKind.Null, dom.RootElement.GetProperty("issued").ValueKind); + + var expiresElement = dom.RootElement.GetProperty("expires"); + Assert.Equal(JsonValueKind.String, expiresElement.ValueKind); + + var elementValue = DateTime.Parse(expiresElement.GetString()); + var elementValueUtc = elementValue.ToUniversalTime(); + // roundtrip DateTime.MaxValue through parsing because it is lossy and we + // need equivalent values to compare against. + var max = DateTime.Parse(DateTime.MaxValue.ToString()); + + Assert.Equal(max, elementValueUtc); + } + + [Fact] + public void CanReadJwtBearerOptionsFromConfig() + { + var services = new ServiceCollection().AddLogging(); + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Authentication:Schemes:Bearer:ValidIssuer", "dotnet-user-jwts"), + new KeyValuePair("Authentication:Schemes:Bearer:ValidAudiences:0", "http://localhost:5000"), + new KeyValuePair("Authentication:Schemes:Bearer:ValidAudiences:1", "https://localhost:5001"), + new KeyValuePair("Authentication:Schemes:Bearer:BackchannelTimeout", "00:01:00"), + new KeyValuePair("Authentication:Schemes:Bearer:RequireHttpsMetadata", "false"), + new KeyValuePair("Authentication:Schemes:Bearer:SaveToken", "True"), + }).Build(); + services.AddSingleton(config); + + // Act + var builder = services.AddAuthentication(o => + { + o.AddScheme("Bearer", "Bearer"); + }); + builder.AddJwtBearer("Bearer"); + RegisterAuth(builder, _ => { }); + var sp = services.BuildServiceProvider(); + + // Assert + var jwtBearerOptions = sp.GetRequiredService>().Get(JwtBearerDefaults.AuthenticationScheme); + Assert.Equal(jwtBearerOptions.TokenValidationParameters.ValidIssuers, new[] { "dotnet-user-jwts" }); + Assert.Equal(jwtBearerOptions.TokenValidationParameters.ValidAudiences, new[] { "http://localhost:5000", "https://localhost:5001" }); + Assert.Equal(jwtBearerOptions.BackchannelTimeout, TimeSpan.FromSeconds(60)); + Assert.False(jwtBearerOptions.RequireHttpsMetadata); + Assert.True(jwtBearerOptions.SaveToken); + Assert.True(jwtBearerOptions.MapInboundClaims); // Assert default values are respected + } + + [Fact] + public void CanReadMultipleIssuersFromConfig() + { + var services = new ServiceCollection().AddLogging(); + var firstKey = "qPG6tDtfxFYZifHW3sEueQ=="; + var secondKey = "6JPzXj6aOPdojlZdeLshaA=="; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Authentication:Schemes:Bearer:ValidIssuers:0", "dotnet-user-jwts"), + new KeyValuePair("Authentication:Schemes:Bearer:ValidIssuers:1", "dotnet-user-jwts-2"), + new KeyValuePair("Authentication:Schemes:Bearer:SigningKeys:0:Issuer", "dotnet-user-jwts"), + new KeyValuePair("Authentication:Schemes:Bearer:SigningKeys:0:Value", firstKey), + new KeyValuePair("Authentication:Schemes:Bearer:SigningKeys:0:Length", "32"), + new KeyValuePair("Authentication:Schemes:Bearer:SigningKeys:1:Issuer", "dotnet-user-jwts-2"), + new KeyValuePair("Authentication:Schemes:Bearer:SigningKeys:1:Value", secondKey), + new KeyValuePair("Authentication:Schemes:Bearer:SigningKeys:1:Length", "32"), + }).Build(); + services.AddSingleton(config); + + // Act + var builder = services.AddAuthentication(o => + { + o.AddScheme("Bearer", "Bearer"); + }); + builder.AddJwtBearer("Bearer"); + RegisterAuth(builder, _ => { }); + var sp = services.BuildServiceProvider(); + + // Assert + var jwtBearerOptions = sp.GetRequiredService>().Get(JwtBearerDefaults.AuthenticationScheme); + Assert.Equal(2, jwtBearerOptions.TokenValidationParameters.IssuerSigningKeys.Count()); + Assert.Equal(firstKey, Convert.ToBase64String(jwtBearerOptions.TokenValidationParameters.IssuerSigningKeys.OfType().FirstOrDefault()?.Key)); + Assert.Equal(secondKey, Convert.ToBase64String(jwtBearerOptions.TokenValidationParameters.IssuerSigningKeys.OfType().LastOrDefault()?.Key)); + } + + class InvalidTokenValidator : TokenHandler + { + public InvalidTokenValidator() + { + ExceptionType = typeof(SecurityTokenException); + } + + public InvalidTokenValidator(Type exceptionType) + { + ExceptionType = exceptionType; + } + + public Type ExceptionType { get; set; } + + public override SecurityToken ReadToken(string token) + { + return new JsonWebToken(token); + } + + public override Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) + { + var constructor = ExceptionType.GetTypeInfo().GetConstructor(new[] { typeof(string) }); + var exception = (Exception)constructor.Invoke(new[] { ExceptionType.Name }); + throw exception; + } + } + + class DetailedInvalidTokenValidator : TokenHandler + { + public DetailedInvalidTokenValidator() + { + ExceptionType = typeof(SecurityTokenException); + } + + public DetailedInvalidTokenValidator(Type exceptionType) + { + ExceptionType = exceptionType; + } + + public Type ExceptionType { get; set; } + + public override SecurityToken ReadToken(string token) + { + return new JsonWebToken(token); + } + + public override Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) + { + if (ExceptionType == typeof(SecurityTokenInvalidAudienceException)) + { + throw new SecurityTokenInvalidAudienceException("SecurityTokenInvalidAudienceException") { InvalidAudience = "Bad Audience" }; + } + if (ExceptionType == typeof(SecurityTokenInvalidIssuerException)) + { + throw new SecurityTokenInvalidIssuerException("SecurityTokenInvalidIssuerException") { InvalidIssuer = "Bad Issuer" }; + } + if (ExceptionType == typeof(SecurityTokenInvalidLifetimeException)) + { + throw new SecurityTokenInvalidLifetimeException("SecurityTokenInvalidLifetimeException") + { + NotBefore = new DateTime(2001, 1, 15), + Expires = new DateTime(2000, 2, 20), + }; + } + if (ExceptionType == typeof(SecurityTokenNotYetValidException)) + { + throw new SecurityTokenNotYetValidException("SecurityTokenNotYetValidException") + { + NotBefore = new DateTime(2045, 1, 15), + }; + } + if (ExceptionType == typeof(SecurityTokenExpiredException)) + { + throw new SecurityTokenExpiredException("SecurityTokenExpiredException") + { + Expires = new DateTime(2000, 2, 20), + }; + } + else + { + throw new NotImplementedException(ExceptionType.Name); + } + } + } + + class BlobTokenValidator : TokenHandler + { + private readonly Action _tokenValidator; + + public BlobTokenValidator(string authenticationScheme) + { + AuthenticationScheme = authenticationScheme; + } + + public BlobTokenValidator(string authenticationScheme, Action tokenValidator) + { + AuthenticationScheme = authenticationScheme; + _tokenValidator = tokenValidator; + } + + public string AuthenticationScheme { get; } + + public override SecurityToken ReadToken(string token) + { + return new JsonWebToken(token); + } + + public override Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) + { + var validatedToken = new TestSecurityToken(); + _tokenValidator?.Invoke(token); + + var claims = new[] + { + // Make sure to use a different name identifier + // than the one defined by CustomTokenValidated. + new Claim(ClaimTypes.NameIdentifier, "Bob le Tout Puissant"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), + }; + + return Task.FromResult(new TokenValidationResult + { + ClaimsIdentity = new ClaimsIdentity(claims, AuthenticationScheme), + SecurityToken = validatedToken, + IsValid = true + }); + } + } + + private static async Task CreateHost(Action options = null, Func, Task> handlerBeforeAuth = null) + { + var host = new HostBuilder() + .ConfigureWebHost(builder => + builder.UseTestServer() + .Configure(app => + { + if (handlerBeforeAuth != null) + { + app.Use(handlerBeforeAuth); + } + + app.UseAuthentication(); + app.Use(async (context, next) => + { + if (context.Request.Path == new PathString("/checkforerrors")) + { + var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); // this used to be "Automatic" + if (result.Failure != null) + { + throw new Exception("Failed to authenticate", result.Failure); + } + return; + } + else if (context.Request.Path == new PathString("/oauth")) + { + if (context.User == null || + context.User.Identity == null || + !context.User.Identity.IsAuthenticated) + { + context.Response.StatusCode = 401; + // REVIEW: no more automatic challenge + await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); + return; + } + + var identifier = context.User.FindFirst(ClaimTypes.NameIdentifier); + if (identifier == null) + { + context.Response.StatusCode = 500; + return; + } + + await context.Response.WriteAsync(identifier.Value); + } + else if (context.Request.Path == new PathString("/token")) + { + var token = await context.GetTokenAsync("access_token"); + await context.Response.WriteAsync(token); + } + else if (context.Request.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); + await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); + } + else if (context.Request.Path == new PathString("/forbidden")) + { + // Simulate Forbidden + await context.ForbidAsync(JwtBearerDefaults.AuthenticationScheme); + } + else if (context.Request.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal())); + } + else if (context.Request.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync(JwtBearerDefaults.AuthenticationScheme)); + } + else if (context.Request.Path == new PathString("/expiration")) + { + var authenticationResult = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); + await context.Response.WriteAsJsonAsync( + new { Expires = authenticationResult.Properties?.ExpiresUtc, Issued = authenticationResult.Properties?.IssuedUtc }); + } + else + { + await next(context); + } + }); + }) + .ConfigureServices(services => services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options))) + .Build(); + + await host.StartAsync(); + return host; + } + + // TODO: see if we can share the TestExtensions SendAsync method (only diff is auth header) + private static async Task SendAsync(TestServer server, string uri, string authorizationHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(authorizationHeader)) + { + request.Headers.Add("Authorization", authorizationHeader); + } + + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + + return transaction; + } + + private static (string tokenText, SymmetricSecurityKey key) CreateStandardTokenAndKey() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob") + }; + + var token = new JwtSecurityToken( + issuer: "issuer.contoso.com", + audience: "audience.contoso.com", + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: creds); + + var tokenText = new JwtSecurityTokenHandler().WriteToken(token); + return (tokenText, key); + } +} diff --git a/src/Security/Authentication/test/WsFederation/TestTokenHandler.cs b/src/Security/Authentication/test/WsFederation/TestTokenHandler.cs new file mode 100644 index 000000000000..a3a8d3d089ac --- /dev/null +++ b/src/Security/Authentication/test/WsFederation/TestTokenHandler.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.WsFederation; + +internal class TestSecurityTokenHandler : TokenHandler +{ + public override SecurityToken ReadToken(string token) + { + return new TestSecurityToken(); + } + + public override Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) + { + if (!string.IsNullOrEmpty(token) && token.Contains("ThisIsAValidToken")) + { + return Task.FromResult(new TokenValidationResult + { + ClaimsIdentity = new ClaimsIdentity("Test"), + IsValid = true, + SecurityToken = new TestSecurityToken() + }); + } + + throw new SecurityTokenException("The security token did not contain ThisIsAValidToken"); + } +} diff --git a/src/Security/Authentication/test/WsFederation/WsFederationTest.cs b/src/Security/Authentication/test/WsFederation/WsFederationTest.cs index e11ea3c57c73..6ea9ebdb733e 100644 --- a/src/Security/Authentication/test/WsFederation/WsFederationTest.cs +++ b/src/Security/Authentication/test/WsFederation/WsFederationTest.cs @@ -288,11 +288,14 @@ private async Task CreateClient(bool allowUnsolicited = false) .AddCookie() .AddWsFederation(options => { + options.UseSecurityTokenHandlers = true; options.Wtrealm = "http://Automation1"; options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml"; options.BackchannelHttpHandler = new WaadMetadataDocumentHandler(); options.StateDataFormat = new CustomStateDataFormat(); +#pragma warning disable CS0618 // Type or member is obsolete options.SecurityTokenHandlers = new List() { new TestSecurityTokenValidator() }; +#pragma warning restore CS0618 // Type or member is obsolete options.UseTokenLifetime = false; options.AllowUnsolicitedLogins = allowUnsolicited; options.Events = new WsFederationEvents() diff --git a/src/Security/Authentication/test/WsFederation/WsFederationTest_Handler.cs b/src/Security/Authentication/test/WsFederation/WsFederationTest_Handler.cs new file mode 100644 index 000000000000..da9590c9691b --- /dev/null +++ b/src/Security/Authentication/test/WsFederation/WsFederationTest_Handler.cs @@ -0,0 +1,453 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Authentication.WsFederation; + +public class WsFederationTestHandlers +{ + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddSingleton(new ConfigurationManager()); + services.AddAuthentication().AddWsFederation(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(WsFederationDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("WsFederationHandler", scheme.HandlerType.Name); + Assert.Equal(WsFederationDefaults.AuthenticationScheme, scheme.DisplayName); + } + + [Fact] + public async Task MissingConfigurationThrows() + { + using var host = new HostBuilder() + .ConfigureWebHost(builder => + builder.UseTestServer() + .Configure(ConfigureApp) + .ConfigureServices(services => + { + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddWsFederation(); + })) + .Build(); + + await host.StartAsync(); + using var server = host.GetTestServer(); + var httpClient = server.CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var exception = await Assert.ThrowsAsync(() => httpClient.GetAsync("/")); + Assert.Equal("Provide MetadataAddress, Configuration, or ConfigurationManager to WsFederationOptions", exception.Message); + } + + [Fact] + public async Task ChallengeRedirects() + { + var httpClient = await CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/"); + Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path)); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + Assert.Equal("http://Automation1", queryItems["wtrealm"]); + Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData"); + Assert.Equal(httpClient.BaseAddress + "signin-wsfed", queryItems["wreply"]); + Assert.Equal("wsignin1.0", queryItems["wa"]); + } + + [Fact] + public async Task MapWillNotAffectRedirect() + { + var httpClient = await CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/mapped-challenge"); + Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path)); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + Assert.Equal("http://Automation1", queryItems["wtrealm"]); + Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData"); + Assert.Equal(httpClient.BaseAddress + "signin-wsfed", queryItems["wreply"]); + Assert.Equal("wsignin1.0", queryItems["wa"]); + } + + [Fact] + public async Task PreMappedWillAffectRedirect() + { + var httpClient = await CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/premapped-challenge"); + Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path)); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + Assert.Equal("http://Automation1", queryItems["wtrealm"]); + Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData"); + Assert.Equal(httpClient.BaseAddress + "premapped-challenge/signin-wsfed", queryItems["wreply"]); + Assert.Equal("wsignin1.0", queryItems["wa"]); + } + + [Fact] + public async Task ValidTokenIsAccepted() + { + var httpClient = await CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/"); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]); + CopyCookies(response, request); + request.Content = CreateSignInContent("WsFederation/ValidToken.xml", queryItems["wctx"]); + response = await httpClient.SendAsync(request); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + + request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location); + CopyCookies(response, request); + response = await httpClient.SendAsync(request); + + // Did the request end in the actual resource requested for + Assert.Equal(WsFederationDefaults.AuthenticationScheme, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task ValidUnsolicitedTokenIsRefused() + { + var httpClient = await CreateClient(); + var form = CreateSignInContent("WsFederation/ValidToken.xml", suppressWctx: true); + var exception = await Assert.ThrowsAsync(() => httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form)); + Assert.Contains("Unsolicited logins are not allowed.", exception.InnerException.Message); + } + + [Fact] + public async Task ValidUnsolicitedTokenIsAcceptedWhenAllowed() + { + var httpClient = await CreateClient(allowUnsolicited: true); + + var form = CreateSignInContent("WsFederation/ValidToken.xml", suppressWctx: true); + var response = await httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + + var request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location); + CopyCookies(response, request); + response = await httpClient.SendAsync(request); + + // Did the request end in the actual resource requested for + Assert.Equal(WsFederationDefaults.AuthenticationScheme, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task InvalidTokenIsRejected() + { + var httpClient = await CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/"); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]); + CopyCookies(response, request); + request.Content = CreateSignInContent("WsFederation/InvalidToken.xml", queryItems["wctx"]); + response = await httpClient.SendAsync(request); + + // Did the request end in the actual resource requested for + Assert.Equal("AuthenticationFailed", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task RemoteSignoutRequestTriggersSignout() + { + var httpClient = await CreateClient(); + + var response = await httpClient.GetAsync("/signin-wsfed?wa=wsignoutcleanup1.0"); + response.EnsureSuccessStatusCode(); + + var cookie = response.Headers.GetValues(HeaderNames.SetCookie).Single(); + Assert.Equal(".AspNetCore.Cookies=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; samesite=lax; httponly", cookie); + Assert.Equal("OnRemoteSignOut", response.Headers.GetValues("EventHeader").Single()); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task EventsResolvedFromDI() + { + using var host = new HostBuilder() + .ConfigureWebHost(builder => + builder.UseTestServer() + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddWsFederation(options => + { + options.Wtrealm = "http://Automation1"; + options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml"; + options.BackchannelHttpHandler = new WaadMetadataDocumentHandler(); + options.EventsType = typeof(MyWsFedEvents); + }); + }) + .Configure(app => + { + app.Run(context => context.ChallengeAsync()); + })) + .Build(); + + await host.StartAsync(); + using var server = host.GetTestServer(); + + var result = await server.CreateClient().GetAsync(""); + Assert.Contains("CustomKey=CustomValue", result.Headers.Location.Query); + } + + private class MyWsFedEvents : WsFederationEvents + { + public override Task RedirectToIdentityProvider(RedirectContext context) + { + context.ProtocolMessage.SetParameter("CustomKey", "CustomValue"); + return base.RedirectToIdentityProvider(context); + } + } + + private FormUrlEncodedContent CreateSignInContent(string tokenFile, string wctx = null, bool suppressWctx = false) + { + var kvps = new List>(); + kvps.Add(new KeyValuePair("wa", "wsignin1.0")); + kvps.Add(new KeyValuePair("wresult", File.ReadAllText(tokenFile))); + if (!string.IsNullOrEmpty(wctx)) + { + kvps.Add(new KeyValuePair("wctx", wctx)); + } + if (suppressWctx) + { + kvps.Add(new KeyValuePair("suppressWctx", "true")); + } + return new FormUrlEncodedContent(kvps); + } + + private void CopyCookies(HttpResponseMessage response, HttpRequestMessage request) + { + var cookies = SetCookieHeaderValue.ParseList(response.Headers.GetValues(HeaderNames.SetCookie).ToList()); + foreach (var cookie in cookies) + { + if (cookie.Value.HasValue) + { + request.Headers.Add(HeaderNames.Cookie, new CookieHeaderValue(cookie.Name, cookie.Value).ToString()); + } + } + } + + private async Task CreateClient(bool allowUnsolicited = false) + { + var host = new HostBuilder() + .ConfigureWebHost(builder => + builder.UseTestServer() + .Configure(ConfigureApp) + .ConfigureServices(services => + { + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddWsFederation(options => + { + options.Wtrealm = "http://Automation1"; + options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml"; + options.BackchannelHttpHandler = new WaadMetadataDocumentHandler(); + options.StateDataFormat = new CustomStateDataFormat(); + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new TestSecurityTokenHandler()); + options.UseTokenLifetime = false; + options.AllowUnsolicitedLogins = allowUnsolicited; + options.Events = new WsFederationEvents() + { + OnMessageReceived = context => + { + if (!context.ProtocolMessage.Parameters.TryGetValue("suppressWctx", out var suppress)) + { + Assert.True(context.ProtocolMessage.Wctx.Equals("customValue"), "wctx is not my custom value"); + } + context.HttpContext.Items["MessageReceived"] = true; + return Task.FromResult(0); + }, + OnRedirectToIdentityProvider = context => + { + if (context.ProtocolMessage.IsSignInMessage) + { + // Sign in message + context.ProtocolMessage.Wctx = "customValue"; + } + + return Task.FromResult(0); + }, + OnSecurityTokenReceived = context => + { + context.HttpContext.Items["SecurityTokenReceived"] = true; + return Task.FromResult(0); + }, + OnSecurityTokenValidated = context => + { + Assert.True((bool)context.HttpContext.Items["MessageReceived"], "MessageReceived notification not invoked"); + Assert.True((bool)context.HttpContext.Items["SecurityTokenReceived"], "SecurityTokenReceived notification not invoked"); + + if (context.Principal != null) + { + var identity = context.Principal.Identities.Single(); + identity.AddClaim(new Claim("ReturnEndpoint", "true")); + identity.AddClaim(new Claim("Authenticated", "true")); + identity.AddClaim(new Claim(identity.RoleClaimType, "Guest", ClaimValueTypes.String)); + } + + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + context.HttpContext.Items["AuthenticationFailed"] = true; + //Change the request url to something different and skip Wsfed. This new url will handle the request and let us know if this notification was invoked. + context.HttpContext.Request.Path = new PathString("/AuthenticationFailed"); + context.SkipHandler(); + return Task.FromResult(0); + }, + OnRemoteSignOut = context => + { + context.Response.Headers["EventHeader"] = "OnRemoteSignOut"; + return Task.FromResult(0); + } + }; + }); + })) + .Build(); + + await host.StartAsync(); + var server = host.GetTestServer(); + return server.CreateClient(); + } + + private void ConfigureApp(IApplicationBuilder app) + { + app.Map("/PreMapped-Challenge", mapped => + { + mapped.UseAuthentication(); + mapped.Run(async context => + { + await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + }); + }); + + app.UseAuthentication(); + + app.Map("/Logout", subApp => + { + subApp.Run(async context => + { + if (context.User.Identity.IsAuthenticated) + { + var authProperties = new AuthenticationProperties() { RedirectUri = context.Request.GetEncodedUrl() }; + await context.SignOutAsync(WsFederationDefaults.AuthenticationScheme, authProperties); + await context.Response.WriteAsync("Signing out..."); + } + else + { + await context.Response.WriteAsync("SignedOut"); + } + }); + }); + + app.Map("/AuthenticationFailed", subApp => + { + subApp.Run(async context => + { + await context.Response.WriteAsync("AuthenticationFailed"); + }); + }); + + app.Map("/signout-wsfed", subApp => + { + subApp.Run(async context => + { + await context.Response.WriteAsync("signout-wsfed"); + }); + }); + + app.Map("/mapped-challenge", subApp => + { + subApp.Run(async context => + { + await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + }); + }); + + app.Run(async context => + { + var result = context.AuthenticateAsync(); + if (context.User == null || !context.User.Identity.IsAuthenticated) + { + await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + await context.Response.WriteAsync("Unauthorized"); + } + else + { + var identity = context.User.Identities.Single(); + if (identity.NameClaimType == "Name_Failed" && identity.RoleClaimType == "Role_Failed") + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("SignIn_Failed"); + } + else if (!identity.HasClaim("Authenticated", "true") || !identity.HasClaim("ReturnEndpoint", "true") || !identity.HasClaim(identity.RoleClaimType, "Guest")) + { + await context.Response.WriteAsync("Provider not invoked"); + return; + } + else + { + await context.Response.WriteAsync(WsFederationDefaults.AuthenticationScheme); + } + } + }); + } + + private class WaadMetadataDocumentHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var metadata = File.ReadAllText(@"WsFederation/federationmetadata.xml"); + var newResponse = new HttpResponseMessage() { Content = new StringContent(metadata, Encoding.UTF8, "text/xml") }; + return Task.FromResult(newResponse); + } + } +} From b7c470198284175730a65781abe4ca39c390242c Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Thu, 20 Jul 2023 08:31:50 -0700 Subject: [PATCH 18/19] Changes from API review --- .../OpenIdConnect/src/OpenIdConnectHandler.cs | 8 +++- .../OpenIdConnect/src/OpenIdConnectOptions.cs | 41 +++++++++---------- .../OpenIdConnect/src/PublicAPI.Unshipped.txt | 6 +-- .../OpenIdConnect/OpenIdConnectEventTests.cs | 4 +- .../OpenIdConnectEventTests_Handler.cs | 2 +- .../test/OpenIdConnect/OpenIdConnectTests.cs | 4 ++ 6 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs index f234f2059b24..bba3df86d0ef 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs @@ -652,7 +652,7 @@ protected override async Task HandleRemoteAuthenticateAsync { Logger.ReceivedIdToken(); - if (Options.UseTokenHandler) + if (!Options.UseSecurityTokenValidator) { var tokenValidationResult = await ValidateTokenUsingHandlerAsync(authorizationResponse.IdToken, properties, validationParameters); user = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); @@ -732,7 +732,7 @@ protected override async Task HandleRemoteAuthenticateAsync ClaimsPrincipal tokenEndpointUser; JwtSecurityToken tokenEndpointJwt; - if (Options.UseTokenHandler) + if (!Options.UseSecurityTokenValidator) { var tokenValidationResult = await ValidateTokenUsingHandlerAsync(tokenEndpointResponse.IdToken, properties, validationParameters); tokenEndpointUser = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); @@ -1268,11 +1268,13 @@ private async Task RunAuthenticationFailedEventAsyn // Note this modifies properties if Options.UseTokenLifetime private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) { +#pragma warning disable CS0618 // Type or member is obsolete if (!Options.SecurityTokenValidator.CanReadToken(idToken)) { Logger.UnableToReadIdToken(idToken); throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); } +#pragma warning restore CS0618 // Type or member is obsolete if (_configuration != null) { @@ -1283,7 +1285,9 @@ private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties p ?? _configuration.SigningKeys; } +#pragma warning disable CS0618 // Type or member is obsolete var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out SecurityToken validatedToken); +#pragma warning restore CS0618 // Type or member is obsolete if (validatedToken is JwtSecurityToken validatedJwt) { jwt = validatedJwt; diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs index 1491c680e322..a942658b07b4 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs @@ -18,7 +18,12 @@ public class OpenIdConnectOptions : RemoteAuthenticationOptions { private CookieBuilder _nonceCookieBuilder; private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler(); - private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler(); + private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler + { + MapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims + }; + + private bool _mapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims; /// /// Initializes a new @@ -39,12 +44,11 @@ public OpenIdConnectOptions() CallbackPath = new PathString("/signin-oidc"); SignedOutCallbackPath = new PathString("/signout-callback-oidc"); RemoteSignOutPath = new PathString("/signout-oidc"); +#pragma warning disable CS0618 // Type or member is obsolete SecurityTokenValidator = _defaultHandler; +#pragma warning restore CS0618 // Type or member is obsolete TokenHandler = _defaultTokenHandler; - // reconcile the difference between default values. - _defaultTokenHandler.MapInboundClaims = _defaultHandler.MapInboundClaims; - Events = new OpenIdConnectEvents(); Scope.Add("openid"); Scope.Add("profile"); @@ -259,12 +263,13 @@ public override void Validate() /// /// Gets or sets the used to validate identity tokens. /// + [Obsolete("SecurityTokenValidator is no longer used by default. Use TokenHandler instead. To continue using SecurityTokenValidator, set UseSecurityTokenValidator to true. See https://aka.ms/aspnetcore8/security-token-changes")] public ISecurityTokenValidator SecurityTokenValidator { get; set; } /// /// Gets or sets the used to validate identity tokens. /// - /// This will be used instead of if is + /// This will be used instead of if is /// /// public TokenHandler TokenHandler { get; set; } @@ -367,31 +372,25 @@ public override CookieOptions Build(HttpContext context, DateTimeOffset expiresF public TimeSpan RefreshInterval { get; set; } = ConfigurationManager.DefaultRefreshInterval; /// - /// Gets or sets the property on the default instance of in SecurityTokenValidator, which is used when determining + /// Gets or sets the property on the default instance of in SecurityTokenValidator + /// and default instance of in TokenHandler, which is used when determining /// whether or not to map claim types that are extracted when validating a . /// If this is set to true, the Claim Type is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs. /// The default value is true. /// public bool MapInboundClaims { - get => _defaultHandler.MapInboundClaims; - set => _defaultHandler.MapInboundClaims = value; - } - - /// - /// Gets or sets the property on the default instance of in TokenHandler, which is used when determining - /// whether or not to map claim types that are extracted when validating a . - /// If this is set to true, the Claim Type is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs. - /// The default value is true. - /// - public bool MapInboundClaimsTokenHandler - { - get => _defaultTokenHandler.MapInboundClaims; - set => _defaultTokenHandler.MapInboundClaims = value; + get => _mapInboundClaims; + set + { + _mapInboundClaims = value; + _defaultHandler.MapInboundClaims = value; + _defaultTokenHandler.MapInboundClaims = value; + } } /// /// Gets or sets whether to use the or the for validating identity tokens. /// - public bool UseTokenHandler { get; set; } = true; + public bool UseSecurityTokenValidator { get; set; } } diff --git a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt index 0c5388da6f06..d6dbc14fed1e 100644 --- a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt @@ -1,8 +1,6 @@ #nullable enable Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.OpenIdConnectHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.HtmlEncoder! htmlEncoder, System.Text.Encodings.Web.UrlEncoder! encoder) -> void -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaimsTokenHandler.get -> bool -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaimsTokenHandler.set -> void Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.get -> Microsoft.IdentityModel.Tokens.TokenHandler! Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.set -> void -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseTokenHandler.get -> bool -Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseTokenHandler.set -> void +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseSecurityTokenValidator.get -> bool +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseSecurityTokenValidator.set -> void diff --git a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs index 230f862626e4..b54800821e43 100644 --- a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs +++ b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs @@ -1286,8 +1286,10 @@ private TestServer CreateServer(OpenIdConnectEvents events, RequestDelegate appC EndSessionEndpoint = "http://testhost/end" }; o.StateDataFormat = new TestStateDataFormat(); +#pragma warning disable CS0618 // Type or member is obsolete o.SecurityTokenValidator = new TestTokenValidator(); - o.UseTokenHandler = false; +#pragma warning restore CS0618 // Type or member is obsolete + o.UseSecurityTokenValidator = true; o.ProtocolValidator = new TestProtocolValidator(); o.BackchannelHttpHandler = new TestBackchannel(); }); diff --git a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests_Handler.cs b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests_Handler.cs index 0749108b219c..7279b02a724a 100644 --- a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests_Handler.cs +++ b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests_Handler.cs @@ -1286,7 +1286,7 @@ private TestServer CreateServer(OpenIdConnectEvents events, RequestDelegate appC EndSessionEndpoint = "http://testhost/end" }; o.StateDataFormat = new TestStateDataFormat(); - o.UseTokenHandler = true; + o.UseSecurityTokenValidator = false; o.TokenHandler = new TestTokenHandler(); o.ProtocolValidator = new TestProtocolValidator(); o.BackchannelHttpHandler = new TestBackchannel(); diff --git a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectTests.cs b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectTests.cs index d9c3f04d9420..49977e6c80e9 100644 --- a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectTests.cs +++ b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectTests.cs @@ -374,7 +374,9 @@ public void MapInboundClaimsDefaultsToTrue() { var options = new OpenIdConnectOptions(); Assert.True(options.MapInboundClaims); +#pragma warning disable CS0618 // Type or member is obsolete var jwtHandler = options.SecurityTokenValidator as JwtSecurityTokenHandler; +#pragma warning restore CS0618 // Type or member is obsolete Assert.NotNull(jwtHandler); Assert.True(jwtHandler.MapInboundClaims); } @@ -385,7 +387,9 @@ public void MapInboundClaimsCanBeSetToFalse() var options = new OpenIdConnectOptions(); options.MapInboundClaims = false; Assert.False(options.MapInboundClaims); +#pragma warning disable CS0618 // Type or member is obsolete var jwtHandler = options.SecurityTokenValidator as JwtSecurityTokenHandler; +#pragma warning restore CS0618 // Type or member is obsolete Assert.NotNull(jwtHandler); Assert.False(jwtHandler.MapInboundClaims); } From 7d0bc0b59778b41b6283f0872e1770fd83575ba1 Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Thu, 20 Jul 2023 12:23:53 -0700 Subject: [PATCH 19/19] Comments from review --- .../Authentication/JwtBearer/src/JwtBearerOptions.cs | 6 ++++-- .../OpenIdConnect/src/LoggingExtensions.cs | 4 ++-- .../OpenIdConnect/src/OpenIdConnectHandler.cs | 4 ++-- .../OpenIdConnect/src/OpenIdConnectOptions.cs | 10 +++++++++- .../Authentication/OpenIdConnect/src/Resources.resx | 2 +- .../WsFederation/src/WsFederationHandler.cs | 1 - .../WsFederation/src/WsFederationOptions.cs | 4 +++- 7 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs index 3ef2893b6fef..f5b56ed32531 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs @@ -142,7 +142,7 @@ public JwtBearerOptions() public bool IncludeErrorDetails { get; set; } = true; /// - /// Gets or sets the property on the default instance of in SecurityTokenValidators, or in TokenHandlers which is used when determining + /// Gets or sets the property on the default instance of in SecurityTokenValidators, or in TokenHandlers, which is used when determining /// whether or not to map claim types that are extracted when validating a or a . /// If this is set to true, the Claim Type is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs. /// The default value is true. @@ -178,10 +178,12 @@ public bool MapInboundClaims /// Gets or sets whether or will be used to validate the inbound token. /// /// - /// The advantage of using TokenHandlers are: + /// The advantages of using TokenHandlers are: /// There is an Async model. /// The default token handler is a which is faster than a . /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. + /// SecurityTokenValidators can be used when needs a . + /// When using TokenHandlers, will be a . /// public bool UseSecurityTokenValidators { get; set; } } diff --git a/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs b/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs index 79f0d2e63861..3be7240ab4a7 100644 --- a/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs +++ b/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs @@ -168,6 +168,6 @@ internal static partial class LoggingExtensions [LoggerMessage(56, LogLevel.Error, "Unable to validate the 'id_token', no suitable TokenHandler was found for: '{IdToken}'.", EventName = "UnableToValidateIdTokenFromHandler")] public static partial void UnableToValidateIdTokenFromHandler(this ILogger logger, string idToken); - [LoggerMessage(57, LogLevel.Error, "The Validated Security Token must be of type JsonWebToken, but instead its type is: '{SecurityTokenType}'", EventName = "InvalidSecurityTokenTypeFromHandler")] - public static partial void InvalidSecurityTokenTypeFromHandler(this ILogger logger, string? securityTokenType); + [LoggerMessage(57, LogLevel.Error, "The Validated Security Token must be of type JsonWebToken, but instead its type is: '{SecurityTokenType}.'", EventName = "InvalidSecurityTokenTypeFromHandler")] + public static partial void InvalidSecurityTokenTypeFromHandler(this ILogger logger, Type? securityTokenType); } diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs index bba3df86d0ef..4b074f5621e3 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs @@ -740,7 +740,7 @@ protected override async Task HandleRemoteAuthenticateAsync } else { - tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out tokenEndpointJwt); + tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out tokenEndpointJwt); } // Avoid reading & deleting the nonce cookie, running the event, etc, if it was already done as part of the authorization response validation. @@ -1355,7 +1355,7 @@ private async Task ValidateTokenUsingHandlerAsync(string if (validatedToken is not JsonWebToken) { - Logger.InvalidSecurityTokenTypeFromHandler(validatedToken?.GetType().ToString()); + Logger.InvalidSecurityTokenTypeFromHandler(validatedToken?.GetType()); throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJsonWebToken, validatedToken?.GetType())); } diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs index a942658b07b4..0047d67ea828 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs @@ -269,7 +269,7 @@ public override void Validate() /// /// Gets or sets the used to validate identity tokens. /// - /// This will be used instead of if is + /// This will be used instead of if is . /// /// public TokenHandler TokenHandler { get; set; } @@ -392,5 +392,13 @@ public bool MapInboundClaims /// /// Gets or sets whether to use the or the for validating identity tokens. /// + /// + /// The advantages of using TokenHandler are: + /// There is an Async model. + /// The default token handler is a which is faster than a . + /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. + /// SecurityTokenValidator can be used when needs a . + /// When using TokenHandler, will be a . + /// public bool UseSecurityTokenValidator { get; set; } } diff --git a/src/Security/Authentication/OpenIdConnect/src/Resources.resx b/src/Security/Authentication/OpenIdConnect/src/Resources.resx index fec68b579f86..b2a996626273 100644 --- a/src/Security/Authentication/OpenIdConnect/src/Resources.resx +++ b/src/Security/Authentication/OpenIdConnect/src/Resources.resx @@ -136,7 +136,7 @@ Cannot process the message. Both id_token and code are missing. - The Validated Security Token must be of type JsonWebToken, but instead its tye is '{0}'. + The Validated Security Token must be of type JsonWebToken, but instead its type is '{0}'. Unable to validate the 'id_token', no suitable TokenHandler was found for: '{0}'." diff --git a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs index 3b2594b8137a..d00103ddb26a 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs @@ -402,7 +402,6 @@ private void RequestRefresh(Exception exception) { Options.ConfigurationManager.RequestRefresh(); } - } /// diff --git a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs index bfb8de3b36c7..ae043451d7bb 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs @@ -204,10 +204,12 @@ public TokenValidationParameters TokenValidationParameters /// Gets or sets whether or will be used to validate the inbound token. /// /// - /// The advantage of using the TokenHandlers are: + /// The advantages of using the TokenHandlers are: /// There is an Async model. /// The default token handler for JsonWebTokens is a which is faster than a . /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. + /// SecurityTokenHandlers can be used when needs a when the security token is a JWT. + /// When using TokenHandlers, will be a when the security token is a JWT. /// public bool UseSecurityTokenHandlers { get; set; } }