diff --git a/.gitignore b/.gitignore index 13e13dd435fbfe8febcdb01de8f2410b7c9f92be..d3d3e50d719fc2a4b2e4fcb1de3ad08509bc147d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ **/**notes.md private_notes/ +IntegrationTests/certificates ### VisualStudioCode template .vscode/* diff --git a/BasicUnitTest/BasicUnitTest.csproj b/BasicUnitTest/BasicUnitTest.csproj index a25229d9e9f5e8e59e9fc82b1b505b8f02f4e46a..057e18bb263762eb67d8a5b62554aac2515dbaee 100644 --- a/BasicUnitTest/BasicUnitTest.csproj +++ b/BasicUnitTest/BasicUnitTest.csproj @@ -5,23 +5,27 @@ <Nullable>enable</Nullable> <IsPackable>false</IsPackable> - - <RootNamespace>FluentApiTest</RootNamespace> </PropertyGroup> <ItemGroup> - <PackageReference Include="FluentAssertions" Version="6.7.0"/> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0"/> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0"/> - <PackageReference Include="Moq" Version="4.18.1"/> - <PackageReference Include="NUnit" Version="3.13.2"/> - <PackageReference Include="NUnit3TestAdapter" Version="4.0.0"/> - <PackageReference Include="coverlet.collector" Version="3.1.0"/> + <PackageReference Include="FluentAssertions" Version="6.7.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> + <PackageReference Include="Moq" Version="4.18.1" /> + <PackageReference Include="NUnit" Version="3.13.2" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> + <PackageReference Include="coverlet.collector" Version="3.1.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> + <ProjectReference Include="..\MockContainer\MockContainer.csproj" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> - <ProjectReference Include="..\MockContainer\MockContainer.csproj"/> + <None Update="events.txt"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> </ItemGroup> </Project> diff --git a/BasicUnitTest/JsonSchemaValidation.cs b/BasicUnitTest/JsonSchemaValidation.cs new file mode 100644 index 0000000000000000000000000000000000000000..390a14410a29d34612d23707f83ee2c2ee3cc511 --- /dev/null +++ b/BasicUnitTest/JsonSchemaValidation.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using FluentAssertions; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Schema; +using NUnit.Framework; + +namespace BasicUnitTest; + +[TestFixture] +public class JsonSchemaValidation { + private List<string> _jsonData = null!; + private string _schemaJson = null!; + + [OneTimeSetUp] + public void OneTimeSetUp() { + var jsonItems = File.ReadAllLines("events.txt"); + _jsonData = jsonItems.Where(l => !l.StartsWith("#")) + .Select(l => Base64UrlEncoder.Decode(l.Split('.')[1])) + .ToList(); + + var schemaResponse = new HttpClient() + .GetAsync( + "https://schema.fitko.de/fit-connect/set-payload/1.0.0/set-payload.schema.json") + .Result; + + _schemaJson = schemaResponse.Content.ReadAsStringAsync().Result; + } + + [Test] + [Ignore("Package does not support this yet")] + public void NJson_ShouldPass() { + var schema = NJsonSchema.JsonSchema.FromJsonAsync(_schemaJson).Result; + _jsonData.ForEach(l => schema.Validate(l)); + } + + [Test] + public void JsonSchema_ShouldPass() { + _jsonData.ForEach(l => { + var jObject = JObject.Parse(l); + var schema = Newtonsoft.Json.Schema.JSchema.Parse(_schemaJson); + var result = jObject.IsValid(schema); + if (!result) { + Console.WriteLine(jObject.ToString()); + } + else Console.WriteLine("OK"); + + result.Should().BeTrue(); + }); + } + + [Test] + public void JsonSchema_ShouldFail() { + var schema = Newtonsoft.Json.Schema.JSchema.Parse(_schemaJson); + JObject.Parse( + "{\"sub\":\"submission:22a69c34-4ca4-43f7-a909-dcfe7f972c27\",\"$schema\":\"https://schema.fitko.de/fit-connect/set-payload/1.0.0/set-payload.schema.json\",\"iss\":\"https://submission-api-testing.fit-connect.fitko.dev\",\"txn\":\"case:5ef6d3c7-a9db-4b23-89a6-75f5011bd57a\",\"iat\":1661228799,\"jti\":\"c7ff0d9d-2caa-481d-b9ce-c6d4a1c6ee02\",\"event\":{\"https://schema.fitko.de/fit-connect/events/submit-submission\":{\"authenticationTags\":{\"data\":\"6hvHPpXSgRfeFAUWJRb5eQ\",\"metadata\":\"T_kVHCwoMCpg42c3CjeZAQ\",\"attachments\":{\"33b17890-2a86-47a4-8fb9-92befc782e13\":\"DxndA7vZAq_karEDRL31-Q\"}}}}}") + .IsValid(schema).Should().BeFalse(); + } +} diff --git a/BasicUnitTest/SecurityEventTokenTests.cs b/BasicUnitTest/SecurityEventTokenTests.cs index 09fce920c718d9fcd65767072f87152924734748..800215d6a0e4557711264543a191985e2feffe3f 100644 --- a/BasicUnitTest/SecurityEventTokenTests.cs +++ b/BasicUnitTest/SecurityEventTokenTests.cs @@ -1,14 +1,18 @@ using System; +using System.Collections.Generic; +using System.IO; using Autofac; +using FitConnect; using FitConnect.Encryption; using FitConnect.Models; using FitConnect.Models.v1.Api; +using FitConnect.Services.Models.v1.Submission; using FluentAssertions; using MockContainer; using NUnit.Framework; using SecurityEventToken = FitConnect.Models.SecurityEventToken; -namespace FluentApiTest; +namespace BasicUnitTest; [TestFixture] public class SecurityEventTokenTests { @@ -20,6 +24,7 @@ public class SecurityEventTokenTests { private const string acceptSubmission = SecurityEventToken.AcceptSubmissionSchema; + [SetUp] public void Setup() { var container = Container.Create(); @@ -29,12 +34,10 @@ public class SecurityEventTokenTests { [Test] public void CreateJwt_AcceptSubmission() { - var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), - acceptSubmission, - null - ); + var token = _encryption.CreateAcceptSecurityEventToken(new SubmissionForPickupDto { + Id = Guid.NewGuid().ToString(), CaseId = Guid.NewGuid().ToString(), + DestinationId = Guid.NewGuid().ToString() + }); Console.WriteLine(token); var decoded = new SecurityEventToken(token); decoded.Event?.Type.Should() @@ -44,10 +47,9 @@ public class SecurityEventTokenTests { [Test] public void CreateJwt_Reject_WithEncryptionIssue() { - var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + var token = _encryption.CreateRejectSecurityEventToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), - rejectSubmission, new[] { Problems.EncryptionIssue } ); Console.WriteLine(token); @@ -59,10 +61,9 @@ public class SecurityEventTokenTests { [Test] public void CreateJwt_Reject_WithMissingSchema() { - var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + var token = _encryption.CreateRejectSecurityEventToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), - rejectSubmission, new[] { Problems.MissingSchema } ); Console.WriteLine(token); @@ -75,10 +76,9 @@ public class SecurityEventTokenTests { [Test] public void CreateJwt_Reject_WithSchemaViolation() { - var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + var token = _encryption.CreateRejectSecurityEventToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), - rejectSubmission, new[] { Problems.SchemaViolation } ); Console.WriteLine(token); @@ -90,10 +90,9 @@ public class SecurityEventTokenTests { [Test] public void CreateJwt_Reject_WithSyntaxViolation() { - var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + var token = _encryption.CreateRejectSecurityEventToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), - rejectSubmission, new[] { Problems.SyntaxViolation } ); Console.WriteLine(token); @@ -106,10 +105,9 @@ public class SecurityEventTokenTests { [Test] public void CreateJwt_Reject_WithUnsupportedSchema() { - var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + var token = _encryption.CreateRejectSecurityEventToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), - rejectSubmission, new[] { Problems.UnsupportedSchema } ); Console.WriteLine(token); @@ -121,10 +119,9 @@ public class SecurityEventTokenTests { [Test] public void CreateJwt_Reject_WithIncorrectAuthenticationTag() { - var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), + var token = _encryption.CreateRejectSecurityEventToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), - rejectSubmission, new[] { Problems.IncorrectAuthenticationTag } ); Console.WriteLine(token); @@ -135,17 +132,20 @@ public class SecurityEventTokenTests { } [Test] - public void CreateJwt_Reject_WithCustomProblem() { - var token = _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), - rejectSubmission, - new[] { new Problems() { Description = "A real big issue" } } + public void CreateJwt_WithEncryptionKey_ShouldWork() { + _encryption.CreateSecurityEventToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), "DummyEvent", null); + } + + [Test] + public void CreateJwt_WithEncryptionKey_ShouldThrowAnException() { + Assert.Throws<InvalidOperationException>(() => + new FitEncryption(_encryption.PrivateKeyDecryption, + _encryption.PublicKeyEncryption, // using wrong key + _encryption.PublicKeyEncryption, + _encryption.PublicKeySignatureVerification, null) + .CreateSecurityEventToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), "DummyEvent", null) ); - Console.WriteLine(token); - var decoded = new SecurityEventToken(token); - decoded.Event?.Type.Should() - .Be(rejectSubmission); - decoded.EventType.Should().Be(EventType.Reject); } } diff --git a/BasicUnitTest/SenderTests.cs b/BasicUnitTest/SenderTests.cs index e815e8ff29f1a2d31bf333c193767cc96b8cacbb..e824755cf22491380dab5cd161d3f1e15f6e3436 100644 --- a/BasicUnitTest/SenderTests.cs +++ b/BasicUnitTest/SenderTests.cs @@ -1,13 +1,14 @@ using System; using Autofac; using FitConnect; +using FitConnect.Encryption; using FitConnect.Models; using FluentAssertions; using Microsoft.Extensions.Logging; using MockContainer; using NUnit.Framework; -namespace FluentApiTest; +namespace BasicUnitTest; public class SenderTests { private IContainer _container = null!; @@ -28,6 +29,7 @@ public class SenderTests { [SetUp] public void Setup() { + logger = _container.Resolve<ILogger>(); } [Test] @@ -77,10 +79,9 @@ public class SenderTests { var metadata = Sender.CreateMetadata(submission); // Assert - var errors = Subscriber.VerifyMetadata(metadata); - foreach (var error in errors) Console.WriteLine(error.ToString()); + var valid = JsonHelper.VerifyMetadata(metadata, logger); - errors.Count.Should().Be(0); + valid.Should().BeTrue(); } [Test] @@ -92,9 +93,7 @@ public class SenderTests { var metadata = Sender.CreateMetadata(submission); // Assert - var errors = Subscriber.VerifyMetadata(metadata); - foreach (var error in errors) Console.WriteLine(error.ToString()); - - errors.Count.Should().Be(1); + var valid = JsonHelper.VerifyMetadata(metadata, logger); + valid.Should().BeFalse(); } } diff --git a/BasicUnitTest/SubscriberReceiveTests.cs b/BasicUnitTest/SubscriberReceiveTests.cs index 88473285446ee3b3e862d6e269483393efbe320d..36cf79d037bce410b25eb6858851a1a0efd6c989 100644 --- a/BasicUnitTest/SubscriberReceiveTests.cs +++ b/BasicUnitTest/SubscriberReceiveTests.cs @@ -5,11 +5,9 @@ using Microsoft.Extensions.Logging; using MockContainer; using NUnit.Framework; -namespace FluentApiTest; +namespace BasicUnitTest; public class SubscriberReceiveTests { - private readonly string clientId = "clientId"; - private readonly string clientSecret = "clientSecret"; private IContainer _container = null!; private ILogger _logger = null!; private MockSettings _mockSettings = null!; diff --git a/BasicUnitTest/events.txt b/BasicUnitTest/events.txt new file mode 100644 index 0000000000000000000000000000000000000000..8758f9ea52932a99d3d5f8f7035880881c2c46b1 --- /dev/null +++ b/BasicUnitTest/events.txt @@ -0,0 +1,14 @@ +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjIyYTY5YzM0LTRjYTQtNDNmNy1hOTA5LWRjZmU3Zjk3MmMyNyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6NWVmNmQzYzctYTlkYi00YjIzLTg5YTYtNzVmNTAxMWJkNTdhIiwiaWF0IjoxNjYxMjI4Nzk3LCJqdGkiOiJiN2U2ZjQwNi01ODE2LTRmNTAtYmU4MC00YzM2ODIwY2Q5MWEiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvY3JlYXRlLXN1Ym1pc3Npb24iOnt9fX0.Vh9wOX6EYP0NiXDGKusXKdKxaLbIM6wW_dQnYA3DQbl6v9SNPzx2i-6DBTwX-6VUZSChV12QErM6k5WziX1AFmLmCh9S22oKlU-q4ra1JBZxgbupyMoMztvgYTneiG3wgUarAQ9ocKclmvbXF-sP45hYeP1yBUYBwMq0O1rY5CZPY1BUpwA7J8MAx8KEw-Io0Fw46gO5KV1ppLPdUB24wT1pCjZuntP6_qlp-EAxJrdmOfHLncjnzUt3bGn-s1I5yNgVYcs9C4uV6_bGkFFJHMc18ZtpQ9dZTVcEIJrgPRThCoKKFGpjDoX1u1uXML-iklM3bydbqk3Xe1SV6HdOGqlVurHJ2yDUzOkih6tz8S2PvsUvbEGOunJAeQncdjyaaW8tEFXDnDbaQpd9WSamMm4zTRB062zqIhGXBzHubCmK2Ry_0o5zm-MId4YuYXF5AgXcCozdERjjmiYYcfspam_Ytsq_K84k11SSGDuEAWWvrRyH4srRgzBSw9vMNSmVtvNolfi-wfFux8BTcG4-QyBS342PH-UjTrrmOqpfpWW7hZDb_3U_b7yAxjGP4jMtpM86lv-rwKSn--2UkwHsLZkxfLfNXho-YtKjs3jazeCqxFbSxwwWf6TmPszVW6QsFQtr4hTpwB_taxpTtm4T9U7b3kl-lnUV0K-rMJQUWVA +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjIyYTY5YzM0LTRjYTQtNDNmNy1hOTA5LWRjZmU3Zjk3MmMyNyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6NWVmNmQzYzctYTlkYi00YjIzLTg5YTYtNzVmNTAxMWJkNTdhIiwiaWF0IjoxNjYxMjI4Nzk5LCJqdGkiOiJjN2ZmMGQ5ZC0yY2FhLTQ4MWQtYjljZS1jNmQ0YTFjNmVlMDIiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvc3VibWl0LXN1Ym1pc3Npb24iOnsiYXV0aGVudGljYXRpb25UYWdzIjp7ImRhdGEiOiI2aHZIUHBYU2dSZmVGQVVXSlJiNWVRIiwibWV0YWRhdGEiOiJUX2tWSEN3b01DcGc0MmMzQ2plWkFRIiwiYXR0YWNobWVudHMiOnsiMzNiMTc4OTAtMmE4Ni00N2E0LThmYjktOTJiZWZjNzgyZTEzIjoiRHhuZEE3dlpBcV9rYXJFRFJMMzEtUSJ9fX19fQ.q1uZRkb8WIBZUqZdEJlvuvW_XrKTock743dF3nrjwfG7EWZ1hyZylLAT4-d-TUJDeidLUAYmathMN8ZTzCUYhpYwBTZZK68qbmYHlzR7axxoQsBzeV1AVe6HfBU2LAebCBzEHc3Hk1H52p0A1s7JJ2jSoIlwDMi7ErVqrgedY53rntvkabhwbvK8x8Kb2c4yEFBBBoAF5TGFZcpP8oDN-vg8VuilZ0OwYMxTBr_SvmxUSvKDEhV_YuFP4uj13SfHBlpDAtxbI5BrdWGj6banQ6qari_zbbVmiM820SVMagDiGvJDctqiRBxNpLmxkAZ1CWUn26vmsq7ZyU26x2vhKbAWGYgVu5zXiVwG6KU7-XNfuQC8v7ZfqMv6Jg3qsAQSESiOcC5pHw_fAOhUkMGhdQadffmdVJMVSCAmWQAz6X3xvF39SfdtrcsBP3YlnZvpg76FVpIEtcBkfyPWnJbqF_31sRqorD4S9T4mG9R16oFcAlt_Zw-15fkrQ9k8JkzbtSFNxvy7CTO7ssHSHeBco35uQHKuUee7of3PDtsS-slyoraMlgQSZTjwnooaF2U4VMVEPr-mcQqHj_2kQ1yuxoNs7m47thqajjM4kQKCGksg4jtBIDoqIPKQGwjKkp-LGxNiAVNuyPqxef4FYODOuPKpWWuG9zhF2gJGBqA46nw +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjIyYTY5YzM0LTRjYTQtNDNmNy1hOTA5LWRjZmU3Zjk3MmMyNyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6NWVmNmQzYzctYTlkYi00YjIzLTg5YTYtNzVmNTAxMWJkNTdhIiwiaWF0IjoxNjYxMjI4Nzk3LCJqdGkiOiJiN2U2ZjQwNi01ODE2LTRmNTAtYmU4MC00YzM2ODIwY2Q5MWEiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvY3JlYXRlLXN1Ym1pc3Npb24iOnt9fX0.Vh9wOX6EYP0NiXDGKusXKdKxaLbIM6wW_dQnYA3DQbl6v9SNPzx2i-6DBTwX-6VUZSChV12QErM6k5WziX1AFmLmCh9S22oKlU-q4ra1JBZxgbupyMoMztvgYTneiG3wgUarAQ9ocKclmvbXF-sP45hYeP1yBUYBwMq0O1rY5CZPY1BUpwA7J8MAx8KEw-Io0Fw46gO5KV1ppLPdUB24wT1pCjZuntP6_qlp-EAxJrdmOfHLncjnzUt3bGn-s1I5yNgVYcs9C4uV6_bGkFFJHMc18ZtpQ9dZTVcEIJrgPRThCoKKFGpjDoX1u1uXML-iklM3bydbqk3Xe1SV6HdOGqlVurHJ2yDUzOkih6tz8S2PvsUvbEGOunJAeQncdjyaaW8tEFXDnDbaQpd9WSamMm4zTRB062zqIhGXBzHubCmK2Ry_0o5zm-MId4YuYXF5AgXcCozdERjjmiYYcfspam_Ytsq_K84k11SSGDuEAWWvrRyH4srRgzBSw9vMNSmVtvNolfi-wfFux8BTcG4-QyBS342PH-UjTrrmOqpfpWW7hZDb_3U_b7yAxjGP4jMtpM86lv-rwKSn--2UkwHsLZkxfLfNXho-YtKjs3jazeCqxFbSxwwWf6TmPszVW6QsFQtr4hTpwB_taxpTtm4T9U7b3kl-lnUV0K-rMJQUWVA +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjIyYTY5YzM0LTRjYTQtNDNmNy1hOTA5LWRjZmU3Zjk3MmMyNyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6NWVmNmQzYzctYTlkYi00YjIzLTg5YTYtNzVmNTAxMWJkNTdhIiwiaWF0IjoxNjYxMjI4Nzk5LCJqdGkiOiJjN2ZmMGQ5ZC0yY2FhLTQ4MWQtYjljZS1jNmQ0YTFjNmVlMDIiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvc3VibWl0LXN1Ym1pc3Npb24iOnsiYXV0aGVudGljYXRpb25UYWdzIjp7ImRhdGEiOiI2aHZIUHBYU2dSZmVGQVVXSlJiNWVRIiwibWV0YWRhdGEiOiJUX2tWSEN3b01DcGc0MmMzQ2plWkFRIiwiYXR0YWNobWVudHMiOnsiMzNiMTc4OTAtMmE4Ni00N2E0LThmYjktOTJiZWZjNzgyZTEzIjoiRHhuZEE3dlpBcV9rYXJFRFJMMzEtUSJ9fX19fQ.q1uZRkb8WIBZUqZdEJlvuvW_XrKTock743dF3nrjwfG7EWZ1hyZylLAT4-d-TUJDeidLUAYmathMN8ZTzCUYhpYwBTZZK68qbmYHlzR7axxoQsBzeV1AVe6HfBU2LAebCBzEHc3Hk1H52p0A1s7JJ2jSoIlwDMi7ErVqrgedY53rntvkabhwbvK8x8Kb2c4yEFBBBoAF5TGFZcpP8oDN-vg8VuilZ0OwYMxTBr_SvmxUSvKDEhV_YuFP4uj13SfHBlpDAtxbI5BrdWGj6banQ6qari_zbbVmiM820SVMagDiGvJDctqiRBxNpLmxkAZ1CWUn26vmsq7ZyU26x2vhKbAWGYgVu5zXiVwG6KU7-XNfuQC8v7ZfqMv6Jg3qsAQSESiOcC5pHw_fAOhUkMGhdQadffmdVJMVSCAmWQAz6X3xvF39SfdtrcsBP3YlnZvpg76FVpIEtcBkfyPWnJbqF_31sRqorD4S9T4mG9R16oFcAlt_Zw-15fkrQ9k8JkzbtSFNxvy7CTO7ssHSHeBco35uQHKuUee7of3PDtsS-slyoraMlgQSZTjwnooaF2U4VMVEPr-mcQqHj_2kQ1yuxoNs7m47thqajjM4kQKCGksg4jtBIDoqIPKQGwjKkp-LGxNiAVNuyPqxef4FYODOuPKpWWuG9zhF2gJGBqA46nw +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjIyYTY5YzM0LTRjYTQtNDNmNy1hOTA5LWRjZmU3Zjk3MmMyNyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6NWVmNmQzYzctYTlkYi00YjIzLTg5YTYtNzVmNTAxMWJkNTdhIiwiaWF0IjoxNjYxMjI4Nzk3LCJqdGkiOiJiN2U2ZjQwNi01ODE2LTRmNTAtYmU4MC00YzM2ODIwY2Q5MWEiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvY3JlYXRlLXN1Ym1pc3Npb24iOnt9fX0.Vh9wOX6EYP0NiXDGKusXKdKxaLbIM6wW_dQnYA3DQbl6v9SNPzx2i-6DBTwX-6VUZSChV12QErM6k5WziX1AFmLmCh9S22oKlU-q4ra1JBZxgbupyMoMztvgYTneiG3wgUarAQ9ocKclmvbXF-sP45hYeP1yBUYBwMq0O1rY5CZPY1BUpwA7J8MAx8KEw-Io0Fw46gO5KV1ppLPdUB24wT1pCjZuntP6_qlp-EAxJrdmOfHLncjnzUt3bGn-s1I5yNgVYcs9C4uV6_bGkFFJHMc18ZtpQ9dZTVcEIJrgPRThCoKKFGpjDoX1u1uXML-iklM3bydbqk3Xe1SV6HdOGqlVurHJ2yDUzOkih6tz8S2PvsUvbEGOunJAeQncdjyaaW8tEFXDnDbaQpd9WSamMm4zTRB062zqIhGXBzHubCmK2Ry_0o5zm-MId4YuYXF5AgXcCozdERjjmiYYcfspam_Ytsq_K84k11SSGDuEAWWvrRyH4srRgzBSw9vMNSmVtvNolfi-wfFux8BTcG4-QyBS342PH-UjTrrmOqpfpWW7hZDb_3U_b7yAxjGP4jMtpM86lv-rwKSn--2UkwHsLZkxfLfNXho-YtKjs3jazeCqxFbSxwwWf6TmPszVW6QsFQtr4hTpwB_taxpTtm4T9U7b3kl-lnUV0K-rMJQUWVA +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjIyYTY5YzM0LTRjYTQtNDNmNy1hOTA5LWRjZmU3Zjk3MmMyNyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6NWVmNmQzYzctYTlkYi00YjIzLTg5YTYtNzVmNTAxMWJkNTdhIiwiaWF0IjoxNjYxMjI4Nzk5LCJqdGkiOiJjN2ZmMGQ5ZC0yY2FhLTQ4MWQtYjljZS1jNmQ0YTFjNmVlMDIiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvc3VibWl0LXN1Ym1pc3Npb24iOnsiYXV0aGVudGljYXRpb25UYWdzIjp7ImRhdGEiOiI2aHZIUHBYU2dSZmVGQVVXSlJiNWVRIiwibWV0YWRhdGEiOiJUX2tWSEN3b01DcGc0MmMzQ2plWkFRIiwiYXR0YWNobWVudHMiOnsiMzNiMTc4OTAtMmE4Ni00N2E0LThmYjktOTJiZWZjNzgyZTEzIjoiRHhuZEE3dlpBcV9rYXJFRFJMMzEtUSJ9fX19fQ.q1uZRkb8WIBZUqZdEJlvuvW_XrKTock743dF3nrjwfG7EWZ1hyZylLAT4-d-TUJDeidLUAYmathMN8ZTzCUYhpYwBTZZK68qbmYHlzR7axxoQsBzeV1AVe6HfBU2LAebCBzEHc3Hk1H52p0A1s7JJ2jSoIlwDMi7ErVqrgedY53rntvkabhwbvK8x8Kb2c4yEFBBBoAF5TGFZcpP8oDN-vg8VuilZ0OwYMxTBr_SvmxUSvKDEhV_YuFP4uj13SfHBlpDAtxbI5BrdWGj6banQ6qari_zbbVmiM820SVMagDiGvJDctqiRBxNpLmxkAZ1CWUn26vmsq7ZyU26x2vhKbAWGYgVu5zXiVwG6KU7-XNfuQC8v7ZfqMv6Jg3qsAQSESiOcC5pHw_fAOhUkMGhdQadffmdVJMVSCAmWQAz6X3xvF39SfdtrcsBP3YlnZvpg76FVpIEtcBkfyPWnJbqF_31sRqorD4S9T4mG9R16oFcAlt_Zw-15fkrQ9k8JkzbtSFNxvy7CTO7ssHSHeBco35uQHKuUee7of3PDtsS-slyoraMlgQSZTjwnooaF2U4VMVEPr-mcQqHj_2kQ1yuxoNs7m47thqajjM4kQKCGksg4jtBIDoqIPKQGwjKkp-LGxNiAVNuyPqxef4FYODOuPKpWWuG9zhF2gJGBqA46nw +eyJhbGciOiJQUzUxMiIsImtpZCI6InRpMVowS2tHMXVBWHZkMlhuV2I1d1d1OHNsR3lsdnN2ejNuT1NlN3l1QWMiLCJ0eXAiOiJzZWNldmVudCtqd3QifQ.eyJzdWIiOiJzdWJtaXNzaW9uOjIyYTY5YzM0LTRjYTQtNDNmNy1hOTA5LWRjZmU3Zjk3MmMyNyIsImp0aSI6ImI1ZmQ5YzY3LTJmZGQtNGNmZS05MjY0LWFiNjE1YzlkZjQ2NCIsImlhdCI6MTY2MTIyODgwOCwiaXNzIjoiYWEzNzA0ZDYtOGJkNy00ZDQwLWE4YWYtNTAxODUxZjkzOTM0IiwiZXZlbnRzIjp7Imh0dHBzOi8vc2NoZW1hLmZpdGtvLmRlL2ZpdC1jb25uZWN0L2V2ZW50cy9yZWplY3Qtc3VibWlzc2lvbiI6eyJwcm9ibGVtcyI6W3siaW5zdGFuY2UiOiJtZXRhZGF0YSIsImRldGFpbCI6IkRlciBNZXRhZGF0ZW5zYXR6IGlzdCBuaWNodCBzY2hlbWEtdmFsaWRlLiIsInRpdGxlIjoiU2NoZW1hLUZlaGxlciIsInR5cGUiOiJodHRwczovL3NjaGVtYS5maXRrby5kZS9maXQtY29ubmVjdC9ldmVudHMvcHJvYmxlbXMvc2NoZW1hLXZpb2xhdGlvbiJ9XX19LCJ0eG4iOiJjYXNlOjVlZjZkM2M3LWE5ZGItNGIyMy04OWE2LTc1ZjUwMTFiZDU3YSIsIiRzY2hlbWEiOiJodHRwczovL3NjaGVtYS5maXRrby5kZS9maXQtY29ubmVjdC9zZXQtcGF5bG9hZC8xLjAuMC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiJ9.n_CYNTuSS72iNjqnjG_FPLUrw5yaUrAobYwHkjBrv8DtCfAKWwOre83jmptiQtn95Mo95GWF9A9RDJ0xHXlE3x3IBiHoM1gwpnTzQ2druXRTmFAS2yFbNOI6ky9_5X2DGI8AglWa4dXpV_ZkeFs4cRpWEPFCYScMLwvfOwJn02O0PhaVvN52tViFt8TFfPHHnuCtj0XpxJGoPzmTN-LJcXlWTQIfNovS-GFzXqtgHpFvrDeLtOMgJ2TvSvGWDWz2_TWeeb9ToJN78V9WhEdF0kp-o3wpj2wj-NcKxeZ__Xb_9gqbJUoYGapUgMhibE7Aq8v-q5DY9MYJNw9g8tFZ37Sb745x0gnkosbk5CvUhocq1xdgjbBXNjABLnns3DlWoeoFq2PJIrfz-Mjkyux4xbZD5YgOx8Wjb0y9u3IE8kpG5cFSEomTtJy5v7YiZtYj4tyJ3hW6wGA39BzGRD7ip0Se_bJe9Dj0HvbuPWA_mZ1TWqD7jw6SXgWGbxQcbyYAx2fj-QkLqSiKY4kNUXe9FUzBanjm2FoNBu_Gip5wSmPS22Eu2l6UjXWfVyTzGY9y4cw6CE9eillZx5025_ty_6xa5AnvFAtQgiHrAHREr5qoxuUVkH-LrGCqY-qneXnUL8fSms8YkTFsGZ43colhflDQ304ZhqXjk8g2V4eX2ik +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjg3OTI0NDkzLTgwYWYtNDNjMC1iNWI2LWQ3N2EyZGY5NzVjYyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6ZGE1OTA4YTktNjcxZi00ZGEyLWJkOTItZDJkN2I2N2UxOTRmIiwiaWF0IjoxNjYxMjI4ODE2LCJqdGkiOiI5NjM5ZTM1OS0xNTY4LTQyOTMtOTgzZS03YWRhZTU3NTY2OGIiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvY3JlYXRlLXN1Ym1pc3Npb24iOnt9fX0.AuzzS006nNp21wAzCZluH3lmLR4lucSa1xFYIxXXRk66XaqQArtn9tlduwCYO_PQCMHpsy1rtcDrJo1267J_GW1uGeDkXQ0vlpJ_Mu5zr09ppIKlMFJpEf9TuNpp3KTBVvbr-3Plb8I7KMee0RazLB2J23ia5kC6cH9xM3UVkuIWpIUyCoe9BcXQhnfemPLBduiaILACqdoHcAGTyAItFYB9nah4dt7pzCCy4aigIp4g5DnfHPPlDFk4cILuD9riv--Mn5njXF7no9wFtqzvyLqUijjbu3lhvE0m8fYd6I6SjIAj2gyE807Aeke9mSuCTVf4rPQvimQpVoWxx4fESpX8FUfpvyaU3suMNNGXfSjwcQTVe6F6fN-A-HFVYjjYiyERMQdznunfwLchECGTJYIPx5ZRjgz4wu1X6KgFBWSj-OwVjeozPahghS2srma3N0lrt_WSliR3Qp1Gzvf-2_ZjPYtYKzYuJki0dXSTKtfA0GevjObPqoG4DAqejFYkoElqrKk9eOUSSR-kjZfytnFGd2Q8QpLvuV9wGYHSXQLRqAHNTYg_4-0NsRNAi5563TJ-V5Adqjxps2euhERTD6IxVndkBt5W3i1oOxg1soojDtyYM2Hr3efSs7xBQKiBbBn46ILeyKtwm1GLF37wlXjxeSw8ULJTdZWVZ_CA4Yw +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjg3OTI0NDkzLTgwYWYtNDNjMC1iNWI2LWQ3N2EyZGY5NzVjYyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6ZGE1OTA4YTktNjcxZi00ZGEyLWJkOTItZDJkN2I2N2UxOTRmIiwiaWF0IjoxNjYxMjI4ODE4LCJqdGkiOiI2NDA3OWVmZC1kMGQ2LTRkMGEtODY2MC01Y2UyY2Y3NjJhZTYiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvc3VibWl0LXN1Ym1pc3Npb24iOnsiYXV0aGVudGljYXRpb25UYWdzIjp7ImRhdGEiOiIzTFFXd0V6blZTY0JuZmV6QjdzTHZRIiwibWV0YWRhdGEiOiJ3TWJORW5iX1pWS25fMGpmd0hnYjlBIiwiYXR0YWNobWVudHMiOnsiODVjNmM3ZGUtYzhiMS00NTQ1LWEyOTgtNDlhYjFhNmYyOTczIjoiZHd3YmkwM0x2ejUzaUN2YVo5NDhKUSJ9fX19fQ.cKFTUJX54xS9aBrqilE14HfcoNn_IajwPDyovO429iaIukg2JmvywRWxjSnJe4bneW8pV4zqYKGNZseNmJH8xMwy2rZIl5J-BgCZzsFpjS3GWCMvO32w5KIHCqFEZzUZ25ou8Umx6k9LFDGHD-3xUG4cjcg4M2FL0pugxClPh8zdUf0c4USZinzyG6-NTNni8GLwXDHe3fF8YW-BYlXNV8D5r_nHLVyC_Z6WAU7cyOR4JImzugEFld6sO7j5mbfhzf7t8Py52pU2pKP-PQgPVLRZPNsbBDcGbxZBUMxgJCX79J5xonob_o5Bve1P_7ZmeL9u1p8kOjmRTIWo1XB6RhdXe-csGarQvisowjvZX3pDOsxodr9XDTY5mhQ9ueZJesKpRjc-oRa5k4vjtDpiO15kGcbdIkGIFDv6QWxBOcPTfn14eGvr3629tOzmo1XFumZz4KfJuoMktpiduo7o4A7WAE_XF9SiwvxsQAbWrwMA0JYeRxKq-EYOwjCUnvVq0VLALaQ6tPki6Ljb1nVOQL9-dfZj8dwc9l91fZDyA6MSrAEfTUtLLPhuIaXw9idvPuSceEFjQb-9KrMrw0IbnUluciVkwueXIz6aRLaPfVE1JCHVIcvjCpCeksScrT7AUqaKThGvxnd510q7vVZArRHpBvlV75vPLTsfgCc3KME +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjg3OTI0NDkzLTgwYWYtNDNjMC1iNWI2LWQ3N2EyZGY5NzVjYyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6ZGE1OTA4YTktNjcxZi00ZGEyLWJkOTItZDJkN2I2N2UxOTRmIiwiaWF0IjoxNjYxMjI4ODE2LCJqdGkiOiI5NjM5ZTM1OS0xNTY4LTQyOTMtOTgzZS03YWRhZTU3NTY2OGIiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvY3JlYXRlLXN1Ym1pc3Npb24iOnt9fX0.AuzzS006nNp21wAzCZluH3lmLR4lucSa1xFYIxXXRk66XaqQArtn9tlduwCYO_PQCMHpsy1rtcDrJo1267J_GW1uGeDkXQ0vlpJ_Mu5zr09ppIKlMFJpEf9TuNpp3KTBVvbr-3Plb8I7KMee0RazLB2J23ia5kC6cH9xM3UVkuIWpIUyCoe9BcXQhnfemPLBduiaILACqdoHcAGTyAItFYB9nah4dt7pzCCy4aigIp4g5DnfHPPlDFk4cILuD9riv--Mn5njXF7no9wFtqzvyLqUijjbu3lhvE0m8fYd6I6SjIAj2gyE807Aeke9mSuCTVf4rPQvimQpVoWxx4fESpX8FUfpvyaU3suMNNGXfSjwcQTVe6F6fN-A-HFVYjjYiyERMQdznunfwLchECGTJYIPx5ZRjgz4wu1X6KgFBWSj-OwVjeozPahghS2srma3N0lrt_WSliR3Qp1Gzvf-2_ZjPYtYKzYuJki0dXSTKtfA0GevjObPqoG4DAqejFYkoElqrKk9eOUSSR-kjZfytnFGd2Q8QpLvuV9wGYHSXQLRqAHNTYg_4-0NsRNAi5563TJ-V5Adqjxps2euhERTD6IxVndkBt5W3i1oOxg1soojDtyYM2Hr3efSs7xBQKiBbBn46ILeyKtwm1GLF37wlXjxeSw8ULJTdZWVZ_CA4Yw +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjg3OTI0NDkzLTgwYWYtNDNjMC1iNWI2LWQ3N2EyZGY5NzVjYyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6ZGE1OTA4YTktNjcxZi00ZGEyLWJkOTItZDJkN2I2N2UxOTRmIiwiaWF0IjoxNjYxMjI4ODE4LCJqdGkiOiI2NDA3OWVmZC1kMGQ2LTRkMGEtODY2MC01Y2UyY2Y3NjJhZTYiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvc3VibWl0LXN1Ym1pc3Npb24iOnsiYXV0aGVudGljYXRpb25UYWdzIjp7ImRhdGEiOiIzTFFXd0V6blZTY0JuZmV6QjdzTHZRIiwibWV0YWRhdGEiOiJ3TWJORW5iX1pWS25fMGpmd0hnYjlBIiwiYXR0YWNobWVudHMiOnsiODVjNmM3ZGUtYzhiMS00NTQ1LWEyOTgtNDlhYjFhNmYyOTczIjoiZHd3YmkwM0x2ejUzaUN2YVo5NDhKUSJ9fX19fQ.cKFTUJX54xS9aBrqilE14HfcoNn_IajwPDyovO429iaIukg2JmvywRWxjSnJe4bneW8pV4zqYKGNZseNmJH8xMwy2rZIl5J-BgCZzsFpjS3GWCMvO32w5KIHCqFEZzUZ25ou8Umx6k9LFDGHD-3xUG4cjcg4M2FL0pugxClPh8zdUf0c4USZinzyG6-NTNni8GLwXDHe3fF8YW-BYlXNV8D5r_nHLVyC_Z6WAU7cyOR4JImzugEFld6sO7j5mbfhzf7t8Py52pU2pKP-PQgPVLRZPNsbBDcGbxZBUMxgJCX79J5xonob_o5Bve1P_7ZmeL9u1p8kOjmRTIWo1XB6RhdXe-csGarQvisowjvZX3pDOsxodr9XDTY5mhQ9ueZJesKpRjc-oRa5k4vjtDpiO15kGcbdIkGIFDv6QWxBOcPTfn14eGvr3629tOzmo1XFumZz4KfJuoMktpiduo7o4A7WAE_XF9SiwvxsQAbWrwMA0JYeRxKq-EYOwjCUnvVq0VLALaQ6tPki6Ljb1nVOQL9-dfZj8dwc9l91fZDyA6MSrAEfTUtLLPhuIaXw9idvPuSceEFjQb-9KrMrw0IbnUluciVkwueXIz6aRLaPfVE1JCHVIcvjCpCeksScrT7AUqaKThGvxnd510q7vVZArRHpBvlV75vPLTsfgCc3KME +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjg3OTI0NDkzLTgwYWYtNDNjMC1iNWI2LWQ3N2EyZGY5NzVjYyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6ZGE1OTA4YTktNjcxZi00ZGEyLWJkOTItZDJkN2I2N2UxOTRmIiwiaWF0IjoxNjYxMjI4ODE2LCJqdGkiOiI5NjM5ZTM1OS0xNTY4LTQyOTMtOTgzZS03YWRhZTU3NTY2OGIiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvY3JlYXRlLXN1Ym1pc3Npb24iOnt9fX0.AuzzS006nNp21wAzCZluH3lmLR4lucSa1xFYIxXXRk66XaqQArtn9tlduwCYO_PQCMHpsy1rtcDrJo1267J_GW1uGeDkXQ0vlpJ_Mu5zr09ppIKlMFJpEf9TuNpp3KTBVvbr-3Plb8I7KMee0RazLB2J23ia5kC6cH9xM3UVkuIWpIUyCoe9BcXQhnfemPLBduiaILACqdoHcAGTyAItFYB9nah4dt7pzCCy4aigIp4g5DnfHPPlDFk4cILuD9riv--Mn5njXF7no9wFtqzvyLqUijjbu3lhvE0m8fYd6I6SjIAj2gyE807Aeke9mSuCTVf4rPQvimQpVoWxx4fESpX8FUfpvyaU3suMNNGXfSjwcQTVe6F6fN-A-HFVYjjYiyERMQdznunfwLchECGTJYIPx5ZRjgz4wu1X6KgFBWSj-OwVjeozPahghS2srma3N0lrt_WSliR3Qp1Gzvf-2_ZjPYtYKzYuJki0dXSTKtfA0GevjObPqoG4DAqejFYkoElqrKk9eOUSSR-kjZfytnFGd2Q8QpLvuV9wGYHSXQLRqAHNTYg_4-0NsRNAi5563TJ-V5Adqjxps2euhERTD6IxVndkBt5W3i1oOxg1soojDtyYM2Hr3efSs7xBQKiBbBn46ILeyKtwm1GLF37wlXjxeSw8ULJTdZWVZ_CA4Yw +eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJ0eXAiOiJzZWNldmVudCtqd3QiLCJhbGciOiJQUzUxMiJ9.eyJzdWIiOiJzdWJtaXNzaW9uOjg3OTI0NDkzLTgwYWYtNDNjMC1iNWI2LWQ3N2EyZGY5NzVjYyIsIiRzY2hlbWEiOiJodHRwczpcL1wvc2NoZW1hLmZpdGtvLmRlXC9maXQtY29ubmVjdFwvc2V0LXBheWxvYWRcLzEuMC4wXC9zZXQtcGF5bG9hZC5zY2hlbWEuanNvbiIsImlzcyI6Imh0dHBzOlwvXC9zdWJtaXNzaW9uLWFwaS10ZXN0aW5nLmZpdC1jb25uZWN0LmZpdGtvLmRldiIsInR4biI6ImNhc2U6ZGE1OTA4YTktNjcxZi00ZGEyLWJkOTItZDJkN2I2N2UxOTRmIiwiaWF0IjoxNjYxMjI4ODE4LCJqdGkiOiI2NDA3OWVmZC1kMGQ2LTRkMGEtODY2MC01Y2UyY2Y3NjJhZTYiLCJldmVudHMiOnsiaHR0cHM6XC9cL3NjaGVtYS5maXRrby5kZVwvZml0LWNvbm5lY3RcL2V2ZW50c1wvc3VibWl0LXN1Ym1pc3Npb24iOnsiYXV0aGVudGljYXRpb25UYWdzIjp7ImRhdGEiOiIzTFFXd0V6blZTY0JuZmV6QjdzTHZRIiwibWV0YWRhdGEiOiJ3TWJORW5iX1pWS25fMGpmd0hnYjlBIiwiYXR0YWNobWVudHMiOnsiODVjNmM3ZGUtYzhiMS00NTQ1LWEyOTgtNDlhYjFhNmYyOTczIjoiZHd3YmkwM0x2ejUzaUN2YVo5NDhKUSJ9fX19fQ.cKFTUJX54xS9aBrqilE14HfcoNn_IajwPDyovO429iaIukg2JmvywRWxjSnJe4bneW8pV4zqYKGNZseNmJH8xMwy2rZIl5J-BgCZzsFpjS3GWCMvO32w5KIHCqFEZzUZ25ou8Umx6k9LFDGHD-3xUG4cjcg4M2FL0pugxClPh8zdUf0c4USZinzyG6-NTNni8GLwXDHe3fF8YW-BYlXNV8D5r_nHLVyC_Z6WAU7cyOR4JImzugEFld6sO7j5mbfhzf7t8Py52pU2pKP-PQgPVLRZPNsbBDcGbxZBUMxgJCX79J5xonob_o5Bve1P_7ZmeL9u1p8kOjmRTIWo1XB6RhdXe-csGarQvisowjvZX3pDOsxodr9XDTY5mhQ9ueZJesKpRjc-oRa5k4vjtDpiO15kGcbdIkGIFDv6QWxBOcPTfn14eGvr3629tOzmo1XFumZz4KfJuoMktpiduo7o4A7WAE_XF9SiwvxsQAbWrwMA0JYeRxKq-EYOwjCUnvVq0VLALaQ6tPki6Ljb1nVOQL9-dfZj8dwc9l91fZDyA6MSrAEfTUtLLPhuIaXw9idvPuSceEFjQb-9KrMrw0IbnUluciVkwueXIz6aRLaPfVE1JCHVIcvjCpCeksScrT7AUqaKThGvxnd510q7vVZArRHpBvlV75vPLTsfgCc3KME +# eyJhbGciOiJQUzUxMiIsImtpZCI6InRpMVowS2tHMXVBWHZkMlhuV2I1d1d1OHNsR3lsdnN2ejNuT1NlN3l1QWMiLCJ0eXAiOiJzZWNldmVudCtqd3QifQ.eyJzdWIiOiJzdWJtaXNzaW9uOjg3OTI0NDkzLTgwYWYtNDNjMC1iNWI2LWQ3N2EyZGY5NzVjYyIsImp0aSI6IjE5MmQzYTNhLThmZDMtNGI5Yy04OTcyLWQwNDgzYzAyNDBmMSIsImlhdCI6MTY2MTIyODgyNCwiaXNzIjoiYWEzNzA0ZDYtOGJkNy00ZDQwLWE4YWYtNTAxODUxZjkzOTM0IiwiZXZlbnRzIjp7Imh0dHBzOi8vc2NoZW1hLmZpdGtvLmRlL2ZpdC1jb25uZWN0L2V2ZW50cy9hY2NlcHQtc3VibWlzc2lvbiI6e319LCJ0eG4iOiJjYXNlOmRhNTkwOGE5LTY3MWYtNGRhMi1iZDkyLWQyZDdiNjdlMTk0ZiJ9.G_7hJVnXAVxbToNTnFvAdAqUdLKuzt6Vpnx5nEBjqQrV4sW3YOBOsDS8l68U5P_k1PkYhh8TYqSCq7o-NwnekqcwR4aT3bRvk5nlY_ygTyR7SJco2c60g74T3ysQZveHSGPI5G1ipKR5ywU5DDQnPoPO57vLar_3tRJqtMON_5CmRHHO2QZwvyQUP_ot-RlY7dmiHVp_jHbIiRvLNXFLGE_V_8XbdXBJXRc9rJ5ij4cq2Ry0d1Sd8Vg3HoTHgcIRpDlzb0_rUXZ4KYiata-JbU8cTkgmFC-C_bnPRq_sCj5LW5paudBa5YupTFYxOgWuLi_7qYPVbB9-_Id4MHvyCjMTpN_tym6pmA_b_i-ChZlJtGJzxxw2PMq5DwuPSddmHgRspBdw8cnREz0tWrKZ3EeWkaM_tPTTcaGyHDTIzh4hiahb4cg5IhOUDZPRIvVAMI6bP-CK6EOcPIzYB07pBPb1HjzRDzQMLx3FwPCsxafCsdQPP9Ssfd0a0g7lvspzmC1nZsEoE6XeVEq-qa5j3SM1CmfKG1gCVqWd6gZ2WIXtNYJ0x9EPSZ07q9PdaDrHF7SzV2rdw5PaMcgrfMXOxzQf_IrPsyx5S20lFeEVDxelI-k4ZMAFaXJKQRHWxNH8zl96syVv7pN9E8hru9E8By87B2gZrKL5eCugtQDmSfU diff --git a/DemoRunner/DemoRunner.csproj b/DemoRunner/DemoRunner.csproj index 1e670320d5da6e923c6e02273e721b38128fafd4..ac77d58532b65d9b7ac13e46f81cec6b37a0afe4 100644 --- a/DemoRunner/DemoRunner.csproj +++ b/DemoRunner/DemoRunner.csproj @@ -10,11 +10,11 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="FitConnect" Version="0.0.1-beta.2"/> - <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0"/> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0"/> - <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0"/> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0"/> + <PackageReference Include="FitConnect" Version="0.0.1-beta.2" /> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> </ItemGroup> <ItemGroup> @@ -42,7 +42,7 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> + <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> </ItemGroup> </Project> diff --git a/DemoRunner/SubscriberDemo.cs b/DemoRunner/SubscriberDemo.cs index 16047cf3e0a8c34eee0900eedecdf8899580ecd6..5a8c96bd75a1f464731e43a92f13e9932fa9f6ee 100644 --- a/DemoRunner/SubscriberDemo.cs +++ b/DemoRunner/SubscriberDemo.cs @@ -30,14 +30,19 @@ public static class SubscriberDemo { var submissions = subscriber.GetAvailableSubmissions(); - foreach (var submission in submissions) { - var subscriberWithSubmission = subscriber.RequestSubmission(submission.SubmissionId); - var attachments = subscriberWithSubmission - .GetAttachments(); - - logger.LogInformation("Fachdaten: {Data}", subscriberWithSubmission.GetDataJson()); - subscriberWithSubmission - .AcceptSubmission(); - } + foreach (var submission in submissions) + try { + var subscriberWithSubmission = + subscriber.RequestSubmission(submission.Id); + var attachments = subscriberWithSubmission + .GetAttachments(); + + logger.LogInformation("Fachdaten: {Data}", subscriberWithSubmission.GetDataJson()); + subscriberWithSubmission + .AcceptSubmission(); + } + catch (Exception e) { + logger.LogError(e, "Fehler beim Abrufen der Einreichung"); + } } } diff --git a/Documentation/documentation.de-DE.md b/Documentation/documentation.de-DE.md index 24fb1aab9576d4eff7efe71eb64cb18695d72a76..d6e5cb5d0dc561dc6bba94c2a290977932ca0a8a 100644 --- a/Documentation/documentation.de-DE.md +++ b/Documentation/documentation.de-DE.md @@ -1,3 +1,6 @@ +<br /> +<div align="center"><img src="https://www.fitko.de/fileadmin/_processed_/b/9/csm_FIT-Connect_3e8e926015.jpg" alt="Logo" width="50%" height="50%"> </div> + # FIT-Connect .NET SDK User Guide ## Einleitung @@ -8,13 +11,25 @@ Das FIT-Connect .NET SDK bietet eine einfache Möglichkeit, sowohl einen Antrags ### OSX +#### Apple M1 Chip +__Anmerkung:__ +Bei einem Mac mit einem M1 chip, muss brew in der _x86-64_-Version installiert werden. + +_Die Version x86_64 kann parallel zur arm64 Version installiert werden._ + +```sh +arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +#### Apple Intel Chip + Auf OSX wird das SDK nur dann unterstützt, wenn OpenSSL auf dem System installiert ist. ```sh brew install openssl@1.1 ``` -Die Environment-Variable ```DYLD_LIBRARY_PATH``` muss auf den Pfad zu OpenSSL verweisen. +Die Environment-Variable `DYLD_LIBRARY_PATH` muss auf den Pfad zu OpenSSL verweisen. _Beispiele:_ diff --git a/E2ETest/EndToEndTestBase.cs b/E2ETest/EndToEndTestBase.cs index d4bd425041264b583e5895cf9a3288c9e740cb86..4052e4d4985699624cb5be89e707846f677134eb 100644 --- a/E2ETest/EndToEndTestBase.cs +++ b/E2ETest/EndToEndTestBase.cs @@ -14,22 +14,21 @@ public abstract class EndToEndTestBase { protected MockSettings Settings = null!; protected ISubscriber Subscriber = null!; - [OneTimeSetUp] + [SetUp] public void Setup() { var container = Container.Create(); Settings = container.Resolve<MockSettings>(); Logger = LoggerFactory.Create( builder => { - builder.AddSimpleConsole(); - builder.SetMinimumLevel(LogLevel.Information); + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Debug); }).CreateLogger("E2E Test"); Sender = Client.GetSender(FitConnectEnvironment.Testing, Settings.SenderClientId, Settings.SenderClientSecret, Logger); - Subscriber = Client.GetSubscriber(FitConnectEnvironment.Testing, Settings.SubscriberClientId, Settings.SubscriberClientSecret, Settings.PrivateKeyDecryption, diff --git a/E2ETest/RejectSubmissionTest.cs b/E2ETest/RejectSubmissionTest.cs index 4ee630072f9cd19fb1ffe863055a25a2adac1cc7..064aa666f119d440c3d7a12c02472674495cb503 100644 --- a/E2ETest/RejectSubmissionTest.cs +++ b/E2ETest/RejectSubmissionTest.cs @@ -6,8 +6,8 @@ using Microsoft.Extensions.Logging; namespace E2ETest; public class RejectSubmissionTest : EndToEndTestBase { - private string? _caseId; - private string? _submissionId; + private string _caseId = null!; + private string _submissionId = null!; [Order(10)] [Test] @@ -18,8 +18,8 @@ public class RejectSubmissionTest : EndToEndTestBase { .WithData(@"{""data"":""value""}") .Submit(); - _caseId = submission.CaseId; - _submissionId = submission.Id; + _caseId = submission.CaseId!; + _submissionId = submission.Id!; _caseId.Should().NotBeNull(); _submissionId.Should().NotBeNull(); @@ -29,7 +29,7 @@ public class RejectSubmissionTest : EndToEndTestBase { [Order(20)] public void Sender_GetSubmissionState() { // Act - var status = Sender.GetStatusForSubmission(_caseId); + var status = Sender.GetStatusForSubmission(_caseId, Settings.DestinationId); // Assert status.Count.Should().BeGreaterThan(0); @@ -41,7 +41,7 @@ public class RejectSubmissionTest : EndToEndTestBase { [Order(30)] public void Subscriber_GetSubmissionState() { // Act - var status = Subscriber.GetStatusForSubmission(_caseId); + var status = Subscriber.GetStatusForSubmission(_caseId, Settings.DestinationId); // Assert status.Count.Should().BeGreaterThan(0); @@ -62,16 +62,17 @@ public class RejectSubmissionTest : EndToEndTestBase { var attachments = subscriberWithSubmission.GetAttachments(); attachments.First().Filename.Should().Be("Test.pdf"); - subscriberWithSubmission.RejectSubmission(Problems.SchemaViolation, - new Problems { Detail = "A critical problem" }); + subscriberWithSubmission.RejectSubmission(Problems.SchemaViolation + //,new Problems { Detail = "A critical problem" } + ); } [Test] [Order(50)] public void Sender_GetSubmissionState_AfterRejecting() { // Act - var status = Sender.GetStatusForSubmission(_caseId); - + var status = Sender.GetStatusForSubmission(_caseId, Settings.DestinationId); + // Assert status.Count.Should().BeGreaterThan(0); status.ForEach( @@ -80,6 +81,6 @@ public class RejectSubmissionTest : EndToEndTestBase { var rejection = status.First(s => s.EventType == EventType.Reject); rejection.Problems.Should().NotBeNull(); - rejection.Problems?.ForEach(p => Logger.LogWarning(p.Detail)); + rejection.Problems?.ForEach(p => Logger.LogWarning(p.detail)); } } diff --git a/E2ETest/StraightForwardTest.cs b/E2ETest/StraightForwardTest.cs index 2826a3f7b3fb0a1366e471872ce4706447b3fb6a..19a3000b38c5d3a3d60fa82ef2dfff4c3d6f50e4 100644 --- a/E2ETest/StraightForwardTest.cs +++ b/E2ETest/StraightForwardTest.cs @@ -5,8 +5,8 @@ using Microsoft.Extensions.Logging; namespace E2ETest; public class StraightForwardTest : EndToEndTestBase { - private string? _caseId; - private string? _submissionId; + private string _caseId = null!; + private string _submissionId = null!; [Order(10)] [Test] @@ -17,8 +17,8 @@ public class StraightForwardTest : EndToEndTestBase { .WithData(@"{""data"":""value""}") .Submit(); - _caseId = submission.CaseId; - _submissionId = submission.Id; + _caseId = submission.CaseId!; + _submissionId = submission.Id!; _caseId.Should().NotBeNull(); _submissionId.Should().NotBeNull(); @@ -28,7 +28,7 @@ public class StraightForwardTest : EndToEndTestBase { [Order(20)] public void Sender_GetSubmissionState() { // Act - var status = Sender.GetStatusForSubmission(_caseId); + var status = Sender.GetStatusForSubmission(_caseId, Settings.DestinationId); // Assert status.Count.Should().BeGreaterThan(0); @@ -40,14 +40,14 @@ public class StraightForwardTest : EndToEndTestBase { [Order(30)] public void Subscriber_GetSubmissionState() { // Act - var status = Subscriber.GetStatusForSubmission(_caseId); + var status = Subscriber.GetStatusForSubmission(_caseId, Settings.DestinationId); // Assert status.Count.Should().BeGreaterThan(0); status.ForEach( s => Logger.LogInformation("Status {When} {Event}", s.EventTime, s.EventType)); } - + [Test] [Order(40)] public void RequestSubmission() { @@ -68,7 +68,7 @@ public class StraightForwardTest : EndToEndTestBase { [Order(50)] public void Sender_GetSubmissionState_AfterAccepting() { // Act - var status = Sender.GetStatusForSubmission(_caseId); + var status = Sender.GetStatusForSubmission(_caseId, Settings.DestinationId); // Assert status.Count.Should().BeGreaterThan(0); diff --git a/EncryptionTests/EncryptionTests.csproj b/EncryptionTests/EncryptionTests.csproj index 075edba167f38f1e82e563b1ade734453e403914..86c48ff27f44f56c75667c55b9446ac99c37e00a 100644 --- a/EncryptionTests/EncryptionTests.csproj +++ b/EncryptionTests/EncryptionTests.csproj @@ -10,22 +10,22 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="FluentAssertions" Version="6.7.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> - <PackageReference Include="NUnit" Version="3.13.2" /> - <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> - <PackageReference Include="coverlet.collector" Version="3.1.0" /> + <PackageReference Include="FluentAssertions" Version="6.7.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0"/> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0"/> + <PackageReference Include="NUnit" Version="3.13.2"/> + <PackageReference Include="NUnit3TestAdapter" Version="4.0.0"/> + <PackageReference Include="coverlet.collector" Version="3.1.0"/> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> - <ProjectReference Include="..\MockContainer\MockContainer.csproj" /> + <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> + <ProjectReference Include="..\MockContainer\MockContainer.csproj"/> </ItemGroup> <ItemGroup> - <Folder Include="Certificates" /> + <Folder Include="Certificates"/> </ItemGroup> <ItemGroup> diff --git a/EncryptionTests/FileEncryptionTest.cs b/EncryptionTests/FileEncryptionTest.cs index 457630b02e541c551ac5f81a6a34c3133a7543ca..b81d8f394e6fbdd1c53a3cda92fd4049690df698 100644 --- a/EncryptionTests/FileEncryptionTest.cs +++ b/EncryptionTests/FileEncryptionTest.cs @@ -10,13 +10,13 @@ using NUnit.Framework; namespace SenderTest; public class FileEncryptionTest { - private string _encryptedFile; - private FitEncryption _encryption; - private byte[] sourceFile = null!; + private string _encryptedFile = null!; + private FitEncryption _encryption = null!; + private byte[] _sourceFile = null!; [SetUp] public void Setup() { - sourceFile = RandomNumberGenerator.GetBytes(4096); + _sourceFile = RandomNumberGenerator.GetBytes(4096); var container = Container.Create(); var keySet = container.Resolve<KeySet>(); _encryption = new FitEncryption(keySet, null); @@ -25,7 +25,7 @@ public class FileEncryptionTest { [Test] [Order(10)] public void EncryptFile() { - _encryptedFile = _encryption.Encrypt(sourceFile); + _encryptedFile = _encryption.Encrypt(_sourceFile); } [Test] diff --git a/EncryptionTests/JweTest.cs b/EncryptionTests/JweTest.cs index 207a5689f55a99833542c8673b16220fc5b18dcc..73beadb512ed5f5fa443a0a8ed0bec5eab34b0d2 100644 --- a/EncryptionTests/JweTest.cs +++ b/EncryptionTests/JweTest.cs @@ -13,10 +13,10 @@ using NUnit.Framework; namespace SenderTest; public class JweTest { - private IContainer _container; - private ILogger<JweTest> _logger; - private Sender _sender; - private MockSettings _settings; + private IContainer _container = null!; + private ILogger<JweTest> _logger = null!; + private Sender _sender = null!; + private MockSettings _settings = null!; [SetUp] diff --git a/FitConnect/Encryption/CertificateHelper.cs b/FitConnect/Encryption/CertificateHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..92a6abee65fdc52db579ca7ef3060722d8464459 --- /dev/null +++ b/FitConnect/Encryption/CertificateHelper.cs @@ -0,0 +1,150 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.Unicode; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Win32.SafeHandles; +using Newtonsoft.Json; + +namespace FitConnect.Encryption; + +public class CertificateHelper { + private readonly ILogger? _logger; + + public CertificateHelper(ILogger? logger = null) { + _logger = logger; + } + + internal bool ValidateCertificate(string keyJson, LogLevel logLevel, + X509Certificate2[]? rootCertificate = null) => + ValidateCertificate(new JsonWebKey(keyJson), logLevel, rootCertificate); + + internal bool ValidateCertificate(X509Certificate2 certificate, + out X509ChainStatus[] chainStatus, + X509Certificate2[]? rootCertificate = null, + LogLevel logLevel = LogLevel.Warning) { + var certificateChain = new X509Chain(); + +// certificate.ExportToPem($"./temp/{Guid.NewGuid().ToString()}"); + _logger?.LogDebug("Issuers: {Issuer}", certificate.Issuer); + + if (rootCertificate != null) { + certificateChain.ChainPolicy.CustomTrustStore.AddRange(rootCertificate); + certificateChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.Offline; + certificateChain.ChainPolicy.RevocationFlag = X509RevocationFlag.EndCertificateOnly; + _logger?.LogDebug("Using custom root certificate"); + } + else { + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + certificateChain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain; + certificateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; + } + + + var result = certificateChain.Build(certificate); + + chainStatus = certificateChain.ChainStatus + .Where(s => s.Status != X509ChainStatusFlags.PartialChain).ToArray(); + + var statusAggregation = certificateChain.ChainStatus.Aggregate("", + (r, s) => r + "\n\t - " + s.Status + ": " + s.StatusInformation); + + if (certificateChain.ChainStatus.Length > 0) { + _logger?.Log(logLevel, "Certificate status: {ObjStatusInformation}", + statusAggregation); + // _logger?.Log(logLevel, "Chain status: {ChainStatus}", certificateChain.ToDisplayString()); + } + + return result; + } + + internal bool ValidateCertificate(JsonWebKey key, LogLevel logLevel = LogLevel.Error, + X509Certificate2[]? root = null) { + var certificates = key.X5c.Select(s => new X509Certificate2(Convert.FromBase64String(s))) + .ToList(); + + // if (certificates.Count != 3) { + // _logger?.Log(logLevel, "Found {Count} certificate(s) but should be 3", + // certificates.Count); + // return false; + // } + // root ??= new X509Certificate2(Convert.FromBase64String(key.X5t)); + + var valid = certificates.Aggregate(true, + (result, cert) => result + && ValidateCertificate(cert, out _, root, logLevel) + // && cert.Verify() + ); + return valid; + } +} + +public static class X509Certificate2Extensions { + public static void ExportToPem(this X509Certificate2 certificate, string fileName) { + StringBuilder builder = new StringBuilder(); + builder.AppendLine("-----BEGIN CERTIFICATE-----"); + builder.AppendLine( + Convert.ToBase64String(certificate.RawData, Base64FormattingOptions.InsertLineBreaks)); + builder.AppendLine("-----END CERTIFICATE-----"); + var content = builder.ToString(); + + File.WriteAllText(fileName + ".pem", content); + File.WriteAllText(fileName + ".json", JsonConvert.SerializeObject(certificate)); + } + + public static string ToDisplayString(this X509Chain chain) { + var b = new StringBuilder(); + + b.AppendLine($"[{nameof(chain.ChainPolicy.RevocationFlag)}]"); + b.AppendLine($" {chain.ChainPolicy.RevocationFlag}"); + b.AppendLine(); + b.AppendLine($"[{nameof(chain.ChainPolicy.RevocationMode)}]"); + b.AppendLine($" {chain.ChainPolicy.RevocationMode}"); + b.AppendLine(); + b.AppendLine($"[{nameof(chain.ChainPolicy.VerificationFlags)}]"); + b.AppendLine($" {chain.ChainPolicy.VerificationFlags}"); + b.AppendLine(); + b.AppendLine($"[{nameof(chain.ChainPolicy.VerificationTime)}]"); + b.AppendLine($" {chain.ChainPolicy.VerificationTime}"); + b.AppendLine(); + b.AppendLine($"[Application Policy]"); + foreach (var policy in chain.ChainPolicy.ApplicationPolicy) { + b.AppendLine($" {policy.ToDisplayString()}"); + } + + b.AppendLine(); + b.AppendLine($"[Certificate Policy]"); + foreach (var policy in chain.ChainPolicy.CertificatePolicy) { + b.AppendLine($" {policy.ToDisplayString()}"); + } + + b.AppendLine(); + b.AppendLine($"[Elements]"); + foreach (var (element, index) in chain.ChainElements.Cast<X509ChainElement>() + .Select((element, index) => (element, index))) { + b.AppendLine(); + b.AppendLine($"[Element {index + 1}]"); + b.AppendLine(); + b.Append(element.Certificate.ToString()); + b.AppendLine(); + b.AppendLine($"[Status]"); + foreach (var status in element.ChainElementStatus) { + b.AppendLine($" {status.ToDisplayString()}"); + } + } + + return b.ToString(); + } + + public static string ToDisplayString(this Oid oid) { + return oid.FriendlyName == oid.Value + ? $"{oid.Value}" + : $"{oid.FriendlyName}: {oid.Value}"; + } + + public static string ToDisplayString(this X509ChainStatus status) { + return $"{status.Status}: {status.StatusInformation}"; + } +} diff --git a/FitConnect/Encryption/FitEncryption.cs b/FitConnect/Encryption/FitEncryption.cs index e337765e8054ac21b1787c52ee7ca00d9a0dea7e..58e68f580483a2b5df4f496f02e622429e50a1c5 100644 --- a/FitConnect/Encryption/FitEncryption.cs +++ b/FitConnect/Encryption/FitEncryption.cs @@ -1,9 +1,10 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using System.Text; +using FitConnect.Models; using FitConnect.Models.v1.Api; +using FitConnect.Services.Models.v1.Submission; using IdentityModel; -using Jose; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; @@ -20,7 +21,11 @@ public class KeySet { public class FitEncryption { private readonly IEncryptor _encryptor = new JoseEncryptor(); private readonly ILogger? _logger; - private JwtHeader _jwtHeader; + private JwtHeader? _jwtHeader; + public string? PrivateKeyDecryption { get; set; } + public string? PrivateKeySigning { get; set; } + public string? PublicKeyEncryption { get; set; } + public string? PublicKeySignatureVerification { get; set; } internal FitEncryption(ILogger? logger) { _logger = logger; @@ -44,10 +49,6 @@ public class FitEncryption { PublicKeySignatureVerification = keySet.PublicKeySignatureVerification; } - public string? PrivateKeyDecryption { get; set; } - public string? PrivateKeySigning { get; set; } - public string? PublicKeyEncryption { get; set; } - public string? PublicKeySignatureVerification { get; set; } public (string plainText, byte[] plainBytes, byte[] tag) Decrypt(string cypherText, string key) { @@ -84,51 +85,119 @@ public class FitEncryption { return Encrypt(plain, PublicKeyEncryption); } - public string CreateSecurityEventToken(string submissionId, + public string CreateRejectSecurityEventToken(string submissionId, + string caseId, + string destinationId, Problems[]? problemsArray) { + return CreateSecurityEventToken(submissionId, caseId, destinationId, + "https://schema.fitko.de/fit-connect/events/reject-submission", + new { problems = problemsArray }); + } + + private dynamic GenerateAuthenticationTags(Submission submission) { + _logger?.LogInformation("Generating authentication tags"); + var attachmentDictionary = new Dictionary<string, string>(); + submission.Attachments.ForEach(a => + attachmentDictionary.Add(a.Id, a.AttachmentAuthentication!)); + + if (submission.Data != null) + return new { + authenticationTags = new { + data = submission.DataAuthentication, + metadata = submission.MetaAuthentication, + attachments = attachmentDictionary + } + }; + return new { + authenticationTags = new { + metadata = submission.MetaAuthentication, + attachments = attachmentDictionary + } + }; + } + + + public string CreateAcceptSecurityEventToken(SubmissionForPickupDto submission, + dynamic? payload = null) { + if (submission?.Id == null || submission?.CaseId == null || + submission?.DestinationId == null) { + throw new ArgumentException("SubmissionId, CaseId and DestinationId are required"); + } + + return CreateSecurityEventToken(submission.Id, submission.CaseId, + submission.DestinationId, + "https://schema.fitko.de/fit-connect/events/accept-submission", null); + } + + + public string CreateAcceptSecurityEventToken(Submission submission) { + var payload = GenerateAuthenticationTags(submission); + return CreateSecurityEventToken(submission.Id, submission.CaseId, + submission.DestinationId, + "https://schema.fitko.de/fit-connect/events/accept-submission", payload); + } + + + internal string CreateSecurityEventToken(string submissionId, string caseId, string destinationId, - string eventName, Problems[]? problemsArray) { - var signingKey = Jwk.FromJson(PrivateKeySigning, new JsonMapper()); + string eventName, object? content) { + var signingKey = new JsonWebKey(PrivateKeySigning); var transactionId = "case:" + caseId; var subject = "submission:" + submissionId; _jwtHeader = - new JwtHeader(new SigningCredentials(new JsonWebKey(PrivateKeySigning), "PS512")) { - //{ "typ", "secevent+jwt" }, - // { "kid", signingKey.KeyId }, - // { "alg", "PS512" } - }; + new JwtHeader(new SigningCredentials(signingKey, "PS512")); - object problems = problemsArray == null - ? new { } - : new { problems = problemsArray }; _jwtHeader["typ"] = "secevent+jwt"; + var jwtPayload = new JwtPayload { + { "sub", subject }, + { "jti", Guid.NewGuid().ToString() }, + { "iat", DateTime.UtcNow.ToEpochTime() }, + { "iss", destinationId }, { + "events", + new Dictionary<string, object> { + { eventName, content ?? new { } } + } + }, + { "txn", transactionId } + }; + + if (content != null) + jwtPayload.Add("$schema", + "https://schema.fitko.de/fit-connect/set-payload/1.0.0/set-payload.schema.json"); + var token = new JwtSecurityToken( - _jwtHeader, new JwtPayload { - { "sub", subject }, - { "jti", Guid.NewGuid().ToString() }, - { "iat", DateTime.UtcNow.ToEpochTime() }, - { "iss", destinationId }, { - "events", - new Dictionary<string, object> { - { eventName, problems } - } - }, - { "txn", transactionId } - }); + _jwtHeader, jwtPayload) { + SigningKey = signingKey + }; var handler = new JwtSecurityTokenHandler(); + var tokenString = handler.WriteToken(token); + + if (content != null) { + var jsonData = Base64UrlEncoder.Decode(tokenString.Split('.')[1]); + if (!JsonHelper.ValidateJsonSchema( + new Uri( + "https://schema.fitko.de/fit-connect/set-payload/1.0.0/set-payload.schema.json"), + jsonData + )) { + throw new Exception("Invalid token schema for \n" + jsonData); + } + } - - return handler.WriteToken(token); + return tokenString; } public static string CalculateHash(string data) { return ByteToHexString(SHA512.Create().ComputeHash(Encoding.UTF8.GetBytes(data))); } + public static string CalculateHash(byte[] data) { + return ByteToHexString(SHA512.Create().ComputeHash(data)); + } + private static string ByteToHexString(IEnumerable<byte> data) { var sb = new StringBuilder(); foreach (var b in data) sb.Append(b.ToString("x2")); @@ -137,9 +206,25 @@ public class FitEncryption { } + public static bool VerifyJwt(string signature, IEnumerable<JsonWebKey> keys, + ILogger? logger = null) { + foreach (var key in keys) + try { + var valid = VerifyJwt(signature, key, logger); + if (valid) return true; + } + catch (Exception e) { + logger?.LogError(e, "Error verifying JWT"); + } + + return false; + } + public static bool VerifyJwt(string signature, string secret, - ILogger? logger = null) => VerifyJwt(signature, - new JsonWebKey(secret), logger); + ILogger? logger = null) { + return VerifyJwt(signature, + new JsonWebKey(secret), logger); + } public static bool VerifyJwt(string signature, JsonWebKey key, ILogger? logger = null) { @@ -166,16 +251,15 @@ public class FitEncryption { RequireSignedTokens = true, RequireExpirationTime = false, IgnoreTrailingSlashWhenValidatingAudience = true, - TryAllIssuerSigningKeys = true, + TryAllIssuerSigningKeys = true }); var error = result.Exception?.Message; if (error != null) logger?.LogWarning("Validation error {Error}", error); - else { + else logger?.LogInformation("JWT is valid"); - } return result.IsValid; } diff --git a/FitConnect/Encryption/JoseEncryptor.cs b/FitConnect/Encryption/JoseEncryptor.cs index ef6bd4c98786dea596ea7b6adb3d18681581401b..da813bd4149b36b2b8987903adbe1f719277db40 100644 --- a/FitConnect/Encryption/JoseEncryptor.cs +++ b/FitConnect/Encryption/JoseEncryptor.cs @@ -1,10 +1,4 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Principal; -using System.Text; using Jose; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; // ReSharper disable RedundantExplicitArrayCreation diff --git a/FitConnect/Encryption/JsonHelper.cs b/FitConnect/Encryption/JsonHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..ba567b42995a610ae0a5cc95b450e06749ecb2cd --- /dev/null +++ b/FitConnect/Encryption/JsonHelper.cs @@ -0,0 +1,47 @@ +using System.Reflection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Schema; + +namespace FitConnect.Encryption; + +public static class JsonHelper { + public static bool ValidateJsonSchema(string schema, string json, ILogger? logger = null) { + var jSchema = Newtonsoft.Json.Schema.JSchema.Parse(schema); + return JObject.Parse(json).IsValid(jSchema); + } + + /// <summary> + /// Verify the metadata hash and content to fit the schema + /// </summary> + /// <param name="metadataString"></param> + /// <param name="logger">Optional logger, to print errors to logger</param> + /// <returns></returns> + public static bool VerifyMetadata(string metadataString, ILogger? logger = null) { + var schemaString = LoadContentOfResource("metadata.schema.json"); + var schema = NJsonSchema.JsonSchema.FromJsonAsync(schemaString).Result; + var result = schema.Validate(metadataString).ToList(); + logger?.LogWarning("Validation failed with {Errors}", + result.Aggregate("", (a, b) => a + "\n\t" + b)); + return result.Count == 0; + } + + + private static string LoadContentOfResource(string resourceName) { + var assembly = Assembly.GetExecutingAssembly(); + var fullQualifiedName = $"{assembly.GetName().Name}.{resourceName}"; + var resourceStream = assembly.GetManifestResourceStream(fullQualifiedName); + if (resourceStream == null) + throw new Exception($"Resource {fullQualifiedName} not found"); + var reader = new StreamReader(resourceStream); + return reader.ReadToEnd(); + } + + public static bool ValidateJsonSchema(Uri schema, string jsonData) { + var schemaResponse = new HttpClient() + .GetAsync(schema) + .Result; + + return ValidateJsonSchema(schemaResponse.Content.ReadAsStringAsync().Result, jsonData); + } +} diff --git a/FitConnect/FitConnectClient.cs b/FitConnect/FitConnectClient.cs index 0348804c8289987b9625b8ff167a4da02073a832..6e348149fbdfb4e664e36734a0fcadafd980a5b7 100644 --- a/FitConnect/FitConnectClient.cs +++ b/FitConnect/FitConnectClient.cs @@ -4,7 +4,9 @@ using FitConnect.Encryption; using FitConnect.Models; using FitConnect.Services; using FitConnect.Services.Interfaces; +using FitConnect.Services.Models.v1.Submission; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; namespace FitConnect; @@ -13,6 +15,7 @@ public abstract class FitConnectClient { private readonly string? _privateKeySigning; private readonly string? _publicKeyEncryption; private readonly string? _publicKeySignatureVerification; + protected readonly bool VerifiedKeysAreMandatory; protected FitConnectClient(FitConnectEnvironment environment, @@ -29,12 +32,17 @@ public abstract class FitConnectClient { _publicKeySignatureVerification = publicKeySignatureVerification; OAuthService = new OAuthService(environment.TokenUrl, "V1", clientId, clientSecret, logger); SubmissionService = - new SubmissionService(environment.SubmissionUrl[0], OAuthService, logger: logger); + new SubmissionService(environment.SubmissionUrl[0], OAuthService, + publicKeySignatureVerification == null + ? null + : new JsonWebKey(publicKeySignatureVerification), + logger: logger); RouteService = new RouteService(environment.RoutingUrl, logger: logger); CasesService = new CasesService(environment.SubmissionUrl[0], OAuthService, logger: logger); DestinationService = new DestinationService(environment.SubmissionUrl[0], OAuthService, logger: logger); Logger = logger; + VerifiedKeysAreMandatory = environment.VerifiedKeysAreMandatory; } protected FitConnectClient(FitConnectEnvironment environment, @@ -59,16 +67,20 @@ public abstract class FitConnectClient { private FitConnectClient() { + // Take care of OAuthService if make the constructor public Encryption = new FitEncryption(Logger); } - public IOAuthService OAuthService { get; } - public ISubmissionService SubmissionService { get; } - public IRouteService RouteService { get; } - public IDestinationService DestinationService { get; } - public ICasesService CasesService { get; } - public ILogger? Logger { get; } - public FitEncryption Encryption { get; set; } + + public IOAuthService OAuthService { get; } = + null!; // To resolve the warning, is only applicable for the **private** constructor + + protected ISubmissionService SubmissionService { get; } = null!; + protected IRouteService RouteService { get; } = null!; + protected IDestinationService DestinationService { get; } = null!; + protected ICasesService CasesService { get; } = null!; + protected ILogger? Logger { get; } + protected FitEncryption Encryption { get; set; } public void SetProxy(WebProxy proxy) { OAuthService.Proxy = proxy; @@ -86,10 +98,34 @@ public abstract class FitConnectClient { /// Retrieve the events for the submission /// </summary> /// <param name="caseId"></param> + /// <param name="destinationId"></param> + /// <param name="skipTest"></param> + /// <returns></returns> + public List<SecurityEventToken> GetStatusForSubmission(string caseId, string destinationId, + bool skipTest = false) { + var events = SubmissionService.GetStatusForSubmissionAsync(caseId, destinationId, skipTest) + .Result?.Select(e => new SecurityEventToken(e!)).ToList() ?? + new List<SecurityEventToken>(); + return events; + } + + /// <summary> + /// Retrieve the events for the submission + /// </summary> + /// <param name="submission"></param> + /// <param name="skipTest"></param> /// <returns></returns> - public List<SecurityEventToken> GetStatusForSubmission(string caseId) { - var events = SubmissionService.GetStatusForSubmissionAsync(caseId).Result; - return events.Select(e => new SecurityEventToken(e)).ToList(); + public List<SecurityEventToken> GetStatusForSubmission(SubmissionForPickupDto submission, + bool skipTest = false) { + if (submission?.CaseId == null || submission.DestinationId == null) { + throw new ArgumentNullException(nameof(submission)); + } + + var events = SubmissionService + .GetStatusForSubmissionAsync(submission.CaseId, submission.DestinationId, skipTest) + .Result; + return events?.Select(e => new SecurityEventToken(e)).ToList() ?? + new List<SecurityEventToken>(); } /// <summary> @@ -99,7 +135,7 @@ public abstract class FitConnectClient { /// <param name="attachment"></param> /// <returns></returns> public KeyValuePair<string, string> Encrypt(string publicKey, Attachment attachment) { - var content = Encryption.Encrypt(attachment.Content, publicKey); + var content = Encryption.Encrypt(attachment.Content ?? Array.Empty<byte>(), publicKey); return new KeyValuePair<string, string>(attachment.Id, content); } diff --git a/FitConnect/HttpCalls/OAuthToken.http b/FitConnect/HttpCalls/OAuthToken.http index 2f94b0f5eb0d9af3cb0100c270efe7d175242ba5..171634d06b336d2a773c57cf5f21fe2c7a8c76ca 100644 --- a/FitConnect/HttpCalls/OAuthToken.http +++ b/FitConnect/HttpCalls/OAuthToken.http @@ -1,4 +1,4 @@ -### Getting the data from the database BEARER +### SENDER Getting the data from the database BEARER POST {{oauth_api_url}}/token Content-Type: application/x-www-form-urlencoded Accept: application/json @@ -10,3 +10,15 @@ grant_type=client_credentials&client_id={{senderId}}&client_secret={{senderSecre // client.global.scope = response.body['scope'] %} +### SUBSCRIBER Getting the data from the database BEARER +POST {{oauth_api_url}}/token +Content-Type: application/x-www-form-urlencoded +Accept: application/json + +grant_type=client_credentials&client_id={{subscriberId}}&client_secret={{subscriberSecret}} + +> {% + client.global.set("token", response.body.access_token); + // client.global.scope = response.body['scope'] + %} + diff --git a/FitConnect/HttpCalls/SenderCalls.http b/FitConnect/HttpCalls/SenderCalls.http index 57695ee4231e76bfee2aa9e4e4680f911f97789b..5da29a439a652900654d2e140b711c64e9df7518 100644 --- a/FitConnect/HttpCalls/SenderCalls.http +++ b/FitConnect/HttpCalls/SenderCalls.http @@ -90,6 +90,12 @@ Content-Type: application/json Authorization: Bearer {{token}} +### Get well known keys in Production +GET https://submission-api-prod.fit-connect.niedersachsen.de/.well-known/jwks.json +Accept: application/json +Content-Type: application/json + + ### GET Destination from DVDV GET https://dvdv-testsystem.governikus.de/dvdv-fitconnect/v1/destinations/{{destinationId}} Accept: application/json diff --git a/FitConnect/HttpCalls/StaticTests.http b/FitConnect/HttpCalls/StaticTests.http index 7cfb63e4e49b90c1728b4fd4ea41f90c147bb67a..51a5a1010ca78ef31717310a5dd5b6d903ebdc59 100644 --- a/FitConnect/HttpCalls/StaticTests.http +++ b/FitConnect/HttpCalls/StaticTests.http @@ -12,6 +12,13 @@ grant_type=client_credentials&client_id={{senderId}}&client_secret={{senderSecre +### Submit submission +GET {{submission_api_url}}/v1/submissions/d63ee290-8dd0-4c78-a764-57126e7a873f +Accept: application/json +Content-Type: application/json +Authorization: Bearer {{token}} + + ### Submit submission PUT {{submission_api_url}}/v1/submissions/d698150c-0792-4be7-aa42-580fb0774f02 Accept: application/json @@ -33,3 +40,8 @@ Content-Type: application/json GET https://dvdv-testsystem.governikus.de/dvdv-fitconnect/v1/destinations/d40e7b13-da98-4b09-9e16-bbd61ca81510 Accept: application/json Content-Type: application/json + +### Get Json SET Schema +GET https://schema.fitko.de/fit-connect/set-payload/1.0.0/set-payload.schema.json +Accept: application/json +Content-Type: application/json diff --git a/FitConnect/Interfaces/IFitConnectClient.cs b/FitConnect/Interfaces/IFitConnectClient.cs new file mode 100644 index 0000000000000000000000000000000000000000..d27dc7f9f2c5e898ae05859e98f45f6a68c45c3b --- /dev/null +++ b/FitConnect/Interfaces/IFitConnectClient.cs @@ -0,0 +1,27 @@ +using FitConnect.Models; +using FitConnect.Services.Models.v1.Submission; + +namespace FitConnect.Interfaces; + +public interface IFitConnectClient { + // public List<SecurityEventToken> GetStatusForSubmission(string caseId, bool skipTest = false); + + /// <summary> + /// Receives the SecurityEventTokens for a given case + /// </summary> + /// <param name="caseId">ID of the case</param> + /// <param name="destinationId">Skips signature validation if null!</param> + /// <param name="skipTest">Specifies if the SET signature test is skipped</param> + /// <returns>List of the <see cref="SecurityEventToken" /></returns> + public List<SecurityEventToken> GetStatusForSubmission(string caseId, string destinationId, + bool skipTest = false); + + /// <summary> + /// + /// </summary> + /// <param name="submission"></param> + /// <param name="skipTest"></param> + /// <returns></returns> + public List<SecurityEventToken> GetStatusForSubmission(SubmissionForPickupDto submission, + bool skipTest = false); +} diff --git a/FitConnect/Interfaces/Sender/ISender.cs b/FitConnect/Interfaces/Sender/ISender.cs index 4f481f3578d80aa8421e854532030da91e15caf7..cbd6bfa5d0b8e8a31475276b4a42814f36fa5cc6 100644 --- a/FitConnect/Interfaces/Sender/ISender.cs +++ b/FitConnect/Interfaces/Sender/ISender.cs @@ -1,9 +1,7 @@ -using FitConnect.Interfaces.Subscriber; - namespace FitConnect.Interfaces.Sender; public interface ISender : IFitConnectClient { - public string PublicKey { get; } + public string? PublicKey { get; } /// <summary> /// Determines the destination id and configures the sender. diff --git a/FitConnect/Interfaces/Sender/ISenderWithAttachments.cs b/FitConnect/Interfaces/Sender/ISenderWithAttachments.cs index 073048275129459bf5485962d2759d684ae4e71c..d25ea83e297998541a8f27b64d5a39eeffb3103b 100644 --- a/FitConnect/Interfaces/Sender/ISenderWithAttachments.cs +++ b/FitConnect/Interfaces/Sender/ISenderWithAttachments.cs @@ -1,10 +1,6 @@ -using FitConnect.Models; - namespace FitConnect.Interfaces.Sender; public interface ISenderWithAttachments : ISenderReady { - public Submission Submission { get; } - /// <summary> /// Data as string. /// </summary> diff --git a/FitConnect/Interfaces/Sender/ISenderWithService.cs b/FitConnect/Interfaces/Sender/ISenderWithService.cs index 8eaef30e0d76cd322e309051735d5e07fbff6ad9..2ce1321028967510fc5d2bdcd9441a0215eba562 100644 --- a/FitConnect/Interfaces/Sender/ISenderWithService.cs +++ b/FitConnect/Interfaces/Sender/ISenderWithService.cs @@ -3,8 +3,6 @@ using FitConnect.Models; namespace FitConnect.Interfaces.Sender; public interface ISenderWithService { - public string PublicKey { get; set; } - /// <summary> /// Sends the submission with a list of attachments /// </summary> diff --git a/FitConnect/Interfaces/Subscriber/ISubscriber.cs b/FitConnect/Interfaces/Subscriber/ISubscriber.cs index 7f478989ddacb801680cad62b171c20d1b9633bf..2f3b0520aa008b362a9b4d71cbd4d9953ffc64e6 100644 --- a/FitConnect/Interfaces/Subscriber/ISubscriber.cs +++ b/FitConnect/Interfaces/Subscriber/ISubscriber.cs @@ -20,15 +20,6 @@ public interface ISubscriber : IFitConnectClient { /// <param name="submissionId">unique identifier of a <see cref="Submission" /></param> /// <param name="skipSchemaTest"></param> /// <returns>A subscriber object with a submission</returns> - public ISubscriberWithSubmission RequestSubmission(string? submissionId, + public ISubscriberWithSubmission RequestSubmission(string submissionId, bool skipSchemaTest = false); } - -public interface IFitConnectClient { - /// <summary> - /// Receives the SecurityEventTokens for a given case - /// </summary> - /// <param name="caseId">ID of the case</param> - /// <returns>List of the <see cref="SecurityEventToken" /></returns> - public List<SecurityEventToken> GetStatusForSubmission(string caseId); -} diff --git a/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs b/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs index ada7ea0de673e21ccef1d7432df7815441ffd6b2..c7cd242a8b95c98145a98ff751ddb6a6600719b3 100644 --- a/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs +++ b/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs @@ -1,17 +1,18 @@ using FitConnect.Models; using FitConnect.Models.v1.Api; +using FitConnect.Services.Models; namespace FitConnect.Interfaces.Subscriber; -public interface ISubscriberWithSubmission { - public Submission Submission { get; } +public interface ISubscriberWithSubmission : WithSubmission { + public Submission? Submission { get; } /// <summary> /// Returns the data (Fachdaten) of the submission /// </summary> /// <returns></returns> public string? GetDataJson() { - return Submission.Data; + return Submission?.Data; } /// <summary> @@ -31,10 +32,10 @@ public interface ISubscriberWithSubmission { /// <param name="problems">Reasons for the rejection</param> public void RejectSubmission(params Problems[] problems); - /// <summary> - /// Set submission state to forwarded - /// </summary> - public void ForwardSubmission(); + // /// <summary> + // /// Set submission state to forwarded + // /// </summary> + // public void ForwardSubmission(); /// <summary> /// Set submission state @@ -42,3 +43,12 @@ public interface ISubscriberWithSubmission { /// <param name="status">state the submission has to be set to</param> public void CompleteSubmission(FinishSubmissionStatus status); } + +public interface WithSubmission { + /// <summary> + /// Verifies the submission acceptance status + /// </summary> + /// <param name="acceptanceStatus"></param> + /// <returns></returns> + public bool VerifyStatus(AcceptanceStatus acceptanceStatus); +} diff --git a/FitConnect/Models/Attachment.cs b/FitConnect/Models/Attachment.cs index c33d21d4867987ef6987e69ec08a5ffc90251066..88cdc74a7c7f13e0279dd7f47f13f441c8b8cfa9 100644 --- a/FitConnect/Models/Attachment.cs +++ b/FitConnect/Models/Attachment.cs @@ -5,9 +5,10 @@ using FitConnect.Models.Api.Metadata; namespace FitConnect.Models; public class Attachment { - public Attachment(Api.Metadata.Attachment metadata, byte[] content) { + public Attachment(Api.Metadata.Attachment metadata, byte[] content, string attachmentAuthentication) { Filename = metadata.Filename; Content = content; + AttachmentAuthentication = attachmentAuthentication; MimeType = Path.GetExtension(metadata.Filename) switch { "pdf" => "application/pdf", "xml" => "application/xml", @@ -43,6 +44,7 @@ public class Attachment { public string Id { get; } = Guid.NewGuid().ToString(); public byte[]? Content { get; init; } + public string? AttachmentAuthentication { get; } public string? Hash => CalculateHash(); @@ -57,7 +59,7 @@ public class Attachment { public AttachmentSignature? Signature { get; } private string CalculateHash() { - return ByteToHexString(SHA512.Create().ComputeHash(Content)); + return ByteToHexString(SHA512.Create().ComputeHash(Content ?? Array.Empty<byte>())); } private static string ByteToHexString(IEnumerable<byte> data) { diff --git a/FitConnect/Models/Callback.cs b/FitConnect/Models/Callback.cs index b4d7e94fb7bd1cca3711e9ab5ed288d016b870aa..9e77458289dd2b29cef838f3fe1199b1accbc429 100644 --- a/FitConnect/Models/Callback.cs +++ b/FitConnect/Models/Callback.cs @@ -3,9 +3,8 @@ using FitConnect.Services.Models; namespace FitConnect.Models; public record Callback(string? Url, string? Secret) { - public static explicit operator Callback(CallbackDto dto) { - return new Callback(dto?.Url, null); - } + public static explicit operator Callback(CallbackDto dto) + => new(dto.Url, null); public static explicit operator CallbackDto(Callback model) { return new CallbackDto { Url = model.Url }; diff --git a/FitConnect/Models/FitConnectEnvironment.cs b/FitConnect/Models/FitConnectEnvironment.cs index 104a1e0fc67c567ddb7974318c6b7ee0480656c9..fd377f512e52e9f507a0deb46d3355522337d847 100644 --- a/FitConnect/Models/FitConnectEnvironment.cs +++ b/FitConnect/Models/FitConnectEnvironment.cs @@ -1,29 +1,43 @@ namespace FitConnect.Models; public class FitConnectEnvironment { + // List of Domains + // https://wiki.fit-connect.fitko.dev/de/Betrieb/Dokumentation/Domains + + public static readonly FitConnectEnvironment Develop = new( + "https://auth-dev.fit-connect.fitko.dev/token", + new[] { "https://submission-api-dev.fit-connect.fitko.dev" }, + string.Empty, // "https://routing-api-testing.fit-connect.fitko.dev", // Dev does not have a routing API + "https://portal.auth-dev.fit-connect.fitko.dev" + ) { VerifiedKeysAreMandatory = false }; public static readonly FitConnectEnvironment Testing = new( "https://auth-testing.fit-connect.fitko.dev/token", new[] { "https://submission-api-testing.fit-connect.fitko.dev" }, "https://routing-api-testing.fit-connect.fitko.dev", "https://portal.auth-testing.fit-connect.fitko.dev" - ); + ) { VerifiedKeysAreMandatory = false }; public static readonly FitConnectEnvironment Staging = new( - "https://auth-refz.fit-connect.fitko.dev/token", - new[] { "https://submission-api-refz.fit-connect.fitko.dev" }, - "https://routing-api-refz.fit-connect.fitko.dev", - "https://portal.auth-testing.fit-connect.fitko.dev/.well-known/jwks.json" + "https://auth-refz.fit-connect.fitko.net/token", + new[] { "submission-api-refz.fit-connect.niedersachsen.de" }, + string.Empty, // "https://routing-api-testing.fit-connect.fitko.dev", // Stage does not have a routing API + "https://portal.auth-refz.fit-connect.fitko.net" ); public static readonly FitConnectEnvironment Production = new( - "https://auth.fit-connect.fitko.net/token", - new[] { "https://submission-api.fit-connect.fitko.net" }, - "https://routing-api.fit-connect.fitko.net", - "https://portal.auth-testing.fit-connect.fitko.dev/.well-known/jwks.json" + "https://auth-prod.fit-connect.fitko.net/token", + new[] { "https://submission-api-prod.fit-connect.niedersachsen.de" }, + "https://routing-api-prod.fit-connect.fitko.net", + "https://portal.auth-prod.fit-connect.fitko.net" ); - public FitConnectEnvironment() { + public FitConnectEnvironment(string sspUrl, string tokenUrl, string[] submissionUrl, + string routingUrl) { + SspUrl = sspUrl; + TokenUrl = tokenUrl; + SubmissionUrl = submissionUrl; + RoutingUrl = routingUrl; } /// <summary> @@ -41,9 +55,10 @@ public class FitConnectEnvironment { } /// <summary> - /// Self service portal URL + /// Self service portal URL /// </summary> public string SspUrl { get; } + /// <summary> /// URL for receiving the OAuth token. /// </summary> @@ -59,6 +74,8 @@ public class FitConnectEnvironment { /// </summary> public string RoutingUrl { get; } + public bool VerifiedKeysAreMandatory { get; private init; } = true; + /// <summary> /// Creates the endpoints for the given environment. /// </summary> diff --git a/FitConnect/Models/SecurityEventToken.cs b/FitConnect/Models/SecurityEventToken.cs index 94fba7ac5a4a94f5e7b5b6e7ac6243e42c4ba7c8..ded9ac85a4951dfb2d712cfafece3dba3bfc4f77 100644 --- a/FitConnect/Models/SecurityEventToken.cs +++ b/FitConnect/Models/SecurityEventToken.cs @@ -40,9 +40,13 @@ public class SecurityEventToken { "https://schema.fitko.de/fit-connect/events/delete-submission"; public SecurityEventToken(string jwtEncodedString) { - Token = new JsonWebToken(jwtEncodedString); + TokenString = jwtEncodedString; EventType = DecodeEventType(Token.Claims); - var iat = Token.Claims.FirstOrDefault(c => c.Type == "iat").Value; + + if (Token.Claims.All(c => c.Type != "iat")) + return; + + var iat = Token.Claims.FirstOrDefault(c => c.Type == "iat")!.Value; if (long.TryParse(iat, out var timeEpoch)) EventTime = DateTime.UnixEpoch.AddSeconds(timeEpoch); } @@ -53,7 +57,8 @@ public class SecurityEventToken { public Events? Event { get; set; } public object? Payload { get; set; } - public JsonWebToken Token { get; set; } + public JsonWebToken Token => new JsonWebToken(TokenString); + public string TokenString { get; set; } private EventType DecodeEventType(IEnumerable<Claim> claims) { var eventsClaim = claims.FirstOrDefault(c => c.Type == "events"); @@ -88,6 +93,7 @@ public class SecurityEventToken { if (eventsClaim.Value.Contains(AcceptSubmissionSchema)) + return EventType.Accept; @@ -103,12 +109,12 @@ public class SecurityEventToken { return (JsonConvert.DeserializeObject<dynamic>(payload)?.problems as JArray) ?.Select(j => j as dynamic).ToList() ?.Select(p => new Problems { - Title = p.Title, - Description = p.Description, - Items = p.Items, - Type = p.Type, - Detail = p.Detail, - Instance = p.Instance + title = p.Title, + // description = p.Description, + // items = p.Items, + type = p.Type, + detail = p.Detail, + instance = p.Instance }) ?.ToList() ?? new List<Problems>(); } diff --git a/FitConnect/Models/Submission.cs b/FitConnect/Models/Submission.cs index 63bc466e4cabc6fbb950150fd37cf6d2afea16e7..b8792a1d46a3790e20986c32dc9b4a42afad5ee0 100644 --- a/FitConnect/Models/Submission.cs +++ b/FitConnect/Models/Submission.cs @@ -1,15 +1,16 @@ +using System.Security.Cryptography.X509Certificates; using FitConnect.Services.Models; using FitConnect.Services.Models.v1.Submission; namespace FitConnect.Models; public class Submission { - public string? Id { get; set; } - public string? CaseId { get; set; } - public Destination? Destination { get; set; } = new(); + public string Id { get; set; } = null!; + public string CaseId { get; set; } = null!; + public Destination Destination { get; set; } = new(); public string DestinationId { - get => Destination.DestinationId; + get => Destination.DestinationId ?? throw new ArgumentNullException(nameof(DestinationId)); set => Destination.DestinationId = value; } @@ -23,6 +24,8 @@ public class Submission { public string? Data { get; set; } public string? EncryptedMetadata { get; set; } public string? EncryptedData { get; set; } + public string? MetaAuthentication => EncryptedMetadata?.Split('.').Last(); + public string? DataAuthentication => EncryptedData?.Split('.').Last(); public bool IsSubmissionReadyToAdd(out string? error) { var innerError = ""; @@ -45,7 +48,7 @@ public class Submission { public static implicit operator SubmissionForPickupDto(Submission sub) { return new SubmissionForPickupDto { - SubmissionId = sub.Id, + Id = sub.Id, CaseId = sub.CaseId, DestinationId = sub.DestinationId }; @@ -53,22 +56,25 @@ public class Submission { public static explicit operator Submission(SubmissionForPickupDto dto) { return new Submission { - Id = dto.SubmissionId, + Id = dto.Id, Callback = null, - DestinationId = dto.DestinationId, - ServiceType = null + DestinationId = dto.DestinationId ?? + throw new NullReferenceException(nameof(dto.DestinationId)), + ServiceType = new ServiceType() }; } public static explicit operator Submission(SubmissionDto dto) { return new Submission { Id = dto.SubmissionId, - Callback = (Callback)dto.Callback, - DestinationId = dto.DestinationId, - ServiceType = (ServiceType)dto.ServiceType, + Callback = dto.Callback == null ? null : (Callback)dto.Callback, + DestinationId = dto.DestinationId ?? + throw new NullReferenceException(nameof(dto.DestinationId)), + ServiceType = + dto.ServiceType == null ? new ServiceType() : (ServiceType)dto.ServiceType, EncryptedData = dto.EncryptedData, EncryptedMetadata = dto.EncryptedMetadata, - AttachmentIds = dto.Attachments, + AttachmentIds = dto.Attachments ?? new List<string>(), CaseId = dto.CaseId }; } diff --git a/FitConnect/Properties/AssemblyInfo.cs b/FitConnect/Properties/AssemblyInfo.cs index 49986d47feefcf501c1b2de8b2b4b8d7025ecd6f..5a9aa85b20efa09d0c3536b5d3edd8d71205c4e2 100644 --- a/FitConnect/Properties/AssemblyInfo.cs +++ b/FitConnect/Properties/AssemblyInfo.cs @@ -13,6 +13,7 @@ using System.Runtime.CompilerServices; [assembly:InternalsVisibleTo("IntegrationTests")] +[assembly:InternalsVisibleTo("BasicUnitTest")] // Von der MSBuild WriteCodeFragment-Klasse generiert. diff --git a/FitConnect/Router.cs b/FitConnect/Router.cs index 6f2976853794c7802c73fdf480818c4f7ab167df..665540a4bdea63c5a8fa4b10dcd28528bfa3b2f4 100644 --- a/FitConnect/Router.cs +++ b/FitConnect/Router.cs @@ -1,7 +1,4 @@ -using System.Data; -using System.Diagnostics.CodeAnalysis; using System.Text; -using System.Text.Encodings.Web; using FitConnect.Encryption; using FitConnect.Models; using FitConnect.Services; @@ -17,7 +14,7 @@ using Route = FitConnect.Services.Models.v1.Routes.Route; namespace FitConnect; -public class Router : IRouter { +internal class Router : IRouter { private readonly FitConnectEnvironment _environment; private readonly ILogger? _logger; private readonly IRouteService _routeService; @@ -40,7 +37,6 @@ public class Router : IRouter { foreach (var route in routes) { _logger?.LogInformation("Testing destination {DestinationId}", route.DestinationId); - var verifyJwt = await VerifyDestinationSignature(route); if (!verifyJwt) { throw new Exception($"Invalid destination signature"); @@ -77,6 +73,7 @@ public class Router : IRouter { return destinationUri.Host == submissionHost; } + private async Task<bool> VerifyDestinationParametersSignature(Route route) { // Get Key from SubmissionAPI var submissionKey = @@ -94,14 +91,14 @@ public class Router : IRouter { var signature = route.DestinationParametersSignature.Replace("..", $".{encodedParameter}."); - // KID to check is in signature header kid var header = JsonConvert.DeserializeObject<dynamic>( Base64UrlEncoder.Decode(route.DestinationParametersSignature.Split('.')[0]) ); - var kid = (string)header.kid; - _logger?.LogInformation("Testing with kid: {kid}", kid); + var kid = (string)header?.kid!; + + _logger?.LogInformation("Testing with kid: {Kid}", kid); return FitEncryption.VerifyJwt(signature, new JsonWebKeySet(submissionKey).Keys.First(k => k.Kid == kid), _logger); } @@ -116,12 +113,11 @@ public class Router : IRouter { } /// <summary> - /// /// </summary> /// <returns></returns> - public async Task<string> GetSubmissionServiceValidationJwk(string baseUrl) { - var client = new HttpClient() { - BaseAddress = new Uri(baseUrl), + public static async Task<string> GetSubmissionServiceValidationJwk(string baseUrl) { + var client = new HttpClient { + BaseAddress = new Uri(baseUrl) }; var result = await client.GetAsync("/.well-known/jwks.json"); diff --git a/FitConnect/SecuritySpecification.cs b/FitConnect/SecuritySpecification.cs new file mode 100644 index 0000000000000000000000000000000000000000..d476074188e69935a77d371e82b91f020e95003f --- /dev/null +++ b/FitConnect/SecuritySpecification.cs @@ -0,0 +1,18 @@ +using System.Security.Cryptography; +using System.Text; + +namespace FitConnect; + +/// <summary> +/// This class contains all security related methods that can be changed due to future +/// security issues or decisions from the BSI. +/// </summary> +internal static class SecuritySpecification { + public const int MaxCallbackAge = 5; + + public static byte[] CalculateCallbackHmac(string callbackSecret, long timestamp, string body) { + return new HMACSHA512(Encoding.UTF8.GetBytes(callbackSecret)) + .ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}")); + } + +} diff --git a/FitConnect/Sender.cs b/FitConnect/Sender.cs index c82db00bf21d6fd24491318586ea0d57a6073895..cc30c2ab888c13bd77729926eafc80ed2f5556e2 100644 --- a/FitConnect/Sender.cs +++ b/FitConnect/Sender.cs @@ -1,3 +1,4 @@ +using System.Security; using System.Text.RegularExpressions; using Autofac; using FitConnect.Encryption; @@ -29,6 +30,9 @@ namespace FitConnect; /// </example> public class Sender : FitConnectClient, ISender, ISenderWithDestination, ISenderWithAttachments, ISenderWithData, ISenderWithService { + public string? PublicKey { get; set; } + public Submission? Submission { get; set; } + public Sender(FitConnectEnvironment environment, string clientId, string clientSecret, ILogger? logger = null) : base(environment, clientId, clientSecret, logger) { } @@ -38,8 +42,6 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, container) { } - public string? PublicKey { get; set; } - public ISenderWithDestination FindDestinationId(string leiaKey, string? ags = null, string? ars = null, @@ -75,7 +77,6 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, return this; } - public Submission? Submission { get; set; } Submission ISenderReady.Submit() { if (Submission == null) { @@ -84,11 +85,9 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, } var metadata = CreateMetadata(Submission); - Logger?.LogTrace("MetaData: {metadata}", metadata); - var validationErrors = Subscriber.VerifyMetadata(metadata); - if (validationErrors.Count != 0) { - validationErrors.ToList() - .ForEach(v => Logger?.LogError("Validation error: {error}", v)); + Logger?.LogTrace("MetaData: {Metadata}", metadata); + var valid = JsonHelper.VerifyMetadata(metadata); + if (!valid) { Logger?.LogError("Sending submission aborted due to validation errors"); throw new InvalidOperationException("Submission is not ready"); } @@ -96,7 +95,7 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, Logger?.LogInformation("MetaData validation check, done"); Logger?.LogInformation("Sending submission"); var encryptedMeta = Encryption.Encrypt(metadata); - Logger?.LogTrace("Encrypted metadata: {encryptedMeta}", encryptedMeta); + Logger?.LogTrace("Encrypted metadata: {EncryptedMeta}", encryptedMeta); Submission.EncryptedMetadata = encryptedMeta; if (Submission.Data != null) Submission.EncryptedData = Encryption.Encrypt(Submission.Data); @@ -155,7 +154,7 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, created.SubmissionId, Submission.CaseId); var encryptedAttachments = Encrypt(PublicKey!, Submission.Attachments); - UploadAttachmentsAsync(Submission.Id!, encryptedAttachments).Wait(); + UploadAttachments(Submission.Id!, encryptedAttachments); return this; } @@ -182,8 +181,14 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, return submission; } - private async Task<string> GetPublicKeyFromDestination(string destinationId) { + internal async Task<string> GetPublicKeyFromDestination(string destinationId) { var publicKey = await DestinationService.GetPublicKey(destinationId); + + var keyIsValid = new CertificateHelper(Logger).ValidateCertificate(publicKey, + VerifiedKeysAreMandatory ? LogLevel.Error : LogLevel.Warning); + if (VerifiedKeysAreMandatory && !keyIsValid) + throw new SecurityException("Public key is not trusted"); + return publicKey; } @@ -226,17 +231,31 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, return JsonConvert.SerializeObject(metaData); } + /// <summary> + /// Finding Areas + /// </summary> + /// <param name="filter"></param> + /// <param name="totalCount"></param> + /// <param name="offset"></param> + /// <param name="limit"></param> + /// <returns></returns> + public IEnumerable<Area> GetAreas(string filter, out int totalCount, int offset = 0, + int limit = 100) { + var dto = RouteService.GetAreas(filter, offset, limit).Result; + totalCount = dto?.TotalCount ?? 0; + return dto?.Areas ?? new List<Area>(); + } /// <summary> /// Uploading the encrypted data to the server /// </summary> /// <param name="submissionId">Submissions ID</param> /// <param name="encryptedAttachments">Encrypted attachments with id and content</param> - private async Task<bool> UploadAttachmentsAsync(string submissionId, + private bool UploadAttachments(string submissionId, Dictionary<string, string> encryptedAttachments) { try { foreach (var (id, content) in encryptedAttachments) { - Logger?.LogInformation("Uploading attachment {id}", id); + Logger?.LogInformation("Uploading attachment {Id}", id); SubmissionService.UploadAttachment(submissionId, id, content); } diff --git a/FitConnect/Services/Interfaces/ISelfServicePortalService.cs b/FitConnect/Services/Interfaces/ISelfServicePortalService.cs index dce0390c717dbc7e6df5c696f1a6f8c57f265c27..7b87c7804a264e92343ac2e828c46bb741d52acd 100644 --- a/FitConnect/Services/Interfaces/ISelfServicePortalService.cs +++ b/FitConnect/Services/Interfaces/ISelfServicePortalService.cs @@ -1,3 +1,4 @@ +using Jose; using Microsoft.IdentityModel.Tokens; namespace FitConnect.Services.Interfaces; diff --git a/FitConnect/Services/Interfaces/ISubmissionService.cs b/FitConnect/Services/Interfaces/ISubmissionService.cs index caf0054cfa16b03c9475b9d309f67bf1bce6c70f..a343a62a522a6a93bfd05ad5aa6c21f2c5eb161c 100644 --- a/FitConnect/Services/Interfaces/ISubmissionService.cs +++ b/FitConnect/Services/Interfaces/ISubmissionService.cs @@ -39,7 +39,7 @@ public interface ISubmissionService : IRestCallService { /// <param name="offset">RequestParam</param> /// <param name="limit">RequestParam</param> /// <returns></returns> - Task<SubmissionsForPickupDto> ListSubmissions(string? destinationId, int offset, + SubmissionsForPickupDto ListSubmissions(string? destinationId, int offset, int limit); /// <summary> @@ -82,8 +82,11 @@ public interface ISubmissionService : IRestCallService { /// Retrieves the events of a submission /// </summary> /// <param name="caseId"></param> + /// <param name="destinationId"></param> + /// <param name="skipTest"></param> /// <returns></returns> - Task<List<string>> GetStatusForSubmissionAsync(string caseId); + Task<List<string>?> GetStatusForSubmissionAsync(string caseId, string destinationId, + bool skipTest = false); } public interface ICasesService { diff --git a/FitConnect/Services/Models/ServiceTypeDto.cs b/FitConnect/Services/Models/ServiceTypeDto.cs index c34a7521674d279968d1b07a9c36c8421563055b..25591062ac054a093237cb3654ccc6ad84a7dd1b 100644 --- a/FitConnect/Services/Models/ServiceTypeDto.cs +++ b/FitConnect/Services/Models/ServiceTypeDto.cs @@ -12,3 +12,14 @@ public class ServiceTypeDto { [JsonProperty("identifier")] public string? Identifier { get; set; } } + +public class AcceptanceStatus { + [JsonProperty("metadata")] + public string? Metadata { get; set; } + + [JsonProperty("data")] + public string? Data { get; set; } + + [JsonProperty("attachments")] + public Dictionary<string, string> Attachments { get; set; } = new Dictionary<string, string>(); +} diff --git a/FitConnect/Services/Models/v1/Api/Metadata.cs b/FitConnect/Services/Models/v1/Api/Metadata.cs index c5ccaf433011c00cc3ca3068b80d95a09ac78c45..77fa78761a218643d7ada6cfa73899479c6af4e5 100644 --- a/FitConnect/Services/Models/v1/Api/Metadata.cs +++ b/FitConnect/Services/Models/v1/Api/Metadata.cs @@ -53,6 +53,8 @@ namespace FitConnect.Models.Api.Metadata [JsonProperty("replyChannel", NullValueHandling = NullValueHandling.Ignore)] public ReplyChannel ReplyChannel { get; set; } + + public override string ToString() => JsonConvert.SerializeObject(this); } /// <summary> diff --git a/FitConnect/Services/Models/v1/Api/SecEventToken.cs b/FitConnect/Services/Models/v1/Api/SecEventToken.cs index ecb69eb12c3e78c66c1116e09c41e5111b6faffa..46a1e1d43e4f5d33069b6fbef0fc7397b850ecb6 100644 --- a/FitConnect/Services/Models/v1/Api/SecEventToken.cs +++ b/FitConnect/Services/Models/v1/Api/SecEventToken.cs @@ -5,7 +5,6 @@ // using FitConnect.Models.v1.Api; // // var welcome = Welcome.FromJson(jsonString); - namespace FitConnect.Models.v1.Api { using System; @@ -143,10 +142,10 @@ namespace FitConnect.Models.v1.Api public partial class Iss { - [JsonProperty("description")] + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; } - [JsonProperty("type")] + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; set; } } @@ -203,97 +202,100 @@ namespace FitConnect.Models.v1.Api public partial class Problems { public static readonly Problems IncorrectAuthenticationTag = new Problems{ - Type = "https://schema.fitko.de/fit-connect/events/problems/incorrect-authentication-tag", - Title = "Authentication-Tag ungültig", - Detail = "Das Authentication-Tag des Metadatensatzes ist ungültig.", - Instance = "metadata" + type = "https://schema.fitko.de/fit-connect/events/problems/incorrect-authentication-tag", + title = "Authentication-Tag ungültig", + detail = "Das Authentication-Tag des Metadatensatzes ist ungültig.", + instance = "metadata" }; public static readonly Problems EncryptionIssue = new Problems{ - Type = "https://schema.fitko.de/fit-connect/events/problems/encryption-issue", - Title = "Entschlüsselungs-Fehler", - Detail = "Der Schlüssel {kid} ist nicht der zu diesem Zweck vorgesehene Schlüssel.", - Instance = "metadata" + type = "https://schema.fitko.de/fit-connect/events/problems/encryption-issue", + title = "Entschlüsselungs-Fehler", + detail = "Der Schlüssel {kid} ist nicht der zu diesem Zweck vorgesehene Schlüssel.", + instance = "metadata" }; public static readonly Problems SyntaxViolation = new Problems{ - Type = "https://schema.fitko.de/fit-connect/events/problems/syntax-violation", - Title = "Syntax-Fehler", - Detail= "Der Metadatensatz ist kein valides JSON.", - Instance= "metadata" + type = "https://schema.fitko.de/fit-connect/events/problems/syntax-violation", + title = "Syntax-Fehler", + detail= "Der Metadatensatz ist kein valides JSON.", + instance= "metadata" }; public static readonly Problems MissingSchema = new Problems{ - Type = "https://schema.fitko.de/fit-connect/events/problems/missing-schema", - Title = "Schema-Referenz fehlt", - Detail = "Die Schema-Referenz fehlt im Metadatensatz.", - Instance = "metadata" + type = "https://schema.fitko.de/fit-connect/events/problems/missing-schema", + title = "Schema-Referenz fehlt", + detail = "Die Schema-Referenz fehlt im Metadatensatz.", + instance = "metadata" }; public static readonly Problems UnsupportedSchema = new Problems{ - Type = "https://schema.fitko.de/fit-connect/events/problems/unsupported-schema", - Title = "Metadatenschema nicht unterstützt", - Detail = "Die angegebene Metadatenschema-URI ('$schema') ist keines der unterstützten Metadatenschemas.", - Instance = "metadata" + type = "https://schema.fitko.de/fit-connect/events/problems/unsupported-schema", + title = "Metadatenschema nicht unterstützt", + detail = "Die angegebene Metadatenschema-URI ('$schema') ist keines der unterstützten Metadatenschemas.", + instance = "metadata" }; public static readonly Problems SchemaViolation = new Problems { - Type = "https://schema.fitko.de/fit-connect/events/problems/schema-violation", - Title = "Schema-Fehler", - Detail = "Der Metadatensatz ist nicht schema-valide.", - Instance = "metadata" + type = "https://schema.fitko.de/fit-connect/events/problems/schema-violation", + title = "Schema-Fehler", + detail = "Der Metadatensatz ist nicht schema-valide.", + instance = "metadata" }; - [JsonProperty("instance")] - public string Instance { get; set; } + [JsonProperty("instance", NullValueHandling = NullValueHandling.Ignore)] + public string instance { get; set; } - [JsonProperty("detail")] - public string Detail { get; set; } + [JsonProperty("detail", NullValueHandling = NullValueHandling.Ignore)] + public string detail { get; set; } - [JsonProperty("title")] - public string Title { get; set; } + [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] + public string title { get; set; } - [JsonProperty("description")] - public string Description { get; set; } + // [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + // public string? description { get; set; } - [JsonProperty("type")] - public string Type { get; set; } + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string type { get; set; } - [JsonProperty("items")] - public Items Items { get; set; } + // [JsonProperty("items", NullValueHandling = NullValueHandling.Ignore)] + // public Items? items { get; set; } + } public partial class Items { - [JsonProperty("type")] + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; set; } - [JsonProperty("properties")] + [JsonProperty("properties", NullValueHandling = NullValueHandling.Ignore)] public ItemsProperties Properties { get; set; } - [JsonProperty("required")] + [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] public string[] ItemsRequired { get; set; } - } + + } public partial class ItemsProperties { - [JsonProperty("type")] + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public Sub Type { get; set; } - [JsonProperty("title")] + [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] public Iss Title { get; set; } - [JsonProperty("detail")] + [JsonProperty("detail", NullValueHandling = NullValueHandling.Ignore)] public Iss Detail { get; set; } - [JsonProperty("instance")] + [JsonProperty("instance", NullValueHandling = NullValueHandling.Ignore)] public Sub Instance { get; set; } + } public partial class Sub { - [JsonProperty("description")] + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; } - [JsonProperty("type")] + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; set; } - [JsonProperty("pattern")] + [JsonProperty("pattern", NullValueHandling = NullValueHandling.Ignore)] public string Pattern { get; set; } } diff --git a/FitConnect/Services/Models/v1/Destination/ContactInformationDto.cs b/FitConnect/Services/Models/v1/Destination/ContactInformationDto.cs index df520ee45a8ad2f2d7af6fde7522e49451b79dbc..d1942db155f5b8c66e3a6f735130f3a9926f4548 100644 --- a/FitConnect/Services/Models/v1/Destination/ContactInformationDto.cs +++ b/FitConnect/Services/Models/v1/Destination/ContactInformationDto.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Newtonsoft.Json; namespace FitConnect.Services.Models.v1.Destination; diff --git a/FitConnect/Services/Models/v1/Destination/SubmissionSchemaDto.cs b/FitConnect/Services/Models/v1/Destination/SubmissionSchemaDto.cs index 8da11c5cdbd92303e6b7269c980d860349407716..4711ef29132294523b7d512afc292ee0e908fe05 100644 --- a/FitConnect/Services/Models/v1/Destination/SubmissionSchemaDto.cs +++ b/FitConnect/Services/Models/v1/Destination/SubmissionSchemaDto.cs @@ -1,12 +1,12 @@ using Newtonsoft.Json; namespace FitConnect.Services.Models.v1.Destination; - +#nullable disable public class SubmissionSchemaDto { [JsonProperty("mimeType")] // private SubmissionSchemaMimeTypeDto mimeType; public string mimeType; [JsonProperty("schemaUri")] - public string? SchemaUri { get; set; } + public string SchemaUri { get; set; } } diff --git a/FitConnect/Services/Models/v1/Routes/Routes.cs b/FitConnect/Services/Models/v1/Routes/Routes.cs index a1b912b7492a56aada6f6258a14c701342d86289..8719150b365692ec259ad3c0f311ffd8f6814f21 100644 --- a/FitConnect/Services/Models/v1/Routes/Routes.cs +++ b/FitConnect/Services/Models/v1/Routes/Routes.cs @@ -1,5 +1,5 @@ // Root myDeserializedClass = JsonSerializer.Deserialize<Root>(myJsonResponse); - +#nullable disable using Newtonsoft.Json; namespace FitConnect.Services.Models.v1.Routes; @@ -54,7 +54,7 @@ public class Key { [JsonProperty("e")] public string E { get; set; } - public string ToString() { + public override string ToString() { return JsonConvert.SerializeObject(this); } } @@ -99,9 +99,9 @@ public class Route { [JsonProperty("destinationName")] public string DestinationName { get; set; } - + [System.Text.Json.Serialization.JsonIgnore] - public string DestinationParameterString { get;set; } + public string DestinationParameterString { get; set; } } public class SubmissionSchema { diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs index fa607ae2dfa44a67008a0052ee68f75abd6d6fb8..048c3490e83081d74952465691e80bf56f4b0026 100644 --- a/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs +++ b/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs @@ -4,11 +4,11 @@ namespace FitConnect.Services.Models.v1.Submission; public class SubmissionCreatedDto { [JsonProperty("destinationId")] - public string? DestinationId { get; set; } + public string DestinationId { get; set; } = null!; [JsonProperty("submissionId")] - public string? SubmissionId { get; set; } + public string SubmissionId { get; set; } = null!; [JsonProperty("caseId")] - public string? CaseId { get; set; } + public string CaseId { get; set; } = null!; } diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs index 9a4f1aa1c17a3796cb435cc7ee5a6022597931de..1c0e3be45e4188ee8111f9434a463cf4491388c4 100644 --- a/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs +++ b/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs @@ -12,10 +12,10 @@ public class SubmissionDto { [JsonProperty("caseId")] - public string? CaseId { get; set; } + public string CaseId { get; set; } = null!; [JsonProperty("destinationId")] - public string? DestinationId { get; set; } + public string DestinationId { get; set; } = null!; [JsonProperty("encryptedData")] @@ -31,5 +31,5 @@ public class SubmissionDto { [JsonProperty("submissionId")] - public string? SubmissionId { get; set; } + public string SubmissionId { get; set; } = null!; } diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs index 7540c18ff8926a41e13d75ccfadd16e242339072..c1a7911aacd32b07f4bf7427993e173b18ad1523 100644 --- a/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs +++ b/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs @@ -10,5 +10,5 @@ public class SubmissionForPickupDto { public string? DestinationId { get; set; } [JsonProperty("submissionId")] - public string? SubmissionId { get; set; } + public string Id { get; set; } = null!; } diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionReducedDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionReducedDto.cs index 36deb67e518f1fcf6b1c79cd800573dc5e603b3d..7f2c2312e1249acbc6a2702093cf16276f714738 100644 --- a/FitConnect/Services/Models/v1/Submission/SubmissionReducedDto.cs +++ b/FitConnect/Services/Models/v1/Submission/SubmissionReducedDto.cs @@ -4,7 +4,7 @@ namespace FitConnect.Services.Models.v1.Submission; public class SubmissionReducedDto { [JsonProperty("serviceType")] - private ServiceTypeDto _serviceTypeDto { get; set; } + private ServiceTypeDto? ServiceTypeDto { get; set; } [JsonProperty("attachments")] diff --git a/FitConnect/Services/OAuthService.cs b/FitConnect/Services/OAuthService.cs index cad62fefe305cdc265e666be14dd62958f90c54a..1df17608f732d16abc599abca08c0b6ae965f30a 100644 --- a/FitConnect/Services/OAuthService.cs +++ b/FitConnect/Services/OAuthService.cs @@ -1,6 +1,4 @@ using System.Net; -using System.Net.Http.Json; -using System.Runtime.CompilerServices; using System.Security.Authentication; using FitConnect.Services.Interfaces; using FitConnect.Services.Models; @@ -40,8 +38,6 @@ internal class OAuthService : RestCallService, IOAuthService { /// https://portal.auth-testing.fit-connect.fitko.dev /// </para> /// </summary> - /// <param name="clientId">Your client Id</param> - /// <param name="clientSecret">Your client Secret</param> /// <param name="scope">Scope if needed</param> /// <returns>The received token or null</returns> public async Task AuthenticateAsync( @@ -66,7 +62,9 @@ internal class OAuthService : RestCallService, IOAuthService { var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { - var result = JsonConvert.DeserializeObject<OAuthAccessToken>(await response.Content.ReadAsStringAsync()); + var result = + JsonConvert.DeserializeObject<OAuthAccessToken>( + await response.Content.ReadAsStringAsync()); if (result == null) throw new AuthenticationException("Failed to authenticate"); Token = result; @@ -81,4 +79,4 @@ internal class OAuthService : RestCallService, IOAuthService { public void EnsureAuthenticated() { if (!IsAuthenticated) AuthenticateAsync().Wait(); } -} \ No newline at end of file +} diff --git a/FitConnect/Services/RestCallService.cs b/FitConnect/Services/RestCallService.cs index 9c40b4a13fcbd48f6d42c7d378ea6cb61fbaab9a..c3573eb200466450b689004464ddf20c0197aa1b 100644 --- a/FitConnect/Services/RestCallService.cs +++ b/FitConnect/Services/RestCallService.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Http.Headers; -using System.Runtime.CompilerServices; using System.Text; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -36,11 +35,23 @@ internal class RestCallService : IRestCallService { return client; } + /// <summary> + /// </summary> + /// <param name="endpoint">Endpoint is attached to the base url if not starts with http</param> + /// <param name="method"></param> + /// <param name="body"></param> + /// <param name="contentType"></param> + /// <param name="accept"></param> + /// <returns></returns> protected async Task<string> RestCallForString(string endpoint, HttpMethod method, string? body = null, string contentType = "application/json", - string accept = "application/json") => - await RestCallForString(new Uri($"{_baseUrl}{endpoint}"), method, body, contentType, + string accept = "application/json") { + var uri = endpoint.StartsWith("http") + ? new Uri(endpoint) + : new Uri($"{_baseUrl}{endpoint}"); + return await RestCallForString(uri, method, body, contentType, accept); + } protected async Task<string> RestCallForString(Uri requestUri, HttpMethod method, string? body = null, string contentType = "application/json", @@ -67,7 +78,8 @@ internal class RestCallService : IRestCallService { var response = await client.SendAsync(request); if (response.Headers.Contains("jws-signature")) - Console.WriteLine(response.Headers.GetValues("jws-signature").Aggregate((a,b)=>$"{a},{b}")); + Console.WriteLine(response.Headers.GetValues("jws-signature") + .Aggregate((a, b) => $"{a},{b}")); _logger?.LogDebug("Server call: {Method} {Uri} - {StatusCode}", method, request.RequestUri, response.StatusCode); diff --git a/FitConnect/Services/SubmissionService.cs b/FitConnect/Services/SubmissionService.cs index a59e5b27450a17b56a9eb1cbb21ecb3ec789bcfc..f29a496adf5a323b1622c24b5d9c20195198b2a7 100644 --- a/FitConnect/Services/SubmissionService.cs +++ b/FitConnect/Services/SubmissionService.cs @@ -1,23 +1,27 @@ -using System.Text.Encodings.Web; -using System.Text.Json; -using Newtonsoft.Json; +using FitConnect.Encryption; using FitConnect.Services.Interfaces; using FitConnect.Services.Models.v1.Case; using FitConnect.Services.Models.v1.Submission; using Microsoft.Extensions.Logging; -using JsonSerializer = System.Text.Json.JsonSerializer; - +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using NJsonSchema; namespace FitConnect.Services; internal class SubmissionService : RestCallService, ISubmissionService { + private readonly string _baseUrl; private readonly ILogger? _logger; private readonly IOAuthService _oAuthService; + private readonly JsonWebKey? _signatureValidationKey; public SubmissionService(string baseUrl, IOAuthService oAuthService, + JsonWebKey? signatureValidationKey, string version = "v1", ILogger? logger = null) : base( $"{baseUrl}/{version}", logger) { + _baseUrl = baseUrl; _oAuthService = oAuthService; + _signatureValidationKey = signatureValidationKey; _logger = logger; } @@ -53,11 +57,11 @@ internal class SubmissionService : RestCallService, ISubmissionService { public bool AddSubmissionAttachment(string submissionId, string attachmentId, string encryptedAttachmentContent) { _oAuthService.EnsureAuthenticated(); - var result = RestCallForString( + RestCallForString( $"/submissions/{submissionId}/attachments/{attachmentId}", HttpMethod.Put, encryptedAttachmentContent, - "application/jose").Result; + "application/jose").Wait(); return true; } @@ -70,7 +74,7 @@ internal class SubmissionService : RestCallService, ISubmissionService { public async Task<SubmissionReducedDto?> SubmitSubmission(string submissionId, SubmitSubmissionDto submitSubmission) { _oAuthService.EnsureAuthenticated(); - var body = JsonConvert.SerializeObject(submitSubmission, new JsonSerializerSettings() { + var body = JsonConvert.SerializeObject(submitSubmission, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented }); @@ -90,7 +94,7 @@ internal class SubmissionService : RestCallService, ISubmissionService { /// <param name="offset">RequestParam</param> /// <param name="limit">RequestParam</param> /// <returns></returns> - public async Task<SubmissionsForPickupDto> ListSubmissions(string? destinationId, + public SubmissionsForPickupDto ListSubmissions(string? destinationId, int offset = 0, int limit = 100) { _oAuthService.EnsureAuthenticated(); @@ -140,13 +144,166 @@ internal class SubmissionService : RestCallService, ISubmissionService { throw new NotImplementedException(); } - public async Task<List<string>> GetStatusForSubmissionAsync(string caseId) { + public async Task<List<string>?> GetStatusForSubmissionAsync(string caseId, + string destinationId, + bool skipTest = false) { _oAuthService.EnsureAuthenticated(); var events = await RestCall<EventLogDto>($"/cases/{caseId}/events", HttpMethod.Get); - return events.EventLog.ToList(); + if (events == null) + return null; + + var valid = await ValidateSignature(events, destinationId); + valid &= await ValidateSchema(events); + + if (!valid) { + _logger?.LogError("Invalid SET, signature can not be verified"); + if (events.EventLog != null) + _logger?.LogTrace("Tested events: {Events}", + events.EventLog.Aggregate((a, b) => a + "\n" + b)); + if (!skipTest) + throw new InvalidOperationException("Invalid SET, signature can not be verified"); + } + + return events.EventLog?.ToList(); } - public async Task GetValidationJwk() { - throw new NotImplementedException(); + + private async Task<bool> ValidateSchema(EventLogDto events, bool mandatory = false) { + var valid = true; + if (events.EventLog == null) + return true; + + foreach (var eventString in events.EventLog) { + var eventContent = Base64UrlEncoder.Decode(eventString.Split('.')[1]); + _logger?.LogTrace("Validating event: {Event}", eventContent); + var payload = JsonConvert.DeserializeObject<Dictionary<string, object>>(eventContent); + if (payload == null) { + _logger?.LogWarning("Invalid event: {Event}", eventContent); + continue; + } + + if (payload.ContainsKey("$schema")) { + var schema = (string)((dynamic)payload)["$schema"]; + _logger?.LogInformation("Validating event against schema: {Schema}", schema); + + try { + var schemaRaw = await RestCallForString(new Uri(schema), HttpMethod.Get); + var schemaOk = JsonHelper.ValidateJsonSchema(schemaRaw, eventContent); + + + if (mandatory) + valid = false; + + + if (!schemaOk) { + _logger?.Log(mandatory ? LogLevel.Error : LogLevel.Warning, + "Error on validating JSON Schema"); + + if (mandatory) + throw new ArgumentException("Invalid JSON Schema for SET event"); + } + else { + _logger?.LogDebug("SET event is valid"); + } + } + catch (Exception e) { + _logger?.LogError(e, "Error validating event against schema {Exception}", e); + if (mandatory) + valid = false; + } + } + } + + return valid; + } + + private async Task<bool> ValidateSignature(EventLogDto events, string destinationId) { + var keys = (await GetJsonWebKeysForEvent(events, destinationId)).ToList(); + + if (events.EventLog == null) + return true; + + var result = true; + foreach (var s in events.EventLog) { + var header = JsonConvert.DeserializeObject<Dictionary<string, object>>( + Base64UrlEncoder.Decode(s.Split('.')[0])); + + if (header == null) + throw new ArgumentException("Invalid event header"); + + var kid = (string)header["kid"]; + result &= FitEncryption.VerifyJwt(s, keys.Where(k => k.Kid == kid), _logger); + } + + var valid = result; + + if (!valid) { + _logger?.LogDebug("Signature is invalid"); + _logger?.LogDebug("Tested with keys: {Keys}", + keys.Aggregate("", (a, b) => $"{b.Kid}, {a}")); + } + + return valid; + } + + private async Task<IEnumerable<JsonWebKey>> GetJsonWebKeysForEvent(EventLogDto events, + string destinationId) { + var keySet = new JsonWebKeySet(await Router.GetSubmissionServiceValidationJwk(_baseUrl)); + var keys = (_signatureValidationKey == null + ? keySet.Keys + : keySet.Keys.Append(_signatureValidationKey)).ToList(); + + + var result = + (await GetKeyIdsFromEvent(events, destinationId, keys.Select(k => k.Kid).ToList())) + .Union(keys).ToList(); + + var valid = result.Aggregate(true, + (a, key) => (new CertificateHelper(_logger).ValidateCertificate(key))); + if (!valid) { + _logger?.LogError("(SET Events verification) Invalid certificate"); + throw new ArgumentException("(SET Events verification) Invalid certificate"); + } + else { + _logger?.LogInformation("(SET Events verification) Certificate is valid"); + } + + return result; + } + + private async Task<IEnumerable<JsonWebKey>> GetKeyIdsFromEvent(EventLogDto events, + string destinationId, ICollection<string> knownKeys) { + if (events.EventLog == null) + return new List<JsonWebKey>(); + + // Load Key from GET {{submission_api_url}}/v1/destinations/{{destinationId}}/keys/{{keyId}} + var keyIds = events.EventLog.Select(ExtractSubmissionIdFromEvent).ToList(); + + var result = new List<JsonWebKey>(); + foreach (var (submission, keyId) in keyIds) { + if (knownKeys.Contains(keyId)) + continue; + + var keyJson = await RestCallForString($"/destinations/{destinationId}/keys/{keyId}", + HttpMethod.Get); + result.Add(new JsonWebKey(keyJson)); + } + + return result; + } + + private (string submissionId, string keyId) ExtractSubmissionIdFromEvent(string events) { + var jwtParts = events.Split('.').Select(Base64UrlEncoder.Decode).ToList(); + + var header = JsonConvert.DeserializeObject<Dictionary<string, object>>(jwtParts[0]); + var payload = JsonConvert.DeserializeObject<Dictionary<string, object>>(jwtParts[1]); + + if (header == null || payload == null) + throw new NullReferenceException("Event string is invalid"); + + var keyId = (string)header["kid"]; + var submissionId = ((string)payload["sub"]).Split(':')[1]; + + return (submissionId, keyId); } } diff --git a/FitConnect/Subscriber.cs b/FitConnect/Subscriber.cs index d528b62b023b1fc20a31c801c51b88e0d88add67..549558585bbf08fd17f11c919a4fcda1d7f023b4 100644 --- a/FitConnect/Subscriber.cs +++ b/FitConnect/Subscriber.cs @@ -1,17 +1,21 @@ +using System.Net; using System.Reflection; using System.Security.Cryptography; using System.Text; using Autofac; +using Autofac.Core.Activators.Reflection; using FitConnect.Encryption; using FitConnect.Interfaces.Subscriber; using FitConnect.Models; using FitConnect.Models.v1.Api; +using FitConnect.Services.Models; using FitConnect.Services.Models.v1.Submission; using IdentityModel; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using NJsonSchema; +using NJsonSchema.Annotations; using NJsonSchema.Validation; using Metadata = FitConnect.Models.Api.Metadata.Metadata; @@ -23,6 +27,8 @@ namespace FitConnect; public class Subscriber : FitConnectClient, ISubscriber, ISubscriberWithSubmission { + public Submission? Submission { get; private set; } + public Subscriber(FitConnectEnvironment environment, string clientId, string clientSecret, string privateKeyDecryption, @@ -61,10 +67,10 @@ public class Subscriber : FitConnectClient, public IEnumerable<SubmissionForPickupDto> GetAvailableSubmissions(string? destinationId = null, int skip = 0, int take = 100) { - var submissionsResult = SubmissionService.ListSubmissions(destinationId, 0, 100).Result; + var submissionsResult = SubmissionService.ListSubmissions(destinationId, 0, 100); // Creating a dictionary of destinationId to submissionIds from the REST API result - return submissionsResult.Submissions; + return submissionsResult.Submissions ?? new List<SubmissionForPickupDto>(); } @@ -77,29 +83,27 @@ public class Subscriber : FitConnectClient, public ISubscriberWithSubmission RequestSubmission(string submissionId, bool skipSchemaTest = false) { var submission = (Submission)SubmissionService.GetSubmission(submissionId); - var (metaDataString, _, metaHash) = Encryption.Decrypt(submission.EncryptedMetadata); + var (metaDataString, _, metaHash) = Encryption.Decrypt(submission.EncryptedMetadata!); if (!skipSchemaTest) { - var errors = VerifyMetadata(metaDataString); - if (errors.Count > 0) { + var valid = JsonHelper.VerifyMetadata(metaDataString); + if (!valid) { Logger?.LogWarning("Invalid metadata: {MetaData}", metaDataString); - foreach (var error in errors) Logger?.LogError("Error: {Error}", error.ToString()); - throw new Exception($"Metadata validation failed: {string.Join(", ", errors)}"); + throw new Exception($"Metadata validation failed"); } } submission.Metadata = JsonConvert.DeserializeObject<Metadata>(metaDataString); - if (submission.EncryptedData != null) { var (dataString, _, dataHash) = Encryption.Decrypt(submission.EncryptedData); submission.Data = dataString; - if (submission?.Metadata?.ContentStructure.Data.Hash.Content != + if (submission.Metadata?.ContentStructure.Data.Hash.Content != FitEncryption.CalculateHash(dataString)) { Logger?.LogWarning("Data hash mismatch: {DataHash} != {CalculatedHash}", - submission?.Metadata?.ContentStructure.Data.Hash.Content, + submission.Metadata?.ContentStructure.Data.Hash.Content, FitEncryption.CalculateHash(dataString)); throw new Exception("Data hash mismatch"); } @@ -109,14 +113,14 @@ public class Subscriber : FitConnectClient, return this; } - public Submission? Submission { get; private set; } /// <summary> /// Reading attachments for a submission. /// </summary> /// <returns></returns> public IEnumerable<Attachment> GetAttachments() { - // TODO add guard calls + if (Submission?.Id == null || Submission?.Metadata == null) + throw new Exception("No submission available"); var attachments = new List<Attachment>(); foreach (var id in Submission!.AttachmentIds) { @@ -125,7 +129,8 @@ public class Subscriber : FitConnectClient, var attachmentMeta = Submission.Metadata.ContentStructure.Attachments.First(a => a.AttachmentId == id); - attachments.Add(new Attachment(attachmentMeta, content)); + attachments.Add(new Attachment(attachmentMeta, content, + encryptedAttachment.Split('.').Last())); } Submission.Attachments = attachments; @@ -140,85 +145,75 @@ public class Subscriber : FitConnectClient, CompleteSubmission(Submission!, FinishSubmissionStatus.Rejected, problems); } - public void ForwardSubmission() { - CompleteSubmission(Submission!, FinishSubmissionStatus.Forwarded); - } + + // public void ForwardSubmission() { + // CompleteSubmission(Submission!, FinishSubmissionStatus.Forwarded); + // } public void CompleteSubmission(FinishSubmissionStatus status) { CompleteSubmission(Submission!, status); } + public bool VerifyStatus(AcceptanceStatus acceptanceStatus) { + if (Submission == null) + throw new NullReferenceException("Submission is null"); + var result = true; + result &= acceptanceStatus.Metadata == Submission.MetaAuthentication; + result &= acceptanceStatus.Data == Submission.DataAuthentication; + foreach (var attachment in Submission.Attachments) { + result &= acceptanceStatus.Attachments[attachment.Id] == + attachment.AttachmentAuthentication; + } - /// <summary> - /// Verify the metadata hash and content to fit the schema - /// </summary> - /// <param name="metadataString"></param> - /// <returns></returns> - public static ICollection<ValidationError> VerifyMetadata(string metadataString) { - var schemaString = LoadContentOfResource("metadata.schema.json"); - var schema = JsonSchema.FromJsonAsync(schemaString).Result; - return schema.Validate(metadataString); - } - - private static string LoadContentOfResource(string resourceName) { - var assembly = Assembly.GetExecutingAssembly(); - var fullQualifiedName = $"{assembly.GetName().Name}.{resourceName}"; - var resourceStream = assembly.GetManifestResourceStream(fullQualifiedName); - var reader = new StreamReader(resourceStream); - return reader.ReadToEnd(); + return result; } - private string GetEvent(FinishSubmissionStatus state) { - return state switch { - FinishSubmissionStatus.Accepted => - "https://schema.fitko.de/fit-connect/events/accept-submission", - FinishSubmissionStatus.Rejected => - "https://schema.fitko.de/fit-connect/events/reject-submission", - FinishSubmissionStatus.Forwarded => - "https://schema.fitko.de/fit-connect/events/forward-submission", - _ => throw new ArgumentException("Invalid state") - }; - } - - public void CompleteSubmission(SubmissionForPickupDto submission, + /// <summary> + /// Accept or reject submission + /// </summary> + private void CompleteSubmission(Submission submission, FinishSubmissionStatus status, Problems[]? problems = null) { - if (submission.SubmissionId == null || submission.CaseId == null || + if (submission.Id == null || submission.CaseId == null || submission.DestinationId == null) throw new ArgumentException("Submission does not contain all required fields"); if (status != FinishSubmissionStatus.Rejected && problems != null) throw new ArgumentException("Problems can only be set for rejected submissions"); - var eventName = GetEvent(status); - - var token = - Encryption.CreateSecurityEventToken(submission.SubmissionId, submission.CaseId, - submission.DestinationId, eventName, problems); + var token = status switch { + FinishSubmissionStatus.Rejected => + Encryption.CreateRejectSecurityEventToken(submission.Id, + submission.CaseId, + submission.DestinationId, problems), + FinishSubmissionStatus.Accepted => Encryption.CreateAcceptSecurityEventToken( + submission), + _ => throw new ArgumentOutOfRangeException(nameof(status), status, null) + }; + Logger?.LogDebug("Token to accept submission: {Token}", token); var result = CasesService.FinishSubmission(submission.CaseId, token); - Logger?.LogInformation("Submission completed {status}", result); + Logger?.LogInformation("Submission completed {Status}", result); } public static string VerifyCallback(string callbackSecret, long timestamp, string body) { - if (timestamp < DateTime.Now.AddMinutes(-5).ToEpochTime()) + if (timestamp < DateTime.Now.AddMinutes(SecuritySpecification.MaxCallbackAge * -1) + .ToEpochTime()) throw new ArgumentException("Request is too old"); - - var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(callbackSecret)) - .ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}")); - - + var hmac = SecuritySpecification.CalculateCallbackHmac(callbackSecret, timestamp, body); return Convert.ToHexString(hmac).ToLower(); } + public static bool VerifyCallback(string callbackSecret, HttpRequest request) { if (!request.Headers.ContainsKey("callback-timestamp")) throw new ArgumentException("Missing callback-timestamp header"); var timeStampString = request.Headers["callback-timestamp"].ToString(); - if (!long.TryParse(timeStampString, out var timestamp)) + if (!long.TryParse(timeStampString, out var timestamp)) { throw new ArgumentException("Invalid callback-timestamp header"); + } var authentication = request.Headers["callback-authentication"]; @@ -234,6 +229,7 @@ public class Subscriber : FitConnectClient, public enum FinishSubmissionStatus { Accepted, - Rejected, - Forwarded + + Rejected + // Forwarded } diff --git a/IntegrationTests/CallbackTest.cs b/IntegrationTests/CallbackTest.cs index 503ca89c6bd85dfeff8590bb81987eae1b2dde78..86bd62d855ae0c8a6a964708a517f895c275beee 100644 --- a/IntegrationTests/CallbackTest.cs +++ b/IntegrationTests/CallbackTest.cs @@ -84,7 +84,7 @@ public class CallbackTest { Assert.Throws<ArgumentException>(() => { FitConnect.Subscriber.VerifyCallback(_callbackSecret, _request); }) - .Message.Should().Be("Request is too old"); + ?.Message.Should().Be("Request is too old"); } [Test] @@ -98,6 +98,6 @@ public class CallbackTest { Assert.Throws<ArgumentException>(() => { FitConnect.Subscriber.VerifyCallback(_callbackSecret, _request); }) - .Message.Should().Be("Verified request does not match authentication"); + ?.Message.Should().Be("Verified request does not match authentication"); } } diff --git a/IntegrationTests/CertificateValidation.cs b/IntegrationTests/CertificateValidation.cs new file mode 100644 index 0000000000000000000000000000000000000000..c0ca8ac6dc85117299741acddb3d5bdfc6e22d1b --- /dev/null +++ b/IntegrationTests/CertificateValidation.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security; +using System.Security.Cryptography.X509Certificates; +using Autofac; +using FitConnect; +using FitConnect.Encryption; +using FitConnect.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using MockContainer; +using Moq; +using NUnit.Framework; + +namespace IntegrationTests; + +[TestFixture] +public class CertificateValidation { + private MockSettings _settings = null!; + private ILogger _logger = null!; + private CertificateHelper _certificateHelper = null!; + + [SetUp] + public void Setup() { + var container = Container.Create(); + _settings = container.Resolve<MockSettings>(); + + _logger = LoggerFactory.Create( + builder => { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Debug); + }).CreateLogger("E2E Test"); + + + _certificateHelper = new CertificateHelper(_logger); + } + + [Test] + [Ignore("No credentials for dev environment")] + public void CheckCertificateInEnvironment_Dev() { + var environment = FitConnectEnvironment.Develop; + var sender = Client.GetSender(environment, _settings.SenderClientId, + _settings.SenderClientSecret, + _logger); + + var certificate = (sender as FitConnect.Sender)! + .GetPublicKeyFromDestination(_settings.DestinationId).Result; + new CertificateHelper(_logger).ValidateCertificate(JsonWebKey.Create(certificate), + LogLevel.Trace); + } + + [Test] + public void CheckCertificateInEnvironment_Testing() { + var environment = FitConnectEnvironment.Testing; + var sender = Client.GetSender(environment, _settings.SenderClientId, + _settings.SenderClientSecret, + _logger); + + var certificate = (sender as FitConnect.Sender)! + .GetPublicKeyFromDestination(_settings.DestinationId).Result; + new CertificateHelper(_logger).ValidateCertificate(JsonWebKey.Create(certificate), + LogLevel.Trace); + } + + + [Test] + [Ignore("No credentials for staging environment")] + public void CheckCertificateInEnvironment_Staging() { + var environment = FitConnectEnvironment.Staging; + var sender = Client.GetSender(environment, _settings.SenderClientId, + _settings.SenderClientSecret, + _logger); + + Assert.Throws<AggregateException>(() => { + sender.WithDestination(_settings.DestinationId) + .WithServiceType("", _settings.LeikaKey) + .WithAttachments(new Attachment("Test.pdf", "Simple Test PDF")) + .Submit(); + })!.InnerExceptions.Any(e => e.GetType() == typeof(SecurityException)).Should().BeTrue(); + } + + [Test] + [Ignore("No credentials for production environment")] + public void CheckCertificateInEnvironment_Production() { + var environment = FitConnectEnvironment.Production; + var sender = Client.GetSender(environment, _settings.SenderClientId, + _settings.SenderClientSecret, + _logger); + + Assert.Throws<AggregateException>(() => { + sender.WithDestination(_settings.DestinationId) + .WithServiceType("", _settings.LeikaKey) + .WithAttachments(new Attachment("Test.pdf", "Simple Test PDF")) + .Submit(); + })!.InnerExceptions.Any(e => e.GetType() == typeof(SecurityException)).Should().BeTrue(); + } + + [Test] + public void CheckPublicKeyEncryption() { + _certificateHelper.ValidateCertificate(new JsonWebKey(_settings.PublicKeyEncryption)) + .Should().BeFalse(); + } + + [Test] + public void CheckPublicKeySignature() { + _certificateHelper + .ValidateCertificate(new JsonWebKey(_settings.PublicKeySignatureVerification)) + .Should().BeFalse(); + } + + [Test] + public void CheckPrivateKeyDecryption() { + _certificateHelper.ValidateCertificate(new JsonWebKey(_settings.PrivateKeyDecryption)) + .Should().BeTrue(); + } + + [Test] + public void CheckSetPublicKey() { + _certificateHelper.ValidateCertificate(new JsonWebKey(_settings.SetPublicKeys)) + .Should().BeTrue(); + } + + [Test] + public void CheckPrivateKeySigning() { + _certificateHelper.ValidateCertificate(new JsonWebKey(_settings.PrivateKeySigning)) + .Should().BeTrue(); + } + + [Ignore("Not the scope of this branch - reactivate later")] + [Test] + public void CheckPemFiles() { + var files = Directory.GetFiles("./certificates"); + var success = 0; + var failed = 0; + var failedCerts = new List<string>(); + + foreach (var fileName in files) { + _logger.LogInformation("Checking file: {FileName}", fileName); + + var certificateContents = Directory.GetFiles("./certificates/roots"); + var rootCertificates = certificateContents + .Select(file => new X509Certificate2(file)).ToArray(); + certificateContents.Length.Should().Be(rootCertificates.Length); + + + if (fileName.EndsWith(".json")) { + var shouldFail = !fileName.Contains("/valid"); + var jwk = new JsonWebKey(File.ReadAllText(fileName)); + var valid = _certificateHelper.ValidateCertificate(jwk, + shouldFail ? LogLevel.Warning : LogLevel.Critical, rootCertificates + ); + + if (shouldFail) + valid = !valid; + + if (valid) { + success++; + } + else { + failed++; + failedCerts.Add(fileName); + } + } + else { + var certificate = new X509Certificate2(fileName); + var valid = _certificateHelper.ValidateCertificate(certificate, out var states, + null); + if (valid) { + success++; + } + else { + failed++; + failedCerts.Add(fileName); + } + } + } + + _logger.LogWarning("Failed certificates: {Certs}", + failedCerts.Aggregate("\n", (a, b) => a + "\t - " + b + "\n")); + _logger.LogInformation("Success: {Success}, Failed: {Failed}", success, failed); + failed.Should().Be(0); + } +} diff --git a/IntegrationTests/HelperMethods.cs b/IntegrationTests/HelperMethods.cs index e076c7014e78d6d0d189afeb49a04a98b34ce707..e5334dd76542651ce8e18296a04d32c5bdbf3b53 100644 --- a/IntegrationTests/HelperMethods.cs +++ b/IntegrationTests/HelperMethods.cs @@ -25,6 +25,6 @@ public static class HelperMethods { var jsonContent = File.ReadAllText(secretFile); var secret = JsonConvert.DeserializeObject<dynamic>(jsonContent); - return (secret.sender.id, secret.sender.secret); + return (secret!.sender.id, secret.sender.secret); } } diff --git a/IntegrationTests/IntegrationTests.csproj b/IntegrationTests/IntegrationTests.csproj index b72a272c095a5aec106e6b69d21df06851572f41..8dd3721c0ca364e3d36d5bcde31b0c97317accdd 100644 --- a/IntegrationTests/IntegrationTests.csproj +++ b/IntegrationTests/IntegrationTests.csproj @@ -15,6 +15,7 @@ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.22.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> + <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> <PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="coverlet.collector" Version="3.1.0" /> @@ -29,6 +30,156 @@ <None Update="Test.pdf"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> + <None Update="certificates\www-amazon-de.pem"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\www-amazon-de-zertifikatskette.pem"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\invalidEncJW_KeyUse.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\invalidEncJWK.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\invalidSigJWK.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\revokedEncJWK.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="temp\readme.md"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.21636.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.30244.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\root.pem"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.17478.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29249.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29267.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29284.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29302.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29319.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29336.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29353.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29370.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29387.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\root\ca.26281.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\root\ca.26305.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\root\ca.30244.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\root\root.pem"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1534.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1553.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1570.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1587.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1604.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1622.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1639.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1656.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1673.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.26281.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.26305.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\invalid_publicKey_encryption.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\invalid_publicKey_signature_verification.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\valid_productionKey.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1212.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1230.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1249.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1269.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1286.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1305.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1322.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1340.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1357.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\validEncJWK_KeyUse.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\ValidEncJWK.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\validSigJWK.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> </ItemGroup> </Project> diff --git a/IntegrationTests/JwtTokenValidation.cs b/IntegrationTests/JwtTokenValidation.cs index 7cafa1d712982e9e7447ae4ed7da7de78fcfdd4d..2db5089e63a7e3e73fa32adcfd247f6aa1d0cc70 100644 --- a/IntegrationTests/JwtTokenValidation.cs +++ b/IntegrationTests/JwtTokenValidation.cs @@ -1,4 +1,3 @@ -using System; using FitConnect.Encryption; using FluentAssertions; using Microsoft.IdentityModel.Logging; diff --git a/IntegrationTests/OAuthServiceTest.cs b/IntegrationTests/OAuthServiceTest.cs index 6f1f29fd860b4e9b0231ddba5f56eced4f2c9144..5291ca1173bf30e5251105fae844208ad8be6afe 100644 --- a/IntegrationTests/OAuthServiceTest.cs +++ b/IntegrationTests/OAuthServiceTest.cs @@ -7,8 +7,8 @@ using NUnit.Framework; namespace IntegrationTests; public class OAuthServiceTest { - private string _clientId; - private string _clientSecret; + private string _clientId = null!; + private string _clientSecret = null!; private OAuthService _oAuthService = null!; [OneTimeSetUp] diff --git a/IntegrationTests/ProxyTest.cs b/IntegrationTests/ProxyTest.cs index 2024efe0d5098ffba29e1c07ba7e63581702ac1b..7d7fee7f90337f1e562d626506ad85de5de6cee1 100644 --- a/IntegrationTests/ProxyTest.cs +++ b/IntegrationTests/ProxyTest.cs @@ -13,6 +13,8 @@ using NUnit.Framework; namespace IntegrationTests; +// Change to https://hub.docker.com/r/mitmproxy/mitmproxy/ + [Ignore("Not testable in docker container, take to long to run every time")] [TestFixture] public class ProxyTest { @@ -25,8 +27,8 @@ public class ProxyTest { File.WriteAllText("proxy/access.log", ""); _container = new TestcontainersBuilder<TestcontainersContainer>() - .WithImage("ubuntu/squid") - .WithPortBinding("3128", "3128") + .WithImage("mitmproxy/mitmproxy") + .WithPortBinding("3128", "8081") .WithBindMount(path, @"/var/log/squid") .Build(); _container.StartAsync().Wait(); @@ -39,9 +41,11 @@ public class ProxyTest { new FitConnect.Sender(FitConnectEnvironment.Testing, _id, _secret) - .WithProxy("localhost", 3128); + .WithProxy("localhost", 3128, null, null); +#pragma warning disable SYSLIB0014 _webClient = new WebClient { +#pragma warning restore SYSLIB0014 Proxy = new WebProxy("http://localhost:3128") }; diff --git a/IntegrationTests/Routing/RoutingTests.cs b/IntegrationTests/Routing/RoutingTests.cs index 3635e72c0ad53c422b14b1ad955501014a0b1d95..c87cbd5d8770a864828b85262e4ce4efbc965b5f 100644 --- a/IntegrationTests/Routing/RoutingTests.cs +++ b/IntegrationTests/Routing/RoutingTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Text; using FitConnect; @@ -17,11 +17,6 @@ namespace IntegrationTests.Routing; [TestFixture] public class RoutingTests { - private IRouter _router; - private ILogger _logger; - private string _body; - private string _detached; - [SetUp] public void Setup() { _logger = LoggerFactory.Create( @@ -37,6 +32,11 @@ public class RoutingTests { @"{""encryptionKid"":""1e95f036-ccff-425c-a0de-3d89d5cc59fa"",""metadataVersions"":[""1.0.0""],""publicKeys"":{""keys"":[{""alg"":""RSA-OAEP-256"",""e"":""AQAB"",""key_ops"":[""wrapKey""],""kid"":""1e95f036-ccff-425c-a0de-3d89d5cc59fa"",""kty"":""RSA"",""n"":""2ch1Ir3_Lyb9_HxW9RqIodxi9fXhix6APKwqiSfi-JlRqVa1FoAFsW1nW0IbQjkW6sNkWUFWuA9AfVoKT9nnIcnLSjSQ84SI-if6qTbornyKvBjXg8BSecSUUPYyT0-4NmxrXMGHPYbJV7fQq6jPXzkWC5P5jqQ7ObraQp752BcE_JVQUmFk1ydhhbnroHpGUkA-8jG_kiVL-lAz7uUmZCh6i3ZJD5HN1JOE5LMyzUQOgOFUUPiviBywQAbPQuDLydZ2diO5wqm4mwBadAAzC27OllSkNXSgnd9MVajXmtBVpz2ksMaSCAbfB4rK9q5jXd5YMwu1ZlA-ZuWKYm_p1GjbZdx4xk9w23Zkgnrr3SvWnW98686fd03MG1ACAGatq5FcAvGp8BXCKwz5FpyYtOONx-tECYHcHhx_SafOe9siLYObLmBSsLF3TAjigZjpGOuEjBtKyv5OwJj-6YfIYYjlofuqv6GHUGDvv8iQsy6U4eHCoRpKJzmX6L22MUQgisYvQdGY2jbdEni3g_MpciMIbnZFLENVrqHXYcgHN-SbXl_GVR5b3F0ompES55xA7fuYlt4lp5j0IUo0OWM2_tYYHtASZicVAwnbzLQZEA0u-wXZr0ByMWE07Od_KaLUomlBPi1Ac_FU3KOx0APKJUm7D3__aiLZll3Sh9EnIvE"",""x5c"":[""MIIE6jCCAtKgAwIBAgIGAXo1pG0GMA0GCSqGSIb3DQEBCwUAMDYxNDAyBgNVBAMMK2FaV0ptMG1XRmFxRGFOTkJOaDNabVluVmw3eG52SVMxVEJ5SHhaVnNUNm8wHhcNMjEwNjIyMjEzMzI2WhcNMjIwNDE4MjEzMzI2WjA2MTQwMgYDVQQDDCthWldKbTBtV0ZhcURhTk5CTmgzWm1ZblZsN3hudklTMVRCeUh4WlZzVDZvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ch1Ir3/Lyb9/HxW9RqIodxi9fXhix6APKwqiSfi+JlRqVa1FoAFsW1nW0IbQjkW6sNkWUFWuA9AfVoKT9nnIcnLSjSQ84SI+if6qTbornyKvBjXg8BSecSUUPYyT0+4NmxrXMGHPYbJV7fQq6jPXzkWC5P5jqQ7ObraQp752BcE/JVQUmFk1ydhhbnroHpGUkA+8jG/kiVL+lAz7uUmZCh6i3ZJD5HN1JOE5LMyzUQOgOFUUPiviBywQAbPQuDLydZ2diO5wqm4mwBadAAzC27OllSkNXSgnd9MVajXmtBVpz2ksMaSCAbfB4rK9q5jXd5YMwu1ZlA+ZuWKYm/p1GjbZdx4xk9w23Zkgnrr3SvWnW98686fd03MG1ACAGatq5FcAvGp8BXCKwz5FpyYtOONx+tECYHcHhx/SafOe9siLYObLmBSsLF3TAjigZjpGOuEjBtKyv5OwJj+6YfIYYjlofuqv6GHUGDvv8iQsy6U4eHCoRpKJzmX6L22MUQgisYvQdGY2jbdEni3g/MpciMIbnZFLENVrqHXYcgHN+SbXl/GVR5b3F0ompES55xA7fuYlt4lp5j0IUo0OWM2/tYYHtASZicVAwnbzLQZEA0u+wXZr0ByMWE07Od/KaLUomlBPi1Ac/FU3KOx0APKJUm7D3//aiLZll3Sh9EnIvECAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAFi24JFEmYL2sOqaaQOMHCVm+WQ9QyN/rkvjHLG8th5TIF8NJ4uwxl+n6GBO2uOZOBixHbU5pK9slyt9Xryw7CbL/hhRGSs3GSFR2hkxoBHdzfFnAwegnd1H5XHQwSgh43jGOn9AxsyISpkvFo7+DObh9Cv8NsjpL57No4UJ62XggfYJW43u4+I/fHcDwnvIN0dvnYpQkCbTmrjLa5IkGim9BynfW1k6VuxLya1SsyjBHWw2YNQ4xBJI4OzXhL6OmBSrohF1RbIKOpjtqGfkXZpxufLNV2CnL36r/41c1nop6cmCIMDtnFEQdAmGe8m/8wvVpLnks59C02/WotlK3iORHCYB6G0pHMKFB4zOVANYtLFgqTgN4HNciV3FN0TvI19qzjkAdcB+m+L+LdseIzcQ/BToGyPvWkJ1mvJZIp0ejnlMWIl3VlNpMKeZ7lJbPpZvABO00lK+FynhITtb6N29toE+7JgHAlWmxw6PFFY1x+3xTHBTOU0oUR/TyKsEU0+bNSb/0S+ZyodmnIFbgYWarjK5pUwfTRyPyeVEukg1Gf30c/7f/5KZ/dpLFUNBb/YTNIzYEhGNUyLJ1mrSz33gr4MtvI4uSu0Jpr1NrwdMGvFhr5QOCULuoC9KlokusUpi0GTH0gK3K/TUi6qvU+Wztfa7mqah17BVVFT1wATs=""]}]},""replyChannels"":{""email"":{""usePgp"":true}},""status"":""active"",""submissionSchemas"":[{""mimeType"":""application/json"",""schemaUri"":""https://schema.fitko.de/fim/s06000178_0.4.schema.json""}],""submissionUrl"":""https://submission-api-testing.fit-connect.fitko.dev""}"; } + private IRouter _router = null!; + private ILogger _logger = null!; + private string _body = null!; + private string _detached = null!; + [Test] [TestCase("99123456760610", "064350014014")] @@ -121,6 +121,7 @@ public class RoutingTests { [Test] [Order(80)] public void BaseSignatureTest() { + var parameter = JsonConvert.DeserializeObject(_body); // Get Key from SubmissionAPI var parameterJson = JsonConvert.SerializeObject(parameter, @@ -159,12 +160,12 @@ public class RoutingTests { var combined = _detached.Replace("..", $".{Base64Url.Encode(Encoding.UTF8.GetBytes(parameterJson))}."); - var secret = new Router(FitConnectEnvironment.Testing, _logger) + var secret = Router .GetSubmissionServiceValidationJwk( "https://submission-api-testing.fit-connect.fitko.dev").Result; var keySet = new JsonWebKeySet(secret); - var result = FitEncryption.VerifyJwt(combined, (JsonWebKey)keySet.Keys[0], _logger); + var result = FitEncryption.VerifyJwt(combined, keySet.Keys[0], _logger); Assert.IsTrue(result); } @@ -172,12 +173,10 @@ public class RoutingTests { [Test] [Order(100)] public void BaseSignatureTest_DownloadKeyAndRoute() { - // GET destination from here: https://dvdv-testsystem.governikus.de/dvdv-fitconnect/v1/destinations/{id - var body = - ((new DvdvService("https://dvdv-testsystem.governikus.de/dvdv-fitconnect", - logger: _logger)).GetDestinationJson("d40e7b13-da98-4b09-9e16-bbd61ca81510") - .Result); + new DvdvService("https://dvdv-testsystem.governikus.de/dvdv-fitconnect", + logger: _logger).GetDestinationJson("d40e7b13-da98-4b09-9e16-bbd61ca81510") + .Result; var parameter = JsonConvert.DeserializeObject<DestinationParameters>(body); // Get Key from SubmissionAPI @@ -195,12 +194,12 @@ public class RoutingTests { Console.WriteLine(combined); - var secret = new Router(FitConnectEnvironment.Testing, _logger) + var secret = Router .GetSubmissionServiceValidationJwk( "https://submission-api-testing.fit-connect.fitko.dev").Result; var keySet = new JsonWebKeySet(secret); - var result = FitEncryption.VerifyJwt(combined, (JsonWebKey)keySet.Keys[0], _logger); + var result = FitEncryption.VerifyJwt(combined, keySet.Keys[0], _logger); Assert.IsTrue(result); } diff --git a/IntegrationTests/Sender/SenderTestBase.cs b/IntegrationTests/Sender/SenderTestBase.cs index 24fc1c7a1cf226a82fb21701ca4daae0c6297079..146d5921e12363e9227feca919d28d66820b98dc 100644 --- a/IntegrationTests/Sender/SenderTestBase.cs +++ b/IntegrationTests/Sender/SenderTestBase.cs @@ -36,9 +36,9 @@ public abstract class SenderTestBase { } var jsonContent = File.ReadAllText(secretFile); - var secret = JsonConvert.DeserializeObject<dynamic>(jsonContent); - _clientId = secret.sender.id; - _clientSecret = secret.sender.secret; + var secret = JsonConvert.DeserializeObject<dynamic>(jsonContent)!; + ClientId = secret.sender.id; + ClientSecret = secret.sender.secret; } [SetUp] @@ -46,21 +46,21 @@ public abstract class SenderTestBase { var logger = LoggerFactory.Create(b => b.AddConsole()).CreateLogger<FitConnect.Sender>(); Sender = new FitConnect.Sender( FitConnectEnvironment.Testing, - _clientId, _clientSecret, logger); + ClientId, ClientSecret, logger); } protected string LeikaKey = ""; - protected const string desitnationId = "aa3704d6-8bd7-4d40-a8af-501851f93934"; - protected string _clientId = "73a8ff88-076b-4263-9a80-8ebadac97b0d"; - protected string _clientSecret = "rdlXms-4ikO47AbTmmCTdzFoE4cTSt13JmSbcY5Dhsw"; - protected ISender Sender; + protected const string DestinationId = "aa3704d6-8bd7-4d40-a8af-501851f93934"; + protected string ClientId = "73a8ff88-076b-4263-9a80-8ebadac97b0d"; + protected string ClientSecret = "rdlXms-4ikO47AbTmmCTdzFoE4cTSt13JmSbcY5Dhsw"; + protected ISender Sender = null!; protected Submission GetSubmissionInfo(ISender sender) { var submission = sender.GetType().GetProperties() - .FirstOrDefault(p => p.Name == "Submission") + .FirstOrDefault(p => p.Name == "Submission")! .GetValue(Sender) as Submission; - return submission; + return submission!; } } diff --git a/IntegrationTests/Sender/SenderTestHappyPath.cs b/IntegrationTests/Sender/SenderTestHappyPath.cs index a3c451c1fe1628a69aecc3127b237a8ab3dfa8e3..89b33074301e82595b1b68adced0f28b72a11b04 100644 --- a/IntegrationTests/Sender/SenderTestHappyPath.cs +++ b/IntegrationTests/Sender/SenderTestHappyPath.cs @@ -12,8 +12,8 @@ namespace IntegrationTests.Sender; public class SenderTestHappyPath : SenderTestBase { [Test] public void CheckIfSecretsAreValid() { - _clientId.Should().NotBe("00000000-0000-0000-0000-000000000000"); - _clientSecret.Should().NotBe("0000000000000000000000000000000000000000000"); + ClientId.Should().NotBe("00000000-0000-0000-0000-000000000000"); + ClientSecret.Should().NotBe("0000000000000000000000000000000000000000000"); } [Test] @@ -21,7 +21,7 @@ public class SenderTestHappyPath : SenderTestBase { // Arrange // Act - Sender.WithDestination(desitnationId); + Sender.WithDestination(DestinationId); // Assert Sender.PublicKey.Should().NotBeNullOrEmpty(); @@ -31,7 +31,7 @@ public class SenderTestHappyPath : SenderTestBase { public void WithAttachments_IntroduceSubmission_ShouldGetIdFromServer() { // Arrange var dut = Sender - .WithDestination(desitnationId) + .WithDestination(DestinationId) .WithServiceType("ServiceName", "urn:de:fim:leika:leistung:99400048079000"); var attachments = new List<Attachment>(); @@ -61,7 +61,8 @@ public class SenderTestHappyPath : SenderTestBase { [TestCase("0b8e6fbd-62e2-4b6f-b333-5308d82e0a00")] [TestCase("d2be2027-9368-4c0c-a265-2fdbf7ecd4d9")] public void GetSubmissionStatus(string caseId) { - var status = Sender.GetStatusForSubmission(caseId); + var status = + Sender.GetStatusForSubmission(caseId, "aa3704d6-8bd7-4d40-a8af-501851f93934", true); status.ForEach(s => Console.WriteLine($"{s.EventTime} - {s.EventType}")); status.Count.Should().BeGreaterThan(0); } @@ -75,7 +76,7 @@ public class SenderTestHappyPath : SenderTestBase { var attachment = new Attachment("Test.pdf", "Just an attachment"); attachments.Add(attachment); var dut = Sender - .WithDestination(desitnationId) + .WithDestination(DestinationId) .WithServiceType("ServiceName", "urn:de:fim:leika:leistung:99400048079000") .WithAttachments(attachments); @@ -103,7 +104,7 @@ public class SenderTestHappyPath : SenderTestBase { }; attachments.Add(attachment); var dut = Sender - .WithDestination(desitnationId) + .WithDestination(DestinationId) .WithServiceType("ServiceName", "urn:de:fim:leika:leistung:99400048079000") .WithAttachments(attachments) .WithData(JsonConvert.SerializeObject(new { @@ -118,7 +119,7 @@ public class SenderTestHappyPath : SenderTestBase { // Assert var submission = GetSubmissionInfo(Sender); - var statusForSubmission = Sender.GetStatusForSubmission(submission.CaseId); + var statusForSubmission = Sender.GetStatusForSubmission(submission); foreach (var securityEventToken in statusForSubmission) Console.WriteLine(securityEventToken.Token.Subject); @@ -159,7 +160,7 @@ public class SenderTestHappyPath : SenderTestBase { // Assert var submission = GetSubmissionInfo(Sender); - var statusForSubmission = Sender.GetStatusForSubmission(submission.CaseId); + var statusForSubmission = Sender.GetStatusForSubmission(submission); foreach (var securityEventToken in statusForSubmission) Console.WriteLine(securityEventToken.Token.Subject); diff --git a/IntegrationTests/Sender/SenderTestUnhappyPath.cs b/IntegrationTests/Sender/SenderTestUnhappyPath.cs index f2de015cf55858ab1b1aeb18f9ab057dbc98dcf9..6e0f9c7e4bff7d67266d6c792b87971d00d00139 100644 --- a/IntegrationTests/Sender/SenderTestUnhappyPath.cs +++ b/IntegrationTests/Sender/SenderTestUnhappyPath.cs @@ -10,25 +10,25 @@ namespace IntegrationTests.Sender; public class SenderTestUnhappyPath : SenderTestBase { [Test] public void WithDestination_UnknownUUID_ShouldThrowAggregateWithInnerHttpRequestException() { - Assert.Throws<AggregateException>(() => { Sender.WithDestination(Guid.Empty.ToString()); }) + Assert.Throws<AggregateException>(() => { Sender.WithDestination(Guid.Empty.ToString()); })! .InnerExceptions.Should().ContainItemsAssignableTo<HttpRequestException>(); } [Test] public void WithDestination_CompletelyWrong_ShouldThrowArgumentException() { - Assert.Throws<ArgumentException>(() => { Sender.WithDestination("This is very wrong"); }) + Assert.Throws<ArgumentException>(() => { Sender.WithDestination("This is very wrong"); })! .Message.Should().Be("The destination must be a valid GUID"); } [Test] public void WithData_DataIsInvalidJson_ShouldThrowArgumentException() { Assert.Throws<ArgumentException>(() => { - Sender.WithDestination(desitnationId) + Sender.WithDestination(DestinationId) .WithServiceType("Name", "urn:de:fim:leika:leistung:00000000000000") .WithAttachments(new Attachment("Test.pdf", "Test PDF")) .WithData("This is very wrong"); - }) + })! .Message.Should().Be("The data must be valid JSON string"); } } diff --git a/IntegrationTests/Sender/ThreadTest.cs b/IntegrationTests/Sender/ThreadTest.cs index 4342d36af08a42eeeeb329ac83720470752b5ce5..2588b1db76c0418696901346514e9156392bab15 100644 --- a/IntegrationTests/Sender/ThreadTest.cs +++ b/IntegrationTests/Sender/ThreadTest.cs @@ -29,9 +29,9 @@ public class ThreadTest { .CreateLogger<FitConnect.Sender>(); } - private IContainer _container; - private MockSettings _setting; - private ILogger _logger; + private IContainer _container = null!; + private MockSettings _setting = null!; + private ILogger _logger = null!; private const int NumberOfThreads = 34; [Test] @@ -42,11 +42,11 @@ public class ThreadTest { for (var i = 0; i < NumberOfThreads; i++) tasks.Add(Task.Run(() => { var counter = Thread.CurrentThread.ManagedThreadId; - var Sender = new FitConnect.Sender( + var sender = new FitConnect.Sender( FitConnectEnvironment.Testing, _setting.SenderClientId, _setting.SenderClientSecret, _logger); - var delayed = Sender.WithDestination(_setting.DestinationId) + var delayed = sender.WithDestination(_setting.DestinationId) .WithServiceType($"ThreadTest_{counter}", _setting.LeikaKey) .WithAttachments(new Attachment("Test.pdf", $"Attachment_{counter}")); @@ -75,7 +75,7 @@ public class ThreadTest { foreach (var submission in submissions) subscriber - .RequestSubmission(submission.SubmissionId) + .RequestSubmission(submission.Id!) .AcceptSubmission(); submissions.Count.Should().Be(NumberOfThreads); diff --git a/IntegrationTests/Subscriber/SubscriberTestBase.cs b/IntegrationTests/Subscriber/SubscriberTestBase.cs index f4b2684a0c0b53d8279b4203f8f62d733332b7ae..ace3c93995db3181157ee2d589ded0aa9a3b68e8 100644 --- a/IntegrationTests/Subscriber/SubscriberTestBase.cs +++ b/IntegrationTests/Subscriber/SubscriberTestBase.cs @@ -8,12 +8,12 @@ using NUnit.Framework; namespace IntegrationTests.Subscriber; public abstract class SubscriberTestBase { - protected const string desitnationId = "aa3704d6-8bd7-4d40-a8af-501851f93934"; - protected string _clientId = "20175c2b-c4dd-4a01-99b1-3a08436881a1"; - protected string _clientSecret = "KV2qd7qc5n-xESB6dvfrTlMDx2BWHJd5hXJ6pKKnbEQ"; - private IContainer _container; - protected ILogger Logger; - protected ISubscriber subscriber { get; set; } + protected const string DestinationId = "aa3704d6-8bd7-4d40-a8af-501851f93934"; + private const string ClientId = "20175c2b-c4dd-4a01-99b1-3a08436881a1"; + private const string ClientSecret = "KV2qd7qc5n-xESB6dvfrTlMDx2BWHJd5hXJ6pKKnbEQ"; + private IContainer _container = null!; + protected ILogger Logger = null!; + protected ISubscriber Subscriber { get; set; } = null!; [OneTimeSetUp] public void OneTimeSetUp() { @@ -31,10 +31,10 @@ public abstract class SubscriberTestBase { b.SetMinimumLevel(LogLevel.Trace); }) .CreateLogger<FitConnect.Sender>(); - subscriber = new FitConnect.Subscriber( + Subscriber = new FitConnect.Subscriber( FitConnectEnvironment.Testing, - _clientId, - _clientSecret, + ClientId, + ClientSecret, _container.Resolve<MockSettings>().PrivateKeyDecryption, _container.Resolve<MockSettings>().PrivateKeySigning, _container.Resolve<MockSettings>().PublicKeyEncryption, diff --git a/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs b/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs index 0837029663223585e5bdc0d60d4c96ab6cf401db..9255aed0061d7c5af65a71c18052bac6d2a418b9 100644 --- a/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs +++ b/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs @@ -16,18 +16,18 @@ public class SubscriberTestHappyPath : SubscriberTestBase { [Test] public void GetAvailableSubmissions_WithDestinationId_ShouldReturnSubmissionsForPickupDto() { // Act - var submissions = subscriber.GetAvailableSubmissions(desitnationId).ToList(); + var submissions = Subscriber.GetAvailableSubmissions(DestinationId).ToList(); // Assert submissions.Count().Should().BeGreaterThanOrEqualTo(2); - submissions.All(s => s.DestinationId == desitnationId).Should().BeTrue(); + submissions.All(s => s.DestinationId == DestinationId).Should().BeTrue(); } [Order(202)] [Test] public void GetAvailableSubmissions_WithOutDestinationId_ShouldReturnSubmissionsForPickupDto() { // Act - var submissions = subscriber.GetAvailableSubmissions().ToList(); + var submissions = Subscriber.GetAvailableSubmissions().ToList(); // Assert submissions.Count().Should().BeGreaterThan(0); @@ -39,13 +39,13 @@ public class SubscriberTestHappyPath : SubscriberTestBase { public void GetAllSubmission_WithSubmissionId_ShouldReturnSubmissionsForPickupDto() { // Arrange var errorCounter = 0; - var submissions = subscriber.GetAvailableSubmissions().ToList(); + var submissions = Subscriber.GetAvailableSubmissions().ToList(); submissions.Count().Should().BeGreaterThan(0); var i = 0; - foreach (var submissionId in submissions.Select(s => s.SubmissionId)) { + foreach (var submissionId in submissions.Select(s => s.Id!)) { // Act Console.WriteLine($"Getting submission {submissionId}"); - var dto = subscriber.RequestSubmission(submissionId); + var dto = Subscriber.RequestSubmission(submissionId); // Assert errorCounter.Should().BeLessThan(submissions.Count()); @@ -53,7 +53,7 @@ public class SubscriberTestHappyPath : SubscriberTestBase { if (i++ % 2 == 0) dto.AcceptSubmission(); else - dto.RejectSubmission(new Problems { Description = "A really critical problem" }); + dto.RejectSubmission(Problems.MissingSchema); } } @@ -62,9 +62,9 @@ public class SubscriberTestHappyPath : SubscriberTestBase { public void GetAttachment_WithSubmissionId_ShouldReturnSubmissionsForPickupDto( string submissionId) { // Act - var attachments = subscriber.RequestSubmission(submissionId).GetAttachments(); + var attachments = Subscriber.RequestSubmission(submissionId).GetAttachments(); foreach (var attachment in attachments) { - attachment.Content.Length.Should().BeGreaterThan(0); + attachment.Content!.Length.Should().BeGreaterThan(0); File.WriteAllBytes("attachments/test.pdf", attachment.Content); } } @@ -72,11 +72,11 @@ public class SubscriberTestHappyPath : SubscriberTestBase { [Test] [Order(203)] public void GetStatus_ForAllPendingSubmissions() { - var submissions = subscriber.GetAvailableSubmissions().ToList(); + var submissions = Subscriber.GetAvailableSubmissions().ToList(); submissions.Count().Should().BeGreaterThan(0); foreach (var submission in submissions) - subscriber.GetStatusForSubmission(submission.CaseId).ForEach(s => + Subscriber.GetStatusForSubmission(submission).ForEach(s => Logger.LogInformation("{SubmissionCaseId} - {ObjEventTime} - {ObjEventType}", submission.CaseId, s.EventTime, s.EventType)); } @@ -86,20 +86,20 @@ public class SubscriberTestHappyPath : SubscriberTestBase { [Order(204)] public void GetAttachment_FromAllPendingSubmission_ShouldReturnAttachment() { // Arrange - var submissions = subscriber.GetAvailableSubmissions().ToList(); + var submissions = Subscriber.GetAvailableSubmissions().ToList(); submissions.Count().Should().BeGreaterThan(0); foreach (var submission in submissions) { Console.WriteLine( - $"Getting submission {submission.SubmissionId} - case {submission.CaseId}"); - var submissionId = submission.SubmissionId!; + $"Getting submission {submission.Id} - case {submission.CaseId}"); + var submissionId = submission.Id!; if (!Directory.Exists($"./attachments/{submissionId}/")) Directory.CreateDirectory($"./attachments/{submissionId}/"); - subscriber.GetStatusForSubmission(submission.CaseId).ForEach(s => + Subscriber.GetStatusForSubmission(submission).ForEach(s => Console.WriteLine($"{s.EventTime} - {s.EventType}")); - var subscriberWithSubmission = subscriber + var subscriberWithSubmission = Subscriber .RequestSubmission(submissionId, true); var attachments = subscriberWithSubmission .GetAttachments(); @@ -107,8 +107,8 @@ public class SubscriberTestHappyPath : SubscriberTestBase { if ((attachment?.Content?.Length ?? 0) > 0) File.WriteAllBytes( Path.Combine($"./attachments/{submissionId}/", - attachment.Filename), - attachment.Content); + attachment!.Filename!), + attachment.Content!); Console.WriteLine($"Json Fachdaten: \r\n{subscriberWithSubmission.GetDataJson()}"); Console.WriteLine($"Success {submissionId}"); @@ -123,14 +123,14 @@ public class SubscriberTestHappyPath : SubscriberTestBase { public void GetSingleSubmission_WithSubmissionId_ShouldReturnSubmissionsForPickupDto( string submissionId) { // Arrange - var submissions = subscriber.GetAvailableSubmissions().ToList(); + var submissions = Subscriber.GetAvailableSubmissions().ToList(); submissions.Count().Should().BeGreaterThan(0); Console.WriteLine($"Getting submission {submissionId}"); - var dto = subscriber.RequestSubmission(submissionId); + var dto = Subscriber.RequestSubmission(submissionId); // Assert - dto.Submission.Id.Should().Be(submissionId); + dto.Submission!.Id.Should().Be(submissionId); Console.WriteLine(dto.Submission.Data); diff --git a/IntegrationTests/Subscriber/SubscriberTestUnHappyPath.cs b/IntegrationTests/Subscriber/SubscriberTestUnHappyPath.cs index ff8cda3c79431e77d1771ab9f70d6cd0849aa1a5..6da31d6854a799bee618eef447692f6ab538d803 100644 --- a/IntegrationTests/Subscriber/SubscriberTestUnHappyPath.cs +++ b/IntegrationTests/Subscriber/SubscriberTestUnHappyPath.cs @@ -1,4 +1,5 @@ using System.Linq; +using FitConnect.Encryption; using FluentAssertions; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -14,11 +15,11 @@ public class SubscriberTestUnHappyPath : SubscriberTestBase { var wrongData = "{\"name\":\"value\"}"; // Act - var validationErrors = FitConnect.Subscriber.VerifyMetadata(wrongData); - validationErrors.ToList().ForEach(v => Logger?.LogWarning("ERROR: {V}", v.ToString())); + var valid = JsonHelper.VerifyMetadata(wrongData); + // validationErrors.ToList().ForEach(v => Logger?.LogWarning("ERROR: {V}", v.ToString())); // Assert - validationErrors.Count.Should().BeGreaterThan(0); + valid.Should().BeFalse(); } [Test] @@ -28,8 +29,8 @@ public class SubscriberTestUnHappyPath : SubscriberTestBase { // Act && Assert Assert.Throws<JsonReaderException>(() => { - var validationErrors = FitConnect.Subscriber.VerifyMetadata(wrongData); - validationErrors.ToList().ForEach(v => Logger.LogWarning("ERROR: {V}", v.ToString())); + var valid = JsonHelper.VerifyMetadata(wrongData, Logger); + // validationErrors.ToList().ForEach(v => Logger.LogWarning("ERROR: {V}", v.ToString())); }); } } diff --git a/IntegrationTests/temp/readme.md b/IntegrationTests/temp/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MockContainer/MockContainer.cs b/MockContainer/MockContainer.cs index cc86b4352d92891d372d4f840a684244c5e77289..81f9b7f91b6a42029106470c8e250117e7e9453d 100644 --- a/MockContainer/MockContainer.cs +++ b/MockContainer/MockContainer.cs @@ -1,7 +1,4 @@ -using System.Text.Encodings.Web; -using System.Text.Json; -using Newtonsoft.Json; -using Autofac; +using Autofac; using FitConnect.Encryption; using FitConnect.Models.Api.Metadata; using FitConnect.Services.Interfaces; @@ -11,7 +8,6 @@ using Microsoft.Extensions.Logging; using Moq; using Newtonsoft.Json; using Data = FitConnect.Models.Data; -using JsonSerializer = System.Text.Json.JsonSerializer; using Metadata = FitConnect.Models.Api.Metadata.Metadata; using Route = FitConnect.Services.Models.v1.Routes.Route; @@ -20,7 +16,7 @@ namespace MockContainer; public record MockSettings(string PrivateKeyDecryption, string PrivateKeySigning, string PublicKeyEncryption, string PublicKeySignatureVerification, string SenderClientId, string SenderClientSecret, string SubscriberClientId, string SubscriberClientSecret, - string DestinationId, string LeikaKey, string CallbackSecret); + string DestinationId, string LeikaKey, string CallbackSecret, string SetPublicKeys); public class TestFile { public byte[] Content; @@ -44,7 +40,7 @@ public static class Container { builder.Register(c => Mock.Of<ICasesService>()).As<ICasesService>(); builder.Register(c => LoggerFactory.Create( b => { - b.AddSimpleConsole(); + b.AddConsole(); b.SetMinimumLevel(LogLevel.Information); }).CreateLogger("FluentSenderTests") ).As<ILogger>(); @@ -67,10 +63,11 @@ public static class Container { var publicKeyEncryption = File.ReadAllText("./encryptionKeys/publicKey_encryption.json"); var publicKeySignature = File.ReadAllText("./encryptionKeys/publicKey_signature_verification.json"); + var setPublicKeys = File.ReadAllText("./encryptionKeys/set-public-keys.json"); var credentials = JsonConvert.DeserializeObject<dynamic>( - File.ReadAllText("./encryptionKeys/credentials.json")); + File.ReadAllText("./encryptionKeys/credentials.json"))!; var senderClientId = (string)credentials.sender.clientId; var senderClientSecret = (string)credentials.sender.clientSecret; @@ -85,7 +82,7 @@ public static class Container { publicKeyEncryption, publicKeySignature, senderClientId, senderClientSecret, subscriberClientId, subscriberClientSecret, - destinationId, leikaKey, callbackSecret)) + destinationId, leikaKey, callbackSecret, setPublicKeys)) .As<MockSettings>(); builder.Register(c => new KeySet { PrivateKeyDecryption = privateKeyDecryption, @@ -117,8 +114,9 @@ public static class Container { submissionService.Setup(s => s.ListSubmissions(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).Returns( () => - Task.Run(() => JsonConvert.DeserializeObject<SubmissionsForPickupDto>( - @"{""offset"":0,""count"":3,""totalCount"":3,""submissions"":[{""destinationId"":""879ee109-a690-4db8-ab32-424284184d7d"",""submissionId"":""ce75a6b8-d72f-4b94-b09e-af6be35bc2ae""},{""destinationId"":""19c8489b-29b8-422f-b7db-919852cfb04b"",""submissionId"":""e364430f-5a3b-4284-ba9a-f2867ba421e6""},{""destinationId"":""80a0aac3-148d-42bb-9366-516ce6355348"",""submissionId"":""530ba588-2db9-4899-ab0d-0c0b57689271""}]}")) + JsonConvert.DeserializeObject<SubmissionsForPickupDto>( + @"{""offset"":0,""count"":3,""totalCount"":3,""submissions"":[{""destinationId"":""879ee109-a690-4db8-ab32-424284184d7d"",""submissionId"":""ce75a6b8-d72f-4b94-b09e-af6be35bc2ae""},{""destinationId"":""19c8489b-29b8-422f-b7db-919852cfb04b"",""submissionId"":""e364430f-5a3b-4284-ba9a-f2867ba421e6""},{""destinationId"":""80a0aac3-148d-42bb-9366-516ce6355348"",""submissionId"":""530ba588-2db9-4899-ab0d-0c0b57689271""}]}") + ! ); submissionService.Setup(s => s.CreateSubmission(It.IsAny<CreateSubmissionDto>())).Returns( () => new SubmissionCreatedDto { @@ -135,11 +133,11 @@ public static class Container { (string id, SubmitSubmissionDto dto) => { Console.WriteLine( $@"Submitting submission {id} with - {JsonConvert.SerializeObject(dto, new JsonSerializerSettings() { + {JsonConvert.SerializeObject(dto, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented })}"); - return Task.Run(() => new SubmissionReducedDto()); + return Task.Run(() => new SubmissionReducedDto())!; }); var data = new Data(); diff --git a/readme.md b/readme.md index 06ba1661eb5d6fb7cb5eec162ff059d497d9ff19..edcfc7c0ce308c7a30e6aae349810f15034b87f2 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,8 @@ +<br /> +<div align="center"><img src="https://www.fitko.de/fileadmin/_processed_/b/9/csm_FIT-Connect_3e8e926015.jpg" alt="Logo" width="50%" height="50%"> </div> + + + # FIT-Connect .NET SDK For an implementation example take a look at the [DemoRunner](DemoRunner/Program.cs) @@ -11,8 +16,8 @@ Die Verwendung des SDKs ist in der [documentation (ger)](./Documentation/documen ## For information how to use the SDK and FIT-Connect visit: * [SDK-Dokumentation](./documentation/documentation.de-DE.md) -* [FIT-Connect Dokumentation](https://docs.fitko.de/fit-connect/docs) -* [FIT-Connect Dokumentation](https://fit-connect.com/docs) +* [FIT-Connect Dokumentation](https://docs.fitko.de/FIT-Connect/docs) +* [FIT-Connect Dokumentation](https://FIT-Connect.com/docs) <!-- ## Structure @@ -30,14 +35,14 @@ Das FIT-Connect SDK kann an die folgenden Umgebungen angeschlossen werden: - Production: ```FitConnectEnvironment.Production``` -[FIT Connect Umgebungen](https://docs.fitko.de/fit-connect/docs/getting-started/first-steps/#environments) +[FIT Connect Umgebungen](https://docs.fitko.de/FIT-Connect/docs/getting-started/first-steps/#environments) Hierfür muss der Client die Environment auswählen oder einen eigenen Environment-Parameter übergeben. ## Credentials ClientId und ClientSecret sind die Grundlage, um einen Token vom OAuth-Server abfragen zu können. -Die beiden Werte sind im [Self-Service Portal der Testumgebung von FIT-Connect](https://portal.auth-testing.fit-connect.fitko.dev/clients) zu erstellen. +Die beiden Werte sind im [Self-Service Portal der Testumgebung von FIT-Connect](https://portal.auth-testing.FIT-Connect.fitko.dev/clients) zu erstellen. # Sender @@ -215,3 +220,4 @@ Client.GetRouter(FitConnectEnvironment.Development, logger); Source code is licensed under the [EUPL](LICENSES/EUPL-1.2.txt). Rechtlicher Hinweis: Dieses Software Development Kit (SDK) ist dazu bestimmt, die Anbindung einer Software an die FIT-Connect-Infrastruktur zu ermöglichen. Hierfür kann das SDK in die anzubindenden Software integriert werden. Erfolgt die Integration des SDK in unveränderter Form, liegt keine Bearbeitung im Sinne der EUPL bzw. des deutschen Urheberrechts vor. Die Art und Weise der Verlinkung des SDK führt insbesondere nicht zur Schaffung eines abgeleiteten Werkes. Die unveränderte Übernahme des SDK in eine anzubindende Software führt damit nicht dazu, dass die anzubindende Software unter den Bedingungen der EUPL zu lizenzieren ist. Für die Weitergabe des SDK selbst - in unveränderter oder bearbeiteter Form, als Quellcode oder ausführbares Programm - gelten die Lizenzbedingungen der EUPL in unveränderter Weise. +