diff --git a/src/Fhi.Authentication.Extensions/Fhi.Authentication.Extensions.csproj b/src/Fhi.Authentication.Extensions/Fhi.Authentication.Extensions.csproj index 2f19e26..63b8099 100644 --- a/src/Fhi.Authentication.Extensions/Fhi.Authentication.Extensions.csproj +++ b/src/Fhi.Authentication.Extensions/Fhi.Authentication.Extensions.csproj @@ -3,6 +3,7 @@ net8.0;net9.0 Fhi.Authentication + 2.1.0-local3 diff --git a/src/Fhi.Authentication.Extensions/OpenIdConnect/CookieEventExtensions.cs b/src/Fhi.Authentication.Extensions/OpenIdConnect/CookieEventExtensions.cs index 0bbdfe1..762fd39 100644 --- a/src/Fhi.Authentication.Extensions/OpenIdConnect/CookieEventExtensions.cs +++ b/src/Fhi.Authentication.Extensions/OpenIdConnect/CookieEventExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using System.Globalization; +using Duende.AccessTokenManagement.OpenIdConnect; namespace Fhi.Authentication.OpenIdConnect { @@ -44,8 +45,8 @@ public static async Task ValidateToken(this CookieValid var expiresAt = DateTimeOffset.Parse(tokens.SingleOrDefault(t => t.Name == ExpiresAt)?.Value ?? string.Empty, CultureInfo.InvariantCulture); if (expiresAt <= DateTimeOffset.UtcNow) { - var tokenService = context.HttpContext.RequestServices.GetRequiredService(); - var refreshedTokens = await tokenService.RefreshAccessTokenAsync(refreshToken.Value); + var userTokenEndpointService = context.HttpContext.RequestServices.GetRequiredService(); + var refreshedTokens = await userTokenEndpointService.RefreshAccessTokenAsync(new UserToken() { RefreshToken = refreshToken.Value }, new UserTokenRequestParameters()); if (refreshedTokens.IsError) { return new TokenValidationResponse(true, TokenValidationErrorCodes.ExpiredRefreshToken, "Refresh token is expired. Rejecting principal so that the user can re-authenticate"); diff --git a/src/Fhi.Authentication.Extensions/OpenIdConnect/ITokenService.cs b/src/Fhi.Authentication.Extensions/OpenIdConnect/ITokenService.cs index 4578961..897523b 100644 --- a/src/Fhi.Authentication.Extensions/OpenIdConnect/ITokenService.cs +++ b/src/Fhi.Authentication.Extensions/OpenIdConnect/ITokenService.cs @@ -1,39 +1,59 @@ -using Duende.AccessTokenManagement.OpenIdConnect; +using Duende.IdentityModel.Client; +using Fhi.Authentication.Setup; using Microsoft.Extensions.Logging; namespace Fhi.Authentication.OpenIdConnect { - /// - /// Response for token validation. - /// - /// - public record TokenResponse(bool IsError = false); - /// - /// Abstraction for token service. - /// TODO: response should be improved - /// + public record TokenResponse(string? AccessToken, bool IsError, string? ErrorDescription); public interface ITokenService { /// - /// Refresh access token. + /// 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 /// - Task RefreshAccessTokenAsync(string refreshToken); + public Task RequestDPoPToken(string authority, string clientId, string jwk, string scopes, string dPopJwk); } - internal class DefaultTokenService(IUserTokenEndpointService UserTokenEndpointService, ILogger Logger) : ITokenService + public class TokenService( + ILogger Logger, + IHttpClientFactory HttpClientFactory) : ITokenService { - public async Task RefreshAccessTokenAsync(string refreshToken) + public async Task RequestDPoPToken( + string authority, + string clientId, + string jwk, + string scopes, + string dPopJwk) { - var userToken = await UserTokenEndpointService.RefreshAccessTokenAsync(new UserToken() { RefreshToken = refreshToken }, new UserTokenRequestParameters()); - if (userToken.IsError) + 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) { - Logger.LogError(message: userToken.Error); - return new TokenResponse(true); + 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(); + return new TokenResponse(null, true, discovery is null ? "No discovery document" : discovery.Error); } } -} +} \ No newline at end of file diff --git a/src/Fhi.Authentication.Extensions/ServiceCollectionExtensions.cs b/src/Fhi.Authentication.Extensions/ServiceCollectionExtensions.cs index 1a457c1..e941560 100644 --- a/src/Fhi.Authentication.Extensions/ServiceCollectionExtensions.cs +++ b/src/Fhi.Authentication.Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Fhi.Authentication.OpenIdConnect; +using Duende.AccessTokenManagement.OpenIdConnect; +using Fhi.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -18,7 +19,8 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddOpenIdConnectCookieOptions(this IServiceCollection services) { services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddSingleton, OpenIdConnectCookieAuthenticationOptions>(); return services; diff --git a/src/Fhi.Authentication.Extensions/Setup/HttpClientExtensions.cs b/src/Fhi.Authentication.Extensions/Setup/HttpClientExtensions.cs new file mode 100644 index 0000000..c26a308 --- /dev/null +++ b/src/Fhi.Authentication.Extensions/Setup/HttpClientExtensions.cs @@ -0,0 +1,40 @@ +using Duende.IdentityModel; +using Duende.IdentityModel.Client; +using Fhi.Authentication.Tokens; + +namespace Fhi.Authentication.Setup; + +public static class HttpClientExtensions +{ + public 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); + } +} \ No newline at end of file diff --git a/src/Fhi.Authentication.Extensions/Tokens/DPoPProofHandler.cs b/src/Fhi.Authentication.Extensions/Tokens/DPoPProofHandler.cs deleted file mode 100644 index 35dbb4c..0000000 --- a/src/Fhi.Authentication.Extensions/Tokens/DPoPProofHandler.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; - -namespace Fhi.Authentication.Tokens -{ - //TODO - internal interface IDPoPProofHandler - { - public string CreateDPoPProof(string url, string httpMethod, string key); - public string CreateDPoPProofWithNonce(string url, string httpMethod, string jwk, string dPoPNonce); - public string CreateDPoPProofWithAccessToken(string url, string httpMethod, string jwk, string accessToken); - } - - internal class DefaultDPoPProofHandler : IDPoPProofHandler - { - public string CreateDPoPProof(string url, string httpMethod, string jwk) - { - var securityKey = new JsonWebKey(jwk); - var signingCredentials = new SigningCredentials(securityKey, securityKey.Alg); - - //TODO: Go through supported algs with HelseId and MS - var jwkHeaderKey = 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(), - }; - var jwtSecurityToken = new JwtSecurityToken(jwtHeader, payload); - return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); - } - - public string CreateDPoPProofWithAccessToken(string url, string httpMethod, string jwk, string accessToken) - { - throw new NotImplementedException(); - } - - public string CreateDPoPProofWithNonce(string url, string httpMethod, string jwk, string dPoPNonce) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Fhi.Authentication.Extensions/Tokens/DPopProofGenerator.cs b/src/Fhi.Authentication.Extensions/Tokens/DPopProofGenerator.cs new file mode 100644 index 0000000..b41738c --- /dev/null +++ b/src/Fhi.Authentication.Extensions/Tokens/DPopProofGenerator.cs @@ -0,0 +1,85 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Cryptography; +using System.Text; +using Duende.IdentityModel; +using Microsoft.IdentityModel.Tokens; + +namespace Fhi.Authentication.Tokens +{ + // 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 + + public 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); + } + } +} \ No newline at end of file diff --git a/tests/Fhi.Auth.IntegrationTests/AuthorizationTests.cs b/tests/Fhi.Auth.IntegrationTests/AuthorizationTests.cs index 8232634..9fa8127 100644 --- a/tests/Fhi.Auth.IntegrationTests/AuthorizationTests.cs +++ b/tests/Fhi.Auth.IntegrationTests/AuthorizationTests.cs @@ -20,7 +20,7 @@ public partial class Tests [Test] public async Task EndpointWithScopeAuthorization() { - FakeAuthHandler.TestClaims = + Setup.Tests.FakeAuthHandler.TestClaims = [ new System.Security.Claims.Claim("scope", "fhi:webapi/health-records.read"), new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "Test User") @@ -31,7 +31,7 @@ public async Task EndpointWithScopeAuthorization() .WithServices(services => { services.AddAuthentication("Fake") - .AddScheme("Fake", options => { }); + .AddScheme("Fake", options => { }); services.AddSingleton(); services.AddAuthorization(); }); diff --git a/tests/Fhi.Auth.IntegrationTests/Setup/FakeAuthHandler.cs b/tests/Fhi.Auth.IntegrationTests/Setup/FakeAuthHandler.cs index def40d0..081d012 100644 --- a/tests/Fhi.Auth.IntegrationTests/Setup/FakeAuthHandler.cs +++ b/tests/Fhi.Auth.IntegrationTests/Setup/FakeAuthHandler.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Authentication; -namespace Fhi.Auth.IntegrationTests +namespace Fhi.Auth.IntegrationTests.Setup { public partial class Tests { diff --git a/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/CookieEventTests.cs b/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/CookieEventTests.cs index 7523c2e..41726f9 100644 --- a/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/CookieEventTests.cs +++ b/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/CookieEventTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using NSubstitute; using System.Security.Claims; +using Duende.AccessTokenManagement.OpenIdConnect; namespace Fhi.Authentication.Extensions.UnitTests.OpenIdConnect { @@ -121,11 +122,14 @@ internal class CookieContextBuilder { private readonly List _tokens = []; private bool _isAuthenticated = true; - private static readonly ITokenService _tokenService = Substitute.For(); + private readonly IUserTokenEndpointService _userTokenEndpointService = Substitute.For(); public CookieContextBuilder WithRefreshAccessTokenError(bool isError) { - _tokenService.RefreshAccessTokenAsync(Arg.Any()).Returns(Task.FromResult(new TokenResponse(isError))); + var userToken = isError ? new UserToken { Error = "invalid_grant" } : new UserToken(); + _userTokenEndpointService + .RefreshAccessTokenAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(userToken)); return this; } @@ -150,18 +154,10 @@ public CookieValidatePrincipalContext Build() var principal = new ClaimsPrincipal(identity); var props = new AuthenticationProperties(); props.StoreTokens(_tokens); - DefaultHttpContext httpContext; - if (_tokenService != null) - { - var services = new ServiceCollection(); - services.AddSingleton(_tokenService); - var serviceProvider = services.BuildServiceProvider(); - httpContext = new DefaultHttpContext { RequestServices = serviceProvider }; - } - else - { - httpContext = new DefaultHttpContext(); - } + var services = new ServiceCollection(); + services.AddSingleton(_userTokenEndpointService); + var serviceProvider = services.BuildServiceProvider(); + var httpContext = new DefaultHttpContext { RequestServices = serviceProvider }; return new CookieValidatePrincipalContext( httpContext, new AuthenticationScheme("oidc", "oidc", typeof(CookieAuthenticationHandler)), diff --git a/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/TokenServiceTest.cs b/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/TokenServiceTest.cs deleted file mode 100644 index 6e221d8..0000000 --- a/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/TokenServiceTest.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Duende.AccessTokenManagement.OpenIdConnect; -using Fhi.Authentication.OpenIdConnect; -using Microsoft.Extensions.Logging; -using NSubstitute; - -namespace Fhi.Authentication.Extensions.UnitTests.OpenIdConnect -{ - public class TokenServiceTest - { - [Test] - public async Task RefreshAccessToken_ReturnsSuccess_WhenNoError() - { - var userTokenEndpointService = Substitute.For(); - var logger = Substitute.For>(); - var refreshToken = "refresh-token"; - userTokenEndpointService.RefreshAccessTokenAsync( - Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new UserToken())); - - var service = new DefaultTokenService(userTokenEndpointService, logger); - var result = await service.RefreshAccessTokenAsync(refreshToken); - - Assert.That(result.IsError, Is.False); - } - - [Test] - public async Task RefreshAccessToken_ReturnsError_WhenUserTokenIsError() - { - var userTokenEndpointService = Substitute.For(); - var logger = Substitute.For>(); - var refreshToken = "refresh-token"; - userTokenEndpointService.RefreshAccessTokenAsync( - Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new UserToken { Error = "invalid grant" })); - - var service = new DefaultTokenService(userTokenEndpointService, logger); - var result = await service.RefreshAccessTokenAsync(refreshToken); - - Assert.That(result.IsError, Is.True); - } - } -} diff --git a/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/TokenServiceTests.cs b/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/TokenServiceTests.cs new file mode 100644 index 0000000..f663971 --- /dev/null +++ b/tests/Fhi.Authentication.Extensions.UnitTests/OpenIdConnect/TokenServiceTests.cs @@ -0,0 +1,83 @@ +using Fhi.Authentication.Extensions.UnitTests.Setup; +using Fhi.Authentication.OpenIdConnect; +using Microsoft.Extensions.Logging; +using NSubstitute; + +namespace Fhi.Authentication.Extensions.UnitTests.OpenIdConnect +{ + public class TokenServiceTests + { + + [Test] + public async Task GetDPoPToken_InvalidAuthority_DiscoveryendpointLookupReturnsError() + { + var handler = new TestHttpMessageHandler(new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)); + var httpClient = new HttpClient(handler) + { + BaseAddress = new Uri("https://wronguri") + }; + + var factory = Substitute.For(); + factory.CreateClient().Returns(httpClient); + var tokenService = new TokenService( + Substitute.For>(), + factory); + + var tokenResponse = await tokenService.RequestDPoPToken( + "https://wronguri", + "clientId", + "private-jwk", + "nhn:selvbetjening/client", + "dpop-jwk"); + using (Assert.EnterMultipleScope()) + { + Assert.That(tokenResponse, Is.Not.Null); + Assert.That(tokenResponse.IsError, expression: Is.True); + Assert.That(tokenResponse.ErrorDescription, + Is.EqualTo("Error connecting to https://wronguri/.well-known/openid-configuration: Bad Request")); + Assert.That(tokenResponse.AccessToken, Is.Null); + } + } + + /// + /// Manually test to get a DPoP token from HelseID using the TokenService. + /// In order to test replace the clientId and jwk with your own values. + /// + /// + [Test, Explicit("This test getting DPoP token from HelseID with the TokenService")] + public async Task RequestDPoPToken() + { + var tokenService = new TokenService( + Substitute.For>(), + HttpClientFactoryCreator.CreateFactory()); + + var jwk = """ + { + "d": "Q4x8XiZ3JKn0-ijW-H9plfw7QF4VLK43jHxYtPJvX6GcBuEk_rMedziQuqbBCZrK6aWVspnYS6dQtj33Z2TtSkXu2gy_1xR2nR8h9XeZ6h6QRbL9bj1Qxrk70ry7bXz5WIjyyuPmY73aPw9OFrZ_NDeUQjiEofzTHkr86ZIVjAmNLarVufG9P2V6fz14wwHc3aLBVgUt7Rxx5sFOQR30zYGpd1BH-xK6ykA6n6BdaIc4luWw_SkmVowwO4toScj07qoAYTUR4IFQHYt7sQZNufFG89nB-v_Er0a2tRvtME2NnU_4rn4ea1yyGFlYH_6Amtb8u4-TAeOESjrMw9ylBkvb6vIvtqT0lQdBJJEPI_Hx-655ElvO4zT48HBS6oVZHCARN17d7pQWrnxiSusYEdM9RwJET57ieVayo-baQe3NOvj2Y5V2H034cWCJt_DTh7ye9RXD4gtMnHDQ-tgV6ztwW8GkGvbJzXUnkqGXUvKqjeJAnOc2Ahoxpc-9cnMnW2DrwPnI0f9Jsq0n3hQyqwnnyimIeZn32WVe2Q4XC7d_VB21E8oDZhdeUlxuTZX-foTrYB3xvDKB6tLCaaMbfpzvUsSfSYqbAXQfqhQWosyt7w-ZIYJOY05fWspR3mlpo5IMGkaDp8clvz51f8zdMfSYFTml4e_zjoduvlz2wyE", + "dp": "GsxR4BGYEb460zNcX1_SROtq2zVG8IfYVFy-3pFQvmerfiRJr0uuWvZ1WCtqalXKV33ACdf5njmkKdA-z-RbH07axt52b8SQZOTQ8527i5p_zJ6QGp6Lw4iuepAX64T_POtqmUDKcusIOGxxZbC_SjVr7dtYHVWuqPZlNjFbqQpWcerQybQvsyBeVDzDYkcdM7dhW8wXuIPDukU0zkgKBvW-23LR6dN9t7zh3suLdhkaV381-lODAU_U02-wIhXwDcHKi_8a1dMtMKQFxKyngp0d3P8R5hDT71UOttD0zMxt6HB_c6cwLmOYPclYXsK0-bIIDgJq7rERIA6KF0GqaQ", + "dq": "CSI90DDVZFFj0DWpUXcpWjH5NOP7Yca9dTeFkWqnhmpS_XOMNvCLa9pyTO7o-Iw-aRbVhlpyIQN4pSdMmjnW5eEpBg8zsd8LcV8gkv9sL08bL-8dWcqy5kD9pgBEK49HgiobTWpdKd02PErgXbY28hWRx4JafVRjk7PkRXD4yJjK_qJ-fwlY51K0ynAIX4L7C14LW7AVYp4QkRjaHd_O1CRorVijR_N_sEvfa1jfZHNtBmgaUbJxn_4rYVZfehu5nbqoXLB4VqJBwVf26rNyT-fxMbAW4OH0ubjWrcTCedfAIMMegbXG7cHxrrbAL-50PggVWWjxbKAt0gBoN5KNIQ", + "e": "AQAB", + "kid": "-JYdQcqGy0Qmbpv6pX_2EdJkGciRu7BaDJk3Hz4WdZ4", + "kty": "RSA", + "n": "iu9EmQLoIJBPBqm3jLYW4oI8yLkOxvKg-OagE8HlzP-RQnDXH9hBe2cTRZ3oNqG1viWmv6-dxNtKU1QxOpezWLx-N-AJ7dIlXTMUGkCheHUorPSzakeBUOCHtvT1Tdv9Mzue9fVt3JxpPX6mQNlsOzwk9L8HmbgojMcApKmQcfNriVV72byLuaAoh9fcXSNm6TUuwO5cPmnHgS5B5Hfe5P0OIte027oZyjPiYm-QbV4YJNjwwwZnPvkLaRjw6L8sV5TAOLvNQIt63OpF8UHPjBsM8LJHdHFUMgx2BaMaJC8tNCi_8UWGG59sd4-_vJC78s3wZNEGL6OwCngpF7NLwaP9Zqxx8DDkOY71MvvcAyu4i0D6_8A8_qewLvb_SPxNpCe8zH5MJIKNJB38InWd8FpvpbPuEJt4oK1gfUBWLWQ39YIHzodKhkN-qAXYWGyzJ2nJdNIMAclefw251Cvjcyf3gmVATXDBAo-piUJIGXC3y7yqfyMupe_4oRe69DFBZTecXSLEdbAbUtiaH9r4rY5oeYCiZ70wcFcieHFZLwfleCPm5Cz8rEQxK8KjMis2kb1aRxVytTj_0pOkw1HEJU1tv_TWmD136RgoRtiqnVoxmCM6Q4XxXrOnGMPZR0_ScYHdW_YjDgnJBQykAbzW0nC47d3KSotktz1cPejo5_s", + "p": "wSqXfloa5ikf6d_G3N2x-IBjTEB7mibEif9qTNED7x4f7J8_vB_rdrsxoTCdY3R5j2BIS3XKyiTIkonfV5mYQvSu9RwxgX4ImcTsTXMxewuyQ031OJz0ruvlgvHELzdgkyo4q4NQaDigejfp_DsoEM0nLHeNXYfTz_AuTlkG8BA0JzrBK0aoBPUdwB1_WoJoJYVhST-B_PHQ3eaFOQNGXQXoYc6YZt4WKmu3WModjezdqnKVKSaORTuG5-mTQLS2jxZr1XlEDXWN53tH_Oon2WMDGbSCVz_qYVZUWorbjvUmxJnYP0H-lIxLRfxYE68DnYlXSWOzcfpdD6VIlP972w", + "q": "uCCszPIHfiHe5UOa0poW8WELpL_YItCdK_AnwulRHOM2FIQbmWBVZBMRguQJuMCvjIAWdQNEOrZYt1BhIknziUHSvSnLU0qszJ_ZHByZS54834CIxc-0etVkKbpnJGpjzgucvmEPNzrUko2ip512iOY9WTqB54Awg56rS-3oBw2_iqEzioAI30pD9-AX5xDRBEJcRJ8mPxw0iSDGVkxwIQNLBlML79cGNGjdrzsJyjPuMMDTLadHnRSAybmqhVTJYwEH5t37_f_fho2aoPu_sW65LdoW6Z-V042xnieMm3XhA8yZxonao2LH6Bh7Xk7qinWYpYuF6UpKqhtrpI8OYQ", + "qi": "Z7GWLxO3q2BolVFaOjuskhYc0V4GZ-b-SA2Rv110HovMDatlUGZKgoAOponFcvEddJSNf7stRM35HWiTN9W5iSUP4VLqAlJ2W7ftIsRJv0D-Lcs4HoXTAHcny36j0eEzhNLUYwfS9Y7ICWEWHv_WTG8Iz_I87JKLCnjGutZDmsM_fHDPmUkv7Pf2GG9r9hzS8uylC43ik4gfrp0Hm_6rAHKB4EHVHfYu51zl9yLkPgqq8ycHi0tF7VmVtDUsIMJdz7nlGOCS-468WI95dAfdfTC8v9JKXj9JL3ylM3dDbiC3m0p-rpaM2VzuO4OrMk-jWFCuYbDCYS-bcYFG4XwmtA" + } + """; + + var tokenResponse = await tokenService.RequestDPoPToken( + "https://helseid-sts.test.nhn.no", + "88d474a8-07df-4dc4-abb0-6b759c2b99ec", + jwk, + "nhn:selvbetjening/client", + jwk); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tokenResponse, Is.Not.Null); + Assert.That(tokenResponse.IsError, expression: Is.False); + } + } + } +} \ No newline at end of file diff --git a/tests/Fhi.Authentication.Extensions.UnitTests/Setup/HttpClientFactoryCreator.cs b/tests/Fhi.Authentication.Extensions.UnitTests/Setup/HttpClientFactoryCreator.cs new file mode 100644 index 0000000..94c2856 --- /dev/null +++ b/tests/Fhi.Authentication.Extensions.UnitTests/Setup/HttpClientFactoryCreator.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Fhi.Authentication.Extensions.UnitTests.Setup; + +public static class HttpClientFactoryCreator +{ + public static IHttpClientFactory CreateFactory() + { + var services = new ServiceCollection(); + + services.AddHttpClient(); + + var provider = services.BuildServiceProvider(); + return provider.GetRequiredService(); + } +} \ No newline at end of file diff --git a/tests/Fhi.Authentication.Extensions.UnitTests/Setup/TestHttpMessageHandler.cs b/tests/Fhi.Authentication.Extensions.UnitTests/Setup/TestHttpMessageHandler.cs new file mode 100644 index 0000000..101c727 --- /dev/null +++ b/tests/Fhi.Authentication.Extensions.UnitTests/Setup/TestHttpMessageHandler.cs @@ -0,0 +1,10 @@ +namespace Fhi.Authentication.Extensions.UnitTests.Setup +{ + internal class TestHttpMessageHandler(HttpResponseMessage response) : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(response); + } + } +} \ No newline at end of file