DPoP autentisering - eksempel
Dette er et .NET 8 konsollprogram som viser hvordan man autentiserer og konsumerer Meldingstjenerens REST-API med DPoP.
Nuget-pakker: IdentityModel og System.IdentityModel.Tokens.Jwt.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using IdentityModel;
using IdentityModel.Client;
using Microsoft.IdentityModel.Tokens;
using static IdentityModel.OidcConstants;
using JsonWebKey = Microsoft.IdentityModel.Tokens.JsonWebKey;
using TokenResponse = IdentityModel.Client.TokenResponse;
namespace MeldingstjenerApiClient;
internal static class Program
{
public static async Task Main()
{
var client = new RestWithDPoPClient(new HelseIdConfig());
var response = await client.Get("https://localhost:7106/messages?ToHerIds=8140582&IncludeApprec=true");
Console.WriteLine("Response:");
Console.WriteLine(response);
Console.WriteLine("Content:");
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
}
internal record HelseIdConfig
{
// MUST BE CONFIGURED!
internal readonly string HelseIdClientId = "";
internal readonly string HelseIdPrivateJwk = "";
internal readonly string HelseIdScopes = "nhn:msh/api";
internal readonly string TokenEndpoint = "https://helseid-sts.test.nhn.no/connect/token";
internal HelseIdConfig()
{
if (string.IsNullOrEmpty(HelseIdClientId) || string.IsNullOrEmpty(HelseIdPrivateJwk) || string.IsNullOrEmpty(HelseIdScopes))
{
throw new Exception($"{nameof(HelseIdClientId)} and {nameof(HelseIdPrivateJwk)} must be configured.");
}
}
}
internal class RestWithDPoPClient
{
private readonly HttpClient _httpClient;
private readonly DPoPProofCreator _dPoPProofCreator;
private readonly HelseIdService _helseIdService;
public RestWithDPoPClient(HelseIdConfig helseIdConfig)
{
_httpClient = new HttpClient();
_dPoPProofCreator = new DPoPProofCreator(helseIdConfig.HelseIdPrivateJwk);
_helseIdService = new HelseIdService(_httpClient, _dPoPProofCreator, helseIdConfig);
}
internal async Task<HttpResponseMessage> Get(string url)
{
var accessToken = await _helseIdService.GetAccessToken();
var dPoPProof = _dPoPProofCreator.CreateDPoPProof(url, "GET", null, accessToken);
Console.WriteLine("DPoP:");
Console.WriteLine(dPoPProof);
var response = await Get(url, accessToken, dPoPProof);
return response;
}
private async Task<HttpResponseMessage> Get(string url, string accessToken, string dPoPProof)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
requestMessage.SetDPoPToken(accessToken, dPoPProof);
Console.WriteLine("Sending request...");
var response = await _httpClient.SendAsync(requestMessage);
return response;
}
}
internal class HelseIdService(HttpClient httpClient, DPoPProofCreator dPoPProofCreator, HelseIdConfig helseIdConfig)
{
private DateTime _cachedAccessTokenExpiresAt = DateTime.MinValue;
private string _cachedAccessToken = string.Empty;
internal async Task<string> GetAccessToken()
{
if (DateTime.UtcNow <= _cachedAccessTokenExpiresAt)
{
Console.WriteLine("Using cached DPoP access token");
return _cachedAccessToken;
}
Console.WriteLine("Getting DPoP access token from HelseId");
var tokenResponse = await GetAccessTokenFromHelseId();
_cachedAccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn - 30); // Refresh token 30 seconds ahead of expiry
_cachedAccessToken = tokenResponse.AccessToken!;
Console.WriteLine(_cachedAccessToken);
return _cachedAccessToken;
}
private async Task<TokenResponse> GetAccessTokenFromHelseId()
{
// 1. Send a token request without a DPoP nonce
var firstRequest = CreateTokenRequest();
var firstTokenResponse = await httpClient.RequestClientCredentialsTokenAsync(firstRequest);
if (!firstTokenResponse.IsError || string.IsNullOrEmpty(firstTokenResponse.DPoPNonce))
{
throw new Exception("Expected a DPoP nonce to be returned from the authorization server.");
}
// 2. Send a second token request with the DPoP nonce from the first response
var secondRequest = CreateTokenRequest(firstTokenResponse.DPoPNonce);
var secondTokenResponse = await httpClient.RequestClientCredentialsTokenAsync(secondRequest);
if (secondTokenResponse.IsError || secondTokenResponse.AccessToken == null)
{
throw new Exception($"Error retrieving access token: {secondTokenResponse.Error}");
}
return secondTokenResponse;
}
private ClientCredentialsTokenRequest CreateTokenRequest(string? dPoPNonce = null)
{
var securityKey = new JsonWebKey(helseIdConfig.HelseIdPrivateJwk);
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha512);
var claims = new List<Claim>
{
new(JwtClaimTypes.Subject, helseIdConfig.HelseIdClientId),
new(JwtClaimTypes.IssuedAt, new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
new(JwtClaimTypes.JwtId, Guid.NewGuid().ToString("N"))
};
var token = new JwtSecurityToken(helseIdConfig.HelseIdClientId, helseIdConfig.TokenEndpoint, claims, DateTime.Now, DateTime.Now.AddMinutes(1), signingCredentials);
var tokenHandler = new JwtSecurityTokenHandler();
var clientAssertion = tokenHandler.WriteToken(token);
var request = new ClientCredentialsTokenRequest
{
Address = helseIdConfig.TokenEndpoint,
ClientAssertion = new ClientAssertion { Value = clientAssertion, Type = ClientAssertionTypes.JwtBearer },
ClientId = helseIdConfig.HelseIdClientId,
Scope = helseIdConfig.HelseIdScopes,
GrantType = GrantTypes.ClientCredentials,
ClientCredentialStyle = ClientCredentialStyle.PostBody,
DPoPProofToken = dPoPProofCreator.CreateDPoPProof(helseIdConfig.TokenEndpoint, "POST", dPoPNonce: dPoPNonce)
};
return request;
}
}
internal class DPoPProofCreator(string privateJwk)
{
public string CreateDPoPProof(string url, string httpMethod, string? dPoPNonce = null, string? accessToken = null)
{
var count = dPoPNonce == null ? "1st" : "2nd";
Console.WriteLine($"Creating DPoP proof ({count} round) for url {url}");
var securityKey = new JsonWebKey(privateJwk);
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha512);
var jwk = securityKey.Kty switch
{
JsonWebAlgorithmsKeyTypes.EllipticCurve => new Dictionary<string, string>
{
[JsonWebKeyParameterNames.Kty] = securityKey.Kty,
[JsonWebKeyParameterNames.X] = securityKey.X,
[JsonWebKeyParameterNames.Y] = securityKey.Y,
[JsonWebKeyParameterNames.Crv] = securityKey.Crv,
},
JsonWebAlgorithmsKeyTypes.RSA => new Dictionary<string, string>
{
[JsonWebKeyParameterNames.Kty] = securityKey.Kty,
[JsonWebKeyParameterNames.N] = securityKey.N,
[JsonWebKeyParameterNames.E] = securityKey.E,
[JsonWebKeyParameterNames.Alg] = signingCredentials.Algorithm,
},
_ => throw new InvalidOperationException("Invalid key type for DPoP proof.")
};
var jwtHeader = new JwtHeader(signingCredentials)
{
[JwtClaimTypes.TokenType] = "dpop+jwt",
[JwtClaimTypes.JsonWebKey] = jwk,
};
var urlWithoutQuery = url.Split('?')[0];
var payload = new JwtPayload
{
[JwtClaimTypes.JwtId] = Guid.NewGuid().ToString(),
[JwtClaimTypes.DPoPHttpMethod] = httpMethod,
[JwtClaimTypes.DPoPHttpUrl] = urlWithoutQuery,
[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.
var hash = SHA256.HashData(Encoding.ASCII.GetBytes(accessToken));
var ath = Base64Url.Encode(hash);
payload[JwtClaimTypes.DPoPAccessTokenHash] = ath;
}
var jwtSecurityToken = new JwtSecurityToken(jwtHeader, payload);
return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
}