From c99dbfd45a82126fd2b16a6781fedbe01a916c01 Mon Sep 17 00:00:00 2001 From: Fatih Tolga Ata Date: Wed, 27 Nov 2024 13:53:13 +0300 Subject: [PATCH 1/2] .NET 9.0 Preview --- Directory.Build.props | 6 +-- Directory.Build.targets | 2 +- Directory.Packages.props | 38 +++++++++---------- docs/CHANGELOG.md | 6 ++- global.json | 2 +- .../Default/DiscoveryResponseGenerator.cs | 2 +- .../Default/DefaultKeyMaterialService.cs | 2 +- .../CustomTokenRequestValidatorClient.cs | 2 +- .../Clients/CustomTokenResponseClients.cs | 4 +- .../Clients/UserInfoClient.cs | 2 +- .../Introspection/IntrospectionTests.cs | 8 ++-- version.json | 2 +- 12 files changed, 40 insertions(+), 36 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9a4744e5..51ca78d5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,10 +1,10 @@ - net8.0 + net9.0 - 8.0.4 - 8.0.4 + 8.0.5-preview-net9 + 8.0.5-preview-net9 HigginsSoft.$(MSBuildProjectName) $(MSBuildProjectName) enable diff --git a/Directory.Build.targets b/Directory.Build.targets index a6f1625a..b82d9ece 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -8,7 +8,7 @@ BeforeTargets="ProcessFrameworkReferences"> diff --git a/Directory.Packages.props b/Directory.Packages.props index 2b0e2c7b..568ba268 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,11 +2,11 @@ true true - 8.0.0 - 8.0.1 - 8.0.0 - 8.0.0 - 8.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 8.0.0-preview.2.23619.3 2.59.0 8.0.0 @@ -23,7 +23,7 @@ - + @@ -47,7 +47,7 @@ - + @@ -59,7 +59,7 @@ - + @@ -71,10 +71,10 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -95,22 +95,22 @@ - - + + - + - - + + @@ -144,7 +144,7 @@ - + @@ -160,7 +160,7 @@ - + @@ -171,6 +171,6 @@ all - + \ No newline at end of file diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index db27fa11..ac681abc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,9 +6,13 @@ and this project adheres to [Semantic Versioning 2](http://semver.org/). ## [Unreleased] - 2024-02-17 +- NET 9.0 Support +- Updated IdentityModel to 7 and updated OpenIdConnect to 8.1.2 +- Removed CollectionUtilities.IsNullOrEmpty dependecy because of obselence in new version of Microsoft.IdentityModel.Tokens - Current templates and quickstarts being added to seperate template and quickstart repositories to continue previous version functionality. - DotNet tool to install template currently under development. -- + + ## [8.0.4] - 2024-02-17 Identity Server 8.0.4 is a security release that addresses hundreds of security vulnerabilities in the IdentityServer8 code base. We recommend that you update to this version. diff --git a/global.json b/global.json index f3365c41..20f482a8 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.100" + "version": "9.0.100" } } \ No newline at end of file diff --git a/src/IdentityServer8/src/ResponseHandling/Default/DiscoveryResponseGenerator.cs b/src/IdentityServer8/src/ResponseHandling/Default/DiscoveryResponseGenerator.cs index 0bdce2f3..c31e8f39 100644 --- a/src/IdentityServer8/src/ResponseHandling/Default/DiscoveryResponseGenerator.cs +++ b/src/IdentityServer8/src/ResponseHandling/Default/DiscoveryResponseGenerator.cs @@ -331,7 +331,7 @@ where scope.ShowInDiscoveryDocument } // custom entries - if (!Options.Discovery.CustomEntries.IsNullOrEmpty()) + if (!Options.Discovery.CustomEntries.EnumerableIsNullOrEmpty()) { foreach ((string key, object value) in Options.Discovery.CustomEntries) { diff --git a/src/IdentityServer8/src/Services/Default/DefaultKeyMaterialService.cs b/src/IdentityServer8/src/Services/Default/DefaultKeyMaterialService.cs index caed7439..fc5d6f8c 100644 --- a/src/IdentityServer8/src/Services/Default/DefaultKeyMaterialService.cs +++ b/src/IdentityServer8/src/Services/Default/DefaultKeyMaterialService.cs @@ -39,7 +39,7 @@ public async Task GetSigningCredentialsAsync(IEnumerable GetFields(JsonElement json) } private void ValidateCustomFields(TokenResponse response) { - var fields = GetFields(response.Json); + var fields = GetFields(response.Json.Value); fields["custom"].ToString().Should().Be("custom"); } diff --git a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/CustomTokenResponseClients.cs b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/CustomTokenResponseClients.cs index 8e8721ae..2873c4f6 100644 --- a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/CustomTokenResponseClients.cs +++ b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/CustomTokenResponseClients.cs @@ -287,13 +287,13 @@ private CustomResponseDto GetDto(JsonElement responseObject) private Dictionary GetFieldsD(TokenResponse response) { - return response.Json.ToObject>(); + return response.Json?.Deserialize>() ?? []; } private Dictionary GetFields(TokenResponse response) { - return GetFields(response.Json); + return GetFields(response.Json.Value); } private Dictionary GetFields(JsonElement json) diff --git a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/UserInfoClient.cs b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/UserInfoClient.cs index 1f6600b6..d7a83219 100644 --- a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/UserInfoClient.cs +++ b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/UserInfoClient.cs @@ -203,7 +203,7 @@ public async Task Complex_json_should_be_correct() }); //roles = ((JArray)userInfo.Json["role"]).Select(x => x.ToString()).ToArray(); - roles = userInfo.Json.TryGetStringArray("role").ToArray(); + roles = userInfo.Json.Value.TryGetStringArray("role").ToArray(); roles.Length.Should().Be(2); roles.Should().Contain("Geek"); roles.Should().Contain("Developer"); diff --git a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Endpoints/Introspection/IntrospectionTests.cs b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Endpoints/Introspection/IntrospectionTests.cs index 630a317f..bcf671a9 100644 --- a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Endpoints/Introspection/IntrospectionTests.cs +++ b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Endpoints/Introspection/IntrospectionTests.cs @@ -188,7 +188,7 @@ public async Task Response_data_should_be_valid_using_single_scope() Token = tokenResponse.AccessToken }); - var values = introspectionResponse.Json.ToObject>(); + var values = introspectionResponse.Json?.Deserialize>() ?? []; values["aud"].ValueKind.Should().Be(JsonValueKind.String); values["iss"].ValueKind.Should().Be(JsonValueKind.String); @@ -227,7 +227,7 @@ public async Task Response_data_with_user_authentication_should_be_valid_using_s Token = tokenResponse.AccessToken }); - var values = introspectionResponse.Json.ToObject>(); + var values = introspectionResponse.Json?.Deserialize>() ?? []; values["aud"].ValueKind.Should().Be(JsonValueKind.String); values["iss"].ValueKind.Should().Be(JsonValueKind.String); @@ -266,7 +266,7 @@ public async Task Response_data_should_be_valid_using_multiple_scopes_multiple_a Token = tokenResponse.AccessToken }); - var values = introspectionResponse.Json.ToObject>(); + var values = introspectionResponse.Json?.Deserialize>() ?? []; values["aud"].GetType().Name.Should().Be("JsonElement"); @@ -317,7 +317,7 @@ public async Task Response_data_should_be_valid_using_multiple_scopes_single_aud Token = tokenResponse.AccessToken }); - var values = introspectionResponse.Json.ToObject>(); + var values = introspectionResponse.Json?.Deserialize>() ?? []; values["aud"].ValueKind.Should().Be(JsonValueKind.String); values["aud"].ValueKind.Should().Be(JsonValueKind.String); values["iss"].ValueKind.Should().Be(JsonValueKind.String); diff --git a/version.json b/version.json index 06202593..bd0d0882 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "8.0.4-beta.4", + "version": "8.0.5-preview-net9", "nugetPackageVersion": { "semVer": 2, // optional. Set to either 1 or 2 to control how the NuGet package version string is generated. Default is 1. "precision": "revision" From b8bf243f4df9d3d76624e5c2d7ce73cb7d5f29a3 Mon Sep 17 00:00:00 2001 From: Fatih Tolga Ata Date: Wed, 27 Nov 2024 16:15:04 +0300 Subject: [PATCH 2/2] Upgraded old JwtSecurityToken to new JsonWebToken. --- Directory.Packages.props | 10 +- .../DbContexts/ClientDbContextTests.cs | 2 +- .../test/IntegrationTests/IntegrationTest.cs | 2 +- .../Stores/ClientStoreTests.cs | 2 +- .../Stores/DeviceFlowStoreTests.cs | 2 +- .../Stores/PersistedGrantStoreTests.cs | 2 +- .../Stores/ResourceStoreTests.cs | 2 +- .../TokenCleanup/TokenCleanupTests.cs | 2 +- .../IntegrationTest.cs | 2 +- .../Services/CorsPolicyServiceTests.cs | 2 +- .../src/Extensions/TokenExtensions.cs | 6 +- .../Default/DefaultTokenCreationService.cs | 39 ++++- src/IdentityServer8/src/Test/TestUserStore.cs | 5 - .../JwtBearerClientAssertionSecretParser.cs | 4 +- .../Validation/Default/JwtRequestValidator.cs | 75 +++++---- .../Default/PrivateKeyJwtSecretValidator.cs | 37 +++-- .../src/Validation/Default/TokenValidator.cs | 13 +- .../Clients/ClientAssertionClient.cs | 43 ++--- .../Clients/ExtensionGrantClient.cs | 8 +- .../Conformance/Basic/CodeFlowTests.cs | 5 +- .../Basic/ResponseTypeResponseModeTests.cs | 4 +- .../Authorize/JwtRequestAuthorizeTests.cs | 34 ++-- .../Validation/AccessTokenValidation.cs | 9 +- .../Validation/IdentityTokenValidation.cs | 7 +- .../Secrets/ClientAssertionSecretParsing.cs | 13 +- .../Secrets/PrivateKeyJwtSecretValidation.cs | 152 +++++++++++++----- 26 files changed, 306 insertions(+), 176 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 568ba268..3f0f6c02 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,13 +27,13 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + @@ -112,7 +112,7 @@ - + @@ -165,8 +165,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/EntityFramework.Storage/test/IntegrationTests/DbContexts/ClientDbContextTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/DbContexts/ClientDbContextTests.cs index 283bbe56..101834cc 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/DbContexts/ClientDbContextTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/DbContexts/ClientDbContextTests.cs @@ -23,7 +23,7 @@ public class ClientDbContextTests : IntegrationTest fixture) : base(fixture) { - foreach (var options in TestDatabaseProviders.SelectMany(x => x.Select(y => (DbContextOptions)y)).ToList()) + foreach (var options in TestDatabaseProviders) { using (var context = new ConfigurationDbContext(options, StoreOptions)) context.Database.EnsureCreated(); diff --git a/src/EntityFramework.Storage/test/IntegrationTests/IntegrationTest.cs b/src/EntityFramework.Storage/test/IntegrationTests/IntegrationTest.cs index c17dc35f..757b330a 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/IntegrationTest.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/IntegrationTest.cs @@ -60,7 +60,7 @@ static IntegrationTest() protected IntegrationTest(DatabaseProviderFixture fixture) { - fixture.Options = TestDatabaseProviders.SelectMany(x => x.Select(y => (DbContextOptions)y)).ToList(); + fixture.Options = (TestDatabaseProviders as IEnumerable>).ToList(); fixture.StoreOptions = StoreOptions; } } diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/ClientStoreTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/ClientStoreTests.cs index ac5a895c..110259a4 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/ClientStoreTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/ClientStoreTests.cs @@ -26,7 +26,7 @@ public class ClientStoreTests : IntegrationTest fixture) : base(fixture) { - foreach (var options in TestDatabaseProviders.SelectMany(x => x.Select(y => (DbContextOptions) y)).ToList()) + foreach (var options in TestDatabaseProviders) { using (var context = new ConfigurationDbContext(options, StoreOptions)) { diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/DeviceFlowStoreTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/DeviceFlowStoreTests.cs index 1f96c47b..4b2ac2d7 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/DeviceFlowStoreTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/DeviceFlowStoreTests.cs @@ -31,7 +31,7 @@ public class DeviceFlowStoreTests : IntegrationTest fixture) : base(fixture) { - foreach (var options in TestDatabaseProviders.SelectMany(x => x.Select(y => (DbContextOptions)y)).ToList()) + foreach (var options in TestDatabaseProviders) { using (var context = new PersistedGrantDbContext(options, StoreOptions)) context.Database.EnsureCreated(); diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/PersistedGrantStoreTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/PersistedGrantStoreTests.cs index 11c16d9d..c0157544 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/PersistedGrantStoreTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/PersistedGrantStoreTests.cs @@ -26,7 +26,7 @@ public class PersistedGrantStoreTests : IntegrationTest fixture) : base(fixture) { - foreach (var options in TestDatabaseProviders.SelectMany(x => x.Select(y => (DbContextOptions)y)).ToList()) + foreach (var options in TestDatabaseProviders) { using (var context = new PersistedGrantDbContext(options, StoreOptions)) context.Database.EnsureCreated(); diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/ResourceStoreTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/ResourceStoreTests.cs index eb4882f0..cbb398b9 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/ResourceStoreTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/ResourceStoreTests.cs @@ -25,7 +25,7 @@ public class ScopeStoreTests : IntegrationTest fixture) : base(fixture) { - foreach (var options in TestDatabaseProviders.SelectMany(x => x.Select(y => (DbContextOptions)y)).ToList()) + foreach (var options in TestDatabaseProviders) { using (var context = new ConfigurationDbContext(options, StoreOptions)) context.Database.EnsureCreated(); diff --git a/src/EntityFramework.Storage/test/IntegrationTests/TokenCleanup/TokenCleanupTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/TokenCleanup/TokenCleanupTests.cs index 5d131ffa..46a28c19 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/TokenCleanup/TokenCleanupTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/TokenCleanup/TokenCleanupTests.cs @@ -30,7 +30,7 @@ public class TokenCleanupTests : IntegrationTest fixture) : base(fixture) { - foreach (var options in TestDatabaseProviders.SelectMany(x => x.Select(y => (DbContextOptions)y)).ToList()) + foreach (var options in TestDatabaseProviders) { using (var context = new PersistedGrantDbContext(options, StoreOptions)) { diff --git a/src/EntityFramework/test/IdentityServer8.EntityFramework.Tests/IntegrationTest.cs b/src/EntityFramework/test/IdentityServer8.EntityFramework.Tests/IntegrationTest.cs index 71abbef8..d653da21 100644 --- a/src/EntityFramework/test/IdentityServer8.EntityFramework.Tests/IntegrationTest.cs +++ b/src/EntityFramework/test/IdentityServer8.EntityFramework.Tests/IntegrationTest.cs @@ -60,7 +60,7 @@ static IntegrationTest() protected IntegrationTest(DatabaseProviderFixture fixture) { - fixture.Options = TestDatabaseProviders.SelectMany(x => x.Select(y => (DbContextOptions)y)).ToList(); + fixture.Options = (TestDatabaseProviders as IEnumerable>).ToList(); fixture.StoreOptions = StoreOptions; } } diff --git a/src/EntityFramework/test/IdentityServer8.EntityFramework.Tests/Services/CorsPolicyServiceTests.cs b/src/EntityFramework/test/IdentityServer8.EntityFramework.Tests/Services/CorsPolicyServiceTests.cs index d4b16db8..46459162 100644 --- a/src/EntityFramework/test/IdentityServer8.EntityFramework.Tests/Services/CorsPolicyServiceTests.cs +++ b/src/EntityFramework/test/IdentityServer8.EntityFramework.Tests/Services/CorsPolicyServiceTests.cs @@ -27,7 +27,7 @@ public class CorsPolicyServiceTests : IntegrationTest fixture) : base(fixture) { - foreach (var options in TestDatabaseProviders.SelectMany(x => x.Select(y => (DbContextOptions)y)).ToList()) + foreach (var options in TestDatabaseProviders) { using (var context = new ConfigurationDbContext(options, StoreOptions)) context.Database.EnsureCreated(); diff --git a/src/IdentityServer8/src/Extensions/TokenExtensions.cs b/src/IdentityServer8/src/Extensions/TokenExtensions.cs index b045ca68..a1942867 100644 --- a/src/IdentityServer8/src/Extensions/TokenExtensions.cs +++ b/src/IdentityServer8/src/Extensions/TokenExtensions.cs @@ -44,7 +44,7 @@ public static JwtPayload CreateJwtPayload(this Token token, ISystemClock clock, var amrClaims = token.Claims.Where(x => x.Type == JwtClaimTypes.AuthenticationMethod).ToArray(); var scopeClaims = token.Claims.Where(x => x.Type == JwtClaimTypes.Scope).ToArray(); var jsonClaims = token.Claims.Where(x => x.ValueType == IdentityServerConstants.ClaimValueTypes.Json).ToList(); - + // add confirmation claim if present (it's JSON valued) if (token.Confirmation.IsPresent()) { @@ -79,7 +79,7 @@ public static JwtPayload CreateJwtPayload(this Token token, ISystemClock clock, var amrValues = amrClaims.Select(x => x.Value).Distinct().ToArray(); payload.Add(JwtClaimTypes.AuthenticationMethod, amrValues); } - + // deal with json types // calling ToArray() to trigger JSON parsing once and so later // collection identity comparisons work for the anonymous type @@ -121,7 +121,7 @@ public static JwtPayload CreateJwtPayload(this Token token, ISystemClock clock, var newArr = new List(); foreach (var arrays in group) { - var arr = (JArray)arrays.JsonValue; + var arr = (JArray) arrays.JsonValue; newArr.AddRange(arr); } diff --git a/src/IdentityServer8/src/Services/Default/DefaultTokenCreationService.cs b/src/IdentityServer8/src/Services/Default/DefaultTokenCreationService.cs index 99852661..385dce88 100644 --- a/src/IdentityServer8/src/Services/Default/DefaultTokenCreationService.cs +++ b/src/IdentityServer8/src/Services/Default/DefaultTokenCreationService.cs @@ -10,6 +10,7 @@ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. */ +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; namespace IdentityServer8.Services; @@ -70,7 +71,7 @@ public virtual async Task CreateTokenAsync(Token token) var header = await CreateHeaderAsync(token); var payload = await CreatePayloadAsync(token); - return await CreateJwtAsync(new JwtSecurityToken(header, payload)); + return await CreateJwtAsync(header.Header, header.Credentials, payload); } /// @@ -78,7 +79,7 @@ public virtual async Task CreateTokenAsync(Token token) /// /// The token. /// The JWT header - protected virtual async Task CreateHeaderAsync(Token token) + protected virtual async Task<(Dictionary Header, SigningCredentials Credentials)> CreateHeaderAsync(Token token) { var credential = await Keys.GetSigningCredentialsAsync(token.AllowedSigningAlgorithms); @@ -87,7 +88,11 @@ protected virtual async Task CreateHeaderAsync(Token token) throw new InvalidOperationException("No signing credential is configured. Can't create JWT token"); } - var header = new JwtHeader(credential); + var header = new Dictionary + { + { "alg", credential.Algorithm }, + { "kid", credential.Key.KeyId } + }; // emit x5t claim for backwards compatibility with v4 of MS JWT library if (credential.Key is X509SecurityKey x509Key) @@ -109,7 +114,7 @@ protected virtual async Task CreateHeaderAsync(Token token) } } - return header; + return (header, credential); } /// @@ -126,11 +131,29 @@ protected virtual Task CreatePayloadAsync(Token token) /// /// Applies the signature to the JWT /// - /// The JWT object. + /// The JWT header. + /// The JWT payload. /// The signed JWT - protected virtual Task CreateJwtAsync(JwtSecurityToken jwt) + protected virtual Task CreateJwtAsync(Dictionary header, SigningCredentials signingCredentials, JwtPayload payload) { - var handler = new JwtSecurityTokenHandler(); - return Task.FromResult(handler.WriteToken(jwt)); + var handler = new JsonWebTokenHandler(); + var descriptor = new SecurityTokenDescriptor + { + Claims = payload, + Issuer = payload.TryGetValue("iss", out var issuer) ? issuer.ToString() : null, + Expires = payload.TryGetValue("exp", out var exp) ? DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(exp)).UtcDateTime : null, + SigningCredentials = signingCredentials + }; + if (header.TryGetValue("typ", out var typ)) + descriptor.TokenType = typ.ToString(); + + if (payload.TryGetValue("aud", out var audObj) && audObj is List audiences) + audiences.ForEach(descriptor.Audiences.Add); + + var jwt = handler.CreateToken(descriptor); + + return Task.FromResult(jwt); } + + } diff --git a/src/IdentityServer8/src/Test/TestUserStore.cs b/src/IdentityServer8/src/Test/TestUserStore.cs index e0ba90b0..85a66e57 100644 --- a/src/IdentityServer8/src/Test/TestUserStore.cs +++ b/src/IdentityServer8/src/Test/TestUserStore.cs @@ -103,11 +103,6 @@ public TestUser AutoProvisionUser(string provider, string userId, List cl { filtered.Add(new Claim(JwtClaimTypes.Name, claim.Value)); } - // if the JWT handler has an outbound mapping to an OIDC claim use that - else if (JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.ContainsKey(claim.Type)) - { - filtered.Add(new Claim(JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap[claim.Type], claim.Value)); - } // copy the claim as-is else { diff --git a/src/IdentityServer8/src/Validation/Default/JwtBearerClientAssertionSecretParser.cs b/src/IdentityServer8/src/Validation/Default/JwtBearerClientAssertionSecretParser.cs index 2fff5f4c..55913758 100644 --- a/src/IdentityServer8/src/Validation/Default/JwtBearerClientAssertionSecretParser.cs +++ b/src/IdentityServer8/src/Validation/Default/JwtBearerClientAssertionSecretParser.cs @@ -10,6 +10,8 @@ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. */ +using Microsoft.IdentityModel.JsonWebTokens; + namespace IdentityServer8.Validation; /// @@ -104,7 +106,7 @@ private string GetClientIdFromToken(string token) { try { - var jwt = new JwtSecurityToken(token); + var jwt = new JsonWebToken(token); return jwt.Subject; } catch (Exception e) diff --git a/src/IdentityServer8/src/Validation/Default/JwtRequestValidator.cs b/src/IdentityServer8/src/Validation/Default/JwtRequestValidator.cs index fdcd2b9a..97e17790 100644 --- a/src/IdentityServer8/src/Validation/Default/JwtRequestValidator.cs +++ b/src/IdentityServer8/src/Validation/Default/JwtRequestValidator.cs @@ -10,6 +10,7 @@ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. */ +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; namespace IdentityServer8.Validation; @@ -21,14 +22,11 @@ public class JwtRequestValidator { private readonly string _audienceUri; private readonly IHttpContextAccessor _httpContextAccessor; - + /// /// JWT handler /// - protected JwtSecurityTokenHandler Handler = new JwtSecurityTokenHandler - { - MapInboundClaims = false - }; + protected JsonWebTokenHandler Handler = new JsonWebTokenHandler(); /// /// The audience URI to use @@ -50,7 +48,7 @@ protected string AudienceUri /// The logger /// protected readonly ILogger Logger; - + /// /// The optione /// @@ -62,7 +60,7 @@ protected string AudienceUri public JwtRequestValidator(IHttpContextAccessor contextAccessor, IdentityServerOptions options, ILogger logger) { _httpContextAccessor = contextAccessor; - + Options = options; Logger = logger; } @@ -106,10 +104,10 @@ public virtual async Task ValidateAsync(Client clien return fail; } - JwtSecurityToken jwtSecurityToken; + JsonWebToken jwtToken; try { - jwtSecurityToken = await ValidateJwtAsync(jwtTokenString, trustedKeys, client); + jwtToken = await ValidateJwtAsync(jwtTokenString, trustedKeys, client); } catch (Exception e) { @@ -117,14 +115,14 @@ public virtual async Task ValidateAsync(Client clien return fail; } - if (jwtSecurityToken.Payload.ContainsKey(OidcConstants.AuthorizeRequest.Request) || - jwtSecurityToken.Payload.ContainsKey(OidcConstants.AuthorizeRequest.RequestUri)) + // Check for prohibited claims + if (jwtToken.Claims.Any(c => c.Type == OidcConstants.AuthorizeRequest.Request || c.Type == OidcConstants.AuthorizeRequest.RequestUri)) { Logger.LogError("JWT payload must not contain request or request_uri"); return fail; } - var payload = await ProcessPayloadAsync(jwtSecurityToken); + var payload = await ProcessPayloadAsync(jwtToken); var result = new JwtRequestValidationResult { @@ -153,7 +151,7 @@ protected virtual Task> GetKeysAsync(Client client) /// The keys /// The client /// - protected virtual Task ValidateJwtAsync(string jwtTokenString, IEnumerable keys, Client client) + protected virtual async Task ValidateJwtAsync(string jwtTokenString, IEnumerable keys, Client client) { var tokenValidationParameters = new TokenValidationParameters { @@ -175,9 +173,15 @@ protected virtual Task ValidateJwtAsync(string jwtTokenString, tokenValidationParameters.ValidTypes = new[] { JwtClaimTypes.JwtTypes.AuthorizationRequest }; } - Handler.ValidateToken(jwtTokenString, tokenValidationParameters, out var token); - - return Task.FromResult((JwtSecurityToken)token); + var validationResult = await Handler.ValidateTokenAsync(jwtTokenString, tokenValidationParameters); + + if (!validationResult.IsValid) + { + throw new SecurityTokenValidationException(validationResult.Exception?.Message ?? "JWT validation failed."); + } + + var jwt = Handler.ReadJsonWebToken(jwtTokenString); + return jwt; } /// @@ -185,27 +189,38 @@ protected virtual Task ValidateJwtAsync(string jwtTokenString, /// /// The JWT token /// - protected virtual Task> ProcessPayloadAsync(JwtSecurityToken token) + protected virtual Task> ProcessPayloadAsync(JsonWebToken token) { // filter JWT validation values var payload = new Dictionary(); - foreach (var key in token.Payload.Keys) + + foreach (var claim in token.Claims) { - if (!Constants.Filters.JwtRequestClaimTypesFilter.Contains(key)) + if (!Constants.Filters.JwtRequestClaimTypesFilter.Contains(claim.Type)) { - var value = token.Payload[key]; + var value = claim.Value; - switch (value) + try + { + // Try to parse the value as JSON + var parsedJson = JToken.Parse(value); + switch (parsedJson) + { + case JObject jobj: + payload.Add(claim.Type, jobj.ToString(Formatting.None)); + break; + case JArray jarr: + payload.Add(claim.Type, jarr.ToString(Formatting.None)); + break; + default: + payload.Add(claim.Type, value); + break; + } + } + catch (JsonReaderException) { - case string s: - payload.Add(key, s); - break; - case JObject jobj: - payload.Add(key, jobj.ToString(Formatting.None)); - break; - case JArray jarr: - payload.Add(key, jarr.ToString(Formatting.None)); - break; + // If parsing fails, treat as a string + payload.Add(claim.Type, value); } } } diff --git a/src/IdentityServer8/src/Validation/Default/PrivateKeyJwtSecretValidator.cs b/src/IdentityServer8/src/Validation/Default/PrivateKeyJwtSecretValidator.cs index 3f2a8d53..20b42387 100644 --- a/src/IdentityServer8/src/Validation/Default/PrivateKeyJwtSecretValidator.cs +++ b/src/IdentityServer8/src/Validation/Default/PrivateKeyJwtSecretValidator.cs @@ -10,7 +10,9 @@ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. */ +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; +using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; namespace IdentityServer8.Validation; @@ -24,7 +26,7 @@ public class PrivateKeyJwtSecretValidator : ISecretValidator private readonly ILogger _logger; private const string Purpose = nameof(PrivateKeyJwtSecretValidator); - + /// /// Instantiates an instance of private_key_jwt secret validator /// @@ -86,7 +88,7 @@ public async Task ValidateAsync(IEnumerable secr string.Concat(_contextAccessor.HttpContext.GetIdentityServerIssuerUri().EnsureTrailingSlash(), Constants.ProtocolRoutePaths.Token) }; - + var tokenValidationParameters = new TokenValidationParameters { IssuerSigningKeys = trustedKeys, @@ -100,34 +102,43 @@ public async Task ValidateAsync(IEnumerable secr RequireSignedTokens = true, RequireExpirationTime = true, - + ClockSkew = TimeSpan.FromMinutes(5) }; try { - var handler = new JwtSecurityTokenHandler(); - handler.ValidateToken(jwtTokenString, tokenValidationParameters, out var token); + var handler = new JsonWebTokenHandler(); + var validateResult = await handler.ValidateTokenAsync(jwtTokenString, tokenValidationParameters); - var jwtToken = (JwtSecurityToken)token; + var jwtToken = (JsonWebToken) validateResult.SecurityToken; if (jwtToken.Subject != jwtToken.Issuer) { _logger.LogError("Both 'sub' and 'iss' in the client assertion token must have a value of client_id."); return fail; } - - var exp = jwtToken.Payload.Exp; - if (!exp.HasValue) + + var expClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Exp); + + if (expClaim == null) { _logger.LogError("exp is missing."); return fail; } - - var jti = jwtToken.Payload.Jti; - if (jti.IsMissing()) + + if (!long.TryParse(expClaim.Value, out var exp)) + { + _logger.LogError("Invalid exp value."); + return fail; + } + + var jtiClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Jti); + + if (jtiClaim == null || string.IsNullOrEmpty(jtiClaim.Value)) { _logger.LogError("jti is missing."); return fail; } + var jti = jtiClaim.Value; if (await _replayCache.ExistsAsync(Purpose, jti)) { @@ -136,7 +147,7 @@ public async Task ValidateAsync(IEnumerable secr } else { - await _replayCache.AddAsync(Purpose, jti, DateTimeOffset.FromUnixTimeSeconds(exp.Value).AddMinutes(5)); + await _replayCache.AddAsync(Purpose, jti, DateTimeOffset.FromUnixTimeSeconds(exp).AddMinutes(5)); } return success; diff --git a/src/IdentityServer8/src/Validation/Default/TokenValidator.cs b/src/IdentityServer8/src/Validation/Default/TokenValidator.cs index 665ff315..92ae15ae 100644 --- a/src/IdentityServer8/src/Validation/Default/TokenValidator.cs +++ b/src/IdentityServer8/src/Validation/Default/TokenValidator.cs @@ -10,6 +10,7 @@ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. */ +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; namespace IdentityServer8.Validation; @@ -237,7 +238,7 @@ public async Task ValidateAccessTokenAsync(string token, private async Task ValidateJwtAsync(string jwt, IEnumerable validationKeys, bool validateLifetime = true, string audience = null) { - var handler = new JwtSecurityTokenHandler(); + var handler = new JsonWebTokenHandler(); handler.InboundClaimTypeMap.Clear(); var parameters = new TokenValidationParameters @@ -258,15 +259,17 @@ private async Task ValidateJwtAsync(string jwt, IEnumerab try { - var id = handler.ValidateToken(jwt, parameters, out var securityToken); - var jwtSecurityToken = securityToken as JwtSecurityToken; + var result = await handler.ValidateTokenAsync(jwt, parameters); + var securityToken = result.SecurityToken; + var id = result.ClaimsIdentity; + var jwtSecurityToken = securityToken as JsonWebToken; // if no audience is specified, we make at least sure that it is an access token if (audience.IsMissing()) { if (_options.AccessTokenJwtType.IsPresent()) { - var type = jwtSecurityToken.Header.Typ; + var type = jwtSecurityToken.Typ; if (!string.Equals(type, _options.AccessTokenJwtType)) { return new TokenValidationResult @@ -405,7 +408,7 @@ private string GetClientIdFromJwt(string token) { try { - var jwt = new JwtSecurityToken(token); + var jwt = new JsonWebToken(token); var clientId = jwt.Audiences.FirstOrDefault(); return clientId; diff --git a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/ClientAssertionClient.cs b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/ClientAssertionClient.cs index 3ee6bedb..78503987 100644 --- a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/ClientAssertionClient.cs +++ b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/ClientAssertionClient.cs @@ -17,6 +17,7 @@ copies or substantial portions of the Software. using IdentityServer.IntegrationTests.Common; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; @@ -246,24 +247,28 @@ private string CreateToken(string clientId, DateTime? nowOverride = null) var certificate = TestCert.Load(); var now = nowOverride ?? DateTime.UtcNow; - var token = new JwtSecurityToken( - clientId, - TokenEndpoint, - new List() - { - new Claim("jti", Guid.NewGuid().ToString()), - new Claim(JwtClaimTypes.Subject, clientId), - new Claim(JwtClaimTypes.IssuedAt, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) - }, - now, - now.AddMinutes(1), - new SigningCredentials( - new X509SecurityKey(certificate), - SecurityAlgorithms.RsaSha256 - ) - ); - - var tokenHandler = new JwtSecurityTokenHandler(); - return tokenHandler.WriteToken(token); + var tokenHandler = new JsonWebTokenHandler(); + + var claims = new List + { + new Claim("jti", Guid.NewGuid().ToString()), + new Claim(JwtClaimTypes.Subject, clientId), + new Claim(JwtClaimTypes.IssuedAt, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) + }; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Issuer = clientId, + Audience = TokenEndpoint, + Claims = claims.ToDictionary(c => c.Type, c => (object)c.Value), + NotBefore = now, + Expires = now.AddMinutes(1), + SigningCredentials = new SigningCredentials( + new X509SecurityKey(certificate), + SecurityAlgorithms.RsaSha256 + ) + }; + + return tokenHandler.CreateToken(tokenDescriptor); } } diff --git a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/ExtensionGrantClient.cs b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/ExtensionGrantClient.cs index 95b61c17..1964fc5a 100644 --- a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/ExtensionGrantClient.cs +++ b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Clients/ExtensionGrantClient.cs @@ -16,6 +16,7 @@ copies or substantial portions of the Software. using IdentityServer.IntegrationTests.Clients.Setup; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.IdentityModel.JsonWebTokens; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.IdentityModel.Tokens.Jwt; @@ -468,8 +469,11 @@ public async Task Impersonate_client_should_succeed() response.AccessToken.Should().Contain("."); - var jwt = new JwtSecurityToken(response.AccessToken); - jwt.Payload["client_id"].Should().Be("impersonated_client_id"); + var handler = new JsonWebTokenHandler(); + var jwt = handler.ReadJsonWebToken(response.AccessToken); + + // Payload'dan client_id değerini kontrol et + jwt.Claims.First(c => c.Type == "client_id").Value.Should().Be("impersonated_client_id"); } [Fact] diff --git a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Conformance/Basic/CodeFlowTests.cs b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Conformance/Basic/CodeFlowTests.cs index 1cc61ec9..5a428745 100644 --- a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Conformance/Basic/CodeFlowTests.cs +++ b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Conformance/Basic/CodeFlowTests.cs @@ -23,6 +23,7 @@ copies or substantial portions of the Software. using IdentityServer8.Configuration; using IdentityServer8.Models; using IdentityServer8.Test; +using Microsoft.IdentityModel.JsonWebTokens; using Xunit; namespace IdentityServer.IntegrationTests.Conformance.Basic; @@ -114,7 +115,7 @@ public async Task No_state_should_not_result_in_shash() tokenResult.ExpiresIn.Should().BeGreaterThan(0); tokenResult.IdentityToken.Should().NotBeNull(); - var token = new JwtSecurityToken(tokenResult.IdentityToken); + var token = new JsonWebToken(tokenResult.IdentityToken); var s_hash = token.Claims.FirstOrDefault(c => c.Type == "s_hash"); s_hash.Should().BeNull(); @@ -163,7 +164,7 @@ public async Task State_should_result_in_shash() tokenResult.ExpiresIn.Should().BeGreaterThan(0); tokenResult.IdentityToken.Should().NotBeNull(); - var token = new JwtSecurityToken(tokenResult.IdentityToken); + var token = new JsonWebToken(tokenResult.IdentityToken); var s_hash = token.Claims.FirstOrDefault(c => c.Type == "s_hash"); s_hash.Should().NotBeNull(); diff --git a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Conformance/Basic/ResponseTypeResponseModeTests.cs b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Conformance/Basic/ResponseTypeResponseModeTests.cs index db96a8ab..da05bf0c 100644 --- a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Conformance/Basic/ResponseTypeResponseModeTests.cs +++ b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Conformance/Basic/ResponseTypeResponseModeTests.cs @@ -101,7 +101,7 @@ public async Task Request_with_response_type_code_supported() // to follow the RFC to address open redirect in original OAuth RFC [Fact] [Trait("Category", Category)] - public async Task Request_missing_response_type_rejected() + public async Task Request_unsupported_response_type_rejected() { await _mockPipeline.LoginAsync("bob"); @@ -110,7 +110,7 @@ public async Task Request_missing_response_type_rejected() var url = _mockPipeline.CreateAuthorizeUrl( clientId: "code_client", - responseType: null, // missing + responseType: "__wrong__", //unsupported scope: "openid", redirectUri: "https://code_client/callback", state: state, diff --git a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs index 4021a23f..56e59dea 100644 --- a/src/IdentityServer8/test/IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs +++ b/src/IdentityServer8/test/IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs @@ -26,6 +26,7 @@ copies or substantial portions of the Software. using IdentityServer8.Configuration; using IdentityServer8.Models; using IdentityServer8.Test; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; using Xunit; @@ -181,21 +182,24 @@ public JwtRequestAuthorizeTests() string CreateRequestJwt(string issuer, string audience, SigningCredentials credential, Claim[] claims, bool setJwtTyp = false) { - var handler = new JwtSecurityTokenHandler(); - handler.OutboundClaimTypeMap.Clear(); + var handler = new JsonWebTokenHandler(); + + var claimsDictionary = claims.ToDictionary(c => c.Type, c => (object) c.Value); + + var descriptor = new SecurityTokenDescriptor + { + Issuer = issuer, + Audience = audience, + SigningCredentials = credential, + Claims = claimsDictionary + }; + if (setJwtTyp) + descriptor.TokenType = JwtClaimTypes.JwtTypes.AuthorizationRequest; - var token = handler.CreateJwtSecurityToken( - issuer: issuer, - audience: audience, - signingCredentials: credential, - subject: Identity.Create("pwd", claims)); + var token = handler.CreateToken(descriptor); - if (setJwtTyp) - { - token.Header["typ"] = JwtClaimTypes.JwtTypes.AuthorizationRequest; - } - return handler.WriteToken(token); + return token; } [Fact] @@ -539,16 +543,16 @@ public async Task authorize_should_accept_complex_objects_in_request_object() var someObj2 = JsonSerializer.Deserialize(_mockPipeline.LoginRequest.Parameters["someObj"], someObj.GetType()); someObj.Should().BeEquivalentTo(someObj2); _mockPipeline.LoginRequest.Parameters["someArr"].Should().NotBeNull(); - var someArr2 =JsonSerializer.Deserialize(_mockPipeline.LoginRequest.Parameters["someArr"]); + var someArr2 = JsonSerializer.Deserialize(_mockPipeline.LoginRequest.Parameters["someArr"]); someArr2.Should().Contain(new[] { "a", "c", "b" }); someArr2.Length.Should().Be(3); _mockPipeline.LoginRequest.RequestObjectValues.Count.Should().Be(13); _mockPipeline.LoginRequest.RequestObjectValues["someObj"].Should().NotBeNull(); - someObj2 =JsonSerializer.Deserialize(_mockPipeline.LoginRequest.RequestObjectValues["someObj"], someObj.GetType()); + someObj2 = JsonSerializer.Deserialize(_mockPipeline.LoginRequest.RequestObjectValues["someObj"], someObj.GetType()); someObj.Should().BeEquivalentTo(someObj2); _mockPipeline.LoginRequest.RequestObjectValues["someArr"].Should().NotBeNull(); - someArr2 =JsonSerializer.Deserialize(_mockPipeline.LoginRequest.Parameters["someArr"]); + someArr2 = JsonSerializer.Deserialize(_mockPipeline.LoginRequest.Parameters["someArr"]); someArr2.Should().Contain(new[] { "a", "c", "b" }); someArr2.Length.Should().Be(3); } diff --git a/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/AccessTokenValidation.cs b/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/AccessTokenValidation.cs index 99097ed9..5592644e 100644 --- a/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/AccessTokenValidation.cs +++ b/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/AccessTokenValidation.cs @@ -31,12 +31,7 @@ public class AccessTokenValidation private IClientStore _clients = Factory.CreateClientStore(); private IdentityServerOptions _options = new IdentityServerOptions(); - private StubClock _clock = new StubClock(); - - static AccessTokenValidation() - { - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - } + private StubClock _clock = new StubClock(); private DateTime now; public DateTime UtcNow @@ -196,7 +191,7 @@ public async Task JWT_Token_with_scopes_have_expected_claims(bool flag) result.Jwt.Should().NotBeNullOrEmpty(); result.Client.ClientId.Should().Be("roclient"); - result.Claims.Count().Should().Be(8); + result.Claims.Count().Should().Be(9); var scopes = result.Claims.Where(c => c.Type == "scope").Select(c => c.Value).ToArray(); scopes.Count().Should().Be(2); scopes[0].Should().Be("read"); diff --git a/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/IdentityTokenValidation.cs b/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/IdentityTokenValidation.cs index d6e0ae69..94fa15c2 100644 --- a/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/IdentityTokenValidation.cs +++ b/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/IdentityTokenValidation.cs @@ -21,12 +21,7 @@ namespace IdentityServer.UnitTests.Validation { public class IdentityTokenValidation { - private const string Category = "Identity token validation"; - - static IdentityTokenValidation() - { - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - } + private const string Category = "Identity token validation"; [Fact] [Trait("Category", Category)] diff --git a/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/Secrets/ClientAssertionSecretParsing.cs b/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/Secrets/ClientAssertionSecretParsing.cs index cbf01dac..c8a751df 100644 --- a/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/Secrets/ClientAssertionSecretParsing.cs +++ b/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/Secrets/ClientAssertionSecretParsing.cs @@ -21,6 +21,8 @@ copies or substantial portions of the Software. using IdentityServer8.Validation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; using Xunit; namespace IdentityServer.UnitTests.Validation.Secrets @@ -53,8 +55,15 @@ public async void Valid_ClientAssertion() { var context = new DefaultHttpContext(); - var token = new JwtSecurityToken(issuer: "issuer", claims: new[] { new Claim("sub", "client") }); - var tokenString = new JwtSecurityTokenHandler().WriteToken(token); + var tokenHandler = new JsonWebTokenHandler(); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Issuer = "issuer", + Claims = new Dictionary{ ["sub"] = "client" }, + }; + + var tokenString = tokenHandler.CreateToken(tokenDescriptor); var body = "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=" + tokenString; diff --git a/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs b/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs index 9cd5af49..48a478ef 100644 --- a/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs +++ b/src/IdentityServer8/test/IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs @@ -27,6 +27,7 @@ copies or substantial portions of the Software. using IdentityServer8.Stores; using IdentityServer8.Validation; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using Xunit; @@ -42,38 +43,47 @@ public PrivateKeyJwtSecretValidation() _validator = new PrivateKeyJwtSecretValidator( new MockHttpContextAccessor( new IdentityServerOptions() - { - IssuerUri = "https://idsrv3.com" - } + { + IssuerUri = "https://idsrv3.com" + } ), - new DefaultReplayCache(new TestCache()), + new DefaultReplayCache(new TestCache()), new LoggerFactory().CreateLogger() ); _clients = new InMemoryClientStore(ClientValidationTestClients.Get()); } - private JwtSecurityToken CreateToken(string clientId, DateTime? nowOverride = null) + private JsonWebToken CreateToken(string clientId, DateTime? nowOverride = null) { var certificate = TestCert.Load(); var now = nowOverride ?? DateTime.UtcNow; - var token = new JwtSecurityToken( - clientId, - "https://idsrv3.com/connect/token", - new List() - { - new Claim("jti", Guid.NewGuid().ToString()), - new Claim(JwtClaimTypes.Subject, clientId), - new Claim(JwtClaimTypes.IssuedAt, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) - }, - now, - now.AddMinutes(1), - new SigningCredentials( - new X509SecurityKey(certificate), - SecurityAlgorithms.RsaSha256 - ) - ); - return token; + var tokenHandler = new JsonWebTokenHandler(); + + var claims = new Dictionary + { + { "jti", Guid.NewGuid().ToString() }, + { JwtClaimTypes.Subject, clientId }, + { JwtClaimTypes.IssuedAt, new DateTimeOffset(now).ToUnixTimeSeconds() } + }; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Issuer = clientId, + Audience = "https://idsrv3.com/connect/token", + Claims = claims, + Expires = now.AddMinutes(1), + SigningCredentials = new SigningCredentials( + new X509SecurityKey(certificate), + SecurityAlgorithms.RsaSha256 + ) + }; + + // Token string olarak oluşturuluyor + var tokenString = tokenHandler.CreateToken(tokenDescriptor); + + // String token'ı JsonWebToken nesnesine dönüştürüp döndür + return tokenHandler.ReadJsonWebToken(tokenString); } [Fact] @@ -86,7 +96,7 @@ public async Task Invalid_Certificate_X5t_Only_Requires_Full_Certificate() var secret = new ParsedSecret { Id = clientId, - Credential = new JwtSecurityTokenHandler().WriteToken(token), + Credential = $"{token.EncodedHeader}.{token.EncodedPayload}.{token.EncodedSignature}", Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; @@ -101,10 +111,11 @@ public async Task Invalid_Certificate_Thumbprint() var clientId = "certificate_invalid"; var client = await _clients.FindEnabledClientByIdAsync(clientId); + var token = CreateToken(clientId); var secret = new ParsedSecret { Id = clientId, - Credential = new JwtSecurityTokenHandler().WriteToken(CreateToken(clientId)), + Credential = $"{token.EncodedHeader}.{token.EncodedPayload}.{token.EncodedSignature}", Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; @@ -119,10 +130,11 @@ public async Task Valid_Certificate_Base64() var clientId = "certificate_base64_valid"; var client = await _clients.FindEnabledClientByIdAsync(clientId); + var token = CreateToken(clientId); var secret = new ParsedSecret { Id = clientId, - Credential = new JwtSecurityTokenHandler().WriteToken(CreateToken(clientId)), + Credential = $"{token.EncodedHeader}.{token.EncodedPayload}.{token.EncodedSignature}", Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; @@ -130,24 +142,24 @@ public async Task Valid_Certificate_Base64() result.Success.Should().BeTrue(); } - + [Fact] public async Task Invalid_Replay() { var clientId = "certificate_base64_valid"; var client = await _clients.FindEnabledClientByIdAsync(clientId); - var token = new JwtSecurityTokenHandler().WriteToken(CreateToken(clientId)); - + + var token = CreateToken(clientId); var secret = new ParsedSecret { Id = clientId, - Credential = token, + Credential = $"{token.EncodedHeader}.{token.EncodedPayload}.{token.EncodedSignature}", Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; var result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeTrue(); - + result = await _validator.ValidateAsync(client.ClientSecrets, secret); result.Success.Should().BeFalse(); } @@ -158,10 +170,11 @@ public async Task Invalid_Certificate_Base64() var clientId = "certificate_base64_invalid"; var client = await _clients.FindEnabledClientByIdAsync(clientId); + var token = CreateToken(clientId); var secret = new ParsedSecret { Id = clientId, - Credential = new JwtSecurityTokenHandler().WriteToken(CreateToken(clientId)), + Credential = $"{token.EncodedHeader}.{token.EncodedPayload}.{token.EncodedSignature}", Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; @@ -177,12 +190,27 @@ public async Task Invalid_Issuer() var client = await _clients.FindEnabledClientByIdAsync(clientId); var token = CreateToken(clientId); - token.Payload.Remove(JwtClaimTypes.Issuer); - token.Payload.Add(JwtClaimTypes.Issuer, "invalid"); + + // "iss" claim'ini manipüle et + var claims = token.Claims.ToList(); + claims.RemoveAll(c => c.Type == JwtClaimTypes.Issuer); + claims.Add(new Claim(JwtClaimTypes.Issuer, "invalid")); + + var modifiedToken = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor + { + Issuer = clientId, + Audience = "https://idsrv3.com/connect/token", + Claims = claims.ToDictionary(c => c.Type, c => (object) c.Value), + SigningCredentials = new SigningCredentials( + new X509SecurityKey(TestCert.Load()), + SecurityAlgorithms.RsaSha256 + ) + }); + var secret = new ParsedSecret { Id = clientId, - Credential = new JwtSecurityTokenHandler().WriteToken(token), + Credential = modifiedToken, Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; @@ -198,12 +226,27 @@ public async Task Invalid_Subject() var client = await _clients.FindEnabledClientByIdAsync(clientId); var token = CreateToken(clientId); - token.Payload.Remove(JwtClaimTypes.Subject); - token.Payload.Add(JwtClaimTypes.Subject, "invalid"); + + // "sub" claim'ini manipüle et + var claims = token.Claims.ToList(); + claims.RemoveAll(c => c.Type == JwtClaimTypes.Subject); + claims.Add(new Claim(JwtClaimTypes.Subject, "invalid")); + + var modifiedToken = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor + { + Issuer = clientId, + Audience = "https://idsrv3.com/connect/token", + Claims = claims.ToDictionary(c => c.Type, c => (object) c.Value), + SigningCredentials = new SigningCredentials( + new X509SecurityKey(TestCert.Load()), + SecurityAlgorithms.RsaSha256 + ) + }); + var secret = new ParsedSecret { Id = clientId, - Credential = new JwtSecurityTokenHandler().WriteToken(token), + Credential = modifiedToken, Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; @@ -212,6 +255,7 @@ public async Task Invalid_Subject() result.Success.Should().BeFalse(); } + [Fact] public async Task Invalid_Expired_Token() { @@ -222,7 +266,7 @@ public async Task Invalid_Expired_Token() var secret = new ParsedSecret { Id = clientId, - Credential = new JwtSecurityTokenHandler().WriteToken(token), + Credential = $"{token.EncodedHeader}.{token.EncodedPayload}.{token.EncodedSignature}", Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; @@ -237,13 +281,35 @@ public async Task Invalid_Unsigned_Token() var clientId = "certificate_valid"; var client = await _clients.FindEnabledClientByIdAsync(clientId); - var token = CreateToken(clientId); - token.Header.Remove("alg"); - token.Header.Add("alg", "none"); + // Unsigned token için header ve claims oluştur + var header = new Dictionary + { + { "alg", "none" } + }; + + var claims = new Dictionary + { + { "jti", Guid.NewGuid().ToString() }, + { JwtClaimTypes.Subject, clientId }, + { JwtClaimTypes.Issuer, clientId }, + { JwtClaimTypes.IssuedAt, new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds() } + }; + + // Header ve Claims JSON string'e dönüştürülüyor + var headerJson = System.Text.Json.JsonSerializer.Serialize(header); + var claimsJson = System.Text.Json.JsonSerializer.Serialize(claims); + + // JSON string Base64Url formatına dönüştürülüyor + var encodedHeader = Base64Url.Encode(System.Text.Encoding.UTF8.GetBytes(headerJson)); + var encodedClaims = Base64Url.Encode(System.Text.Encoding.UTF8.GetBytes(claimsJson)); + + // İmzasız JWT oluşturuluyor + var token = $"{encodedHeader}.{encodedClaims}."; + var secret = new ParsedSecret { Id = clientId, - Credential = new JwtSecurityTokenHandler().WriteToken(token), + Credential = token, Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; @@ -251,5 +317,7 @@ public async Task Invalid_Unsigned_Token() result.Success.Should().BeFalse(); } + + } }