diff --git a/eng/Versions.props b/eng/Versions.props index 2fed627a089c..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 - 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 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(",", ",
") + "
"); }); } 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..bba3df86d0ef 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,17 @@ protected override async Task HandleRemoteAuthenticateAsync if (!string.IsNullOrEmpty(authorizationResponse.IdToken)) { Logger.ReceivedIdToken(); - user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); + + if (!Options.UseSecurityTokenValidator) + { + var tokenValidationResult = await ValidateTokenUsingHandlerAsync(authorizationResponse.IdToken, properties, validationParameters); + user = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); + jwt = JwtSecurityTokenConverter.Convert(tokenValidationResult.SecurityToken as JsonWebToken); + } + else + { + user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); + } nonce = jwt.Payload.Nonce; if (!string.IsNullOrEmpty(nonce)) @@ -717,7 +729,19 @@ 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.UseSecurityTokenValidator) + { + var tokenValidationResult = await ValidateTokenUsingHandlerAsync(tokenEndpointResponse.IdToken, properties, validationParameters); + tokenEndpointUser = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); + tokenEndpointJwt = JwtSecurityTokenConverter.Convert(tokenValidationResult.SecurityToken as JsonWebToken); + } + 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) @@ -1244,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) { @@ -1259,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; @@ -1294,6 +1322,61 @@ 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 (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager) + { + validationParameters.ConfigurationManager = baseConfigurationManager; + } + else 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..a942658b07b4 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,12 @@ public class OpenIdConnectOptions : RemoteAuthenticationOptions { private CookieBuilder _nonceCookieBuilder; private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler(); + private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler + { + MapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims + }; + + private bool _mapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims; /// /// Initializes a new @@ -37,7 +44,10 @@ 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; Events = new OpenIdConnectEvents(); Scope.Add("openid"); @@ -253,8 +263,17 @@ 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 + /// + /// + public TokenHandler TokenHandler { get; set; } + /// /// Gets or sets the parameters used to validate identity tokens. /// @@ -353,14 +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; + 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 UseSecurityTokenValidator { get; set; } } diff --git a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt index 8ff5e3305e99..d6dbc14fed1e 100644 --- a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt @@ -1,2 +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.TokenHandler.get -> Microsoft.IdentityModel.Tokens.TokenHandler! +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.set -> void +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseSecurityTokenValidator.get -> bool +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseSecurityTokenValidator.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 diff --git a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectEventTests.cs index 3da118f7e03e..b54800821e43 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; @@ -1285,7 +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(); +#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 new file mode 100644 index 000000000000..7279b02a724a --- /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.UseSecurityTokenValidator = false; + 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()); + } + } +} 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); }