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