From d9bf9d3274992e9e7d64ff6f902ec48b74ff2f00 Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Wed, 19 Jul 2023 13:01:50 -0700 Subject: [PATCH] Update Wilson7 branch Default to using new handlers Changes from API review --- .../JwtBearer/src/JwtBearerHandler.cs | 15 +- .../JwtBearer/src/JwtBearerOptions.cs | 3 +- .../JwtBearer/src/PublicAPI.Unshipped.txt | 4 +- .../WsFederation/src/PublicAPI.Unshipped.txt | 4 +- .../WsFederation/src/Resources.resx | 2 +- .../WsFederation/src/WsFederationHandler.cs | 4 +- .../WsFederation/src/WsFederationOptions.cs | 5 +- .../Authentication/test/JwtBearerTests.cs | 41 +- .../test/JwtBearerTests_Handler.cs | 1293 +++++++++++++++++ .../test/WsFederation/TestTokenHandler.cs | 30 + .../test/WsFederation/WsFederationTest.cs | 1 + .../WsFederation/WsFederationTest_Handler.cs | 453 ++++++ 12 files changed, 1827 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..ad2263ddd403 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,7 +120,7 @@ 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); } } } @@ -194,7 +191,7 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Fail(authenticationFailedContext.Exception); } - if (Options.UseTokenHandlers) + if (!Options.UseSecurityTokenValidators) { return AuthenticateResults.TokenHandlerUnableToValidate; } @@ -251,10 +248,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..fac7b4b79c3e 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs @@ -111,6 +111,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.")] public IList SecurityTokenValidators { get; private set; } /// @@ -180,5 +181,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..ed289c5842eb 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) { @@ -298,7 +298,6 @@ protected override async Task HandleRemoteAuthenticateAsync if (principal == null) { - // TODO - need new string for TokenHandler if (validationFailures == null || validationFailures.Count == 0) { throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound); @@ -375,7 +374,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..355128db4a1c 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.")] 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..7fdf51bfbb20 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; @@ -183,6 +185,7 @@ public async Task ThrowAtAuthenticationFailedEvent() return Task.FromResult(0); } }; + o.UseSecurityTokenValidators = true; o.SecurityTokenValidators.Clear(); o.SecurityTokenValidators.Insert(0, new InvalidTokenValidator()); }, @@ -228,6 +231,7 @@ public async Task CustomHeaderReceived() return Task.FromResult(null); } }; + o.UseSecurityTokenValidators = true; }); using var server = host.GetTestServer(); @@ -239,7 +243,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 +252,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 +261,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,6 +273,7 @@ public async Task InvalidTokenReceived() { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new InvalidTokenValidator()); }); @@ -293,6 +298,7 @@ public async Task ExceptionReportedInHeaderForAuthenticationFailures(Type errorT { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType)); }); @@ -314,6 +320,7 @@ public async Task ExceptionReportedInHeaderWithDetailsForAuthenticationFailures( { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new DetailedInvalidTokenValidator(errorType)); }); @@ -331,6 +338,7 @@ public async Task ExceptionNotReportedInHeaderForOtherFailures(Type errorType) { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType)); }); @@ -347,6 +355,7 @@ public async Task ExceptionsReportedInHeaderForMultipleAuthenticationFailures() { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenInvalidAudienceException))); options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenSignatureKeyNotFoundException))); @@ -382,6 +391,7 @@ public async Task ExceptionsReportedInHeaderExposesUserDefinedError(string error return Task.FromResult(0); } }; + options.UseSecurityTokenValidators = true; }); using var server = host.GetTestServer(); @@ -430,6 +440,7 @@ public async Task ExceptionNotReportedInHeaderWhenIncludeErrorDetailsIsFalse() using var host = await CreateHost(o => { o.IncludeErrorDetails = false; + o.UseSecurityTokenValidators = true; }); using var server = host.GetTestServer(); @@ -442,7 +453,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,6 +488,7 @@ public async Task CustomTokenValidated() return Task.FromResult(null); } }; + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator(JwtBearerDefaults.AuthenticationScheme)); }); @@ -500,6 +512,7 @@ public async Task RetrievingTokenFromAlternateLocation() return Task.FromResult(null); } }; + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT", token => { @@ -518,6 +531,7 @@ public async Task EventOnMessageReceivedSkip_NoMoreEventsExecuted() { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; options.Events = new JwtBearerEvents() { OnMessageReceived = context => @@ -551,6 +565,7 @@ public async Task EventOnMessageReceivedReject_NoMoreEventsExecuted() { using var host = await CreateHost(options => { + options.UseSecurityTokenValidators = true; options.Events = new JwtBearerEvents() { OnMessageReceived = context => @@ -604,6 +619,7 @@ public async Task EventOnTokenValidatedSkip_NoMoreEventsExecuted() throw new NotImplementedException(); }, }; + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); }); @@ -636,6 +652,7 @@ public async Task EventOnTokenValidatedReject_NoMoreEventsExecuted() throw new NotImplementedException(); }, }; + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); }); @@ -670,6 +687,7 @@ public async Task EventOnAuthenticationFailedSkip_NoMoreEventsExecuted() throw new NotImplementedException(); }, }; + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); }); @@ -702,6 +720,7 @@ public async Task EventOnAuthenticationFailedReject_NoMoreEventsExecuted() throw new NotImplementedException(); }, }; + options.UseSecurityTokenValidators = true; options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); }); @@ -728,6 +747,7 @@ public async Task EventOnChallengeSkip_ResponseNotModified() return Task.FromResult(0); }, }; + o.UseSecurityTokenValidators = true; }); using var server = host.GetTestServer(); @@ -750,6 +770,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 +797,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 +825,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 +869,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 +914,7 @@ public async Task ExpirationAndIssuedSetOnAuthenticateResult() ValidAudience = "audience.contoso.com", IssuerSigningKey = key, }; + o.UseSecurityTokenValidators = true; }); var newBearerToken = "Bearer " + tokenText; @@ -934,6 +958,7 @@ public async Task ExpirationAndIssuedNullWhenMinOrMaxValue() ValidAudience = "audience.contoso.com", IssuerSigningKey = key, }; + o.UseSecurityTokenValidators = true; }); var newBearerToken = "Bearer " + tokenText; @@ -966,7 +991,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 +1029,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..7025eba36ee7 --- /dev/null +++ b/src/Security/Authentication/test/JwtBearerTests_Handler.cs @@ -0,0 +1,1293 @@ +// 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); + + 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] + public void MapInboundClaimsCanBeSetToFalse() + { + var options = new JwtBearerOptions(); + options.MapInboundClaims = false; + Assert.False(options.MapInboundClaims); + var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler; + 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..a5fd18c02a72 100644 --- a/src/Security/Authentication/test/WsFederation/WsFederationTest.cs +++ b/src/Security/Authentication/test/WsFederation/WsFederationTest.cs @@ -288,6 +288,7 @@ 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(); 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); + } + } +}