diff --git a/.azuredevops/pipelines/build-dcr-func.yml b/.azuredevops/pipelines/build-dcr-func.yml index 3ba6896..28cd2f0 100644 --- a/.azuredevops/pipelines/build-dcr-func.yml +++ b/.azuredevops/pipelines/build-dcr-func.yml @@ -1,3 +1,11 @@ +schedules: +- cron: '0 5 * * 0' + displayName: 'Run at 5:00 AM every Sunday (UTC)' + always: true + branches: + include: + - develop + trigger: - develop - main @@ -8,10 +16,10 @@ pool: steps: - task: UseDotNet@2 - displayName: 'Install .NET 6 SDK' + displayName: 'Install .NET 8 SDK' inputs: packageType: 'sdk' - version: '6.0.x' + version: '8.0.x' performMultiLevelLookup: true - script: | diff --git a/.azuredevops/pipelines/build-dh-func.yml b/.azuredevops/pipelines/build-dh-func.yml index 50c7fe2..8927d6c 100644 --- a/.azuredevops/pipelines/build-dh-func.yml +++ b/.azuredevops/pipelines/build-dh-func.yml @@ -1,3 +1,11 @@ +schedules: +- cron: '0 5 * * 0' + displayName: 'Run at 5:00 AM every Sunday (UTC)' + always: true + branches: + include: + - develop + trigger: - develop - main @@ -8,10 +16,10 @@ pool: steps: - task: UseDotNet@2 - displayName: 'Install .NET 6 SDK' + displayName: 'Install .NET 8 SDK' inputs: packageType: 'sdk' - version: '6.0.x' + version: '8.0.x' performMultiLevelLookup: true - script: | diff --git a/.azuredevops/pipelines/build-no-tests.yml b/.azuredevops/pipelines/build-no-tests.yml index 0a5cf0d..9f9f715 100644 --- a/.azuredevops/pipelines/build-no-tests.yml +++ b/.azuredevops/pipelines/build-no-tests.yml @@ -59,7 +59,7 @@ steps: condition: always() inputs: packageType: sdk - version: '6.0.x' + version: '8.0.x' performMultiLevelLookup: true - task: CmdLine@2 diff --git a/.azuredevops/pipelines/build-v2.yml b/.azuredevops/pipelines/build-v2.yml index b129292..fca897e 100644 --- a/.azuredevops/pipelines/build-v2.yml +++ b/.azuredevops/pipelines/build-v2.yml @@ -1,4 +1,11 @@ -# Build pipeline v2 (Containerised) +schedules: +- cron: '0 5 * * 0' + displayName: 'Run at 5:00 AM every Sunday (UTC)' + always: true + branches: + include: + - develop + variables: @@ -194,18 +201,18 @@ jobs: artifact: Mock-Data-Recipient - E2E tests - task: UseDotNet@2 - displayName: 'Use .NET 6 sdk' + displayName: 'Use .NET 8 sdk' condition: always() inputs: packageType: sdk - version: '6.0.x' + version: '8.0.x' performMultiLevelLookup: true - task: CmdLine@2 displayName: 'Install dotnet-ef' condition: always() inputs: - script: 'dotnet tool install --version 7.0.13 --global dotnet-ef' + script: 'dotnet tool install --version 8.0.4 --global dotnet-ef' - task: CmdLine@2 displayName: 'Check dotnet-ef version' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4cce631..6c83d51 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,8 +12,7 @@ on: - '.github/ISSUE_TEMPLATE/**' - '.github/pull_request_template.md' - '.github/stale.yml' - - 'LICENSE' - - 'Postman/**' + - 'LICENSE' pull_request: branches: [main, develop] types: [opened, synchronize, reopened] @@ -24,8 +23,7 @@ on: - '.github/ISSUE_TEMPLATE/**' - '.github/pull_request_template.md' - '.github/stale.yml' - - 'LICENSE' - - 'Postman/**' + - 'LICENSE' env: DOCKER_IMAGE: consumerdataright/mock-data-recipient @@ -36,11 +34,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Docker Metadata id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v5 with: # list of Docker images to use as base name for tags images: | @@ -54,21 +52,21 @@ jobs: type=semver,pattern={{major}} - name: Setup Docker QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub if: ${{ github.repository_owner == 'ConsumerDataRight' && github.event_name != 'pull_request' }} - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Docker image id: docker_build - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v6 with: context: ./Source file: ./Source/Dockerfile diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 43666af..abb644a 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -10,8 +10,7 @@ on: - '.github/ISSUE_TEMPLATE/**' - '.github/pull_request_template.md' - '.github/stale.yml' - - 'LICENSE' - - 'Postman/**' + - 'LICENSE' pull_request: branches: [ main, develop ] types: [opened, synchronize, reopened] @@ -22,8 +21,7 @@ on: - '.github/ISSUE_TEMPLATE/**' - '.github/pull_request_template.md' - '.github/stale.yml' - - 'LICENSE' - - 'Postman/**' + - 'LICENSE' env: buildConfiguration: 'Release' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a1cdb7..c00372d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.1.0] - 2024-08-16 +### Changed +- Updated nuget package versions + +## [2.0.0] - 2024-06-12 +### Changed +- Migrated from .NET 6 to .NET 8 +- Migrated docker compose from v1 to v2 + +## [1.3.0] - 2024-03-13 +### Changed +- Removed Bank Participant Data scope (i.e. cdr-register:bank:read) references. +- Updated NuGet packages to avoid vulnerabilities. +- DCR and PAR screen defaults to Authorisation Code Flow (ACF). + +### Fixed +- Fixed Consumer Data Sharing Common swagger proxy UI failure due to 'IndustryName' validation - [Mock Data Recipient Issue 68](https://github.com/ConsumerDataRight/mock-data-recipient/issues/68) + ## [1.2.5] - 2023-11-29 ### Fixed - Refactored code and minor bug fixes diff --git a/Help/azurefunctions/HELP.md b/Help/azurefunctions/HELP.md index b2c3431..9f5ee83 100644 --- a/Help/azurefunctions/HELP.md +++ b/Help/azurefunctions/HELP.md @@ -45,7 +45,7 @@ azurite --silent --location c:\azurite --debug c:\azurite\debug.log ``` navigate to .\mock-data-holder\Source\CDR.GetDataRecipients -func start --verbose +func host start --verbose ```
diff --git a/Help/container/HELP.md b/Help/container/HELP.md index b233ddc..bdfa1aa 100644 --- a/Help/container/HELP.md +++ b/Help/container/HELP.md @@ -49,7 +49,7 @@ Example of accepting the `ACCEPT_EULA` environment variable of the SQL Server co ``` mssql: container_name: sql1 - image: 'mcr.microsoft.com/mssql/server:2019-latest' + image: 'mcr.microsoft.com/mssql/server:2022-latest' ports: - '1433:1433' environment: @@ -109,7 +109,7 @@ docker build -f Dockerfile -t mock-data-recipient . ``` Run the SQL Server image. ``` -docker run -d -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pa{}w0rd2019" -p 1433:1433 --name sql1 -h sql1 -d mcr.microsoft.com/mssql/server:2019-latest +docker run -d -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pa{}w0rd2019" -p 1433:1433 --name sql1 -h sql1 -d mcr.microsoft.com/mssql/server:2022-latest ``` Run the new docker image. ``` diff --git a/README.md b/README.md index 10abca6..6a4c525 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Consumer Data Right Logo](./cdr-logo.png?raw=true) -[![Consumer Data Standards v1.27.0](https://img.shields.io/badge/Consumer%20Data%20Standards-v1.27.0-blue.svg)](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.27.0/#introduction) +[![Consumer Data Standards v1.31.0](https://img.shields.io/badge/Consumer%20Data%20Standards-v1.31.0-blue.svg)](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.31.0/#introduction) [![made-with-dotnet](https://img.shields.io/badge/Made%20with-.NET-1f425Ff.svg)](https://dotnet.microsoft.com/) [![made-with-csharp](https://img.shields.io/badge/Made%20with-C%23-1f425Ff.svg)](https://docs.microsoft.com/en-us/dotnet/csharp/) [![MIT License](https://img.shields.io/github/license/ConsumerDataRight/mock-data-recipient)](./LICENSE) @@ -13,7 +13,7 @@ This repository contains a mock implementation of a Data Recipient and is offere ## Mock Data Recipient - Alignment The Mock Data Recipient in this release: -* aligns to [v1.27.0](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.27.0/#introduction) of the [Consumer Data Standards](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.27.0/#introduction); +* aligns to [v1.31.0](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.31.0/#introduction) of the [Consumer Data Standards](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.31.0/#introduction); * can connect to and complete authentication against both [FAPI 1.0 Migration Phase 2 and Phase 3](https://consumerdatastandardsaustralia.github.io/standards/#authentication-flows) compliant data holders. ## Getting Started @@ -89,7 +89,7 @@ The Mock Data Recipient contains the following components: - Used internally within the Mock Data Recipient to simplify interactions with the Register and Data Holders. - Azure Functions - Azure Functions that can automate the continuous Get Data Holders discovery and Dynamic Client Registration process. - - For each Data Holder retrieved from the Register, a message will be added to the DynamicClietnRegistration queue. A function listening to the queue, will pick up the message and attempt to register the Data Recipient with the Data Holder. + - For each Data Holder retrieved from the Register, a message will be added to the DynamicClientRegistration queue. A function listening to the queue, will pick up the message and attempt to register the Data Recipient with the Data Holder. - To get help on the Azure Functions, see the [help guide](./Help/azurefunctions/HELP.md). - Repository - A SQL repository is included that contains local data used within the Mock Data Recipient. @@ -100,7 +100,7 @@ The Mock Data Recipient contains the following components: ## Technology Stack The following technologies have been used to build the Mock Data Recipient: -- The source code has been written in `C#` using the `.Net 6` framework. +- The source code has been written in `C#` using the `.Net 8` framework. - The Repository utilises a `SQL` instance. # Testing @@ -125,4 +125,4 @@ See our [security policy](./SECURITY.md) for information on security controls, r [MIT License](./LICENSE) # Notes -The Mock Data Recipient is provided as a development tool only. It conforms to the Consumer Data Standards. +The Mock Data Recipient is provided as a development tool only. It conforms to the Consumer Data Standards. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index 084cf77..ef53b05 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,9 +11,8 @@ Visit our [Responsible disclosure of security vulnerabilities policy](https://ww | Version | Supported | | ------- | ------------------ | -| 1.2.x | :white_check_mark: | -| 1.1.x | :x: | -| 1.0.x | :x: | +| 2.1.x | :white_check_mark: | +| 1.x.x | :x: | ## Reporting a Vulnerability diff --git a/Source/CDR.DCR/CDR.DCR.csproj b/Source/CDR.DCR/CDR.DCR.csproj index 7d979dd..89b420d 100644 --- a/Source/CDR.DCR/CDR.DCR.csproj +++ b/Source/CDR.DCR/CDR.DCR.csproj @@ -1,23 +1,30 @@  - net6.0 v4 <_FunctionsSkipCleanOutput>true - 1.2.5 - 1.2.5 - 1.2.5 + $(TargetFrameworkVersion) + $(Version) + $(Version) + $(Version) + Exe + enabled - - - - + + + + + + + + PreserveNewest + Always @@ -29,4 +36,7 @@ Never - + + + + \ No newline at end of file diff --git a/Source/CDR.DCR/DCR.cs b/Source/CDR.DCR/DCR.cs index 4b9e50a..cdeb070 100644 --- a/Source/CDR.DCR/DCR.cs +++ b/Source/CDR.DCR/DCR.cs @@ -5,160 +5,130 @@ using CDR.DataRecipient.SDK.Enum; using CDR.DataRecipient.SDK.Extensions; using CDR.DataRecipient.SDK.Models; -using Microsoft.Azure.WebJobs; +using CDR.DCR.Extensions; +using CDR.DCR.Models; +using Microsoft.Azure.Functions.Worker; using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; -using Microsoft.Azure.Storage.Queue; using Newtonsoft.Json; using System; -using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Net.Http; using System.Net.Http.Headers; -using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; -using System.Linq; -using CDR.DCR.Models; namespace CDR.DCR { - public static class DynamicClientRegistrationFunction + public class DynamicClientRegistrationFunction { - // Error - Unable to perform DCR as there are no mutually supported values in the mandatory claim [CLAIM_NAME] - private const string ErrorMessage = "Unable to perform DCR as there are no mutually supported values in the mandatory claim"; + private readonly ILogger _logger; + private readonly DcrOptions _options; + private readonly IHttpClientFactory _httpClientFactory; + private readonly X509Certificate2 _signingCertificate; + + public DynamicClientRegistrationFunction(ILogger logger, IOptions options, IHttpClientFactory httpClientFactory) + { + _options = options.Value; + _logger = logger; + _httpClientFactory = httpClientFactory; + + //get the certs + _logger.LogInformation("Loading the client certificate..."); + byte[] clientCertBytes = Convert.FromBase64String(_options.Client_Certificate); + X509Certificate2 cclientCertificate = new(clientCertBytes, _options.Client_Certificate_Password, X509KeyStorageFlags.MachineKeySet); + _logger.LogInformation("Client certificate loaded: {thumbprint}", cclientCertificate.Thumbprint); + + + _logger.LogInformation("Loading the signing certificate..."); + byte[] signCertBytes = Convert.FromBase64String(_options.Signing_Certificate); + _signingCertificate = new(signCertBytes, _options.Signing_Certificate_Password, X509KeyStorageFlags.MachineKeySet); + _logger.LogInformation("Signing certificate loaded: {thumbprint}", _signingCertificate.Thumbprint); + } /// /// Dynamic Client Registration Function /// /// Registers the Data Holders in the messaging queue and updates the local repository - [FunctionName("FunctionDCR")] - public static async Task DCR([QueueTrigger("dynamicclientregistration", Connection = "StorageConnectionString")] CloudQueueMessage myQueueItem, ILogger log, ExecutionContext context) + [Function("FunctionDCR")] + public async Task DCR([QueueTrigger("dynamicclientregistration", Connection = "StorageConnectionString")] DcrQueueMessage myQueueItem, FunctionContext context) { string msg = string.Empty; string dataHolderBrandName = string.Empty; string infosecBaseUri = string.Empty; string regEndpoint = string.Empty; - DcrQueueMessage myQMsg = JsonConvert.DeserializeObject(myQueueItem.AsString); - try { - var isLocalDev = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT").Equals("Development"); - var configBuilder = new ConfigurationBuilder().SetBasePath(context.FunctionAppDirectory); - - if (isLocalDev) - { - configBuilder = configBuilder.AddJsonFile("local.settings.json", optional: false, reloadOnChange: true); - } - - // Get environment variables. - string qConnString = Environment.GetEnvironmentVariable("StorageConnectionString"); - string dbConnString = Environment.GetEnvironmentVariable("DataRecipient_DB_ConnectionString"); - string dbLoggingConnString = Environment.GetEnvironmentVariable("DataRecipient_Logging_DB_ConnectionString"); - string tokenEndpoint = Environment.GetEnvironmentVariable("Register_Token_Endpoint"); - string ssaEndpoint = Environment.GetEnvironmentVariable("Register_Get_SSA_Endpoint"); - string xv = Environment.GetEnvironmentVariable("Register_Get_SSA_XV"); - string brandId = Environment.GetEnvironmentVariable("Brand_Id"); - string softwareProductId = Environment.GetEnvironmentVariable("Software_Product_Id"); - string redirectUris = Environment.GetEnvironmentVariable("Redirect_Uris"); - string clientCert = Environment.GetEnvironmentVariable("Client_Certificate"); - string clientCertPwd = Environment.GetEnvironmentVariable("Client_Certificate_Password"); - string signCert = Environment.GetEnvironmentVariable("Signing_Certificate"); - string signCertPwd = Environment.GetEnvironmentVariable("Signing_Certificate_Password"); - var retries = Convert.ToInt16(Environment.GetEnvironmentVariable("Retries")); - bool ignoreServerCertificateErrors = Environment.GetEnvironmentVariable("Ignore_Server_Certificate_Errors").Equals("true", StringComparison.OrdinalIgnoreCase); - - // DCR queue. - log.LogInformation("Retrieving count for dynamicclientregistration queue..."); + _logger.LogInformation("Retrieving count for dynamicclientregistration queue..."); string qName = "dynamicclientregistration"; - int qCount = await GetQueueCountAsync(qConnString, qName); - log.LogInformation($"qCount = {qCount}"); + int qCount = await GetQueueCountAsync(_options.StorageConnectionString, qName); + _logger.LogInformation("qCount = {count}", qCount); - if (string.IsNullOrEmpty(myQMsg.DataHolderBrandId)) + if (string.IsNullOrEmpty(myQueueItem.DataHolderBrandId)) { // Add messsage to deadletter queue - await AddDeadLetterQueMsgAsync(log, dbConnString, qConnString, myQMsg.DataHolderBrandId, myQueueItem, "deadletter"); + await AddDeadLetterQueMsgAsync(myQueueItem, _options.DeadLetterQueueName, context); } else { - msg = $"DHBrandId - {myQMsg.DataHolderBrandId}"; - - log.LogInformation("Loading the client certificate..."); - byte[] clientCertBytes = Convert.FromBase64String(clientCert); - X509Certificate2 clientCertificate = new(clientCertBytes, clientCertPwd, X509KeyStorageFlags.MachineKeySet); - log.LogInformation("Client certificate loaded: {thumbprint}", clientCertificate.Thumbprint); + msg = $"DHBrandId - {myQueueItem.DataHolderBrandId}"; - log.LogInformation("Loading the signing certificate..."); - byte[] signCertBytes = Convert.FromBase64String(signCert); - X509Certificate2 signCertificate = new(signCertBytes, signCertPwd, X509KeyStorageFlags.MachineKeySet); - log.LogInformation("Signing certificate loaded: {thumbprint}", signCertificate.Thumbprint); - - Response tokenResponse = await GetAccessToken(tokenEndpoint, softwareProductId, clientCertificate, signCertificate, log, ignoreServerCertificateErrors); + Response tokenResponse = await GetAccessToken(); if (tokenResponse.IsSuccessful) - { - var softwareStatementAssertion = new SoftwareStatementAssertion() - { - SsaEndpoint = ssaEndpoint, - Version = xv, - AccessToken = tokenResponse.Data.AccessToken, - ClientCertificate = clientCertificate, - BrandId = brandId, - SoftwareProductId = softwareProductId, - Log = log, - IgnoreServerCertificateErrors = ignoreServerCertificateErrors - }; - var ssa = await GetSoftwareStatementAssertion(softwareStatementAssertion); + { + var ssa = await GetSoftwareStatementAssertion(tokenResponse.Data.AccessToken); if (ssa.IsSuccessful) - { + { //DOES the Data Holder Brand EXIST in the REPO? - DataHolderBrand dh = await new SqlDataAccess(dbConnString).GetDataHolderBrand(myQMsg.DataHolderBrandId); + DataHolderBrand dh = await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).GetDataHolderBrand(myQueueItem.DataHolderBrandId); if (dh == null) { // NO - DOES the DcrMessage exist? - (string dcrMsgId, string dcrMsgState) = await new SqlDataAccess(dbConnString).CheckDcrMessageExistByDHBrandId(myQMsg.DataHolderBrandId); - if (!string.IsNullOrEmpty(dcrMsgId)) + var result = await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).CheckDcrMessageExistByDHBrandId(myQueueItem.DataHolderBrandId); + if (!string.IsNullOrEmpty(result.msgId)) { // YES - UPDATE EXISTING DcrMessage (with ADDED Queue MessageId, Failed STATE and ERROR) DcrMessage dcrMsg = new() { DataHolderBrandId = Guid.Empty, - MessageId = dcrMsgId, + MessageId = result.msgId, MessageState = Message.DCRFailed.ToString(), MessageError = $"{msg} - does not exist in the repo" }; - await new SqlDataAccess(dbConnString).UpdateDcrMsgReplaceMessageIdWithoutBrand(dcrMsg, myQueueItem.Id); + + await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).UpdateDcrMsgReplaceMessageIdWithoutBrand(dcrMsg, context.BindingContext.BindingData["Id"].ToString()); } - await InsertLog(log, dbConnString, $"{msg} - does not exist in the repo", "Error", "DCR"); + await InsertLog(_options.DataRecipient_DB_ConnectionString, $"{msg} - does not exist in the repo", "Error", "DCR"); } else { - dataHolderBrandName = dh.BrandName; + dataHolderBrandName = dh.BrandName; // YES - DOES a Registration already exist for the DataHolderBrandId in the local repo? - string clientId = await new SqlDataAccess(dbConnString).GetRegByDHBrandId(dh.DataHolderBrandId); - if (clientId == string.Empty) + string clientId = await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).GetRegByDHBrandId(dh.DataHolderBrandId); + if (string.IsNullOrEmpty(clientId)) { // NO - register this Data Holder Brand infosecBaseUri = dh.EndpointDetail.InfoSecBaseUri; - var oidcDiscovery = (await GetOidcDiscovery(infosecBaseUri, ignoreServerCertificateErrors: ignoreServerCertificateErrors)).Data; + var oidcDiscovery = (await GetOidcDiscovery(infosecBaseUri)).Data; if (oidcDiscovery != null) { regEndpoint = oidcDiscovery.RegistrationEndpoint; - var dcrRequest = new DcrRequest() + var dcrRequest = new DcrRequest() { - SoftwareProductId = softwareProductId, - RedirectUris = redirectUris, - Ssa = ssa.Data, + SoftwareProductId = _options.Software_Product_Id, + RedirectUris = _options.Redirect_Uris, + Ssa = ssa.Data, Audience = oidcDiscovery.Issuer, ResponseTypesSupported = oidcDiscovery.ResponseTypesSupported, AuthorizationSigningResponseAlgValuesSupported = oidcDiscovery.AuthorizationSigningResponseAlgValuesSupported, AuthorizationEncryptionResponseEncValuesSupported = oidcDiscovery.AuthorizationEncryptionResponseEncValuesSupported, AuthorizationEncryptionResponseAlgValuesSupported = oidcDiscovery.AuthorizationEncryptionResponseAlgValuesSupported, - SignCertificate = signCertificate + SignCertificate = _signingCertificate }; (string errorMessage, string dcrRequestJwt) = PopulateDCRRequestJwt(dcrRequest); @@ -169,13 +139,13 @@ public static async Task DCR([QueueTrigger("dynamicclientregistration", Connecti string regMessage = ""; string regClientId = ""; string jsonDoc = ""; - + // DO NOT register if FAPI claims are invalid if (string.IsNullOrEmpty(errorMessage)) { do { - var dcrResponse = await Register(regEndpoint, clientCertificate, dcrRequestJwt, ignoreServerCertificateErrors: ignoreServerCertificateErrors); + var dcrResponse = await Register(regEndpoint, dcrRequestJwt); if (dcrResponse.IsSuccessful) { regSuccess = true; @@ -187,9 +157,15 @@ public static async Task DCR([QueueTrigger("dynamicclientregistration", Connecti { regStatusCode = dcrResponse.StatusCode.ToString(); regMessage = dcrResponse.Message; - retries--; + + //no need to retry if the error is duplicate registration. + if (dcrResponse.Message.Contains("ERR-DCR-001")) + { + break; + } + _options.Retries--; } - } while (!regSuccess && retries > 0); + } while (!regSuccess && _options.Retries > 0); } // Successful -> Update DcrMessage and Insert into Data Holder Registration repo // DO NOT register if FAPI claims are invalid @@ -203,19 +179,19 @@ public static async Task DCR([QueueTrigger("dynamicclientregistration", Connecti InfosecBaseUri = infosecBaseUri, MessageState = Message.DCRComplete.ToString() }; - await new SqlDataAccess(dbConnString).UpdateDcrMsgByDHBrandId(dcrMsg); + await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).UpdateDcrMsgByDHBrandId(dcrMsg); - var dcrInserted = await new SqlDataAccess(dbConnString).InsertDcrRegistration(regClientId, dh.DataHolderBrandId, jsonDoc); + var dcrInserted = await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).InsertDcrRegistration(regClientId, dh.DataHolderBrandId, jsonDoc); if (dcrInserted) - await InsertLog(log, dbConnString, $"{msg}, REGISTERED as ClientId - {regClientId}, {qCount - 1} items remain in queue, ", "Information", "DCR"); + await InsertLog(_options.DataRecipient_DB_ConnectionString, $"{msg}, REGISTERED as ClientId - {regClientId}, {qCount - 1} items remain in queue, ", "Information", "DCR"); else - await InsertLog(log, dbConnString, $"{msg}, REGISTERED as ClientId - {regClientId}, {qCount - 1} items remain in queue - FAILED to add to MDR REPO", "Error", "DCR"); + await InsertLog(_options.DataRecipient_DB_ConnectionString, $"{msg}, REGISTERED as ClientId - {regClientId}, {qCount - 1} items remain in queue - FAILED to add to MDR REPO", "Error", "DCR"); } // FAILED -> Update DcrMessage as DCRFailed // FAPI 1.0 validation errors should also be logged else if (!string.IsNullOrEmpty(errorMessage)) - { + { DcrMessage dcrMsg = new() { DataHolderBrandId = new Guid(dh.DataHolderBrandId), @@ -224,8 +200,8 @@ public static async Task DCR([QueueTrigger("dynamicclientregistration", Connecti MessageState = Message.DCRFailed.ToString(), MessageError = $"{errorMessage}" }; - await new SqlDataAccess(dbConnString).UpdateDcrMsgByDHBrandId(dcrMsg); - await InsertLog(log, dbConnString, $"{msg}, REGISTRATION CLAIMS VALIDATIONS FAILED, {errorMessage}", "Error", "DCR"); + await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).UpdateDcrMsgByDHBrandId(dcrMsg); + await InsertLog(_options.DataRecipient_DB_ConnectionString, $"{msg}, REGISTRATION CLAIMS VALIDATIONS FAILED, {errorMessage}", "Error", "DCR"); } else if (!regSuccess) { @@ -238,8 +214,8 @@ public static async Task DCR([QueueTrigger("dynamicclientregistration", Connecti MessageState = Message.DCRFailed.ToString(), MessageError = $"StatusCode: {regStatusCode}, {regMessage}" }; - await new SqlDataAccess(dbConnString).UpdateDcrMsgByDHBrandId(dcrMsg); - await InsertLog(log, dbConnString, $"{msg}, REGISTRATION FAILED, StatusCode: {regStatusCode}, {regMessage}", "Error", "DCR"); + await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).UpdateDcrMsgByDHBrandId(dcrMsg); + await InsertLog(_options.DataRecipient_DB_ConnectionString, $"{msg}, REGISTRATION FAILED, StatusCode: {regStatusCode}, {regMessage}", "Error", "DCR"); } } else @@ -247,53 +223,50 @@ public static async Task DCR([QueueTrigger("dynamicclientregistration", Connecti // Oidc Discovery failed DcrMessage dcrMsg = new() { - DataHolderBrandId = new Guid(myQMsg.DataHolderBrandId), + DataHolderBrandId = new Guid(myQueueItem.DataHolderBrandId), BrandName = dh.BrandName, InfosecBaseUri = infosecBaseUri, MessageState = Message.DCRFailed.ToString(), MessageError = "OidcDiscovery failed InfosecBaseUri: " + infosecBaseUri }; - await new SqlDataAccess(dbConnString).UpdateDcrMsgByDHBrandId(dcrMsg); + await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).UpdateDcrMsgByDHBrandId(dcrMsg); string extraMsg = ""; if (!string.IsNullOrEmpty(infosecBaseUri)) extraMsg = " - InfosecBaseUri: " + infosecBaseUri; - await InsertLog(log, dbLoggingConnString, $"{msg}, REGISTRATION FAILED{extraMsg}", "Exception", "DCR"); + await InsertLog(_options.DataRecipient_Logging_DB_ConnectionString, $"{msg}, REGISTRATION FAILED{extraMsg}", "Exception", "DCR"); } } else { // YES - log this result - await InsertLog(log, dbConnString, $"{msg} - is trying to be REGISTERED, but is already REGISTERED to ClientId - {clientId}", "Error", "DCR"); + await InsertLog(_options.DataRecipient_Logging_DB_ConnectionString, $"{msg} - is trying to be REGISTERED, but is already REGISTERED to ClientId - {clientId}", "Error", "DCR"); } } } else { - await InsertLog(log, dbConnString, $"{msg}, Unable to get the SSA from: {ssaEndpoint}, Ver: {xv}, BrandId: {brandId}, SoftwareProductId - {softwareProductId}", "Error", "DCR"); + await InsertLog(_options.DataRecipient_Logging_DB_ConnectionString, $"{msg}, Unable to get the SSA from: {_options.Register_Get_SSA_Endpoint}, Ver: {_options.Register_Get_SSA_XV}, BrandId: {_options.Brand_Id}, SoftwareProductId - {_options.Software_Product_Id}", "Error", "DCR"); } } else { - await InsertLog(log, dbConnString, $"{msg}, Unable to get the Access Token for SoftwareProductId - {softwareProductId} - at the endpoint - {tokenEndpoint}", "Error", "DCR"); + await InsertLog(_options.DataRecipient_Logging_DB_ConnectionString, $"{msg}, Unable to get the Access Token for SoftwareProductId - {_options.Software_Product_Id} - at the endpoint - {_options.Register_Token_Endpoint}", "Error", "DCR"); } } } catch (Exception ex) { - string dbConnString = Environment.GetEnvironmentVariable("DataRecipient_DB_ConnectionString"); - string dbLoggingConnString = Environment.GetEnvironmentVariable("DataRecipient_Logging_DB_ConnectionString"); - DcrMessage dcrMsg = new() { - DataHolderBrandId = new Guid(myQMsg.DataHolderBrandId), + DataHolderBrandId = new Guid(myQueueItem.DataHolderBrandId), BrandName = dataHolderBrandName, InfosecBaseUri = infosecBaseUri, MessageState = Message.DCRFailed.ToString(), MessageError = ex.Message }; - await new SqlDataAccess(dbConnString).UpdateDcrMsgByDHBrandId(dcrMsg); + await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).UpdateDcrMsgByDHBrandId(dcrMsg); string extraMsg = ""; if (!string.IsNullOrEmpty(infosecBaseUri)) @@ -304,10 +277,10 @@ public static async Task DCR([QueueTrigger("dynamicclientregistration", Connecti if (ex is JsonReaderException) { - await InsertLog(log, dbLoggingConnString, $"{msg}, REGISTRATION FAILED: OidcDiscovery can't be deserialized {extraMsg}", "Exception", "DCR", ex); + await InsertLog(_options.DataRecipient_Logging_DB_ConnectionString, $"{msg}, REGISTRATION FAILED: OidcDiscovery can't be deserialized {extraMsg}", "Exception", "DCR", ex); } - else - await InsertLog(log, dbLoggingConnString, $"{msg}, REGISTRATION FAILED{extraMsg}", "Exception", "DCR", ex); + else + await InsertLog(_options.DataRecipient_Logging_DB_ConnectionString, $"{msg}, REGISTRATION FAILED{extraMsg}", "Exception", "DCR", ex); } } @@ -315,24 +288,18 @@ public static async Task DCR([QueueTrigger("dynamicclientregistration", Connecti /// Get Access Token /// /// JWT - private static async Task> GetAccessToken( - string tokenEndpoint, - string clientId, - X509Certificate2 clientCertificate, - X509Certificate2 signingCertificate, - ILogger log, - bool ignoreServerCertificateErrors = false) + private async Task> GetAccessToken() { // Setup the http client. - var client = GetHttpClient(clientCertificate, ignoreServerCertificateErrors: ignoreServerCertificateErrors); + var client = GetHttpClient(); // Make the request to the token endpoint. - log.LogInformation("Retrieving access_token from the Register: {tokenEndpoint}", tokenEndpoint); + _logger.LogInformation("Retrieving access_token from the Register: {tokenEndpoint}", _options.Register_Token_Endpoint); var response = await client.SendPrivateKeyJwtRequest( - tokenEndpoint, - signingCertificate, - clientId, - clientId, + _options.Register_Token_Endpoint, + _signingCertificate, + _options.Software_Product_Id, + _options.Software_Product_Id, scope: Constants.Scopes.CDR_REGISTER, grantType: Constants.GrantTypes.CLIENT_CREDENTIALS); @@ -342,7 +309,7 @@ private static async Task> GetAccessToken( StatusCode = response.StatusCode }; - log.LogInformation("Register response: {statusCode} - {body}", tokenResponse.StatusCode, body); + _logger.LogInformation("Register response: {statusCode} - {body}", tokenResponse.StatusCode, body); if (response.IsSuccessStatusCode) tokenResponse.Data = JsonConvert.DeserializeObject(body); @@ -355,15 +322,15 @@ private static async Task> GetAccessToken( /// /// Generate the SSA /// - private static async Task> GetSoftwareStatementAssertion(SoftwareStatementAssertion softwareStatementAssertion) + private async Task> GetSoftwareStatementAssertion(string accessToken) { // Setup the request to the get ssa endpoint. - var endpoint = $"{softwareStatementAssertion.SsaEndpoint}{softwareStatementAssertion.BrandId}/software-products/{softwareStatementAssertion.SoftwareProductId}/ssa"; + var endpoint = $"{_options.Register_Get_SSA_Endpoint}{_options.Brand_Id}/software-products/{_options.Software_Product_Id}/ssa"; // Setup the http client. - var client = GetHttpClient(softwareStatementAssertion.ClientCertificate, softwareStatementAssertion.AccessToken, softwareStatementAssertion.Version, ignoreServerCertificateErrors: softwareStatementAssertion.IgnoreServerCertificateErrors); + var client = GetHttpClient( accessToken, _options.Register_Get_SSA_XV); - softwareStatementAssertion.Log.LogInformation("Retrieving SSA from the Register: {ssaEndpoint}", endpoint); + _logger.LogInformation("Retrieving SSA from the Register: {ssaEndpoint}", endpoint); // Make the request to the get data holder brands endpoint. var response = await client.GetAsync(endpoint); @@ -373,7 +340,7 @@ private static async Task> GetSoftwareStatementAssertion(Softwa StatusCode = response.StatusCode }; - softwareStatementAssertion.Log.LogInformation("SSA response: {statusCode} - {body}", ssaResponse.StatusCode, body); + _logger.LogInformation("SSA response: {statusCode} - {body}", ssaResponse.StatusCode, body); if (response.IsSuccessStatusCode) { @@ -390,18 +357,11 @@ private static async Task> GetSoftwareStatementAssertion(Softwa /// /// Get the OpenID Discovery /// - private static async Task> GetOidcDiscovery(string infosecBaseUri, bool ignoreServerCertificateErrors = false) + private async Task> GetOidcDiscovery(string infosecBaseUri) { var oidcResponse = new Response(); - var clientHandler = new HttpClientHandler(); - - if (ignoreServerCertificateErrors) - { - clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; - } - - var client = new HttpClient(clientHandler); + var client = GetHttpClient(); var configUrl = string.Concat(infosecBaseUri.TrimEnd('/'), "/.well-known/openid-configuration"); var configResponse = await client.GetAsync(configUrl); @@ -422,101 +382,8 @@ private static async Task> GetOidcDiscovery(string infos /// private static (string, string) PopulateDCRRequestJwt(DcrRequest dcrRequest) { - string errorMessage = string.Empty; - var claims = new List - { - new Claim("jti", Guid.NewGuid().ToString()), - new Claim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer), - new Claim("token_endpoint_auth_signing_alg", "PS256"), - new Claim("token_endpoint_auth_method", "private_key_jwt"), - new Claim("application_type", "web"), - new Claim("id_token_signed_response_alg", "PS256"), - new Claim("id_token_encrypted_response_alg", "RSA-OAEP"), - new Claim("id_token_encrypted_response_enc", "A256GCM"), - new Claim("request_object_signing_alg", "PS256"), - new Claim("software_statement", dcrRequest.Ssa ?? ""), - new Claim("grant_types", "client_credentials"), - new Claim("grant_types", "authorization_code"), - new Claim("grant_types", "refresh_token") - }; - - // response_types updated below "code, code id_token" both types are returned and added below - // A response type is mandatory - if (!dcrRequest.ResponseTypesSupported.Contains("code") && !dcrRequest.ResponseTypesSupported.Contains("code id_token")) - { - // Return the error - errorMessage = ErrorMessage + " response_types"; - return (errorMessage, ""); - } - - var responseTypesList = dcrRequest.ResponseTypesSupported.Where(x => x.ToLower().Equals("code") || x.ToLower().Equals("code id_token")).ToList(); - claims.Add(new Claim("response_types", JsonConvert.SerializeObject(responseTypesList), JsonClaimValueTypes.JsonArray)); - + var (claims, errorMessage) = dcrRequest.CreateClaimsForDCRRequest(); - var isCodeFlow = dcrRequest.ResponseTypesSupported.Contains("code"); - if (isCodeFlow && !dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Any()) - { - // Log error message to the mandatory claim missing - errorMessage = ErrorMessage + " authorization_signed_response_alg"; - return (errorMessage, ""); - } - - // Mandatory for code flow - if (isCodeFlow) - { - if (!dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Contains("PS256") && !dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Contains("ES256")) - { - // Return the error - errorMessage = ErrorMessage + " authorization_signed_response_alg"; - return (errorMessage, ""); - } - - if (dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Contains("PS256")) - { - claims.Add(new Claim("authorization_signed_response_alg", "PS256")); - } - else if (dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Contains("ES256")) - { - claims.Add(new Claim("authorization_signed_response_alg", "ES256")); - } - } - - // Check if the enc is empty but a alg is specified. - if ((dcrRequest.AuthorizationEncryptionResponseEncValuesSupported == null || !dcrRequest.AuthorizationEncryptionResponseEncValuesSupported.Any()) // No enc specified - && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported != null && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Contains("RSA-OAEP-256") - && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Contains("RSA-OAEP")) // but alg specified. - { - errorMessage = ErrorMessage + " authorization_encrypted_response_enc"; - return (errorMessage, ""); - } - - - if (dcrRequest.AuthorizationEncryptionResponseEncValuesSupported != null && dcrRequest.AuthorizationEncryptionResponseEncValuesSupported.Contains("A128CBC-HS256")) - { - claims.Add(new Claim("authorization_encrypted_response_enc", "A128CBC-HS256")); - } - else if (dcrRequest.AuthorizationEncryptionResponseEncValuesSupported != null && dcrRequest.AuthorizationEncryptionResponseEncValuesSupported.Contains("A256GCM")) - { - claims.Add(new Claim("authorization_encrypted_response_enc", "A256GCM")); - } - - // Conditional: Optional for response_type "code" if authorization_encryption_enc_values_supported is present - if (isCodeFlow && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported != null && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Any()) - { - if (dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Contains("RSA-OAEP-256")) - { - claims.Add(new Claim("authorization_encrypted_response_alg", "RSA-OAEP-256")); - } - else if (dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Contains("RSA-OAEP")) - { - claims.Add(new Claim("authorization_encrypted_response_alg", "RSA-OAEP")); - } - } - - char[] delimiters = { ',', ' '}; - var redirectUrisList = dcrRequest.RedirectUris?.Split(delimiters).ToList(); - claims.Add(new Claim("redirect_uris", JsonConvert.SerializeObject(redirectUrisList), JsonClaimValueTypes.JsonArray)); - var jwt = new JwtSecurityToken( issuer: dcrRequest.SoftwareProductId, audience: dcrRequest.Audience, @@ -525,20 +392,18 @@ private static (string, string) PopulateDCRRequestJwt(DcrRequest dcrRequest) signingCredentials: new X509SigningCredentials(dcrRequest.SignCertificate, SecurityAlgorithms.RsaSsaPssSha256)); var tokenHandler = new JwtSecurityTokenHandler(); - return (errorMessage, tokenHandler.WriteToken(jwt)); + return (errorMessage, tokenHandler.WriteToken(jwt)); } /// /// DCR /// - private static async Task Register( - string dcrEndpoint, - X509Certificate2 clientCertificate, - string payload, - bool ignoreServerCertificateErrors = false) + private async Task Register( + string dcrEndpoint, + string payload) { // Setup the http client. - var client = GetHttpClient(clientCertificate, ignoreServerCertificateErrors: ignoreServerCertificateErrors); + var client = GetHttpClient(); // Create the post content. var content = new StringContent(payload); @@ -557,70 +422,53 @@ private static async Task Register( }; } - private static HttpClient GetHttpClient( - X509Certificate2 clientCertificate = null, - string accessToken = null, - string version = null, - bool ignoreServerCertificateErrors = false) + private HttpClient GetHttpClient(string accessToken = null, string version = null) { - var clientHandler = new HttpClientHandler(); - - // Set the client certificate for the connection if supplied. - if (clientCertificate != null) - { - clientHandler.ClientCertificates.Add(clientCertificate); - } - - if (ignoreServerCertificateErrors) - { - clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; - } - - var client = new HttpClient(clientHandler); - + var httpClient = _httpClientFactory.CreateClient(DcrConstants.DcrHttpClientName); + // If an access token has been provided then add to the Authorization header of the client. if (!string.IsNullOrEmpty(accessToken)) - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); // Add the x-v header to the request if provided. if (!string.IsNullOrEmpty(version)) - client.DefaultRequestHeaders.Add("x-v", version); + httpClient.DefaultRequestHeaders.Add("x-v", version); - return client; + return httpClient; } /// /// Insert the Message into the Queue /// - private static async Task AddDeadLetterQueMsgAsync(ILogger log, string dbConnString, string qConnString, string dhBrandId, CloudQueueMessage myQueueItem, string qName) + private async Task AddDeadLetterQueMsgAsync( DcrQueueMessage myQueueItem, string qName, FunctionContext context) { QueueClientOptions options = new() { MessageEncoding = QueueMessageEncoding.Base64 }; - QueueClient qClient = new(qConnString, qName, options); + QueueClient qClient = new(_options.StorageConnectionString, qName, options); await qClient.CreateIfNotExistsAsync(); DeadLetterQueueMessage qMsg = new() { MessageVersion = "1.0", - MessageSource = "dynamicclientregistration", - SourceMessageId = myQueueItem.Id, - SourceMessageInsertionTime = myQueueItem.InsertionTime.ToString(), - DataHolderBrandId = dhBrandId + MessageSource = _options.QueueName, + SourceMessageId = context.BindingContext.BindingData["Id"].ToString(), + SourceMessageInsertionTime = context.BindingContext.BindingData["InsertionTime"].ToString(), + DataHolderBrandId = myQueueItem.DataHolderBrandId }; string qMessage = JsonConvert.SerializeObject(qMsg); await qClient.SendMessageAsync(qMessage); - int qCount = await GetQueueCountAsync(qConnString, qName); + int qCount = await GetQueueCountAsync(_options.StorageConnectionString, qName); DcrMessage dcrMsg = new() { - MessageId = myQueueItem.Id, + MessageId = context.BindingContext.BindingData["Id"].ToString(), MessageState = Message.Abandoned.ToString(), MessageError = $"DCR - {qCount} items queued, this DataHolderBrandId is malformed" }; - await new SqlDataAccess(dbConnString).UpdateDcrMsgByMessageId(dcrMsg); - await InsertLog(log, dbConnString, $"DCR - {qCount} items in {qName} queue", "Error", "DCR"); + await new SqlDataAccess(_options.DataRecipient_DB_ConnectionString).UpdateDcrMsgByMessageId(dcrMsg); + await InsertLog(_options.DataRecipient_DB_ConnectionString, $"DCR - {qCount} items in {qName} queue", "Error", "DCR"); } /// @@ -640,11 +488,11 @@ private static async Task GetQueueCountAsync(string qConnString, string qNa /// /// Update the Log table /// - private static async Task InsertLog(ILogger log, string dbConnString, string msg, string lvl, string methodName, Exception exMsg = null) + private async Task InsertLog( string DataRecipient_DB_ConnectionString, string msg, string lvl, string methodName, Exception exMsg = null) { - log.LogInformation($"{methodName} - {msg}"); + _logger.LogInformation("{methodName} - {message}", methodName, msg); - string exMessage = ""; + string exMessage = string.Empty; if (exMsg != null) { @@ -654,7 +502,7 @@ private static async Task InsertLog(ILogger log, string dbConnString, string msg do { - // skip the first inner exeception message as it is the same as the exception message + // skip the first inner exception message as it is the same as the exception message if (ctr > 0) { innerMsg.Append(string.IsNullOrEmpty(innerException.Message) ? string.Empty : innerException.Message); @@ -680,26 +528,24 @@ private static async Task InsertLog(ILogger log, string dbConnString, string msg exMessage = exMessage.Replace("'", ""); } - using (SqlConnection db = new(dbConnString)) - { - db.Open(); - var cmdText = ""; + using SqlConnection db = new(DataRecipient_DB_ConnectionString); + db.Open(); + var cmdText = string.Empty; - if (string.IsNullOrEmpty(exMessage)) - cmdText = $"INSERT INTO [LogEventsDcrService] ([Message], [Level], [TimeStamp], [ProcessName], [MethodName], [SourceContext]) VALUES (@msg,@lvl,GETUTCDATE(),@procName,@methodName,@srcContext)"; - else - cmdText = $"INSERT INTO [LogEventsDcrService] ([Message], [Level], [TimeStamp], [Exception], [ProcessName], [MethodName], [SourceContext]) VALUES (@msg,@lvl,GETUTCDATE(), @exMessage,@procName,@methodName,@srcContext)"; - - using var cmd = new SqlCommand(cmdText, db); - cmd.Parameters.AddWithValue("@msg", msg); - cmd.Parameters.AddWithValue("@lvl", lvl); - cmd.Parameters.AddWithValue("@exMessage", exMessage); - cmd.Parameters.AddWithValue("@procName", "Azure Function"); - cmd.Parameters.AddWithValue("@methodName", methodName); - cmd.Parameters.AddWithValue("@srcContext", "CDR.DCR"); - await cmd.ExecuteNonQueryAsync(); - db.Close(); - } + if (string.IsNullOrEmpty(exMessage)) + cmdText = $"INSERT INTO [LogEventsDcrService] ([Message], [Level], [TimeStamp], [ProcessName], [MethodName], [SourceContext]) VALUES (@msg,@lvl,GETUTCDATE(),@procName,@methodName,@srcContext)"; + else + cmdText = $"INSERT INTO [LogEventsDcrService] ([Message], [Level], [TimeStamp], [Exception], [ProcessName], [MethodName], [SourceContext]) VALUES (@msg,@lvl,GETUTCDATE(), @exMessage,@procName,@methodName,@srcContext)"; + + using var cmd = new SqlCommand(cmdText, db); + cmd.Parameters.AddWithValue("@msg", msg); + cmd.Parameters.AddWithValue("@lvl", lvl); + cmd.Parameters.AddWithValue("@exMessage", exMessage); + cmd.Parameters.AddWithValue("@procName", "Azure Function"); + cmd.Parameters.AddWithValue("@methodName", methodName); + cmd.Parameters.AddWithValue("@srcContext", "CDR.DCR"); + await cmd.ExecuteNonQueryAsync(); + db.Close(); } } } \ No newline at end of file diff --git a/Source/CDR.DCR/DCRConstants.cs b/Source/CDR.DCR/DCRConstants.cs new file mode 100644 index 0000000..768e348 --- /dev/null +++ b/Source/CDR.DCR/DCRConstants.cs @@ -0,0 +1,7 @@ +namespace CDR.DCR +{ + public static class DcrConstants + { + public const string DcrHttpClientName = "DCRHttpClient"; + } +} diff --git a/Source/CDR.DCR/DCRHttpClientHandler.cs b/Source/CDR.DCR/DCRHttpClientHandler.cs new file mode 100644 index 0000000..39118a3 --- /dev/null +++ b/Source/CDR.DCR/DCRHttpClientHandler.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; + +namespace CDR.DCR +{ + public class DcrHttpClientHandler : HttpClientHandler + { + public DcrHttpClientHandler( + IOptions options, + ILogger logger) + { + var dcrOptions = options.Value; + logger.LogInformation("Loading the client certificate..."); + + byte[] clientCertBytes = Convert.FromBase64String(dcrOptions.Client_Certificate); + X509Certificate2 clientCertificate = new(clientCertBytes, dcrOptions.Client_Certificate_Password, X509KeyStorageFlags.MachineKeySet); + logger.LogInformation("Client certificate loaded: {thumbprint}", clientCertificate.Thumbprint); + + + ClientCertificates.Add(clientCertificate); + + if (dcrOptions.Ignore_Server_Certificate_Errors) + { + ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + } + } + } +} \ No newline at end of file diff --git a/Source/CDR.DCR/DCROptions.cs b/Source/CDR.DCR/DCROptions.cs new file mode 100644 index 0000000..c72e480 --- /dev/null +++ b/Source/CDR.DCR/DCROptions.cs @@ -0,0 +1,26 @@ +namespace CDR.DCR +{ + public class DcrOptions + { + public string AzureWebJobsStorage { get; set; } + public string StorageConnectionString { get; set; } + public string FUNCTIONS_WORKER_RUNTIME { get; set; } + public string DataRecipient_DB_ConnectionString { get; set; } + public string DataRecipient_Logging_DB_ConnectionString { get; set; } + public string Register_Token_Endpoint { get; set; } + public string Register_Get_SSA_Endpoint { get; set; } + public string Register_Get_SSA_XV { get; set; } + public string Brand_Id { get; set; } + public string Software_Product_Id { get; set; } + public string Redirect_Uris { get; set; } + public string Client_Certificate { get; set; } + public string Client_Certificate_Password { get; set; } + public string Signing_Certificate { get; set; } + public string Signing_Certificate_Password { get; set; } + public int Retries { get; set; } + public bool Ignore_Server_Certificate_Errors { get; set; } + public string QueueName { get; set; } + public string DeadLetterQueueName { get; set; } + + } +} diff --git a/Source/CDR.DCR/DCRQueueMessage.cs b/Source/CDR.DCR/DCRQueueMessage.cs index d3f3963..e29f7da 100644 --- a/Source/CDR.DCR/DCRQueueMessage.cs +++ b/Source/CDR.DCR/DCRQueueMessage.cs @@ -1,4 +1,6 @@ -using Newtonsoft.Json; +using Azure.Storage.Queues.Models; +using Newtonsoft.Json; +using System; namespace CDR.DCR { diff --git a/Source/CDR.DCR/Extensions/RequestExtensions.cs b/Source/CDR.DCR/Extensions/RequestExtensions.cs new file mode 100644 index 0000000..135b58c --- /dev/null +++ b/Source/CDR.DCR/Extensions/RequestExtensions.cs @@ -0,0 +1,117 @@ +using CDR.DCR.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; + +namespace CDR.DCR.Extensions +{ + public static class RequestExtensions + { + public static (List, string) CreateClaimsForDCRRequest(this DcrRequest dcrRequest) + { + string errorMessage = string.Empty; + + // Error - Unable to perform DCR as there are no mutually supported values in the mandatory claim [CLAIM_NAME] + const string ErrorMessage = "Unable to perform DCR as there are no mutually supported values in the mandatory claim"; + + var claims = new List + { + new("jti", Guid.NewGuid().ToString()), + new("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer), + new("token_endpoint_auth_signing_alg", "PS256"), + new("token_endpoint_auth_method", "private_key_jwt"), + new("application_type", "web"), + new("id_token_signed_response_alg", "PS256"), + new("id_token_encrypted_response_alg", "RSA-OAEP"), + new("id_token_encrypted_response_enc", "A256GCM"), + new("request_object_signing_alg", "PS256"), + new("software_statement", dcrRequest.Ssa ?? ""), + new("grant_types", "client_credentials"), + new("grant_types", "authorization_code"), + new("grant_types", "refresh_token") + }; + + // response_types updated below "code, code id_token" both types are returned and added below + // A response type is mandatory + if (!dcrRequest.ResponseTypesSupported.Contains("code") && !dcrRequest.ResponseTypesSupported.Contains("code id_token")) + { + // Return the error + errorMessage = ErrorMessage + " response_types"; + return (null, errorMessage); + } + + var responseTypesList = dcrRequest.ResponseTypesSupported.Where(x => x.ToLower().Equals("code") || x.ToLower().Equals("code id_token")).ToList(); + claims.Add(new Claim("response_types", JsonConvert.SerializeObject(responseTypesList), JsonClaimValueTypes.JsonArray)); + + + var isCodeFlow = dcrRequest.ResponseTypesSupported.Contains("code"); + if (isCodeFlow && dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Length==0) + { + // Log error message to the mandatory claim missing + errorMessage = ErrorMessage + " authorization_signed_response_alg"; + return (null, errorMessage); + } + + // Mandatory for code flow + if (isCodeFlow) + { + if (!dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Contains("PS256") && !dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Contains("ES256")) + { + // Return the error + errorMessage = ErrorMessage + " authorization_signed_response_alg"; + return (null, errorMessage); + } + + if (dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Contains("PS256")) + { + claims.Add(new Claim("authorization_signed_response_alg", "PS256")); + } + else if (dcrRequest.AuthorizationSigningResponseAlgValuesSupported.Contains("ES256")) + { + claims.Add(new Claim("authorization_signed_response_alg", "ES256")); + } + } + + // Check if the enc is empty but a alg is specified. + if ((dcrRequest.AuthorizationEncryptionResponseEncValuesSupported == null || dcrRequest.AuthorizationEncryptionResponseEncValuesSupported.Length==0) // No enc specified + && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported != null && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Contains("RSA-OAEP-256") + && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Contains("RSA-OAEP")) // but alg specified. + { + errorMessage = ErrorMessage + " authorization_encrypted_response_enc"; + return (null, errorMessage); + } + + + if (dcrRequest.AuthorizationEncryptionResponseEncValuesSupported != null && dcrRequest.AuthorizationEncryptionResponseEncValuesSupported.Contains("A128CBC-HS256")) + { + claims.Add(new Claim("authorization_encrypted_response_enc", "A128CBC-HS256")); + } + else if (dcrRequest.AuthorizationEncryptionResponseEncValuesSupported != null && dcrRequest.AuthorizationEncryptionResponseEncValuesSupported.Contains("A256GCM")) + { + claims.Add(new Claim("authorization_encrypted_response_enc", "A256GCM")); + } + + // Conditional: Optional for response_type "code" if authorization_encryption_enc_values_supported is present + if (isCodeFlow && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported != null && dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Length!=0) + { + if (dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Contains("RSA-OAEP-256")) + { + claims.Add(new Claim("authorization_encrypted_response_alg", "RSA-OAEP-256")); + } + else if (dcrRequest.AuthorizationEncryptionResponseAlgValuesSupported.Contains("RSA-OAEP")) + { + claims.Add(new Claim("authorization_encrypted_response_alg", "RSA-OAEP")); + } + } + + char[] delimiters = [',', ' ']; + var redirectUrisList = dcrRequest.RedirectUris?.Split(delimiters).ToList(); + claims.Add(new Claim("redirect_uris", JsonConvert.SerializeObject(redirectUrisList), JsonClaimValueTypes.JsonArray)); + + return (claims, errorMessage); + } + } +} diff --git a/Source/CDR.DCR/Models/SoftwareStatementAssertion.cs b/Source/CDR.DCR/Models/SoftwareStatementAssertion.cs deleted file mode 100644 index 992073c..0000000 --- a/Source/CDR.DCR/Models/SoftwareStatementAssertion.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Security.Cryptography.X509Certificates; - -namespace CDR.DCR.Models -{ - public class SoftwareStatementAssertion - { - public string SsaEndpoint { get; set; } - public string Version { get; set; } - public string AccessToken { get; set; } - public X509Certificate2 ClientCertificate { get; set; } - public string BrandId { get; set; } - public string SoftwareProductId { get; set; } - public ILogger Log { get; set; } - public bool IgnoreServerCertificateErrors { get; set; } = false; - } -} diff --git a/Source/CDR.DCR/Program.cs b/Source/CDR.DCR/Program.cs new file mode 100644 index 0000000..5048ec6 --- /dev/null +++ b/Source/CDR.DCR/Program.cs @@ -0,0 +1,45 @@ +using CDR.DCR; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.IO; +using System.Reflection; + +var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .ConfigureAppConfiguration((context, builder) => + { + builder + .SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) + //while running on local machine via vs studio these settings used + .AddJsonFile("local.settings.json", true, true) + //while running docker these are config values used + .AddJsonFile("appsettings.docker.json", true, true) + .AddEnvironmentVariables() + .AddCommandLine(Environment.GetCommandLineArgs()) + .Build(); + + if (context.HostingEnvironment.IsDevelopment() && !string.IsNullOrEmpty(context.HostingEnvironment.ApplicationName)) + { + Console.WriteLine("Development environment"); + builder.AddUserSecrets(Assembly.GetExecutingAssembly(), true); + } + }) + .ConfigureServices(services => + { + services.AddOptions() + .Configure((settings, configuration) => + { + configuration.Bind(settings); + }); + + services.AddTransient(); + services.AddHttpClient(DcrConstants.DcrHttpClientName, (provider, client) => + { + + }).ConfigurePrimaryHttpMessageHandler(); + }) + .Build(); + +host.Run(); diff --git a/Source/CDR.DCR/appsettings.docker.json b/Source/CDR.DCR/appsettings.docker.json new file mode 100644 index 0000000..97dc880 --- /dev/null +++ b/Source/CDR.DCR/appsettings.docker.json @@ -0,0 +1,20 @@ +{ + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "StorageConnectionString": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + "DataRecipient_DB_ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdr;Integrated Security=true", + "DataRecipient_Logging_DB_ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdr;Integrated Security=true", + "Register_Token_Endpoint": "https://localhost:7001/idp/connect/token", + "Register_Get_SSA_Endpoint": "https://localhost:7001/cdr-register/v1/all/data-recipients/brands/", + "Register_Get_SSA_XV": "3", + "Brand_Id": "ffb1c8ba-279e-44d8-96f0-1bc34a6b436f", + "Software_Product_Id": "c6327f87-687a-4369-99a4-eaacd3bb8210", + "Redirect_Uris": "https://localhost:9001/consent/callback", + "Client_Certificate": "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", + "Client_Certificate_Password": "#M0ckDataRecipient#", + "Signing_Certificate": "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", + "Signing_Certificate_Password": "#M0ckDataRecipient#", + "Retries": 3, + "Ignore_Server_Certificate_Errors": "true", + "QueueName": "dynamicclientregistration" +} diff --git a/Source/CDR.DCR/azure.settings.json b/Source/CDR.DCR/azure.settings.json index be487e0..1bd9e85 100644 --- a/Source/CDR.DCR/azure.settings.json +++ b/Source/CDR.DCR/azure.settings.json @@ -1,6 +1,6 @@ { "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "Schedule": "*/5 * * * *", "Brand_Id": "ffb1c8ba-279e-44d8-96f0-1bc34a6b436f", "Software_Product_Id": "c6327f87-687a-4369-99a4-eaacd3bb8210", diff --git a/Source/CDR.DCR/local.settings.json b/Source/CDR.DCR/local.settings.json index 6bf7cd8..fa822b2 100644 --- a/Source/CDR.DCR/local.settings.json +++ b/Source/CDR.DCR/local.settings.json @@ -3,7 +3,7 @@ "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "StorageConnectionString": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "DataRecipient_DB_ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdr;Integrated Security=true", "DataRecipient_Logging_DB_ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-mdr;Integrated Security=true", "Schedule": "0-59 * * * *", @@ -18,7 +18,9 @@ "Signing_Certificate": "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", "Signing_Certificate_Password": "#M0ckDataRecipient#", "Retries": 3, - "Ignore_Server_Certificate_Errors": "false" + "Ignore_Server_Certificate_Errors": "true", + "QueueName": "dynamicclientregistration", + "DeadLetterQueueName": "deadletter" }, "Host": { "LocalHttpPort": 7072, diff --git a/Source/CDR.DataRecipient.API.Logger/CDR.DataRecipient.API.Logger.csproj b/Source/CDR.DataRecipient.API.Logger/CDR.DataRecipient.API.Logger.csproj index 2d62a1a..fc693ab 100644 --- a/Source/CDR.DataRecipient.API.Logger/CDR.DataRecipient.API.Logger.csproj +++ b/Source/CDR.DataRecipient.API.Logger/CDR.DataRecipient.API.Logger.csproj @@ -1,20 +1,18 @@  - - net6.0 enable enable - 1.2.5 - 1.2.5 - 1.2.5 + $(TargetFrameworkVersion) + $(Version) + $(Version) + $(Version) - - - - - - + + + + + + - - + \ No newline at end of file diff --git a/Source/CDR.DataRecipient.API.Logger/RequestResponseLoggingMiddleware.cs b/Source/CDR.DataRecipient.API.Logger/RequestResponseLoggingMiddleware.cs index d980c8e..6a9e8e2 100644 --- a/Source/CDR.DataRecipient.API.Logger/RequestResponseLoggingMiddleware.cs +++ b/Source/CDR.DataRecipient.API.Logger/RequestResponseLoggingMiddleware.cs @@ -38,7 +38,6 @@ public class RequestResponseLoggingMiddleware private string? _fapiInteractionId; private string? _dataHolderBrandId; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; readonly RequestDelegate _next; private readonly ILogger _requestResponseLogger; @@ -95,7 +94,6 @@ private void LogWithContext() } } - private async Task ExtractRequestProperties(HttpContext context) { try @@ -159,7 +157,7 @@ private void ExtractIdFromRequest(HttpRequest request) _dataHolderBrandId = String.Empty; SetIdFromJwt(parameter, ClaimIdentifiers.Iss, ref _dataHolderBrandId); } - } + } } catch (Exception ex) { @@ -234,7 +232,7 @@ private async Task ExtractResponseProperties(HttpContext httpContext) await responseBody.CopyToAsync(originalBodyStream); } } - private string GetHost(HttpRequest request) + private string? GetHost(HttpRequest request) { // 1. check if the X-Forwarded-Host header has been provided -> use that // 2. If not, use the request.Host @@ -260,7 +258,7 @@ private string GetHost(HttpRequest request) // The Client IP address may contain a comma separated list of ip addresses based on the network devices // the traffic traverses through. We get the first (and potentially only) ip address from the list as the client IP. // We also remove any port numbers that may be included on the client IP. - return keys[0] + return keys[0]? .Split(',')[0] // Get the first IP address in the list, in case there are multiple. .Split(':')[0]; // Strip off the port number, in case it is attached to the IP address. } diff --git a/Source/CDR.DataRecipient.E2ETests/BaseTest.cs b/Source/CDR.DataRecipient.E2ETests/BaseTest.cs index 8b5fb34..e3de8fd 100644 --- a/Source/CDR.DataRecipient.E2ETests/BaseTest.cs +++ b/Source/CDR.DataRecipient.E2ETests/BaseTest.cs @@ -8,6 +8,7 @@ using System; using System.IO; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using CDR.DataRecipient.E2ETests.Pages; using FluentAssertions; @@ -15,7 +16,6 @@ using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Microsoft.Playwright; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -42,11 +42,13 @@ public class BaseTest // Data Holder public const string DH_BRANDID = "804fc2fb-18a7-4235-9a49-2af393d18bc7"; - public const string DH_BRANDID_ENERGY = "cfcaf0df-401b-47f2-98af-94787289eca8"; // Mock Data Holder (Energy) + public const string DH_BRANDID_ENERGY = "cfcaf0df-401b-47f2-98af-94787289eca8"; // Mock Data Holder (Energy) + public const string DH_BRANDID_DUMMY_DH = "e748eadf-4aa4-4e2f-b3da-fb4a9d511994"; // Use "Bank Brand 2" Dummy Data Holder for negative testing // Data Recipient public const string DR_BRANDID = "ffb1c8ba-279e-44d8-96f0-1bc34a6b436f"; public const string DR_SOFTWAREPRODUCTID = "c6327f87-687a-4369-99a4-eaacd3bb8210"; + public const string DR_DEFAULT_SCOPES = "openid profile common:customer.basic:read common:customer.detail:read bank:accounts.basic:read bank:accounts.detail:read bank:transactions:read bank:regular_payments:read bank:payees:read energy:accounts.basic:read energy:accounts.detail:read energy:accounts.concessions:read energy:accounts.paymentschedule:read energy:billing:read energy:electricity.servicepoints.basic:read energy:electricity.servicepoints.detail:read energy:electricity.der:read energy:electricity.usage:read cdr:registration"; // URLs static public string REGISTER_MTLS_BaseURL => Configuration["MTLS_BaseURL"] @@ -160,7 +162,7 @@ static void DeleteFile(string filename) // Setup browser await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { - SlowMo = 250, + SlowMo = 0, #if TEST_DEBUG_MODE Headless = false, Timeout = 5000 // DEBUG - 5 seconds @@ -239,7 +241,7 @@ protected async Task CleanupAsync(CleanupDelegate cleanup) // Setup browser await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { - SlowMo = 250, + SlowMo = 0, #if TEST_DEBUG_MODE Headless = false, Timeout = 5000 // DEBUG - 5 seconds @@ -261,9 +263,7 @@ protected async Task CleanupAsync(CleanupDelegate cleanup) try { var page = await context.NewPageAsync(); - page.Close += async (_, page) => - { - }; + page.Close += (_, page) => { }; using (new AssertionScope()) { @@ -310,7 +310,7 @@ static void DeleteFile(string filename) // Setup browser await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { - SlowMo = 250, + SlowMo = 0, #if TEST_DEBUG_MODE Headless = false, Timeout = 5000 // DEBUG - 5 seconds @@ -564,7 +564,7 @@ static protected async Task DataHolders_Discover(IPage page, string industry = " // Arrange - Set industry if (String.IsNullOrEmpty(industry)) // Clear industry { - await page.Locator("select[name=\"Industry\"]").SelectOptionAsync(new SelectOptionValue[] { }); + await page.Locator("select[name=\"Industry\"]").SelectOptionAsync(Array.Empty()); } else { @@ -582,6 +582,9 @@ static protected async Task DataHolders_Discover(IPage page, string industry = " // Arrange - Set version await page.Locator("input[name=\"Version\"]").FillAsync(version); + // Workaround issue where Refresh is clicked before form has loaded. + Thread.Sleep(300); + // Act - Click Refresh button await page.Locator(@"h5:has-text(""Refresh Data Holders"") ~ div.card-body >> input:has-text(""Refresh"")").ClickAsync(); @@ -636,52 +639,51 @@ static protected async Task SSA_Get(IPage page, string industry, string version, // Create Client Registration returning DH client ID of client that was registered static protected async Task ClientRegistration_Create(IPage page, - string dhBrandId, - string drBrandId, - string drSoftwareProductId, + string dhBrandId, string? jarmSigningAlgo = null, - string responseTypes = "code id_token", + string responseTypes = "code,code id_token", string? jarmEncrypAlg = null, string? jarmEncryptEnc = null) { - // Arrange - Goto home page, click menu button, check page loaded - await page.GotoAsync(WEB_URL); - await page.Locator("a >> text=Dynamic Client Registration").ClickAsync(); - await page.Locator("h2 >> text=Dynamic Client Registration").TextContentAsync(); - // Set data holder brand id - await page.Locator("select[name=\"DataHolderBrandId\"]").SelectOptionAsync(new[] { dhBrandId }); + DynamicClientRegistrationPage dcrPage = new DynamicClientRegistrationPage(page, WEB_URL); - // Assert - Check software product id - (await page.Locator("input[name=\"SoftwareProductId\"]").InputValueAsync()).Should().Be(drSoftwareProductId); + await dcrPage.GotoDynamicClientRegistrationPage(); - await page.Locator("input[name=\"ResponseTypes\"]").FillAsync(responseTypes); + await dcrPage.SelectDataHolderBrandId(dhBrandId); + await dcrPage.EnterResponseTypes(responseTypes); if (jarmSigningAlgo != null) { - await page.Locator("input[name=\"AuthorizationSignedResponseAlg\"]").FillAsync(jarmSigningAlgo); + await dcrPage.EnterAuthorisedSignedResponsegAlgo(jarmSigningAlgo); } if (jarmEncrypAlg != null) { - await page.Locator("input[name=\"AuthorizationEncryptedResponseAlg\"]").FillAsync(jarmEncrypAlg); + await dcrPage.EnterAuthorisedEncryptedResponseAlgo(jarmEncrypAlg); } if (jarmEncryptEnc != null) { - await page.Locator("input[name=\"AuthorizationEncryptedResponseEnc\"]").FillAsync(jarmEncryptEnc); + await dcrPage.EnterAuthorisedEncryptedResponseEnc(jarmEncryptEnc); + } + + if (responseTypes.Contains("id_token")) + { + await dcrPage.EnterIdTokenEncryptedResponseAlgo("RSA-OAEP"); + await dcrPage.EnterIdTokenEncryptedResponseEnc("A128CBC-HS256"); } - // Act - Click create button - await page.Locator(@"h5:has-text(""Create Client Registration"") ~ div.card-body >> input:has-text(""Register"")").ClickAsync(); + await dcrPage.ClickRegister(); - // Assert - Check client was registered - await page.Locator(@"h5:has-text(""Create Client Registration"") ~ div.card-footer:has-text(""Created - Registered"")").TextContentAsync(); + var registrationResponseJson = await dcrPage.GetRegistrationResponse(); + return GetClientIdFromRegistrationResponse(registrationResponseJson); - // Assert - Get json result - var json = await page.Locator(@"h5:has-text(""Create Client Registration"") ~ div.card-footer >> pre").InnerTextAsync(); + } + static protected string GetClientIdFromRegistrationResponse(string registrationResponseJson) + { // Deserialise response and return DH client id - DCRResponse dcrResponse = JsonConvert.DeserializeObject(json) ?? throw new NullReferenceException(nameof(json)); - return dcrResponse.ClientId ?? throw new NullReferenceException(nameof(dcrResponse.ClientId)); + DCRResponse dcrResponse = JsonConvert.DeserializeObject(registrationResponseJson) ?? throw new NullReferenceException(nameof(registrationResponseJson)); + return dcrResponse.ClientId ?? throw new NullReferenceException($"{nameof(dcrResponse.ClientId)} could not be found in {nameof(registrationResponseJson)} - {registrationResponseJson}"); } static protected async Task ConsentAndAuthorisation2(IPage page, string customerId = CUSTOMERID_BANKING, string customerAccounts = CUSTOMERACCOUNTS_BANKING) diff --git a/Source/CDR.DataRecipient.E2ETests/CDR.DataRecipient.E2ETests.csproj b/Source/CDR.DataRecipient.E2ETests/CDR.DataRecipient.E2ETests.csproj index 87bef6c..1327b1b 100644 --- a/Source/CDR.DataRecipient.E2ETests/CDR.DataRecipient.E2ETests.csproj +++ b/Source/CDR.DataRecipient.E2ETests/CDR.DataRecipient.E2ETests.csproj @@ -1,17 +1,14 @@ - - + - net6.0 false - 1.2.5 - 1.2.5 - 1.2.5 + $(TargetFrameworkVersion) + $(Version) + $(Version) + $(Version) - - Always @@ -22,32 +19,30 @@ Always - - + - - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - - - Always - - - + + Always + + + \ No newline at end of file diff --git a/Source/CDR.DataRecipient.E2ETests/Fixtures/TestFixture.cs b/Source/CDR.DataRecipient.E2ETests/Fixtures/TestFixture.cs index d289b17..ca87e98 100644 --- a/Source/CDR.DataRecipient.E2ETests/Fixtures/TestFixture.cs +++ b/Source/CDR.DataRecipient.E2ETests/Fixtures/TestFixture.cs @@ -1,5 +1,3 @@ -using Microsoft.Data.SqlClient; -using System; using System.Threading.Tasks; using Xunit; @@ -9,14 +7,17 @@ namespace CDR.DataRecipient.E2ETests { public class TestFixture : IAsyncLifetime { + private static readonly string[] installArguments = ["install"]; + private static readonly string[] installDepsArguments = ["install-deeps"]; + public Task InitializeAsync() { // Only install Playwright if not running in container, since Dockerfile.e2e-tests already installed Playwright if (!BaseTest.RUNNING_IN_CONTAINER) { // Ensure that Playwright has been fully installed. - Microsoft.Playwright.Program.Main(new string[] { "install" }); - Microsoft.Playwright.Program.Main(new string[] { "install-deps" }); + Microsoft.Playwright.Program.Main(installArguments); + Microsoft.Playwright.Program.Main(installDepsArguments); } return Task.CompletedTask; diff --git a/Source/CDR.DataRecipient.E2ETests/Infrastructure/AccessToken.cs b/Source/CDR.DataRecipient.E2ETests/Infrastructure/AccessToken.cs index 72efdb8..efae57a 100644 --- a/Source/CDR.DataRecipient.E2ETests/Infrastructure/AccessToken.cs +++ b/Source/CDR.DataRecipient.E2ETests/Infrastructure/AccessToken.cs @@ -16,7 +16,7 @@ public class AccessToken { private static readonly string IDENTITYSERVER_URL = BaseTest.REGISTER_IDENTITYSERVER_URL; private static readonly string AUDIENCE = IDENTITYSERVER_URL; - private const string SCOPE = "cdr-register:bank:read"; + private const string SCOPE = "cdr-register:read"; private const string GRANT_TYPE = "client_credentials"; private const string CLIENT_ID = "c6327f87-687a-4369-99a4-eaacd3bb8210"; private const string CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; diff --git a/Source/CDR.DataRecipient.E2ETests/Pages/ConsentAndAuthorisationPages.cs b/Source/CDR.DataRecipient.E2ETests/Pages/ConsentAndAuthorisationPages.cs index d0ca394..c6745bb 100644 --- a/Source/CDR.DataRecipient.E2ETests/Pages/ConsentAndAuthorisationPages.cs +++ b/Source/CDR.DataRecipient.E2ETests/Pages/ConsentAndAuthorisationPages.cs @@ -19,9 +19,9 @@ internal class ConsentAndAuthorisationPages public ConsentAndAuthorisationPages(IPage page) { _page = page; - _txtCustomerId = _page.Locator("id=mui-1"); + _txtCustomerId = _page.Locator("id=customerId"); _btnContinue = _page.Locator("button:has-text(\"Continue\")"); - _txtOneTimePassword = _page.Locator("id=mui-2"); + _txtOneTimePassword = _page.Locator("id=otp"); _btnAuthorise = _page.Locator("text=Authorise"); _btnCancel = _page.Locator("text=Cancel"); diff --git a/Source/CDR.DataRecipient.E2ETests/Pages/DynamicClientRegistrationPage.cs b/Source/CDR.DataRecipient.E2ETests/Pages/DynamicClientRegistrationPage.cs new file mode 100644 index 0000000..ea9b03e --- /dev/null +++ b/Source/CDR.DataRecipient.E2ETests/Pages/DynamicClientRegistrationPage.cs @@ -0,0 +1,285 @@ +using Microsoft.Playwright; +using System.Threading.Tasks; + +namespace CDR.DataRecipient.E2ETests.Pages +{ + + internal class DynamicClientRegistrationPage + { + private IPage _page; + private readonly string _dataRecipientBaseUrl; + private readonly ILocator _lnkDcrMenuItem; + private readonly ILocator _hedPageHeading; + + private readonly ILocator _selSelectBrand; + private readonly ILocator _txtClientId; + private readonly ILocator _txtSsaVersion; + private readonly ILocator _selIndustry; + private readonly ILocator _selIndustrySelected; + private readonly ILocator _txtSoftwareProductId; + private readonly ILocator _txtRedirectUris; + private readonly ILocator _txtScope; + private readonly ILocator _txtTokenSigningAlgo; + private readonly ILocator _txtTokenAuthMethod; + private readonly ILocator _txtGrantTypes; + private readonly ILocator _txtResponseTypes; + private readonly ILocator _txtApplicationType; + + private readonly ILocator _txtIdTokenSignedResponseAlgo; + private readonly ILocator _txtIdTokenEncryptedResponseAlgo; + private readonly ILocator _txtIdTokenEncryptedResponseEnc; + + private readonly ILocator _txtRequestSigningAlgo; + private readonly ILocator _txtAuthorisedSignedResponsegAlgo; + private readonly ILocator _txtAuthorisedEncryptedResponseAlgo; + private readonly ILocator _txtAuthorisedEncryptedResponseEnc; + + private readonly ILocator _btnRegister; + private readonly ILocator _btnUpdate; + + private readonly ILocator _divRegistrationResponseWithHeading; + private readonly ILocator _divRegistrationResponseJson; + private readonly ILocator _divViewRegistrationResponse; + private ILocator _divDiscoveryDocumentDetails; + + + public DynamicClientRegistrationPage(IPage page, string dataRecipientBaseUrl) + { + _page = page; + _dataRecipientBaseUrl = dataRecipientBaseUrl; + + _lnkDcrMenuItem = _page.Locator("a >> text=Dynamic Client Registration"); + _hedPageHeading = _page.Locator("h2 >> text=Dynamic Client Registration"); + + _selSelectBrand = _page.Locator("id=DataHolderBrandId"); + _txtClientId = _page.Locator("id=ClientId"); + _txtSsaVersion = _page.Locator("id=SsaVersion"); + _selIndustry = _page.Locator("id=Industry"); + _selIndustrySelected = _page.Locator("//select[@id='Industry']/option[@selected='selected']"); + _txtSoftwareProductId = _page.Locator("id=SoftwareProductId"); + _txtRedirectUris = _page.Locator("id=RedirectUris"); + _txtScope = _page.Locator("id=Scope"); + + _txtTokenSigningAlgo = _page.Locator("id=TokenEndpointAuthSigningAlg"); + _txtTokenAuthMethod = _page.Locator("id=TokenEndpointAuthMethod"); + _txtGrantTypes = _page.Locator("id=GrantTypes"); + _txtResponseTypes = _page.Locator("id=ResponseTypes"); + _txtApplicationType = _page.Locator("id=ApplicationType"); + + _txtIdTokenSignedResponseAlgo = _page.Locator("id=IdTokenSignedResponseAlg"); + _txtIdTokenEncryptedResponseAlgo = _page.Locator("id=IdTokenEncryptedResponseAlg"); + _txtIdTokenEncryptedResponseEnc = _page.Locator("id=IdTokenEncryptedResponseEnc"); + + _txtRequestSigningAlgo = _page.Locator("id=RequestObjectSigningAlg"); + _txtAuthorisedSignedResponsegAlgo = _page.Locator("id=AuthorizationSignedResponseAlg"); + _txtAuthorisedEncryptedResponseAlgo = _page.Locator("id=AuthorizationEncryptedResponseAlg"); + _txtAuthorisedEncryptedResponseEnc = _page.Locator("id=AuthorizationEncryptedResponseEnc"); + + _btnRegister = _page.Locator("//input[@name='register' and @value='Register']"); + _btnUpdate = _page.Locator("//input[@name='register' and @value='Update']"); + + _divRegistrationResponseWithHeading = _page.Locator("//div[@id='registration']"); + _divRegistrationResponseJson = _page.Locator(@"h5:has-text(""Client Registration"") ~ div.card-footer >> pre"); + _divViewRegistrationResponse = _page.Locator("//div[@id='modal-registration' and @class='modal show']//div[@class='modal-body']"); + + } + + public async Task GotoDynamicClientRegistrationPage() + { + await _page.GotoAsync(_dataRecipientBaseUrl); + await _lnkDcrMenuItem.ClickAsync(); + await _hedPageHeading.TextContentAsync(); + } + + public async Task SelectDataHolderBrandId(string dataholderBrandId) + { + await _selSelectBrand.SelectOptionAsync(new[] { dataholderBrandId }); + } + + public async Task EnterClientId(string clientId) + { + await _txtClientId.FillAsync(clientId); + } + public async Task EnterSsaVersion(string ssaVersion) + { + await _txtSsaVersion.FillAsync(ssaVersion); + } + public async Task SelectIndustry(string industry) + { + await _selIndustry.SelectOptionAsync(new[] { industry }); + } + public async Task EnterRedirectUris(string redirectUris) + { + await _txtRedirectUris.FillAsync(redirectUris); + } + public async Task EnterScope(string scope) + { + await _txtScope.FillAsync(scope); + } + public async Task EnterTokenAuthSigningAlgo(string tokenAuthSigningAlgo) + { + await _txtTokenSigningAlgo.FillAsync(tokenAuthSigningAlgo); + } + public async Task EnterTokenAuthSigningMethod(string tokenAuthSigningMethod) + { + await _txtTokenAuthMethod.FillAsync(tokenAuthSigningMethod); + } + public async Task EnterGrantTypes(string grantTypes) + { + await _txtGrantTypes.FillAsync(grantTypes); + } + public async Task EnterResponseTypes(string responseTypes) + { + await _txtResponseTypes.FillAsync(responseTypes); + } + public async Task EnterIdTokenIdTokenSignedResponseAlgo(string idTokenIdTokenSignedResponseAlgo) + { + await _txtIdTokenSignedResponseAlgo.FillAsync(idTokenIdTokenSignedResponseAlgo); + } + public async Task EnterIdTokenEncryptedResponseAlgo(string idTokenEncryptedResponseAlgo) + { + await _txtIdTokenEncryptedResponseAlgo.FillAsync(idTokenEncryptedResponseAlgo); + } + public async Task EnterIdTokenEncryptedResponseEnc(string idTokenEncryptedResponseEnc) + { + await _txtIdTokenEncryptedResponseEnc.FillAsync(idTokenEncryptedResponseEnc); + } + public async Task EnterRequestSigningAlgo(string requestSigningAlgo) + { + await _txtRequestSigningAlgo.FillAsync(requestSigningAlgo); + } + public async Task EnterAuthorisedSignedResponsegAlgo(string authorisedSignedResponsegAlgo) + { + await _txtAuthorisedSignedResponsegAlgo.FillAsync(authorisedSignedResponsegAlgo); + } + public async Task EnterAuthorisedEncryptedResponseAlgo(string authorisedEncryptedResponseAlgo) + { + await _txtAuthorisedEncryptedResponseAlgo.FillAsync(authorisedEncryptedResponseAlgo); + } + public async Task EnterAuthorisedEncryptedResponseEnc(string authorisedEncryptedResponseEnc) + { + await _txtAuthorisedEncryptedResponseEnc.FillAsync(authorisedEncryptedResponseEnc); + } + + public async Task ClickRegister() + { + await _btnRegister.ClickAsync(); + } + public async Task ClickUpdate() + { + await _btnUpdate.ClickAsync(); + } + public async Task ClickViewRegistration(string clientId) + { + await _page.Locator($"//a[@data-id='{clientId}' and text()='View']").ClickAsync(); + } + public async Task ClickEditRegistration(string clientId) + { + await _page.Locator($"//tr[@id='{clientId}']//a[text()='Edit']").ClickAsync(); + } + public async Task GetClientId() + { + return await _txtClientId.InputValueAsync(); + } + public async Task GetSsaVersion() + { + return await _txtSsaVersion.InputValueAsync(); + } + public async Task GetIndustry() + { + return await _selIndustrySelected.TextContentAsync(); + } + public async Task GetSoftwareProductId() + { + return await _txtSoftwareProductId.InputValueAsync(); + } + public async Task GetRedirectUris() + { + return await _txtRedirectUris.InputValueAsync(); + } + public async Task GetScope() + { + return await _txtScope.InputValueAsync(); + } + public async Task GetTokenSigningAlgo() + { + return await _txtTokenSigningAlgo.InputValueAsync(); + } + public async Task GetTokenAuthMethod() + { + return await _txtTokenAuthMethod.InputValueAsync(); + } + public async Task GetGrantTypes() + { + return await _txtGrantTypes.InputValueAsync(); + } + public async Task GetResponseTypes() + { + return await _txtResponseTypes.InputValueAsync(); + } + public async Task GetApplicationType() + { + return await _txtApplicationType.InputValueAsync(); + } + public async Task GetIdTokenSignedResponseAlgo() + { + return await _txtIdTokenSignedResponseAlgo.InputValueAsync(); + } + public async Task GetIdTokenEncryptedResponseAlgo() + { + return await _txtIdTokenEncryptedResponseAlgo.InputValueAsync(); + } + public async Task GetIdTokenEncryptedResponseEnc() + { + return await _txtIdTokenEncryptedResponseEnc.InputValueAsync(); + } + public async Task GetRequestSigningAlgo() + { + return await _txtRequestSigningAlgo.InputValueAsync(); + } + public async Task GetAuthorisedSignedResponsegAlgo() + { + return await _txtAuthorisedSignedResponsegAlgo.InputValueAsync(); + } + public async Task GetAuthorisedEncryptedResponseAlgo() + { + return await _txtAuthorisedEncryptedResponseAlgo.InputValueAsync(); + } + public async Task GetAuthorisedEncryptedResponseEnc() + { + return await _txtAuthorisedEncryptedResponseEnc.InputValueAsync(); + } + public async Task GetRegistrationResponse(bool includeHeading = false) + { + if(includeHeading) + { + return await _divRegistrationResponseWithHeading.TextContentAsync(); + } + else + { + return await _divRegistrationResponseJson.TextContentAsync(); + } + + } + public async Task GetViewRegistrationResponse() + { + return await _divViewRegistrationResponse.TextContentAsync(); + } + public async Task GetDiscoveryDocumentDetails(string textToSynchroniseWith = null) + { + // Workaround to wait for text to synchonise with. + // Without synchronisation, current text content is returned instead of waiting for text (page to reload) + if (textToSynchroniseWith == null) + { + _divDiscoveryDocumentDetails = _page.Locator("#discovery-document"); + } + else + { + _divDiscoveryDocumentDetails = _page.Locator($"//div[@id='discovery-document']/div[contains(text(), '{textToSynchroniseWith}')]/.."); + } + + return await _divDiscoveryDocumentDetails.TextContentAsync(); + } + + } +} diff --git a/Source/CDR.DataRecipient.E2ETests/Pages/ParPage.cs b/Source/CDR.DataRecipient.E2ETests/Pages/ParPage.cs index e5bcb22..b091940 100644 --- a/Source/CDR.DataRecipient.E2ETests/Pages/ParPage.cs +++ b/Source/CDR.DataRecipient.E2ETests/Pages/ParPage.cs @@ -1,12 +1,6 @@ -using Microsoft.IdentityModel.Tokens; -using Microsoft.Playwright; +using Microsoft.Playwright; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; -using System.Web; -using static System.Formats.Asn1.AsnWriter; namespace CDR.DataRecipient.E2ETests.Pages { @@ -23,10 +17,13 @@ internal class ParPage private readonly ILocator _txtResponseType; private readonly ILocator _txtResponseMode; private readonly ILocator _btnInitiatePar; + private readonly ILocator _btnViewRegistration; + private readonly ILocator _divViewRegistrationError; private readonly ILocator _lnkRequestUri; private readonly ILocator _chkUsePkce; private readonly ILocator _divErrorMessage; private readonly ILocator _lblRequestUri; + private readonly ILocator _divRegistrationModal; public ParPage(IPage page) { @@ -41,10 +38,12 @@ public ParPage(IPage page) _txtResponseType = _page.Locator("input[name=\"ResponseType\"]"); _txtResponseMode = _page.Locator("input[name=\"ResponseMode\"]"); _btnInitiatePar = _page.Locator("div.form >> text=Initiate PAR"); + _btnViewRegistration = _page.Locator("#ViewRegistration"); + _divViewRegistrationError = _page.Locator("#registrationid-validation-message"); _lnkRequestUri = _page.Locator("p.results > a"); _divErrorMessage = _page.Locator(".card-footer"); _lblRequestUri = _page.Locator("dd:has-text(\"urn:\")"); - + _divRegistrationModal = _page.Locator("//div[@id='modal-registration' and @class='modal show']//div[@class='modal-body']"); } public async Task GotoPar() @@ -53,31 +52,18 @@ public async Task GotoPar() await _hedPageHeading.TextContentAsync(); } - public async Task CompleteParFormOld(string dhClientId, string dhBrandId, string cdrArrangement, string sharingDuration) - { - await _selSelectRegistration.SelectOptionAsync(new[] { $"{dhClientId}|||{dhBrandId}" }); - await _selSelectRegistration.ClickAsync(); // there is JS that runs on the click event, so simulate click here - await Task.Delay(2000); - await _selSelectArrangementId.SelectOptionAsync(new[] { cdrArrangement }); - await _txtSharingDuration.FillAsync(sharingDuration); - await _btnInitiatePar.ClickAsync(); - await _lnkRequestUri.ClickAsync(); - - } - public async Task CompleteParForm( string dhClientId, string dhBrandId, string scope = null, string cdrArrangement = null, - string responseType = "code id_token", - string responseMode = "fragment", + string responseType = "code", + string responseMode = "jwt", string sharingDuration = "", bool usePkce = true) { - await _selSelectRegistration.SelectOptionAsync(new[] { $"{dhClientId}|||{dhBrandId}" }); - await _selSelectRegistration.ClickAsync(); // there is JS that runs on the click event, so simulate click here - await Task.Delay(2000); + await SelectRegistration(dhBrandId, dhClientId); + if (cdrArrangement != null) { await _selSelectArrangementId.SelectOptionAsync(new[] { cdrArrangement }); @@ -103,11 +89,24 @@ public async Task CompleteParForm( } + public async Task SelectRegistration(string dhBrandId, string dhClientId) + { + await _selSelectRegistration.SelectOptionAsync(new[] { $"{dhClientId}|||{dhBrandId}" }); + await _selSelectRegistration.ClickAsync(); // there is JS that runs on the click event, so simulate click here + await Task.Delay(2000); + } public async Task ClickInitiatePar() { await _btnInitiatePar.ClickAsync(); } - + public async Task ClickViewRegistration() + { + await _btnViewRegistration.ClickAsync(); + } + public async Task GetViewRegistrationError() + { + return await _divViewRegistrationError.InnerTextAsync(); + } public async Task ClickAuthorizeUrl() { await _lnkRequestUri.ClickAsync(); @@ -124,16 +123,18 @@ public async Task GetErrorMessage() { return await _divErrorMessage.InnerTextAsync(); } - public async Task GetResponseType() { return await _txtResponseType.InputValueAsync(); } - public async Task GetResponseMode() { return await _txtResponseMode.InputValueAsync(); } + public async Task GetViewRegistrationResponse() + { + return await _divRegistrationModal.InnerTextAsync(); + } public async Task ErrorExists(string errorToCheckFor) { try diff --git a/Source/CDR.DataRecipient.E2ETests/US23863_MDR_E2ETests.cs b/Source/CDR.DataRecipient.E2ETests/US23863_MDR_E2ETests.cs index 1549359..e96c2c6 100644 --- a/Source/CDR.DataRecipient.E2ETests/US23863_MDR_E2ETests.cs +++ b/Source/CDR.DataRecipient.E2ETests/US23863_MDR_E2ETests.cs @@ -1,5 +1,6 @@ using CDR.DataRecipient.E2ETests.Pages; using FluentAssertions; +using FluentAssertions.Execution; using Microsoft.Data.SqlClient; using Microsoft.Playwright; using System; @@ -51,7 +52,7 @@ static string GetClientId() return clientId; } - public static async Task TestToken(IPage page, string menuText, string? expectedToken) + static async Task TestToken(IPage page, string menuText, string? expectedToken) { // Arrange - Goto home page, click menu button, check page loaded await page.GotoAsync(WEB_URL); @@ -194,7 +195,218 @@ await ArrangeAsync(testName, async (page) => await TestAsync(testName, async (page) => { - await ClientRegistration_Create(page, dhBrandId, drBrandId, drSoftwareProductId); + + // Create a default Banking Dataholder Registration using defaults + DynamicClientRegistrationPage dcrPage = new DynamicClientRegistrationPage(page, WEB_URL); + await dcrPage.GotoDynamicClientRegistrationPage(); + await dcrPage.SelectDataHolderBrandId(DH_BRANDID); + await dcrPage.ClickRegister(); + + // Assert Software Product Registered + var registrationResponse = await dcrPage.GetRegistrationResponse(includeHeading: true); + registrationResponse.Should().Contain("Created - Registered"); + + }); + } + finally + { + await CleanupAsync(async (page) => + { + try { await ClientRegistration_Delete(page); } catch { }; + }); + } + } + + [Theory] + [InlineData(DH_BRANDID, DR_BRANDID, DR_SOFTWAREPRODUCTID)] + [InlineData(DH_BRANDID_ENERGY, DR_BRANDID, DR_SOFTWAREPRODUCTID)] // Also test for Energy DH + public async Task AC04_DynamicClientRegistration_Defaults(string dhBrandId = DH_BRANDID, string drBrandId = DR_BRANDID, string drSoftwareProductId = DR_SOFTWAREPRODUCTID) + { + string testName = $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC04_DynamicClientRegistration_Defaults)} - DH_BrandId={dhBrandId} - DR_BrandId={drBrandId} - DR_SoftwareProductId={drSoftwareProductId}"; + + await ArrangeAsync(testName, async (page) => + { + await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands + await SSA_Get(page, "ALL", "3", drBrandId, drSoftwareProductId, "OK - SSA Generated"); + }); + + await TestAsync(testName, async (page) => + { + + // Select Dataholder + DynamicClientRegistrationPage dcrPage = new DynamicClientRegistrationPage(page, WEB_URL); + await dcrPage.GotoDynamicClientRegistrationPage(); + await dcrPage.SelectDataHolderBrandId(dhBrandId); + + // Assert all default values + using (new AssertionScope()) + { + dcrPage.GetClientId().Result.Should().Be(String.Empty); + dcrPage.GetSsaVersion().Result.Should().Be("3"); + dcrPage.GetIndustry().Result.Should().Be("ALL"); + dcrPage.GetSoftwareProductId().Result.Should().Be(drSoftwareProductId); + dcrPage.GetRedirectUris().Result.Should().Be($"https://{BaseTest.HOSTNAME_DATARECIPIENT}:9001/consent/callback"); + dcrPage.GetScope().Result.Should().Be(DR_DEFAULT_SCOPES); + dcrPage.GetTokenSigningAlgo().Result.Should().Be("PS256"); + dcrPage.GetGrantTypes().Result.Should().Be("client_credentials,authorization_code,refresh_token"); + dcrPage.GetResponseTypes().Result.Should().Be("code"); + dcrPage.GetApplicationType().Result.Should().Be("web"); + dcrPage.GetIdTokenSignedResponseAlgo().Result.Should().Be("PS256"); + dcrPage.GetIdTokenEncryptedResponseAlgo().Result.Should().Be(String.Empty); + dcrPage.GetIdTokenEncryptedResponseEnc().Result.Should().Be(String.Empty); + dcrPage.GetRequestSigningAlgo().Result.Should().Be("PS256"); + dcrPage.GetAuthorisedSignedResponsegAlgo().Result.Should().Be("PS256"); + dcrPage.GetAuthorisedEncryptedResponseAlgo().Result.Should().Be(String.Empty); + dcrPage.GetAuthorisedEncryptedResponseEnc().Result.Should().Be(String.Empty); + } + + }); + + } + + [Fact] + public async Task AC04_DynamicClientRegistrationViewRegistration() + { + string testName = $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC04_DynamicClientRegistrationViewRegistration)}"; + try + { + string? dhClientId = null; + await ArrangeAsync(testName, async (page) => + { + await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands + dhClientId = await ClientRegistration_Create(page, DH_BRANDID) ?? throw new NullReferenceException(nameof(dhClientId)); + }); + + await TestAsync(testName, async (page) => + { + // View newly created registration + DynamicClientRegistrationPage dcrPage = new DynamicClientRegistrationPage(page, WEB_URL); + await dcrPage.GotoDynamicClientRegistrationPage(); + await dcrPage.ClickViewRegistration(dhClientId); + + // Assert + using (new AssertionScope()) + { + string viewRegistrationResponse = await dcrPage.GetViewRegistrationResponse(); + viewRegistrationResponse.Should().Contain("Registration retrieved successfully."); + viewRegistrationResponse.Should().Contain($"\"client_id\": \"{dhClientId}\""); + } + + }); + } + finally + { + await CleanupAsync(async (page) => + { + try { await ClientRegistration_Delete(page); } catch { }; + }); + } + } + + [Fact] + public async Task AC04_DynamicClientRegistrationViewDiscoveryDocument() + { + string testName = $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC04_DynamicClientRegistrationViewDiscoveryDocument)}"; + + await ArrangeAsync(testName, async (page) => + { + await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands + }); + + await TestAsync(testName, async (page) => + { + // View discovery document + DynamicClientRegistrationPage dcrPage = new DynamicClientRegistrationPage(page, WEB_URL); + await dcrPage.GotoDynamicClientRegistrationPage(); + await dcrPage.SelectDataHolderBrandId(DH_BRANDID); + + // Assert + using (new AssertionScope()) + { + string discoveryDocumentDetails = await dcrPage.GetDiscoveryDocumentDetails("Discovery Document details loaded"); + discoveryDocumentDetails.Should().Contain($"Discovery Document details loaded from https://{HOSTNAME_DATAHOLDER}:8001/.well-known/openid-configuration"); + discoveryDocumentDetails.Should().Contain($"\"issuer\": \"https://{HOSTNAME_DATAHOLDER}:8001\""); + } + + }); + + } + + [Fact] + public async Task AC04_DynamicClientRegistrationViewDiscoveryDocument_Invalid() + { + string testName = $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC04_DynamicClientRegistrationViewDiscoveryDocument_Invalid)}"; + + await ArrangeAsync(testName, async (page) => + { + await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands + }); + + await TestAsync(testName, async (page) => + { + // View discovery document + DynamicClientRegistrationPage dcrPage = new DynamicClientRegistrationPage(page, WEB_URL); + await dcrPage.GotoDynamicClientRegistrationPage(); + await dcrPage.SelectDataHolderBrandId(DH_BRANDID_DUMMY_DH); + + // Assert + using (new AssertionScope()) + { + string discoveryDocumentDetails = await dcrPage.GetDiscoveryDocumentDetails("Discovery Document"); + discoveryDocumentDetails.Should().Contain("Unable to load Discovery Document from https://idp.bank2/.well-known/openid-configuration"); + } + + }); + } + + [Theory] + [InlineData(DH_BRANDID)] + [InlineData(DH_BRANDID_ENERGY)] // Also test for Energy DH + public async Task AC04_DynamicClientRegistrationUpdate(string dhBrandId) + { + string testName = $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC04_DynamicClientRegistrationUpdate)}"; + try + { + await ArrangeAsync(testName, async (page) => + { + await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands + }); + + await TestAsync(testName, async (page) => + { + DynamicClientRegistrationPage dcrPage = new DynamicClientRegistrationPage(page, WEB_URL); + + // Create a default Banking Dataholder Registration using defaults + await dcrPage.GotoDynamicClientRegistrationPage(); + await dcrPage.SelectDataHolderBrandId(dhBrandId); + await dcrPage.ClickRegister(); + + var registrationResponseJson = await dcrPage.GetRegistrationResponse(); + + // Deserialise response and return DH client id + string clientId = GetClientIdFromRegistrationResponse(registrationResponseJson); + + // Navigate to fresh DCR page + await dcrPage.GotoDynamicClientRegistrationPage(); + await dcrPage.ClickEditRegistration(clientId); + + // Assert Client Correctly Loaded + dcrPage.GetClientId().Result.Should().Be(clientId); + + // Modify to valid hybrid mode values + await dcrPage.EnterResponseTypes("code,code id_token"); + await dcrPage.EnterIdTokenEncryptedResponseAlgo("RSA-OAEP"); + await dcrPage.EnterIdTokenEncryptedResponseEnc("A128CBC-HS256"); + + await dcrPage.ClickUpdate(); + + // Assert Software Product Registration Updated + var registrationResponse = await dcrPage.GetRegistrationResponse(includeHeading: true); + registrationResponse.Should().Contain("Registration update successful."); + registrationResponse.Should().Contain("code id_token"); + registrationResponse.Should().Contain("\"id_token_encrypted_response_alg\": \"RSA-OAEP\""); + registrationResponse.Should().Contain("\"id_token_encrypted_response_enc\": \"A128CBC-HS256\""); + }); } finally @@ -207,7 +419,7 @@ await CleanupAsync(async (page) => } public delegate Task ConsentsDelegate(IPage page, ConsentAndAuthorisationResponse response); - public async Task Test_Consents(string dhBrandId, string drBrandId, string drSoftwareProductId, string customerId, string customerAccounts, string testName, ConsentsDelegate test) + async Task Test_Consents(string dhBrandId, string drBrandId, string drSoftwareProductId, string customerId, string customerAccounts, string testName, ConsentsDelegate test) { try { @@ -217,7 +429,7 @@ await ArrangeAsync($"{testName} - DH_BrandId={dhBrandId}", async (page) => { await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands await SSA_Get(page, "ALL", "3", drBrandId, drSoftwareProductId, "OK - SSA Generated"); - dhClientId = await ClientRegistration_Create(page, dhBrandId, drBrandId, drSoftwareProductId) + dhClientId = await ClientRegistration_Create(page, dhBrandId) ?? throw new NullReferenceException(nameof(dhClientId)); cdrArrangement = await NewConsentAndAuthorisationWithPAR(page, dhClientId, customerId, customerAccounts, dhBrandId) ?? throw new NullReferenceException(nameof(cdrArrangement)); @@ -245,6 +457,7 @@ public async Task AC06_Consents_ViewIDToken(string dhBrandId, string drBrandId, { await Test_Consents(dhBrandId, drBrandId, drSoftwareProductId, customerId, customerAccounts, $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC06_Consents_ViewIDToken)}", async (page, response) => { + // CT: This needs to be a different exception type await TestToken(page, "View ID Token", response?.IDToken ?? throw new ArgumentNullException(nameof(response.IDToken))); }); } @@ -256,6 +469,7 @@ public async Task AC06_Consents_ViewAccessToken(string dhBrandId, string drBrand { await Test_Consents(dhBrandId, drBrandId, drSoftwareProductId, customerId, customerAccounts, $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC06_Consents_ViewAccessToken)}", async (page, response) => { + // CT: This needs to be a different exception type await TestToken(page, "View Access Token", response?.AccessToken ?? throw new ArgumentNullException(nameof(response.AccessToken))); }); } @@ -267,6 +481,7 @@ public async Task AC06_Consents_ViewRefreshToken(string dhBrandId, string drBran { await Test_Consents(dhBrandId, drBrandId, drSoftwareProductId, customerId, customerAccounts, $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC06_Consents_ViewRefreshToken)}", async (page, response) => { + // CT: This needs to be a different exception type await TestToken(page, "View Refresh Token", response?.RefreshToken ?? throw new ArgumentNullException(nameof(response.RefreshToken))); }); } @@ -302,6 +517,7 @@ public async Task AC06_Consents_Introspect(string dhBrandId, string drBrandId, s { await Test_Consents(dhBrandId, drBrandId, drSoftwareProductId, customerId, customerAccounts, $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC06_Consents_Introspect)}", async (page, response) => { + // CT: This needs to be a different exception type var expected = new (string, string?)[] { ("cdr_arrangement_id", response?.CDRArrangementID ?? throw new ArgumentNullException(nameof(response.CDRArrangementID))), @@ -399,7 +615,7 @@ await ArrangeAsync(testName, async (page) => { await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands await SSA_Get(page, "ALL", "3", drBrandId, drSoftwareProductId, "OK - SSA Generated"); - dhClientId = await ClientRegistration_Create(page, dhBrandId, drBrandId, drSoftwareProductId) + dhClientId = await ClientRegistration_Create(page, dhBrandId) ?? throw new NullReferenceException(nameof(dhClientId)); cdrArrangement = await NewConsentAndAuthorisationWithPAR(page, dhClientId, customerId, customerAccounts, dhBrandId) ?? throw new NullReferenceException(nameof(cdrArrangement)); @@ -441,7 +657,7 @@ await ArrangeAsync(testName, async (page) => { await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands await SSA_Get(page, "ALL", "3", drBrandId, drSoftwareProductId, "OK - SSA Generated"); - dhClientId = await ClientRegistration_Create(page, dhBrandId, drBrandId, drSoftwareProductId) + dhClientId = await ClientRegistration_Create(page, dhBrandId) ?? throw new NullReferenceException(nameof(dhClientId)); cdrArrangement = await NewConsentAndAuthorisationWithPAR(page, dhClientId, customerId, customerAccounts, dhBrandId) ?? throw new NullReferenceException(nameof(cdrArrangement)); @@ -478,7 +694,7 @@ await ArrangeAsync(testName, async (page) => { await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands await SSA_Get(page, "ALL", "3", drBrandId, drSoftwareProductId, "OK - SSA Generated"); - dhClientId = await ClientRegistration_Create(page, dhBrandId, drBrandId, drSoftwareProductId) + dhClientId = await ClientRegistration_Create(page, dhBrandId) ?? throw new NullReferenceException(nameof(dhClientId)); cdrArrangement = await NewConsentAndAuthorisationWithPAR(page, dhClientId, customerId, customerAccounts, dhBrandId) ?? throw new NullReferenceException(nameof(cdrArrangement)); @@ -513,7 +729,7 @@ await iFrame.SelectOptionAsync( await iFrame.ClickAsync("text=Try it out"); // Arrange - Set x-v - await iFrame.FillAsync("[placeholder=\"x-v\"]", "1"); + await iFrame.FillAsync("[placeholder=\"x-v\"]", "2"); // Act - Click Execute await iFrame.ClickAsync("text=Execute"); @@ -545,7 +761,7 @@ await ArrangeAsync(testName, async (page) => { await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands await SSA_Get(page, "ALL", "3", drBrandId, drSoftwareProductId, "OK - SSA Generated"); - dhClientId = await ClientRegistration_Create(page, dhBrandId, drBrandId, drSoftwareProductId) + dhClientId = await ClientRegistration_Create(page, dhBrandId) ?? throw new NullReferenceException(nameof(dhClientId)); cdrArrangement = await NewConsentAndAuthorisationWithPAR(page, dhClientId, customerId, customerAccounts, dhBrandId) ?? throw new NullReferenceException(nameof(cdrArrangement)); @@ -582,7 +798,7 @@ await ArrangeAsync(testName, async (page) => { await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands await SSA_Get(page, "ALL", "3", drBrandId, drSoftwareProductId, "OK - SSA Generated"); - dhClientId = await ClientRegistration_Create(page, dhBrandId, drBrandId, drSoftwareProductId) + dhClientId = await ClientRegistration_Create(page, dhBrandId) ?? throw new NullReferenceException(nameof(dhClientId)); cdrArrangement = await NewConsentAndAuthorisationWithPAR(page, dhClientId, customerId, customerAccounts, dhBrandId) ?? throw new NullReferenceException(nameof(cdrArrangement)); @@ -636,6 +852,73 @@ await CleanupAsync(async (page) => } } + [Theory] + [InlineData(DH_BRANDID, DR_BRANDID, DR_SOFTWAREPRODUCTID, CUSTOMERID_BANKING, CUSTOMERACCOUNTS_BANKING)] + public async Task AC08_ConsumerDataSharing_Common_StatusGet(string dhBrandId, string drBrandId, string drSoftwareProductId, string customerId, string customerAccounts) + { + try + { + string testName = $"{nameof(US23863_MDR_E2ETests)} - {nameof(AC08_ConsumerDataSharing_Common_StatusGet)}"; + string? dhClientId = null; + ConsentAndAuthorisationResponse? cdrArrangement = null; + await ArrangeAsync(testName, async (page) => + { + await DataHolders_Discover(page, "ALL", "2", -1); // get all dh brands + await SSA_Get(page, "ALL", "3", drBrandId, drSoftwareProductId, "OK - SSA Generated"); + dhClientId = await ClientRegistration_Create(page, dhBrandId) + ?? throw new NullReferenceException(nameof(dhClientId)); + cdrArrangement = await NewConsentAndAuthorisationWithPAR(page, dhClientId, customerId, customerAccounts, dhBrandId) + ?? throw new NullReferenceException(nameof(cdrArrangement)); + }); + + await TestAsync(testName, async (page) => + { + // Arrange - Goto home page, click menu button, check page loaded + await page.GotoAsync(WEB_URL); + await page.Locator("span:text(\"Consumer Data Sharing\") + ul >> a:text(\"Common\")").ClickAsync(); + await page.Locator("h2 >> text=Data Sharing - Common").TextContentAsync(); + + // Arrange - Get Swagger iframe + var iFrame = page.FrameByUrl($"{WEB_URL}/{SWAGGER_COMMON_IFRAME}") ?? throw new Exception($"IFrame not found - {SWAGGER_BANKING_IFRAME}"); + + // Wait for the CDR arrangement +
+ @if (Model.TransactionType == "Create") + { + + } + else + { + + } Cancel
@@ -209,7 +221,7 @@ else } - @@ -250,15 +262,8 @@ else @foreach (var reg in Model.Registrations) { - - @if (allowDynamicClientRegistration) - { - @reg.ClientId - } - else - { - @reg.ClientId - } + + @reg.ClientId @reg.BrandName (@reg.DataHolderBrandId) @if (!allowDynamicClientRegistration) @@ -269,8 +274,9 @@ else @if (allowDynamicClientRegistration) { - Delete View + Edit + Delete } else if (!string.IsNullOrEmpty(@reg.ClientId)) { @@ -293,12 +299,29 @@ else Registrations: @Model.Registrations.Count() + +
+ @if (allowDynamicClientRegistration) + { +
+
Discovery Document Details
+
+

+ +

+

+                                
Loading...
+
+

+
+
+ }