From 1eec7eab28b814e802a88fa13cb7fc35b1907966 Mon Sep 17 00:00:00 2001 From: Max Martens Date: Thu, 28 Aug 2025 10:44:22 +0200 Subject: [PATCH] Add ability to explicitly provide userAccessToken to requests; add V3 OTP endpoints (while also keeping V2 endpoints) --- .../PadpReferenceApi/ApimHelper.cs | 614 ++++++++++++++++++ .../PadpReferenceApi/CryptoHelper.cs | 124 ++++ .../PadpReferenceApi/PadpReferenceApi.csproj | 15 + .../PadpReferenceApi/Program.cs | 250 +++++++ .../appsettings template.json | 38 ++ .../model/AdministrativeData.cs | 44 ++ .../PadpReferenceApi/model/B2bAccessToken.cs | 33 + .../model/CreatePersonalDataRequest.cs | 29 + .../model/DecryptedPersonalData.cs | 23 + .../model/DeletePersonalDataResponse.cs | 12 + .../PadpReferenceApi/model/EphemeralKey.cs | 16 + .../PadpReferenceApi/model/ErrorResponse.cs | 38 ++ .../model/GenerateOtpRequestV3.cs | 13 + .../PadpReferenceApi/model/OtpResponse.cs | 12 + .../PadpReferenceApi/model/PersonalData.cs | 42 ++ .../model/SwaggerSchemaExampleAttribute.cs | 42 ++ .../model/UpdatePersonalDataRequest.cs | 29 + .../PadpReferenceApi/model/UserAccessToken.cs | 12 + .../PadpReferenceApi/model/UserAuthInfo.cs | 7 + .../PadpReferenceApi/model/UserProperties.cs | 58 ++ .../model/ValidateOtpRequestV3.cs | 13 + 21 files changed, 1464 insertions(+) create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/ApimHelper.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/CryptoHelper.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/PadpReferenceApi.csproj create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/Program.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/appsettings template.json create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/AdministrativeData.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/B2bAccessToken.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/CreatePersonalDataRequest.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/DecryptedPersonalData.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/DeletePersonalDataResponse.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/EphemeralKey.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/ErrorResponse.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/GenerateOtpRequestV3.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/OtpResponse.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/PersonalData.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/SwaggerSchemaExampleAttribute.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/UpdatePersonalDataRequest.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/UserAccessToken.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/UserAuthInfo.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/UserProperties.cs create mode 100644 src/dotnet/padp-reference-web/PadpReferenceApi/model/ValidateOtpRequestV3.cs diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/ApimHelper.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/ApimHelper.cs new file mode 100644 index 0000000..dfb2003 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/ApimHelper.cs @@ -0,0 +1,614 @@ +using System.Text; +using System.Security.Cryptography.X509Certificates; +using System.Text.Json; +using Microsoft.AspNetCore.Http.HttpResults; + +class ApimHelper +{ + private readonly string _apimBaseUri; + private readonly string _b2bApiUri; + private readonly string _padp1201Uri; + private readonly string _padp1202Uri; + private readonly string _padp1204Uri; + private readonly string _padp1205Uri; + private readonly string _padp1206V2Uri; + private readonly string _padp1207V2Uri; + private readonly string _padp1206V3Uri; + private readonly string _padp1207V3Uri; + + private readonly string _padp1210Uri; + private readonly string _padp1211Uri; + + private readonly string _b2bApiKey; + private readonly string _padApiKey; + private readonly string _clientId; + private readonly string _clientSecret; + private CryptoHelper cryptoHelper; + private readonly HttpClient _httpClient; + + private Dictionary userAuthInfoMap = new Dictionary(); + + public ApimHelper(UserProperties userProperties) + { + _apimBaseUri = userProperties.Uris.ApimBaseUri; + _b2bApiUri = userProperties.Uris.B2bApiUri; + _padp1201Uri = userProperties.Uris.Padp1201Uri; + _padp1202Uri = userProperties.Uris.Padp1202Uri; + _padp1204Uri = userProperties.Uris.Padp1204Uri; + _padp1205Uri = userProperties.Uris.Padp1205Uri; + _padp1206V2Uri = userProperties.Uris.Padp1206V2Uri; + _padp1207V2Uri = userProperties.Uris.Padp1207V2Uri; + _padp1206V3Uri = userProperties.Uris.Padp1206V3Uri; + _padp1207V3Uri = userProperties.Uris.Padp1207V3Uri; + _padp1210Uri = userProperties.Uris.Padp1210Uri; + _padp1211Uri = userProperties.Uris.Padp1211Uri; + + _b2bApiKey = userProperties.Credentials.B2bApiKey; + _padApiKey = userProperties.Credentials.PadApiKey; + _clientId = userProperties.Credentials.ClientId; + _clientSecret = userProperties.Credentials.ClientSecret; + + cryptoHelper = new CryptoHelper(userProperties); + + HttpClientHandler handler = new HttpClientHandler(); + string clientCertPath = Environment.CurrentDirectory + userProperties.Credentials.ClientCertFile; + Console.WriteLine("Loading client cert from: {0}", clientCertPath); + handler.ClientCertificates.Add(new X509Certificate2(clientCertPath, userProperties.Credentials.ClientCertPassword)); + + _httpClient = new HttpClient(handler); + _httpClient.BaseAddress = new Uri(_apimBaseUri); + } + + public async Task, JsonHttpResult>> GetB2bAccessToken() + { + var request = new HttpRequestMessage(HttpMethod.Post, _b2bApiUri); + request.Headers.Add("APIKey", _b2bApiKey); + request.Headers.Add("Accept", "application/json"); + request.Content = new StringContent($"grant_type=client_credentials&client_id={_clientId}&client_secret={_clientSecret}", Encoding.UTF8, "application/x-www-form-urlencoded"); + + Console.WriteLine("Sending request to {0}...", _b2bApiUri); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) { + return TypedResults.Ok(await response.Content.ReadFromJsonAsync()); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + private string GetB2bAccessTokenAsString() + { + var b2bAccessToken = GetB2bAccessToken(); + return ((Ok)b2bAccessToken.Result.Result).Value.accessToken; + } + + public IResult GetImageFromBase64(string base64Image) + { + byte[] imageBytes = Convert.FromBase64String(base64Image); + return TypedResults.File(imageBytes, "image/jpeg"); + } + + public async Task>> CreatePersonalData(string xTat, string email, string? name, string? birthDate, IFormFile? photo) + { + UserAuthInfo userAuthInfo; + if (userAuthInfoMap.TryGetValue(xTat, out UserAuthInfo? authInfo) && authInfo.encryptedEphemeralKey != null) { + userAuthInfo = authInfo; + } else { + var errorResponse = new ErrorResponse("No ephemeral key found for xTAT " + xTat + " - generate and validate OTP first using API 1210!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } + + var b2bAccessToken = await GetB2bAccessToken(); + + var requestUri = _padp1201Uri.Replace("{xtat}", xTat); + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Add("Authorization", "Bearer " + ((Ok)b2bAccessToken.Result).Value.accessToken); + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + + var createPersonalDataRequest = new CreatePersonalDataRequest(); + createPersonalDataRequest.metadata.email = email; + createPersonalDataRequest.metadata.ephemeralKeyAlias = userAuthInfo.ephemeralKeyAlias; + + var decryptedEphemeralKey = cryptoHelper.DecryptEphemeralKey(userAuthInfo.encryptedEphemeralKey); + + if (name != null) { + createPersonalDataRequest.data.personalAccountData.name = cryptoHelper.EncryptText(decryptedEphemeralKey, name); + } + if (birthDate != null) { + createPersonalDataRequest.data.personalAccountData.birthdate = cryptoHelper.EncryptText(decryptedEphemeralKey, birthDate);; + } + if (photo != null) { + using (MemoryStream memoryStream = new MemoryStream()) + { + photo.CopyTo(memoryStream); + byte[] photoBytes = memoryStream.ToArray(); + createPersonalDataRequest.data.personalAccountData.photo = cryptoHelper.EncryptPhoto(decryptedEphemeralKey, photoBytes); + } + } + request.Content = JsonContent.Create(createPersonalDataRequest); + + Console.WriteLine("Sending request to {0} with request body {1}...", requestUri, request.Content.ReadAsStringAsync().Result); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.Created) { + return TypedResults.Created(); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public async Task>> CreatePersonalDataWithEmailVerification(string xTat, string email, string? userAccessToken, string? name, string? birthDate, IFormFile? photo) + { + UserAuthInfo userAuthInfo; + if (userAuthInfoMap.TryGetValue(xTat, out UserAuthInfo? authInfo)) { + if (authInfo.encryptedEphemeralKey == null) { + var errorResponse = new ErrorResponse("No ephemeral key found for xTAT " + xTat + " - generate an ephemeral key first, using API 1210!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } else if (authInfo.UserAccessToken == null && userAccessToken == null) { + var errorResponse = new ErrorResponse("No User Access Token found for xTAT " + xTat + " - provide one in the request, or generate and validate OTP using API 1206 and 1207!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } + userAuthInfo = authInfo; + } else { + var errorResponse = new ErrorResponse("No ephemeral key found for xTAT " + xTat + " - generate an ephemeral key first, using API 1210!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } + + var requestUri = _padp1201Uri.Replace("{xtat}", xTat); + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + + if (userAccessToken != null) { + Console.WriteLine("Using provided User Access Token {0}...", userAccessToken); + request.Headers.Add("Authorization", "Bearer " + userAccessToken); + } else { + Console.WriteLine("Using stored User Access Token {0}...", userAuthInfo.UserAccessToken); + request.Headers.Add("Authorization", "Bearer " + userAuthInfo.UserAccessToken); + } + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + + var createPersonalDataRequest = new CreatePersonalDataRequest(); + createPersonalDataRequest.metadata.email = email; + createPersonalDataRequest.metadata.ephemeralKeyAlias = userAuthInfo.ephemeralKeyAlias; + + var decryptedEphemeralKey = cryptoHelper.DecryptEphemeralKey(userAuthInfo.encryptedEphemeralKey); + + if (name != null) { + createPersonalDataRequest.data.personalAccountData.name = cryptoHelper.EncryptText(decryptedEphemeralKey, name); + } + if (birthDate != null) { + createPersonalDataRequest.data.personalAccountData.birthdate = cryptoHelper.EncryptText(decryptedEphemeralKey, birthDate);; + } + if (photo != null) { + using (MemoryStream memoryStream = new MemoryStream()) + { + photo.CopyTo(memoryStream); + byte[] photoBytes = memoryStream.ToArray(); + createPersonalDataRequest.data.personalAccountData.photo = cryptoHelper.EncryptPhoto(decryptedEphemeralKey, photoBytes); + } + } + request.Content = JsonContent.Create(createPersonalDataRequest); + + Console.WriteLine("Sending request to {0} with request body {1}...", requestUri, request.Content.ReadAsStringAsync().Result); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.Created) { + return TypedResults.Created(); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public async Task, JsonHttpResult>> GetPersonalData(string xTat, string? userAccessToken) + { + UserAuthInfo? userAuthInfo = null; + if (userAuthInfoMap.TryGetValue(xTat, out UserAuthInfo? authInfo) && authInfo.UserAccessToken != null) + { + userAuthInfo = authInfo; + } else if (userAccessToken == null) { + var errorResponse = new ErrorResponse("No User Access Token found for xTAT " + xTat + " - provide one in the request, or generate and validate OTP using API 1206 and 1207!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } + + var requestUri = _padp1202Uri.Replace("{xtat}", xTat) + "?pemRsaPublicKey=" + cryptoHelper.GetPublicKeyRsa(); + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + if (userAccessToken != null) + { + Console.WriteLine("Using provided User Access Token {0}...", userAccessToken); + request.Headers.Add("Authorization", "Bearer " + userAccessToken); + } else { + Console.WriteLine("Using stored User Access Token {0}...", userAuthInfo.UserAccessToken); + request.Headers.Add("Authorization", "Bearer " + userAuthInfo.UserAccessToken); + } + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + + Console.WriteLine("Sending request to {0}...", requestUri); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + return TypedResults.Ok(await response.Content.ReadFromJsonAsync()); + } + else + { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public async Task, JsonHttpResult>> GetDecryptedPersonalData(string xTat, string? userAccessToken) + { + var personalData = await GetPersonalData(xTat, userAccessToken); + if (personalData.Result.GetType() == typeof(JsonHttpResult)) { + return (JsonHttpResult)personalData.Result; + } else { + var decryptedPersonalData = await DecryptPersonalData(((Ok)personalData.Result).Value); + return TypedResults.Ok(decryptedPersonalData); + } + } + + public async Task, JsonHttpResult>> DeletePersonalData(string xTat, string? userAccessToken) + { + UserAuthInfo? userAuthInfo = null; + if (userAuthInfoMap.TryGetValue(xTat, out UserAuthInfo? authInfo) && authInfo.UserAccessToken != null) + { + userAuthInfo = authInfo; + } else if (userAccessToken == null) { + var errorResponse = new ErrorResponse("No User Access Token found for xTAT " + xTat + " - provide one in the request, or generate and validate OTP using API 1206 and 1207!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } + + var requestUri = _padp1204Uri.Replace("{xtat}", xTat); + var request = new HttpRequestMessage(HttpMethod.Delete, requestUri); + + if (userAccessToken != null) { + Console.WriteLine("Using provided User Access Token {0}...", userAccessToken); + request.Headers.Add("Authorization", "Bearer " + userAccessToken); + } else { + Console.WriteLine("Using stored User Access Token {0}...", userAuthInfo.UserAccessToken); + request.Headers.Add("Authorization", "Bearer " + userAuthInfo.UserAccessToken); + } + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + + Console.WriteLine("Sending request to {0}...", requestUri); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) { + return TypedResults.Ok(await response.Content.ReadFromJsonAsync()); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public async Task>> UpdatePersonalData(string xTat, string? userAccessToken, bool skipUpdateCounter, string? name, string? birthDate, IFormFile? photo) + { + UserAuthInfo userAuthInfo; + if (userAuthInfoMap.TryGetValue(xTat, out UserAuthInfo? authInfo)) { + if (authInfo.encryptedEphemeralKey == null) { + var errorResponse = new ErrorResponse("No ephemeral key found for xTAT " + xTat + " - generate an ephemeral key first, using API 1210!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } else if (authInfo.UserAccessToken == null && userAccessToken == null) { + var errorResponse = new ErrorResponse("No User Access Token found for xTAT " + xTat + " - provide one in the request, or generate and validate OTP using API 1206 and 1207!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } + userAuthInfo = authInfo; + } else { + var errorResponse = new ErrorResponse("No User Access Token found for xTAT " + xTat + " - provide one in the request, or generate and validate OTP using API 1206 and 1207!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } + + var requestUri = _padp1205Uri.Replace("{xtat}", xTat); + var request = new HttpRequestMessage(HttpMethod.Put, requestUri); + + if (userAccessToken != null) { + Console.WriteLine("Using provided User Access Token {0}...", userAccessToken); + request.Headers.Add("Authorization", "Bearer " + userAccessToken); + } else { + Console.WriteLine("Using stored User Access Token {0}...", userAuthInfo.UserAccessToken); + request.Headers.Add("Authorization", "Bearer " + userAuthInfo.UserAccessToken); + } + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + + var updatePersonalDataRequest = new UpdatePersonalDataRequest(); + updatePersonalDataRequest.metadata.skipUpdateCounter = skipUpdateCounter; + updatePersonalDataRequest.metadata.ephemeralKeyAlias = userAuthInfo.ephemeralKeyAlias; + + var decryptedEphemeralKey = cryptoHelper.DecryptEphemeralKey(userAuthInfo.encryptedEphemeralKey); + + if (name != null) { + updatePersonalDataRequest.data.personalAccountData.name = cryptoHelper.EncryptText(decryptedEphemeralKey, name); + } + if (birthDate != null) { + updatePersonalDataRequest.data.personalAccountData.birthdate = cryptoHelper.EncryptText(decryptedEphemeralKey, birthDate);; + } + if (photo != null) { + using (MemoryStream memoryStream = new MemoryStream()) + { + photo.CopyTo(memoryStream); + byte[] photoBytes = memoryStream.ToArray(); + updatePersonalDataRequest.data.personalAccountData.photo = cryptoHelper.EncryptPhoto(decryptedEphemeralKey, photoBytes); + } + } + request.Content = JsonContent.Create(updatePersonalDataRequest); + + Console.WriteLine("Sending request to {0} with request body {1}...", requestUri, request.Content.ReadAsStringAsync().Result); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) { + return TypedResults.Ok(); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public async Task, JsonHttpResult>> GenerateOtpV2(string xTat) + { + var requestUri = _padp1206V2Uri.Replace("{xtat}", xTat); + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Add("Authorization", "Bearer " + GetB2bAccessTokenAsString()); + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + request.Content = new StringContent("{\"channel\": \"EMAIL\"}", Encoding.UTF8, "application/json"); + + Console.WriteLine("Sending request to {0}...", requestUri); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.Accepted) { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public async Task, JsonHttpResult>> ValidateOtpV2(string xTat, string otp) + { + var requestUri = _padp1207V2Uri.Replace("{xtat}", xTat); + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Add("Authorization", "Bearer " + GetB2bAccessTokenAsString()); + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + request.Content = new StringContent("{\"otp\": \"" + otp + "\"}", Encoding.UTF8, "application/json"); + + Console.WriteLine("Sending request to {0}...", requestUri); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) { + var content = await response.Content.ReadFromJsonAsync(); + Console.WriteLine("Successfully retrieved User Access Token for xTAT {0} - storing for use in following calls...", xTat); + + if (!userAuthInfoMap.ContainsKey(xTat)) { + userAuthInfoMap.Add(xTat, new UserAuthInfo()); + } + UserAuthInfo userAuthInfo = userAuthInfoMap[xTat]; + userAuthInfo.UserAccessToken = content.accessToken; + + return TypedResults.Ok(content); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public async Task, JsonHttpResult>> GenerateOtpV3(string? email, string? xTat) + { + var requestUri = _padp1206V3Uri; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Add("Authorization", "Bearer " + GetB2bAccessTokenAsString()); + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + + var generateOtpRequest = new GenerateOtpRequestV3(); + + generateOtpRequest.channel = "EMAIL"; + if (email != null) { + generateOtpRequest.source = "EMAIL"; + generateOtpRequest.recipient = email; + } else if (xTat != null) { + generateOtpRequest.source = "XTAT"; + generateOtpRequest.recipient = xTat; + } else if (email != null && xTat != null){ + var errorResponse = new ErrorResponse("Both e-mail and xTAT filled, only one should be filled in the request!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } + + request.Content = JsonContent.Create(generateOtpRequest); + + Console.WriteLine("Sending request to {0}...", requestUri); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.Accepted) { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public async Task, JsonHttpResult>> ValidateOtpV3(string? email, string? xTat, string otp) + { + var requestUri = _padp1207V3Uri; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Add("Authorization", "Bearer " + GetB2bAccessTokenAsString()); + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + + var validateOtpRequest = new ValidateOtpRequestV3(); + + validateOtpRequest.otp = otp; + if (email != null) + { + validateOtpRequest.source = "EMAIL"; + validateOtpRequest.recipient = email; + } + else if (xTat != null) + { + validateOtpRequest.source = "XTAT"; + validateOtpRequest.recipient = xTat; + } + else if (email != null && xTat != null) + { + var errorResponse = new ErrorResponse("Both e-mail and xTAT filled, only one should be filled in the request!", 400); + return TypedResults.Json(errorResponse, JsonSerializerOptions.Default, "application/json", 400); + } + + request.Content = JsonContent.Create(validateOtpRequest); + + Console.WriteLine("Sending request to {0}...", requestUri); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) { + var content = await response.Content.ReadFromJsonAsync(); + + if (xTat != null) { + Console.WriteLine("Successfully retrieved User Access Token for xTAT {0} - storing for use in following calls...", xTat); + + if (!userAuthInfoMap.ContainsKey(xTat)) + { + userAuthInfoMap.Add(xTat, new UserAuthInfo()); + } + UserAuthInfo userAuthInfo = userAuthInfoMap[xTat]; + userAuthInfo.UserAccessToken = content.accessToken; + } + else { + Console.WriteLine("Successfully retrieved User Access Token for e-mail {0} - we don't know the xTAT (yet), so we don't cannot the user access token for later use..."); + } + + + return TypedResults.Ok(content); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public async Task, JsonHttpResult>> GetAdministrativeData(string xTat) + { + var requestUri = _padp1211Uri.Replace("{xtat}", xTat); + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", "Bearer " + GetB2bAccessTokenAsString()); + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + + Console.WriteLine("Sending request to {0}...", requestUri); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + return TypedResults.Ok(await response.Content.ReadFromJsonAsync()); + } + else + { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + public string EncryptDecryptPoc(string textToEncrypt, string encryptedEphemeralKey) + { + Console.WriteLine("Encrypting, then decrypting text {0} using encrypted ephemeral key {1}...", textToEncrypt, encryptedEphemeralKey); + var decryptedEphemeralKey = cryptoHelper.DecryptEphemeralKey(encryptedEphemeralKey); + var encryptedText = cryptoHelper.EncryptText(decryptedEphemeralKey, textToEncrypt); + Console.WriteLine($"Encrypted text: {encryptedText}"); + + Console.WriteLine("Decrypting text {0}...", encryptedText); + var decryptedText = cryptoHelper.DecryptTextAsync(decryptedEphemeralKey, encryptedText); + Console.WriteLine($"Decrypted text: {decryptedText.Result}"); + + return "Encrypted text: " + encryptedText + "\nDecrypted text: " + decryptedText.Result; + } + + public string EncryptPoc(string textToEncrypt, string encryptedEphemeralKey) + { + Console.WriteLine("Encrypting text {0} using encrypted ephemeral key {1}...", textToEncrypt, encryptedEphemeralKey); + var decryptedEphemeralKey = cryptoHelper.DecryptEphemeralKey(encryptedEphemeralKey); + var encryptedText = cryptoHelper.EncryptText(decryptedEphemeralKey, textToEncrypt); + Console.WriteLine($"Encrypted text: {encryptedText}"); + + return "Encrypted text: " + encryptedText; + } + + public string DecryptPoc(string textToDecrypt, string encryptedEphemeralKey) + { + Console.WriteLine("Decrypting text {0} using ephemeral key {1}...", textToDecrypt, encryptedEphemeralKey); + var decryptedEphemeralKey = cryptoHelper.DecryptEphemeralKey(encryptedEphemeralKey); + var decryptedText = cryptoHelper.DecryptTextAsync(decryptedEphemeralKey, textToDecrypt); + Console.WriteLine($"Decrypted text: {decryptedText.Result}"); + + return "Decrypted text: " + decryptedText.Result; + } + + public async Task, JsonHttpResult>> CreateEphemeralKey(string xTat) + { + var requestUri = _padp1210Uri + "?pemRsaPublicKey=" + cryptoHelper.GetPublicKeyRsa(); + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Add("Authorization", "Bearer " + GetB2bAccessTokenAsString()); + request.Headers.Add("APIKey", _padApiKey); + request.Headers.Add("requestId", Guid.NewGuid().ToString()); + request.Headers.Add("Accept", "application/json"); + + Console.WriteLine("Sending request to {0}...", requestUri); + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.Created) { + var content = await response.Content.ReadFromJsonAsync(); + Console.WriteLine("Successfully created Ephemeral Key for xTAT {0} - storing for use in following calls...", xTat); + if (!userAuthInfoMap.ContainsKey(xTat)) { + userAuthInfoMap.Add(xTat, new UserAuthInfo()); + } + UserAuthInfo userAuthInfo = userAuthInfoMap[xTat]; + userAuthInfo.ephemeralKeyAlias = content.ephemeralKeyAlias; + userAuthInfo.encryptedEphemeralKey = content.encryptedEphemeralKey; + + return TypedResults.Ok(content); + } else { + return TypedResults.Json(await response.Content.ReadFromJsonAsync(), JsonSerializerOptions.Default, "application/json", (int)response.StatusCode); + } + } + + private async Task DecryptPersonalData(PersonalData personalData) + { + var decryptedEphemeralKey = cryptoHelper.DecryptEphemeralKey(personalData.metadata.encryptedEphemeralKey); + Console.WriteLine("Decrypted encryptedEphemeralKey: {0}", Convert.ToBase64String(decryptedEphemeralKey)); + + var decryptedData = new DecryptedPersonalData.DecryptedData(); + + if (personalData.data.name != null) { + decryptedData.decryptedName = await cryptoHelper.DecryptTextAsync(decryptedEphemeralKey, personalData.data.name); + Console.WriteLine($"Decrypted name: {decryptedData.decryptedName}"); + } + + if (personalData.data.birthdate != null) { + decryptedData.decryptedBirthdate = await cryptoHelper.DecryptTextAsync(decryptedEphemeralKey, personalData.data.birthdate); + Console.WriteLine($"Decrypted birthDate: {decryptedData.decryptedBirthdate}"); + } + + if (personalData.data.photo != null) { + decryptedData.decryptedPhoto = await cryptoHelper.DecryptPhotoAsync(decryptedEphemeralKey, personalData.data.photo); + Console.WriteLine($"Decrypted photo: {decryptedData.decryptedPhoto}"); + } + + return new DecryptedPersonalData(decryptedData, personalData); + } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/CryptoHelper.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/CryptoHelper.cs new file mode 100644 index 0000000..0e9799b --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/CryptoHelper.cs @@ -0,0 +1,124 @@ +using System.Security.Cryptography; +using System.Text; + +public class CryptoHelper +{ + private RSA rsa; + private string _publicKeyRsa; + + public CryptoHelper(UserProperties userProperties) + { + string publicKeyPath = Environment.CurrentDirectory + userProperties.Rsa.PublicKeyFile; + string privateKeyPath = Environment.CurrentDirectory + userProperties.Rsa.PrivateKeyFile; + + Console.WriteLine("Loading public key from: {0}", publicKeyPath); + Console.WriteLine("Loading private key from: {0}", privateKeyPath); + + rsa = RSA.Create(); + rsa.ImportFromPem(File.ReadAllText(publicKeyPath)); + rsa.ImportFromPem(File.ReadAllText(privateKeyPath)); + + _publicKeyRsa = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()); + } + + public string GetPublicKeyRsa() => _publicKeyRsa; + + public byte[] DecryptEphemeralKey(string encryptedEphemeralKey) + { + return rsa.Decrypt(Convert.FromBase64String(encryptedEphemeralKey), RSAEncryptionPadding.OaepSHA512); + } + + public async Task DecryptTextAsync(byte[] decryptedEphemeralKey, string encryptedText) + { + var decryptedBytes = await DecryptContentAsync(decryptedEphemeralKey, encryptedText); + return Encoding.UTF8.GetString(decryptedBytes); + } + + public async Task DecryptPhotoAsync(byte[] decryptedEphemeralKey, string encryptedPhoto) + { + var decryptedBytes = await DecryptContentAsync(decryptedEphemeralKey, encryptedPhoto); + return Convert.ToBase64String(decryptedBytes); + } + + public string EncryptText(byte[] decryptedEphemeralKey, string textValue) + { + return EncryptContent(decryptedEphemeralKey, Encoding.UTF8.GetBytes(textValue)); + } + + public string EncryptPhoto(byte[] decryptedEphemeralKey, byte[] photoBytes) + { + return EncryptContent(decryptedEphemeralKey, photoBytes); + } + + private async Task DecryptContentAsync(byte[] decryptedEphemeralKey, string encryptedData) + { + byte[] encryptedDataByteArray = Convert.FromBase64String(encryptedData); + using MemoryStream memoryStream = new(encryptedDataByteArray); + using Aes aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + + byte[] iv = new byte[aes.IV.Length]; + int numBytesToRead = aes.IV.Length; + int numBytesRead = 0; + while (numBytesToRead > 0) + { + int n = memoryStream.Read(iv, numBytesRead, numBytesToRead); + if (n == 0) + { + break; + } + numBytesRead += n; + numBytesToRead -= n; + } + + byte[] key = decryptedEphemeralKey; + + await using CryptoStream cryptoStream = new(memoryStream, + aes.CreateDecryptor(key, iv), + CryptoStreamMode.Read); + + using MemoryStream ms = new(); + await cryptoStream.CopyToAsync(ms); + var bytes = ms.ToArray(); + return bytes; + } + + private string EncryptContent(byte[] decryptedEphemeralKey, byte[] content) + { + byte[] encrypted; + byte[] iv; + + using (var aesAlg = Aes.Create()) + { + aesAlg.Key = decryptedEphemeralKey; + + aesAlg.GenerateIV(); + iv = aesAlg.IV; + + aesAlg.Mode = CipherMode.CBC; + aesAlg.Padding = PaddingMode.PKCS7; + + // Create an encryptor to perform the stream transform. + var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + + // Create the streams used for encryption. + using (MemoryStream msEncrypt = new()) + { + using (CryptoStream csEncrypt = new(msEncrypt, encryptor, CryptoStreamMode.Write)) + { + + //Write all data to the stream. + csEncrypt.Write(content, 0, content.Length); + + } + encrypted = msEncrypt.ToArray(); + } + } + var combinedIvCt = new byte[iv.Length + encrypted.Length]; + Array.Copy(iv, 0, combinedIvCt, 0, iv.Length); + Array.Copy(encrypted, 0, combinedIvCt, iv.Length, encrypted.Length); + var encryptedData = Convert.ToBase64String(combinedIvCt); + return encryptedData; + } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/PadpReferenceApi.csproj b/src/dotnet/padp-reference-web/PadpReferenceApi/PadpReferenceApi.csproj new file mode 100644 index 0000000..0ed99c3 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/PadpReferenceApi.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/Program.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/Program.cs new file mode 100644 index 0000000..f352e32 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/Program.cs @@ -0,0 +1,250 @@ +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(setupAction => +{ + setupAction.EnableAnnotations(); + setupAction.SchemaFilter(); +}); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +var group = app.MapGroup("/").DisableAntiforgery().WithTags("PADP Reference API"); +var pocGroup = app.MapGroup("/poc").DisableAntiforgery().WithTags("Encrypt/decrypt POC"); + +UserProperties userProperties = app.Configuration.GetSection("UserProperties").Get(); + +ApimHelper apimHelper = new ApimHelper(userProperties); + +group.MapGet("/idp/b2b-access-token", () => +{ + return apimHelper.GetB2bAccessToken(); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("GetB2bAccessToken") +.WithSummary("API 1020 - Get B2B Access Token") +.WithDescription("Returns a client access token, needed for most other PADP APIs.") +.WithOpenApi(); + +group.MapGet("/get-image-from-base64", ([FromHeader]string base64String) => +{ + return apimHelper.GetImageFromBase64(base64String); +}) +.Produces(200, "image/jpeg") +.WithName("GetImageFromBase64") +.WithSummary("Get rendered image from Base64 encoded String") +.WithOpenApi(); + +group.MapPost("/personal-data/{xtat}", (string xTat, [SwaggerParameter("Email address to be used for OTP challenges")] string email, [SwaggerParameter("Should be at least two words (first name and last name)")] string? name, [SwaggerParameter("Should be a date between 1900-01-01 and now, in the format YYYY-MM-DD")] string? birthDate, [SwaggerSchema("Should be a JPG image, of max. 512KB and resolution between 520x520 and 720x720")] IFormFile? photo) => +{ + Console.WriteLine("Creating personal data for xTAT: " + xTat); + return apimHelper.CreatePersonalData(xTat, email, name, birthDate, photo); +}) +.Produces(201) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("CreatePersonalData") +.WithSummary("API 1201 - Create Personal Data - First create an ephemeral key using API 1210!") +.WithDescription("First create an ephemeral key using API 1210!") +.WithOpenApi(); + +group.MapPost("/personal-data/{xtat}/with-email-verification", (string xTat, [SwaggerParameter("Email address to be used for OTP challenges")]string email, [SwaggerParameter("This user access token will be used for e-mail verification if provided - otherwise, the internally stored one (based on previous V3 1206+1207 calls) will be used")]string? userAccessToken, [SwaggerParameter("Should be at least two words (first name and last name)")]string? name, [SwaggerParameter("Should be a date between 1900-01-01 and now, in the format YYYY-MM-DD")]string? birthDate, [SwaggerSchema("Should be a JPG image, of max. 512KB and resolution between 520x520 and 720x720")]IFormFile? photo) => +{ + Console.WriteLine("Creating personal data with e-mail verification for xTAT: " + xTat); + return apimHelper.CreatePersonalDataWithEmailVerification(xTat, email, userAccessToken,name, birthDate, photo); +}) +.Produces(201) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("CreatePersonalDataWithEmailVerification") +.WithSummary("API 1201 - Create Personal Data with e-mail verification - First create an ephemeral key using API 1210!") +.WithDescription("Performs extra e-mail verification using OTP flow; using internally stored (after 1206+1207 call) or explicitly provided user access token. First create an ephemeral key using API 1210!") +.WithOpenApi(); + +group.MapGet("/personal-data/{xtat}", (string xTat, [SwaggerParameter("If provided, this user access token will be used over the internally stored one (that was restrieved via earlier 1206+1207 calls)")]string? userAccessToken) => +{ + Console.WriteLine("Retrieving personal data for xTAT: " + xTat); + return apimHelper.GetPersonalData(xTat, userAccessToken); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("GetPersonalData") +.WithSummary("API 1202 - Get Personal Data - First perform an OTP challenge using API 1206 and 1207 or explicitly provide user access token!") +.WithDescription("First perform an OTP challenge using API 1206 and 1207 or explicitly provide user access token!") +.WithOpenApi(); + +group.MapGet("/personal-data/{xtat}/decrypted", (string xTat, [SwaggerParameter("If provided, this user access token will be used over the internally stored one (that was restrieved via earlier 1206+1207 calls)")]string? userAccessToken) => +{ + Console.WriteLine("Retrieving decrypted personal data for xTAT: " + xTat); + return apimHelper.GetDecryptedPersonalData(xTat, userAccessToken); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("GetDecryptedPersonalData") +.WithSummary("API 1202 - Get Personal Data AND decrypt response - First perform an OTP challenge using API 1206 and 1207 or explicitly provide user access token!") +.WithDescription("First perform an OTP challenge using API 1206 and 1207 or explicitly provide user access token!") +.WithOpenApi(); + +group.MapDelete("/personal-data/{xtat}", (string xTat, [SwaggerParameter("If provided, this user access token will be used over the internally stored one (that was restrieved via earlier 1206+1207 calls)")]string? userAccessToken) => +{ + Console.WriteLine("Deleting personal data for xTAT: " + xTat); + return apimHelper.DeletePersonalData(xTat, userAccessToken); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("DeletePersonalData") +.WithSummary("API 1204 - Delete Personal Data - First perform an OTP challenge using API 1206 and 1207 or explicitly provide user access token!") +.WithDescription("First perform an OTP challenge using API 1206 and 1207 or explicitly provide user access token!") +.WithOpenApi(); + +group.MapPut("/personal-data/{xtat}", (string xTat, [SwaggerParameter("If provided, this user access token will be used over the internally stored one (that was restrieved via earlier 1206+1207 calls)")]string? userAccessToken, bool skipUpdateCounter, [SwaggerParameter("Should be at least two words (first name and last name)")]string? name, [SwaggerParameter("Should be a date between 1900-01-01 and now, in the format YYYY-MM-DD")]string? birthDate, [SwaggerSchema("Should be a JPG image, of max. 512KB and resolution between 520x520 and 720x720")]IFormFile? photo) => +{ + Console.WriteLine("Replacing personal data for xTAT: " + xTat); + return apimHelper.UpdatePersonalData(xTat, userAccessToken, skipUpdateCounter, name, birthDate, photo); +}) +.WithName("UpdatePersonalData") +.WithSummary("API 1205 - Update Personal Data - First perform an OTP challenge using API 1206 and 1207 or explicitly provide user access token!") +.WithDescription("Performs a complete replacement; empty request parameters will result in the corresponding PADP attribute being deleted.") +.WithOpenApi(); + +group.MapGet("/v2/personal-data/{xtat}/generate-otp", (string xTat) => +{ + Console.WriteLine("Generating OTP for xTAT: " + xTat); + return apimHelper.GenerateOtpV2(xTat); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("GenerateOtpV2") +.WithSummary("API 1206 V2 - Generate OTP") +.WithOpenApi(); + +group.MapGet("/v2/personal-data/{xtat}/validate-otp", (string xTat, string otp) => +{ + Console.WriteLine("Validating OTP {0} for xTAT: {1}", otp, xTat); + return apimHelper.ValidateOtpV2(xTat, otp); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("ValidateOtpV2") +.WithSummary("API 1207 V2 - Validate OTP") +.WithOpenApi(); + +group.MapGet("/v3/personal-data/generate-otp", (string? email, string? xTat) => +{ + if (email != null){ + Console.WriteLine("Generating OTP for e-mail: " + email); + } else if (xTat != null){ + Console.WriteLine("Generating OTP for xTAT: " + xTat); + } + return apimHelper.GenerateOtpV3(email, xTat); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("GenerateOtpV3") +.WithSummary("API 1206 V3 - Generate OTP") +.WithDescription("If e-mail is provided, the OTP is generated for the e-mail address. If xTAT is provided, the OTP is generated for the xTAT.") +.WithOpenApi(); + +group.MapGet("/v3/personal-data/validate-otp", (string? email, string? xTat, string otp) => +{ + if (email != null){ + Console.WriteLine("Validating OTP {0} for e-mail: {1}", otp, email); + } else if (xTat != null){ + Console.WriteLine("Validating OTP {0} for xTAT: {1}", otp, xTat); + } + return apimHelper.ValidateOtpV3(email, xTat, otp); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("ValidateOtpV3") +.WithSummary("API 1207 V3 - Validate OTP") +.WithDescription("If e-mail is provided, the OTP is validated for the e-mail address. If xTAT is provided, the OTP is validated for the xTAT.") +.WithOpenApi(); + +group.MapGet("/personal-data/{xtat}/encrypted-update-init", (string xTat) => +{ + return apimHelper.CreateEphemeralKey(xTat); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("CreateEphemeralKey") +.WithSummary("API 1210 - Create Ephemeral Key") +.WithOpenApi(); + +group.MapGet("/personal-data/{xtat}/administrative-data", (string xTat) => +{ + Console.WriteLine("Retrieving administrative data for xTAT: {0}", xTat); + return apimHelper.GetAdministrativeData(xTat); +}) +.Produces(200) +.Produces(400) +.Produces(404) +.Produces(500) +.WithName("GetAdministrativeData") +.WithSummary("API 1211 - Get Administrative Data") +.WithOpenApi(); + +pocGroup.MapGet("/encrypt-decrypt-poc", ([FromHeader]string textToEncrypt, string encryptedEphemeralKey) => +{ + Console.WriteLine("Text to encrypt: {0}", textToEncrypt); + return apimHelper.EncryptDecryptPoc(textToEncrypt, encryptedEphemeralKey); +}) +.WithName("EncryptDecryptPoc") +.WithSummary("Encrypt/Decrypt POC") +.WithOpenApi(); + +pocGroup.MapGet("/encrypt-poc", ([FromHeader]string textToEncrypt, string encryptedEphemeralKey) => +{ + Console.WriteLine("Text to encrypt: {0}", textToEncrypt); + return apimHelper.EncryptPoc(textToEncrypt, encryptedEphemeralKey); +}) +.WithName("EncryptPoc") +.WithSummary("Encrypt POC") +.WithOpenApi(); + +pocGroup.MapGet("/decrypt-poc", ([FromHeader]string textToDecrypt, string encryptedEphemeralKey) => +{ + Console.WriteLine("Text to decrypt: {0}", textToDecrypt); + return apimHelper.DecryptPoc(textToDecrypt, encryptedEphemeralKey); +}) +.WithName("DecryptPoc") +.WithSummary("Decrypt POC") +.WithOpenApi(); + +app.Run(); diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/appsettings template.json b/src/dotnet/padp-reference-web/PadpReferenceApi/appsettings template.json new file mode 100644 index 0000000..29ee000 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/appsettings template.json @@ -0,0 +1,38 @@ +// Rename this file to appsettings.json and provide correct credentials and locations for the cryptographic files +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "UserProperties": { + "uris": { + "apimBaseUri": "https://api-ovpay-acc.translink.nl", + "b2bApiUri": "/b2b-client-authentication/v1/token", + "padp1201Uri": "/pad-management/v2/personal-data/{xtat}", + "padp1202Uri": "/pad-management/v2/personal-data/{xtat}", + "padp1204Uri": "/pad-management/v2/personal-data/{xtat}", + "padp1205Uri": "/pad-management/v2/personal-data/{xtat}", + "padp1206V2Uri": "/pad-management/v2/personal-data/{xtat}/generate-otp", + "padp1207V2Uri": "/pad-management/v2/personal-data/{xtat}/validate-otp", + "padp1206V3Uri": "/pad-management/v3/personal-data/generate-otp", + "padp1207V3Uri": "/pad-management/v3/personal-data/validate-otp", + "padp1210Uri": "/pad-management/v2/personal-data/encrypted-update-init", + "padp1211Uri": "/pad-management/v2/personal-data/{xtat}/administrative-data" + }, + "rsa": { + "publicKeyFile": "\\Properties\\{public key in PEM format}.pem", + "privateKeyFile": "\\Properties\\{private key in PEM format}.pem" + }, + "credentials": { + "clientCertFile": "\\Properties\\{APIM client certificate}.pfx", + "clientCertPassword": "{pfxPassword}", + "clientId": "HTM_Retailer", + "clientSecret": "{clientSecret}", + "b2bApiKey": "{b2bApiKey}", + "padApiKey": "{padApiKey}" + } + } +} diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/AdministrativeData.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/AdministrativeData.cs new file mode 100644 index 0000000..e061744 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/AdministrativeData.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +public class AdministrativeData +{ + public AdministrativeData(AdministrativeDataElement name, AdministrativeDataElement photo, AdministrativeDataElement birthdate) + { + this.name = name; + this.photo = photo; + this.birthdate = birthdate; + } + + public AdministrativeDataElement name { get; set; } + public AdministrativeDataElement photo { get; set; } + public AdministrativeDataElement birthdate { get; set; } + + public class AdministrativeDataElement + { + public AdministrativeDataElement(bool inaccuracyFlag, string inaccuracyFlagReason, int inaccuracyFlagCounter, int changeCounter, int maxUpdatesVerificationCount, DateTime lastChangeDate, bool isValidated) + { + this.inaccuracyFlag = inaccuracyFlag; + this.inaccuracyFlagReason = inaccuracyFlagReason; + this.inaccuracyFlagCounter = inaccuracyFlagCounter; + this.changeCounter = changeCounter; + this.maxUpdatesVerificationCount = maxUpdatesVerificationCount; + this.lastChangeDate = lastChangeDate; + this.isValidated = isValidated; + } + + [JsonPropertyName("inaccuracyFlag")] + public bool inaccuracyFlag { get; set; } + [JsonPropertyName("inaccuracyFlagReason")] + public string inaccuracyFlagReason { get; set; } + [JsonPropertyName("inaccuracyFlagCounter")] + public int inaccuracyFlagCounter { get; set; } + [JsonPropertyName("changeCounter")] + public int changeCounter { get; set; } + [JsonPropertyName("maxUpdatesVerificationCount")] + public int maxUpdatesVerificationCount { get; set; } + [JsonPropertyName("lastChangeDate")] + public DateTime lastChangeDate { get; set; } + [JsonPropertyName("isValidated")] + public bool isValidated { get; set; } + } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/B2bAccessToken.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/B2bAccessToken.cs new file mode 100644 index 0000000..c5de68f --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/B2bAccessToken.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +public class B2bAccessToken +{ + public B2bAccessToken(string accessToken, int expiresIn, int refreshExpiresIn, string refreshToken, string tokenType, int notBeforePolicy, string scope, string beId) + { + this.accessToken = accessToken; + this.expiresIn = expiresIn; + this.refreshExpiresIn = refreshExpiresIn; + this.refreshToken = refreshToken; + this.tokenType = tokenType; + this.notBeforePolicy = notBeforePolicy; + this.scope = scope; + this.beId = beId; + } + + [JsonPropertyName("access_token")] + public string accessToken { get; set; } + [JsonPropertyName("expires_in")] + public int expiresIn { get; set; } + [JsonPropertyName("refresh_expires_in")] + public int refreshExpiresIn { get; set; } + [JsonPropertyName("refresh_token")] + public string refreshToken { get; set; } + [JsonPropertyName("token_type")] + public string tokenType { get; set; } + [JsonPropertyName("not-before-policy")] + public int notBeforePolicy { get; set; } + [JsonPropertyName("scope")] + public string scope { get; set; } + [JsonPropertyName("BE_ID")] + public string beId { get; set; } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/CreatePersonalDataRequest.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/CreatePersonalDataRequest.cs new file mode 100644 index 0000000..b01bec7 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/CreatePersonalDataRequest.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +public class CreatePersonalDataRequest +{ + + [JsonPropertyName("metadata")] + public Metadata metadata { get; set; } = new Metadata(); + + [JsonPropertyName("data")] + public Data data { get; set; } = new Data(); + + public class Metadata + { + public string? email { get; set; } + public string? ephemeralKeyAlias { get; set; } + } + + public class Data + { + public PersonalAccountData personalAccountData { get; set; } = new PersonalAccountData(); + } + + public class PersonalAccountData + { + public string? name { get; set; } + public string? birthdate { get; set; } + public string? photo { get; set; } + } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/DecryptedPersonalData.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/DecryptedPersonalData.cs new file mode 100644 index 0000000..379e85d --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/DecryptedPersonalData.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using Swashbuckle.AspNetCore.Annotations; + +public class DecryptedPersonalData +{ + public DecryptedPersonalData(DecryptedData decryptedData, PersonalData encryptedData) + { + this.decryptedData = decryptedData; + this.encryptedData = encryptedData; + } + + public DecryptedData decryptedData { get; set; } + public PersonalData encryptedData { get; set; } + public class DecryptedData + { + [JsonPropertyName("decryptedName")] + public string? decryptedName { get; set; } + [JsonPropertyName("decryptedBirthdate")] + public string? decryptedBirthdate { get; set; } + [SwaggerSchema(Format = "byte", Description = "Base64 encoded photo")] + public string? decryptedPhoto { get; set; } + } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/DeletePersonalDataResponse.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/DeletePersonalDataResponse.cs new file mode 100644 index 0000000..e003672 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/DeletePersonalDataResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +public class DeletePersonalDataResponse +{ + public DeletePersonalDataResponse(string[] deletedAttributes) + { + this.deletedAttributes = deletedAttributes; + } + + [JsonPropertyName("deletedAttributes")] + public string[] deletedAttributes { get; set; } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/EphemeralKey.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/EphemeralKey.cs new file mode 100644 index 0000000..71c8806 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/EphemeralKey.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +public class EphemeralKey +{ + public EphemeralKey(string ephemeralKeyAlias, string encryptedEphemeralKey) + { + this.ephemeralKeyAlias = ephemeralKeyAlias; + this.encryptedEphemeralKey = encryptedEphemeralKey; + } + + [JsonPropertyName("ephemeralKeyAlias")] + public string ephemeralKeyAlias { get; set; } + + [JsonPropertyName("encryptedEphemeralKey")] + public string encryptedEphemeralKey { get; set; } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/ErrorResponse.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/ErrorResponse.cs new file mode 100644 index 0000000..3c09561 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/ErrorResponse.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; + +public class ErrorResponse +{ + [JsonConstructor] + public ErrorResponse(Error[] errors, string exceptionClassName, string exceptionStackTrace) + { + this.errors = errors; + this.exceptionClassName = exceptionClassName; + this.exceptionStackTrace = exceptionStackTrace; + } + + public ErrorResponse(string errorMessage, int statusCode) + { + this.errors = new Error[] { new Error(statusCode.ToString(), new string[] { }, errorMessage) }; + this.exceptionClassName = null; + this.exceptionStackTrace = null; + } + + public Error[] errors { get; set; } + public string? exceptionClassName { get; set; } + public string? exceptionStackTrace { get; set; } + + public class Error + { + public Error(string code, string[] data, string message) + { + this.code = code; + this.data = data; + this.message = message; + } + + public string code { get; set; } + public string[] data { get; set; } + public string message { get; set; } + } + +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/GenerateOtpRequestV3.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/GenerateOtpRequestV3.cs new file mode 100644 index 0000000..b158287 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/GenerateOtpRequestV3.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +public class GenerateOtpRequestV3 +{ + [JsonPropertyName("source")] + public string source { get; set; } + + [JsonPropertyName("recipient")] + public string recipient { get; set; } + + [JsonPropertyName("channel")] + public string channel { get; set; } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/OtpResponse.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/OtpResponse.cs new file mode 100644 index 0000000..3293a74 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/OtpResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +public class OtpResponse +{ + public OtpResponse(string maskedEmailAddress) + { + this.maskedEmailAddress = maskedEmailAddress; + } + + [JsonPropertyName("maskedEmailAddress")] + public string maskedEmailAddress { get; set; } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/PersonalData.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/PersonalData.cs new file mode 100644 index 0000000..ffb7acc --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/PersonalData.cs @@ -0,0 +1,42 @@ +using System.Reflection.Metadata.Ecma335; +using System.Text.Json.Serialization; + +public class PersonalData +{ + public PersonalData(Metadata metadata, Data data) + { + this.metadata = metadata; + this.data = data; + } + + public Metadata metadata { get; set; } + public Data data { get; set; } + + public class Metadata + { + public Metadata(string encryptedEphemeralKey) + { + this.encryptedEphemeralKey = encryptedEphemeralKey; + } + + [JsonPropertyName("encryptedEphemeralKey")] + public string encryptedEphemeralKey { get; set; } + } + + public class Data + { + public Data(string name, string birthdate, string photo) + { + this.name = name; + this.birthdate = birthdate; + this.photo = photo; + } + + [JsonPropertyName("name")] + public string name { get; set; } + [JsonPropertyName("birthdate")] + public string birthdate { get; set; } + [JsonPropertyName("photo")] + public string photo { get; set; } + } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/SwaggerSchemaExampleAttribute.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/SwaggerSchemaExampleAttribute.cs new file mode 100644 index 0000000..ca2cd05 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/SwaggerSchemaExampleAttribute.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +[AttributeUsage( + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Parameter | + AttributeTargets.Property | + AttributeTargets.Enum, + AllowMultiple = false)] +public class SwaggerSchemaExampleAttribute : Attribute +{ + public SwaggerSchemaExampleAttribute(string example) + { + Example = example; + } + + public string Example { get; set; } +} + +public class SwaggerSchemaExampleFilter : ISchemaFilter +{ + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (context.MemberInfo != null) + { + var schemaAttribute = context.MemberInfo.GetCustomAttributes() + .FirstOrDefault(); + if (schemaAttribute != null) + ApplySchemaAttribute(schema, schemaAttribute); + } + } + + private void ApplySchemaAttribute(OpenApiSchema schema, SwaggerSchemaExampleAttribute schemaAttribute) + { + if (schemaAttribute.Example != null) + { + schema.Example = new Microsoft.OpenApi.Any.OpenApiString(schemaAttribute.Example); + } + } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/UpdatePersonalDataRequest.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/UpdatePersonalDataRequest.cs new file mode 100644 index 0000000..69b2e7b --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/UpdatePersonalDataRequest.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +public class UpdatePersonalDataRequest +{ + + [JsonPropertyName("metadata")] + public Metadata metadata { get; set; } = new Metadata(); + + [JsonPropertyName("data")] + public Data data { get; set; } = new Data(); + + public class Metadata + { + public bool skipUpdateCounter { get; set; } + public string? ephemeralKeyAlias { get; set; } + } + + public class Data + { + public PersonalAccountData personalAccountData { get; set; } = new PersonalAccountData(); + } + + public class PersonalAccountData + { + public string? name { get; set; } + public string? birthdate { get; set; } + public string? photo { get; set; } + } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/UserAccessToken.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/UserAccessToken.cs new file mode 100644 index 0000000..cfaa62f --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/UserAccessToken.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +public class UserAccessToken +{ + public UserAccessToken(string accessToken) + { + this.accessToken = accessToken; + } + + [JsonPropertyName("accessToken")] + public string accessToken { get; set; } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/UserAuthInfo.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/UserAuthInfo.cs new file mode 100644 index 0000000..3ee6c5a --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/UserAuthInfo.cs @@ -0,0 +1,7 @@ +public class UserAuthInfo +{ + public string? UserAccessToken { get; set; } + public string? ephemeralKeyAlias { get; set; } + public string? encryptedEphemeralKey { get; set; } + +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/UserProperties.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/UserProperties.cs new file mode 100644 index 0000000..4ee9540 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/UserProperties.cs @@ -0,0 +1,58 @@ +public class UserProperties +{ + public UserProperties(UrisSettings uris, RsaSettings rsa, CredentialsSettings credentials) + { + (Uris, Rsa, Credentials) = (uris, rsa, credentials); + } + + public UrisSettings Uris { get; set; } + public RsaSettings Rsa { get; set; } + public CredentialsSettings Credentials { get; set; } + + public class UrisSettings + { + public UrisSettings(string apimBaseUri, string b2bApiUri, string padp1201Uri, string padp1202Uri, string padp1204Uri, string padp1205Uri, string padp1206V2Uri, string padp1207V2Uri, string padp1206V3Uri, string padp1207V3Uri, string padp1210Uri, string padp1211Uri) + { + (ApimBaseUri, B2bApiUri, Padp1201Uri, Padp1202Uri, Padp1204Uri, Padp1205Uri, Padp1206V2Uri, Padp1207V2Uri, Padp1206V3Uri, Padp1207V3Uri, Padp1210Uri, Padp1211Uri) = (apimBaseUri, b2bApiUri, padp1201Uri, padp1202Uri, padp1204Uri, padp1205Uri, padp1206V2Uri, padp1207V2Uri, padp1206V3Uri, padp1207V3Uri, padp1210Uri, padp1211Uri); + } + + public string ApimBaseUri { get; set; } + public string B2bApiUri { get; set; } + public string Padp1201Uri { get; set; } + public string Padp1202Uri { get; set; } + public string Padp1204Uri { get; set; } + public string Padp1205Uri { get; set; } + public string Padp1206V2Uri { get; set; } + public string Padp1207V2Uri { get; set; } + public string Padp1206V3Uri { get; set; } + public string Padp1207V3Uri { get; set; } + public string Padp1210Uri { get; set; } + public string Padp1211Uri { get; set; } + } + + public class RsaSettings + { + public RsaSettings(string publicKeyFile, string privateKeyFile) + { + (PublicKeyFile, PrivateKeyFile) = (publicKeyFile, privateKeyFile); + } + + public string PublicKeyFile { get; set; } + public string PrivateKeyFile { get; set; } + } + + public class CredentialsSettings + { + public CredentialsSettings(string clientCertFile, string clientCertPassword, string clientId, string clientSecret, string b2bApiKey, string padApiKey) + { + (ClientCertFile, ClientCertPassword, ClientId, ClientSecret, B2bApiKey, PadApiKey) = (clientCertFile, clientCertPassword, clientId, clientSecret, b2bApiKey, padApiKey); + } + + public string ClientCertFile { get; set; } + public string ClientCertPassword { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string B2bApiKey { get; set; } + public string PadApiKey { get; set; } + } +} \ No newline at end of file diff --git a/src/dotnet/padp-reference-web/PadpReferenceApi/model/ValidateOtpRequestV3.cs b/src/dotnet/padp-reference-web/PadpReferenceApi/model/ValidateOtpRequestV3.cs new file mode 100644 index 0000000..9f7e0a9 --- /dev/null +++ b/src/dotnet/padp-reference-web/PadpReferenceApi/model/ValidateOtpRequestV3.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +public class ValidateOtpRequestV3 +{ + [JsonPropertyName("otp")] + public string otp { get; set; } + + [JsonPropertyName("source")] + public string source { get; set; } + + [JsonPropertyName("recipient")] + public string recipient { get; set; } +} \ No newline at end of file -- 2.45.2