From 2f292bc7a86cef98a941df379a77e4eac6f62eb3 Mon Sep 17 00:00:00 2001 From: Henriette Leknes Date: Tue, 23 Sep 2025 09:32:10 +0200 Subject: [PATCH 1/2] Using local Authextension nuget --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dd73af4..e99146b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + From 51a5464e30ac23ea9edcdcf4f2fd06079eca3705 Mon Sep 17 00:00:00 2001 From: Henriette Leknes Date: Tue, 23 Sep 2025 12:18:16 +0200 Subject: [PATCH 2/2] Replaced local Tokenservice with service from Fhi.Authextensions --- Directory.Packages.props | 2 +- .../Business/HelseIdSelvbetjeningService.cs | 20 ++-- .../Extensions/DPoPProofGenerator.cs | 85 ---------------- .../HttpRequestMessageExtensions.cs | 1 + .../Infrastructure/TokenService.cs | 96 ------------------- .../ServiceCollectionExtensions.cs | 2 +- .../IntegrationTests/ClientUpdateTests.cs | 2 +- .../ReadClientSecretExpirationTests.cs | 3 +- .../ReadClientSecretExpirationTests.cs | 4 +- .../Business/UpdateClientSecretTests.cs | 4 +- .../Infrastructure/TokenServiceTests.cs | 2 +- .../HelseIdSelvbetjeningServiceBuilder.cs | 3 +- 12 files changed, 19 insertions(+), 205 deletions(-) delete mode 100644 src/Fhi.HelseId.Selvbetjening/Extensions/DPoPProofGenerator.cs delete mode 100644 src/Fhi.HelseId.Selvbetjening/Infrastructure/TokenService.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index e99146b..0d295c2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + diff --git a/src/Fhi.HelseId.Selvbetjening/Business/HelseIdSelvbetjeningService.cs b/src/Fhi.HelseId.Selvbetjening/Business/HelseIdSelvbetjeningService.cs index cdccde1..1241c48 100644 --- a/src/Fhi.HelseId.Selvbetjening/Business/HelseIdSelvbetjeningService.cs +++ b/src/Fhi.HelseId.Selvbetjening/Business/HelseIdSelvbetjeningService.cs @@ -1,8 +1,8 @@ using System.Net; +using Fhi.Authentication.OpenIdConnect; using Fhi.Authentication.Tokens; using Fhi.HelseIdSelvbetjening.Business.Models; using Fhi.HelseIdSelvbetjening.Extensions; -using Fhi.HelseIdSelvbetjening.Infrastructure; using Fhi.HelseIdSelvbetjening.Infrastructure.Selvbetjening; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; @@ -14,18 +14,14 @@ internal class HelseIdSelvbetjeningService( ISelvbetjeningApi selvbetjeningApi, ILogger logger) : IHelseIdSelvbetjeningService { - private readonly ITokenService _tokenService = tokenService; - private readonly ISelvbetjeningApi _selvbetjeningApi = selvbetjeningApi; - private readonly ILogger _logger = logger; - public async Task UpdateClientSecret(ClientConfiguration clientToUpdate, string authority, string baseAddress, string newPublicJwk) { - _logger.LogInformation("Start updating client {@ClientId} with new key.", clientToUpdate.ClientId); + logger.LogInformation("Start updating client {@ClientId} with new key.", clientToUpdate.ClientId); var dPoPKey = CreateDPoPKey(); - var response = await _tokenService.RequestDPoPToken(authority, clientToUpdate.ClientId, clientToUpdate.Jwk, "nhn:selvbetjening/client", dPoPKey); + var response = await tokenService.RequestDPoPToken(authority, clientToUpdate.ClientId, clientToUpdate.Jwk, "nhn:selvbetjening/client", dPoPKey); if (!response.IsError && response.AccessToken != null) { - var (ClientSecretUpdate, ProblemDetail) = await _selvbetjeningApi.UpdateClientSecretsAsync( + var (ClientSecretUpdate, ProblemDetail) = await selvbetjeningApi.UpdateClientSecretsAsync( baseAddress, dPoPKey, response.AccessToken, @@ -33,14 +29,14 @@ public async Task UpdateClientSecret(ClientConfigura if (ProblemDetail != null) { - _logger.LogError("Failed to update client {@ClientId}. Error: {@ProblemDetail}", clientToUpdate.ClientId, ProblemDetail.Detail); + logger.LogError("Failed to update client {@ClientId}. Error: {@ProblemDetail}", clientToUpdate.ClientId, ProblemDetail.Detail); return new ClientSecretUpdateResponse(HttpStatusCode.BadRequest, ProblemDetail.Detail); } //TODO: improve response with IResult. Should not serialize output return new ClientSecretUpdateResponse(HttpStatusCode.OK, ClientSecretUpdate?.Serialize()); } - _logger.LogError("Could not update client {@ClientId}. Error: {@Message}", clientToUpdate.ClientId, response.ErrorDescription); + logger.LogError("Could not update client {@ClientId}. Error: {@Message}", clientToUpdate.ClientId, response.ErrorDescription); return new(null, response.ErrorDescription); } @@ -51,7 +47,7 @@ public async Task> ReadClie return new Error(errorResult); var dPoPKey = CreateDPoPKey(); - var response = await _tokenService.RequestDPoPToken( + var response = await tokenService.RequestDPoPToken( authority, clientConfiguration.ClientId, clientConfiguration.Jwk, @@ -64,7 +60,7 @@ public async Task> ReadClie return new Error(errorResult); } - var (ClientSecrets, ProblemDetail) = await _selvbetjeningApi.GetClientSecretsAsync(baseAddress, dPoPKey, response.AccessToken); + var (ClientSecrets, ProblemDetail) = await selvbetjeningApi.GetClientSecretsAsync(baseAddress, dPoPKey, response.AccessToken); if (ProblemDetail != null) { errorResult.AddError($"Failed to read client secret expiration: {ProblemDetail.Detail}"); diff --git a/src/Fhi.HelseId.Selvbetjening/Extensions/DPoPProofGenerator.cs b/src/Fhi.HelseId.Selvbetjening/Extensions/DPoPProofGenerator.cs deleted file mode 100644 index 9581c6e..0000000 --- a/src/Fhi.HelseId.Selvbetjening/Extensions/DPoPProofGenerator.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Cryptography; -using System.Text; -using Duende.IdentityModel; -using Microsoft.IdentityModel.Tokens; - -namespace Fhi.HelseIdSelvbetjening.Extensions -{ - // TODO: Need to go through this. Copied from HelseId samples and Duende. Should we handle multiple algorithms? - // need to figure out what algs, size etc. to support - - internal static class DPoPProofGenerator - { - /// - /// - /// - /// - /// - /// public and private key - /// - /// - /// - /// - /// - public static string CreateDPoPProof(string url, string httpMethod, string key, string keyAlgorithm, string? dPoPNonce = null, string? accessToken = null) - { - var securityKey = new JsonWebKey(key); - var signingCredentials = new SigningCredentials(securityKey, keyAlgorithm); - - var jwk = securityKey.Kty switch - { - JsonWebAlgorithmsKeyTypes.EllipticCurve => new Dictionary - { - [JsonWebKeyParameterNames.Kty] = securityKey.Kty, - [JsonWebKeyParameterNames.X] = securityKey.X, - [JsonWebKeyParameterNames.Y] = securityKey.Y, - [JsonWebKeyParameterNames.Crv] = securityKey.Crv, - }, - JsonWebAlgorithmsKeyTypes.RSA => new Dictionary - { - [JsonWebKeyParameterNames.Kty] = securityKey.Kty, - [JsonWebKeyParameterNames.N] = securityKey.N, - [JsonWebKeyParameterNames.E] = securityKey.E, - }, - _ => throw new InvalidOperationException("Invalid key type for DPoP proof.") - }; - - var jwtHeader = new JwtHeader(signingCredentials) - { - [JwtClaimTypes.TokenType] = "dpop+jwt", - [JwtClaimTypes.JsonWebKey] = jwk, - }; - - var payload = new JwtPayload - { - [JwtClaimTypes.JwtId] = Guid.NewGuid().ToString(), - [JwtClaimTypes.DPoPHttpMethod] = httpMethod, - [JwtClaimTypes.DPoPHttpUrl] = url, - [JwtClaimTypes.IssuedAt] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - }; - - // Used when accessing the authentication server (HelseID): - if (!string.IsNullOrEmpty(dPoPNonce)) - { - // nonce: A recent nonce provided via the DPoP-Nonce HTTP header. - payload[JwtClaimTypes.Nonce] = dPoPNonce; - } - - // Used when accessing an API that requires a DPoP token: - if (!string.IsNullOrEmpty(accessToken)) - { - // ath: hash of the access token. The value MUST be the result of a base64url encoding - // the SHA-256 [SHS] hash of the ASCII encoding of the associated access token's value. - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(Encoding.ASCII.GetBytes(accessToken)); - var ath = Base64Url.Encode(hash); - - payload[JwtClaimTypes.DPoPAccessTokenHash] = ath; - } - - var jwtSecurityToken = new JwtSecurityToken(jwtHeader, payload); - return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); - } - } -} diff --git a/src/Fhi.HelseId.Selvbetjening/Extensions/HttpRequestMessageExtensions.cs b/src/Fhi.HelseId.Selvbetjening/Extensions/HttpRequestMessageExtensions.cs index 70e8847..1c757e3 100644 --- a/src/Fhi.HelseId.Selvbetjening/Extensions/HttpRequestMessageExtensions.cs +++ b/src/Fhi.HelseId.Selvbetjening/Extensions/HttpRequestMessageExtensions.cs @@ -1,4 +1,5 @@ using Duende.IdentityModel.Client; +using Fhi.Authentication.Tokens; namespace Fhi.HelseIdSelvbetjening.Extensions { diff --git a/src/Fhi.HelseId.Selvbetjening/Infrastructure/TokenService.cs b/src/Fhi.HelseId.Selvbetjening/Infrastructure/TokenService.cs deleted file mode 100644 index 914a475..0000000 --- a/src/Fhi.HelseId.Selvbetjening/Infrastructure/TokenService.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Duende.IdentityModel; -using Duende.IdentityModel.Client; -using Fhi.Authentication.Tokens; -using Fhi.HelseIdSelvbetjening.Extensions; -using Microsoft.Extensions.Logging; - -namespace Fhi.HelseIdSelvbetjening.Infrastructure -{ - internal record TokenResponse(string? AccessToken, bool IsError, string? ErrorDescription); - internal interface ITokenService - { - /// - /// Create DPoP token - /// - /// The OpenId connectprovider authority Url - /// Client Identifier - /// The private json web key for client assertion - /// Separated list of scopes - /// The private json web key for DPoP - /// - public Task RequestDPoPToken(string authority, string clientId, string jwk, string scopes, string dPopJwk); - } - - internal class TokenService( - ILogger Logger, - IHttpClientFactory HttpClientFactory) : ITokenService - { - public async Task RequestDPoPToken( - string authority, - string clientId, - string jwk, - string scopes, - string dPopJwk) - { - var client = HttpClientFactory.CreateClient(); - Logger.LogInformation("Get metadata from discovery endpoint from Authority {@Authority}", authority); - var discovery = await client.GetDiscoveryDocumentAsync(authority); - if (discovery is not null && !discovery.IsError && discovery.Issuer is not null && discovery.TokenEndpoint is not null) - { - var response = await client.RequestTokenWithDPoP(discovery, clientId, jwk, scopes, dPopJwk); - - if (response.IsError && - response.Error == "use_dpop_nonce" && - response.HttpResponse?.Headers.Contains("DPoP-Nonce") == true) - { - var nonce = response.HttpResponse.Headers.GetValues("DPoP-Nonce").FirstOrDefault(); - if (!string.IsNullOrEmpty(nonce)) - { - response = await client.RequestTokenWithDPoP(discovery, clientId, jwk, scopes, dPopJwk, nonce); - } - } - - return response.IsError ? - new TokenResponse(null, true, response.ErrorDescription) : - new TokenResponse(response.AccessToken, false, string.Empty); - } - - return new TokenResponse(null, true, discovery is null ? "No discovery document" : discovery.Error); - } - } - - internal static class HttpClientExtensions - { - internal static async Task RequestTokenWithDPoP( - this HttpClient client, - DiscoveryDocumentResponse discovery, - string clientId, - string jwk, - string scopes, - string dPopJwk, - string? nonce = null) - { - var tokenRequest = new ClientCredentialsTokenRequest - { - ClientId = clientId, - Address = discovery.TokenEndpoint, - GrantType = OidcConstants.GrantTypes.ClientCredentials, - ClientCredentialStyle = ClientCredentialStyle.PostBody, - DPoPProofToken = DPoPProofGenerator.CreateDPoPProof( - discovery.TokenEndpoint!, - "POST", - dPopJwk, - "PS256", - dPoPNonce: nonce), - ClientAssertion = new ClientAssertion - { - Type = OidcConstants.ClientAssertionTypes.JwtBearer, - Value = ClientAssertionTokenHandler.CreateJwtToken(discovery.Issuer!, clientId, jwk) - }, - Scope = scopes - }; - - return await client.RequestClientCredentialsTokenAsync(tokenRequest); - } - } -} diff --git a/src/Fhi.HelseId.Selvbetjening/ServiceCollectionExtensions.cs b/src/Fhi.HelseId.Selvbetjening/ServiceCollectionExtensions.cs index c0cde43..5fa0d0b 100644 --- a/src/Fhi.HelseId.Selvbetjening/ServiceCollectionExtensions.cs +++ b/src/Fhi.HelseId.Selvbetjening/ServiceCollectionExtensions.cs @@ -1,5 +1,5 @@ +using Fhi.Authentication.OpenIdConnect; using Fhi.HelseIdSelvbetjening.Business; -using Fhi.HelseIdSelvbetjening.Infrastructure; using Fhi.HelseIdSelvbetjening.Infrastructure.Selvbetjening; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ClientUpdateTests.cs b/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ClientUpdateTests.cs index 7f7ed38..2f0ef87 100644 --- a/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ClientUpdateTests.cs +++ b/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ClientUpdateTests.cs @@ -1,6 +1,5 @@ using Fhi.HelseIdSelvbetjening.CLI.Commands.UpdateClientKey; using Fhi.HelseIdSelvbetjening.CLI.IntegrationTests.Setup; -using Fhi.HelseIdSelvbetjening.Infrastructure; using Fhi.HelseIdSelvbetjening.Infrastructure.Selvbetjening; using Fhi.HelseIdSelvbetjening.Infrastructure.Selvbetjening.Dtos; using Fhi.HelseIdSelvbetjening.UnitTests.Setup; @@ -8,6 +7,7 @@ using Microsoft.Extensions.Logging.Testing; using NSubstitute; using System.CommandLine; +using Fhi.Authentication.OpenIdConnect; namespace Fhi.HelseIdSelvbetjening.CLI.IntegrationTests { diff --git a/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs b/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs index c5677b6..2d061cd 100644 --- a/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs +++ b/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs @@ -1,13 +1,12 @@ using Fhi.HelseIdSelvbetjening.Business; using Fhi.HelseIdSelvbetjening.CLI.Commands.ReadClientSecretExpiration; using Fhi.HelseIdSelvbetjening.CLI.IntegrationTests.Setup; -using Fhi.HelseIdSelvbetjening.Infrastructure; using Fhi.HelseIdSelvbetjening.Infrastructure.Selvbetjening.Dtos; using Fhi.HelseIdSelvbetjening.UnitTests.Setup; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using System.CommandLine; -using System.CommandLine.Parsing; +using Fhi.Authentication.OpenIdConnect; namespace Fhi.HelseIdSelvbetjening.CLI.IntegrationTests { diff --git a/tests/Fhi.HelseIdSelvbetjening.UnitTests/Business/ReadClientSecretExpirationTests.cs b/tests/Fhi.HelseIdSelvbetjening.UnitTests/Business/ReadClientSecretExpirationTests.cs index 15d1635..7beaac4 100644 --- a/tests/Fhi.HelseIdSelvbetjening.UnitTests/Business/ReadClientSecretExpirationTests.cs +++ b/tests/Fhi.HelseIdSelvbetjening.UnitTests/Business/ReadClientSecretExpirationTests.cs @@ -1,9 +1,9 @@ +using Fhi.Authentication.OpenIdConnect; using Fhi.HelseIdSelvbetjening.Business.Models; -using Fhi.HelseIdSelvbetjening.Infrastructure; using Fhi.HelseIdSelvbetjening.UnitTests.Setup; using ClientSecretDto = Fhi.HelseIdSelvbetjening.Infrastructure.Selvbetjening.Dtos.ClientSecret; -namespace Fhi.HelseIdSelvbetjening.UnitTests.Services +namespace Fhi.HelseIdSelvbetjening.UnitTests.Business { public class ReadClientSecretExpirationTests { diff --git a/tests/Fhi.HelseIdSelvbetjening.UnitTests/Business/UpdateClientSecretTests.cs b/tests/Fhi.HelseIdSelvbetjening.UnitTests/Business/UpdateClientSecretTests.cs index 2953227..0d54c4d 100644 --- a/tests/Fhi.HelseIdSelvbetjening.UnitTests/Business/UpdateClientSecretTests.cs +++ b/tests/Fhi.HelseIdSelvbetjening.UnitTests/Business/UpdateClientSecretTests.cs @@ -1,10 +1,10 @@ +using Fhi.Authentication.OpenIdConnect; using Fhi.HelseIdSelvbetjening.Business.Models; -using Fhi.HelseIdSelvbetjening.Infrastructure; using Fhi.HelseIdSelvbetjening.UnitTests.Setup; using Microsoft.Extensions.Logging; using NSubstitute; -namespace Fhi.HelseIdSelvbetjening.UnitTests.Services +namespace Fhi.HelseIdSelvbetjening.UnitTests.Business { public class UpdateClientSecretTests { diff --git a/tests/Fhi.HelseIdSelvbetjening.UnitTests/Infrastructure/TokenServiceTests.cs b/tests/Fhi.HelseIdSelvbetjening.UnitTests/Infrastructure/TokenServiceTests.cs index ddebb6c..d03dfe3 100644 --- a/tests/Fhi.HelseIdSelvbetjening.UnitTests/Infrastructure/TokenServiceTests.cs +++ b/tests/Fhi.HelseIdSelvbetjening.UnitTests/Infrastructure/TokenServiceTests.cs @@ -1,4 +1,4 @@ -using Fhi.HelseIdSelvbetjening.Infrastructure; +using Fhi.Authentication.OpenIdConnect; using Fhi.HelseIdSelvbetjening.UnitTests.Setup; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/tests/Fhi.HelseIdSelvbetjening.UnitTests/Setup/HelseIdSelvbetjeningServiceBuilder.cs b/tests/Fhi.HelseIdSelvbetjening.UnitTests/Setup/HelseIdSelvbetjeningServiceBuilder.cs index 9fc0580..4c9f96f 100644 --- a/tests/Fhi.HelseIdSelvbetjening.UnitTests/Setup/HelseIdSelvbetjeningServiceBuilder.cs +++ b/tests/Fhi.HelseIdSelvbetjening.UnitTests/Setup/HelseIdSelvbetjeningServiceBuilder.cs @@ -1,6 +1,5 @@ +using Fhi.Authentication.OpenIdConnect; using Fhi.HelseIdSelvbetjening.Business; -using Fhi.HelseIdSelvbetjening.Business.Models; -using Fhi.HelseIdSelvbetjening.Infrastructure; using Fhi.HelseIdSelvbetjening.Infrastructure.Selvbetjening; using Fhi.HelseIdSelvbetjening.Infrastructure.Selvbetjening.Dtos; using Microsoft.Extensions.Logging;