/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Security/Authentication/test/JwtBearerTests_Handler.cs
1 360 строк
55 KB
Deev Andrey
#62774 Fix JWT Bearer unit tests failing if local timezone is UTC+N (#62775)
31 июл 2025, 23:09
Не верифицирован
31 июл 2025, 23:09
7296d4d
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; 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<JwtBearerOptions> { 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<JwtBearerOptions> configure = null) { services.AddJwtBearer(configure); } [Fact] public async Task BearerTokenValidation() { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "Bob") }; var token = new JwtSecurityToken( issuer: "issuer.contoso.com", audience: "audience.contoso.com", claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); var tokenText = new JwtSecurityTokenHandler().WriteToken(token); using var host = await CreateHost(o => { o.TokenValidationParameters = new TokenValidationParameters() { ValidIssuer = "issuer.contoso.com", ValidAudience = "audience.contoso.com", IssuerSigningKey = key, }; }); var newBearerToken = "Bearer " + tokenText; using var server = host.GetTestServer(); var response = await SendAsync(server, "http://example.com/oauth", newBearerToken); Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); } [Fact] public async Task SaveBearerToken() { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "Bob") }; var token = new JwtSecurityToken( issuer: "issuer.contoso.com", audience: "audience.contoso.com", claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); var tokenText = new JwtSecurityTokenHandler().WriteToken(token); using var host = await CreateHost(o => { o.SaveToken = true; o.TokenValidationParameters = new TokenValidationParameters() { ValidIssuer = "issuer.contoso.com", ValidAudience = "audience.contoso.com", IssuerSigningKey = key, }; }); var newBearerToken = "Bearer " + tokenText; using var server = host.GetTestServer(); var response = await SendAsync(server, "http://example.com/token", newBearerToken); Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); Assert.Equal(tokenText, await response.Response.Content.ReadAsStringAsync()); } [Fact] public void MapInboundClaimsDefaultsToTrue() { var options = new JwtBearerOptions(); Assert.True(options.MapInboundClaims); #pragma warning disable CS0618 // Type or member is obsolete var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler; #pragma warning restore CS0618 // Type or member is obsolete Assert.NotNull(jwtHandler); Assert.True(jwtHandler.MapInboundClaims); var tokenHandler = options.TokenHandlers.First() as JsonWebTokenHandler; Assert.NotNull(tokenHandler); Assert.True(tokenHandler.MapInboundClaims); options.MapInboundClaims = false; Assert.False(jwtHandler.MapInboundClaims); Assert.False(tokenHandler.MapInboundClaims); } [Fact] public void MapInboundClaimsCanBeSetToFalse() { var options = new JwtBearerOptions(); options.MapInboundClaims = false; Assert.False(options.MapInboundClaims); #pragma warning disable CS0618 // Type or member is obsolete var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler; #pragma warning restore CS0618 // Type or member is obsolete Assert.NotNull(jwtHandler); Assert.False(jwtHandler.MapInboundClaims); } [Fact] public async Task SignInThrows() { using var host = await CreateHost(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("https://example.com/signIn"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); } [Fact] public async Task SignOutThrows() { using var host = await CreateHost(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("https://example.com/signOut"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); } [Fact] public async Task ThrowAtAuthenticationFailedEvent() { using var host = await CreateHost(o => { o.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { context.Response.StatusCode = 401; throw new Exception(); }, OnMessageReceived = context => { context.Token = "something"; return Task.FromResult(0); } }; o.TokenHandlers.Clear(); o.TokenHandlers.Insert(0, new InvalidTokenValidator()); }, async (context, next) => { try { await next(); Assert.Fail("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<object>(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<object>(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<object>(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<Exception>(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<Exception>(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<Exception>(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: new DateTime(DateTime.MaxValue.Ticks, DateTimeKind.Utc), 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 elementValueUtc = DateTime.Parse(expiresElement.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); // roundtrip DateTime.MaxValue through parsing because it is lossy and we // need equivalent values to compare against. var max = DateTime.Parse(DateTime.MaxValue.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); Assert.Equal(max, elementValueUtc); } [Fact] public void CanReadJwtBearerOptionsFromConfig() { var services = new ServiceCollection(); var key = "qPG6tDtfxFYZifHW3sEueQ=="; var config = new ConfigurationBuilder().AddInMemoryCollection([ new("Authentication:Schemes:Bearer:ValidIssuer", "dotnet-user-jwts"), new("Authentication:Schemes:Bearer:ValidIssuers:0", "dotnet-user-jwts-2"), new("Authentication:Schemes:Bearer:ValidAudience", "http://localhost:5000"), new("Authentication:Schemes:Bearer:ValidAudiences:0", "http://localhost:5001"), new("Authentication:Schemes:Bearer:BackchannelTimeout", "00:01:00"), new("Authentication:Schemes:Bearer:RequireHttpsMetadata", "false"), new("Authentication:Schemes:Bearer:SaveToken", "True"), new("Authentication:Schemes:Bearer:SigningKeys:0:Issuer", "dotnet-user-jwts"), new("Authentication:Schemes:Bearer:SigningKeys:0:Value", key), new("Authentication:Schemes:Bearer:SigningKeys:0:Length", "32"), ]).Build(); services.AddSingleton<IConfiguration>(config); // Act RegisterAuth(services.AddAuthentication()); var sp = services.BuildServiceProvider(); // Assert var jwtBearerOptions = sp.GetRequiredService<IOptionsMonitor<JwtBearerOptions>>().Get(JwtBearerDefaults.AuthenticationScheme); Assert.Equal("dotnet-user-jwts", jwtBearerOptions.TokenValidationParameters.ValidIssuer); Assert.Equal(["dotnet-user-jwts-2"], jwtBearerOptions.TokenValidationParameters.ValidIssuers); Assert.Equal("http://localhost:5000", jwtBearerOptions.TokenValidationParameters.ValidAudience); Assert.Equal(["http://localhost:5001"], jwtBearerOptions.TokenValidationParameters.ValidAudiences); Assert.Equal(TimeSpan.FromSeconds(60), jwtBearerOptions.BackchannelTimeout); Assert.False(jwtBearerOptions.RequireHttpsMetadata); Assert.True(jwtBearerOptions.SaveToken); // ValidateIssuerSigningKey should always be set to its non-default value of true if options are read from config. Assert.True(jwtBearerOptions.TokenValidationParameters.ValidateIssuerSigningKey); // Assert default values for other options are respected. Assert.True(jwtBearerOptions.MapInboundClaims); Assert.True(jwtBearerOptions.TokenValidationParameters.ValidateIssuer); Assert.True(jwtBearerOptions.TokenValidationParameters.ValidateAudience); var securityKey = Assert.Single(jwtBearerOptions.TokenValidationParameters.IssuerSigningKeys); var symmetricKey = Assert.IsType<SymmetricSecurityKey>(securityKey); Assert.Equal(key, Convert.ToBase64String(symmetricKey.Key)); } [Fact] public void CanReadMultipleAudiencesFromConfig() { var services = new ServiceCollection().AddLogging(); var config = new ConfigurationBuilder().AddInMemoryCollection(new[] { new KeyValuePair<string, string>("Authentication:Schemes:Bearer:ValidAudiences:0", "http://localhost:5000"), new KeyValuePair<string, string>("Authentication:Schemes:Bearer:ValidAudiences:1", "https://localhost:5001") }).Build(); services.AddSingleton<IConfiguration>(config); // Act var builder = services.AddAuthentication(o => { o.AddScheme<TestHandler>("Bearer", "Bearer"); }); builder.AddJwtBearer("Bearer"); RegisterAuth(builder, _ => { }); var sp = services.BuildServiceProvider(); // Assert var jwtBearerOptions = sp.GetRequiredService<IOptionsMonitor<JwtBearerOptions>>().Get(JwtBearerDefaults.AuthenticationScheme); Assert.Equal(["http://localhost:5000", "https://localhost:5001"], jwtBearerOptions.TokenValidationParameters.ValidAudiences); } [Fact] public void CanReadMultipleIssuersFromConfig() { var services = new ServiceCollection(); var firstKey = "qPG6tDtfxFYZifHW3sEueQ=="; var secondKey = "6JPzXj6aOPdojlZdeLshaA=="; var config = new ConfigurationBuilder().AddInMemoryCollection([ new("Authentication:Schemes:Bearer:ValidIssuers:0", "dotnet-user-jwts"), new("Authentication:Schemes:Bearer:ValidIssuers:1", "dotnet-user-jwts-2"), new("Authentication:Schemes:Bearer:SigningKeys:0:Issuer", "dotnet-user-jwts"), new("Authentication:Schemes:Bearer:SigningKeys:0:Value", firstKey), new("Authentication:Schemes:Bearer:SigningKeys:0:Length", "32"), new("Authentication:Schemes:Bearer:SigningKeys:1:Issuer", "dotnet-user-jwts-2"), new("Authentication:Schemes:Bearer:SigningKeys:1:Value", secondKey), new("Authentication:Schemes:Bearer:SigningKeys:1:Length", "32"), ]).Build(); services.AddSingleton<IConfiguration>(config); // Act RegisterAuth(services.AddAuthentication()); var sp = services.BuildServiceProvider(); // Assert var jwtBearerOptions = sp.GetRequiredService<IOptionsMonitor<JwtBearerOptions>>().Get(JwtBearerDefaults.AuthenticationScheme); Assert.Equal(2, jwtBearerOptions.TokenValidationParameters.IssuerSigningKeys.Count()); Assert.Equal(firstKey, Convert.ToBase64String(jwtBearerOptions.TokenValidationParameters.IssuerSigningKeys.OfType<SymmetricSecurityKey>().FirstOrDefault()?.Key)); Assert.Equal(secondKey, Convert.ToBase64String(jwtBearerOptions.TokenValidationParameters.IssuerSigningKeys.OfType<SymmetricSecurityKey>().LastOrDefault()?.Key)); } [Fact] public void IssuerAndAudienceValidationEnabledByDefaultWhenOptionsAreReadFromConfig() { var services = new ServiceCollection(); var config = new ConfigurationBuilder().AddInMemoryCollection([ new("Authentication:Schemes:Bearer:Authority", "https://localhost:5001"), ]).Build(); services.AddSingleton<IConfiguration>(config); // Act RegisterAuth(services.AddAuthentication()); var sp = services.BuildServiceProvider(); // Assert var jwtBearerOptions = sp.GetRequiredService<IOptionsMonitor<JwtBearerOptions>>().Get(JwtBearerDefaults.AuthenticationScheme); Assert.Equal("https://localhost:5001", jwtBearerOptions.Authority); Assert.True(jwtBearerOptions.TokenValidationParameters.ValidateIssuer); Assert.True(jwtBearerOptions.TokenValidationParameters.ValidateAudience); } [Fact] public void IssuerAndAudienceValidationCanBeDisabledFromConfig() { var services = new ServiceCollection(); var config = new ConfigurationBuilder().AddInMemoryCollection([ new("Authentication:Schemes:Bearer:Authority", "https://localhost:5001"), new("Authentication:Schemes:Bearer:ValidateIssuer", "false"), new("Authentication:Schemes:Bearer:ValidateAudience", "false"), ]).Build(); services.AddSingleton<IConfiguration>(config); // Act RegisterAuth(services.AddAuthentication()); var sp = services.BuildServiceProvider(); // Assert var jwtBearerOptions = sp.GetRequiredService<IOptionsMonitor<JwtBearerOptions>>().Get(JwtBearerDefaults.AuthenticationScheme); Assert.Equal("https://localhost:5001", jwtBearerOptions.Authority); Assert.False(jwtBearerOptions.TokenValidationParameters.ValidateIssuer); Assert.False(jwtBearerOptions.TokenValidationParameters.ValidateAudience); } 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<TokenValidationResult> 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<TokenValidationResult> 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<string> _tokenValidator; public BlobTokenValidator(string authenticationScheme) { AuthenticationScheme = authenticationScheme; } public BlobTokenValidator(string authenticationScheme, Action<string> tokenValidator) { AuthenticationScheme = authenticationScheme; _tokenValidator = tokenValidator; } public string AuthenticationScheme { get; } public override SecurityToken ReadToken(string token) { return new JsonWebToken(token); } public override Task<TokenValidationResult> 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<IHost> CreateHost(Action<JwtBearerOptions> options = null, Func<HttpContext, Func<Task>, 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<InvalidOperationException>(() => context.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal())); } else if (context.Request.Path == new PathString("/signOut")) { await Assert.ThrowsAsync<InvalidOperationException>(() => 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<Transaction> 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); } }