From 338510c705d69d5d60ab071805f701e08e30efbc Mon Sep 17 00:00:00 2001 From: Joe Harvey Date: Fri, 16 Oct 2026 12:59:38 +0100 Subject: [PATCH 1/7] Updated to .net 10 and updated nuget packages replaced IdentityServer4 with Open.IdentityServer --- Rsk.TokenExchange.sln | 4 +-- .../IdentityServerSubjectTokenValidator.cs | 3 ++- ....TokenExchange.DuendeIdentityServer.csproj | 4 +-- .../TokenExchangeExtensionGrantValidator.cs | 8 +++--- .../IdentityServerBuilderExtensions.cs | 4 +-- .../IdentityServerSubjectTokenValidator.cs | 6 ++--- ....TokenExchange.Open.IdentityServer.csproj} | 9 ++++--- .../TokenExchangeExtensionGrantValidator.cs | 13 +++++++--- .../Rsk.TokenExchange.csproj | 20 +++++--------- ...Exchange.DuendeIdentityServer.Tests.csproj | 12 ++++----- ...kenExchangeExtensionGrantValidatorTests.cs | 4 +-- .../IdentityServerBuilderExtensionTests.cs | 6 ++--- ...Exchange.Open.IdentityServer.Tests.csproj} | 15 ++++++----- ...kenExchangeExtensionGrantValidatorTests.cs | 8 +++--- ...Rsk.TokenExchange.Tests.Integration.csproj | 19 ++++++++------ .../TestServer.cs | 8 +++--- .../Rsk.TokenExchange.Tests.csproj | 26 +++++++------------ 17 files changed, 85 insertions(+), 84 deletions(-) rename src/{Rsk.TokenExchange.IdentityServer4 => Rsk.TokenExchange.Open.IdentityServer}/IdentityServerBuilderExtensions.cs (86%) rename src/{Rsk.TokenExchange.IdentityServer4 => Rsk.TokenExchange.Open.IdentityServer}/IdentityServerSubjectTokenValidator.cs (86%) rename src/{Rsk.TokenExchange.IdentityServer4/Rsk.TokenExchange.IdentityServer4.csproj => Rsk.TokenExchange.Open.IdentityServer/Rsk.TokenExchange.Open.IdentityServer.csproj} (79%) rename src/{Rsk.TokenExchange.IdentityServer4 => Rsk.TokenExchange.Open.IdentityServer}/TokenExchangeExtensionGrantValidator.cs (94%) rename test/{Rsk.TokenExchange.IdentityServer4.Tests => Rsk.TokenExchange.Open.IdentityServer.Tests}/IdentityServerBuilderExtensionTests.cs (88%) rename test/{Rsk.TokenExchange.IdentityServer4.Tests/Rsk.TokenExchange.IdentityServer4.Tests.csproj => Rsk.TokenExchange.Open.IdentityServer.Tests/Rsk.TokenExchange.Open.IdentityServer.Tests.csproj} (50%) rename test/{Rsk.TokenExchange.IdentityServer4.Tests => Rsk.TokenExchange.Open.IdentityServer.Tests}/TokenExchangeExtensionGrantValidatorTests.cs (97%) diff --git a/Rsk.TokenExchange.sln b/Rsk.TokenExchange.sln index 65e6bcc..aa7b710 100644 --- a/Rsk.TokenExchange.sln +++ b/Rsk.TokenExchange.sln @@ -13,9 +13,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.Tests", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.Tests.Integration", "test\Rsk.TokenExchange.Tests.Integration\Rsk.TokenExchange.Tests.Integration.csproj", "{98215ED6-5DE9-42AD-BF08-176833F1F22C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.IdentityServer4", "src\Rsk.TokenExchange.IdentityServer4\Rsk.TokenExchange.IdentityServer4.csproj", "{D41B4200-C0A6-488A-9FC5-C0AD075A8228}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.Open.IdentityServer", "src\Rsk.TokenExchange.Open.IdentityServer\Rsk.TokenExchange.Open.IdentityServer.csproj", "{D41B4200-C0A6-488A-9FC5-C0AD075A8228}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.IdentityServer4.Tests", "test\Rsk.TokenExchange.IdentityServer4.Tests\Rsk.TokenExchange.IdentityServer4.Tests.csproj", "{3B3A599B-0B55-4146-A96D-A6A4E1DC1E77}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.Open.IdentityServer.Tests", "test\Rsk.TokenExchange.Open.IdentityServer.Tests\Rsk.TokenExchange.Open.IdentityServer.Tests.csproj", "{3B3A599B-0B55-4146-A96D-A6A4E1DC1E77}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rsk.TokenExchange.DuendeIdentityServer", "src\Rsk.TokenExchange.DuendeIdentityServer\Rsk.TokenExchange.DuendeIdentityServer.csproj", "{4F826FC4-8C6B-43A0-9EB3-031400C01427}" EndProject diff --git a/src/Rsk.TokenExchange.DuendeIdentityServer/IdentityServerSubjectTokenValidator.cs b/src/Rsk.TokenExchange.DuendeIdentityServer/IdentityServerSubjectTokenValidator.cs index 74fab99..65a925b 100644 --- a/src/Rsk.TokenExchange.DuendeIdentityServer/IdentityServerSubjectTokenValidator.cs +++ b/src/Rsk.TokenExchange.DuendeIdentityServer/IdentityServerSubjectTokenValidator.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; using Duende.IdentityServer.Validation; using Rsk.TokenExchange.Validators.Adaptors; @@ -24,7 +25,7 @@ public IdentityServerSubjectTokenValidator(ITokenValidator tokenValidator) /// public async Task ValidateAccessToken(string token) { - var result = await tokenValidator.ValidateAccessTokenAsync(token); + var result = await tokenValidator.ValidateAccessTokenAsync(token, expectedScope: null, CancellationToken.None); return new TokenValidationResult(result.IsError, result.Claims); } } diff --git a/src/Rsk.TokenExchange.DuendeIdentityServer/Rsk.TokenExchange.DuendeIdentityServer.csproj b/src/Rsk.TokenExchange.DuendeIdentityServer/Rsk.TokenExchange.DuendeIdentityServer.csproj index 11fa972..cebd707 100644 --- a/src/Rsk.TokenExchange.DuendeIdentityServer/Rsk.TokenExchange.DuendeIdentityServer.csproj +++ b/src/Rsk.TokenExchange.DuendeIdentityServer/Rsk.TokenExchange.DuendeIdentityServer.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1;net5.0 + net10.0 Rock Solid Knowledge Ltd Token Exchange (RFC 8693) implementation for Deunde IdentityServer. https://github.com/RockSolidKnowledge/TokenExchange @@ -15,7 +15,7 @@ - + diff --git a/src/Rsk.TokenExchange.DuendeIdentityServer/TokenExchangeExtensionGrantValidator.cs b/src/Rsk.TokenExchange.DuendeIdentityServer/TokenExchangeExtensionGrantValidator.cs index e01a4bf..dc24e28 100644 --- a/src/Rsk.TokenExchange.DuendeIdentityServer/TokenExchangeExtensionGrantValidator.cs +++ b/src/Rsk.TokenExchange.DuendeIdentityServer/TokenExchangeExtensionGrantValidator.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; -using IdentityModel; using Microsoft.Extensions.Logging; using Rsk.TokenExchange.Exceptions; using Rsk.TokenExchange.Validators; @@ -46,8 +46,10 @@ public TokenExchangeExtensionGrantValidator( public string GrantType => TokenExchangeConstants.GrantType; /// - public async Task ValidateAsync(ExtensionGrantValidationContext context) + public async Task ValidateAsync(ExtensionGrantValidationContext context, CancellationToken cancellationToken = default) { + if (context == null) throw new ArgumentNullException(nameof(context)); + // warn on no client auth if (context.Request.Secret == null) logger.LogInformation("Recieved unauthenticated token exchange request"); @@ -109,7 +111,7 @@ public virtual void UpdateRequest(ValidatedTokenRequest request, IEnumerable x.Type == JwtClaimTypes.ClientId).Value; + var clientId = claims.First(x => x.Type == "client_id").Value; request.ClientId = clientId; } diff --git a/src/Rsk.TokenExchange.IdentityServer4/IdentityServerBuilderExtensions.cs b/src/Rsk.TokenExchange.Open.IdentityServer/IdentityServerBuilderExtensions.cs similarity index 86% rename from src/Rsk.TokenExchange.IdentityServer4/IdentityServerBuilderExtensions.cs rename to src/Rsk.TokenExchange.Open.IdentityServer/IdentityServerBuilderExtensions.cs index 2073ee5..7cbf22c 100644 --- a/src/Rsk.TokenExchange.IdentityServer4/IdentityServerBuilderExtensions.cs +++ b/src/Rsk.TokenExchange.Open.IdentityServer/IdentityServerBuilderExtensions.cs @@ -2,7 +2,7 @@ using Rsk.TokenExchange.Configuration; using Rsk.TokenExchange.Validators.Adaptors; -namespace Rsk.TokenExchange.IdentityServer4 +namespace Rsk.TokenExchange.Open.IdentityServer { /// /// Token exchange registrations for IIdentityServerBuilder @@ -10,7 +10,7 @@ namespace Rsk.TokenExchange.IdentityServer4 public static class IdentityServerBuilderExtensions { /// - /// Adds the default token exchange registrations for IdentityServer4. + /// Adds the default token exchange registrations for Open.IdentityServer. /// Uses the default authorization policy, designed for token exchange between microservices. /// public static IIdentityServerBuilder AddTokenExchange(this IIdentityServerBuilder builder) diff --git a/src/Rsk.TokenExchange.IdentityServer4/IdentityServerSubjectTokenValidator.cs b/src/Rsk.TokenExchange.Open.IdentityServer/IdentityServerSubjectTokenValidator.cs similarity index 86% rename from src/Rsk.TokenExchange.IdentityServer4/IdentityServerSubjectTokenValidator.cs rename to src/Rsk.TokenExchange.Open.IdentityServer/IdentityServerSubjectTokenValidator.cs index b3e525a..388f924 100644 --- a/src/Rsk.TokenExchange.IdentityServer4/IdentityServerSubjectTokenValidator.cs +++ b/src/Rsk.TokenExchange.Open.IdentityServer/IdentityServerSubjectTokenValidator.cs @@ -1,12 +1,12 @@ using System.Threading.Tasks; -using IdentityServer4.Validation; +using Open.IdentityServer.Validation; using Rsk.TokenExchange.Validators.Adaptors; using TokenValidationResult = Rsk.TokenExchange.Validators.Adaptors.TokenValidationResult; -namespace Rsk.TokenExchange.IdentityServer4 +namespace Rsk.TokenExchange.Open.IdentityServer { /// - /// Wrapper around IdentityServer4's ITokenValidator + /// Wrapper around Open.IdentityServer's ITokenValidator /// public class IdentityServerSubjectTokenValidator : ITokenValidatorAdaptor { diff --git a/src/Rsk.TokenExchange.IdentityServer4/Rsk.TokenExchange.IdentityServer4.csproj b/src/Rsk.TokenExchange.Open.IdentityServer/Rsk.TokenExchange.Open.IdentityServer.csproj similarity index 79% rename from src/Rsk.TokenExchange.IdentityServer4/Rsk.TokenExchange.IdentityServer4.csproj rename to src/Rsk.TokenExchange.Open.IdentityServer/Rsk.TokenExchange.Open.IdentityServer.csproj index 7b1553a..9ea3ed5 100644 --- a/src/Rsk.TokenExchange.IdentityServer4/Rsk.TokenExchange.IdentityServer4.csproj +++ b/src/Rsk.TokenExchange.Open.IdentityServer/Rsk.TokenExchange.Open.IdentityServer.csproj @@ -1,21 +1,22 @@ - netcoreapp3.1 + net10.0 Rock Solid Knowledge Ltd - Token Exchange (RFC 8693) implementation for IdentityServer4. + Token Exchange (RFC 8693) implementation for Open.IdentityServer. https://github.com/RockSolidKnowledge/TokenExchange https://github.com/RockSolidKnowledge/TokenExchange/releases Copyright 2021 (c) Rock Solid Knowledge Ltd. All rights reserved. - IdentityServer OAuth Token Exchange IdentityServer4 + IdentityServer OAuth Token Exchange IdentityServer4 Open.IdentityServer true icon.png Apache-2.0 true + Rsk.TokenExchange.Open.IdentityServer - + diff --git a/src/Rsk.TokenExchange.IdentityServer4/TokenExchangeExtensionGrantValidator.cs b/src/Rsk.TokenExchange.Open.IdentityServer/TokenExchangeExtensionGrantValidator.cs similarity index 94% rename from src/Rsk.TokenExchange.IdentityServer4/TokenExchangeExtensionGrantValidator.cs rename to src/Rsk.TokenExchange.Open.IdentityServer/TokenExchangeExtensionGrantValidator.cs index 941e678..f9dee57 100644 --- a/src/Rsk.TokenExchange.IdentityServer4/TokenExchangeExtensionGrantValidator.cs +++ b/src/Rsk.TokenExchange.Open.IdentityServer/TokenExchangeExtensionGrantValidator.cs @@ -3,14 +3,14 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using IdentityModel; -using IdentityServer4.Models; -using IdentityServer4.Validation; +using Open.IdentityServer.Models; +using Open.IdentityServer.Validation; using Microsoft.Extensions.Logging; +using Open.IdentityServer; using Rsk.TokenExchange.Exceptions; using Rsk.TokenExchange.Validators; -namespace Rsk.TokenExchange.IdentityServer4 +namespace Rsk.TokenExchange.Open.IdentityServer { /// /// Extension grant validator for OAuth Token Exchange (RFC 8693). @@ -48,6 +48,11 @@ public TokenExchangeExtensionGrantValidator( /// public async Task ValidateAsync(ExtensionGrantValidationContext context) { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + // warn on no client auth if (context.Request.Secret == null) logger.LogInformation("Recieved unauthenticated token exchange request"); diff --git a/src/Rsk.TokenExchange/Rsk.TokenExchange.csproj b/src/Rsk.TokenExchange/Rsk.TokenExchange.csproj index 18e544e..f4af5d7 100644 --- a/src/Rsk.TokenExchange/Rsk.TokenExchange.csproj +++ b/src/Rsk.TokenExchange/Rsk.TokenExchange.csproj @@ -1,9 +1,9 @@ - netcoreapp3.1;net5.0 + net10.0 Rock Solid Knowledge Ltd - Token Exchange (RFC 8693) implementation for IdentityServer. This is the core library shared between the IdentityServer4 and Duende IdentityServer implementations. + Token Exchange (RFC 8693) implementation for IdentityServer. This is the core library shared between the Open.IdentityServer and Duende IdentityServer implementations. https://github.com/RockSolidKnowledge/TokenExchange https://github.com/RockSolidKnowledge/TokenExchange/releases Copyright 2021 (c) Rock Solid Knowledge Ltd. All rights reserved. @@ -14,17 +14,11 @@ true - - - - - - - - - - - + + + + + diff --git a/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/Rsk.TokenExchange.DuendeIdentityServer.Tests.csproj b/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/Rsk.TokenExchange.DuendeIdentityServer.Tests.csproj index 184cc0f..c5ba680 100644 --- a/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/Rsk.TokenExchange.DuendeIdentityServer.Tests.csproj +++ b/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/Rsk.TokenExchange.DuendeIdentityServer.Tests.csproj @@ -1,16 +1,16 @@ - netcoreapp3.1;net5.0 + net10.0 false - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs b/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs index 707456b..8806f55 100644 --- a/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs +++ b/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs @@ -83,8 +83,8 @@ public void GrantType_ExpectRfcGrantType() } [Fact] - public void ValidateAsync_WhenContextIsNull_ExpectArgumentNullException() - => Assert.ThrowsAsync(() => CreateMockedSut().Object.ValidateAsync(null)); + public async Task ValidateAsync_WhenContextIsNull_ExpectArgumentNullException() + => await Assert.ThrowsAsync(() => CreateMockedSut().Object.ValidateAsync(null)); [Fact] public async Task ValidateAsync_WhenParseRequestThrowsTokenExchangeException_ExpectFailureResult() diff --git a/test/Rsk.TokenExchange.IdentityServer4.Tests/IdentityServerBuilderExtensionTests.cs b/test/Rsk.TokenExchange.Open.IdentityServer.Tests/IdentityServerBuilderExtensionTests.cs similarity index 88% rename from test/Rsk.TokenExchange.IdentityServer4.Tests/IdentityServerBuilderExtensionTests.cs rename to test/Rsk.TokenExchange.Open.IdentityServer.Tests/IdentityServerBuilderExtensionTests.cs index 35895fd..fdf6550 100644 --- a/test/Rsk.TokenExchange.IdentityServer4.Tests/IdentityServerBuilderExtensionTests.cs +++ b/test/Rsk.TokenExchange.Open.IdentityServer.Tests/IdentityServerBuilderExtensionTests.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; using FluentAssertions; -using IdentityServer4.Models; -using IdentityServer4.Validation; using Microsoft.Extensions.DependencyInjection; +using Open.IdentityServer.Models; +using Open.IdentityServer.Validation; using Xunit; -namespace Rsk.TokenExchange.IdentityServer4.Tests +namespace Rsk.TokenExchange.Open.IdentityServer.Tests { public class IdentityServerBuilderExtensionTests { diff --git a/test/Rsk.TokenExchange.IdentityServer4.Tests/Rsk.TokenExchange.IdentityServer4.Tests.csproj b/test/Rsk.TokenExchange.Open.IdentityServer.Tests/Rsk.TokenExchange.Open.IdentityServer.Tests.csproj similarity index 50% rename from test/Rsk.TokenExchange.IdentityServer4.Tests/Rsk.TokenExchange.IdentityServer4.Tests.csproj rename to test/Rsk.TokenExchange.Open.IdentityServer.Tests/Rsk.TokenExchange.Open.IdentityServer.Tests.csproj index a2dc157..74e4909 100644 --- a/test/Rsk.TokenExchange.IdentityServer4.Tests/Rsk.TokenExchange.IdentityServer4.Tests.csproj +++ b/test/Rsk.TokenExchange.Open.IdentityServer.Tests/Rsk.TokenExchange.Open.IdentityServer.Tests.csproj @@ -1,23 +1,24 @@ - netcoreapp3.1 + net10.0 false + Rsk.TokenExchange.Open.IdentityServer.Tests - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/test/Rsk.TokenExchange.IdentityServer4.Tests/TokenExchangeExtensionGrantValidatorTests.cs b/test/Rsk.TokenExchange.Open.IdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs similarity index 97% rename from test/Rsk.TokenExchange.IdentityServer4.Tests/TokenExchangeExtensionGrantValidatorTests.cs rename to test/Rsk.TokenExchange.Open.IdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs index 4ed68c0..e0aff8e 100644 --- a/test/Rsk.TokenExchange.IdentityServer4.Tests/TokenExchangeExtensionGrantValidatorTests.cs +++ b/test/Rsk.TokenExchange.Open.IdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs @@ -5,15 +5,15 @@ using System.Security.Claims; using System.Threading.Tasks; using FluentAssertions; -using IdentityServer4.Validation; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; +using Open.IdentityServer.Validation; using Rsk.TokenExchange.Exceptions; using Rsk.TokenExchange.Validators; using Xunit; -namespace Rsk.TokenExchange.IdentityServer4.Tests +namespace Rsk.TokenExchange.Open.IdentityServer.Tests { public class TokenExchangeExtensionGrantValidatorTests { @@ -82,8 +82,8 @@ public void GrantType_ExpectRfcGrantType() } [Fact] - public void ValidateAsync_WhenContextIsNull_ExpectArgumentNullException() - => Assert.ThrowsAsync(() => CreateMockedSut().Object.ValidateAsync(null)); + public async Task ValidateAsync_WhenContextIsNull_ExpectArgumentNullException() + => await Assert.ThrowsAsync(() => CreateMockedSut().Object.ValidateAsync(null)); [Fact] public async Task ValidateAsync_WhenParseRequestThrowsTokenExchangeException_ExpectFailureResult() diff --git a/test/Rsk.TokenExchange.Tests.Integration/Rsk.TokenExchange.Tests.Integration.csproj b/test/Rsk.TokenExchange.Tests.Integration/Rsk.TokenExchange.Tests.Integration.csproj index 633044d..cfae197 100644 --- a/test/Rsk.TokenExchange.Tests.Integration/Rsk.TokenExchange.Tests.Integration.csproj +++ b/test/Rsk.TokenExchange.Tests.Integration/Rsk.TokenExchange.Tests.Integration.csproj @@ -1,21 +1,24 @@ - netcoreapp3.1 + net10.0 false - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + diff --git a/test/Rsk.TokenExchange.Tests.Integration/TestServer.cs b/test/Rsk.TokenExchange.Tests.Integration/TestServer.cs index 06f1824..85579bf 100644 --- a/test/Rsk.TokenExchange.Tests.Integration/TestServer.cs +++ b/test/Rsk.TokenExchange.Tests.Integration/TestServer.cs @@ -2,9 +2,8 @@ using System.IO; using System.Security.Cryptography; using System.Threading.Tasks; -using IdentityServer4; -using IdentityServer4.Models; -using IdentityServer4.Test; +using Open.IdentityServer.Models; +using Open.IdentityServer.Test; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; @@ -13,7 +12,8 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; -using Rsk.TokenExchange.IdentityServer4; +using Open.IdentityServer; +using Rsk.TokenExchange.Open.IdentityServer; namespace Rsk.TokenExchange.Tests.Integration { diff --git a/test/Rsk.TokenExchange.Tests/Rsk.TokenExchange.Tests.csproj b/test/Rsk.TokenExchange.Tests/Rsk.TokenExchange.Tests.csproj index 9d7590b..7c34fd5 100644 --- a/test/Rsk.TokenExchange.Tests/Rsk.TokenExchange.Tests.csproj +++ b/test/Rsk.TokenExchange.Tests/Rsk.TokenExchange.Tests.csproj @@ -1,31 +1,25 @@ - netcoreapp3.1;net5.0 + net10.0 false - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - - - - + + + + From 29190450193fd1d73a054a5f0bed3f444df89144 Mon Sep 17 00:00:00 2001 From: Joe Harvey Date: Fri, 16 Oct 2026 13:04:37 +0100 Subject: [PATCH 2/7] replaced Newtonsoft.Json with System.Text.Json --- src/Rsk.TokenExchange/Models/Actor.cs | 8 ++--- .../Parsers/TokenExchangeClaimsParser.cs | 5 ++-- .../Rsk.TokenExchange.csproj | 1 - .../TestServerTests.cs | 29 +++++++++++-------- .../Parsers/TokenExchangeClaimsParserTests.cs | 6 ++-- .../Rsk.TokenExchange.Tests.csproj | 1 - 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/Rsk.TokenExchange/Models/Actor.cs b/src/Rsk.TokenExchange/Models/Actor.cs index 5e8c352..a8ee10e 100644 --- a/src/Rsk.TokenExchange/Models/Actor.cs +++ b/src/Rsk.TokenExchange/Models/Actor.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Rsk.TokenExchange { @@ -12,21 +12,21 @@ public class Actor /// The original issuer (iss) of exchanged token. /// Useful for cross-tenant/identity provider token exchange. /// - [JsonProperty("iss")] + [JsonPropertyName("iss")] public string Issuer { get; set; } /// /// The original client ID (client_id) of the exchanged token. /// Useful for tracking who originally requested the exchanged token. /// - [JsonProperty("client_id")] + [JsonPropertyName("client_id")] public string ClientId { get; set; } /// /// The inner actor claim. /// Useful for multiple token exchange requests. /// - [JsonProperty("act")] + [JsonPropertyName("act")] public string InnerActor { get; set; } } diff --git a/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs b/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs index 7a87d7b..71a589e 100644 --- a/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs +++ b/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs @@ -2,9 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.IdentityModel.JsonWebTokens; -using Newtonsoft.Json; using Rsk.TokenExchange.Exceptions; namespace Rsk.TokenExchange @@ -43,7 +44,7 @@ public virtual Task> ParseClaims(IEnumerable subjectCl var actor = new Actor {ClientId = request.ClientId, InnerActor = previousActor?.Value}; claims.Add(new Claim( "act", - JsonConvert.SerializeObject(actor, new JsonSerializerSettings {NullValueHandling = NullValueHandling.Ignore}), + JsonSerializer.Serialize(actor, new JsonSerializerOptions {DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull}), JsonClaimValueTypes.Json)); } diff --git a/src/Rsk.TokenExchange/Rsk.TokenExchange.csproj b/src/Rsk.TokenExchange/Rsk.TokenExchange.csproj index f4af5d7..c9f14d6 100644 --- a/src/Rsk.TokenExchange/Rsk.TokenExchange.csproj +++ b/src/Rsk.TokenExchange/Rsk.TokenExchange.csproj @@ -18,7 +18,6 @@ - diff --git a/test/Rsk.TokenExchange.Tests.Integration/TestServerTests.cs b/test/Rsk.TokenExchange.Tests.Integration/TestServerTests.cs index 50f1afd..6ff7ad5 100644 --- a/test/Rsk.TokenExchange.Tests.Integration/TestServerTests.cs +++ b/test/Rsk.TokenExchange.Tests.Integration/TestServerTests.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using Microsoft.IdentityModel.JsonWebTokens; -using Newtonsoft.Json.Linq; using Xunit; namespace Rsk.TokenExchange.Tests.Integration @@ -32,7 +32,7 @@ public async Task TokenExchange_WhenAuthorizedApiSwapsToken_ExpectSuccessfulToke subjectTokenResponse.IsSuccessStatusCode.Should().BeTrue(); var subjectTokenResponseString = await subjectTokenResponse.Content.ReadAsStringAsync(); - var subjectToken = JObject.Parse(subjectTokenResponseString)["access_token"]; + var subjectToken = GetStringProperty(subjectTokenResponseString, "access_token"); var tokenExchangeResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new[] { @@ -42,7 +42,7 @@ public async Task TokenExchange_WhenAuthorizedApiSwapsToken_ExpectSuccessfulToke new KeyValuePair( TokenExchangeConstants.RequestParameters.SubjectToken, - subjectToken.Value()), + subjectToken), new KeyValuePair( TokenExchangeConstants.RequestParameters.SubjectTokenType, TokenExchangeConstants.TokenTypes.AccessToken), @@ -50,7 +50,7 @@ public async Task TokenExchange_WhenAuthorizedApiSwapsToken_ExpectSuccessfulToke tokenExchangeResponse.IsSuccessStatusCode.Should().BeTrue(); var tokenExchangeResponseString = await tokenExchangeResponse.Content.ReadAsStringAsync(); - JObject.Parse(tokenExchangeResponseString)["access_token"].Should().NotBeNull(); + GetStringProperty(tokenExchangeResponseString, "access_token").Should().NotBeNullOrEmpty(); } [Fact] @@ -67,7 +67,7 @@ public async Task TokenExchange_WhenUnauthorizedApiSwapsToken_ExpectFailure() subjectTokenResponse.IsSuccessStatusCode.Should().BeTrue(); var subjectTokenResponseString = await subjectTokenResponse.Content.ReadAsStringAsync(); - var subjectToken = JObject.Parse(subjectTokenResponseString)["access_token"]; + var subjectToken = GetStringProperty(subjectTokenResponseString, "access_token"); var tokenExchangeResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new[] { @@ -77,7 +77,7 @@ public async Task TokenExchange_WhenUnauthorizedApiSwapsToken_ExpectFailure() new KeyValuePair( TokenExchangeConstants.RequestParameters.SubjectToken, - subjectToken.Value()), + subjectToken), new KeyValuePair( TokenExchangeConstants.RequestParameters.SubjectTokenType, TokenExchangeConstants.TokenTypes.AccessToken), @@ -99,9 +99,8 @@ public async Task TokenExchange_ExpectCorrectClientIdAndActorClaim() // actor claim containing client app acting on their behalf token.Claims.Should().Contain(x => x.Type == "act"); var actorClaim = token.Claims.First(x => x.Type == "act"); - var actorClientId = JObject.Parse(actorClaim.Value)["client_id"]; - actorClientId.Should().NotBeNull(); - actorClientId.Value().Should().Be(TestStartup.Api1.Name); + var actorClientId = GetStringProperty(actorClaim.Value, "client_id"); + actorClientId.Should().Be(TestStartup.Api1.Name); } private async Task TokenExchange() @@ -117,7 +116,7 @@ private async Task TokenExchange() subjectTokenResponse.IsSuccessStatusCode.Should().BeTrue(); var subjectTokenResponseString = await subjectTokenResponse.Content.ReadAsStringAsync(); - var subjectToken = JObject.Parse(subjectTokenResponseString)["access_token"]; + var subjectToken = GetStringProperty(subjectTokenResponseString, "access_token"); var tokenExchangeResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new[] { @@ -127,7 +126,7 @@ private async Task TokenExchange() new KeyValuePair( TokenExchangeConstants.RequestParameters.SubjectToken, - subjectToken.Value()), + subjectToken), new KeyValuePair( TokenExchangeConstants.RequestParameters.SubjectTokenType, TokenExchangeConstants.TokenTypes.AccessToken), @@ -136,7 +135,13 @@ private async Task TokenExchange() tokenExchangeResponse.IsSuccessStatusCode.Should().BeTrue(); var tokenExchangeResponseString = await tokenExchangeResponse.Content.ReadAsStringAsync(); - return JObject.Parse(tokenExchangeResponseString)["access_token"].Value(); + return GetStringProperty(tokenExchangeResponseString, "access_token"); + } + + private static string GetStringProperty(string json, string propertyName) + { + using var document = JsonDocument.Parse(json); + return document.RootElement.GetProperty(propertyName).GetString(); } } } \ No newline at end of file diff --git a/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeClaimsParserTests.cs b/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeClaimsParserTests.cs index b6bb283..4bc1995 100644 --- a/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeClaimsParserTests.cs +++ b/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeClaimsParserTests.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using Microsoft.IdentityModel.JsonWebTokens; using Moq; -using Newtonsoft.Json; using Rsk.TokenExchange.Exceptions; using Xunit; @@ -115,7 +115,7 @@ public async Task ParseClaims_WhenClientIdIsDifferent_ExpectSubjectClaimsWithAct actor.Should().NotBeNull(); actor?.ValueType.Should().Be(JsonClaimValueTypes.Json); - var parsedActor = JsonConvert.DeserializeObject(actor?.Value); + var parsedActor = JsonSerializer.Deserialize(actor?.Value); parsedActor.ClientId.Should().Be(actorClientId); parsedActor.InnerActor.Should().BeNull(); } @@ -142,7 +142,7 @@ public async Task ParseClaims_WhenClientIdIsDifferentAndActorClaimAlreadyPresent actor.Should().NotBeNull(); actor?.ValueType.Should().Be(JsonClaimValueTypes.Json); - var parsedActor = JsonConvert.DeserializeObject(actor?.Value); + var parsedActor = JsonSerializer.Deserialize(actor?.Value); parsedActor.ClientId.Should().Be(actorClientId); parsedActor.InnerActor.Should().Be(innerActor); } diff --git a/test/Rsk.TokenExchange.Tests/Rsk.TokenExchange.Tests.csproj b/test/Rsk.TokenExchange.Tests/Rsk.TokenExchange.Tests.csproj index 7c34fd5..93fdc21 100644 --- a/test/Rsk.TokenExchange.Tests/Rsk.TokenExchange.Tests.csproj +++ b/test/Rsk.TokenExchange.Tests/Rsk.TokenExchange.Tests.csproj @@ -19,7 +19,6 @@ - From a92dceb9f4c53029f4e81d56b6056c9d2e540158 Mon Sep 17 00:00:00 2001 From: Joe Harvey Date: Fri, 16 Oct 2026 13:18:47 +0100 Subject: [PATCH 3/7] cleanup --- .../TokenExchangeExtensionGrantValidator.cs | 17 ++++---- .../TokenExchangeExtensionGrantValidator.cs | 12 +++--- .../Exceptions/ClaimsParsingException.cs | 3 -- .../Exceptions/InvalidRequestException.cs | 3 -- .../Exceptions/TokenExchangeException.cs | 3 -- .../Models/TokenExchangeRequest.cs | 10 +++-- .../Parsers/TokenExchangeClaimsParser.cs | 11 +++-- .../DefaultSubjectTokenValidator.cs | 11 ++++- .../DefaultTokenExchangeRequestValidator.cs | 5 ++- ...kenExchangeExtensionGrantValidatorTests.cs | 10 ++--- ...kenExchangeExtensionGrantValidatorTests.cs | 10 ++--- .../TestServer.cs | 32 +++++++-------- .../TestServerTests.cs | 40 ++++++++----------- .../Models/TokenExchangeRequestTests.cs | 2 +- .../Parsers/TokenExchangeClaimsParserTests.cs | 15 ++++--- .../TokenExchangeRequestParserTests.cs | 4 +- .../DefaultSubjectTokenValidatorTests.cs | 2 +- ...faultTokenExchangeRequestValidatorTests.cs | 6 +-- 18 files changed, 103 insertions(+), 93 deletions(-) diff --git a/src/Rsk.TokenExchange.DuendeIdentityServer/TokenExchangeExtensionGrantValidator.cs b/src/Rsk.TokenExchange.DuendeIdentityServer/TokenExchangeExtensionGrantValidator.cs index dc24e28..dfa27c1 100644 --- a/src/Rsk.TokenExchange.DuendeIdentityServer/TokenExchangeExtensionGrantValidator.cs +++ b/src/Rsk.TokenExchange.DuendeIdentityServer/TokenExchangeExtensionGrantValidator.cs @@ -4,6 +4,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using Duende.IdentityModel; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Microsoft.Extensions.Logging; @@ -48,10 +49,13 @@ public TokenExchangeExtensionGrantValidator( /// public async Task ValidateAsync(ExtensionGrantValidationContext context, CancellationToken cancellationToken = default) { - if (context == null) throw new ArgumentNullException(nameof(context)); + ArgumentNullException.ThrowIfNull(context); // warn on no client auth - if (context.Request.Secret == null) logger.LogInformation("Recieved unauthenticated token exchange request"); + if (context.Request.Secret == null) + { + logger.LogInformation("Received unauthenticated token exchange request"); + } // parse request ITokenExchangeRequest tokenExchangeRequest; @@ -107,15 +111,14 @@ public async Task ValidateAsync(ExtensionGrantValidationContext context, Cancell /// public virtual void UpdateRequest(ValidatedTokenRequest request, IEnumerable claims) { - if (request == null) throw new ArgumentNullException(nameof(request)); - if (claims == null) throw new ArgumentNullException(nameof(claims)); + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(claims); // update client_id to act as original client app - var clientId = claims.First(x => x.Type == "client_id").Value; + var clientId = claims.First(x => x.Type == JwtClaimTypes.ClientId).Value; request.ClientId = clientId; } - private static readonly Dictionary CustomResponseParameters = new Dictionary - {{TokenExchangeConstants.ResponseParameters.IssuedTokenType, TokenExchangeConstants.TokenTypes.AccessToken}}; + private static readonly Dictionary CustomResponseParameters = new() {{TokenExchangeConstants.ResponseParameters.IssuedTokenType, TokenExchangeConstants.TokenTypes.AccessToken}}; } } \ No newline at end of file diff --git a/src/Rsk.TokenExchange.Open.IdentityServer/TokenExchangeExtensionGrantValidator.cs b/src/Rsk.TokenExchange.Open.IdentityServer/TokenExchangeExtensionGrantValidator.cs index f9dee57..c42d8a5 100644 --- a/src/Rsk.TokenExchange.Open.IdentityServer/TokenExchangeExtensionGrantValidator.cs +++ b/src/Rsk.TokenExchange.Open.IdentityServer/TokenExchangeExtensionGrantValidator.cs @@ -54,7 +54,10 @@ public async Task ValidateAsync(ExtensionGrantValidationContext context) } // warn on no client auth - if (context.Request.Secret == null) logger.LogInformation("Recieved unauthenticated token exchange request"); + if (context.Request.Secret == null) + { + logger.LogInformation("Received unauthenticated token exchange request"); + } // parse request ITokenExchangeRequest tokenExchangeRequest; @@ -110,15 +113,14 @@ public async Task ValidateAsync(ExtensionGrantValidationContext context) /// public virtual void UpdateRequest(ValidatedTokenRequest request, IEnumerable claims) { - if (request == null) throw new ArgumentNullException(nameof(request)); - if (claims == null) throw new ArgumentNullException(nameof(claims)); + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(claims); // update client_id to act as original client app var clientId = claims.First(x => x.Type == JwtClaimTypes.ClientId).Value; request.ClientId = clientId; } - private static readonly Dictionary CustomResponseParameters = new Dictionary - {{TokenExchangeConstants.ResponseParameters.IssuedTokenType, TokenExchangeConstants.TokenTypes.AccessToken}}; + private static readonly Dictionary CustomResponseParameters = new() {{TokenExchangeConstants.ResponseParameters.IssuedTokenType, TokenExchangeConstants.TokenTypes.AccessToken}}; } } \ No newline at end of file diff --git a/src/Rsk.TokenExchange/Exceptions/ClaimsParsingException.cs b/src/Rsk.TokenExchange/Exceptions/ClaimsParsingException.cs index 874322c..923c8d9 100644 --- a/src/Rsk.TokenExchange/Exceptions/ClaimsParsingException.cs +++ b/src/Rsk.TokenExchange/Exceptions/ClaimsParsingException.cs @@ -16,8 +16,5 @@ public SubjectParsingException(string message) : base(message) { } /// public SubjectParsingException(string message, Exception inner) : base(message, inner) { } - - /// - protected SubjectParsingException(SerializationInfo info, StreamingContext context) : base(info, context) { } } } \ No newline at end of file diff --git a/src/Rsk.TokenExchange/Exceptions/InvalidRequestException.cs b/src/Rsk.TokenExchange/Exceptions/InvalidRequestException.cs index 3469db2..9d7d2c5 100644 --- a/src/Rsk.TokenExchange/Exceptions/InvalidRequestException.cs +++ b/src/Rsk.TokenExchange/Exceptions/InvalidRequestException.cs @@ -17,8 +17,5 @@ public InvalidRequestException(string message) : base(message) { } /// public InvalidRequestException(string message, Exception inner) : base(message, inner) { } - - /// - protected InvalidRequestException(SerializationInfo info, StreamingContext context) : base(info, context) { } } } \ No newline at end of file diff --git a/src/Rsk.TokenExchange/Exceptions/TokenExchangeException.cs b/src/Rsk.TokenExchange/Exceptions/TokenExchangeException.cs index c77f5d9..f2efdd1 100644 --- a/src/Rsk.TokenExchange/Exceptions/TokenExchangeException.cs +++ b/src/Rsk.TokenExchange/Exceptions/TokenExchangeException.cs @@ -17,8 +17,5 @@ public TokenExchangeException(string message) : base(message) { } /// public TokenExchangeException(string message, Exception inner) : base(message, inner) { } - - /// - protected TokenExchangeException(SerializationInfo info, StreamingContext context) : base(info, context) { } } } \ No newline at end of file diff --git a/src/Rsk.TokenExchange/Models/TokenExchangeRequest.cs b/src/Rsk.TokenExchange/Models/TokenExchangeRequest.cs index 78bb3a5..99f7760 100644 --- a/src/Rsk.TokenExchange/Models/TokenExchangeRequest.cs +++ b/src/Rsk.TokenExchange/Models/TokenExchangeRequest.cs @@ -17,9 +17,13 @@ public class TokenExchangeRequest : ITokenExchangeRequest /// Throw on missing grant_type, subject_token, and subject_token_type. public TokenExchangeRequest(string clientId, NameValueCollection request) { - if (string.IsNullOrWhiteSpace(clientId)) throw new ArgumentNullException(nameof(clientId)); - if (request == null) throw new ArgumentNullException(nameof(request)); - + if (string.IsNullOrWhiteSpace(clientId)) + { + throw new ArgumentNullException(nameof(clientId)); + } + + ArgumentNullException.ThrowIfNull(request); + ClientId = clientId; // grant type must be urn:ietf:params:oauth:grant-type:token-exchange diff --git a/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs b/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs index 71a589e..8aa94fa 100644 --- a/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs +++ b/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs @@ -18,10 +18,13 @@ public class TokenExchangeClaimsParser : ITokenExchangeClaimsParser /// Throws when more than one subject claim is present in the subject token claims. public virtual Task ParseSubject(IEnumerable subjectClaims, ITokenExchangeRequest request) { - if (subjectClaims == null) throw new ArgumentNullException(nameof(subjectClaims)); + ArgumentNullException.ThrowIfNull(subjectClaims); var subjectValues = subjectClaims.Where(x => x.Type == "sub").Select(x => x.Value).ToList(); - if (1 < subjectValues.Count) throw new SubjectParsingException("More than one subject claim present on token exchange request"); + if (1 < subjectValues.Count) + { + throw new SubjectParsingException("More than one subject claim present on token exchange request"); + } return Task.FromResult(subjectValues.FirstOrDefault()); } @@ -30,8 +33,8 @@ public virtual Task ParseSubject(IEnumerable subjectClaims, IToke /// Returns the subject claims along with an actor claim when client_id != subject token client_id public virtual Task> ParseClaims(IEnumerable subjectClaims, ITokenExchangeRequest request) { - if (subjectClaims == null) throw new ArgumentNullException(nameof(subjectClaims)); - if (request == null) throw new ArgumentNullException(nameof(request)); + ArgumentNullException.ThrowIfNull(subjectClaims); + ArgumentNullException.ThrowIfNull(request); var claims = subjectClaims.ToList(); diff --git a/src/Rsk.TokenExchange/Validators/DefaultSubjectTokenValidator.cs b/src/Rsk.TokenExchange/Validators/DefaultSubjectTokenValidator.cs index 7a39bc7..aa632b4 100644 --- a/src/Rsk.TokenExchange/Validators/DefaultSubjectTokenValidator.cs +++ b/src/Rsk.TokenExchange/Validators/DefaultSubjectTokenValidator.cs @@ -26,8 +26,15 @@ public DefaultSubjectTokenValidator(ITokenValidatorAdaptor tokenValidator, ILogg /// public async Task Validate(string token, string tokenType) { - if (string.IsNullOrWhiteSpace(token)) throw new ArgumentNullException(nameof(token)); - if (string.IsNullOrWhiteSpace(tokenType)) throw new ArgumentNullException(nameof(tokenType)); + if (string.IsNullOrWhiteSpace(token)) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrWhiteSpace(tokenType)) + { + throw new ArgumentNullException(nameof(tokenType)); + } if (tokenType != TokenExchangeConstants.TokenTypes.AccessToken) { diff --git a/src/Rsk.TokenExchange/Validators/DefaultTokenExchangeRequestValidator.cs b/src/Rsk.TokenExchange/Validators/DefaultTokenExchangeRequestValidator.cs index 634735f..16c4d02 100644 --- a/src/Rsk.TokenExchange/Validators/DefaultTokenExchangeRequestValidator.cs +++ b/src/Rsk.TokenExchange/Validators/DefaultTokenExchangeRequestValidator.cs @@ -23,7 +23,10 @@ public DefaultTokenExchangeRequestValidator(ISubjectTokenValidator subjectTokenV /// public async Task Validate(ITokenExchangeRequest request) { - if (request == null) throw new ArgumentNullException(nameof(request)); + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } // validate subject token is issued by IdentityServer var result = await subjectTokenValidator.Validate(request.SubjectToken, request.SubjectTokenType); diff --git a/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs b/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs index 8806f55..2e9ceee 100644 --- a/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs +++ b/test/Rsk.TokenExchange.DuendeIdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs @@ -18,12 +18,12 @@ namespace Rsk.TokenExchange.Tests.IdentityServer { public class TokenExchangeExtensionGrantValidatorTests { - private Mock mockParser = new Mock(); - private Mock mockRequestValidator = new Mock(); - private Mock mockClaimsParser = new Mock(); + private Mock mockParser = new(); + private Mock mockRequestValidator = new(); + private Mock mockClaimsParser = new(); private ILogger logger = new NullLogger(); - private ExtensionGrantValidationContext context = new ExtensionGrantValidationContext + private ExtensionGrantValidationContext context = new() { Request = new ValidatedTokenRequest { @@ -33,7 +33,7 @@ public class TokenExchangeExtensionGrantValidatorTests }; private TokenExchangeExtensionGrantValidator CreateSut() - => new TokenExchangeExtensionGrantValidator(mockParser?.Object, mockRequestValidator?.Object, mockClaimsParser?.Object, logger); + => new(mockParser?.Object, mockRequestValidator?.Object, mockClaimsParser?.Object, logger); private Mock CreateMockedSut() { diff --git a/test/Rsk.TokenExchange.Open.IdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs b/test/Rsk.TokenExchange.Open.IdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs index e0aff8e..148ad8a 100644 --- a/test/Rsk.TokenExchange.Open.IdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs +++ b/test/Rsk.TokenExchange.Open.IdentityServer.Tests/TokenExchangeExtensionGrantValidatorTests.cs @@ -17,12 +17,12 @@ namespace Rsk.TokenExchange.Open.IdentityServer.Tests { public class TokenExchangeExtensionGrantValidatorTests { - private Mock mockParser = new Mock(); - private Mock mockRequestValidator = new Mock(); - private Mock mockClaimsParser = new Mock(); + private Mock mockParser = new(); + private Mock mockRequestValidator = new(); + private Mock mockClaimsParser = new(); private ILogger logger = new NullLogger(); - private ExtensionGrantValidationContext context = new ExtensionGrantValidationContext + private ExtensionGrantValidationContext context = new() { Request = new ValidatedTokenRequest { @@ -32,7 +32,7 @@ public class TokenExchangeExtensionGrantValidatorTests }; private TokenExchangeExtensionGrantValidator CreateSut() - => new TokenExchangeExtensionGrantValidator(mockParser?.Object, mockRequestValidator?.Object, mockClaimsParser?.Object, logger); + => new(mockParser?.Object, mockRequestValidator?.Object, mockClaimsParser?.Object, logger); private Mock CreateMockedSut() { diff --git a/test/Rsk.TokenExchange.Tests.Integration/TestServer.cs b/test/Rsk.TokenExchange.Tests.Integration/TestServer.cs index 85579bf..9b23c66 100644 --- a/test/Rsk.TokenExchange.Tests.Integration/TestServer.cs +++ b/test/Rsk.TokenExchange.Tests.Integration/TestServer.cs @@ -38,43 +38,43 @@ protected override IHost CreateHost(IHostBuilder builder) public class TestStartup { - public static readonly Client Client = new Client + public static readonly Client Client = new() { ClientId = "client", RequireClientSecret = false, AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = new[] {"api1", "api2"}, + AllowedScopes = ["api1", "api2"], // pretending this is a token from a user interactive flow - Claims = new[] {new ClientClaim("sub", "1")}, + Claims = [new ClientClaim("sub", "1")], ClientClaimsPrefix = null }; - public static readonly Client ApiClient = new Client + public static readonly Client ApiClient = new() { ClientId = "api1", RequireClientSecret = false, - AllowedGrantTypes = new[] {TokenExchangeConstants.GrantType}, - AllowedScopes = new[] {"api2"} + AllowedGrantTypes = [TokenExchangeConstants.GrantType], + AllowedScopes = ["api2"] }; - public static readonly ApiResource Api1 = new ApiResource("api1") {Scopes = new[] {"api1"}}; - public static readonly ApiResource Api2 = new ApiResource("api2") {Scopes = new[] {"api2"}}; + public static readonly ApiResource Api1 = new("api1") {Scopes = ["api1"] }; + public static readonly ApiResource Api2 = new("api2") {Scopes = ["api2"] }; - public static readonly TestUser Alice = new TestUser {SubjectId = "1", Username = "alice"}; - public static readonly TestUser Bob = new TestUser {SubjectId = "2", Username = "bob"}; + public static readonly TestUser Alice = new() {SubjectId = "1", Username = "alice"}; + public static readonly TestUser Bob = new() {SubjectId = "2", Username = "bob"}; - private static readonly ApiScope ApiScope1 = new ApiScope("api1"); - private static readonly ApiScope ApiScope2 = new ApiScope("api2"); + private static readonly ApiScope ApiScope1 = new("api1"); + private static readonly ApiScope ApiScope2 = new("api2"); public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() - .AddInMemoryClients(new[] {Client, ApiClient}) - .AddInMemoryApiResources(new[] {Api1, Api2}) - .AddInMemoryApiScopes(new[] {ApiScope1, ApiScope2}) + .AddInMemoryClients([Client, ApiClient]) + .AddInMemoryApiResources([Api1, Api2]) + .AddInMemoryApiScopes([ApiScope1, ApiScope2]) .AddInMemoryIdentityResources(new List {new IdentityResources.OpenId(), new IdentityResources.Profile()}) - .AddTestUsers(new List {Alice, Bob}) + .AddTestUsers([Alice, Bob]) .AddSigningCredential( new ECDsaSecurityKey(ECDsa.Create(ECCurve.NamedCurves.nistP256)), IdentityServerConstants.ECDsaSigningAlgorithm.ES256) diff --git a/test/Rsk.TokenExchange.Tests.Integration/TestServerTests.cs b/test/Rsk.TokenExchange.Tests.Integration/TestServerTests.cs index 6ff7ad5..8660483 100644 --- a/test/Rsk.TokenExchange.Tests.Integration/TestServerTests.cs +++ b/test/Rsk.TokenExchange.Tests.Integration/TestServerTests.cs @@ -23,19 +23,17 @@ public async Task TokenExchange_WhenAuthorizedApiSwapsToken_ExpectSuccessfulToke { var client = factory.CreateClient(); - var subjectTokenResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new [] - { + var subjectTokenResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent([ new KeyValuePair("grant_type", "client_credentials"), new KeyValuePair("client_id", TestStartup.Client.ClientId), - new KeyValuePair("scope", TestStartup.Api1.Name) - })); + new KeyValuePair("scope", TestStartup.Api1.Name) + ])); subjectTokenResponse.IsSuccessStatusCode.Should().BeTrue(); var subjectTokenResponseString = await subjectTokenResponse.Content.ReadAsStringAsync(); var subjectToken = GetStringProperty(subjectTokenResponseString, "access_token"); - var tokenExchangeResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new[] - { + var tokenExchangeResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent([ new KeyValuePair("grant_type", TokenExchangeConstants.GrantType), new KeyValuePair("client_id", TestStartup.Api1.Name), new KeyValuePair("scope", TestStartup.Api2.Name), @@ -45,8 +43,8 @@ public async Task TokenExchange_WhenAuthorizedApiSwapsToken_ExpectSuccessfulToke subjectToken), new KeyValuePair( TokenExchangeConstants.RequestParameters.SubjectTokenType, - TokenExchangeConstants.TokenTypes.AccessToken), - })); + TokenExchangeConstants.TokenTypes.AccessToken) + ])); tokenExchangeResponse.IsSuccessStatusCode.Should().BeTrue(); var tokenExchangeResponseString = await tokenExchangeResponse.Content.ReadAsStringAsync(); @@ -58,19 +56,17 @@ public async Task TokenExchange_WhenUnauthorizedApiSwapsToken_ExpectFailure() { var client = factory.CreateClient(); - var subjectTokenResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new [] - { + var subjectTokenResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent([ new KeyValuePair("grant_type", "client_credentials"), new KeyValuePair("client_id", TestStartup.Client.ClientId), - new KeyValuePair("scope", TestStartup.Api2.Name) - })); + new KeyValuePair("scope", TestStartup.Api2.Name) + ])); subjectTokenResponse.IsSuccessStatusCode.Should().BeTrue(); var subjectTokenResponseString = await subjectTokenResponse.Content.ReadAsStringAsync(); var subjectToken = GetStringProperty(subjectTokenResponseString, "access_token"); - var tokenExchangeResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new[] - { + var tokenExchangeResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent([ new KeyValuePair("grant_type", TokenExchangeConstants.GrantType), new KeyValuePair("client_id", TestStartup.Api1.Name), new KeyValuePair("scope", TestStartup.Api2.Name), @@ -80,8 +76,8 @@ public async Task TokenExchange_WhenUnauthorizedApiSwapsToken_ExpectFailure() subjectToken), new KeyValuePair( TokenExchangeConstants.RequestParameters.SubjectTokenType, - TokenExchangeConstants.TokenTypes.AccessToken), - })); + TokenExchangeConstants.TokenTypes.AccessToken) + ])); tokenExchangeResponse.IsSuccessStatusCode.Should().BeFalse(); } @@ -107,19 +103,17 @@ private async Task TokenExchange() { var client = factory.CreateClient(); - var subjectTokenResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new[] - { + var subjectTokenResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent([ new KeyValuePair("grant_type", "client_credentials"), new KeyValuePair("client_id", TestStartup.Client.ClientId), new KeyValuePair("scope", TestStartup.Api1.Name) - })); + ])); subjectTokenResponse.IsSuccessStatusCode.Should().BeTrue(); var subjectTokenResponseString = await subjectTokenResponse.Content.ReadAsStringAsync(); var subjectToken = GetStringProperty(subjectTokenResponseString, "access_token"); - var tokenExchangeResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new[] - { + var tokenExchangeResponse = await client.PostAsync("/connect/token", new FormUrlEncodedContent([ new KeyValuePair("grant_type", TokenExchangeConstants.GrantType), new KeyValuePair("client_id", TestStartup.Api1.Name), new KeyValuePair("scope", TestStartup.Api2.Name), @@ -129,8 +123,8 @@ private async Task TokenExchange() subjectToken), new KeyValuePair( TokenExchangeConstants.RequestParameters.SubjectTokenType, - TokenExchangeConstants.TokenTypes.AccessToken), - })); + TokenExchangeConstants.TokenTypes.AccessToken) + ])); tokenExchangeResponse.IsSuccessStatusCode.Should().BeTrue(); var tokenExchangeResponseString = await tokenExchangeResponse.Content.ReadAsStringAsync(); diff --git a/test/Rsk.TokenExchange.Tests/Models/TokenExchangeRequestTests.cs b/test/Rsk.TokenExchange.Tests/Models/TokenExchangeRequestTests.cs index 951d914..c0cbfda 100644 --- a/test/Rsk.TokenExchange.Tests/Models/TokenExchangeRequestTests.cs +++ b/test/Rsk.TokenExchange.Tests/Models/TokenExchangeRequestTests.cs @@ -10,7 +10,7 @@ namespace Rsk.TokenExchange.Tests.Models { public class TokenExchangeRequestTests { - private readonly NameValueCollection rawRequest = new NameValueCollection + private readonly NameValueCollection rawRequest = new() { {TokenExchangeConstants.RequestParameters.GrantType, TokenExchangeConstants.GrantType}, {TokenExchangeConstants.RequestParameters.SubjectToken, "eyJ.eyJ"}, diff --git a/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeClaimsParserTests.cs b/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeClaimsParserTests.cs index 4bc1995..fffa503 100644 --- a/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeClaimsParserTests.cs +++ b/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeClaimsParserTests.cs @@ -14,9 +14,9 @@ namespace Rsk.TokenExchange.Tests.Parsers { public class TokenExchangeClaimsParserTests { - private readonly Mock mockRequest = new Mock(); + private readonly Mock mockRequest = new(); - private TokenExchangeClaimsParser CreateSut() => new TokenExchangeClaimsParser(); + private TokenExchangeClaimsParser CreateSut() => new(); [Fact] public async Task ParseSubject_WhenSubjectClaimsAreNull_ExpectArgumentNullException() @@ -141,10 +141,13 @@ public async Task ParseClaims_WhenClientIdIsDifferentAndActorClaimAlreadyPresent var actor = parsedClaims.FirstOrDefault(x => x.Type == "act"); actor.Should().NotBeNull(); actor?.ValueType.Should().Be(JsonClaimValueTypes.Json); - - var parsedActor = JsonSerializer.Deserialize(actor?.Value); - parsedActor.ClientId.Should().Be(actorClientId); - parsedActor.InnerActor.Should().Be(innerActor); + + if (actor?.Value != null) + { + var parsedActor = JsonSerializer.Deserialize(actor.Value); + parsedActor.ClientId.Should().Be(actorClientId); + parsedActor.InnerActor.Should().Be(innerActor); + } } } } \ No newline at end of file diff --git a/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeRequestParserTests.cs b/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeRequestParserTests.cs index 55fbc31..b95b1ed 100644 --- a/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeRequestParserTests.cs +++ b/test/Rsk.TokenExchange.Tests/Parsers/TokenExchangeRequestParserTests.cs @@ -8,9 +8,9 @@ namespace Rsk.TokenExchange.Tests.Parsers { public class TokenExchangeRequestParserTests { - private readonly TokenExchangeRequestParser sut = new TokenExchangeRequestParser(); + private readonly TokenExchangeRequestParser sut = new(); - private readonly NameValueCollection parameters = new NameValueCollection + private readonly NameValueCollection parameters = new() { {TokenExchangeConstants.RequestParameters.GrantType, TokenExchangeConstants.GrantType}, {TokenExchangeConstants.RequestParameters.SubjectToken, "eyJ.eyJ"}, diff --git a/test/Rsk.TokenExchange.Tests/Validators/DefaultSubjectTokenValidatorTests.cs b/test/Rsk.TokenExchange.Tests/Validators/DefaultSubjectTokenValidatorTests.cs index d2a7182..9a794e9 100644 --- a/test/Rsk.TokenExchange.Tests/Validators/DefaultSubjectTokenValidatorTests.cs +++ b/test/Rsk.TokenExchange.Tests/Validators/DefaultSubjectTokenValidatorTests.cs @@ -14,7 +14,7 @@ namespace Rsk.TokenExchange.Tests.Validators { public class DefaultSubjectTokenValidatorTests { - private Mock mockTokenValidator = new Mock(); + private Mock mockTokenValidator = new(); private ILogger logger = new NullLogger(); private const string validToken = "eyJ.eyJ"; diff --git a/test/Rsk.TokenExchange.Tests/Validators/DefaultTokenExchangeRequestValidatorTests.cs b/test/Rsk.TokenExchange.Tests/Validators/DefaultTokenExchangeRequestValidatorTests.cs index a1c9a8c..7ae6899 100644 --- a/test/Rsk.TokenExchange.Tests/Validators/DefaultTokenExchangeRequestValidatorTests.cs +++ b/test/Rsk.TokenExchange.Tests/Validators/DefaultTokenExchangeRequestValidatorTests.cs @@ -11,7 +11,7 @@ namespace Rsk.TokenExchange.Tests.Validators { public class DefaultTokenExchangeRequestValidatorTests { - private Mock mockSubjectTokenValidator = new Mock(); + private Mock mockSubjectTokenValidator = new(); private readonly Mock mockRequest; private readonly Mock mockSubjectTokenResponse; @@ -24,7 +24,7 @@ public DefaultTokenExchangeRequestValidatorTests() mockRequest.Setup(x => x.ClientId).Returns(testClientId); mockSubjectTokenResponse = new Mock(); - mockSubjectTokenResponse.Setup(x => x.Claims).Returns(new[] {new Claim("aud", testClientId)}); + mockSubjectTokenResponse.Setup(x => x.Claims).Returns([new Claim("aud", testClientId)]); mockSubjectTokenValidator.Setup(x => x.Validate(It.IsAny(), It.IsAny())) .ReturnsAsync(mockSubjectTokenResponse.Object); @@ -87,7 +87,7 @@ public async Task Validate_WhenSubjectTokenIsValidWithInvalidAudience_ExpectFail mockRequest.Setup(x => x.ClientId).Returns(Guid.NewGuid().ToString); mockSubjectTokenResponse.Setup(x => x.IsValid).Returns(true); - mockSubjectTokenResponse.Setup(x => x.Claims).Returns(new[] {new Claim("aud", Guid.NewGuid().ToString())}); + mockSubjectTokenResponse.Setup(x => x.Claims).Returns([new Claim("aud", Guid.NewGuid().ToString())]); var sut = CreateSut(); var result = await sut.Validate(mockRequest.Object); From c37a040421537aa560d1e9293e46a663bd716367 Mon Sep 17 00:00:00 2001 From: Joe Harvey Date: Fri, 16 Oct 2026 13:55:11 +0100 Subject: [PATCH 4/7] update workflow to .net 10 --- .github/workflows/dotnet.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index fb6d48c..771e403 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,15 +16,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core 3.1 - uses: actions/setup-dotnet@v1 + - uses: actions/checkout@v4 + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '3.1.x' - - name: Setup .NET 5.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '5.0.x' + dotnet-version: '10.0.x' - name: Fix Windows VM bug shell: bash if: matrix.os == 'windows-latest' @@ -41,7 +37,7 @@ jobs: run: dotnet pack -c Release - name: upload artifact if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: nupkg path: '**/*.nupkg' From bde5aef00b0548ec9599bbf520575be46e7603c2 Mon Sep 17 00:00:00 2001 From: Joe Harvey Date: Fri, 16 Oct 2026 14:16:01 +0100 Subject: [PATCH 5/7] update readme --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d29df3d..8b0805b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OAuth Token Exchange (RFC 8693) for IdentityServer -An implementation of OAuth token exchange ([RFC 8693](https://www.rfc-editor.org/rfc/rfc8693.html)) for IdentityServer4 and Duende IdentityServer. This implementation provides the required abstractions for token exchange with extensibility points to implement your own authorization rules, with default implementation covering an API to API scenario. +An implementation of OAuth token exchange ([RFC 8693](https://www.rfc-editor.org/rfc/rfc8693.html)) for [Open.IdentityServer](https://github.com/RockSolidKnowledge/Open.IdentityServer) and Duende IdentityServer. This implementation provides the required abstractions for token exchange with extensibility points to implement your own authorization rules, with default implementation covering an API to API scenario. This library includes: @@ -13,10 +13,11 @@ To learn more about token exchange and why it is useful for microservices and AP ## Installation -This library is available as a nuget package for both IdentityServer4 and Duende IdentityServer. +This library is available as a nuget package for both Open.IdentityServer and Duende IdentityServer. +- [Rsk.TokenExchange.Open.IdentityServer](https://www.nuget.org/packages/Rsk.TokenExchange.Open.IdentityServer/) - [Rsk.TokenExchange.DuendeIdentityServer](https://www.nuget.org/packages/Rsk.TokenExchange.DuendeIdentityServer/) -- [Rsk.TokenExchange.IdentityServer4](https://www.nuget.org/packages/Rsk.TokenExchange.IdentityServer4/) + Once you have installed the correct nuget package, you can enable the token exchange grant type by using the `AddTokenExchange` extension on `IdentityServerBuilder`. From 725218ddcc733fccdf76f8e8451b5279162fbf43 Mon Sep 17 00:00:00 2001 From: Joe Harvey Date: Fri, 16 Oct 2026 14:37:43 +0100 Subject: [PATCH 6/7] flip conditional around --- src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs b/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs index 8aa94fa..c576c2b 100644 --- a/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs +++ b/src/Rsk.TokenExchange/Parsers/TokenExchangeClaimsParser.cs @@ -21,7 +21,7 @@ public virtual Task ParseSubject(IEnumerable subjectClaims, IToke ArgumentNullException.ThrowIfNull(subjectClaims); var subjectValues = subjectClaims.Where(x => x.Type == "sub").Select(x => x.Value).ToList(); - if (1 < subjectValues.Count) + if (subjectValues.Count > 1) { throw new SubjectParsingException("More than one subject claim present on token exchange request"); } From 3ea4081dcff15a0215921acca66c4200817df907 Mon Sep 17 00:00:00 2001 From: Joe Harvey Date: Fri, 16 Oct 2026 14:44:07 +0100 Subject: [PATCH 7/7] remove Fix Windows VM Bug step from workflow --- .github/workflows/dotnet.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 771e403..4ec4f2e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -21,11 +21,6 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - - name: Fix Windows VM bug - shell: bash - if: matrix.os == 'windows-latest' - run: | - dotnet nuget locals all --clear - name: Restore run: dotnet restore - name: Build