using System.Security.Cryptography; using System.Text; using System.Text.Json; using BigRegister.Api.Zgw; using BigRegister.Domain.Authorization; namespace BigRegister.Tests; /// /// The ZGW JWT is hand-signed (no library), so it needs a check that it's actually a valid /// HS256 JWS with the claims OpenZaak requires. Decodes the minted token and re-verifies the /// signature with the shared secret. /// public class ZgwTokenProviderTests { private static readonly ZgwOptions Options = new() { ClientId = "big-register", Secret = "super-secret-signing-key", UserId = "u-123", UserRepresentation = "Dr. Test", }; [Fact] public void Mints_a_three_part_jwt_with_the_required_claims() { var token = new ZgwTokenProvider(Options).Mint(); var parts = token.Split('.'); Assert.Equal(3, parts.Length); var header = JsonSerializer.Deserialize(Decode(parts[0])); Assert.Equal("HS256", header.GetProperty("alg").GetString()); Assert.Equal("JWT", header.GetProperty("typ").GetString()); var payload = JsonSerializer.Deserialize(Decode(parts[1])); Assert.Equal("big-register", payload.GetProperty("iss").GetString()); Assert.Equal("big-register", payload.GetProperty("client_id").GetString()); Assert.Equal("u-123", payload.GetProperty("user_id").GetString()); Assert.Equal("Dr. Test", payload.GetProperty("user_representation").GetString()); // iat is a recent unix second var iat = payload.GetProperty("iat").GetInt64(); Assert.InRange(iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds() - 5, DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 5); } [Fact] public void Mint_with_a_caller_carries_that_citizen_not_the_static_config_identity() { var caller = new ZorgverlenerCaller("111222333", "Dr. Citizen", PrincipalRole.Drafter); var token = new ZgwTokenProvider(Options).Mint(caller); var payload = JsonSerializer.Deserialize(Decode(token.Split('.')[1])); Assert.Equal("111222333", payload.GetProperty("user_id").GetString()); Assert.Equal("Dr. Citizen", payload.GetProperty("user_representation").GetString()); // iss/client_id stay the BFF's own registered client id either way. Assert.Equal("big-register", payload.GetProperty("client_id").GetString()); } [Fact] public void Mint_with_a_medewerker_caller_uses_the_medewerkerId_as_user_id() { // SubjectId is what ZgwTokenProvider.Mint reads — a medewerker's is its // medewerkerId, not a BSN, and this is the only place that's directly observable. var caller = new MedewerkerCaller("m.jansen", [MedewerkerRol.Behandelaar], "M. Jansen", PrincipalRole.Drafter); var token = new ZgwTokenProvider(Options).Mint(caller); var payload = JsonSerializer.Deserialize(Decode(token.Split('.')[1])); Assert.Equal("m.jansen", payload.GetProperty("user_id").GetString()); Assert.Equal("M. Jansen", payload.GetProperty("user_representation").GetString()); } [Fact] public void Signature_verifies_with_the_shared_secret() { var token = new ZgwTokenProvider(Options).Mint(); var parts = token.Split('.'); using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Options.Secret)); var expected = Base64Url(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{parts[0]}.{parts[1]}"))); Assert.Equal(expected, parts[2]); } private static string Decode(string b64Url) { var s = b64Url.Replace('-', '+').Replace('_', '/'); s = s.PadRight(s.Length + (4 - s.Length % 4) % 4, '='); return Encoding.UTF8.GetString(Convert.FromBase64String(s)); } private static string Base64Url(byte[] bytes) => Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_'); }