From 28a92ff8378a8811f070c2a989e1bd0092ad9e65 Mon Sep 17 00:00:00 2001 From: CDR-FarooqK <110141673+CDR-FarooqK@users.noreply.github.com> Date: Tue, 1 Apr 2025 13:53:26 +1100 Subject: [PATCH] v3.0.0 release (#22) Co-authored-by: CDR Open Source Co-authored-by: AidenJahani --- .azuredevops/pipelines/build-v2.yml | 12 +- .github/workflows/codeql-analysis.yml | 6 +- .github/workflows/dotnet.yml | 8 +- .github/workflows/test-report.yml | 4 +- .gitignore | 5 +- CHANGELOG.md | 10 + README.md | 12 +- SECURITY.md | 3 +- Source/.editorconfig | 548 +++++++++++++ .../CdrAuthServer.API.Logger.csproj | 17 +- .../IRequestResponseLogger.cs | 2 +- .../LoggerExtensions.cs | 2 +- .../RequestResponseLogger.cs | 56 +- .../RequestResponseLoggingMiddleware.cs | 69 +- .../CdrAuthServer.Domain.csproj | 3 +- Source/CdrAuthServer.Domain/Constants.cs | 11 +- .../CdrAuthServer.Domain/Entities/Client.cs | 8 +- Source/CdrAuthServer.Domain/Entities/Grant.cs | 2 +- .../CdrAuthServer.Domain/Enums/CdsErrors.cs | 2 +- .../Models/ResponseErrorList.cs | 2 +- Source/CdrAuthServer.E2ETests/BaseTest.cs | 9 +- .../CdrAuthServer.E2ETests.csproj | 11 +- Source/CdrAuthServer.E2ETests/Startup.cs | 18 +- .../US39327_CdrAuthServer_E2ETests.cs | 243 ++---- .../XUnit/Orderers/AlphabeticalOrderer.cs | 8 +- .../BaseTest.cs | 46 +- ....GetDataRecipients.IntegrationTests.csproj | 12 +- .../ConnectionStringCheck.cs | 31 +- .../DatabaseSeeder.cs | 120 +-- .../DisplayTestMethodNameAttribute.cs | 29 + .../Fixtures/TestFixture.cs | 2 +- .../US28391_GetDataRecipients.cs | 53 +- .../XUnit/AlphabeticalOrderer.cs | 8 +- .../CdrAuthServer.GetDataRecipients.csproj | 11 +- .../GetDROptions.cs | 15 +- ...pients.cs => GetDataRecipientsFunction.cs} | 125 +-- ...etDataRecipients_IntegrationTestsHelper.cs | 5 +- .../Program.cs | 16 +- ...eckXVAttribute.cs => ReturnXVAttribute.cs} | 4 +- .../AuthServerAuthorisationPolicyAttribute.cs | 2 +- .../Authorisation/AuthorisationPolicy.cs | 10 +- .../CdrAuthServer.Infrastructure.csproj | 11 +- .../Certificates/CertificateLoadDetails.cs | 9 +- .../Certificates/CertificateLoader.cs | 8 +- .../Certificates/OcspRequester.cs | 13 +- .../CdrAuthServer.Infrastructure/Constants.cs | 4 +- .../Exceptions/ClientCertificateException.cs | 5 +- .../Extensions/AttributeExtensions.cs | 9 +- .../CdrSwaggerMiddlewareExtensions.cs | 11 +- .../Extensions/CertificateExtensions.cs | 5 +- .../Extensions/HttpExtensions.cs | 3 +- .../Extensions/NoHttpsException.cs | 12 + .../Extensions/UrlExtensions.cs | 7 +- .../Extensions/WebServerExtensions.cs | 12 +- .../Models/CdrApiEndpointVersionOptions.cs | 44 +- .../Models/CdrApiOptions.cs | 10 +- .../Models/CdrSwaggerOptions.cs | 6 +- .../Models/Pkce.cs | 5 +- .../Models/Response.cs | 10 +- .../Models/TokenResponse.cs | 2 + .../PrivateKeyJwt.cs | 23 +- .../Versioning/ApiVersionErrorResponse.cs | 2 +- .../Versioning/CdrVersionReader.cs | 6 +- .../BaseTest.cs | 7 +- .../CdrAuthServer.IntegrationTests.csproj | 21 +- .../Infrastructure/DataHolderAccessToken.cs | 4 +- .../Interfaces/IAuthorizationService.cs | 2 +- ...taHolderCDRArrangementRevocationService.cs | 2 +- .../IDataHolderIntrospectionService.cs | 2 +- .../Models/DcrResponse.cs | 35 + .../Models/OpenIdConfiguration.cs | 23 +- .../Services/AuthorizationService.cs | 42 +- ...taHolderCDRArrangementRevocationService.cs | 15 +- .../DataHolderIntrospectionService.cs | 21 +- .../CdrAuthServer.IntegrationTests/Startup.cs | 36 +- .../US44264_CdrAuthServer_JARM_Authorise.cs | 15 +- .../JARM/US44264_CdrAuthServer_JARM_DCR.cs | 737 ++++-------------- .../JARM/US44264_CdrAuthServer_JARM_OIDC.cs | 50 +- .../JARM/US44264_CdrAuthServer_JARM_PAR.cs | 119 +-- .../US12678_CdrAuthServer_Authorisation.cs | 94 ++- ...S12962_CdrAuthServer_OIDC_Configuration.cs | 31 +- .../Tests/US12963_CdrAuthServer_Token.cs | 130 +-- .../Tests/US12964_CDRAuthServer_OIDC_JWKS.cs | 7 +- .../Tests/US12965_CdrAuthServer_UserInfo.cs | 28 +- .../US12966_CdrAuthServer_Introspection.cs | 41 +- .../Tests/US12968_CdrAuthServer_PAR.cs | 87 ++- ...US15584_CdrAuthServer_Registration_POST.cs | 18 +- ..._US15585_CdrAuthServer_Registration_PUT.cs | 28 +- ..._US15586_CdrAuthServer_Registration_GET.cs | 14 +- ...15587_CdrAuthServer_Registration_DELETE.cs | 6 +- ...652_CdrAuthServer_ArrangementRevocation.cs | 68 +- .../US18469_CdrAuthServer_TokenRevocation.cs | 76 +- .../XUnit/Orderers/AlphabeticalOrderer.cs | 8 +- .../XUnit/Skippable/Samples.cs | 2 +- .../XUnit/Skippable/SkipTestException.cs | 4 +- .../XUnit/Skippable/SkippableFactAttribute.cs | 4 +- .../Skippable/SkippableTheoryAttribute.cs | 4 +- .../SkippableFactDiscoverer.cs | 2 +- .../SkippableFactMessageBus.cs | 6 +- .../XunitExtensions/SkippableFactTestCase.cs | 24 +- .../SkippableTheoryDiscoverer.cs | 4 +- .../SkippableTheoryTestCase.cs | 19 +- .../CdrAuthServer.Repository.csproj | 13 +- .../CdrAuthServer.Repository/CdrRepository.cs | 26 +- .../ClientRepository.cs | 15 +- .../ClientClaimsConfiguration.cs | 7 +- .../Configuration/ClientConfiguration.cs | 2 +- .../Configuration/GrantConfiguration.cs | 28 +- .../SoftwareProductConfiguration.cs | 37 +- .../Configuration/TokenConfiguration.cs | 2 +- .../Entities/Client.cs | 5 +- .../Entities/ClientClaims.cs | 8 +- .../Entities/Grant.cs | 11 +- .../Entities/SoftwareProduct.cs | 22 +- .../Entities/Token.cs | 1 + .../GrantRepository.cs | 13 +- .../CdrAuthServerDatabaseContext.cs | 3 +- .../Infrastructure/MappingProfile.cs | 65 +- .../20221006235046_V001-InitialCreate.cs | 32 +- ...205002235_V002-CreateLogEventsDrService.cs | 6 +- ...30130011835_V003-RegisterClientSeeddata.cs | 32 +- ...63622_V004-RegisterClientClaimsSeeddata.cs | 10 +- ..._V005-AddSoftwareProductForCDR-Register.cs | 30 +- ...220828_V006-Seed-SoftwareProduct-Status.cs | 26 +- .../TokenRepository.cs | 4 +- .../CdrAuthServer.TLS.Gateway.csproj | 15 +- Source/CdrAuthServer.TLS.Gateway/Program.cs | 16 +- Source/CdrAuthServer.UI/package-lock.json | 501 ++++++------ Source/CdrAuthServer.UnitTests/BaseTest.cs | 35 +- .../CdrAuthServer.UnitTests.csproj | 11 +- .../CertificateHelper.cs | 27 + .../Controllers/AdminControllerTests.cs | 209 +++++ .../Controllers/UtilityControllerTests.cs | 220 ++++++ .../Extensions/StringExtensionsTests.cs | 5 +- .../Helpers/HttpHelperTests.cs | 37 +- .../HttpClientHelper.cs | 93 +++ .../CdrAuthServer.UnitTests/ResultHelper.cs | 59 ++ .../Services/ConsentRevocationServiceTests.cs | 237 ++++++ .../Services/JwksServiceTests.cs | 207 +++++ .../Services/RegisterClientServiceTests.cs | 126 +++ .../AuthorizeRequestValidatorTests.cs | 168 ++-- .../ClientAssertionValidatorTests.cs | 90 ++- .../ClientRegistrationValidatorTests.cs | 66 +- .../Validators/JwtValidatorTests.cs | 51 +- .../Validators/ParValidatorTest.cs | 147 ++++ .../Validators/ParValidatorTests.cs | 94 --- .../Validators/RequestObjectValidatorTests.cs | 65 +- .../Validators/TokenRequestValidatorTests.cs | 118 +-- .../CdrAuthServer.mTLS.Gateway.csproj | 13 +- .../Certificates/CertificateValidator.cs | 12 +- .../ClientCertificateException.cs | 17 +- .../Extensions/WebServerExtensions.cs | 23 +- Source/CdrAuthServer.mTLS.Gateway/Program.cs | 29 +- Source/CdrAuthServer.sln | 1 + .../Authorisation/AccessTokenHandler.cs | 4 +- .../Authorisation/AuthorisationPolicy.cs | 2 +- .../Authorisation/HolderOfKeyHandler.cs | 8 +- .../Authorisation/PolicyAuthorizeAttribute.cs | 10 +- .../Authorisation/ScopeHandler.cs | 6 +- Source/CdrAuthServer/CdrAuthServer.csproj | 18 +- .../Configuration/CdrRegisterConfiguration.cs | 5 +- .../Configuration/ConfigurationOptions.cs | 74 +- Source/CdrAuthServer/Configuration/Keys.cs | 2 +- ...erverCertificateValidationConfiguration.cs | 2 + ....ParticipantTooling.MockAuthServer.API.xml | 559 ------------- .../Controllers/AdminController.cs | 131 +--- .../ArrangementRevocationController.cs | 4 +- .../Controllers/AuthorisationController.cs | 196 ++--- .../Controllers/DiscoveryController.cs | 17 +- .../Controllers/IntrospectionController.cs | 24 +- .../Controllers/JwksController.cs | 22 +- ...er.cs => PushedAuthorisationController.cs} | 9 +- .../Controllers/RegistrationController.cs | 60 +- .../Controllers/ResourceController.cs | 12 +- .../Controllers/RevocationController.cs | 17 +- .../Controllers/TokenController.cs | 10 +- .../Controllers/UserInfoController.cs | 9 +- .../Controllers/UtilityController.cs | 192 ++--- .../Exceptions/ClientMetadataException.cs | 6 +- .../CdrAuthServer/Exceptions/JwksException.cs | 6 +- .../AuthorizationRequestObjectExtensions.cs | 23 - .../Extensions/ClaimsPrincipalExtensions.cs | 7 +- .../Extensions/ConfigExtensions.cs | 42 +- .../Extensions/DateTimeExtensions.cs | 4 +- .../Extensions/EnumExtensions.cs | 16 + .../Extensions/HashExtensions.cs | 12 +- .../CdrAuthServer/Extensions/JwtExtensions.cs | 6 +- .../Extensions/RequestHeadersExtensions.cs | 15 +- .../Extensions/SwaggerExtensions.cs | 2 +- .../Formatters/JwtInputFormatter.cs | 11 +- Source/CdrAuthServer/Helpers/HttpHelper.cs | 4 +- .../HttpLoggingDelegatingHandler.cs | 77 ++ .../IdPermanence/AesEncryptor.cs | 12 +- .../IdPermanence/CompressionExtensions.cs | 2 +- .../IdPermanence/IIdPermanenceManager.cs | 8 +- .../IdPermanence/IdPermanenceHelper.cs | 73 +- .../IdPermanence/IdPermanenceManager.cs | 53 +- .../IdPermanence/IdPermanenceParameters.cs | 1 + .../IdPermanence/SubPermanenceParameters.cs | 1 + Source/CdrAuthServer/Models/Acr.cs | 16 + .../AdrArrangementRevocationResponse.cs | 9 + .../Models/ArrangeRevocationRequest.cs | 15 + .../Models/ArrangeRevocationResponse.cs | 11 + .../Models/ArrangementRevocationResponse.cs | 25 - .../Models/AuthorizationCodeGrant.cs | 77 ++ .../Models/AuthorizationRequestObject.cs | 34 +- .../Models/AuthorizeCallbackRequest.cs | 14 +- .../CdrAuthServer/Models/AuthorizeClaims.cs | 16 + .../Models/AuthorizeRedirectRequest.cs | 4 +- .../CdrAuthServer/Models/AuthorizeRequest.cs | 30 +- .../CdrAuthServer/Models/AuthorizeResponse.cs | 1 - .../Models/CdrArrangementGrant.cs | 62 ++ Source/CdrAuthServer/Models/Client.cs | 16 +- .../Models/ClientRegistrationRequest.cs | 22 +- .../Models/ClientRegistrationResponse.cs | 7 +- Source/CdrAuthServer/Models/Customer.cs | 9 + Source/CdrAuthServer/Models/Data.cs | 10 + .../Models/DataHolderCustomer.cs | 15 +- .../Models/DataRecipientBrand.cs | 15 - .../Models/DataRecipientRequest.cs | 6 - Source/CdrAuthServer/Models/Discovery.cs | 7 - Source/CdrAuthServer/Models/Error.cs | 1 - Source/CdrAuthServer/Models/ErrorCatalogue.cs | 51 +- Source/CdrAuthServer/Models/Grant.cs | 262 +------ .../CdrAuthServer/Models/HeadlessModeUser.cs | 2 +- Source/CdrAuthServer/Models/IdToken.cs | 10 + Source/CdrAuthServer/Models/JsonWebKey.cs | 27 + Source/CdrAuthServer/Models/JsonWebKeySet.cs | 18 +- Source/CdrAuthServer/Models/LegalEntity.cs | 19 + Source/CdrAuthServer/Models/Person.cs | 9 + .../CdrAuthServer/Models/RefreshTokenGrant.cs | 88 +++ Source/CdrAuthServer/Models/Register/Links.cs | 14 + .../Models/Register/LinksPaginated.cs | 33 + Source/CdrAuthServer/Models/Register/Meta.cs | 15 + .../Models/Register/MetaPaginated.cs | 19 + .../Models/Register/RegisterResponse.cs | 25 + .../Register/RegisterResponsePaginated.cs | 25 + .../CdrAuthServer/Models/RequestUriGrant.cs | 34 + .../CdrAuthServer/Models/SoftwareProduct.cs | 10 + .../CdrAuthServer/Models/SoftwareStatement.cs | 10 +- Source/CdrAuthServer/Models/TokenRequest.cs | 14 +- Source/CdrAuthServer/Models/TokenResponse.cs | 1 - Source/CdrAuthServer/Program.cs | 151 ++-- ...ingProfile.cs => ServiceMappingProfile.cs} | 0 Source/CdrAuthServer/Services/CdrService.cs | 11 +- .../CdrAuthServer/Services/ClientService.cs | 5 +- .../Services/ConsentRevocationService.cs | 162 ++++ .../CdrAuthServer/Services/CustomerService.cs | 26 +- Source/CdrAuthServer/Services/GrantService.cs | 8 +- Source/CdrAuthServer/Services/ICdrService.cs | 2 + .../CdrAuthServer/Services/IClientService.cs | 8 +- .../Services/IConsentRevocationService.cs | 30 + .../Services/ICustomerService.cs | 2 +- .../CdrAuthServer/Services/IGrantService.cs | 4 + Source/CdrAuthServer/Services/IJwksService.cs | 10 +- .../Services/IRegisterClientService.cs | 18 + .../CdrAuthServer/Services/ITokenService.cs | 7 +- Source/CdrAuthServer/Services/JwksService.cs | 23 +- .../Services/RegisterClientService.cs | 31 + Source/CdrAuthServer/Services/TokenService.cs | 170 ++-- .../AuthorizationOperationFilter.cs | 12 +- .../SwaggerFilters/CustomDocumentFilter.cs | 2 +- .../AuthorizeRequestValidationResult.cs | 3 +- .../Validation/AuthorizeRequestValidator.cs | 127 +-- .../Validation/ClientAssertionValidator.cs | 18 +- .../Validation/ClientRegistrationValidator.cs | 38 +- .../Validation/IAuthorizeRequestValidator.cs | 1 + .../Validation/IClientAssertionValidator.cs | 4 +- .../IClientRegistrationValidator.cs | 2 +- .../CdrAuthServer/Validation/IJwtValidator.cs | 23 +- .../CdrAuthServer/Validation/IParValidator.cs | 2 +- .../Validation/IRequestObjectValidator.cs | 2 +- .../CdrAuthServer/Validation/JwtValidator.cs | 22 +- .../CdrAuthServer/Validation/ParValidator.cs | 30 +- .../Validation/RequestObjectValidator.cs | 135 +--- .../Validation/TokenRequestValidator.cs | 38 +- .../ValidateClientAssertionAttribute.cs | 12 +- .../Validation/ValidateMtlsAttribute.cs | 5 +- .../Validation/ValidationResult.cs | 3 +- .../appsettings.Development.json | 383 ++++----- .../appsettings.Release.Standalone.json | 59 +- Source/CdrAuthServer/appsettings.Release.json | 59 +- Source/CdrAuthServer/appsettings.json | 229 ++++-- Source/Directory.Build.props | 5 +- Source/Dockerfile | 1 + Source/Dockerfile.get-data-recipients | 2 + Source/Dockerfile.standalone | 3 +- Source/docker-compose.E2ETests.Standalone.yml | 10 +- ...er-compose.IntegrationTests.Standalone.yml | 10 +- Source/run-e2e-tests-standalone.ps1 | 2 +- 290 files changed, 6401 insertions(+), 5094 deletions(-) create mode 100644 Source/.editorconfig create mode 100644 Source/CdrAuthServer.GetDataRecipients.IntegrationTests/DisplayTestMethodNameAttribute.cs rename Source/CdrAuthServer.GetDataRecipients/{GetDataRecipients.cs => GetDataRecipientsFunction.cs} (76%) rename Source/CdrAuthServer.Infrastructure/Attributes/{CheckXVAttribute.cs => ReturnXVAttribute.cs} (99%) create mode 100644 Source/CdrAuthServer.Infrastructure/Extensions/NoHttpsException.cs create mode 100644 Source/CdrAuthServer.UnitTests/CertificateHelper.cs create mode 100644 Source/CdrAuthServer.UnitTests/Controllers/AdminControllerTests.cs create mode 100644 Source/CdrAuthServer.UnitTests/Controllers/UtilityControllerTests.cs create mode 100644 Source/CdrAuthServer.UnitTests/HttpClientHelper.cs create mode 100644 Source/CdrAuthServer.UnitTests/ResultHelper.cs create mode 100644 Source/CdrAuthServer.UnitTests/Services/ConsentRevocationServiceTests.cs create mode 100644 Source/CdrAuthServer.UnitTests/Services/JwksServiceTests.cs create mode 100644 Source/CdrAuthServer.UnitTests/Services/RegisterClientServiceTests.cs create mode 100644 Source/CdrAuthServer.UnitTests/Validators/ParValidatorTest.cs delete mode 100644 Source/CdrAuthServer.UnitTests/Validators/ParValidatorTests.cs delete mode 100644 Source/CdrAuthServer/ConsumerDataRight.ParticipantTooling.MockAuthServer.API.xml rename Source/CdrAuthServer/Controllers/{PushedAuthorisationRequestController.cs => PushedAuthorisationController.cs} (93%) delete mode 100644 Source/CdrAuthServer/Extensions/AuthorizationRequestObjectExtensions.cs create mode 100644 Source/CdrAuthServer/Extensions/EnumExtensions.cs create mode 100644 Source/CdrAuthServer/HttpPipeline/HttpLoggingDelegatingHandler.cs create mode 100644 Source/CdrAuthServer/Models/Acr.cs create mode 100644 Source/CdrAuthServer/Models/AdrArrangementRevocationResponse.cs create mode 100644 Source/CdrAuthServer/Models/ArrangeRevocationRequest.cs create mode 100644 Source/CdrAuthServer/Models/ArrangeRevocationResponse.cs delete mode 100644 Source/CdrAuthServer/Models/ArrangementRevocationResponse.cs create mode 100644 Source/CdrAuthServer/Models/AuthorizationCodeGrant.cs create mode 100644 Source/CdrAuthServer/Models/AuthorizeClaims.cs create mode 100644 Source/CdrAuthServer/Models/CdrArrangementGrant.cs create mode 100644 Source/CdrAuthServer/Models/Customer.cs create mode 100644 Source/CdrAuthServer/Models/Data.cs create mode 100644 Source/CdrAuthServer/Models/IdToken.cs create mode 100644 Source/CdrAuthServer/Models/JsonWebKey.cs create mode 100644 Source/CdrAuthServer/Models/LegalEntity.cs create mode 100644 Source/CdrAuthServer/Models/Person.cs create mode 100644 Source/CdrAuthServer/Models/RefreshTokenGrant.cs create mode 100644 Source/CdrAuthServer/Models/Register/Links.cs create mode 100644 Source/CdrAuthServer/Models/Register/LinksPaginated.cs create mode 100644 Source/CdrAuthServer/Models/Register/Meta.cs create mode 100644 Source/CdrAuthServer/Models/Register/MetaPaginated.cs create mode 100644 Source/CdrAuthServer/Models/Register/RegisterResponse.cs create mode 100644 Source/CdrAuthServer/Models/Register/RegisterResponsePaginated.cs create mode 100644 Source/CdrAuthServer/Models/RequestUriGrant.cs rename Source/CdrAuthServer/{MappingProfile.cs => ServiceMappingProfile.cs} (100%) create mode 100644 Source/CdrAuthServer/Services/ConsentRevocationService.cs create mode 100644 Source/CdrAuthServer/Services/IConsentRevocationService.cs create mode 100644 Source/CdrAuthServer/Services/IRegisterClientService.cs create mode 100644 Source/CdrAuthServer/Services/RegisterClientService.cs diff --git a/.azuredevops/pipelines/build-v2.yml b/.azuredevops/pipelines/build-v2.yml index 895ebd8..49bc317 100644 --- a/.azuredevops/pipelines/build-v2.yml +++ b/.azuredevops/pipelines/build-v2.yml @@ -194,8 +194,8 @@ jobs: # Output Docker Logs - script: | - docker logs mock-register - docker logs cdr-auth-server-standalone + docker logs mock-register-cdr-auth-server-integration-tests + docker logs cdr-auth-server-standalone-integration-tests displayName: 'Output Docker Logs' condition: always() @@ -245,8 +245,8 @@ jobs: # Output Docker Logs - script: | - docker logs mock-register - docker logs cdr-auth-server-standalone + docker logs mock-register-cdr-auth-server-integration-tests + docker logs cdr-auth-server-standalone-integration-tests displayName: 'Output Docker Logs' condition: always() @@ -314,8 +314,8 @@ jobs: # Output Docker Logs - script: | - docker logs mock-register - docker logs cdr-auth-server-standalone + docker logs mock-register-cdr-auth-server-e2e-tests + docker logs cdr-auth-server-standalone-e2e-tests displayName: 'Output Docker Logs' condition: always() diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 62acd02..cef68d4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -55,11 +55,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -72,4 +72,4 @@ jobs: dotnet build ./Source/CdrAuthServer.sln --configuration 'Release' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 02dc8ca..d919a09 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout Authorisation Server - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: path: ./authorisation-server @@ -101,7 +101,7 @@ jobs: # Archive unit test results - name: Archive unit test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: unit-test-results @@ -109,7 +109,7 @@ jobs: # Archive integration test results - name: Archive integration test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: integration-test-results @@ -117,7 +117,7 @@ jobs: # Archive authorisation server logs - name: Archive authorisation server logs - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: integration-test-artifacts diff --git a/.github/workflows/test-report.yml b/.github/workflows/test-report.yml index e9ddb8c..ced2e2a 100644 --- a/.github/workflows/test-report.yml +++ b/.github/workflows/test-report.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Publish Unit Test Report uses: dorny/test-reporter@v1 @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Publish Integration Test Report uses: dorny/test-reporter@v1 diff --git a/.gitignore b/.gitignore index cc9a5bd..338b153 100644 --- a/.gitignore +++ b/.gitignore @@ -368,4 +368,7 @@ FodyWeavers.xsd # VSCode **/.vscode -**/_temp \ No newline at end of file +**/_temp + +# Local test +.runsettings \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7272f9c..6736a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.0.0] - 2025-03-19 + +### Changed +- Updated NuGet packages +- Fixed multiple build warnings to improve code quality and maintainability +- Updated npm packages used for Authorisation Server User Interface. + +### Removed +- Removed all OIDC Hybrid Flow related code and functionality + ## [2.1.0] - 2024-08-16 ### Changed diff --git a/README.md b/README.md index 1cb4142..b8e5c55 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ ![Consumer Data Right Logo](./Assets/cdr-logo.png?raw=true) -[![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) -[![Conformance Test Suite Data Holders 5.0.0](https://img.shields.io/badge/Conformance%20Test%20Suite%20Data%20Holders-v5.0.0-darkblue.svg)](https://www.cdr.gov.au/for-providers/conformance-test-suite-data-holders) -[![Conformance Test Suite Data Recipients 4.3.0](https://img.shields.io/badge/Conformance%20Test%20Suite%20Data%20Recipients-v4.3.0-darkblue.svg)](https://www.cdr.gov.au/for-providers/conformance-test-suite-data-recipients) +[![Consumer Data Standards v1.33.0](https://img.shields.io/badge/Consumer%20Data%20Standards-v1.33.0-blue.svg)](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.33.0/#introduction) +[![Conformance Test Suite Data Recipients 4.4.0](https://img.shields.io/badge/Conformance%20Test%20Suite%20Data%20Recipients-v4.4.0-darkblue.svg)](https://www.cdr.gov.au/for-providers/conformance-test-suite-data-recipients) [![FAPI 1.0 Advanced Profile](https://img.shields.io/badge/FAPI%201.0-orange.svg)](https://openid.net/specs/openid-financial-api-part-2-1_0.html) [![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/) @@ -17,9 +16,8 @@ The project is used in the Participant Tooling Authorisation Server, providing t ## Authorisation Server - Alignment The Authorisation Server: -- 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) in particular [FAPI 1.0 Migration Phase 4](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.31.0/#introduction) with backwards compatibility to Migration Phase 2 and 3; -- has passed v5.0.0 of the [Conformance Test Suite for Data Holders](https://www.cdr.gov.au/for-providers/conformance-test-suite-data-holders); -- has passed v4.3.0 of the [Conformance Test Suite for Data Recipients](https://www.cdr.gov.au/for-providers/conformance-test-suite-data-recipients); +- aligns to [v1.33.0](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.33.0/#introduction) of the [Consumer Data Standards](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.33.0/#introduction) in particular [FAPI 1.0 Migration Phase 4](https://consumerdatastandardsaustralia.github.io/standards-archives/standards-1.33.0/#introduction) with backwards compatibility to Migration Phase 2 and 3; +- has passed v4.4.0 of the [Conformance Test Suite for Data Recipients](https://www.cdr.gov.au/for-providers/conformance-test-suite-data-recipients); and - is certified with the [FAPI 1.0 Advanced Profile](https://openid.net/specs/openid-financial-api-part-2-1_0.html) . @@ -162,7 +160,7 @@ The Authorisation Server contains the following features: - Pushed Authorization Requests - Authorization endpoint - Support for request_uri parameter - - Hybrid and Authorization Code flow + - Authorization Code flow - Conforms to Consumer Data Right [CX Guidelines](https://consumerdatastandards.gov.au/guidelines-and-conventions/consumer-experience-guidelines). - Token endpoint - Authorization Code diff --git a/SECURITY.md b/SECURITY.md index ef53b05..72af26d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,7 +11,8 @@ Visit our [Responsible disclosure of security vulnerabilities policy](https://ww | Version | Supported | | ------- | ------------------ | -| 2.1.x | :white_check_mark: | +| 3.x.x | :white_check_mark: | +| 2.x.x | :x: | | 1.x.x | :x: | diff --git a/Source/.editorconfig b/Source/.editorconfig new file mode 100644 index 0000000..2013055 --- /dev/null +++ b/Source/.editorconfig @@ -0,0 +1,548 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### +# All files +[*] +indent_style = space +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +# Code files +[*.{cs,csx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom +############################### +# .NET Coding Conventions # +############################### +[*.{cs}] +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_diagnostic.SA1309.severity = none +dotnet_diagnostic.SA1310.severity = none +############################### +# C# Coding Conventions # +############################### +[*.cs] +# var preferences +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + +#Readability Rules + +#A call to an instance member of the local class or a base class is not prefixed with 'this.' +dotnet_diagnostic.SA1101.severity = none + +#The parameter spans multiple lines +dotnet_diagnostic.SA1118.severity = none + +#Spacing Rules +dotnet_diagnostic.SA1010.severity = none + + +#Documentation Rules + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none + +#file header is missing +dotnet_diagnostic.SA1633.severity = none + +#Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = none + +# SA1623: summary text should begin with: 'Gets or sets' +dotnet_diagnostic.SA1623.severity = none + +# documentation for parameter 'cdrArrangementId' is missing +dotnet_diagnostic.SA1611.severity = none + +# Parameter '' has no matching param tag in the XML comment +dotnet_diagnostic.CS1591.severity = none + +# Element return value should be documented +dotnet_diagnostic.SA1615.severity = none + +#parameter has no matching tag in xml +dotnet_diagnostic.CS1573.severity = none + + +#Ordering Rules + +#static members should appear before non-static members +dotnet_diagnostic.SA1204.severity = none + +# public members should come before private +dotnet_diagnostic.SA1202.severity = none + +# 'public' members should come before 'private' members +dotnet_diagnostic.SA1200.severity = none + +# using directive ordering +dotnet_diagnostic.SA1208.severity = none + +# field should not follow class +dotnet_diagnostic.SA1201.severity = none + +#Maintainability + +# S2325: Methods and properties that don't access instance data should be static +dotnet_diagnostic.S2325.severity = none + +[ClientClaims.{cs}] +# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +dotnet_diagnostic.CS8618.severity = silent + +[AuthorizationRequestObject.{cs}] +# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +dotnet_diagnostic.CS8618.severity = silent + +[*Client.{cs}] +# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +dotnet_diagnostic.CS8618.severity = silent + +[DataRecipientRequest.{cs}] +# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +dotnet_diagnostic.CS8618.severity = silent + +[Introspection.{cs}] +# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +dotnet_diagnostic.CS8618.severity = silent + +[ClientAssertionValidatorTests.{cs}] +# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +dotnet_diagnostic.CS8618.severity = silent + + +[US12962_CDRAuthServer_OIDC_JWKS.{cs}] +# CS8602: Dereference of a possibly null reference +dotnet_diagnostic.CS8602.severity = silent + +[US15221_US12969_US15584_CdrAuthServer_Registration_POST.{cs}] +# CS8602: Dereference of a possibly null reference +dotnet_diagnostic.CS8602.severity = silent + +[US12964_CDRAuthServer_OIDC_JWKS.{cs}] +# CS8602: Dereference of a possibly null reference +dotnet_diagnostic.CS8602.severity = silent +dotnet_diagnostic.S1144.severity = silent + +[US12962_CdrAuthServer_OIDC_Configuration.{cs}] +# CS8602: Dereference of a possibly null reference +dotnet_diagnostic.CS8602.severity = silent + +[US44264_CdrAuthServer_JARM_OIDC.{cs}] +# CS8602: Dereference of a possibly null reference +dotnet_diagnostic.CS8602.severity = silent +dotnet_diagnostic.S1144.severity = silent + +[AuthorizationService.{cs}] +# CS8602: Dereference of a possibly null reference +dotnet_diagnostic.CS8602.severity = silent +dotnet_diagnostic.CS8604.severity = silent + +[DataHolderCDRArrangementRevocationService.{cs}] +# CS8602: Dereference of a possibly null reference +dotnet_diagnostic.CS8602.severity = silent + +[US44264_CdrAuthServer_JARM_Authorise.{cs}] +# CS0649: never assigned and will always have default value +dotnet_diagnostic.CS0649.severity = silent +dotnet_diagnostic.CS8602.severity = silent +dotnet_diagnostic.CS8604.severity = silent + +[US12678_CdrAuthServer_Authorisation.{cs}] +# CS0649: never assigned and will always have default value +dotnet_diagnostic.CS0649.severity = silent +dotnet_diagnostic.xUnit1026.severity = silent +dotnet_diagnostic.CS8604.severity = silent +dotnet_diagnostic.SA1210.severity = silent + +[US17652_CdrAuthServer_ArrangementRevocation.{cs}] +dotnet_diagnostic.CS0649.severity = silent +dotnet_diagnostic.CS8600.severity = silent +dotnet_diagnostic.CS8604.severity = silent + +[DataHolderAccessToken.{cs}] +dotnet_diagnostic.CS8601.severity = silent +dotnet_diagnostic.CS8602.severity = silent +dotnet_diagnostic.CS8603.severity = silent + +[DataHolderCDRArrangementRevocationService.{cs}] +dotnet_diagnostic.CS8601.severity = silent + +[US44264_CdrAuthServer_JARM_PAR.{cs}] +dotnet_diagnostic.CS8604.severity = silent +dotnet_diagnostic.xUnit1013.severity = silent + +[US12963_CdrAuthServer_Token.{cs}] +dotnet_diagnostic.CS8600.severity = silent +dotnet_diagnostic.CS8601.severity = silent +dotnet_diagnostic.CS8604.severity = silent + +[US12968_CdrAuthServer_PAR.{cs}] +dotnet_diagnostic.CS8625.severity = silent +dotnet_diagnostic.CS8619.severity = silent + +[US15221_US12969_US15584_CdrAuthServer_Registration_POST.{cs}] +dotnet_diagnostic.CS8604.severity = silent + +[US15221_US12969_US15586_CdrAuthServer_Registration_GET.{cs}] +dotnet_diagnostic.CS8601.severity = silent + +[US18469_CdrAuthServer_TokenRevocation.{cs}] +dotnet_diagnostic.CS8604.severity = silent + +[ClientRegistrationValidatorTests.{cs}] +dotnet_diagnostic.CS8602.severity = silent +dotnet_diagnostic.CS8604.severity = silent +dotnet_diagnostic.CS8620.severity = silent + +[JwtValidatorTest.{cs}] +dotnet_diagnostic.CS8604.severity = silent +dotnet_diagnostic.CS8602.severity = silent + +[StringExtensionsTests.{cs}] +dotnet_diagnostic.CS8600.severity = silent + +[ClientAssertionValidatorTests.{cs}] +dotnet_diagnostic.CS8601.severity = silent +dotnet_diagnostic.CS8602.severity = silent +dotnet_diagnostic.CS8620.severity = silent + + +[ResourceController.{cs}] +dotnet_diagnostic.S6932.severity = silent +dotnet_diagnostic.S6934.severity = silent + +[ClientRegistrationResponse.{cs}] +dotnet_diagnostic.S2094.severity = silent + +[ResourceController.{cs}] +dotnet_diagnostic.S1135.severity = silent + +[*Controller.{cs}] +dotnet_diagnostic.S6934.severity = silent +dotnet_diagnostic.S6931.severity = silent + +[AesEncryptor.{cs}] +dotnet_diagnostic.S3329.severity = silent + +[GetDataRecipientsFunction.{cs}] +dotnet_diagnostic.S4830.severity = silent + +[CertificateValidator.{cs}] +dotnet_diagnostic.SA1300.severity = silent + +[ClientCertificateException.{cs}] +dotnet_diagnostic.SA1300.severity = silent + +[WebServerExtensions.{cs}] +dotnet_diagnostic.SA1300.severity = silent + +[ICertificateValidator.{cs}] +dotnet_diagnostic.SA1300.severity = silent + +[Pkce.{cs}] +dotnet_diagnostic.SA1300.severity = silent + +[JsonWebKey.{cs}] +dotnet_diagnostic.SA1300.severity = silent + +[JsonWebKeySet.{cs}] +dotnet_diagnostic.SA1300.severity = silent + +[GetDataRecipients_IntegrationTestsHelper.{cs}] +dotnet_diagnostic.SA1513.severity = silent +dotnet_diagnostic.SA1028.severity = silent +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.SA1516.severity = silent +dotnet_diagnostic.SA1508.severity = silent +dotnet_diagnostic.SA1003.severity = silent +dotnet_diagnostic.SA1137.severity = silent +dotnet_diagnostic.SA1515.severity = silent + +[ClientRepository.{cs}] +dotnet_diagnostic.S2139.severity = silent + +[SoftwareProductConfiguration.{cs}] +dotnet_diagnostic.S1075.severity = silent + +[GrantRepository.{cs}] +dotnet_diagnostic.S2139.severity = silent + +[US39327_CdrAuthServer_E2ETests.{cs}] +dotnet_diagnostic.S4487.severity = silent +[US44264_CdrAuthServer_JARM_Authorise.{cs}] +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.S3459.severity = silent +dotnet_diagnostic.SA1401.severity = silent +dotnet_diagnostic.SA1307.severity = silent +[US44264_CdrAuthServer_JARM_DCR.{cs}] +dotnet_diagnostic.SA1601.severity = silent +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.S4144.severity = silent +dotnet_diagnostic.SA1115.severity = silent +dotnet_diagnostic.SA1117.severity = silent +dotnet_diagnostic.S3878.severity = silent +[US44264_CdrAuthServer_JARM_OIDC.{cs}] +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.S3459.severity = silent +dotnet_diagnostic.SA1300.severity = silent +dotnet_diagnostic.S3878.severity = silent +[US44264_CdrAuthServer_JARM_PAR.{cs}] +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.SA1300.severity = silent +dotnet_diagnostic.SA1114.severity = silent +dotnet_diagnostic.S1135.severity = silent +[US44264_CdrAuthServer_JARM_Authorise.{cs}] +dotnet_diagnostic.S101.severity = silent +[AuthorizationService.{cs}] +dotnet_diagnostic.S4487.severity = silent +[BaseTest.{cs}] +dotnet_diagnostic.SA1606.severity = silent +dotnet_diagnostic.SA1312.severity = silent +dotnet_diagnostic.SA1313.severity = silent +dotnet_diagnostic.SA1117.severity = silent +[ClientAssertionValidatorTests.{cs}] +dotnet_diagnostic.SA1401.severity = silent +dotnet_diagnostic.SA1312.severity = silent +dotnet_diagnostic.SA1300.severity = silent +[ClientRegistrationValidatorTests.{cs}] +dotnet_diagnostic.SA1300.severity = silent +dotnet_diagnostic.S1135.severity = silent +dotnet_diagnostic.SA1117.severity = silent +dotnet_diagnostic.S125.severity = silent +[ConnectionStringCheck.{cs}] +dotnet_diagnostic.S1135.severity = silent +dotnet_diagnostic.SA1402.severity = silent +[DatabaseSeeder.{cs}] +dotnet_diagnostic.SA1117.severity = silent +dotnet_diagnostic.S1172.severity = silent +[DataHolderAccessToken.{cs}] +dotnet_diagnostic.SA1402.severity = silent +dotnet_diagnostic.SA1649.severity = silent +[DataHolderCDRArrangementRevocationService.{cs}] +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.SA1312.severity = silent +[DataHolderIntrospectionService.{cs}] +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.SA1312.severity = silent +[DcrResponse.{cs}] +dotnet_diagnostic.SA1300.severity = silent +[IDataHolderCDRArrangementRevocationService.{cs}] +dotnet_diagnostic.S101.severity = silent +[JwtValidatorTests.{cs}] +dotnet_diagnostic.SA1649.severity = silent +dotnet_diagnostic.SA1306.severity = silent +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.SA1300.severity = silent +[OpenIdConfiguration.{cs}] +dotnet_diagnostic.SA1300.severity = silent +[RequestObjectValidatorTests.{cs}] +dotnet_diagnostic.SA1649.severity = silent +[SkippableFactTestCase.{cs}] +dotnet_diagnostic.S1133.severity = silent +[SkippableTheoryTestCase.{cs}] +dotnet_diagnostic.S1133.severity = silent +[TokenRequestValidatorTests.{cs}] +dotnet_diagnostic.SA1649.severity = silent +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.SA1306.severity = silent +[US12678_CdrAuthServer_Authorisation.{cs}] +dotnet_diagnostic.SA1117.severity = silent +dotnet_diagnostic.S2589.severity = silent +dotnet_diagnostic.S1135.severity = silent +[US12962_CdrAuthServer_OIDC_Configuration.{cs}] +dotnet_diagnostic.SA1601.severity = silent +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.S3878.severity = silent +[US12963_CdrAuthServer_Token.{cs}] +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.SA1300.severity = silent +dotnet_diagnostic.SA1124.severity = silent +dotnet_diagnostic.SA1413.severity = silent +dotnet_diagnostic.S1135.severity = silent +dotnet_diagnostic.S1481.severity = silent +dotnet_diagnostic.SA1312.severity = silent +[US12964_CDRAuthServer_OIDC_JWKS.{cs}] +dotnet_diagnostic.SA1649.severity = silent +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.S3459.severity = silent +dotnet_diagnostic.SA1300.severity = silent +[US12965_CdrAuthServer_UserInfo.{cs}] +dotnet_diagnostic.S3459.severity = silent +dotnet_diagnostic.SA1300.severity = silent +dotnet_diagnostic.S1172.severity = silent +dotnet_diagnostic.S1144.severity = silent +[US12966_CdrAuthServer_Introspection.{cs}] +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.S3459.severity = silent +dotnet_diagnostic.SA1300.severity = silent +dotnet_diagnostic.S1144.severity = silent +[US12968_CdrAuthServer_PAR.{cs}] +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.S2589.severity = silent +[US15221_US12969_US15584_CdrAuthServer_Registration_POST.{cs}] +dotnet_diagnostic.S101.severity = silent +[US15221_US12969_US15585_CdrAuthServer_Registration_PUT.{cs}] +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.SA1117.severity = silent +[US15221_US12969_US15586_CdrAuthServer_Registration_GET.{cs}] +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.SA1117.severity = silent +dotnet_diagnostic.SA1509.severity = silent +dotnet_diagnostic.S1199.severity = silent +dotnet_diagnostic.S1135.severity = silent +[US15221_US12969_US15587_CdrAuthServer_Registration_DELETE.{cs}] +dotnet_diagnostic.S101.severity = silent +dotnet_diagnostic.SA1316.severity = silent +dotnet_diagnostic.SA1117.severity = silent +[US17652_CdrAuthServer_ArrangementRevocation.{cs}] +dotnet_diagnostic.S4487.severity = silent +dotnet_diagnostic.SA1509.severity = silent +dotnet_diagnostic.S2699.severity = silent +dotnet_diagnostic.S2589.severity = silent +[US28391_GetDataRecipients.{cs}] +dotnet_diagnostic.S1172.severity = silent +dotnet_diagnostic.SA1312.severity = silent +[US39327_CdrAuthServer_E2ETests.{cs}] +dotnet_diagnostic.S2933.severity = silent +dotnet_diagnostic.S1481.severity = silent +[DisplayTestMethodNameAttribute.{cs}] +dotnet_diagnostic.S3993.severity = silent +[HttpHelperTests.{cs}] +dotnet_diagnostic.SA1402.severity = silent +dotnet_diagnostic.SA1601.severity = silent +dotnet_diagnostic.S1144.severity = silent +[ParValidatorTest.{cs}] +dotnet_diagnostic.SA1300.severity = silent +[SkippableFactAttribute.{cs}] +dotnet_diagnostic.S3993.severity = silent +[SkippableFactMessageBus.{cs}] +dotnet_diagnostic.S3881.severity = silent +[SkippableTheoryAttribute.{cs}] +dotnet_diagnostic.S3993.severity = silent +[StringExtensionsTests.{cs}] +dotnet_diagnostic.S1186.severity = silent diff --git a/Source/CdrAuthServer.API.Logger/CdrAuthServer.API.Logger.csproj b/Source/CdrAuthServer.API.Logger/CdrAuthServer.API.Logger.csproj index 59e9d34..8de99f8 100644 --- a/Source/CdrAuthServer.API.Logger/CdrAuthServer.API.Logger.csproj +++ b/Source/CdrAuthServer.API.Logger/CdrAuthServer.API.Logger.csproj @@ -6,16 +6,27 @@ $(Version) $(Version) enable - enable + enable + True + - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/Source/CdrAuthServer.API.Logger/IRequestResponseLogger.cs b/Source/CdrAuthServer.API.Logger/IRequestResponseLogger.cs index b9b488f..3c30a86 100644 --- a/Source/CdrAuthServer.API.Logger/IRequestResponseLogger.cs +++ b/Source/CdrAuthServer.API.Logger/IRequestResponseLogger.cs @@ -6,4 +6,4 @@ public interface IRequestResponseLogger { ILogger Log { get; } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.API.Logger/LoggerExtensions.cs b/Source/CdrAuthServer.API.Logger/LoggerExtensions.cs index 8fa3c2a..d4f0a13 100644 --- a/Source/CdrAuthServer.API.Logger/LoggerExtensions.cs +++ b/Source/CdrAuthServer.API.Logger/LoggerExtensions.cs @@ -10,4 +10,4 @@ public static IServiceCollection AddRequestResponseLogging(this IServiceCollecti return services; } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.API.Logger/RequestResponseLogger.cs b/Source/CdrAuthServer.API.Logger/RequestResponseLogger.cs index 97b1aa8..d604013 100644 --- a/Source/CdrAuthServer.API.Logger/RequestResponseLogger.cs +++ b/Source/CdrAuthServer.API.Logger/RequestResponseLogger.cs @@ -1,33 +1,49 @@ namespace CdrAuthServer.API.Logger { - using System.Diagnostics; using Microsoft.Extensions.Configuration; using Serilog; using Serilog.Core; + using Serilog.Settings.Configuration; public class RequestResponseLogger : IRequestResponseLogger, IDisposable { private readonly Logger _logger; - public ILogger Log { get { return _logger; } } + + public ILogger Log + { + get { return _logger; } + } public RequestResponseLogger(IConfiguration configuration) { - _logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration, sectionName: "SerilogRequestResponseLogger") - .Enrich.WithProperty("RequestMethod", "") - .Enrich.WithProperty("RequestBody", "") - .Enrich.WithProperty("RequestHeaders", "") - .Enrich.WithProperty("RequestPath", "") - .Enrich.WithProperty("RequestQueryString", "") - .Enrich.WithProperty("StatusCode", "") - .Enrich.WithProperty("ElapsedTime", "") - .Enrich.WithProperty("ResponseHeaders", "") - .Enrich.WithProperty("ResponseBody", "") - .Enrich.WithProperty("RequestHost", "") - .Enrich.WithProperty("RequestIpAddress", "") - .Enrich.WithProperty("ClientId", "") - .Enrich.WithProperty("SoftwareId", "") - .Enrich.WithProperty("FapiInteractionId", "") + var loggerConfiguration = new LoggerConfiguration(); + + // If the Serilog response loggins is disabled, do not configure it using the appsettings. + var isSerilogRequestResponseLoggerDisabled = + configuration.GetValue("SerilogRequestResponseLogger:IsDisabled", false); + if (isSerilogRequestResponseLoggerDisabled) + { + _logger = loggerConfiguration.CreateLogger(); + return; + } + + var options = new ConfigurationReaderOptions { SectionName = "SerilogRequestResponseLogger" }; + _logger = loggerConfiguration + .ReadFrom.Configuration(configuration, options) + .Enrich.WithProperty("RequestMethod", string.Empty) + .Enrich.WithProperty("RequestBody", string.Empty) + .Enrich.WithProperty("RequestHeaders", string.Empty) + .Enrich.WithProperty("RequestPath", string.Empty) + .Enrich.WithProperty("RequestQueryString", string.Empty) + .Enrich.WithProperty("StatusCode", string.Empty) + .Enrich.WithProperty("ElapsedTime", string.Empty) + .Enrich.WithProperty("ResponseHeaders", string.Empty) + .Enrich.WithProperty("ResponseBody", string.Empty) + .Enrich.WithProperty("RequestHost", string.Empty) + .Enrich.WithProperty("RequestIpAddress", string.Empty) + .Enrich.WithProperty("ClientId", string.Empty) + .Enrich.WithProperty("SoftwareId", string.Empty) + .Enrich.WithProperty("FapiInteractionId", string.Empty) .CreateLogger(); } @@ -42,7 +58,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { Serilog.Log.CloseAndFlush(); - } + } } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.API.Logger/RequestResponseLoggingMiddleware.cs b/Source/CdrAuthServer.API.Logger/RequestResponseLoggingMiddleware.cs index 63c5615..4213413 100644 --- a/Source/CdrAuthServer.API.Logger/RequestResponseLoggingMiddleware.cs +++ b/Source/CdrAuthServer.API.Logger/RequestResponseLoggingMiddleware.cs @@ -14,12 +14,16 @@ namespace CdrAuthServer.API.Logger { public class RequestResponseLoggingMiddleware { - const string httpSummaryMessageTemplate = - "HTTP {RequestMethod} {RequestScheme:l}://{RequestHost:l}{RequestPathBase:l}{RequestPath:l} responded {StatusCode} in {ElapsedTime:0.0000} ms."; + private const string HttpSummaryMessageTemplate = + "HTTP {RequestMethod} {RequestScheme:l}://{RequestHost:l}{RequestPathBase:l}{RequestPath:l} responded {StatusCode} in {ElapsedTime:0.0000} ms."; - const string httpSummaryExceptionMessageTemplate = + private const string HttpSummaryExceptionMessageTemplate = "HTTP {RequestMethod} {RequestScheme:l}://{RequestHost:l}{RequestPathBase:l}{RequestPath:l} encountered following error {error}"; + private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; + private readonly RequestDelegate _next; + private readonly ILogger _requestResponseLogger; + private readonly IConfiguration _configuration; private string? _requestMethod; private string? _requestBody; private string? _requestHeaders; @@ -36,20 +40,13 @@ public class RequestResponseLoggingMiddleware private string? _requestPathBase; private string? _clientId; private string? _softwareId; - private string? _fapiInteractionId; - - - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - readonly RequestDelegate _next; - private readonly ILogger _requestResponseLogger; - private readonly IConfiguration _configuration; - + private string? _fapiInteractionId; public RequestResponseLoggingMiddleware(RequestDelegate next, IRequestResponseLogger requestResponseLogger, IConfiguration configuration) { _requestResponseLogger = requestResponseLogger.Log.ForContext(); _next = next ?? throw new ArgumentNullException(nameof(next)); - _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); + _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); _configuration = configuration; } @@ -87,15 +84,14 @@ private void LogWithContext() if (!string.IsNullOrEmpty(_exceptionMessage)) { - logger.Error(httpSummaryExceptionMessageTemplate, _requestMethod, _requestScheme, _requestHost, _requestPathBase, _requestPath, _exceptionMessage); + logger.Error(HttpSummaryExceptionMessageTemplate, _requestMethod, _requestScheme, _requestHost, _requestPathBase, _requestPath, _exceptionMessage); } else { - logger.Write(LogEventLevel.Information, httpSummaryMessageTemplate, _requestMethod, _requestScheme, _requestHost, _requestPathBase, _requestPath, _statusCode, _elapsedTime); + logger.Write(LogEventLevel.Information, HttpSummaryMessageTemplate, _requestMethod, _requestScheme, _requestHost, _requestPathBase, _requestPath, _statusCode, _elapsedTime); } } - private async Task ExtractRequestProperties(HttpContext context) { try @@ -126,7 +122,7 @@ private async Task ExtractRequestProperties(HttpContext context) } } - static class ClaimIdentifiers + private static class ClaimIdentifiers { public const string ClientId = "client_id"; public const string SoftwareId = "software_id"; @@ -139,7 +135,7 @@ private static void SetIdFromJwt(string jwt, string identifierType, ref string i if (handler.CanReadToken(jwt)) { var decodedJwt = handler.ReadJwtToken(jwt); - var id = decodedJwt.Claims.FirstOrDefault(x => x.Type == identifierType)?.Value ?? ""; + var id = decodedJwt.Claims.FirstOrDefault(x => x.Type == identifierType)?.Value ?? string.Empty; idToSet = id; } @@ -149,14 +145,14 @@ private void ExtractIdFromRequest(HttpRequest request) { try { - //try fetching x-fapi-interaction-id. After fetching we don't return as we need other important ids. + // try fetching x-fapi-interaction-id. After fetching we don't return as we need other important ids. _fapiInteractionId = string.Empty; if (request.Headers.TryGetValue("x-fapi-interaction-id", out var interactionid)) { _fapiInteractionId = interactionid; } - //try fetching from the JWT in the authorization header + // try fetching from the JWT in the authorization header var authorization = request.Headers[HeaderNames.Authorization]; if (AuthenticationHeaderValue.TryParse(authorization, out var headerValue) && string.IsNullOrEmpty(_clientId)) { @@ -165,12 +161,12 @@ private void ExtractIdFromRequest(HttpRequest request) if (scheme == JwtBearerDefaults.AuthenticationScheme && parameter != null) { - _clientId = String.Empty; + _clientId = string.Empty; SetIdFromJwt(parameter, ClaimIdentifiers.ClientId, ref _clientId); } } - //try fetching from the clientid in the body for connect/par + // try fetching from the clientid in the body for connect/par if (!string.IsNullOrEmpty(_requestBody) && _requestBody.Contains("client_assertion=") && string.IsNullOrEmpty(_clientId)) { var nameValueCollection = HttpUtility.ParseQueryString(_requestBody); @@ -181,14 +177,13 @@ private void ExtractIdFromRequest(HttpRequest request) if (assertion != null) { // in this case we set the iss to clientid - _clientId = String.Empty; + _clientId = string.Empty; SetIdFromJwt(assertion, ClaimIdentifiers.Iss, ref _clientId); } } - } - //try fetching from the clientid in the body account/login, /consent, /token + // try fetching from the clientid in the body account/login, /consent, /token if (!string.IsNullOrEmpty(_requestBody) && _requestBody.Contains(ClaimIdentifiers.ClientId) && string.IsNullOrEmpty(_clientId)) { var decodedBody = HttpUtility.UrlDecode(_requestBody); @@ -211,10 +206,9 @@ private void ExtractIdFromRequest(HttpRequest request) } } - if (request.ContentType == "application/jwt") { - //decode jwt sent to register + // decode jwt sent to register var handler = new JwtSecurityTokenHandler(); if (handler.CanReadToken(_requestBody)) { @@ -223,14 +217,14 @@ private void ExtractIdFromRequest(HttpRequest request) if (softStatementValue != null) { - _softwareId = String.Empty; + _softwareId = string.Empty; SetIdFromJwt(softStatementValue, ClaimIdentifiers.SoftwareId, ref _softwareId); return; } } } - //try fetching from query string, this should be the last place to check for client id. + // try fetching from query string, this should be the last place to check for client id. if (request.QueryString.Value?.Contains(ClaimIdentifiers.ClientId) == true && string.IsNullOrEmpty(_clientId)) { var nameValueCollection = HttpUtility.ParseQueryString(request.QueryString.Value); @@ -244,7 +238,6 @@ private void ExtractIdFromRequest(HttpRequest request) { _exceptionMessage = ex.Message; } - } private string ReadStreamInChunks(Stream stream) @@ -261,7 +254,8 @@ private string ReadStreamInChunks(Stream stream) { readChunkLength = reader.ReadBlock(readChunk, 0, readChunkBufferLength); textWriter.Write(readChunk, 0, readChunkLength); - } while (readChunkLength > 0); + } + while (readChunkLength > 0); return textWriter.ToString(); } catch (Exception ex) @@ -269,13 +263,11 @@ private string ReadStreamInChunks(Stream stream) _exceptionMessage = ex.Message; } - return ""; + return string.Empty; } - private async Task ExtractResponseProperties(HttpContext httpContext) { - var originalBodyStream = httpContext.Response.Body; await using var responseBody = _recyclableMemoryStreamManager.GetStream(); httpContext.Response.Body = responseBody; @@ -318,13 +310,13 @@ 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 string hostHeaderKey = _configuration.GetValue("SerilogRequestResponseLogger:HostNameHeaderKey") ?? "X-Forwarded-Host"; - + if (!request.Headers.TryGetValue(hostHeaderKey, out var keys)) { return request.Host.ToString(); } - return keys[0] ?? ""; + return keys[0] ?? string.Empty; } private string? GetIpAddress(HttpContext context) @@ -340,7 +332,7 @@ private string GetHost(HttpRequest request) // 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]? - .Split(',')[0] // Get the first IP address in the list, in case there are multiple. + .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. } @@ -348,19 +340,18 @@ private string GetSourceContext() { // local containers have ports with requesthost e.g. mock-data-holder:8001 // test environment has mock-data-holder - if (string.IsNullOrEmpty(_requestHost)) { return string.Empty; } - + if (_requestHost.Contains("mock-data-holder-energy")) { return "SB-DHE-ID"; } if (_requestHost.Contains("mock-data-holder")) - { + { return "SB-DHB-ID"; } diff --git a/Source/CdrAuthServer.Domain/CdrAuthServer.Domain.csproj b/Source/CdrAuthServer.Domain/CdrAuthServer.Domain.csproj index 298509a..fe2de7a 100644 --- a/Source/CdrAuthServer.Domain/CdrAuthServer.Domain.csproj +++ b/Source/CdrAuthServer.Domain/CdrAuthServer.Domain.csproj @@ -19,7 +19,8 @@ - + + diff --git a/Source/CdrAuthServer.Domain/Constants.cs b/Source/CdrAuthServer.Domain/Constants.cs index c900b12..3bb4f80 100644 --- a/Source/CdrAuthServer.Domain/Constants.cs +++ b/Source/CdrAuthServer.Domain/Constants.cs @@ -105,8 +105,6 @@ public static class ClientMetadata public const string ClientMetaDataResponseTypes = "response_types"; public const string ApplicationType = "application_type"; public const string IdTokenSignedResponseAlg = "id_token_signed_response_alg"; - public const string IdTokenEncryptedResponseAlg = "id_token_encrypted_response_alg"; - public const string IdTokenEncryptedResponseEnc = "id_token_encrypted_response_enc"; public const string RequestObjectSigningAlg = "request_object_signing_alg"; public const string SoftwareStatement = "software_statement"; public const string AuthorizationSignedResponseAlg = "authorization_signed_response_alg"; @@ -216,16 +214,11 @@ public static class GrantTypes public const string RefreshToken = "refresh_token"; public const string AuthCode = "authorization_code"; public const string ClientCredentials = "client_credentials"; - public const string Hybrid = "hybrid"; public const string RequestUri = "request_uri"; } public static class ResponseModes { - public const string FormPost = "form_post"; - public const string Fragment = "fragment"; - public const string FormPostJwt = "form_post.jwt"; - public const string FragmentJwt = "fragment.jwt"; public const string QueryJwt = "query.jwt"; public const string Jwt = "jwt"; } @@ -233,14 +226,12 @@ public static class ResponseModes public static class ResponseTypes { public const string AuthCode = "code"; - public const string Hybrid = "code id_token"; } // The order of the response modes represents the precedence for each response type. public static readonly ImmutableDictionary SupportedResponseModesForResponseType = new Dictionary { - { ResponseTypes.Hybrid, new string[] { ResponseModes.Fragment, ResponseModes.FormPost } }, - { ResponseTypes.AuthCode, new string[] { ResponseModes.QueryJwt, ResponseModes.FragmentJwt, ResponseModes.FormPostJwt, ResponseModes.Jwt } }, + { ResponseTypes.AuthCode, new string[] { ResponseModes.Jwt } }, }.ToImmutableDictionary(); public static class ValidationRestrictions diff --git a/Source/CdrAuthServer.Domain/Entities/Client.cs b/Source/CdrAuthServer.Domain/Entities/Client.cs index c3def05..07a0b86 100644 --- a/Source/CdrAuthServer.Domain/Entities/Client.cs +++ b/Source/CdrAuthServer.Domain/Entities/Client.cs @@ -141,13 +141,13 @@ public class Client /// Gets a JWE `alg` algorithm with which an id_token is to be encrypted. /// [JsonProperty("id_token_encrypted_response_alg")] - public string IdTokenEncryptedResponseAlg { get; set; } = string.Empty; + public string? IdTokenEncryptedResponseAlg { get; set; } /// /// Gets a JWE `enc` algorithm with which an id_token is to be encrypted. /// [JsonProperty("id_token_encrypted_response_enc")] - public string IdTokenEncryptedResponseEnc { get; set; } = string.Empty; + public string? IdTokenEncryptedResponseEnc { get; set; } /// /// Gets an algorithm which the ADR expects to sign the request object if a request object will be part of the authorization request sent to the Data Holder. @@ -189,7 +189,7 @@ public class Client /// Gets an algorithm with which an auth response is to be signed (JARM). /// [JsonProperty("authorization_signed_response_alg")] - public string? AuthorizationSignedResponseAlg { get; set; } = null; + public string? AuthorizationSignedResponseAlg { get; set; } = null; /// /// Gets a JWE `alg` algorithm with which an auth response is to be encrypted (JARM). @@ -203,4 +203,4 @@ public class Client [JsonProperty("authorization_encrypted_response_enc")] public string? AuthorizationEncryptedResponseEnc { get; set; } = null; } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.Domain/Entities/Grant.cs b/Source/CdrAuthServer.Domain/Entities/Grant.cs index 2ac4f4b..b7ae5ef 100644 --- a/Source/CdrAuthServer.Domain/Entities/Grant.cs +++ b/Source/CdrAuthServer.Domain/Entities/Grant.cs @@ -19,5 +19,5 @@ public class Grant public string Scope { get; set; } = string.Empty; public virtual IDictionary Data { get; set; } = new Dictionary(); - } + } } diff --git a/Source/CdrAuthServer.Domain/Enums/CdsErrors.cs b/Source/CdrAuthServer.Domain/Enums/CdsErrors.cs index a887334..dbae9ff 100644 --- a/Source/CdrAuthServer.Domain/Enums/CdsErrors.cs +++ b/Source/CdrAuthServer.Domain/Enums/CdsErrors.cs @@ -16,7 +16,7 @@ public enum CdsError [CdrError(Constants.ErrorTitles.ServiceUnavailable, Constants.ErrorCodes.Cds.ServiceUnavailable)] ServiceUnavailable, - [CdrError(Constants.ErrorTitles.MissingRequiredField,Constants.ErrorCodes.Cds.MissingRequiredField)] + [CdrError(Constants.ErrorTitles.MissingRequiredField, Constants.ErrorCodes.Cds.MissingRequiredField)] MissingRequiredField, [CdrError(Constants.ErrorTitles.MissingRequiredHeader, Constants.ErrorCodes.Cds.MissingRequiredHeader)] diff --git a/Source/CdrAuthServer.Domain/Models/ResponseErrorList.cs b/Source/CdrAuthServer.Domain/Models/ResponseErrorList.cs index eb92aba..cd1ef52 100644 --- a/Source/CdrAuthServer.Domain/Models/ResponseErrorList.cs +++ b/Source/CdrAuthServer.Domain/Models/ResponseErrorList.cs @@ -11,7 +11,7 @@ public class ResponseErrorList public bool HasErrors() { - return Errors != null && Errors.Count!=0; + return Errors != null && Errors.Count != 0; } public ResponseErrorList() diff --git a/Source/CdrAuthServer.E2ETests/BaseTest.cs b/Source/CdrAuthServer.E2ETests/BaseTest.cs index b72c4b2..8ffa65b 100644 --- a/Source/CdrAuthServer.E2ETests/BaseTest.cs +++ b/Source/CdrAuthServer.E2ETests/BaseTest.cs @@ -1,4 +1,4 @@ -#define TEST_DEBUG_MODE // Run Playwright in non-headless mode for debugging purposes (ie show a browser) +#define TEST_DEBUG_MODE // Run Playwright in non-headless mode for debugging purposes (ie show a browser) // In docker (Ubuntu container) Playwright will fail if running in non-headless mode, so we ensure TEST_DEBUG_MODE is undef'ed #if !DEBUG @@ -15,10 +15,11 @@ namespace CdrAuthServer.E2ETests // Put all tests in same collection because we need them to run sequentially since some tests are mutating DB. [Collection("E2ETests")] [TestCaseOrderer("CdrAuthServer.E2ETests.XUnit.Orderers.AlphabeticalOrderer", "CdrAuthServer.E2ETests")] - abstract public class BaseTest : SharedBaseTest + public abstract class BaseTest : SharedBaseTest { - protected BaseTest(ITestOutputHelperAccessor testOutputHelperAccessor, IConfiguration config) : base(testOutputHelperAccessor, config) + protected BaseTest(ITestOutputHelperAccessor testOutputHelperAccessor, IConfiguration config) + : base(testOutputHelperAccessor, config) { } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.E2ETests/CdrAuthServer.E2ETests.csproj b/Source/CdrAuthServer.E2ETests/CdrAuthServer.E2ETests.csproj index 37c00b7..1e826cf 100644 --- a/Source/CdrAuthServer.E2ETests/CdrAuthServer.E2ETests.csproj +++ b/Source/CdrAuthServer.E2ETests/CdrAuthServer.E2ETests.csproj @@ -8,6 +8,7 @@ Debug;Release;Shared enable enable + True @@ -29,7 +30,7 @@ - + @@ -40,6 +41,14 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Source/CdrAuthServer.E2ETests/Startup.cs b/Source/CdrAuthServer.E2ETests/Startup.cs index 1aa9a0b..2208229 100644 --- a/Source/CdrAuthServer.E2ETests/Startup.cs +++ b/Source/CdrAuthServer.E2ETests/Startup.cs @@ -17,14 +17,14 @@ public Startup(IConfiguration configuration) public static void ConfigureServices(IServiceCollection services) { - //Setup config + // Setup config var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true) .Build(); - //Setting up logger early so we can catch any startup issues + // Setting up logger early so we can catch any startup issues Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration: configuration) .CreateBootstrapLogger(); @@ -37,21 +37,23 @@ public static void ConfigureServices(IServiceCollection services) opt.INDUSTRY = Industry.BANKING; opt.SCOPE = ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Constants.Scopes.ScopeBanking; - opt.DH_MTLS_GATEWAY_URL = configuration["URL:DH_MTLS_Gateway"]??string.Empty; - opt.DH_TLS_AUTHSERVER_BASE_URL = configuration["URL:DH_TLS_AuthServer"]??string.Empty; + opt.DH_MTLS_GATEWAY_URL = configuration["URL:DH_MTLS_Gateway"] ?? string.Empty; + opt.DH_TLS_AUTHSERVER_BASE_URL = configuration["URL:DH_TLS_AuthServer"] ?? string.Empty; opt.DH_TLS_PUBLIC_BASE_URL = configuration["URL:DH_TLS_Public"] ?? string.Empty; opt.REGISTER_MTLS_URL = configuration["URL:Register_MTLS"] ?? string.Empty; + // Connection strings opt.DATAHOLDER_CONNECTIONSTRING = configuration["ConnectionStrings:DataHolder"] ?? string.Empty; opt.AUTHSERVER_CONNECTIONSTRING = configuration["ConnectionStrings:AuthServer"] ?? string.Empty; opt.REGISTER_CONNECTIONSTRING = configuration["ConnectionStrings:Register"] ?? string.Empty; + // Seed-data offset - opt.SEEDDATA_OFFSETDATES = (configuration["SeedData:OffsetDates"] == "true"); + opt.SEEDDATA_OFFSETDATES = configuration["SeedData:OffsetDates"] == "true"; opt.MDH_INTEGRATION_TESTS_HOST = configuration["URL:MDH_INTEGRATION_TESTS_HOST"] ?? string.Empty; opt.MDH_HOST = configuration["URL:MDH_HOST"] ?? string.Empty; - opt.CDRAUTHSERVER_SECUREBASEURI = configuration["URL:CDRAuthServer_SecureBaseUri"] ?? string.Empty; + opt.CDRAUTHSERVER_SECUREBASEURI = configuration["URL:CDRAuthServer_SecureBaseUri"] ?? string.Empty; // Playwright settings. opt.RUNNING_IN_CONTAINER = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")?.ToUpper() == "TRUE"; @@ -62,9 +64,9 @@ public static void ConfigureServices(IServiceCollection services) services.AddTestAutomationAuthServerSettings(opt => { - opt.CDRAUTHSERVER_BASEURI = configuration["URL:CDRAuthServer_BaseUri"] ?? string.Empty; ; + opt.CDRAUTHSERVER_BASEURI = configuration["URL:CDRAuthServer_BaseUri"] ?? string.Empty; opt.STANDALONE = configuration["Standalone"]!.ToUpper() == "TRUE"; - opt.XTLSCLIENTCERTTHUMBPRINT = configuration["XTlsClientCertThumbprint"]??string.Empty; ; + opt.XTLSCLIENTCERTTHUMBPRINT = configuration["XTlsClientCertThumbprint"] ?? string.Empty; opt.XTLSADDITIONALCLIENTCERTTHUMBPRINT = configuration["XTlsAdditionalClientCertThumbprint"] ?? string.Empty; opt.ACCESSTOKENLIFETIMESECONDS = Convert.ToInt32(configuration["AccessTokenLifetimeSeconds"]); }); diff --git a/Source/CdrAuthServer.E2ETests/US39327_CdrAuthServer_E2ETests.cs b/Source/CdrAuthServer.E2ETests/US39327_CdrAuthServer_E2ETests.cs index e52c91c..b3c21d6 100644 --- a/Source/CdrAuthServer.E2ETests/US39327_CdrAuthServer_E2ETests.cs +++ b/Source/CdrAuthServer.E2ETests/US39327_CdrAuthServer_E2ETests.cs @@ -1,4 +1,4 @@ -using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; @@ -34,8 +34,7 @@ public US39327_CdrAuthServer_E2ETests( IApiServiceDirector apiServiceDirector, ITestOutputHelperAccessor testOutputHelperAccessor, Microsoft.Extensions.Configuration.IConfiguration config) - : base(testOutputHelperAccessor, config - ) + : base(testOutputHelperAccessor, config) { _options = options.Value ?? throw new ArgumentNullException(nameof(options)); _authServerOptions = authServerOptions.Value ?? throw new ArgumentNullException(nameof(authServerOptions)); @@ -47,8 +46,8 @@ public US39327_CdrAuthServer_E2ETests( _apiServiceDirector = apiServiceDirector ?? throw new ArgumentNullException(nameof(apiServiceDirector)); } - PlaywrightDriver _playwrightDriver = new PlaywrightDriver(); - IBrowserContext? _browserContext; + private PlaywrightDriver _playwrightDriver = new PlaywrightDriver(); + private IBrowserContext? _browserContext; public const string CUSTOMERID_BANKING = "ksmith"; public const string DEFAULT_OPT = "000789"; @@ -62,59 +61,9 @@ public US39327_CdrAuthServer_E2ETests( private readonly IDataHolderAccessTokenCache _dataHolderAccessTokenCache; private readonly IApiServiceDirector _apiServiceDirector; - [Theory] - [InlineData(ResponseMode.FormPost)] - public async Task AC01_Authorize_HybridFlow_ShouldRespondWith_AuthCodeAndIdToken(ResponseMode responseMode) - { - Log.Information("Running test with Params: {P1}={V1}.", nameof(responseMode), responseMode); - - // Arrange - Uri authRedirect = await Authorise( - ResponseType.CodeIdToken, // ie Hybrid flow - responseMode, - _options.SCOPE); - var authRedirectLeftPart = authRedirect.GetLeftPart(UriPartial.Authority) + "/ui"; - - _browserContext = await _playwrightDriver.NewBrowserContext($"{nameof(AC01_Authorize_HybridFlow_ShouldRespondWith_AuthCodeAndIdToken)} - {responseMode}"); - - var page = await _browserContext.NewPageAsync(); - - await page.GotoAsync(authRedirect.AbsoluteUri); // redirect user to Auth UI to login and consent to share accounts - - // Username - AuthenticateLoginPage authenticateLoginPage = new(page); - await authenticateLoginPage.EnterCustomerId(CUSTOMERID_BANKING); - await authenticateLoginPage.ClickContinue(); - - // OTP - OneTimePasswordPage oneTimePasswordPage = new(page); - await oneTimePasswordPage.EnterOtp(DEFAULT_OPT); - await oneTimePasswordPage.ClickContinue(); - - // Select accounts - SelectAccountsPage selectAccountsPage = new(page); - await selectAccountsPage.SelectAccounts("Personal Loan"); - await selectAccountsPage.ClickContinue(); - - // Confirmation - Click authorise and check callback response - ConfirmAccountSharingPage confirmAccountSharingPage = new(page); - (string? code, string? idtoken) = await HybridFlow_HandleCallback(responseMode: responseMode, page: page, setup: async (page) => - { - await confirmAccountSharingPage.ClickAuthorise(); - }); - - using (new AssertionScope(BaseTestAssertionStrategy)) - { - code.Should().NotBeNullOrEmpty(); - idtoken.Should().NotBeNullOrEmpty(); - } - - } - [Fact] public async Task AC01_Authorize_CodeFlow_ShouldRespondWith_AuthCodeAndIdToken() { - // Arrange Uri authRedirect = await Authorise( ResponseType.Code, // ie Code flow @@ -168,7 +117,6 @@ public async Task AC01_Authorize_CodeFlow_ShouldRespondWith_AuthCodeAndIdToken() tokenResponse?.AccessToken.Should().NotBeNullOrEmpty(); tokenResponse?.IdToken.Should().NotBeNullOrEmpty(); tokenResponse?.RefreshToken.Should().NotBeNullOrEmpty(); - } } @@ -180,7 +128,7 @@ public async Task ACX02_Cancel_Otp_And_Verify_Callback() _browserContext = await _playwrightDriver.NewBrowserContext($"{nameof(ACX02_Cancel_Otp_And_Verify_Callback)}"); var page = await _browserContext.NewPageAsync(); - // Act + // Act await page.GotoAsync(authRedirect.AbsoluteUri); // redirect user to Auth UI to login and consent to share accounts AuthenticateLoginPage authenticateLoginPage = new(page); @@ -192,7 +140,6 @@ public async Task ACX02_Cancel_Otp_And_Verify_Callback() // Assert await AssertCancelCallback(async () => await oneTimePasswordPage.ClickCancel()); - } [Fact] @@ -203,7 +150,7 @@ public async Task ACX03_Cancel_Select_Accounts_And_Verify_Callback() _browserContext = await _playwrightDriver.NewBrowserContext($"{nameof(ACX03_Cancel_Select_Accounts_And_Verify_Callback)}"); var page = await _browserContext.NewPageAsync(); - // Act + // Act await page.GotoAsync(authRedirect.AbsoluteUri); // redirect user to Auth UI to login and consent to share accounts AuthenticateLoginPage authenticateLoginPage = new(page); @@ -219,7 +166,6 @@ public async Task ACX03_Cancel_Select_Accounts_And_Verify_Callback() // Assert await AssertCancelCallback(async () => await selectAccountsPage.ClickCancel()); - } [Fact] @@ -230,7 +176,7 @@ public async Task ACX04_Deny_Account_Confirmation_And_Verify_Callback() _browserContext = await _playwrightDriver.NewBrowserContext($"{nameof(ACX04_Deny_Account_Confirmation_And_Verify_Callback)}"); var page = await _browserContext.NewPageAsync(); - // Act + // Act await page.GotoAsync(authRedirect.AbsoluteUri); // redirect user to Auth UI to login and consent to share accounts AuthenticateLoginPage authenticateLoginPage = new(page); @@ -249,7 +195,6 @@ public async Task ACX04_Deny_Account_Confirmation_And_Verify_Callback() // Assert await AssertCancelCallback(async () => await confirmAccountSharingPage.ClickDeny()); - } [Theory] @@ -260,11 +205,11 @@ public async Task ACX05_Invalid_Customer_Id(string testSuffix, string customerId Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(testSuffix), testSuffix, nameof(customerIdToEnter), customerIdToEnter, nameof(expectedError), expectedError); // Arrange - Uri authRedirect = await Authorise(ResponseType.CodeIdToken, ResponseMode.FormPost, _options.SCOPE); + Uri authRedirect = await Authorise(ResponseType.Code, ResponseMode.Jwt, _options.SCOPE); _browserContext = await _playwrightDriver.NewBrowserContext($"{nameof(ACX05_Invalid_Customer_Id)} - {testSuffix}"); var page = await _browserContext.NewPageAsync(); - // Act + // Act await page.GotoAsync(authRedirect.AbsoluteUri); // redirect user to Auth UI to login and consent to share accounts AuthenticateLoginPage authenticateLoginPage = new(page); @@ -277,7 +222,6 @@ public async Task ACX05_Invalid_Customer_Id(string testSuffix, string customerId { invalidCustomerIdMessageExists.Should().Be(true, $"Customer Id of '{customerIdToEnter}' was entered and expected '{expectedError}'."); } - } [Theory] @@ -286,12 +230,13 @@ public async Task ACX05_Invalid_Customer_Id(string testSuffix, string customerId public async Task ACX06_Invalid_One_Time_Password(string testSuffix, string otpToEnter, string expectedError) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(testSuffix), testSuffix, nameof(otpToEnter), otpToEnter, nameof(expectedError), expectedError); + // Arrange - Uri authRedirect = await Authorise(ResponseType.CodeIdToken, ResponseMode.FormPost, _options.SCOPE); + Uri authRedirect = await Authorise(ResponseType.Code, ResponseMode.Jwt, _options.SCOPE); _browserContext = await _playwrightDriver.NewBrowserContext($"{nameof(ACX06_Invalid_One_Time_Password)} - {testSuffix}"); var page = await _browserContext.NewPageAsync(); - // Act + // Act await page.GotoAsync(authRedirect.AbsoluteUri); // redirect user to Auth UI to login and consent to share accounts AuthenticateLoginPage authenticateLoginPage = new(page); @@ -308,18 +253,17 @@ public async Task ACX06_Invalid_One_Time_Password(string testSuffix, string otpT { invalidOptMessageExists.Should().Be(true, $"OTP of '{otpToEnter}' was entered and expected '{expectedError}'."); } - } [Fact] public async Task ACX07_No_Accounts_Selected() { // Arrange - Uri authRedirect = await Authorise(ResponseType.CodeIdToken, ResponseMode.FormPost, _options.SCOPE); + Uri authRedirect = await Authorise(ResponseType.Code, ResponseMode.Jwt, _options.SCOPE); _browserContext = await _playwrightDriver.NewBrowserContext($"{nameof(ACX06_Invalid_One_Time_Password)}"); var page = await _browserContext.NewPageAsync(); - // Act + // Act await page.GotoAsync(authRedirect.AbsoluteUri); // redirect user to Auth UI to login and consent to share accounts AuthenticateLoginPage authenticateLoginPage = new(page); @@ -339,47 +283,82 @@ public async Task ACX07_No_Accounts_Selected() { noAccountSelectedErrorExists.Should().Be(true, $"No accounts were selected in the account selection form."); } - } // The test below aims to cover all Common, Banking and Energy scopes. Including the merging of Basic and Detailed clusters. [Theory] - //Common - [InlineData("AC01-3AU.02.14 Common Basic with Profile", "openid profile common:customer.basic:read cdr:registration", + + // Common + [InlineData( + "AC01-3AU.02.14 Common Basic with Profile", + "openid profile common:customer.basic:read cdr:registration", new ClusterType[] { ClusterType.CommonName, ClusterType.CommonNameAndOccupation })] - [InlineData("AC02-3AU.02.14 Common Basic with Detailed Common", "openid common:customer.basic:read common:customer.detail:read cdr:registration", + [InlineData( + "AC02-3AU.02.14 Common Basic with Detailed Common", + "openid common:customer.basic:read common:customer.detail:read cdr:registration", new ClusterType[] { ClusterType.CommonNameAndOccupation, ClusterType.CommonContactDetails })] - [InlineData("AC03-3AU.02.14 Common Detailed Common Only", "openid common:customer.detail:read cdr:registration", + [InlineData( + "AC03-3AU.02.14 Common Detailed Common Only", + "openid common:customer.detail:read cdr:registration", new ClusterType[] { ClusterType.CommonNameOccupationAndContactDetails })] - //Banking - [InlineData("AC04-3AU.02.14 Bank Detailed with Bank Basic", "openid bank:accounts.detail:read bank:accounts.basic:read", + + // Banking + [InlineData( + "AC04-3AU.02.14 Bank Detailed with Bank Basic", + "openid bank:accounts.detail:read bank:accounts.basic:read", new ClusterType[] { ClusterType.BankAccountNameTypeAndBalance, ClusterType.BankAccountNumbersAndFeatures })] - [InlineData("AC05-3AU.02.14 Bank Detailed Only", "openid bank:accounts.detail:read", + [InlineData( + "AC05-3AU.02.14 Bank Detailed Only", + "openid bank:accounts.detail:read", new ClusterType[] { ClusterType.BankAccountBalanceAndDetails })] - [InlineData("AC06-3AU.02.14 Bank Detailed with Bank Transations", "openid bank:accounts.detail:read bank:transactions:read cdr:registration", - new ClusterType[] { ClusterType.BankAccountBalanceAndDetails, - ClusterType.BankTransactionDetails})] - [InlineData("AC07-3AU.02.14", "openid bank:regular_payments:read", + [InlineData( + "AC06-3AU.02.14 Bank Detailed with Bank Transations", + "openid bank:accounts.detail:read bank:transactions:read cdr:registration", + new ClusterType[] + { + ClusterType.BankAccountBalanceAndDetails, + ClusterType.BankTransactionDetails, + })] + [InlineData( + "AC07-3AU.02.14", + "openid bank:regular_payments:read", new ClusterType[] { ClusterType.BankDirectDebitAndSheduledPayments })] - [InlineData("AC08-3AU.02.14", "openid bank:payees:read", + [InlineData( + "AC08-3AU.02.14", + "openid bank:payees:read", new ClusterType[] { ClusterType.BankSavedPayees })] - [InlineData("AC09-3AU.02.14 all bank", "openid bank:accounts.basic:read bank:accounts.detail:read bank:transactions:read bank:regular_payments:read bank:payees:read", - new ClusterType[] { ClusterType.BankAccountNameTypeAndBalance, + [InlineData( + "AC09-3AU.02.14 all bank", + "openid bank:accounts.basic:read bank:accounts.detail:read bank:transactions:read bank:regular_payments:read bank:payees:read", + new ClusterType[] + { + ClusterType.BankAccountNameTypeAndBalance, ClusterType.BankAccountNumbersAndFeatures, ClusterType.BankTransactionDetails, ClusterType.BankDirectDebitAndSheduledPayments, - ClusterType.BankSavedPayees})] - //Energy - [InlineData("AC10-3AU.02.14 Energy Basic with detailed", "openid energy:accounts.basic:read openid energy:accounts.detail:read", + ClusterType.BankSavedPayees, + })] + + // Energy + [InlineData( + "AC10-3AU.02.14 Energy Basic with detailed", + "openid energy:accounts.basic:read openid energy:accounts.detail:read", new ClusterType[] { ClusterType.EnergyAccountsAndPlans, ClusterType.EnergyAccountAndPlanDetailsWithBasicScope })] - [InlineData("AC11-3AU.02.14 Energy Detailed Only", "openid energy:accounts.detail:read", + [InlineData( + "AC11-3AU.02.14 Energy Detailed Only", + "openid energy:accounts.detail:read", new ClusterType[] { ClusterType.EnergyAccountAndPlanDetailsWithoutBasicScope })] - [InlineData("AC12-3AU.02.14 Energy connection and meter", "openid energy:electricity.servicepoints.basic:read energy:electricity.servicepoints.detail:read", + [InlineData( + "AC12-3AU.02.14 Energy connection and meter", + "openid energy:electricity.servicepoints.basic:read energy:electricity.servicepoints.detail:read", new ClusterType[] { ClusterType.EnergyElectricityConnection, ClusterType.EnergyElectricityMeter })] - [InlineData("AC13-3AU.02.14 Energy Consessions-Payments-Billing", + [InlineData( + "AC13-3AU.02.14 Energy Consessions-Payments-Billing", "openid energy:accounts.concessions:read energy:accounts.paymentschedule:read energy:billing:read", new ClusterType[] { ClusterType.EnergyConcessionsAndAssistance, ClusterType.EnergyPaymentPreferences, ClusterType.EnergyEnergyBillingPaymentsAndHistory })] - [InlineData("AC14-3AU.02.14 Energy DER-Usage", "openid energy:electricity.der:read energy:electricity.usage:read", + [InlineData( + "AC14-3AU.02.14 Energy DER-Usage", + "openid energy:electricity.der:read energy:electricity.usage:read", new ClusterType[] { ClusterType.EnergyEnergyGenerationAndStorage, ClusterType.EnergyElectricityUsage })] public async Task ACX08_Confirmation_UI_Cluster_Verification(string testSuffix, string actualScope, ClusterType[] expectedClusters) @@ -387,12 +366,11 @@ public async Task ACX08_Confirmation_UI_Cluster_Verification(string testSuffix, Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(testSuffix), testSuffix, nameof(actualScope), actualScope, nameof(expectedClusters), expectedClusters); // Arrange - Uri authRedirect = await Authorise(ResponseType.CodeIdToken, ResponseMode.FormPost, scope: actualScope); + Uri authRedirect = await Authorise(ResponseType.Code, ResponseMode.Jwt, scope: actualScope); _browserContext = await _playwrightDriver.NewBrowserContext($"{nameof(ACX08_Confirmation_UI_Cluster_Verification)} - {testSuffix}"); var page = await _browserContext.NewPageAsync(); - // Act - + // Act await page.GotoAsync(authRedirect.AbsoluteUri); // redirect user to Auth UI to login and consent to share accounts AuthenticateLoginPage authenticateLoginPage = new(page); @@ -410,7 +388,7 @@ public async Task ACX08_Confirmation_UI_Cluster_Verification(string testSuffix, ConfirmAccountSharingPage confirmAccountSharingPage = new(page); - //Assert + // Assert foreach (ClusterType c in expectedClusters) { await VerifyCluster(c, confirmAccountSharingPage); @@ -433,7 +411,7 @@ internal static async Task VerifyCluster(ClusterType clusterType, ConfirmAccount // Remove newline chars from actual and expected to ensure consistency in results. // When running in a docker container, the last element does not always have a newline delimiter. - Assert.Equal(expectedDetail.Replace("\n", ""), actualClusterDetail.Replace("\n", "")); + Assert.Equal(expectedDetail.Replace("\n", string.Empty), actualClusterDetail.Replace("\n", string.Empty)); } public enum ClusterType @@ -458,7 +436,7 @@ public enum ClusterType EnergyElectricityUsage, CommonNameAndOccupation, CommonContactDetails, - CommonNameOccupationAndContactDetails + CommonNameOccupationAndContactDetails, } // Call authorise endpoint, should respond with a redirect to UI, return the redirect URI @@ -491,62 +469,8 @@ private async Task Authorise(ResponseType responseType, ResponseMode respon return response.Headers.Location ?? throw new NullReferenceException(nameof(response.Headers.Location.AbsoluteUri)); } - delegate Task HybridFlow_HandleCallback_Setup(IPage page); - private async Task<(string? code, string? idtoken)> HybridFlow_HandleCallback(ResponseMode responseMode, IPage page, HybridFlow_HandleCallback_Setup setup) - { - var callback = new DataRecipientConsentCallback(_options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS); - callback.Start(); - try - { - await setup(page); - - var callbackRequest = await callback.WaitForCallback(); - using (new AssertionScope(BaseTestAssertionStrategy)) - { - switch (responseMode) - { - case ResponseMode.FormPost: - { - callbackRequest.Should().NotBeNull(); - callbackRequest?.received.Should().BeTrue(); - callbackRequest?.method.Should().Be(HttpMethod.Post); - callbackRequest?.body.Should().NotBeNullOrEmpty(); - - var body = QueryHelpers.ParseQuery(callbackRequest?.body); - var code = body["code"]; - var id_token = body["id_token"]; - - code.Should().NotBeNullOrEmpty(); - id_token.Should().NotBeNullOrEmpty(); - - return (code, id_token); - } - case ResponseMode.Fragment: - { - callbackRequest.Should().NotBeNull(); - callbackRequest?.received.Should().BeTrue(); - callbackRequest?.method.Should().Be(HttpMethod.Get); - throw new NotImplementedException("FIXME - MJS - check request URL fragment for authcode & idtoken"); - } - case ResponseMode.Query: - { - callbackRequest.Should().NotBeNull(); - callbackRequest?.received.Should().BeTrue(); - callbackRequest?.method.Should().Be(HttpMethod.Get); - throw new NotImplementedException("FIXME - MJS - check request URL query string for authcode & idtoken"); - } - default: - throw new NotSupportedException(nameof(responseMode)); - } - } - } - finally - { - await callback.Stop(); - } - } + private delegate Task CodeFlow_HandleCallback_Setup(IPage page); - delegate Task CodeFlow_HandleCallback_Setup(IPage page); private async Task CodeFlow_HandleCallback(IPage page, CodeFlow_HandleCallback_Setup setup) { var callback = new DataRecipientConsentCallback(_options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS); @@ -570,8 +494,7 @@ private async Task Authorise(ResponseType responseType, ResponseMode respon var jwt = new JwtSecurityTokenHandler().ReadJwtToken(encodedJwt); jwt.Claim("code").Should().NotBeNull(); - return (jwt.Claim("code").Value); - + return jwt.Claim("code").Value; } } finally @@ -580,10 +503,10 @@ private async Task Authorise(ResponseType responseType, ResponseMode respon } } - delegate Task CancelAction(); + private delegate Task CancelAction(); + private async Task AssertCancelCallback(CancelAction cancelAction) { - var expectedError = new UserCancelledException(); var callback = new DataRecipientConsentCallback(_options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS); @@ -605,10 +528,9 @@ private async Task AssertCancelCallback(CancelAction cancelAction) var encodedJwt = queryValues["response"]; // Check claims of decode jwt - var jwt = new JwtSecurityTokenHandler().ReadJwtToken(encodedJwt); + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(encodedJwt); jwt.Claim("error").Value.Should().Be(expectedError.Error); jwt.Claim("error_description").Value.Should().Be(expectedError.ErrorDescription); - } } finally @@ -616,7 +538,8 @@ private async Task AssertCancelCallback(CancelAction cancelAction) await callback.Stop(); } } - public static (string expectedHeading, string expectedDetail) GetExpectedClusterDetail(ClusterType scopesType) + + public static (string ExpectedHeading, string ExpectedDetail) GetExpectedClusterDetail(ClusterType scopesType) { switch (scopesType) { diff --git a/Source/CdrAuthServer.E2ETests/XUnit/Orderers/AlphabeticalOrderer.cs b/Source/CdrAuthServer.E2ETests/XUnit/Orderers/AlphabeticalOrderer.cs index 9db967c..e46cdba 100644 --- a/Source/CdrAuthServer.E2ETests/XUnit/Orderers/AlphabeticalOrderer.cs +++ b/Source/CdrAuthServer.E2ETests/XUnit/Orderers/AlphabeticalOrderer.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Xunit.Abstractions; using Xunit.Sdk; @@ -7,7 +7,9 @@ namespace CdrAuthServer.E2ETests.XUnit.Orderers { public class AlphabeticalOrderer : ITestCaseOrderer { - public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase => + public IEnumerable OrderTestCases(IEnumerable testCases) + where TTestCase : ITestCase + => testCases.OrderBy(testCase => testCase.TestMethod.Method.Name); } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/BaseTest.cs b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/BaseTest.cs index 13fcf5d..b22ebd6 100644 --- a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/BaseTest.cs +++ b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/BaseTest.cs @@ -1,63 +1,45 @@ -#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON +#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON #nullable enable -using Microsoft.Extensions.Configuration; using System; +using System.Configuration; using System.IO; -using System.Reflection; -using Xunit; -using Xunit.Sdk; - using CdrAuthServer.GetDataRecipients.IntegrationTests.Fixtures; -using System.Configuration; +using Microsoft.Extensions.Configuration; +using Xunit; namespace CdrAuthServer.GetDataRecipients.IntegrationTests { - class DisplayTestMethodNameAttribute : BeforeAfterTestAttribute - { - static int count = 0; - - public override void Before(MethodInfo methodUnderTest) - { - Console.WriteLine($"Test #{++count} - {methodUnderTest.DeclaringType?.Name}.{methodUnderTest.Name}"); - } - - public override void After(MethodInfo methodUnderTest) - { - } - } - // Put all tests in same collection because we need them to run sequentially since some tests are mutating DB. [Collection("IntegrationTests")] [TestCaseOrderer("CdrAuthServer.GetDataRecipients.IntegrationTests.XUnit.Orderers.AlphabeticalOrderer", "CdrAuthServer.GetDataRecipients.IntegrationTests")] [DisplayTestMethodName] - abstract public class BaseTest : IClassFixture - { + public abstract class BaseTest : IClassFixture + { public static string AZUREFUNCTIONS_URL => Configuration["URL:AZUREFUNCTIONS"] ?? throw new ConfigurationErrorsException($"{nameof(AZUREFUNCTIONS_URL)} - configuration setting not found"); - static public string CONNECTIONSTRING_REGISTER_RW => + public static string CONNECTIONSTRING_REGISTER_RW => ConnectionStringCheck.Check(Configuration.GetConnectionString("Register_RW")); - static public string CONNECTIONSTRING_AUTHSERVER_RW => + + public static string CONNECTIONSTRING_AUTHSERVER_RW => ConnectionStringCheck.Check(Configuration.GetConnectionString("CdrAuthServer_DB_RW")); - static private IConfigurationRoot? configuration; - static public IConfigurationRoot Configuration + private static IConfigurationRoot? configuration; + + public static IConfigurationRoot Configuration { get { - if (configuration == null) - { - configuration = new ConfigurationBuilder() + configuration ??= new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true) .Build(); - } return configuration; } } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/CdrAuthServer.GetDataRecipients.IntegrationTests.csproj b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/CdrAuthServer.GetDataRecipients.IntegrationTests.csproj index 6460646..ef33b24 100644 --- a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/CdrAuthServer.GetDataRecipients.IntegrationTests.csproj +++ b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/CdrAuthServer.GetDataRecipients.IntegrationTests.csproj @@ -4,7 +4,8 @@ $(Version) $(Version) $(Version) - false + false + True @@ -22,7 +23,16 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/ConnectionStringCheck.cs b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/ConnectionStringCheck.cs index 36a8d43..55e4e5e 100644 --- a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/ConnectionStringCheck.cs +++ b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/ConnectionStringCheck.cs @@ -1,24 +1,25 @@ -#nullable enable +#nullable enable using System; -using Xunit; -using FluentAssertions.Execution; using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; namespace CdrAuthServer.GetDataRecipients.IntegrationTests { - static public class ConnectionStringCheck + public static class ConnectionStringCheck { internal const string PRODUCTION_SERVER = "sql-cdrsandbox-prod.database.windows.net"; // TODO - MJS - Whitelist would be better since if production database ever changes server this blacklist will fail unless someone remembers to update - static readonly string[] Blacklist = new string[] { - PRODUCTION_SERVER - }; + private static readonly string[] Blacklist = + [ + PRODUCTION_SERVER, + ]; - static public string Check(string? connectionString) + public static string Check(string? connectionString) { - if (!String.IsNullOrEmpty(connectionString)) + if (!string.IsNullOrEmpty(connectionString)) { // Reject if blacklisted string found in connectionString foreach (string blacklisted in Blacklist) @@ -30,17 +31,17 @@ static public string Check(string? connectionString) } } - return connectionString ?? ""; + return connectionString ?? string.Empty; } } public class ConnectionStringCheckUnitTests { - const string PRODUCTION_SERVER_FOO = "foo" + ConnectionStringCheck.PRODUCTION_SERVER + "foo"; // blacklist is checking for substrings, so surround with "foo" to ensure we are testing this + private const string PRODUCTION_SERVER_FOO = "foo" + ConnectionStringCheck.PRODUCTION_SERVER + "foo"; // blacklist is checking for substrings, so surround with "foo" to ensure we are testing this [Theory] - [InlineData(PRODUCTION_SERVER_FOO)] - [InlineData(PRODUCTION_SERVER_FOO, true)] + [InlineData(PRODUCTION_SERVER_FOO)] + [InlineData(PRODUCTION_SERVER_FOO, true)] public void WhenOnBlackList_ShouldThrowException(string connectionString, bool? uppercase = false) { if (uppercase == true) @@ -77,9 +78,9 @@ public void WhenNotOnBlackList_ShouldNotThrowException(string? connectionString) using (new AssertionScope()) { act.Should().NotThrow(); - returnedConnectionString?.Should().Be(connectionString ?? ""); + returnedConnectionString?.Should().Be(connectionString ?? string.Empty); } } } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/DatabaseSeeder.cs b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/DatabaseSeeder.cs index c167d04..447bc19 100644 --- a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/DatabaseSeeder.cs +++ b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/DatabaseSeeder.cs @@ -1,16 +1,26 @@ -using System; +using System; using System.Threading.Tasks; -using Microsoft.Data.SqlClient; using Dapper; +using Microsoft.Data.SqlClient; #nullable enable namespace CdrAuthServer.GetDataRecipients.IntegrationTests { - static public class DatabaseSeeder + public static class DatabaseSeeder { - private enum IndustryType { Banking, Energy, Telecommunications } - private enum ParticipantType { DH, DR } + private enum IndustryType + { + Banking, + Energy, + Telecommunications, + } + + private enum ParticipantType + { + DH, + DR, + } private static int nextRegisterLegalEntityId = 0; private static int nextRegisterParticipationId = 0; @@ -21,12 +31,11 @@ private enum ParticipantType { DH, DR } private static int nextAuthServerBrandId = 0; private static int nextAuthServerSoftwareProductId = 0; - static public async Task Execute( + public static async Task Execute( int registerLegalEntityCount, int registerBrandCount, int registerSoftwareProductCount, int authServerLegalEntityCount, int authServerBrandCount, int authServerSoftwareProductCount, bool registerModified, // simulate change to register records - bool authServerModified // simulate change to dataholder records - ) + bool authServerModified) // simulate change to dataholder records { // Database is purged so, reset next ids so that ids are consistent across tests nextRegisterLegalEntityId = 0; @@ -39,13 +48,13 @@ bool authServerModified // simulate change to dataholder records // Seed Register using var registerConnection = new SqlConnection(BaseTest.CONNECTIONSTRING_REGISTER_RW); - registerConnection.Open(); + await registerConnection.OpenAsync(); await RegisterPurge(registerConnection); await RegisterInsert(registerConnection, registerLegalEntityCount, registerBrandCount, registerSoftwareProductCount, registerModified); // Seed Software Products using var authServerConnection = new SqlConnection(BaseTest.CONNECTIONSTRING_AUTHSERVER_RW); - authServerConnection.Open(); + await authServerConnection.OpenAsync(); await AuthServerPurge(authServerConnection); await AuthServerInsert(authServerConnection, authServerLegalEntityCount, authServerBrandCount, authServerSoftwareProductCount, authServerModified); } @@ -64,8 +73,8 @@ private static async Task RegisterPurge(SqlConnection connection) // Purge Auth Server database but leave standing data intact private static async Task AuthServerPurge(SqlConnection connection) - { - await connection.ExecuteAsync("delete SoftwareProducts"); + { + await connection.ExecuteAsync("delete SoftwareProducts"); } private static async Task RegisterInsert(SqlConnection connection, int legalEntityCount, int brandCount, int softwareProductCount, bool modified) @@ -75,28 +84,31 @@ static async Task Register_InsertLegalEntity(SqlConnection connection, Ind var legalEntityId = new Guid($"00000000-0000-0000-0000-{++nextRegisterLegalEntityId:d012}"); string legalEntityName = $"LegalEntity_{legalEntityId}".ToString().Replace('-', '_'); - - await connection.ExecuteScalarAsync(@" + + await connection.ExecuteScalarAsync( + @" insert into LegalEntity(LegalEntityId, LegalEntityName, LogoUri, AnzsicDivision, OrganisationTypeId, AccreditationLevelId, AccreditationNumber) values(@LegalEntityId, @LegalEntityName, @LogoUri, @AnzsicDivision, @OrganisationTypeId, @AccreditationLevelId, @AccreditationNumber)", new { - LegalEntityId = legalEntityId, + LegalEntityId = legalEntityId, + // MA LegalEntityName = legalEntityName, LogoUri = $"https://www.{legalEntityName}.com/logo.jpg", - + AnzsicDivision = industryType switch { IndustryType.Banking => "6221", IndustryType.Energy => "2640", IndustryType.Telecommunications => "5801", - _ => throw new NotSupportedException() + _ => throw new NotSupportedException(), }, - OrganisationTypeId = "2", // company - // LegalEntityStatusId = "1", // make it active by default + OrganisationTypeId = "2", // company + + // LegalEntityStatusId = "1", // make it active by default AccreditationLevelId = "1", // unrestricted - AccreditationNumber = $"ABC{nextRegisterLegalEntityId:d012}" + AccreditationNumber = $"ABC{nextRegisterLegalEntityId:d012}", }); return legalEntityId; @@ -106,7 +118,8 @@ static async Task Register_InsertParticipation(SqlConnection connection, G { var participationId = new Guid($"00000000-0000-0000-0000-{++nextRegisterParticipationId:d012}"); - await connection.ExecuteScalarAsync(@" + await connection.ExecuteScalarAsync( + @" insert into Participation(ParticipationId, LegalEntityId, ParticipationTypeId, IndustryId, StatusId) values(@ParticipationId, @LegalEntityId, (select ParticipationTypeId from ParticipationType where ParticipationTypeCode = @ParticipantTypeCode), @@ -116,11 +129,12 @@ insert into Participation(ParticipationId, LegalEntityId, ParticipationTypeId, I { ParticipationId = participationId, LegalEntityId = legalEntityId, - ParticipantTypeCode = participantType.ToString(), + ParticipantTypeCode = participantType.ToString(), + // MA ParticipationStatusCode = "ACTIVE", - IndustryTypeCode = industryType.ToString() + IndustryTypeCode = industryType.ToString(), }); return participationId; @@ -132,7 +146,8 @@ static async Task Register_InsertBrand(SqlConnection connection, Guid part string brandName = $"Brand_{brandId}".ToString().Replace('-', '_'); - await connection.ExecuteScalarAsync(@" + await connection.ExecuteScalarAsync( + @" insert into Brand(BrandId, BrandName, LogoUri, BrandStatusId, ParticipationId, LastUpdated) values(@BrandId, @BrandName, @LogoUri, --(select BrandStatusId from BrandStatus where Upper(BrandStatusCode) = 'ACTIVE'), @@ -141,14 +156,15 @@ insert into Brand(BrandId, BrandName, LogoUri, BrandStatusId, ParticipationId, L @LastUpdated)", new { - BrandId = brandId, - // MA + BrandId = brandId, + + // MA BrandName = brandName, LogoUri = $"https://www.{brandName}.com/logo.jpg", StatusId = "1", // 1=active, 2=inactive ParticipationId = participationId, - LastUpdated = DateTime.UtcNow + LastUpdated = DateTime.UtcNow, }); return brandId; @@ -160,7 +176,8 @@ static async Task Register_InsertSoftwareProduct(SqlConnection connection, string softwareProductName = $"SoftwareProduct_{softwareProductId}".ToString().Replace('-', '_'); - await connection.ExecuteScalarAsync(@" + await connection.ExecuteScalarAsync( + @" insert into SoftwareProduct( SoftwareProductId, SoftwareProductName, @@ -192,11 +209,12 @@ insert into SoftwareProduct( @BrandId)", new { - SoftwareProductId = softwareProductId, + SoftwareProductId = softwareProductId, + // MA SoftwareProductName = $"{softwareProductName}", SoftwareProductDescription = $"{softwareProductName} description", - LogoUri = $"https://www.{softwareProductName}.com/logo.jpg", + LogoUri = $"https://www.{softwareProductName}.com/logo.jpg", ClientUri = $"https://www.{softwareProductName}.com/client", RecipientBaseUri = $"https://www.{softwareProductName}.com/recipientbase", @@ -204,8 +222,8 @@ insert into SoftwareProduct( RedirectUris = $"https://www.{softwareProductName}.com/redirect1,https://www.{softwareProductName}.com/redirect2", JwksUri = $"https://www.{softwareProductName}.com/jwks", Scope = "scope", - - //StatusId = modified ? "2" : "1", // 1=active, 2=inactive + + // StatusId = modified ? "2" : "1", // 1=active, 2=inactive StatusId = "1", // 1=active, 2=inactive BrandId = brandId, @@ -228,7 +246,7 @@ insert into SoftwareProduct( // Insert software products for (int isoftwareProductCount = 0; isoftwareProductCount < softwareProductCount; isoftwareProductCount++) { - var register_SoftwareProductId = await Register_InsertSoftwareProduct(connection, register_BrandId, modified); + _ = await Register_InsertSoftwareProduct(connection, register_BrandId, modified); } } } @@ -238,8 +256,8 @@ private static async Task AuthServerInsert(SqlConnection connection, int legalEn { static async Task AuthServer_InsertCDRSoftwareProduct(SqlConnection connection) { - - await connection.ExecuteScalarAsync(@" + await connection.ExecuteScalarAsync( + @" insert into SoftwareProducts(SoftwareProductId, SoftwareProductName, SoftwareProductDescription, LogoUri, Status, LegalEntityId, LegalEntityName, LegalEntityStatus, BrandId, BrandName, BrandStatus ) values(@SoftwareProductId, @SoftwareProductName, @SoftwareProductDescription, @LogoUri, @Status, @@ -258,38 +276,38 @@ insert into SoftwareProducts(SoftwareProductId, SoftwareProductName, SoftwarePro BrandId = "cdr-register", BrandName = "cdr-register", - BrandStatus = "ACTIVE" - }); ; - - + BrandStatus = "ACTIVE", + }); } + static async Task AuthServer_InsertSoftwareProducts(SqlConnection connection, Guid legalEntityId, Guid brandId, bool modified) { var softwareProductId = new Guid($"00000000-0000-0000-0000-{++nextAuthServerSoftwareProductId:d012}"); string softwareProductName = $"SoftwareProduct_{softwareProductId}".ToString().Replace('-', '_'); - await connection.ExecuteScalarAsync(@" + await connection.ExecuteScalarAsync( + @" insert into SoftwareProducts(SoftwareProductId, SoftwareProductName, SoftwareProductDescription, LogoUri, Status, LegalEntityId, LegalEntityName, LegalEntityStatus, BrandId, BrandName, BrandStatus ) values(@SoftwareProductId, @SoftwareProductName, @SoftwareProductDescription, @LogoUri, @Status, @LegalEntityId, @LegalEntityName, @LegalEntityStatus, @BrandId, @BrandName, @BrandStatus)", new { - SoftwareProductId = softwareProductId, + SoftwareProductId = softwareProductId, SoftwareProductName = softwareProductName, SoftwareProductDescription = $"{softwareProductName} description", - LogoUri = $"https://www.{softwareProductName}.com/logo.jpg", + LogoUri = $"https://www.{softwareProductName}.com/logo.jpg", Status = "ACTIVE", - - LegalEntityId = legalEntityId, + + LegalEntityId = legalEntityId, LegalEntityName = $"LegalEntity_{legalEntityId}".ToString().Replace('-', '_'), LegalEntityStatus = "ACTIVE", - BrandId = brandId, + BrandId = brandId, BrandName = $"Brand_{brandId}".ToString().Replace('-', '_'), - BrandStatus = "ACTIVE" - }); ; + BrandStatus = "ACTIVE", + }); return softwareProductId; } @@ -297,14 +315,14 @@ insert into SoftwareProducts(SoftwareProductId, SoftwareProductName, SoftwarePro await AuthServer_InsertCDRSoftwareProduct(connection); for (int i = 1; i <= legalEntityCount; i++) - { + { var legalEntityId = new Guid($"00000000-0000-0000-0000-{++nextAuthServerLegalEntityId:d012}"); for (int i2 = 1; i2 <= brandCount; i2++) - { + { var brandId = new Guid($"00000000-0000-0000-0000-{++nextAuthServerBrandId:d012}"); for (int i3 = 1; i3 <= brandCount; i3++) - { - var softwareProductId = await AuthServer_InsertSoftwareProducts(connection, legalEntityId, brandId, modified); + { + _ = await AuthServer_InsertSoftwareProducts(connection, legalEntityId, brandId, modified); } } } diff --git a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/DisplayTestMethodNameAttribute.cs b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/DisplayTestMethodNameAttribute.cs new file mode 100644 index 0000000..572f76b --- /dev/null +++ b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/DisplayTestMethodNameAttribute.cs @@ -0,0 +1,29 @@ +#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON + +#nullable enable + +using System; +using System.Configuration; +using System.IO; +using System.Reflection; +using CdrAuthServer.GetDataRecipients.IntegrationTests.Fixtures; +using Microsoft.Extensions.Configuration; +using Xunit; +using Xunit.Sdk; + +namespace CdrAuthServer.GetDataRecipients.IntegrationTests +{ + internal class DisplayTestMethodNameAttribute : BeforeAfterTestAttribute + { + private static int count = 0; + + public override void Before(MethodInfo methodUnderTest) + { + Console.WriteLine($"Test #{++count} - {methodUnderTest.DeclaringType?.Name}.{methodUnderTest.Name}"); + } + + public override void After(MethodInfo methodUnderTest) + { + } + } +} diff --git a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/Fixtures/TestFixture.cs b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/Fixtures/TestFixture.cs index de16c80..2112313 100644 --- a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/Fixtures/TestFixture.cs +++ b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/Fixtures/TestFixture.cs @@ -15,4 +15,4 @@ public Task DisposeAsync() return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/US28391_GetDataRecipients.cs b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/US28391_GetDataRecipients.cs index eed9243..3351185 100644 --- a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/US28391_GetDataRecipients.cs +++ b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/US28391_GetDataRecipients.cs @@ -1,15 +1,15 @@ -// #define DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON +// #define DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON using System; +using System.Net; +using System.Net.Http; using System.Threading.Tasks; -using Microsoft.Data.SqlClient; using Dapper; -using Newtonsoft.Json; -using Xunit; using FluentAssertions; using FluentAssertions.Execution; -using System.Net.Http; -using System.Net; +using Microsoft.Data.SqlClient; +using Newtonsoft.Json; +using Xunit; #nullable enable @@ -34,16 +34,25 @@ private async Task ExecuteAzureFunction() } private async Task Test( - int registerLegalEntityCount, int registerBrandCount, int registerSoftwareProductCount, - int authServerLegalEntityCount, int authServerBrandCount, int authServerSoftwareProductCount, - bool registerModified = false, bool authServerModified = false) + int registerLegalEntityCount, + int registerBrandCount, + int registerSoftwareProductCount, + int authServerLegalEntityCount, + int authServerBrandCount, + int authServerSoftwareProductCount, + bool registerModified = false, + bool authServerModified = false) { // Arrange await DatabaseSeeder.Execute( - registerLegalEntityCount, registerBrandCount, registerSoftwareProductCount, - authServerLegalEntityCount, authServerBrandCount, authServerSoftwareProductCount, - registerModified, authServerModified - ); + registerLegalEntityCount, + registerBrandCount, + registerSoftwareProductCount, + authServerLegalEntityCount, + authServerBrandCount, + authServerSoftwareProductCount, + registerModified, + authServerModified); // Act await ExecuteAzureFunction(); @@ -87,16 +96,15 @@ public async Task ACX01_WhenDataHolderChanged_ShouldSync() await Test(1, 1, 1, 1, 1, 1, false, true); } - - - static private async Task Assert_RegisterAndDataHolderIsSynced() + private static async Task Assert_RegisterAndDataHolderIsSynced() { static async Task Assert_TableDataIsEqual( - SqlConnection registerConnection, string registerSql, - SqlConnection authServerConnection, string authServerSql, + SqlConnection registerConnection, + string registerSql, + SqlConnection authServerConnection, + string authServerSql, string tableName) { - var registerJson = JsonConvert.SerializeObject(await registerConnection.QueryAsync(registerSql)); var authServerJson = JsonConvert.SerializeObject(await authServerConnection.QueryAsync(authServerSql)); @@ -106,7 +114,6 @@ static async Task Assert_TableDataIsEqual( File.WriteAllText($"c:/temp/actual_{tableName}.json", authServerJson); #endif authServerJson.Should().Be(registerJson); - } const string REGISTER_LEGALENTITY_SQL = @" @@ -158,16 +165,16 @@ from SoftwareProduct sp where pt.ParticipationTypeCode = 'DR' -- hardly necessary since only DRs have software products anyway order by SoftwareProductId"; - //Upper(Status) Status, + // Upper(Status) Status, var AUTHSERVER_LEGALENTITY_SQL = "select LegalEntityId, LegalEntityName, LegalEntityStatus Status from SoftwareProducts where LegalEntityId != 'cdr-register' order by LegalEntityId "; var AUTHSERVER_BRAND_SQL = "select BrandId, BrandName, BrandStatus Status, LegalEntityId from SoftwareProducts where BrandId != 'cdr-register' order by BrandId"; var AUTHSERVER_SOFTWAREPRODUCT_SQL = "select SoftwareProductId, SoftwareProductName, SoftwareProductDescription, LogoUri, Status from SoftwareProducts where SoftwareProductId != 'cdr-register' order by SoftwareProductId"; using var registerConnection = new SqlConnection(BaseTest.CONNECTIONSTRING_REGISTER_RW); - registerConnection.Open(); + await registerConnection.OpenAsync(); using var authServerConnection = new SqlConnection(BaseTest.CONNECTIONSTRING_AUTHSERVER_RW); - authServerConnection.Open(); + await authServerConnection.OpenAsync(); // Assert await Assert_TableDataIsEqual(registerConnection, REGISTER_LEGALENTITY_SQL, authServerConnection, AUTHSERVER_LEGALENTITY_SQL, "LegalEntity"); diff --git a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/XUnit/AlphabeticalOrderer.cs b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/XUnit/AlphabeticalOrderer.cs index 40e5054..ff15fce 100644 --- a/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/XUnit/AlphabeticalOrderer.cs +++ b/Source/CdrAuthServer.GetDataRecipients.IntegrationTests/XUnit/AlphabeticalOrderer.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Xunit.Abstractions; using Xunit.Sdk; @@ -7,7 +7,9 @@ namespace CdrAuthServer.GetDataRecipients.IntegrationTests.XUnit.Orderers { public class AlphabeticalOrderer : ITestCaseOrderer { - public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase => + public IEnumerable OrderTestCases(IEnumerable testCases) + where TTestCase : ITestCase + => testCases.OrderBy(testCase => testCase.TestMethod.Method.Name); } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.GetDataRecipients/CdrAuthServer.GetDataRecipients.csproj b/Source/CdrAuthServer.GetDataRecipients/CdrAuthServer.GetDataRecipients.csproj index 65e5548..4d674a2 100644 --- a/Source/CdrAuthServer.GetDataRecipients/CdrAuthServer.GetDataRecipients.csproj +++ b/Source/CdrAuthServer.GetDataRecipients/CdrAuthServer.GetDataRecipients.csproj @@ -8,7 +8,8 @@ <_FunctionsSkipCleanOutput>true Exe /home/site/wwwroot - Linux + Linux + True @@ -26,6 +27,14 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/CdrAuthServer.GetDataRecipients/GetDROptions.cs b/Source/CdrAuthServer.GetDataRecipients/GetDROptions.cs index 4bb2980..9d4defa 100644 --- a/Source/CdrAuthServer.GetDataRecipients/GetDROptions.cs +++ b/Source/CdrAuthServer.GetDataRecipients/GetDROptions.cs @@ -3,18 +3,31 @@ public class GetDROptions { public string AzureWebJobsStorage { get; set; } + public string StorageConnectionString { get; set; } - public string FUNCTIONS_WORKER_RUNTIME { get; set; } + + public string FUNCTIONS_WORKER_RUNTIME { get; set; } + public string Register_CdrAuthServer_Logging_DB_ConnectionString { get; set; } + public string Register_CdrAuthServer_MetadataUpdate_Endpoint { get; set; } + public string Register_CdrAuthServer_Token_Endpoint { get; set; } + public string Register_CdrAuthServer_MetadataUpdate_XV { get; set; } + public string Register_CdrAuthServer_MetadataUpdate_XMINV { get; set; } + public string Register_Client_Id { get; set; } + public string Register_Client_Certificate { get; set; } + public string Register_Client_Certificate_Password { get; set; } + public string Register_Signing_Certificate { get; set; } + public string Register_Signing_Certificate_Password { get; set; } + public string Ignore_Server_Certificate_Errors { get; set; } } } diff --git a/Source/CdrAuthServer.GetDataRecipients/GetDataRecipients.cs b/Source/CdrAuthServer.GetDataRecipients/GetDataRecipientsFunction.cs similarity index 76% rename from Source/CdrAuthServer.GetDataRecipients/GetDataRecipients.cs rename to Source/CdrAuthServer.GetDataRecipients/GetDataRecipientsFunction.cs index dcfb4a5..4af8366 100644 --- a/Source/CdrAuthServer.GetDataRecipients/GetDataRecipients.cs +++ b/Source/CdrAuthServer.GetDataRecipients/GetDataRecipientsFunction.cs @@ -1,4 +1,4 @@ -using CdrAuthServer.Infrastructure; +using CdrAuthServer.Infrastructure; using CdrAuthServer.Infrastructure.Extensions; using CdrAuthServer.Infrastructure.Models; using Microsoft.Azure.Functions.Worker; @@ -23,14 +23,13 @@ public class GetDataRecipientsFunction public GetDataRecipientsFunction(ILoggerFactory loggerFactory, IOptions options) { _logger = loggerFactory.CreateLogger(); - _drOptions =options.Value; + _drOptions = options.Value; } + /// - /// Get Data Recipients Function + /// Get Data Recipients Function. /// - /// Gets the Data Recipients from the Register and updates the local repository - - + /// Gets the Data Recipients from the Register and updates the local repository. [Function("GetDataRecipients")] public async Task Run([TimerTrigger("%Schedule%")] TimerInfo myTimer) { @@ -38,7 +37,6 @@ public async Task Run([TimerTrigger("%Schedule%")] TimerInfo myTimer) try { - // Updated names for the cdrauth server var dataRecipientsEndpoint = _drOptions.Register_CdrAuthServer_MetadataUpdate_Endpoint; var ignoreServerCertificateErrors = _drOptions.Ignore_Server_Certificate_Errors.Equals("true", StringComparison.OrdinalIgnoreCase); @@ -55,7 +53,7 @@ public async Task Run([TimerTrigger("%Schedule%")] TimerInfo myTimer) // Add token to the bear token call // Auth Api should receive the call and authorize it before calling on - //Loading client certificates from Base64 string + // Loading client certificates from Base64 string X509Certificate2 clientCertificate = LoadCertificates(_logger, clientCert, clientCertPwd); _logger.LogInformation("Client certificate loaded: {Thumbprint}", clientCertificate.Thumbprint); @@ -66,7 +64,7 @@ public async Task Run([TimerTrigger("%Schedule%")] TimerInfo myTimer) if (tokenResponse.IsSuccessful) { - // Send access token as bear token request to autherization server + // Send access token as bear token request to autherization server (_, var respStatusCode) = await GetDataRecipients(dataRecipientsEndpoint, tokenResponse.Data.AccessToken, clientCertificate, _logger, ignoreServerCertificateErrors); if (respStatusCode != System.Net.HttpStatusCode.OK) { @@ -84,26 +82,25 @@ public async Task Run([TimerTrigger("%Schedule%")] TimerInfo myTimer) } } - /// - /// Returns certificates + /// Returns certificates. /// - /// - /// - /// - /// + /// logger interface. + /// certificate name. + /// password for the certificate. + /// the loaded certificate. private static X509Certificate2 LoadCertificates(ILogger log, string cert, string certPwd) { log.LogInformation("Loading the certificate..."); byte[] certBytes = Convert.FromBase64String(cert); - X509Certificate2 certificate = new(certBytes, certPwd, X509KeyStorageFlags.MachineKeySet); + X509Certificate2 certificate = new (certBytes, certPwd, X509KeyStorageFlags.MachineKeySet); return certificate; } /// - /// Get Access Token + /// Get Access Token. /// - /// JWT + /// JWT. private async Task> GetAccessToken( string tokenEndpoint, string clientId, @@ -116,7 +113,7 @@ private static X509Certificate2 LoadCertificates(ILogger log, string cert, strin var client = GetHttpClient(clientCertificate, ignoreServerCertificateErrors: ignoreServerCertificateErrors); // Make the request to the token endpoint. - log.LogInformation("Retrieving access_token from the AuthServer: {tokenEndpoint}", tokenEndpoint); + log.LogInformation("Retrieving access_token from the AuthServer: {TokenEndpoint}", tokenEndpoint); try { @@ -132,12 +129,12 @@ private static X509Certificate2 LoadCertificates(ILogger log, string cert, strin var tokenResponse = new Infrastructure.Models.Response() { - StatusCode = response.StatusCode + StatusCode = response.StatusCode, }; if (response.IsSuccessStatusCode) { - log.LogInformation("AuthServer response: {statusCode} - {body}", tokenResponse.StatusCode, body); + log.LogInformation("AuthServer response: {StatusCode} - {Body}", tokenResponse.StatusCode, body); tokenResponse.Data = JsonConvert.DeserializeObject(body); } else @@ -148,7 +145,7 @@ private static X509Certificate2 LoadCertificates(ILogger log, string cert, strin return tokenResponse; } - catch(Exception ex) + catch (Exception ex) { await InsertDBLog(_drOptions.Register_CdrAuthServer_Logging_DB_ConnectionString, "Error", "Exception", "GetAccessToken", ex); log.LogError(ex, "Caught exception in GetAccessToken"); @@ -156,41 +153,41 @@ private static X509Certificate2 LoadCertificates(ILogger log, string cert, strin return new Infrastructure.Models.Response() { - StatusCode = System.Net.HttpStatusCode.InternalServerError + StatusCode = System.Net.HttpStatusCode.InternalServerError, }; } - /// - /// Get the list of Data Recipients from the Register + /// Get the list of Data Recipients from the Register. /// - /// Raw data + /// Raw data. private async Task<(string, System.Net.HttpStatusCode)> GetDataRecipients( string dataRecipientsEndpoint, string accessToken, - X509Certificate2 clientCertificate, + X509Certificate2 clientCertificate, ILogger log, bool ignoreServerCertificateErrors = false) { - var client = GetHttpClient(clientCertificate:clientCertificate, - accessToken:accessToken, + var client = GetHttpClient( + clientCertificate: clientCertificate, + accessToken: accessToken, ignoreServerCertificateErrors: ignoreServerCertificateErrors); - log.LogInformation("Retrieving data recipients from the Register: {dataRecipientsEndpoint}", dataRecipientsEndpoint); + log.LogInformation("Retrieving data recipients from the Register: {DataRecipientsEndpoint}", dataRecipientsEndpoint); var payload = "{\"data\": {\"action\": \"REFRESH\"}}"; HttpContent content = new StringContent(payload, Encoding.UTF8, "application/json"); - - var result = await client.PostAsync(dataRecipientsEndpoint, content); - var data = result.Content.ReadAsStringAsync().Result; - log.LogInformation("Register response: {statusCode} - {body}", result.StatusCode, data); + + var result = await client.PostAsync(dataRecipientsEndpoint, content); + var data = await result.Content.ReadAsStringAsync(); + log.LogInformation("Register response: {StatusCode} - {Body}", result.StatusCode, data); return (data, result.StatusCode); } private HttpClient GetHttpClient( X509Certificate2 clientCertificate = null, - string accessToken = null, + string accessToken = null, bool ignoreServerCertificateErrors = false) { var clientHandler = new HttpClientHandler(); @@ -210,7 +207,9 @@ private HttpClient GetHttpClient( // 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); + } // Add the x-v header to the request from configurations if (!string.IsNullOrEmpty(_drOptions.Register_CdrAuthServer_MetadataUpdate_XV)) @@ -226,18 +225,18 @@ private HttpClient GetHttpClient( return client; } - + /// - /// Update the Log table + /// Update the Log table. /// private static async Task InsertDBLog(string dbConnString, string msg, string lvl, string methodName, Exception exMsg = null, string entity = "") { - string exMessage = ""; + string exMessage = string.Empty; if (exMsg != null) { Exception innerException = exMsg; - StringBuilder innerMsg = new(); + StringBuilder innerMsg = new (); int ctr = 0; do @@ -259,39 +258,47 @@ private static async Task InsertDBLog(string dbConnString, string msg, string lv // Use the Exception message if (innerMsg.Length == 0) + { exMessage = exMsg.Message; + } // Use the inner Exception message (includes the Exception message) else + { exMessage = innerMsg.ToString(); + } // Include the serialised entity for use with Exception message only if (!string.IsNullOrEmpty(entity)) + { exMessage += "\r\nEntity: " + entity; + } - exMessage = exMessage.Replace("'", ""); + exMessage = exMessage.Replace("'", string.Empty); } - using (SqlConnection db = new(dbConnString)) - { - db.Open(); - var cmdText = ""; + using SqlConnection db = new (dbConnString); + await db.OpenAsync(); + var cmdText = string.Empty; - if (string.IsNullOrEmpty(exMessage)) - cmdText = $"INSERT INTO [LogEvents-DrService] ([Message], [Level], [TimeStamp], [ProcessName], [MethodName], [SourceContext]) VALUES (@msg,@lvl,GETUTCDATE(),@procName,@methodName,@srcContext)"; - else - cmdText = $"INSERT INTO [LogEvents-DrService] ([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", "CdrAuthServer.GetDataRecipients"); - await cmd.ExecuteNonQueryAsync(); - db.Close(); + if (string.IsNullOrEmpty(exMessage)) + { + cmdText = $"INSERT INTO [LogEvents-DrService] ([Message], [Level], [TimeStamp], [ProcessName], [MethodName], [SourceContext]) VALUES (@msg,@lvl,GETUTCDATE(),@procName,@methodName,@srcContext)"; } + else + { + cmdText = $"INSERT INTO [LogEvents-DrService] ([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", "CdrAuthServer.GetDataRecipients"); + await cmd.ExecuteNonQueryAsync(); + await db.CloseAsync(); } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.GetDataRecipients/GetDataRecipients_IntegrationTestsHelper.cs b/Source/CdrAuthServer.GetDataRecipients/GetDataRecipients_IntegrationTestsHelper.cs index b9ca383..c8db996 100644 --- a/Source/CdrAuthServer.GetDataRecipients/GetDataRecipients_IntegrationTestsHelper.cs +++ b/Source/CdrAuthServer.GetDataRecipients/GetDataRecipients_IntegrationTestsHelper.cs @@ -1,5 +1,4 @@ - -#if INTEGRATION_TESTS +#if INTEGRATION_TESTS using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Functions.Worker; @@ -37,4 +36,4 @@ public async Task INTEGRATIONTESTS_DATARECIPIENTS( } } } -#endif \ No newline at end of file +#endif diff --git a/Source/CdrAuthServer.GetDataRecipients/Program.cs b/Source/CdrAuthServer.GetDataRecipients/Program.cs index f24fa9d..ddbdfb2 100644 --- a/Source/CdrAuthServer.GetDataRecipients/Program.cs +++ b/Source/CdrAuthServer.GetDataRecipients/Program.cs @@ -1,19 +1,21 @@ -using Microsoft.Extensions.Hosting; +using System; using System.IO; using System.Reflection; -using System; +using CdrAuthServer.GetDataRecipients; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using CdrAuthServer.GetDataRecipients; +using Microsoft.Extensions.Hosting; var host = new HostBuilder() .ConfigureFunctionsWebApplication() .ConfigureAppConfiguration((context, builder) => { - var configuration = builder.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) - //while running on local machine via vs studio these are settings used + _ = builder.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) + + // while running on local machine via vs studio these are settings used .AddJsonFile("local.settings.json", true, true) - //while running docker these are config values used + + // while running docker these are config values used .AddJsonFile("appsettings.docker.json", true, true) .AddEnvironmentVariables() .AddCommandLine(Environment.GetCommandLineArgs()) @@ -35,4 +37,4 @@ }) .Build(); -host.Run(); \ No newline at end of file +await host.RunAsync(); diff --git a/Source/CdrAuthServer.Infrastructure/Attributes/CheckXVAttribute.cs b/Source/CdrAuthServer.Infrastructure/Attributes/ReturnXVAttribute.cs similarity index 99% rename from Source/CdrAuthServer.Infrastructure/Attributes/CheckXVAttribute.cs rename to Source/CdrAuthServer.Infrastructure/Attributes/ReturnXVAttribute.cs index f3f6746..42bbc45 100644 --- a/Source/CdrAuthServer.Infrastructure/Attributes/CheckXVAttribute.cs +++ b/Source/CdrAuthServer.Infrastructure/Attributes/ReturnXVAttribute.cs @@ -3,7 +3,7 @@ namespace CdrAuthServer.Infrastructure.Attributes { /// - /// Checks the x-v header field is a supported version, if not then responds with BadRequest and appropriate ResponseErrorList + /// Checks the x-v header field is a supported version, if not then responds with BadRequest and appropriate ResponseErrorList. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] public class ReturnXVAttribute : ActionFilterAttribute @@ -23,4 +23,4 @@ public override void OnActionExecuted(ActionExecutedContext context) base.OnActionExecuted(context); } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.Infrastructure/Authorisation/AuthServerAuthorisationPolicyAttribute.cs b/Source/CdrAuthServer.Infrastructure/Authorisation/AuthServerAuthorisationPolicyAttribute.cs index 7da0631..9d47b24 100644 --- a/Source/CdrAuthServer.Infrastructure/Authorisation/AuthServerAuthorisationPolicyAttribute.cs +++ b/Source/CdrAuthServer.Infrastructure/Authorisation/AuthServerAuthorisationPolicyAttribute.cs @@ -13,6 +13,6 @@ public enum AuthServerAuthorisationPolicyAttribute [AuthorisationPolicy("GetBankingAccounts", Scopes.ResourceApis.Banking.AccountsBasicRead, false, true, true)] GetBankingAccounts, [AuthorisationPolicy("AdminMetadataUpdate", Scopes.AdminMetadataUpdate, false, true, false)] - AdminMetadataUpdate + AdminMetadataUpdate, } } diff --git a/Source/CdrAuthServer.Infrastructure/Authorisation/AuthorisationPolicy.cs b/Source/CdrAuthServer.Infrastructure/Authorisation/AuthorisationPolicy.cs index 01da057..460f2c0 100644 --- a/Source/CdrAuthServer.Infrastructure/Authorisation/AuthorisationPolicy.cs +++ b/Source/CdrAuthServer.Infrastructure/Authorisation/AuthorisationPolicy.cs @@ -1,7 +1,9 @@ namespace CdrAuthServer.Infrastructure.Authorisation { [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] +#pragma warning disable S3376 // Attribute, EventArgs, and Exception type names should end with the type being extended public class AuthorisationPolicy : Attribute +#pragma warning restore S3376 // Attribute, EventArgs, and Exception type names should end with the type being extended { public AuthorisationPolicy(string name, string? scopeRequirement, bool hasMtlsRequirement, bool hasHolderOfKeyRequirement, bool hasAccessTokenRequirement) { @@ -13,9 +15,15 @@ public AuthorisationPolicy(string name, string? scopeRequirement, bool hasMtlsRe } public string Name { get; private set; } + public string? ScopeRequirement { get; private set; } - public bool HasMtlsRequirement { get; private set; } //TODO: Currently not fully implemented in AuthServer. Implement in future to align with Mock Register + +#pragma warning disable S1135 // Track uses of "TODO" tags + public bool HasMtlsRequirement { get; private set; } // TODO: Currently not fully implemented in AuthServer. Implement in future to align with Mock Register +#pragma warning restore S1135 // Track uses of "TODO" tags + public bool HasHolderOfKeyRequirement { get; private set; } + public bool HasAccessTokenRequirement { get; private set; } } } diff --git a/Source/CdrAuthServer.Infrastructure/CdrAuthServer.Infrastructure.csproj b/Source/CdrAuthServer.Infrastructure/CdrAuthServer.Infrastructure.csproj index 65ea3ee..3a9eab2 100644 --- a/Source/CdrAuthServer.Infrastructure/CdrAuthServer.Infrastructure.csproj +++ b/Source/CdrAuthServer.Infrastructure/CdrAuthServer.Infrastructure.csproj @@ -6,7 +6,8 @@ $(Version) $(Version) enable - enable + enable + True @@ -16,6 +17,14 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/CdrAuthServer.Infrastructure/Certificates/CertificateLoadDetails.cs b/Source/CdrAuthServer.Infrastructure/Certificates/CertificateLoadDetails.cs index 44edae8..d38ad68 100644 --- a/Source/CdrAuthServer.Infrastructure/Certificates/CertificateLoadDetails.cs +++ b/Source/CdrAuthServer.Infrastructure/Certificates/CertificateLoadDetails.cs @@ -5,14 +5,17 @@ public enum CertificateSource Raw, File, Url, - KeyVault + KeyVault, } public class CertificateLoadDetails { public CertificateSource Source { get; set; } + public string Content { get; set; } = string.Empty; - public string Location { get; set; } = String.Empty; - public string Password { get; set; } = String.Empty; + + public string Location { get; set; } = string.Empty; + + public string Password { get; set; } = string.Empty; } } diff --git a/Source/CdrAuthServer.Infrastructure/Certificates/CertificateLoader.cs b/Source/CdrAuthServer.Infrastructure/Certificates/CertificateLoader.cs index 8ec2f69..0c650e9 100644 --- a/Source/CdrAuthServer.Infrastructure/Certificates/CertificateLoader.cs +++ b/Source/CdrAuthServer.Infrastructure/Certificates/CertificateLoader.cs @@ -18,15 +18,15 @@ public async Task Load(CertificateLoadDetails loadDetails) switch (loadDetails.Source) { case CertificateSource.File: - return LoadCertificateFromBytes(File.ReadAllBytes(loadDetails.Location), loadDetails.Password); + return LoadCertificateFromBytes(await File.ReadAllBytesAsync(loadDetails.Location), loadDetails.Password); case CertificateSource.Url: - return LoadCertificateFromBytes((await DownloadData(loadDetails.Location)), loadDetails.Password); + return LoadCertificateFromBytes(await DownloadData(loadDetails.Location), loadDetails.Password); case CertificateSource.Raw: return LoadCertificateFromBytes(Convert.FromBase64String(loadDetails.Content), loadDetails.Password); - case CertificateSource.KeyVault: + case CertificateSource.KeyVault: throw new NotImplementedException(); default: @@ -44,7 +44,7 @@ private static X509Certificate2 LoadCertificateFromBytes(byte[] certBytes, strin return new X509Certificate2(certBytes, password, X509KeyStorageFlags.Exportable); } - private async static Task DownloadData(string url) + private static async Task DownloadData(string url) { using (var http = new HttpClient()) { diff --git a/Source/CdrAuthServer.Infrastructure/Certificates/OcspRequester.cs b/Source/CdrAuthServer.Infrastructure/Certificates/OcspRequester.cs index f1bd1a5..6081802 100644 --- a/Source/CdrAuthServer.Infrastructure/Certificates/OcspRequester.cs +++ b/Source/CdrAuthServer.Infrastructure/Certificates/OcspRequester.cs @@ -7,12 +7,11 @@ using Org.BouncyCastle.X509; using System.Collections; - namespace CdrAuthServer.Infrastructure.Certificates { /// /// Online Certificate Status Protocol (OCSP) request builder class that is used to send a request - /// to the defined OCSP responder defined in the certificates Authority Information Access (AIA) extension. + /// to the defined OCSP responder defined in the certificates Authority Information Access (AIA) extension. /// public class OcspRequester { @@ -72,7 +71,7 @@ public async Task GetResult(string serialNumber) _logger.LogInformation("OCSP Response revocation status = {RevokedStatus}", revokedStatus?.RevocationReason); - return (revokedStatus == null ? OcspResult.Good : OcspResult.Revoked); + return revokedStatus == null ? OcspResult.Good : OcspResult.Revoked; } private BigInteger ConvertSerialNumber(string serialNumber) @@ -87,7 +86,7 @@ private static byte[] BuildOcspRequest(BigInteger serialNumber, X509Certificate CertificateID certId = new(CertificateID.HashSha1, cacert, serialNumber); ocspRequestGenerator.AddRequest(certId); - var derObjectIds = new List(); //Distinguished Encoding Rules (DER) + var derObjectIds = new List(); // Distinguished Encoding Rules (DER) var extensionValues = new Hashtable(); Asn1OctetString asn1OctetString = new DerOctetString(BigInteger.ValueOf(DateTime.Now.Ticks).ToByteArray()); @@ -103,9 +102,9 @@ private static byte[] BuildOcspRequest(BigInteger serialNumber, X509Certificate public static byte[] GetBytesFromPEM(string pem) { var base64 = pem - .Replace(BEGIN_CERTIFICATE_MARKER, "") - .Replace(END_CERTIFICATE_MARKER, "") - .Replace(Environment.NewLine, ""); + .Replace(BEGIN_CERTIFICATE_MARKER, string.Empty) + .Replace(END_CERTIFICATE_MARKER, string.Empty) + .Replace(Environment.NewLine, string.Empty); return Convert.FromBase64String(base64); } diff --git a/Source/CdrAuthServer.Infrastructure/Constants.cs b/Source/CdrAuthServer.Infrastructure/Constants.cs index f389cad..1c53bf1 100644 --- a/Source/CdrAuthServer.Infrastructure/Constants.cs +++ b/Source/CdrAuthServer.Infrastructure/Constants.cs @@ -19,11 +19,11 @@ public static class Scopes { public const string OpenId = "openid"; public const string Profile = "profile"; - public const string Registration = "cdr:registration"; //CDR_DYNAMIC_CLIENT_REGISTRATION + public const string Registration = "cdr:registration"; // CDR_DYNAMIC_CLIENT_REGISTRATION public const string Common = "common:customer.basic:read common:customer.detail:read"; public const string Banking = "bank:accounts.basic:read bank:accounts.detail:read bank:transactions:read bank:payees:read bank:regular_payments:read"; public const string Energy = "energy:electricity.servicepoints.basic:read energy:electricity.servicepoints.detail:read energy:electricity.usage:read energy:electricity.der:read energy:accounts.basic:read energy:accounts.detail:read energy:accounts.paymentschedule:read energy:accounts.concessions:read energy:billing:read"; - public const string AdminMetadataUpdate = "admin:metadata:update"; //CDR_AUTHSERVER + public const string AdminMetadataUpdate = "admin:metadata:update"; // CDR_AUTHSERVER public const string AdminMetricsRead = "admin:metrics.basic:read"; public const string BankingSectorScopes = $"{OpenId} {Profile} {Registration} {Common} {Banking}"; public const string AllSectorScopes = $"{BankingSectorScopes} {Energy}"; diff --git a/Source/CdrAuthServer.Infrastructure/Exceptions/ClientCertificateException.cs b/Source/CdrAuthServer.Infrastructure/Exceptions/ClientCertificateException.cs index 61ad653..17188c2 100644 --- a/Source/CdrAuthServer.Infrastructure/Exceptions/ClientCertificateException.cs +++ b/Source/CdrAuthServer.Infrastructure/Exceptions/ClientCertificateException.cs @@ -2,8 +2,9 @@ { public class ClientCertificateException : Exception { - public ClientCertificateException(string message) : base(message) + public ClientCertificateException(string message) + : base(message) { - } + } } } diff --git a/Source/CdrAuthServer.Infrastructure/Extensions/AttributeExtensions.cs b/Source/CdrAuthServer.Infrastructure/Extensions/AttributeExtensions.cs index 00b0190..9b10764 100644 --- a/Source/CdrAuthServer.Infrastructure/Extensions/AttributeExtensions.cs +++ b/Source/CdrAuthServer.Infrastructure/Extensions/AttributeExtensions.cs @@ -8,12 +8,13 @@ public static IEnumerable GetAttributes(List types, MethodInfo inf { var actionAttributes = info.GetCustomAttributes(inherit); - IEnumerable controllerAttributes = []; + IEnumerable controllerAttributes = []; if (info.DeclaringType != null) { controllerAttributes = info.DeclaringType.GetTypeInfo().GetCustomAttributes(inherit); } + var actionAndControllerAttributes = actionAttributes.Union(controllerAttributes); return actionAndControllerAttributes.Where(attr => types.Contains(attr.GetType())); @@ -23,12 +24,13 @@ public static IEnumerable GetAttributes(List types, MethodInfo inf { var actionAttributes = info.GetCustomAttributes(inherit); - IEnumerable controllerAttributes = []; + IEnumerable controllerAttributes = []; if (info.DeclaringType != null) { controllerAttributes = info.DeclaringType.GetTypeInfo().GetCustomAttributes(inherit); } + var actionAndControllerAttributes = actionAttributes.Union(controllerAttributes); return (T?)actionAndControllerAttributes.SingleOrDefault(attr => attr.GetType() == typeof(T)); @@ -38,12 +40,13 @@ public static bool HasAttribute(MethodInfo info, Type type, bool inherit) { var actionAttributes = info.GetCustomAttributes(inherit); - IEnumerable controllerAttributes = []; + IEnumerable controllerAttributes = []; if (info.DeclaringType != null) { controllerAttributes = info.DeclaringType.GetTypeInfo().GetCustomAttributes(inherit); } + var actionAndControllerAttributes = actionAttributes.Union(controllerAttributes); return actionAndControllerAttributes.Any(attr => attr.GetType() == type); diff --git a/Source/CdrAuthServer.Infrastructure/Extensions/CdrSwaggerMiddlewareExtensions.cs b/Source/CdrAuthServer.Infrastructure/Extensions/CdrSwaggerMiddlewareExtensions.cs index 413167b..031c5f4 100644 --- a/Source/CdrAuthServer.Infrastructure/Extensions/CdrSwaggerMiddlewareExtensions.cs +++ b/Source/CdrAuthServer.Infrastructure/Extensions/CdrSwaggerMiddlewareExtensions.cs @@ -6,12 +6,9 @@ namespace CdrAuthServer.Infrastructure.Extensions { public static class CdrSwaggerMiddlewareExtensions { - public static IApplicationBuilder UseCdrSwagger(this IApplicationBuilder builder, IApiVersionDescriptionProvider provider = null) + public static IApplicationBuilder UseCdrSwagger(this IApplicationBuilder builder, IApiVersionDescriptionProvider? provider = null) { - if (provider == null) - { - provider = builder.ApplicationServices.GetRequiredService(); - } + provider ??= builder.ApplicationServices.GetRequiredService(); builder.UseSwagger(); @@ -21,7 +18,7 @@ public static IApplicationBuilder UseCdrSwagger(this IApplicationBuilder builder // Configure swagger Ui for multiple versions of the API string swaggerJsonBasePath = string.IsNullOrWhiteSpace(options.RoutePrefix) ? "." : ".."; - foreach (var groupName in provider.ApiVersionDescriptions?.Select(d => d.GroupName)) + foreach (var groupName in provider.ApiVersionDescriptions.Select(d => d.GroupName)) { options.SwaggerEndpoint( $"{swaggerJsonBasePath}/swagger/{groupName}/swagger.json", @@ -44,7 +41,7 @@ public static IApplicationBuilder UseCdrSwagger(this IApplicationBuilder builder options.SwaggerEndpoint( $"{swaggerJsonBasePath}/swagger/v1/swagger.json", - name); + name); }); return builder; diff --git a/Source/CdrAuthServer.Infrastructure/Extensions/CertificateExtensions.cs b/Source/CdrAuthServer.Infrastructure/Extensions/CertificateExtensions.cs index 6794117..0b8f825 100644 --- a/Source/CdrAuthServer.Infrastructure/Extensions/CertificateExtensions.cs +++ b/Source/CdrAuthServer.Infrastructure/Extensions/CertificateExtensions.cs @@ -7,7 +7,7 @@ public static class CertificateExtensions { public static string GetOCSPUrlFromCertificate(this X509Certificate2 certificate) { - X509Extension ocspExtension = certificate.Extensions["1.3.6.1.5.5.7.1.1"]; //AuthorityInfoAccess + X509Extension? ocspExtension = certificate.Extensions["1.3.6.1.5.5.7.1.1"]; // AuthorityInfoAccess if (ocspExtension == null) { @@ -30,8 +30,7 @@ public static string GetOCSPUrlFromCertificate(this X509Certificate2 certificate throw new ClientCertificateException("Unable to validate certificate - Missing OCSP URL"); } - return ocspResponderUrl; - + return ocspResponderUrl; } } } diff --git a/Source/CdrAuthServer.Infrastructure/Extensions/HttpExtensions.cs b/Source/CdrAuthServer.Infrastructure/Extensions/HttpExtensions.cs index 007bab3..4b2f6a4 100644 --- a/Source/CdrAuthServer.Infrastructure/Extensions/HttpExtensions.cs +++ b/Source/CdrAuthServer.Infrastructure/Extensions/HttpExtensions.cs @@ -6,7 +6,6 @@ namespace CdrAuthServer.Infrastructure.Extensions { public static class HttpExtensions { - public static void SetClientCertificate(this HttpClientHandler clientHandler, string certificateFileName, string certificatePassword) { clientHandler.ClientCertificates.Add(new X509Certificate2(certificateFileName, certificatePassword, X509KeyStorageFlags.Exportable)); @@ -19,7 +18,7 @@ public static bool IsSuccessful(this HttpStatusCode statusCode) public static int ToInt(this HttpStatusCode statusCode) { - return ((int)statusCode); + return (int)statusCode; } public static async Task SendPrivateKeyJwtRequest( diff --git a/Source/CdrAuthServer.Infrastructure/Extensions/NoHttpsException.cs b/Source/CdrAuthServer.Infrastructure/Extensions/NoHttpsException.cs new file mode 100644 index 0000000..aa71d52 --- /dev/null +++ b/Source/CdrAuthServer.Infrastructure/Extensions/NoHttpsException.cs @@ -0,0 +1,12 @@ +using System.Security; + +namespace CdrAuthServer.Infrastructure.Extensions +{ + public class NoHttpsException : SecurityException + { + public NoHttpsException() + : base("A non-https endpoint has been encountered and blocked") + { + } + } +} diff --git a/Source/CdrAuthServer.Infrastructure/Extensions/UrlExtensions.cs b/Source/CdrAuthServer.Infrastructure/Extensions/UrlExtensions.cs index f17e932..fd8c12c 100644 --- a/Source/CdrAuthServer.Infrastructure/Extensions/UrlExtensions.cs +++ b/Source/CdrAuthServer.Infrastructure/Extensions/UrlExtensions.cs @@ -37,7 +37,7 @@ public static string ValidateEndpoint(this string endpoint, bool enforceHttpsEnd var uri = new Uri(endpoint); uri.ValidateEndpoint(enforceHttpsEndpoint); - return endpoint; + return endpoint; } public static Uri ValidateEndpoint(this Uri uri, bool enforceHttpsEndpoint) @@ -55,9 +55,4 @@ public static Uri ValidateEndpoint(this Uri uri, bool enforceHttpsEndpoint) return uri; } } - - public class NoHttpsException : SecurityException - { - public NoHttpsException() : base("A non-https endpoint has been encountered and blocked") { } - } } diff --git a/Source/CdrAuthServer.Infrastructure/Extensions/WebServerExtensions.cs b/Source/CdrAuthServer.Infrastructure/Extensions/WebServerExtensions.cs index 1915f4f..760b407 100644 --- a/Source/CdrAuthServer.Infrastructure/Extensions/WebServerExtensions.cs +++ b/Source/CdrAuthServer.Infrastructure/Extensions/WebServerExtensions.cs @@ -1,5 +1,4 @@ -using CdrAuthServer.Infrastructure.Certificates; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.Extensions.Configuration; @@ -10,7 +9,7 @@ namespace CdrAuthServer.Infrastructure.Extensions { public static class WebServerExtensions { - public static void ConfigureWebServer( + public static async Task ConfigureWebServer( this IServiceCollection services, IConfiguration configuration, string serverCertificateConfigurationKey, @@ -18,12 +17,15 @@ public static void ConfigureWebServer( int? httpPort = null, bool requireClientCertificate = false) { + // Load the certificate asynchronously before configuring Kestrel + var certificate = await new CertificateLoader().Load(configuration, serverCertificateConfigurationKey); + services.Configure(options => { - options.ConfigureHttpsDefaults(async httpsOptions => + options.ConfigureHttpsDefaults(httpsOptions => { httpsOptions.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13; - httpsOptions.ServerCertificate = await (new CertificateLoader().Load(configuration, serverCertificateConfigurationKey)); + httpsOptions.ServerCertificate = certificate; httpsOptions.ClientCertificateMode = requireClientCertificate ? ClientCertificateMode.RequireCertificate : ClientCertificateMode.NoCertificate; }); diff --git a/Source/CdrAuthServer.Infrastructure/Models/CdrApiEndpointVersionOptions.cs b/Source/CdrAuthServer.Infrastructure/Models/CdrApiEndpointVersionOptions.cs index 20e4477..fcab005 100644 --- a/Source/CdrAuthServer.Infrastructure/Models/CdrApiEndpointVersionOptions.cs +++ b/Source/CdrAuthServer.Infrastructure/Models/CdrApiEndpointVersionOptions.cs @@ -2,21 +2,27 @@ { public class CdrApiEndpointVersionOptions { - public readonly string Path; - public readonly bool IsVersioned; - public readonly bool IsXVHeaderMandatory; - public readonly int? CurrentMinVersion; - public readonly int? CurrentMaxVersion; - public readonly int? MinVerForResponseErrorListV2; //any supported versions earlier than this number will use ResponseErrorList (v1) as per the CDS standards. Potentially everything uses ResponseErrorListv2 now + public string Path { get; } + + public bool IsVersioned { get; } + + public bool IsXVHeaderMandatory { get; } + + public int? CurrentMinVersion { get; } + + public int? CurrentMaxVersion { get; } + + public int? MinVerForResponseErrorListV2 { get; }// any supported versions earlier than this number will use ResponseErrorList (v1) as per the CDS standards. Potentially everything uses ResponseErrorListv2 now /// - /// Constructs an option set where multiple versions of the endpoint are supported + /// Initializes a new instance of the class. + /// option set where multiple versions of the endpoint are supported. /// - /// - /// - /// - /// - /// + /// Path of API endpoints. + /// Options to specifiy Xv is mandatory. + /// minVersion of the API. + /// maxVersion of the API. + /// minVersionForErrorListV2. public CdrApiEndpointVersionOptions(string path, bool isXvMandatory, int minVersion, int maxVersion, int minVersionForErrorListV2) { Path = path; @@ -28,11 +34,12 @@ public CdrApiEndpointVersionOptions(string path, bool isXvMandatory, int minVers } /// - /// Constructs an option set where only one version of the endpoint is supported + /// Initializes a new instance of the class. + /// Constructs an option set where only one version of the endpoint is supported. /// - /// - /// - /// + /// Path of API endpoints. + /// Options to specifiy Xv is mandatory. + /// Version of the API. public CdrApiEndpointVersionOptions(string path, bool isXvMandatory, int version) { Path = path; @@ -44,9 +51,10 @@ public CdrApiEndpointVersionOptions(string path, bool isXvMandatory, int version } /// - /// Constructs an option set where the endpoint is unversioned + /// Initializes a new instance of the class. + /// Constructs an option set where the endpoint is unversioned. /// - /// + /// Path of API endpoints. public CdrApiEndpointVersionOptions(string path) { Path = path; diff --git a/Source/CdrAuthServer.Infrastructure/Models/CdrApiOptions.cs b/Source/CdrAuthServer.Infrastructure/Models/CdrApiOptions.cs index c507722..e24bc59 100644 --- a/Source/CdrAuthServer.Infrastructure/Models/CdrApiOptions.cs +++ b/Source/CdrAuthServer.Infrastructure/Models/CdrApiOptions.cs @@ -5,12 +5,12 @@ namespace CdrAuthServer.Infrastructure.Models public class CdrApiOptions { private static readonly List _supportedApiVersions = new List - { - //(?i) is to make the regex case insensitive - //(%7B)* and (%7D)* represent the possibility of a { and } in the path, to cover the value '{industry}' for OAS generation purposes + { + // (?i) is to make the regex case insensitive + // (%7B)* and (%7D)* represent the possibility of a { and } in the path, to cover the value '{industry}' for OAS generation purposes - //Currently everything is unversioned or v1 with optional x-v, so we don't need to list anything - }; + // Currently everything is unversioned or v1 with optional x-v, so we don't need to list anything + }; public List EndpointVersionOptions { get; } = _supportedApiVersions; diff --git a/Source/CdrAuthServer.Infrastructure/Models/CdrSwaggerOptions.cs b/Source/CdrAuthServer.Infrastructure/Models/CdrSwaggerOptions.cs index ea1cab3..5c9ab08 100644 --- a/Source/CdrAuthServer.Infrastructure/Models/CdrSwaggerOptions.cs +++ b/Source/CdrAuthServer.Infrastructure/Models/CdrSwaggerOptions.cs @@ -2,10 +2,10 @@ { public class CdrSwaggerOptions { - public string SwaggerTitle { get; set; } + public string SwaggerTitle { get; set; } = string.Empty; - public bool IncludeAuthentication { get; set; } - public string VersionedApiGroupNameFormat { get; set; } = Constants.Versioning.GroupNameFormat; //default for group name format + public bool IncludeAuthentication { get; set; } = true; + public string VersionedApiGroupNameFormat { get; set; } = Constants.Versioning.GroupNameFormat; // default for group name format } } diff --git a/Source/CdrAuthServer.Infrastructure/Models/Pkce.cs b/Source/CdrAuthServer.Infrastructure/Models/Pkce.cs index 245ba5c..f9970a4 100644 --- a/Source/CdrAuthServer.Infrastructure/Models/Pkce.cs +++ b/Source/CdrAuthServer.Infrastructure/Models/Pkce.cs @@ -1,10 +1,11 @@ - -namespace CdrAuthServer.Infrastructure.Models +namespace CdrAuthServer.Infrastructure.Models { public class Pkce { public string? CodeVerifier { get; set; } + public string? CodeChallenge { get; set; } + public string CodeChallengeMethod { get; private set; } public Pkce() diff --git a/Source/CdrAuthServer.Infrastructure/Models/Response.cs b/Source/CdrAuthServer.Infrastructure/Models/Response.cs index fc44a5f..9d4165b 100644 --- a/Source/CdrAuthServer.Infrastructure/Models/Response.cs +++ b/Source/CdrAuthServer.Infrastructure/Models/Response.cs @@ -7,14 +7,14 @@ public class Response { public Response() { - this.Errors = new ResponseErrorList(); //This might be right...as it may end up with Errors.Errors + this.Errors = new ResponseErrorList(); // This might be right...as it may end up with Errors.Errors } - public bool IsSuccessful - { + public bool IsSuccessful + { get { - return ((int) this.StatusCode) < 400; + return ((int)this.StatusCode) < 400; } } @@ -25,7 +25,9 @@ public bool IsSuccessful public ResponseErrorList Errors { get; set; } } +#pragma warning disable SA1402 // File may only contain a single type public class Response : Response +#pragma warning restore SA1402 // File may only contain a single type { public T? Data { get; set; } } diff --git a/Source/CdrAuthServer.Infrastructure/Models/TokenResponse.cs b/Source/CdrAuthServer.Infrastructure/Models/TokenResponse.cs index d8451e9..cff7e0f 100644 --- a/Source/CdrAuthServer.Infrastructure/Models/TokenResponse.cs +++ b/Source/CdrAuthServer.Infrastructure/Models/TokenResponse.cs @@ -2,7 +2,9 @@ namespace CdrAuthServer.Infrastructure.Models { +#pragma warning disable SA1649 // File name should match first type name public class Token +#pragma warning restore SA1649 // File name should match first type name { [JsonProperty("id_token")] public string? IdToken { get; set; } diff --git a/Source/CdrAuthServer.Infrastructure/PrivateKeyJwt.cs b/Source/CdrAuthServer.Infrastructure/PrivateKeyJwt.cs index fdeb41c..82308c9 100644 --- a/Source/CdrAuthServer.Infrastructure/PrivateKeyJwt.cs +++ b/Source/CdrAuthServer.Infrastructure/PrivateKeyJwt.cs @@ -1,11 +1,10 @@ -using System.Security.Cryptography.X509Certificates; -using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; -using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.IdentityModel.Tokens; // #nullable enable - namespace CdrAuthServer.Infrastructure { /// @@ -16,15 +15,18 @@ public class PrivateKeyJwt public SigningCredentials SigningCredentials { get; set; } /// + /// Initializes a new instance of the class. /// Provide the Pkcs8 private key from X509 certificate. /// /// The path to the certificate. /// The password of the certificate. - public PrivateKeyJwt(string certFilePath, string pwd) : this(new X509Certificate2(certFilePath, pwd, X509KeyStorageFlags.Exportable)) + public PrivateKeyJwt(string certFilePath, string pwd) + : this(new X509Certificate2(certFilePath, pwd, X509KeyStorageFlags.Exportable)) { } /// + /// Initializes a new instance of the class. /// Provide the Pkcs8 private key from X509 certificate. /// /// The certificate used to sign the private key jwt. @@ -34,6 +36,7 @@ public PrivateKeyJwt(X509Certificate2 signingCertificate) } /// + /// Initializes a new instance of the class. /// Provide the private key directly. /// /// The path to the certificate. @@ -50,9 +53,9 @@ public PrivateKeyJwt(string privateKey) /// /// Generate the private_key_jwt using the provided private key. /// - /// The issuer of the JWT, usually set to the softwareProductId - /// The audience of the JWT, usually set to the target token endpoint - /// A base64 encoded JWT + /// The issuer of the JWT, usually set to the softwareProductId. + /// The audience of the JWT, usually set to the target token endpoint. + /// A base64 encoded JWT. public string Generate( string issuer, string audience, @@ -80,11 +83,11 @@ public string Generate( { new Claim("sub", issuer), new Claim("jti", jti ?? Guid.NewGuid().ToString()), - new Claim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer) + new Claim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer), }; var jwt = new JwtSecurityToken(issuer, audience, claims, expires: expiry, signingCredentials: this.SigningCredentials); var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); return jwtSecurityTokenHandler.WriteToken(jwt); } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.Infrastructure/Versioning/ApiVersionErrorResponse.cs b/Source/CdrAuthServer.Infrastructure/Versioning/ApiVersionErrorResponse.cs index 85f93d9..ab5bcbe 100644 --- a/Source/CdrAuthServer.Infrastructure/Versioning/ApiVersionErrorResponse.cs +++ b/Source/CdrAuthServer.Infrastructure/Versioning/ApiVersionErrorResponse.cs @@ -42,7 +42,7 @@ public override IActionResult CreateResponse(ErrorResponseContext context) statusCode = StatusCodes.Status500InternalServerError; } - _logger.LogError("{Detail}",errorList.Errors[0].Detail); + _logger.LogError("{Detail}", errorList.Errors[0].Detail); return new ObjectResult(errorList) { StatusCode = statusCode }; } diff --git a/Source/CdrAuthServer.Infrastructure/Versioning/CdrVersionReader.cs b/Source/CdrAuthServer.Infrastructure/Versioning/CdrVersionReader.cs index 14231d8..4396aa5 100644 --- a/Source/CdrAuthServer.Infrastructure/Versioning/CdrVersionReader.cs +++ b/Source/CdrAuthServer.Infrastructure/Versioning/CdrVersionReader.cs @@ -29,12 +29,14 @@ public void AddParameters(IApiVersionParameterDescriptionContext context) if (endpointOption == null) { - //handle any endpoint that hasn't been defined in options + // handle any endpoint that hasn't been defined in options endpointOption = new CdrApiEndpointVersionOptions(string.Empty, false, int.Parse(_options.DefaultVersion)); } else if (!endpointOption.IsVersioned) { - return _options.DefaultVersion; //TODO: Check if we want to return a version at all +#pragma warning disable S1135 // Track uses of "TODO" tags + return _options.DefaultVersion; // TODO: Check if we want to return a version at all +#pragma warning restore S1135 // Track uses of "TODO" tags } // If x-min-v is passed in, we expect it to be a Positive Integer, the x-v value is parsed out of the header and will be validated by the Package itself diff --git a/Source/CdrAuthServer.IntegrationTests/BaseTest.cs b/Source/CdrAuthServer.IntegrationTests/BaseTest.cs index f7350e2..369f4aa 100644 --- a/Source/CdrAuthServer.IntegrationTests/BaseTest.cs +++ b/Source/CdrAuthServer.IntegrationTests/BaseTest.cs @@ -1,4 +1,4 @@ -using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using Microsoft.Extensions.Configuration; using Xunit; using Xunit.DependencyInjection; @@ -8,9 +8,10 @@ namespace CdrAuthServer.IntegrationTests // Put all tests in same collection because we need them to run sequentially since some tests are mutating DB. [Collection("IntegrationTests")] [TestCaseOrderer("CdrAuthServer.IntegrationTests.XUnit.Orderers.AlphabeticalOrderer", "CdrAuthServer.IntegrationTests")] - abstract public class BaseTest : SharedBaseTest + public abstract class BaseTest : SharedBaseTest { - protected BaseTest(ITestOutputHelperAccessor testOutputHelperAccessor, IConfiguration config) : base(testOutputHelperAccessor, config) + protected BaseTest(ITestOutputHelperAccessor testOutputHelperAccessor, IConfiguration config) + : base(testOutputHelperAccessor, config) { } } diff --git a/Source/CdrAuthServer.IntegrationTests/CdrAuthServer.IntegrationTests.csproj b/Source/CdrAuthServer.IntegrationTests/CdrAuthServer.IntegrationTests.csproj index 4cb8883..415f413 100644 --- a/Source/CdrAuthServer.IntegrationTests/CdrAuthServer.IntegrationTests.csproj +++ b/Source/CdrAuthServer.IntegrationTests/CdrAuthServer.IntegrationTests.csproj @@ -9,6 +9,7 @@ enable enable true + True @@ -29,19 +30,29 @@ - + - - - + + + + + - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Source/CdrAuthServer.IntegrationTests/Infrastructure/DataHolderAccessToken.cs b/Source/CdrAuthServer.IntegrationTests/Infrastructure/DataHolderAccessToken.cs index 5efa983..214d347 100644 --- a/Source/CdrAuthServer.IntegrationTests/Infrastructure/DataHolderAccessToken.cs +++ b/Source/CdrAuthServer.IntegrationTests/Infrastructure/DataHolderAccessToken.cs @@ -8,7 +8,7 @@ namespace CdrAuthServer.IntegrationTests { public class DataHolderAccessTokenResponse { - static public async Task Deserialize(HttpResponseMessage responseMessage) + public static async Task Deserialize(HttpResponseMessage responseMessage) { var content = await responseMessage.Content.ReadAsStringAsync(); @@ -78,7 +78,7 @@ public async Task GetAccessTokenResponseMessage() CertificateFilename = Constants.Certificates.JwtCertificateFilename, CertificatePassword = Constants.Certificates.JwtCertificatePassword, Issuer = ClientId, - Audience = URL + Audience = URL, }.Generate(); var formFields = new List>(); diff --git a/Source/CdrAuthServer.IntegrationTests/Interfaces/IAuthorizationService.cs b/Source/CdrAuthServer.IntegrationTests/Interfaces/IAuthorizationService.cs index 0728670..805b9d0 100644 --- a/Source/CdrAuthServer.IntegrationTests/Interfaces/IAuthorizationService.cs +++ b/Source/CdrAuthServer.IntegrationTests/Interfaces/IAuthorizationService.cs @@ -9,4 +9,4 @@ public interface IAuthorizationService { Task GetToken(TokenType tokenType, string? clientId = null, int tokenLifetime = Constants.AuthServer.DefaultTokenLifetime, int sharingDuration = Constants.AuthServer.SharingDuration); } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/Interfaces/IDataHolderCDRArrangementRevocationService.cs b/Source/CdrAuthServer.IntegrationTests/Interfaces/IDataHolderCDRArrangementRevocationService.cs index 4cc1dcf..04ec1c4 100644 --- a/Source/CdrAuthServer.IntegrationTests/Interfaces/IDataHolderCDRArrangementRevocationService.cs +++ b/Source/CdrAuthServer.IntegrationTests/Interfaces/IDataHolderCDRArrangementRevocationService.cs @@ -8,4 +8,4 @@ public interface IDataHolderCDRArrangementRevocationService { Task SendRequest(string? grantType = "client_credentials", string? clientId = null, string? clientAssertionType = Constants.ClientAssertionType, string? cdrArrangementId = null, string? clientAssertion = null, string? certificateFilename = null, string? certificatePassword = null, string? jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, string? jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword); } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/Interfaces/IDataHolderIntrospectionService.cs b/Source/CdrAuthServer.IntegrationTests/Interfaces/IDataHolderIntrospectionService.cs index 68e13c3..4ecdc9d 100644 --- a/Source/CdrAuthServer.IntegrationTests/Interfaces/IDataHolderIntrospectionService.cs +++ b/Source/CdrAuthServer.IntegrationTests/Interfaces/IDataHolderIntrospectionService.cs @@ -8,4 +8,4 @@ public interface IDataHolderIntrospectionService { Task SendRequest(string? grantType = "client_credentials", string? clientId = null, string? clientAssertionType = Constants.ClientAssertionType, string? clientAssertion = null, string? token = null, string? tokenTypeHint = "refresh_token"); } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/Models/DcrResponse.cs b/Source/CdrAuthServer.IntegrationTests/Models/DcrResponse.cs index 38193dc..0a7b98c 100644 --- a/Source/CdrAuthServer.IntegrationTests/Models/DcrResponse.cs +++ b/Source/CdrAuthServer.IntegrationTests/Models/DcrResponse.cs @@ -3,40 +3,75 @@ public class DcrResponse { public string? iss { get; set; } + public string? iat { get; set; } + public string? exp { get; set; } + public string? jti { get; set; } + public string? aud { get; set; } + public string? client_id { get; set; } + public string? client_id_issued_at { get; set; } + public string? client_name { get; set; } + public string? client_description { get; set; } + public string? client_uri { get; set; } + public string? org_id { get; set; } + public string? org_name { get; set; } + public string[]? redirect_uris { get; set; } + public string? logo_uri { get; set; } + public string? tos_uri { get; set; } + public string? policy_uri { get; set; } + public string? jwks_uri { get; set; } + public string? revocation_uri { get; set; } + public string? sector_identifier_uri { get; set; } + public string? recipient_base_uri { get; set; } + public string? token_endpoint_auth_method { get; set; } + public string? token_endpoint_auth_signing_alg { get; set; } + public string[]? grant_types { get; set; } + public string[]? response_types { get; set; } + public string? application_type { get; set; } + public string? id_token_signed_response_alg { get; set; } + public string? id_token_encrypted_response_alg { get; set; } + public string? id_token_encrypted_response_enc { get; set; } + public string? request_object_signing_alg { get; set; } + public string? software_statement { get; set; } + public string? software_id { get; set; } + public string? software_roles { get; set; } + public string? scope { get; set; } + public string? authorization_signed_response_alg { get; set; } + public string? authorization_encrypted_response_alg { get; set; } + public string? authorization_encrypted_response_enc { get; set; } } } diff --git a/Source/CdrAuthServer.IntegrationTests/Models/OpenIdConfiguration.cs b/Source/CdrAuthServer.IntegrationTests/Models/OpenIdConfiguration.cs index 5411a64..e34edb4 100644 --- a/Source/CdrAuthServer.IntegrationTests/Models/OpenIdConfiguration.cs +++ b/Source/CdrAuthServer.IntegrationTests/Models/OpenIdConfiguration.cs @@ -1,32 +1,49 @@ #undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON - namespace CdrAuthServer.IntegrationTests.Models { public class OpenIdConfiguration { public string? issuer { get; set; } + public string? authorization_endpoint { get; set; } + public string? jwks_uri { get; set; } + public string? token_endpoint { get; set; } + public string? introspection_endpoint { get; set; } + public string? userinfo_endpoint { get; set; } + public string? registration_endpoint { get; set; } + public string? revocation_endpoint { get; set; } + public string? cdr_arrangement_revocation_endpoint { get; set; } + public string? pushed_authorization_request_endpoint { get; set; } + public string[]? claims_supported { get; set; } + public string[]? scopes_supported { get; set; } + public string[]? response_types_supported { get; set; } + public string[]? response_modes_supported { get; set; } + public string[]? grant_types_supported { get; set; } + public string[]? subject_types_supported { get; set; } + public string[]? id_token_signing_alg_values_supported { get; set; } + public string[]? token_endpoint_auth_signing_alg_values_supported { get; set; } + public string[]? token_endpoint_auth_methods_supported { get; set; } - public string[]? id_token_encryption_alg_values_supported { get; set; } - public string[]? id_token_encryption_enc_values_supported { get; set; } + public string? tls_client_certificate_bound_access_tokens { get; set; } + public string[]? acr_values_supported { get; set; } } } diff --git a/Source/CdrAuthServer.IntegrationTests/Services/AuthorizationService.cs b/Source/CdrAuthServer.IntegrationTests/Services/AuthorizationService.cs index 70ff87d..2caedcd 100644 --- a/Source/CdrAuthServer.IntegrationTests/Services/AuthorizationService.cs +++ b/Source/CdrAuthServer.IntegrationTests/Services/AuthorizationService.cs @@ -29,23 +29,20 @@ public AuthorizationService(IOptions options, IOptions GetToken(TokenType tokenType, + public async Task GetToken( + TokenType tokenType, string? clientId = null, int tokenLifetime = Constants.AuthServer.DefaultTokenLifetime, int sharingDuration = Constants.AuthServer.SharingDuration) { - - if (clientId == null) - { - clientId = _options.LastRegisteredClientId; - } + clientId ??= _options.LastRegisteredClientId; var userId = tokenType switch { TokenType.KamillaSmith => Constants.Users.UserIdKamillaSmith, TokenType.MaryMoss => Constants.Users.Energy.UserIdMaryMoss, TokenType.JaneWilson => Constants.Users.Banking.UserIdJaneWilson, - _ => throw new ArgumentException($"{nameof(GetToken)} - Unsupported token type - {tokenType}") + _ => throw new ArgumentException($"{nameof(GetToken)} - Unsupported token type - {tokenType}"), }; var scope = tokenType switch @@ -53,7 +50,7 @@ public async Task GetToken(TokenType tokenType, TokenType.KamillaSmith => Constants.Scopes.ScopeBanking, TokenType.JaneWilson => Constants.Scopes.ScopeBanking, TokenType.MaryMoss => Constants.Scopes.ScopeEnergy, - _ => throw new ArgumentException($"{nameof(GetToken)} - Unsupported token type - {tokenType}") + _ => throw new ArgumentException($"{nameof(GetToken)} - Unsupported token type - {tokenType}"), }; var authService = await new DataHolderAuthoriseService.DataHolderAuthoriseServiceBuilder(_options, _dataHolderParService, _apiServiceDirector, false, _authServerOptions) @@ -68,11 +65,30 @@ public async Task GetToken(TokenType tokenType, // User authCode to get tokens var tokenResponse = await _dataHolderTokenService.GetResponse(authCode); - if (tokenResponse == null) throw new InvalidOperationException($"{nameof(GetToken)} - TokenResponse is null"); - if (tokenResponse.IdToken == null) throw new InvalidOperationException($"{nameof(GetToken)} - Id token is null"); - if (tokenResponse.AccessToken == null) throw new InvalidOperationException($"{nameof(GetToken)} - Access token is null"); - if (tokenResponse.RefreshToken == null) throw new InvalidOperationException($"{nameof(GetToken)} - Refresh token is null"); - if (tokenResponse.CdrArrangementId == null) throw new InvalidOperationException($"{nameof(GetToken)} - CdrArrangementId is null"); + if (tokenResponse == null) + { + throw new InvalidOperationException($"{nameof(GetToken)} - TokenResponse is null"); + } + + if (tokenResponse.IdToken == null) + { + throw new InvalidOperationException($"{nameof(GetToken)} - Id token is null"); + } + + if (tokenResponse.AccessToken == null) + { + throw new InvalidOperationException($"{nameof(GetToken)} - Access token is null"); + } + + if (tokenResponse.RefreshToken == null) + { + throw new InvalidOperationException($"{nameof(GetToken)} - Refresh token is null"); + } + + if (tokenResponse.CdrArrangementId == null) + { + throw new InvalidOperationException($"{nameof(GetToken)} - CdrArrangementId is null"); + } // Return access token return tokenResponse; diff --git a/Source/CdrAuthServer.IntegrationTests/Services/DataHolderCDRArrangementRevocationService.cs b/Source/CdrAuthServer.IntegrationTests/Services/DataHolderCDRArrangementRevocationService.cs index 6e55347..4ee971b 100644 --- a/Source/CdrAuthServer.IntegrationTests/Services/DataHolderCDRArrangementRevocationService.cs +++ b/Source/CdrAuthServer.IntegrationTests/Services/DataHolderCDRArrangementRevocationService.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Options; using Constants = ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Constants; - namespace CdrAuthServer.IntegrationTests.Services { public class DataHolderCDRArrangementRevocationService : IDataHolderCDRArrangementRevocationService @@ -33,10 +32,7 @@ public async Task SendRequest( string? jwtCertificateFilename = Constants.Certificates.JwtCertificateFilename, string? jwtCertificatePassword = Constants.Certificates.JwtCertificatePassword) { - if (clientId == null) - { - clientId = _options.LastRegisteredClientId; - } + clientId ??= _options.LastRegisteredClientId; var URL = $"{_options.DH_MTLS_GATEWAY_URL}/connect/arrangements/revoke"; @@ -45,27 +41,30 @@ public async Task SendRequest( { formFields.Add(new KeyValuePair("grant_type", grantType)); } + if (clientId != null) { formFields.Add(new KeyValuePair("client_id", clientId.ToLower())); } + if (clientAssertionType != null) { formFields.Add(new KeyValuePair("client_assertion_type", clientAssertionType)); } + if (cdrArrangementId != null) { formFields.Add(new KeyValuePair("cdr_arrangement_id", cdrArrangementId)); } + formFields.Add(new KeyValuePair("client_assertion", clientAssertion ?? new PrivateKeyJwtService() { CertificateFilename = jwtCertificateFilename, CertificatePassword = jwtCertificatePassword, Issuer = clientId ?? throw new NullReferenceException(nameof(clientId)), - Audience = URL - }.Generate() - )); + Audience = URL, + }.Generate())); var content = new FormUrlEncodedContent(formFields); using var client = Helpers.Web.CreateHttpClient(certificateFilename ?? Constants.Certificates.CertificateFilename, certificatePassword ?? Constants.Certificates.CertificatePassword); diff --git a/Source/CdrAuthServer.IntegrationTests/Services/DataHolderIntrospectionService.cs b/Source/CdrAuthServer.IntegrationTests/Services/DataHolderIntrospectionService.cs index 2c0364b..b3aade5 100644 --- a/Source/CdrAuthServer.IntegrationTests/Services/DataHolderIntrospectionService.cs +++ b/Source/CdrAuthServer.IntegrationTests/Services/DataHolderIntrospectionService.cs @@ -1,4 +1,4 @@ -using CdrAuthServer.IntegrationTests.Interfaces; +using CdrAuthServer.IntegrationTests.Interfaces; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; @@ -35,7 +35,7 @@ public class Response [JsonProperty("cdr_arrangement_id")] public string? CdrArrangementId { get; set; } - }; + } public async Task SendRequest( string? grantType = "client_credentials", @@ -45,10 +45,7 @@ public async Task SendRequest( string? token = null, string? tokenTypeHint = "refresh_token") { - if (clientId == null) - { - clientId = _options.LastRegisteredClientId; - } + clientId ??= _options.LastRegisteredClientId; var URL = $"{_options.DH_MTLS_GATEWAY_URL}/connect/introspect"; @@ -57,31 +54,35 @@ public async Task SendRequest( { formFields.Add(new KeyValuePair("grant_type", grantType)); } + if (clientId != null) { formFields.Add(new KeyValuePair("client_id", clientId.ToLower())); } + if (clientAssertionType != null) { formFields.Add(new KeyValuePair("client_assertion_type", clientAssertionType)); } + formFields.Add(new KeyValuePair("client_assertion", clientAssertion ?? new PrivateKeyJwtService() { CertificateFilename = Constants.Certificates.JwtCertificateFilename, CertificatePassword = Constants.Certificates.JwtCertificatePassword, Issuer = clientId ?? throw new NullReferenceException(nameof(clientId)), - Audience = URL - }.Generate() - )); + Audience = URL, + }.Generate())); if (token != null) { formFields.Add(new KeyValuePair("token", token)); } + if (tokenTypeHint != null) { formFields.Add(new KeyValuePair("token_type_hint", tokenTypeHint)); } + var content = new FormUrlEncodedContent(formFields); using var client = Helpers.Web.CreateHttpClient(Constants.Certificates.CertificateFilename, Constants.Certificates.CertificatePassword); @@ -93,7 +94,7 @@ public async Task SendRequest( return responseMessage; } - static public async Task DeserializeResponse(HttpResponseMessage response) + public static async Task DeserializeResponse(HttpResponseMessage response) { var responseContent = await response.Content.ReadAsStringAsync(); diff --git a/Source/CdrAuthServer.IntegrationTests/Startup.cs b/Source/CdrAuthServer.IntegrationTests/Startup.cs index bb41a4f..8976ddb 100644 --- a/Source/CdrAuthServer.IntegrationTests/Startup.cs +++ b/Source/CdrAuthServer.IntegrationTests/Startup.cs @@ -20,14 +20,14 @@ public Startup(IConfiguration configuration) public static void ConfigureServices(IServiceCollection services) { - //Setup config + // Setup config var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true) .AddEnvironmentVariables() .Build(); - //Setting up logger early so we can catch any startup issues + // Setting up logger early so we can catch any startup issues Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration: configuration) .CreateBootstrapLogger(); @@ -39,7 +39,7 @@ public static void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); - //Common startup + // Common startup services.AddMvc().AddCdrNewtonsoftJson(); services.AddTestAutomationServices(configuration); @@ -50,41 +50,41 @@ public static void ConfigureServices(IServiceCollection services) opt.INDUSTRY = Industry.BANKING; opt.SCOPE = ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Constants.Scopes.ScopeBanking; - opt.DH_MTLS_GATEWAY_URL = configuration["URL:DH_MTLS_Gateway"] ?? ""; - opt.DH_TLS_AUTHSERVER_BASE_URL = configuration["URL:DH_TLS_AuthServer"] ?? ""; - opt.DH_TLS_PUBLIC_BASE_URL = configuration["URL:DH_TLS_Public"] ?? ""; - opt.REGISTER_MTLS_URL = configuration["URL:Register_MTLS"] ?? ""; + opt.DH_MTLS_GATEWAY_URL = configuration["URL:DH_MTLS_Gateway"] ?? string.Empty; + opt.DH_TLS_AUTHSERVER_BASE_URL = configuration["URL:DH_TLS_AuthServer"] ?? string.Empty; + opt.DH_TLS_PUBLIC_BASE_URL = configuration["URL:DH_TLS_Public"] ?? string.Empty; + opt.REGISTER_MTLS_URL = configuration["URL:Register_MTLS"] ?? string.Empty; + // Connection strings - opt.DATAHOLDER_CONNECTIONSTRING = configuration["ConnectionStrings:DataHolder"] ?? ""; - opt.AUTHSERVER_CONNECTIONSTRING = configuration["ConnectionStrings:AuthServer"] ?? ""; - opt.REGISTER_CONNECTIONSTRING = configuration["ConnectionStrings:Register"] ?? ""; + opt.DATAHOLDER_CONNECTIONSTRING = configuration["ConnectionStrings:DataHolder"] ?? string.Empty; + opt.AUTHSERVER_CONNECTIONSTRING = configuration["ConnectionStrings:AuthServer"] ?? string.Empty; + opt.REGISTER_CONNECTIONSTRING = configuration["ConnectionStrings:Register"] ?? string.Empty; // Seed-data offset opt.SEEDDATA_OFFSETDATES = configuration["SeedData:OffsetDates"] == "true"; - opt.MDH_INTEGRATION_TESTS_HOST = configuration["URL:MDH_INTEGRATION_TESTS_HOST"] ?? ""; - opt.MDH_HOST = configuration["URL:MDH_HOST"] ?? ""; + opt.MDH_INTEGRATION_TESTS_HOST = configuration["URL:MDH_INTEGRATION_TESTS_HOST"] ?? string.Empty; + opt.MDH_HOST = configuration["URL:MDH_HOST"] ?? string.Empty; - opt.CDRAUTHSERVER_SECUREBASEURI = configuration["URL:CDRAuthServer_SecureBaseUri"] ?? ""; + opt.CDRAUTHSERVER_SECUREBASEURI = configuration["URL:CDRAuthServer_SecureBaseUri"] ?? string.Empty; // Playwright settings. opt.RUNNING_IN_CONTAINER = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")?.ToUpper() == "TRUE"; opt.CREATE_MEDIA = configuration.GetValue("CreateMedia"); opt.TEST_TIMEOUT = configuration.GetValue("TestTimeout"); - opt.MEDIA_FOLDER = configuration["MediaFolder"] ?? ""; + opt.MEDIA_FOLDER = configuration["MediaFolder"] ?? string.Empty; }); services.AddTestAutomationAuthServerSettings(opt => { - opt.CDRAUTHSERVER_BASEURI = configuration["URL:CDRAuthServer_BaseUri"] ?? ""; + opt.CDRAUTHSERVER_BASEURI = configuration["URL:CDRAuthServer_BaseUri"] ?? string.Empty; opt.STANDALONE = configuration.GetValue("Standalone"); - opt.XTLSCLIENTCERTTHUMBPRINT = configuration["XTlsClientCertThumbprint"] ?? ""; - opt.XTLSADDITIONALCLIENTCERTTHUMBPRINT = configuration["XTlsAdditionalClientCertThumbprint"] ?? ""; + opt.XTLSCLIENTCERTTHUMBPRINT = configuration["XTlsClientCertThumbprint"] ?? string.Empty; + opt.XTLSADDITIONALCLIENTCERTTHUMBPRINT = configuration["XTlsAdditionalClientCertThumbprint"] ?? string.Empty; opt.ACCESSTOKENLIFETIMESECONDS = Convert.ToInt32(configuration["AccessTokenLifetimeSeconds"]); opt.HEADLESSMODE = configuration.GetValue("HeadlessMode"); opt.JARM_ENCRYPTION_ON = (Environment.GetEnvironmentVariable("USE_JARM_ENCRYPTION") ?? "false").ToUpper() == "TRUE"; }); - } } } diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_Authorise.cs b/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_Authorise.cs index 26b614d..54e7e06 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_Authorise.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_Authorise.cs @@ -1,4 +1,4 @@ -#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON +#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; @@ -22,11 +22,6 @@ namespace CdrAuthServer.IntegrationTests.JARM { - /* - * MJS - CDRAuthServer tests are run in the CDRAuthServer pipeline, but also run again in the MDH/MDHE pipelines where they test the CdrAuthserver when - * it's running embedded inside MDH/MDHE, ie, testing MDH/MDHE should just happen implicity when CDRAuthServer tests are MDH/MDHE pipeline. - */ - // JARM - Authorise related tests public class US44264_CdrAuthServer_JARM_Authorise : BaseTest, IClassFixture { @@ -54,7 +49,6 @@ public US44264_CdrAuthServer_JARM_Authorise(IOptions opti private class ParResponse { public string? request_uri; - public int? expires_in; } [Fact] @@ -68,8 +62,7 @@ public async Task AC01_MDH_JARM_AC01_HappyPath_JARM_Response_Mode_jwt() scope: _options.SCOPE, responseMode: ResponseMode.Jwt, responseType: responseType, - state: STATE - ); + state: STATE); var parResponseMessageContent = await parResponseMessage.Content.ReadAsStringAsync(); parResponseMessage.StatusCode.Should().Be(HttpStatusCode.Created, because: parResponseMessageContent); @@ -83,7 +76,6 @@ public async Task AC01_MDH_JARM_AC01_HappyPath_JARM_Response_Mode_jwt() .WithRequestUri(parResponse.request_uri) .BuildAsync(); - HttpResponseMessage response = await authService.AuthoriseForJarm(); // Assert @@ -107,6 +99,7 @@ public async Task AC01_MDH_JARM_AC01_HappyPath_JARM_Response_Mode_jwt() var encryptedJwt = new JwtSecurityTokenHandler().ReadJwtToken(encodedJwt); encryptedJwt.Header["alg"].Should().Be("RSA-OAEP", because: "JARM Encryption is turned on."); encryptedJwt.Header["enc"].Should().Be("A128CBC-HS256", because: "JARM Encryption is turned on."); + // Decrypt the JARM JWT. var privateKeyCertificate = new X509Certificate2(Constants.Certificates.JwtCertificateFilename, Constants.Certificates.JwtCertificatePassword, X509KeyStorageFlags.Exportable); var privateKey = privateKeyCertificate.GetRSAPrivateKey(); @@ -126,4 +119,4 @@ public async Task AC01_MDH_JARM_AC01_HappyPath_JARM_Response_Mode_jwt() } } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_DCR.cs b/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_DCR.cs index 40c4e47..fa0a0a4 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_DCR.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_DCR.cs @@ -1,9 +1,12 @@ -// #define DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON +// #define DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON +using System.Net; using CdrAuthServer.IntegrationTests.Models; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Extensions; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Fixtures; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; @@ -15,31 +18,20 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; using Serilog; -using System.Net; using Xunit; using Xunit.DependencyInjection; -using static IdentityModel.ClaimComparer; -using static IdentityModel.OidcConstants; using Constants = ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Constants; -namespace CdrAuthServer.IntegrationTests.JARM +namespace CdrAuthServer.IntegrationTests.Tests.JARM { // JARM - DCR related tests public partial class US44264_CdrAuthServer_JARM_DCR : BaseTest, IClassFixture { - public const string HYBRIDFLOW_RESPONSETYPE = "code id_token"; - public const string HYBRIDFLOW_GRANTTYPES = "authorization_code,client_credentials"; public const string AUTHORIZATIONCODEFLOW_RESPONSETYPE = "code"; public const string AUTHORIZATIONCODEFLOW_GRANTTYPES = "authorization_code"; - public const string ACF_HF_RESPONSETYPES = AUTHORIZATIONCODEFLOW_RESPONSETYPE + "," + HYBRIDFLOW_RESPONSETYPE; - public const string ACF_HF_GRANTTYPES = "authorization_code,client_credentials"; public const string IDTOKEN_SIGNED_RESPONSE_ALG_ES256 = "ES256"; public const string IDTOKEN_SIGNED_RESPONSE_ALG_PS256 = "PS256"; - public const string IDTOKEN_ENCRYPTED_RESPONSE_ALG_RSA_OAEP = "RSA-OAEP"; - public const string IDTOKEN_ENCRYPTED_RESPONSE_ALG_RSA_OAEP_256 = "RSA-OAEP-256"; - public const string IDTOKEN_ENCRYPTED_RESPONSE_ENC_A256GCM = "A256GCM"; - public const string IDTOKEN_ENCRYPTED_RESPONSE_ENC_A128CBC_HS256 = "A128CBC-HS256"; public const string AUTHORIZATION_SIGNED_RESPONSE_ALG_ES256 = "ES256"; public const string AUTHORIZATION_SIGNED_RESPONSE_ALG_PS256 = "PS256"; @@ -72,82 +64,8 @@ public US44264_CdrAuthServer_JARM_DCR(IOptions options, I } [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] - public async Task AC01_MDHJWKS_HF_AC1_POST_With_ShouldRespondWith_201Created(string responseType, string grantTypes, string authorizationSignedResponseAlg) - { - Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); - - //TODO: I think commented code below was looking for a clean way to replace TestPOST. Continue this - //var registerDetails = LookupRegisterDetails(Constants.SoftwareProducts.SOFTWAREPRODUCT_ID); - - //Helpers.AuthServer.DataHolder_PurgeAuthServer(_options); - //var ssa = await _registerSSAService.GetSSA(Constants.Brands.BRANDID, Constants.SoftwareProducts.SOFTWAREPRODUCT_ID, "3"); - - //// Act - //HttpResponseMessage responseMessage = await RegisterSoftwareProduct( - // responseType, - // grantTypes.Split(','), // MJS - CDRAuthServer (see log) fails with:- validation failed: [{"MemberNames": ["GrantTypes"], "ErrorMessage": "The 'grant_types' claim value must contain the 'authorization_code' value.", "$type": "ValidationResult"}] - // null, - // ssa, - // GetIdTokenEncryptedResponseAlg(responseType), - // GetIdTokenEncryptedResponseEnc(responseType) - // ); - - //// Assert - //using (new AssertionScope(BaseTestAssertionStrategy)) - //{ - // if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.Created)) - // { - // return; - // } - - // Assertions.Assert_HasContentType_ApplicationJson(responseMessage.Content); - - // await AssertDCR( - // responseType, - // grantTypes.Split(','), - // authorizationSignedResponseAlg, - // responseMessage, - // registerDetails, - // ssa, - // idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - // idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType)); - - // testPostDelegate(responseMessage, registerDetails, ssa); - //} - - await TestPOST( - responseType: responseType, - grant_types: grantTypes.Split(','), // MJS - CDRAuthServer (see log) fails with:- validation failed: [{"MemberNames": ["GrantTypes"], "ErrorMessage": "The 'grant_types' claim value must contain the 'authorization_code' value.", "$type": "ValidationResult"}] - authorization_signed_response_alg: null, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - - testPostDelegate: async (responseMessage, registerDetails, ssa) => - { - if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.Created)) - { - return; - } - - Assertions.AssertHasContentTypeApplicationJson(responseMessage.Content); - - await AssertDCR( - responseType, - grantTypes.Split(','), - authorizationSignedResponseAlg, - responseMessage, - registerDetails, - ssa, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType)); - }); - } - - [Theory] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "ES256")] - public async Task AC02_MDHJWKS_ACF_AC20_POST_With_AuthSigningAlgES256_ShouldRespondWith_201Created(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "ES256")] + public async Task AC02_MDHJWKS_ACF_AC20_POST_With_AuthSigningAlgES256_ShouldRespondWith_201Created(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); @@ -155,10 +73,6 @@ await TestPOST( responseType: responseType, grant_types: grantTypes.Split(','), authorization_signed_response_alg: authorizationSignedResponseAlg, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - testPostDelegate: async (responseMessage, registerDetails, ssa) => { if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.Created)) @@ -173,26 +87,20 @@ await AssertDCR( authorizationSignedResponseAlg, responseMessage, registerDetails, - ssa, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType)); + ssa); }); } [Theory] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] - public async Task AC03_MDHJWKS_ACF_AC21_POST_With_AuthSigningAlgPS256_ShouldRespondWith_201Created(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] + public async Task AC03_MDHJWKS_ACF_AC21_POST_With_AuthSigningAlgPS256_ShouldRespondWith_201Created(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); - + await TestPOST( responseType: responseType, grant_types: grantTypes.Split(','), authorization_signed_response_alg: authorizationSignedResponseAlg, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - testPostDelegate: async (responseMessage, registerDetails, ssa) => { if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.Created)) @@ -207,23 +115,21 @@ await AssertDCR( authorizationSignedResponseAlg, responseMessage, registerDetails, - ssa, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType)); + ssa); }); } [Theory] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, Constants.Null)] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "")] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "none")] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "RS256")] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "Foo")] - public async Task AC05_MDHJWKS_ACF_AC24_POST_With_InvalidAuthSigningAlg_ShouldRespondWith_400BadRequest(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, Constants.Null)] + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "")] + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "none")] + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "RS256")] + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "Foo")] + public async Task AC05_MDHJWKS_ACF_AC24_POST_With_InvalidAuthSigningAlg_ShouldRespondWith_400BadRequest(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); - //Arrange + // Arrange AuthoriseException expectedError; if (string.IsNullOrEmpty(authorizationSignedResponseAlg) || authorizationSignedResponseAlg == Constants.Null) { @@ -234,17 +140,7 @@ public async Task AC05_MDHJWKS_ACF_AC24_POST_With_InvalidAuthSigningAlg_ShouldRe expectedError = new AuthorizationSignedResponseAlgClaimInvalidException(); } - ////Act - //var responseMessage = await TestRegisterSoftwareProduct( - // responseType: responseType, - // grant_types: grantTypes.Split(','), - // authorization_signed_response_alg: authorizationSignedResponseAlg - // ); - - ////Asert - //await AssertError(responseMessage, expectedError); - - //Act/Assert + // Act/Assert await TestPOST( responseType: responseType, grant_types: grantTypes.Split(','), @@ -256,29 +152,24 @@ await TestPOST( } [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] - public async Task AC06_MDHJWKS_AC2_POST_With_SoftwareIDAlreadyRegistered_ShouldRespondWith_400BadRequest(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] + public async Task AC06_MDHJWKS_AC2_POST_With_SoftwareIDAlreadyRegistered_ShouldRespondWith_400BadRequest(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); // Arrange AuthoriseException expectedError = new DuplicateRegistrationForSoftwareIdException(); - //var registerDetails = LookupRegisterDetails(Constants.SoftwareProducts.SOFTWAREPRODUCT_ID); - Helpers.AuthServer.PurgeAuthServerForDataholder(_options); var ssa = await _registerSSAService.GetSSA(Constants.Brands.BrandId, Constants.SoftwareProducts.SoftwareProductId, _latestSSAVersion); // register software product - HttpResponseMessage responseMessageSetup = await RegisterSoftwareProduct( + var responseMessageSetup = await RegisterSoftwareProduct( responseType: responseType, grant_types: grantTypes.Split(','), authorization_signed_response_alg: authorizationSignedResponseAlg, ssa, - idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType)); + idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256); // Check it was registered if (responseMessageSetup.StatusCode != HttpStatusCode.Created) @@ -287,14 +178,12 @@ public async Task AC06_MDHJWKS_AC2_POST_With_SoftwareIDAlreadyRegistered_ShouldR } // Act - Now try and register it again - HttpResponseMessage responseMessage = await RegisterSoftwareProduct( + var responseMessage = await RegisterSoftwareProduct( responseType: responseType, grant_types: grantTypes.Split(','), authorization_signed_response_alg: authorizationSignedResponseAlg, ssa, - idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType)); + idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -309,21 +198,21 @@ internal Task AC07_MDHJWKS_AC3_POST_With_UnapprovedSSA_ShouldRespondWith_400BadR throw new NotImplementedException(); } - // [Fact] + // [Fact] internal Task AC08_MDHJWKS_AC6_POST_With_InvalidSSAPayload_RedirectURI_ShouldRespondWith_400BadRequest() { throw new NotImplementedException(); // MJS - SSA is signed by Register, would need to create invalid SSA and sign with Register cert? } - // [Fact] + // [Fact] internal Task AC09_MDHJWKS_AC7_POST_With_InvalidSSAPayload_TokenEndpointAuthSigningALG_ShouldRespondWith_400BadRequest() { throw new NotImplementedException(); // MJS - SSA is signed by Register, would need to create invalid SSA and sign with Register cert? } [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] - public async Task AC09_MDHJWKS_AC8_GET_ShouldRespondWith_200OK(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] + public async Task AC10_MDHJWKS_AC28_GET_ShouldRespondWith_200OK(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); @@ -331,10 +220,6 @@ await TestGET( responseType: responseType, grant_types: grantTypes.Split(','), authorization_signed_response_alg: authorizationSignedResponseAlg, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - testGetDelegate: async (responseMessage, registerDetails, ssa) => { if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.OK)) @@ -349,50 +234,13 @@ await AssertDCR( authorizationSignedResponseAlg, responseMessage, registerDetails, - ssa, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType)); + ssa); }); } [Theory] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] - public async Task AC10_MDHJWKS_AC28_GET_ShouldRespondWith_200OK(string responseType, string grantTypes, string authorizationSignedResponseAlg) - { - Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); - - await TestGET( - responseType: responseType, - grant_types: grantTypes.Split(','), - authorization_signed_response_alg: authorizationSignedResponseAlg, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - - testGetDelegate: async (responseMessage, registerDetails, ssa) => - { - if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.OK)) - { - return; - } - - Assertions.AssertHasContentTypeApplicationJson(responseMessage.Content); - await AssertDCR( - responseType, - grantTypes.Split(','), - authorizationSignedResponseAlg, - responseMessage, - registerDetails, - ssa, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType) ?? "", // returns "" for idTokenEncryptedResponseAlg - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType) ?? ""); // returns "" for idTokenEncryptedResponseEnc - }); - } - - [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] - public async Task AC11_MDHJWKS_AC9_GET_With_ExpiredBearerToken_ShouldRespondWith_401Unauthorized(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] + public async Task AC11_MDHJWKS_AC9_GET_With_ExpiredBearerToken_ShouldRespondWith_401Unauthorized(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); @@ -401,23 +249,20 @@ await TestGET( grant_types: grantTypes.Split(','), authorization_signed_response_alg: authorizationSignedResponseAlg, expiredAccessToken: true, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - testGetDelegate: (responseMessage, registerDetails, ssa) => { - responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); // Assert - Check WWWAutheticate header - Assertions.AssertHasHeader(@"Bearer error=""invalid_token"", error_description=""The token expired at ", + Assertions.AssertHasHeader( + @"Bearer error=""invalid_token"", error_description=""The token expired at ", responseMessage.Headers, "WWW-Authenticate", startsWith: true); }); } - // [Theory] + // [Theory] // [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] // [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] internal Task AC12_MDHJWKS_AC10_GET_With_ClientIdNotRegistered_ShouldRespondWith_401Unauthorized(string responseType, string grantTypes, string authorizationSignedResponseAlg) @@ -426,8 +271,8 @@ internal Task AC12_MDHJWKS_AC10_GET_With_ClientIdNotRegistered_ShouldRespondWith } [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] - public async Task AC13_MDHJWKS_HF_AC11_PUT_With_ShouldRespondWith_200OK(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] + public async Task AC14_MDHJWKS_ACF_AC31_PUT_With_ShouldRespondWith_200OK(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); @@ -435,10 +280,6 @@ await TestPUT( responseType: responseType, grantTypes: grantTypes.Split(','), authorizationSignedResponseAlg: authorizationSignedResponseAlg, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - testPutDelegate: async (responseMessage, registerDetails, ssa) => { if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.OK)) @@ -453,79 +294,36 @@ await AssertDCR( authorizationSignedResponseAlg, responseMessage, registerDetails, - ssa, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType) ?? "", // returns "" for idTokenEncryptedResponseAlg - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType) ?? ""); // returns "" for idTokenEncryptedResponseEnc + ssa); }); } [Theory] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] - public async Task AC14_MDHJWKS_ACF_AC31_PUT_With_ShouldRespondWith_200OK(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] + public async Task AC15_MDHJWKS_AC12_PUT_With_InvalidRegistrationProperty_RedirectUri_ShouldRespondWith_400BadRequest(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); - await TestPUT( - responseType: responseType, - grantTypes: grantTypes.Split(','), - authorizationSignedResponseAlg: authorizationSignedResponseAlg, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - - testPutDelegate: async (responseMessage, registerDetails, ssa) => - { - if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.OK)) - { - return; - } - - Assertions.AssertHasContentTypeApplicationJson(responseMessage.Content); - await AssertDCR( - responseType, - grantTypes.Split(','), - authorizationSignedResponseAlg, - responseMessage, - registerDetails, - ssa, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType) ?? "", // returns "" for idTokenEncryptedResponseAlg - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType) ?? ""); // returns "" for idTokenEncryptedResponseEnc - }); - } - - - [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] - public async Task AC15_MDHJWKS_AC12_PUT_With_InvalidRegistrationProperty_RedirectUri_ShouldRespondWith_400BadRequest(string responseType, string grantTypes, string authorizationSignedResponseAlg) - { - Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); - - //Arrange + // Arrange const string REDIRECT_URI = "foo"; var expectedError = new InvalidRedirectUriException(REDIRECT_URI); - //Act-Assert + // Act-Assert await TestPUT( responseType: responseType, grantTypes: grantTypes.Split(','), authorizationSignedResponseAlg: authorizationSignedResponseAlg, - redirectUrisForPut: new string[] { REDIRECT_URI }, // we are testing if invalid URI for PUT fails - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - - testPutDelegate: async (responseMessage, registerDetails, ssa) => + redirectUrisForPut: [REDIRECT_URI], // we are testing if invalid URI for PUT fails + testPutDelegate: async ( + responseMessage, registerDetails, ssa) => { await Assertions.AssertErrorAsync(responseMessage, expectedError); }); } - [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] - public async Task AC16_MDHJWKS_AC13_PUT_With_ExpiredBearerToken_ShouldRespondWith_401Unauthorized(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] + public async Task AC16_MDHJWKS_AC13_PUT_With_ExpiredBearerToken_ShouldRespondWith_401Unauthorized(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); @@ -535,22 +333,20 @@ await TestPUT( authorizationSignedResponseAlg: authorizationSignedResponseAlg, expiredAccessToken: true, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - testPutDelegate: (responseMessage, registerDetails, ssa) => { - responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); // Assert - Check WWWAutheticate header - Assertions.AssertHasHeader(@"Bearer error=""invalid_token"", error_description=""The token expired at ", + Assertions.AssertHasHeader( + @"Bearer error=""invalid_token"", error_description=""The token expired at ", responseMessage.Headers, "WWW-Authenticate", true); }); } - // [Theory] + // [Theory] // [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] // [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] internal Task AC17_MDHJWKS_AC14_PUT_With_ClientIDNotRegistered_ShouldRespondWith_401Unauthorized(string responseType, string grantTypes, string authorizationSignedResponseAlg) @@ -559,9 +355,8 @@ internal Task AC17_MDHJWKS_AC14_PUT_With_ClientIDNotRegistered_ShouldRespondWith } [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] - public async Task AC18_MDHJWKS_AC15_DELETE_ShouldRespondWith_204NoContent(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] + public async Task AC18_MDHJWKS_AC15_DELETE_ShouldRespondWith_204NoContent(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); @@ -569,10 +364,6 @@ await TestDELETE( responseType: responseType, grant_types: grantTypes.Split(','), authorization_signed_response_alg: authorizationSignedResponseAlg, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - testDeleteDelegate: async (responseMessage, registerDetails, ssa) => { if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.NoContent)) @@ -585,9 +376,8 @@ await TestDELETE( } [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null)] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] - public async Task AC19_MDHJWKS_AC17_DELETE_WithExpiredBearerToken_ShouldRespondWith_401Unauthorized(string responseType, string grantTypes, string authorizationSignedResponseAlg) + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, "PS256")] + public async Task AC19_MDHJWKS_AC17_DELETE_WithExpiredBearerToken_ShouldRespondWith_401Unauthorized(ResponseType responseType, string grantTypes, string authorizationSignedResponseAlg) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg); @@ -596,23 +386,21 @@ await TestDELETE( grant_types: grantTypes.Split(','), authorization_signed_response_alg: authorizationSignedResponseAlg, expiredAccessToken: true, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - testDeleteDelegate: (responseMessage, registerDetails, ssa) => { - responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + _ = responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); // Assert - Check WWWAutheticate header - Assertions.AssertHasHeader(@"Bearer error=""invalid_token"", error_description=""The token expired at ", + Assertions.AssertHasHeader( + @"Bearer error=""invalid_token"", error_description=""The token expired at ", responseMessage.Headers, "WWW-Authenticate", startsWith: true); }); } - [Theory, CombinatorialData] + [Theory] + [CombinatorialData] public async Task AC20_MDHJWKS_ACF_AC32_Happy_Path_POST_to_register_endpoint_JARM_Encryption( [CombinatorialValues(AUTHORIZATION_SIGNED_RESPONSE_ALG_ES256, AUTHORIZATION_SIGNED_RESPONSE_ALG_PS256)] string authorizationSignedResponseAlg, @@ -622,29 +410,28 @@ public async Task AC20_MDHJWKS_ACF_AC32_Happy_Path_POST_to_register_endpoint_JAR string authorizationEncryptedResponseEnc) { await TestEncryption( - responseType: AUTHORIZATIONCODEFLOW_RESPONSETYPE, + responseType: ResponseType.Code, grantTypes: AUTHORIZATIONCODEFLOW_GRANTTYPES, authorizationSignedResponseAlg: authorizationSignedResponseAlg, authorizationEncryptedResponseAlg: authorizationEncryptedResponseAlg, authorizationEncryptedResponseEnc: authorizationEncryptedResponseEnc, - idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - idTokenEncryptedResponseAlg: IDTOKEN_ENCRYPTED_RESPONSE_ALG_RSA_OAEP, - idTokenEncryptedResponseEnc: IDTOKEN_ENCRYPTED_RESPONSE_ENC_A256GCM); + idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256); } - // [Fact] + // [Fact] internal Task AC21_MDHJWKS_ACF_AC33_Reject_POST_to_register_endpoint_Invalid_authorization_encrypted_response_alg() { throw new NotImplementedException(); } - // [Fact] + // [Fact] internal Task AC22_MDHJWKS_ACF_AC34_Reject_POST_to_register_endpoint_Invalid_authorization_encrypted_response_enc() { throw new NotImplementedException(); } - [Theory, CombinatorialData] + [Theory] + [CombinatorialData] public async Task AC23_MDHJWKS_ACF_AC35_Happy_Path_POST_to_register_endpoint_JARM_Encryption_alg_specified_without_enc( [CombinatorialValues(AUTHORIZATION_SIGNED_RESPONSE_ALG_ES256, AUTHORIZATION_SIGNED_RESPONSE_ALG_PS256)] string authorizationSignedResponseAlg, @@ -652,137 +439,98 @@ public async Task AC23_MDHJWKS_ACF_AC35_Happy_Path_POST_to_register_endpoint_JAR string authorizationEncryptedResponseAlg) { await TestEncryption( - responseType: AUTHORIZATIONCODEFLOW_RESPONSETYPE, + responseType: ResponseType.Code, grantTypes: AUTHORIZATIONCODEFLOW_GRANTTYPES, authorizationSignedResponseAlg: authorizationSignedResponseAlg, authorizationEncryptedResponseAlg: authorizationEncryptedResponseAlg, authorizationEncryptedResponseEnc: null, - idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - idTokenEncryptedResponseAlg: IDTOKEN_ENCRYPTED_RESPONSE_ALG_RSA_OAEP, - idTokenEncryptedResponseEnc: IDTOKEN_ENCRYPTED_RESPONSE_ENC_A256GCM); + idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256); } - // [Fact] + // [Fact] internal Task AC24_MDHJWKS_ACF_AC36_Reject_POST_to_register_endpoint_Omit_authorization_encrypted_response_alg_when_enc_is_provided() { throw new NotImplementedException(); } [Theory] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, AUTHORIZATIONCODEFLOW_GRANTTYPES, IDTOKEN_SIGNED_RESPONSE_ALG_PS256, null, null, null, null)] + [InlineData(ResponseType.Code, AUTHORIZATIONCODEFLOW_GRANTTYPES, IDTOKEN_SIGNED_RESPONSE_ALG_PS256)] public async Task AC25_MDHJWKS_ACF_AC37_Happy_Path_ACF_only_DCR_without_id_token_encryption( - string responseType, + ResponseType responseType, string grantTypes, - string idTokenSignedResponseAlg, - string idTokenEncryptedResponseAlg, - string idTokenEncryptedResponseEnc, - string expectedIdTokenEncryptedResponseAlg, - string expectedIdTokenEncryptedResponseEnc) + string idTokenSignedResponseAlg) { - Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}, {P4}={V4}, {P5}={V5}, {P6}={V6}, {P7}={V7}.", + Log.Information( + "Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}, {P4}={V4}, {P5}={V5}, {P6}={V6}, {P7}={V7}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, - nameof(idTokenSignedResponseAlg), idTokenSignedResponseAlg, - nameof(idTokenEncryptedResponseAlg), idTokenEncryptedResponseAlg, - nameof(idTokenEncryptedResponseEnc), idTokenEncryptedResponseEnc, - nameof(expectedIdTokenEncryptedResponseAlg), expectedIdTokenEncryptedResponseAlg, - nameof(expectedIdTokenEncryptedResponseEnc), expectedIdTokenEncryptedResponseEnc - ); + nameof(idTokenSignedResponseAlg), idTokenSignedResponseAlg); await TestEncryption( responseType: responseType, grantTypes: grantTypes, authorizationSignedResponseAlg: AUTHORIZATION_SIGNED_RESPONSE_ALG_PS256, - idTokenSignedResponseAlg: idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg: idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc: idTokenEncryptedResponseEnc, - expectedIdTokenEncryptedResponseAlg: expectedIdTokenEncryptedResponseAlg, - expectedIdTokenEncryptedResponseEnc: expectedIdTokenEncryptedResponseEnc); + idTokenSignedResponseAlg: idTokenSignedResponseAlg); } [Fact] public async Task AC26_MDHJWKS_ACF_AC38_Happy_Path_ACF_only_DCR_with_id_token_encryption() { await TestEncryption( - responseType: AUTHORIZATIONCODEFLOW_RESPONSETYPE, + responseType: ResponseType.Code, grantTypes: AUTHORIZATIONCODEFLOW_GRANTTYPES, authorizationSignedResponseAlg: AUTHORIZATION_SIGNED_RESPONSE_ALG_PS256, - idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - idTokenEncryptedResponseAlg: IDTOKEN_ENCRYPTED_RESPONSE_ALG_RSA_OAEP, - idTokenEncryptedResponseEnc: IDTOKEN_ENCRYPTED_RESPONSE_ENC_A256GCM); + idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256); } - // [Fact] + // [Fact] internal Task AC28_MDHJWKS_ACF_AC40_Reject_ACF_DCR_invalid_id_token_signed_response_alg() { throw new NotImplementedException(); } - // [Fact] + // [Fact] internal Task AC29_MDHJWKS_ACF_AC41_Reject_ACF_DCR_invalid_id_token_encrypted_response_alg() { throw new NotImplementedException(); } - // [Fact] + // [Fact] internal Task AC30_MDHJWKS_ACF_AC42_Reject_ACF_DCR_invalid_id_token_encrypted_response_enc() { throw new NotImplementedException(); } - // [Fact] + // [Fact] internal Task AC31_MDHJWKS_HF_AC43_Reject_HF_only_DCR_without_id_token_encryption() { throw new NotImplementedException(); } - [Fact] - public async Task AC32_MDHJWKS_HF_AC44_Happy_Path_HF_only_DCR_with_id_token_encryption() - { - await TestEncryption( - responseType: HYBRIDFLOW_RESPONSETYPE, - grantTypes: HYBRIDFLOW_GRANTTYPES, - idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - idTokenEncryptedResponseAlg: IDTOKEN_ENCRYPTED_RESPONSE_ALG_RSA_OAEP, - idTokenEncryptedResponseEnc: IDTOKEN_ENCRYPTED_RESPONSE_ENC_A256GCM); - } - - // [Fact] + // [Fact] internal Task AC34_MDHJWKS_HF_AC46_Reject_HF_DCR_invalid_id_token_signed_response_alg() { throw new NotImplementedException(); } - // [Fact] + // [Fact] internal Task AC35_MDHJWKS_HF_AC47_Reject_HF_DCR_invalid_id_token_encrypted_response_alg() { throw new NotImplementedException(); } - // [Fact] + // [Fact] internal Task AC36_MDHJWKS_HF_AC48_Reject_HF_DCR_invalid_id_token_encrypted_response_enc() { throw new NotImplementedException(); } - // [Fact] + // [Fact] internal Task AC37_MDHJWKS_ACF_HF_AC49_Reject_ACF_HF_DCR_without_id_token_encryption() { throw new NotImplementedException(); } - [Fact] - public async Task AC38_MDHJWKS_ACF_HF_AC50_Happy_Path_ACF_HF_DCR_with_id_token_encryption() - { - await TestEncryption( - responseType: ACF_HF_RESPONSETYPES, - grantTypes: ACF_HF_GRANTTYPES, - authorizationSignedResponseAlg: AUTHORIZATION_SIGNED_RESPONSE_ALG_PS256, - idTokenSignedResponseAlg: IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - idTokenEncryptedResponseAlg: IDTOKEN_ENCRYPTED_RESPONSE_ALG_RSA_OAEP, - idTokenEncryptedResponseEnc: IDTOKEN_ENCRYPTED_RESPONSE_ENC_A256GCM); - } - // [Fact] internal Task AC40_MDHJWKS_ACF_HF_AC52_Reject_ACF_HF_DCR_invalid_id_token_signed_response_alg() { @@ -802,9 +550,8 @@ internal Task AC42_MDHJWKS_ACF_HF_AC54_Reject_ACF_HF_DCR_invalid_id_token_encryp } [Theory] - [InlineData(AUTHORIZATIONCODEFLOW_RESPONSETYPE, "foo")] - [InlineData(HYBRIDFLOW_RESPONSETYPE, "foo")] - public async Task AC43_MDHJWKS_ACF_HF_AC55_With_InvalidGrantType_ShouldRespondWith_400BadRequest(string responseType, string grantTypes) + [InlineData(ResponseType.Code, "foo")] + public async Task AC43_MDHJWKS_ACF_HF_AC55_With_InvalidGrantType_ShouldRespondWith_400BadRequest(ResponseType responseType, string grantTypes) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes); @@ -814,87 +561,12 @@ await TestPOST( responseType: responseType, grant_types: grantTypes.Split(','), authorization_signed_response_alg: null, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - testPostDelegate: async (responseMessage, registerDetails, ssa) => { await Assertions.AssertErrorAsync(responseMessage, expectedError); }); } - [Theory] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, null, null)] - [InlineData(HYBRIDFLOW_RESPONSETYPE, HYBRIDFLOW_GRANTTYPES, "none", null)] - public async Task AC44_MDHJWKS_HF_AC56_POST_With_AuthorizationSignedResponseAlg_NotProvided_ShouldRespondWith_201Created( - string responseType, - string grantTypes, - string authorizationSignedResponseAlg, - string expected_authorization_signed_response_alg) - { - Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}.", nameof(responseType), responseType, nameof(grantTypes), grantTypes, nameof(authorizationSignedResponseAlg), authorizationSignedResponseAlg, nameof(expected_authorization_signed_response_alg), expected_authorization_signed_response_alg); - - await TestPOST( - responseType: responseType, - grant_types: grantTypes.Split(','), - authorization_signed_response_alg: null, - - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType), - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType), - - testPostDelegate: async (responseMessage, registerDetails, ssa) => - { - if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.Created)) - { - return; - } - - Assertions.AssertHasContentTypeApplicationJson(responseMessage.Content); - - await AssertDCR( - responseType, - grantTypes.Split(','), - authorizationSignedResponseAlg, - responseMessage, - registerDetails, - ssa, - expected_authorization_signed_response_alg: expected_authorization_signed_response_alg, - idTokenEncryptedResponseAlg: GetIdTokenEncryptedResponseAlg(responseType) ?? "", // returns "" for idTokenEncryptedResponseAlg - idTokenEncryptedResponseEnc: GetIdTokenEncryptedResponseEnc(responseType) ?? ""); // returns "" for idTokenEncryptedResponseEnc - }); - } - - /// - /// return an IdTokenEncryptedResponseAlg if response type contains "id_token", otherwise return null - /// - private static string? GetIdTokenEncryptedResponseAlg(string responseType) - { - if (responseType.ToUpper().Contains("ID_TOKEN")) - { - return IDTOKEN_ENCRYPTED_RESPONSE_ALG_RSA_OAEP; - } - else - { - return null; - } - } - - /// - /// return an IdTokenEncryptedResponseEnc if response type contains "id_token", otherwise return null - /// - private static string? GetIdTokenEncryptedResponseEnc(string responseType) - { - if (responseType.ToUpper().Contains("ID_TOKEN")) - { - return IDTOKEN_ENCRYPTED_RESPONSE_ENC_A256GCM; - } - else - { - return null; - } - } - // Lookup details for software product from Register private dynamic LookupRegisterDetails(string softwareProductId) { @@ -917,18 +589,15 @@ from SoftwareProduct sp } private delegate void TestPOSTDelegate(HttpResponseMessage responseMessage, dynamic registerDetails, string ssa); + private async Task TestPOST( - string responseType, + ResponseType responseType, string[] grant_types, string? authorization_signed_response_alg, TestPOSTDelegate testPostDelegate, - string? authorization_encrypted_response_alg = null, string? authorization_encrypted_response_enc = null, - - string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - string? idTokenEncryptedResponseAlg = null, - string? idTokenEncryptedResponseEnc = null) + string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256) { // Arrange var registerDetails = LookupRegisterDetails(Constants.SoftwareProducts.SoftwareProductId); @@ -937,16 +606,14 @@ private async Task TestPOST( var ssa = await _registerSSAService.GetSSA(Constants.Brands.BrandId, Constants.SoftwareProducts.SoftwareProductId, _latestSSAVersion); // Act - HttpResponseMessage responseMessage = await RegisterSoftwareProduct( + var responseMessage = await RegisterSoftwareProduct( responseType, grant_types, authorization_signed_response_alg, ssa, authorization_encrypted_response_alg, authorization_encrypted_response_enc, - idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc); + idTokenSignedResponseAlg); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -955,83 +622,46 @@ private async Task TestPOST( } } - private async Task TestRegisterSoftwareProduct( - string responseType, - string[] grant_types, - string? authorization_signed_response_alg, - string? authorization_encrypted_response_alg = null, - string? authorization_encrypted_response_enc = null, - - string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - string? idTokenEncryptedResponseAlg = null, - string? idTokenEncryptedResponseEnc = null) - { - // Arrange - Helpers.AuthServer.PurgeAuthServerForDataholder(_options); - var ssa = await _registerSSAService.GetSSA(Constants.Brands.BrandId, Constants.SoftwareProducts.SoftwareProductId, _latestSSAVersion); - - // Act - HttpResponseMessage responseMessage = await RegisterSoftwareProduct( - responseType, - grant_types, - authorization_signed_response_alg, - ssa, - authorization_encrypted_response_alg, - authorization_encrypted_response_enc, - idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc); - - return responseMessage; - - } - - private delegate void TestPUTDelegate(HttpResponseMessage responseMessage, dynamic registerDetails, string ssa); + private async Task TestPUT( - string responseType, + ResponseType responseType, string[] grantTypes, string? authorizationSignedResponseAlg, string[]? redirectUrisForPut = null, bool expiredAccessToken = false, TestPUTDelegate? testPutDelegate = null, - - string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - string? idTokenEncryptedResponseAlg = null, - string? idTokenEncryptedResponseEnc = null) + string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256) { - // Arrange + // Arrange var registerDetails = LookupRegisterDetails(Constants.SoftwareProducts.SoftwareProductId); Helpers.AuthServer.PurgeAuthServerForDataholder(_options); var ssa = await _registerSSAService.GetSSA(Constants.Brands.BrandId, Constants.SoftwareProducts.SoftwareProductId, _latestSSAVersion); - var _registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest( + var registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest( ssa, responseType: responseType, grantTypes: grantTypes, authorizationSignedResponseAlg: authorizationSignedResponseAlg, - idTokenSignedResponseAlg: idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg: idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc: idTokenEncryptedResponseEnc); - var dcrResponseMessage = await _dataHolderRegisterService.RegisterSoftwareProduct(_registrationRequest); + idTokenSignedResponseAlg: idTokenSignedResponseAlg); + var dcrResponseMessage = await _dataHolderRegisterService.RegisterSoftwareProduct(registrationRequest); if (dcrResponseMessage.StatusCode != HttpStatusCode.Created) { throw new Exception($"Expected Created but {dcrResponseMessage.StatusCode} - {await dcrResponseMessage.Content.ReadAsStringAsync()}"); } + var dcrResponse = JsonConvert.DeserializeObject(await dcrResponseMessage.Content.ReadAsStringAsync()); - // Act + // Act var registrationRequestForPut = _dataHolderRegisterService.CreateRegistrationRequest( ssa, responseType: responseType, grantTypes: grantTypes, authorizationSignedResponseAlg: authorizationSignedResponseAlg, redirectUris: redirectUrisForPut, - idTokenSignedResponseAlg: idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg: idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc: idTokenEncryptedResponseEnc); + idTokenSignedResponseAlg: idTokenSignedResponseAlg); var accessToken = await new DataHolderAccessToken(dcrResponse?.client_id, _options.DH_MTLS_GATEWAY_URL, _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS, _authServerOptions.XTLSCLIENTCERTTHUMBPRINT, _authServerOptions.STANDALONE).GetAccessToken(expiredAccessToken); @@ -1046,41 +676,38 @@ private async Task TestPUT( } private delegate void TestDELETEDelegate(HttpResponseMessage responseMessage, dynamic registerDetails, string ssa); + private async Task TestDELETE( - string responseType, + ResponseType responseType, string[] grant_types, string? authorization_signed_response_alg, bool expiredAccessToken = false, TestDELETEDelegate? testDeleteDelegate = null, - - string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - string? idTokenEncryptedResponseAlg = null, - string? idTokenEncryptedResponseEnc = null) + string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256) { - // Arrange + // Arrange var registerDetails = LookupRegisterDetails(Constants.SoftwareProducts.SoftwareProductId); Helpers.AuthServer.PurgeAuthServerForDataholder(_options); var ssa = await _registerSSAService.GetSSA(Constants.Brands.BrandId, Constants.SoftwareProducts.SoftwareProductId, _latestSSAVersion); - var _registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest( + var registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest( ssa, responseType: responseType, grantTypes: grant_types, authorizationSignedResponseAlg: authorization_signed_response_alg, - idTokenSignedResponseAlg: idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg: idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc: idTokenEncryptedResponseEnc); + idTokenSignedResponseAlg: idTokenSignedResponseAlg); - var dcrResponseMessage = await _dataHolderRegisterService.RegisterSoftwareProduct(_registrationRequest); + var dcrResponseMessage = await _dataHolderRegisterService.RegisterSoftwareProduct(registrationRequest); if (dcrResponseMessage.StatusCode != HttpStatusCode.Created) { throw new Exception($"Expected Created but {dcrResponseMessage.StatusCode} - {await dcrResponseMessage.Content.ReadAsStringAsync()}"); } + var dcrResponse = JsonConvert.DeserializeObject(await dcrResponseMessage.Content.ReadAsStringAsync()); - // Act + // Act var accessToken = await new DataHolderAccessToken(dcrResponse?.client_id, _options.DH_MTLS_GATEWAY_URL, _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS, _authServerOptions.XTLSCLIENTCERTTHUMBPRINT, _authServerOptions.STANDALONE).GetAccessToken(expiredAccessToken); var api = _apiServiceDirector.BuildDataholderRegisterAPI(accessToken, null, HttpMethod.Delete, dcrResponse?.client_id ?? string.Empty); @@ -1098,16 +725,15 @@ private async Task TestDELETE( } private delegate void TestGETDelegate(HttpResponseMessage responseMessage, dynamic registerDetails, string ssa); + private async Task TestGET( - string responseType, + ResponseType responseType, string[] grant_types, string? authorization_signed_response_alg, bool expiredAccessToken = false, TestGETDelegate? testGetDelegate = null, - string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - string? idTokenEncryptedResponseAlg = null, - string? idTokenEncryptedResponseEnc = null) + string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256) { // Arrange var registerDetails = LookupRegisterDetails(Constants.SoftwareProducts.SoftwareProductId); @@ -1115,14 +741,12 @@ private async Task TestGET( Helpers.AuthServer.PurgeAuthServerForDataholder(_options); var ssa = await _registerSSAService.GetSSA(Constants.Brands.BrandId, Constants.SoftwareProducts.SoftwareProductId, _latestSSAVersion); - HttpResponseMessage dcrResponseMessage = await RegisterSoftwareProduct( + var dcrResponseMessage = await RegisterSoftwareProduct( responseType, grant_types, authorization_signed_response_alg, ssa, - idTokenSignedResponseAlg: idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg: idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc: idTokenEncryptedResponseEnc); + idTokenSignedResponseAlg: idTokenSignedResponseAlg); if (dcrResponseMessage.StatusCode != HttpStatusCode.Created) { @@ -1149,16 +773,13 @@ private async Task TestGET( } private async Task RegisterSoftwareProduct( - string responseType, + ResponseType responseType, string[] grant_types, string? authorization_signed_response_alg, string ssa, string? authorization_encrypted_response_alg = null, string? authorization_encrypted_response_enc = null, - - string? idTokenSignedResponseAlg = null, - string? idTokenEncryptedResponseAlg = null, - string? idTokenEncryptedResponseEnc = null) + string? idTokenSignedResponseAlg = null) { var registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest( ssa, @@ -1167,9 +788,7 @@ private async Task RegisterSoftwareProduct( authorizationSignedResponseAlg: authorization_signed_response_alg, authorizationEncryptedResponseAlg: authorization_encrypted_response_alg, authorizationEncryptedResponseEnc: authorization_encrypted_response_enc, - idTokenSignedResponseAlg: idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg: idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc: idTokenEncryptedResponseEnc); + idTokenSignedResponseAlg: idTokenSignedResponseAlg); var responseMessage = await _dataHolderRegisterService.RegisterSoftwareProduct(registrationRequest); @@ -1177,7 +796,7 @@ private async Task RegisterSoftwareProduct( } private async Task AssertDCR( - string responseType, + ResponseType responseType, string[] grant_types, string? authorization_signed_response_alg, HttpResponseMessage responseMessage, @@ -1185,53 +804,50 @@ private async Task AssertDCR( string ssa, string? authorizationEncryptedResponseAlg = null, string? authorizationEncryptedResponseEnc = null, - string? idTokenSignedResponseAlg = IDTOKEN_SIGNED_RESPONSE_ALG_PS256, - string? idTokenEncryptedResponseAlg = null, - string? idTokenEncryptedResponseEnc = null, - string? expected_authorization_signed_response_alg = null) { var json = await responseMessage.Content.ReadAsStringAsync(); - var response = JsonConvert.DeserializeObject(json)??throw new ArgumentNullException(nameof(responseMessage)); - string[] expectedResponseTypes = responseType.Contains(',') ? responseType.Split(",") : new string[] { responseType }; - response.response_types.Should().BeEquivalentTo(expectedResponseTypes); + var response = JsonConvert.DeserializeObject(json) ?? throw new ArgumentNullException(nameof(responseMessage)); + var expectedResponseTypes = responseType.ToEnumMemberAttrValue(); + _ = response.response_types.Should().BeEquivalentTo(expectedResponseTypes); - response.grant_types.Should().BeEquivalentTo(grant_types); + _ = response.grant_types.Should().BeEquivalentTo(grant_types); if (response.response_types?.Contains("code") ?? false) { if (authorization_signed_response_alg != null || expected_authorization_signed_response_alg != null) { - response.authorization_signed_response_alg.Should().Be(expected_authorization_signed_response_alg ?? authorization_signed_response_alg); + _ = response.authorization_signed_response_alg.Should().Be(expected_authorization_signed_response_alg ?? authorization_signed_response_alg); } if (_authServerOptions.JARM_ENCRYPTION_ON) { if (authorizationEncryptedResponseAlg != null) { - response.authorization_encrypted_response_alg.Should().Be(authorizationEncryptedResponseAlg); + _ = response.authorization_encrypted_response_alg.Should().Be(authorizationEncryptedResponseAlg); } + if (authorizationEncryptedResponseEnc != null) { - response.authorization_encrypted_response_enc.Should().Be(authorizationEncryptedResponseEnc); + _ = response.authorization_encrypted_response_enc.Should().Be(authorizationEncryptedResponseEnc); } } else { - response.authorization_encrypted_response_alg.Should().BeNull(); - response.authorization_encrypted_response_enc.Should().BeNull(); + _ = response.authorization_encrypted_response_alg.Should().BeNull(); + _ = response.authorization_encrypted_response_enc.Should().BeNull(); } } else { - response.authorization_signed_response_alg.Should().BeNull(); - response.authorization_encrypted_response_alg.Should().BeNull(); - response.authorization_encrypted_response_enc.Should().BeNull(); + _ = response.authorization_signed_response_alg.Should().BeNull(); + _ = response.authorization_encrypted_response_alg.Should().BeNull(); + _ = response.authorization_encrypted_response_enc.Should().BeNull(); } - response.client_id.Should().NotBeNullOrEmpty(); - response.client_id_issued_at.Should().NotBeNullOrEmpty(); + _ = response.client_id.Should().NotBeNullOrEmpty(); + _ = response.client_id_issued_at.Should().NotBeNullOrEmpty(); response.client_name.Should().Be(registerDetails.SoftwareProductName); response.client_description.Should().Be(registerDetails.SoftwareProductDescription); response.client_uri.Should().Be(registerDetails.ClientUri); @@ -1239,7 +855,7 @@ private async Task AssertDCR( response.org_id.Should().Be(registerDetails.BrandId.ToString()); response.org_name.Should().Be(registerDetails.BrandName); - response.redirect_uris.Should().BeEquivalentTo(new string[] { registerDetails.RedirectUris }); + _ = response.redirect_uris.Should().BeEquivalentTo([registerDetails.RedirectUris]); response.logo_uri.Should().Be(registerDetails.LogoUri); response.tos_uri.Should().Be(registerDetails.TosUri); @@ -1251,90 +867,52 @@ private async Task AssertDCR( response.recipient_base_uri.Should().Be(registerDetails.RecipientBaseUri); - response.token_endpoint_auth_method.Should().Be("private_key_jwt"); - response.token_endpoint_auth_signing_alg.Should().Be("PS256"); + _ = response.token_endpoint_auth_method.Should().Be("private_key_jwt"); + _ = response.token_endpoint_auth_signing_alg.Should().Be("PS256"); - response.application_type.Should().Be("web"); + _ = response.application_type.Should().Be("web"); - response.id_token_signed_response_alg.Should().Be(idTokenSignedResponseAlg); - response.id_token_encrypted_response_alg.Should().Be(idTokenEncryptedResponseAlg); - response.id_token_encrypted_response_enc.Should().Be(idTokenEncryptedResponseEnc); + _ = response.id_token_signed_response_alg.Should().Be(idTokenSignedResponseAlg); - response.request_object_signing_alg.Should().Be("PS256"); + _ = response.request_object_signing_alg.Should().Be("PS256"); - response.software_statement.Should().Be(ssa); - response.software_id.Should().Be(Constants.SoftwareProducts.SoftwareProductId); - response.software_roles.Should().Be("data-recipient-software-product"); + _ = response.software_statement.Should().Be(ssa); + _ = response.software_id.Should().Be(Constants.SoftwareProducts.SoftwareProductId); + _ = response.software_roles.Should().Be("data-recipient-software-product"); - response.scope.Should().ContainAll("openid", "bank:accounts.basic:read", "cdr:registration"); + _ = response.scope.Should().ContainAll("openid", "bank:accounts.basic:read", "cdr:registration"); return response; } - //private async Task AssertError(HttpResponseMessage responseMessage, HttpStatusCode expectedStatusCode, string expectedResponse) - //{ - // responseMessage.StatusCode.Should().Be(expectedStatusCode); - - // Assertions.Assert_HasContentType_ApplicationJson(responseMessage.Content); - - // var json = await responseMessage.Content.ReadAsStringAsync(); - - // await Assertions.Assert_HasContent_Json(expectedResponse, responseMessage.Content); - //} - - //private async Task AssertError(HttpResponseMessage responseMessage, AuthoriseException expectedError) - //{ - // responseMessage.StatusCode.Should().Be(expectedError.StatusCode); - - // Assertions.Assert_HasContentType_ApplicationJson(responseMessage.Content); - - // var responseContent = await responseMessage.Content.ReadAsStringAsync(); - // var receivedError = JsonConvert.DeserializeObject(responseContent); - - // receivedError.Should().NotBeNull(); - // receivedError.Description.Should().Be(expectedError.ErrorDescription); - // receivedError.Code.Should().Be(expectedError.Error); - //} - - static async Task AssertExpectedResponseCode(HttpResponseMessage responseMessage, HttpStatusCode expectedStatusCode) + private static async Task AssertExpectedResponseCode(HttpResponseMessage responseMessage, HttpStatusCode expectedStatusCode) { - responseMessage.StatusCode.Should().Be(expectedStatusCode); + _ = responseMessage.StatusCode.Should().Be(expectedStatusCode); if (responseMessage.StatusCode == expectedStatusCode) { return true; } var responseContent = await responseMessage.Content.ReadAsStringAsync(); - responseContent.Should().NotBe(responseContent); + _ = responseContent.Should().NotBe(responseContent); return false; } private async Task TestEncryption( - string responseType, + ResponseType responseType, string grantTypes, string? authorizationSignedResponseAlg = null, string? authorizationEncryptedResponseAlg = null, string? authorizationEncryptedResponseEnc = null, - string? idTokenSignedResponseAlg = null, - string? idTokenEncryptedResponseAlg = null, - string? idTokenEncryptedResponseEnc = null, - string? expectedIdTokenEncryptedResponseEnc = null, - string? expectedIdTokenEncryptedResponseAlg = null - ) + string? idTokenSignedResponseAlg = null) { await TestPOST( responseType: responseType, - grant_types: grantTypes.Split(','), - authorization_signed_response_alg: authorizationSignedResponseAlg, authorization_encrypted_response_alg: authorizationEncryptedResponseAlg, authorization_encrypted_response_enc: authorizationEncryptedResponseEnc, - idTokenSignedResponseAlg: idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg: idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc: idTokenEncryptedResponseEnc, - testPostDelegate: async (responseMessage, registerDetails, ssa) => { if (!await AssertExpectedResponseCode(responseMessage, HttpStatusCode.Created)) @@ -1343,7 +921,8 @@ await TestPOST( } Assertions.AssertHasContentTypeApplicationJson(responseMessage.Content); - await AssertDCR(responseType, + await AssertDCR( + responseType, grantTypes.Split(','), authorizationSignedResponseAlg, responseMessage, @@ -1351,12 +930,8 @@ await AssertDCR(responseType, ssa, authorizationEncryptedResponseAlg, authorizationEncryptedResponseEnc, - idTokenSignedResponseAlg: idTokenSignedResponseAlg, - idTokenEncryptedResponseAlg: expectedIdTokenEncryptedResponseAlg ?? idTokenEncryptedResponseAlg, - idTokenEncryptedResponseEnc: expectedIdTokenEncryptedResponseEnc ?? idTokenEncryptedResponseEnc); - + idTokenSignedResponseAlg: idTokenSignedResponseAlg); }); } - } } diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_OIDC.cs b/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_OIDC.cs index f93f926..f6271e9 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_OIDC.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_OIDC.cs @@ -1,13 +1,9 @@ -#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON +#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON -using System; using System.Net; -using System.Net.Http; -using System.Threading.Tasks; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; -using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services; using FluentAssertions; using FluentAssertions.Execution; using Microsoft.Extensions.Configuration; @@ -41,36 +37,63 @@ public US44264_CdrAuthServer_JARM_OIDC(IOptions options, private class OIDCResponse { public string? issuer { get; set; } + public string? jwks_uri { get; set; } + public string? registration_endpoint { get; set; } + public string? authorization_endpoint { get; set; } + public string? token_endpoint { get; set; } + public string? userinfo_endpoint { get; set; } + public string? revocation_endpoint { get; set; } + public string[]? scopes_supported { get; set; } + public string[]? claims_supported { get; set; } + public string[]? id_token_signing_alg_values_supported { get; set; } + public string[]? subject_types_supported { get; set; } + public string[]? code_challenge_methods_supported { get; set; } + public bool request_parameter_supported { get; set; } + public bool request_uri_parameter_supported { get; set; } + public string? introspection_endpoint { get; set; } + public string? pushed_authorization_request_endpoint { get; set; } + public string? cdr_arrangement_revocation_endpoint { get; set; } + public string[]? acr_values_supported { get; set; } + public bool require_pushed_authorization_requests { get; set; } + public string[]? request_object_signing_alg_values_supported { get; set; } + public string[]? id_token_encryption_alg_values_supported { get; set; } + public string[]? id_token_encryption_enc_values_supported { get; set; } + public string[]? token_endpoint_auth_signing_alg_values_supported { get; set; } + public string[]? response_types_supported { get; set; } + public string[]? grant_types_supported { get; set; } + public string[]? token_endpoint_auth_methods_supported { get; set; } + public bool tls_client_certificate_bound_access_tokens { get; set; } + public bool claims_parameter_supported { get; set; } + public string[]? response_modes_supported { get; set; } - public string[]? authorization_encryption_alg_values_supported { get; set; } - public string[]? authorization_encryption_enc_values_supported { get; set; } + public string[]? authorization_signing_alg_values_supported { get; set; } } @@ -110,17 +133,13 @@ private async Task TestOIDC(string[] expectedScopes) response.acr_values_supported.Should().BeSubsetOf(new string[] { "urn:cds.au:cdr:2", "urn:cds.au:cdr:3" }); response.require_pushed_authorization_requests.Should().Be(true); response.request_object_signing_alg_values_supported.Should().Contain(new string[] { "PS256", "ES256" }); - response.id_token_encryption_alg_values_supported.Should().Contain(new string[] { "RSA-OAEP", "RSA-OAEP-256" }); - response.id_token_encryption_enc_values_supported.Should().Contain(new string[] { "A128CBC-HS256", "A256GCM" }); response.token_endpoint_auth_signing_alg_values_supported.Should().Contain(new string[] { "PS256", "ES256" }); - response.response_types_supported.Should().Contain(new string[] { "code", "code id_token" }); + response.response_types_supported.Should().Contain(new string[] { "code" }); response.grant_types_supported.Should().Contain(new string[] { "authorization_code", "refresh_token", "client_credentials" }); response.token_endpoint_auth_methods_supported.Should().Contain(new string[] { "private_key_jwt" }); response.tls_client_certificate_bound_access_tokens.Should().Be(true); response.claims_parameter_supported.Should().Be(true); - - // AC says only "form_post", "fragment", "jwt" are supported - response.response_modes_supported.Should().BeEquivalentTo(new string[] { "form_post", "fragment", "jwt" }); + response.response_modes_supported.Should().BeEquivalentTo(["jwt"]); response.authorization_signing_alg_values_supported.Should().Contain(new string[] { "PS256", "ES256" }); @@ -133,15 +152,14 @@ private async Task TestOIDC(string[] expectedScopes) [Fact] public async Task AC01_MDH_OIDC_AC01_Get_ShouldRespondWith_200OK_OIDC_Configuration() { - await TestOIDC(new string[] { "openid", "profile", "cdr:registration", "bank:accounts.basic:read", "bank:transactions:read", "common:customer.basic:read" }); + await TestOIDC(["openid", "profile", "cdr:registration", "bank:accounts.basic:read", "bank:transactions:read", "common:customer.basic:read"]); } // https://cdr-internal.atlassian.net/wiki/spaces/PT/pages/44728338/MDHE+OIDC+Discovery+.well-known+Acceptance+Criteria [Fact] public async Task AC02_MDHE_OIDC_AC01_Get_ShouldRespondWith_200OK_OIDC_Configuration() { - await TestOIDC(new string[] { "openid", "profile", "cdr:registration", "energy:accounts.basic:read", "energy:accounts.concessions:read", "common:customer.basic:read" }); + await TestOIDC(["openid", "profile", "cdr:registration", "energy:accounts.basic:read", "energy:accounts.concessions:read", "common:customer.basic:read"]); } } } - diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_PAR.cs b/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_PAR.cs index 8604d3d..4712ec7 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_PAR.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/JARM/US44264_CdrAuthServer_JARM_PAR.cs @@ -1,4 +1,4 @@ -// #define DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON +// #define DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; @@ -44,19 +44,21 @@ public US44264_CdrAuthServer_JARM_PAR(IOptions options, I public class PARResponse { public string? request_uri { get; set; } + public string? expires_in { get; set; } } public delegate void TestPOSTDelegate(HttpResponseMessage responseMessage); + public async Task TestPOST( ResponseType? responseType = null, - ResponseMode? responseMode = ResponseMode.Fragment, + ResponseMode? responseMode = ResponseMode.Jwt, string? clientId = null, string? clientAssertionType = Constants.ClientAssertionType, string? clientAssertion = null, TestPOSTDelegate? testPostDelegate = null) { - // Arrange + // Arrange // Act var responseMessage = await _dataHolderParService.SendRequest( @@ -65,8 +67,7 @@ public async Task TestPOST( responseMode: responseMode, clientId: clientId, clientAssertionType: clientAssertionType, - clientAssertion: clientAssertion - ); + clientAssertion: clientAssertion); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -79,9 +80,7 @@ public async Task TestPOST( } [Theory] - [InlineData(ResponseType.CodeIdToken, ResponseMode.Fragment)] //Hybrid mode - [InlineData(ResponseType.CodeIdToken, ResponseMode.FormPost)] //Hybrid mode - [InlineData(ResponseType.Code, ResponseMode.Jwt)] //ACF mode + [InlineData(ResponseType.Code, ResponseMode.Jwt)] // ACF mode public async Task AC01_MDHPAR_AC01_HappyPath_ShouldRespondWith_201Created(ResponseType responseType, ResponseMode responseMode) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}.", nameof(responseType), responseType, nameof(responseMode), responseMode); @@ -92,8 +91,7 @@ public async Task AC01_MDHPAR_AC01_HappyPath_ShouldRespondWith_201Created(Respon var response = await _dataHolderParService.SendRequest( scope: _options.SCOPE, responseType: responseType, - responseMode: responseMode - ); + responseMode: responseMode); var responseText = await response.Content.ReadAsStringAsync(); @@ -115,13 +113,13 @@ public async Task AC01_MDHPAR_AC01_HappyPath_ShouldRespondWith_201Created(Respon } // [Fact] - public async Task AC02_MDHPAR_AC04_HappyPath_AmendExistingArrangement_ShouldRespondWith_200OK() + public Task AC02_MDHPAR_AC04_HappyPath_AmendExistingArrangement_ShouldRespondWith_200OK() { throw new NotImplementedException(); } // [Fact] - public async Task AC03_MDHPAR_AC06_WithUnownedArrangementId_ShouldRespondWith_400BadRequest() + public Task AC03_MDHPAR_AC06_WithUnownedArrangementId_ShouldRespondWith_400BadRequest() { throw new NotImplementedException(); } @@ -129,12 +127,13 @@ public async Task AC03_MDHPAR_AC06_WithUnownedArrangementId_ShouldRespondWith_40 [Fact] public async Task AC05_MDHPAR_AC08_WithUnknownClientId_ShouldRespondWith_400BadRequest() { - //Arrange + // Arrange var expectedError = new ClientNotFoundException(); - //Act-Assert + // Act-Assert await TestPOST( - //grantType: "client_credentials", + + // grantType: "client_credentials", clientId: "foo", clientAssertionType: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", testPostDelegate: async (response) => @@ -146,12 +145,13 @@ await TestPOST( [Fact] public async Task AC06_MDHPAR_AC09_WithInvalidClientAssertion_ShouldRespondWith_400BadRequest() { - //Arrange + // Arrange var expectedError = new InvalidClientAssertionTypeException(); - //Act-Assert + // Act-Assert await TestPOST( - //grantType: "client_credentials", + + // grantType: "client_credentials", clientId: _options.LastRegisteredClientId, clientAssertionType: "foo", testPostDelegate: async (response) => @@ -163,12 +163,13 @@ await TestPOST( [Fact] public async Task AC07_MDHPAR_AC10_WithInvalidClientAssertion_ShouldRespondWith_400BadRequest() { - //Arrange + // Arrange var expectedError = new InvalidClientAssertionFormatException(); - //Act-Assert + // Act-Assert await TestPOST( - //grantType: "client_credentials", + + // grantType: "client_credentials", clientId: _options.LastRegisteredClientId, clientAssertionType: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", clientAssertion: "foo", @@ -179,36 +180,13 @@ await TestPOST( } // [Fact] - public async Task AC08_MDHPAR_AC11_WithClientAssertionAssociatedWithDifferentClientId_ShouldRespondWith_400BadRequest() + public Task AC08_MDHPAR_AC11_WithClientAssertionAssociatedWithDifferentClientId_ShouldRespondWith_400BadRequest() { throw new NotImplementedException(); } [Theory] [InlineData(ResponseMode.TestOnlyFoo)] - [InlineData(ResponseMode.QueryJwt)] - [InlineData(ResponseMode.FragmentJwt)] - [InlineData(ResponseMode.FormPostJwt)] - public async Task AC09_MDHPAR_AC12_HF_WithInvalidResponseMode_ShouldRespondWith_400BadRequest(ResponseMode responseMode) - { - Log.Information("Running test with Params: {P1}={V1}.", nameof(responseMode), responseMode); - - //Arrange - var expectedError = new UnsupportedResponseModeException(); - - //Act-Assert - await TestPOST( - responseType: ResponseType.CodeIdToken, - responseMode: responseMode, - testPostDelegate: async (response) => - { - await Assertions.AssertErrorAsync(response, expectedError); - }); - } - - [Theory] - [InlineData(null)] - [InlineData(ResponseMode.TestOnlyFoo)] [InlineData(ResponseMode.Query)] [InlineData(ResponseMode.QueryJwt)] [InlineData(ResponseMode.FragmentJwt)] @@ -217,10 +195,10 @@ public async Task AC10_MDHPAR_AC13_ACF_WithUnsupportedResponseMode_ShouldRespond { Log.Information("Running test with Params: {P1}={V1}.", nameof(responseMode), responseMode); - //Arrange + // Arrange var expectedError = new UnsupportedResponseModeException(); - //Act-Assert + // Act-Assert await TestPOST( responseType: ResponseType.Code, responseMode: responseMode, @@ -230,20 +208,16 @@ await TestPOST( }); } - [Theory] - [InlineData(ResponseMode.Fragment)] - [InlineData(ResponseMode.FormPost)] - public async Task AC10_MDHPAR_AC13_ACF_WithInvalidResponseMode_ShouldRespondWith_400BadRequest(ResponseMode responseMode) + [Fact] + public async Task AC10_MDHPAR_AC13b_ACF_WithMissingResponseMode_ShouldRespondWith_400BadRequest() { - Log.Information("Running test with Params: {P1}={V1}.", nameof(responseMode), responseMode); - - //Arrange - var expectedError = new InvalidResponseModeForResponseTypeException(); + // Arrange + var expectedError = new MissingResponseModeException(); - //Act-Assert + // Act-Assert await TestPOST( responseType: ResponseType.Code, - responseMode: responseMode, + responseMode: null, testPostDelegate: async (response) => { await Assertions.AssertErrorAsync(response, expectedError); @@ -251,6 +225,7 @@ await TestPOST( } [Theory] + [InlineData(ResponseType.CodeIdToken)] [InlineData(ResponseType.TestOnlyFoo)] [InlineData(ResponseType.TestOnlyToken)] [InlineData(ResponseType.TestOnlyCodeToken)] @@ -262,10 +237,10 @@ public async Task AC13_MDHPAR_AC15_WithInvalidResponseType_ShouldRespondWith_400 { Log.Information("Running test with Params: {P1}={V1}.", nameof(responseType), responseType); - //Arrange + // Arrange var expectedError = new UnsupportedResponseTypeException(); - //Act-Assert + // Act-Assert await TestPOST( responseType: responseType, responseMode: ResponseMode.Fragment, @@ -275,15 +250,14 @@ await TestPOST( }); } - //TODO: Missing test for AC14 - + // TODO: Missing test for AC14 [Fact] public async Task AC15_MDHPAR_AC16_WithMissingResponseType_ShouldRespondWith_400BadRequest() { - //Arrange + // Arrange var expectedError = new MissingResponseTypeException(); - //Act-Assert + // Act-Assert await TestPOST( responseType: null, responseMode: ResponseMode.Fragment, @@ -292,24 +266,5 @@ await TestPOST( await Assertions.AssertErrorAsync(response, expectedError); }); } - - [Theory] - [InlineData(ResponseMode.Jwt)] - public async Task AC16_MDHPAR_AC17_HF_WithInvalidResponseMode_ShouldRespondWith_400BadRequest(ResponseMode responseMode) - { - Log.Information("Running test with Params: {P1}={V1}.", nameof(responseMode), responseMode); - - //Arrange - var expectedError = new InvalidResponseModeForResponseTypeException(); - - //Act-Assert - await TestPOST( - responseType: ResponseType.CodeIdToken, - responseMode: responseMode, - testPostDelegate: async (response) => - { - await Assertions.AssertErrorAsync(response, expectedError); - }); - } } } diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US12678_CdrAuthServer_Authorisation.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US12678_CdrAuthServer_Authorisation.cs index d16f2db..3921e9f 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US12678_CdrAuthServer_Authorisation.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US12678_CdrAuthServer_Authorisation.cs @@ -1,3 +1,7 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Web; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.APIs; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; @@ -14,13 +18,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Serilog; -using System.IdentityModel.Tokens.Jwt; -using System.Net; -using System.Security.Cryptography.X509Certificates; -using System.Web; using Xunit; -using Xunit.DependencyInjection; using XUnit_Skippable; +using Xunit.DependencyInjection; using static ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services.DataHolderAuthoriseService; using Constants = ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Constants; @@ -40,9 +40,9 @@ public class US12678_CdrAuthServer_Authorisation : BaseTest, IClassFixture options, IOptions authServerOptions, - IDataHolderParService dataHolderParService, - IDataHolderTokenService dataHolderTokenService, - ISqlQueryService sqlQueryService, + IDataHolderParService dataHolderParService, + IDataHolderTokenService dataHolderTokenService, + ISqlQueryService sqlQueryService, IApiServiceDirector apiServiceDirector, ITestOutputHelperAccessor testOutputHelperAccessor, IConfiguration config) @@ -81,11 +81,30 @@ public async Task AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_Redire // Act var tokenResponse = await _dataHolderTokenService.GetResponse(authCode); - if (tokenResponse == null) throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - TokenResponse is null"); - if (tokenResponse.IdToken == null) throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - Id token is null"); - if (tokenResponse.AccessToken == null) throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - Access token is null"); - if (tokenResponse.RefreshToken == null) throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - Refresh token is null"); - if (tokenResponse.CdrArrangementId == null) throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - CdrArrangementId is null"); + if (tokenResponse == null) + { + throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - TokenResponse is null"); + } + + if (tokenResponse.IdToken == null) + { + throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - Id token is null"); + } + + if (tokenResponse.AccessToken == null) + { + throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - Access token is null"); + } + + if (tokenResponse.RefreshToken == null) + { + throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - Refresh token is null"); + } + + if (tokenResponse.CdrArrangementId == null) + { + throw new InvalidOperationException($"{nameof(AC01_Get_WithValidRequest_ShouldRespondWith_302Redirect_RedirectToRedirectURI_IdToken)} - CdrArrangementId is null"); + } // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -116,11 +135,17 @@ public async Task AC02_InactiveOrRemovedSoftwareProduct_ShouldReturn_AdrStatusNo try { - var response = await _dataHolderParService.SendRequest(_options.SCOPE, _options.LastRegisteredClientId); - if (response.StatusCode != HttpStatusCode.Created) throw new Exception("Error with PAR request - StatusCode"); + if (response.StatusCode != HttpStatusCode.Created) + { + throw new Exception("Error with PAR request - StatusCode"); + } + var parResponse = await _dataHolderParService.DeserializeResponse(response); - if (string.IsNullOrEmpty(parResponse?.RequestURI)) throw new Exception("Error with PAR request - RequestURI"); + if (string.IsNullOrEmpty(parResponse?.RequestURI)) + { + throw new Exception("Error with PAR request - RequestURI"); + } var authorisationURL = new AuthoriseUrl.AuthoriseUrlBuilder(_options) .WithRequestUri(parResponse.RequestURI) @@ -141,7 +166,8 @@ public async Task AC02_InactiveOrRemovedSoftwareProduct_ShouldReturn_AdrStatusNo if (_authServerOptions.JARM_ENCRYPTION_ON) { // Decrypt the JARM JWT. - var privateKeyCertificate = new X509Certificate2(Constants.Certificates.JwtCertificateFilename, + var privateKeyCertificate = new X509Certificate2( + Constants.Certificates.JwtCertificateFilename, Constants.Certificates.JwtCertificatePassword, X509KeyStorageFlags.Exportable); var privateKey = privateKeyCertificate.GetRSAPrivateKey(); JweToken token = JWE.Decrypt(queryValueResponse, privateKey); @@ -157,7 +183,6 @@ public async Task AC02_InactiveOrRemovedSoftwareProduct_ShouldReturn_AdrStatusNo { _sqlQueryService.SetStatus(entityType, entityId, saveStatus); } - } [SkippableTheory] @@ -205,13 +230,13 @@ public async Task AC02_Get_WithInvalidResponseType_ShouldRespondWith_302Redirect response.StatusCode.Should().Be(expectedStatusCode); // Check redirect path - var redirectPath = response?.Headers?.Location?.GetLeftPart(UriPartial.Path); + var redirectPath = response.Headers.Location?.GetLeftPart(UriPartial.Path); redirectPath.Should().Be(expectedRedirectPath); // Check redirect fragment if (expectedRedirectFragment != null) { - var redirectFragment = HttpUtility.UrlDecode(response?.Headers?.Location?.Fragment.TrimStart('#')); + var redirectFragment = HttpUtility.UrlDecode(response.Headers.Location?.Fragment.TrimStart('#')); redirectFragment.Should().StartWith(HttpUtility.UrlDecode(expectedRedirectFragment)); } } @@ -235,7 +260,6 @@ public async Task AC03_Get_WithInvalidRequestBody_ShouldRespondWith_400BadReques expectedRedirectPath = _accountLoginUrl; } - // Arrange Arrange(); @@ -259,20 +283,16 @@ public async Task AC03_Get_WithInvalidRequestBody_ShouldRespondWith_400BadReques if (expectedRedirectPath != null) { // Check redirect path - var redirectPath = response?.Headers?.Location?.GetLeftPart(UriPartial.Path); + var redirectPath = response.Headers.Location?.GetLeftPart(UriPartial.Path); redirectPath.Should().Be(expectedRedirectPath); } - - // Assert - Check error response - if (response?.StatusCode == HttpStatusCode.BadRequest) - { - } } } [SkippableTheory] - [InlineData(false, HttpStatusCode.Redirect)] // Successful request should redirect to the DH login URI - [InlineData(true, // Additional unsupported scope should be ignored + [InlineData(false, HttpStatusCode.Redirect)] // Successful request should redirect to the DH login URI + [InlineData( + true, // Additional unsupported scope should be ignored HttpStatusCode.Redirect)] public async Task AC04_Get_WithInvalidScope_ShouldRespondWith_200OK_Response(bool useAdditionalScope, HttpStatusCode expectedStatusCode) { @@ -367,13 +387,13 @@ public async Task AC05_Get_WithScopeMissingOpenId_ShouldRespondWith_302Redirect_ response.StatusCode.Should().Be(expectedStatusCode); // Check redirect path - var redirectPath = response?.Headers?.Location?.GetLeftPart(UriPartial.Path); + var redirectPath = response.Headers.Location?.GetLeftPart(UriPartial.Path); redirectPath.Should().Be(expectedRedirectPath); // Check redirect fragment if (expectedRedirectFragment != null) { - var redirectFragment = HttpUtility.UrlDecode(response?.Headers?.Location?.Fragment.TrimStart('#')); + var redirectFragment = HttpUtility.UrlDecode(response.Headers.Location?.Fragment.TrimStart('#')); redirectFragment.Should().StartWith(HttpUtility.UrlDecode(expectedRedirectFragment)); } } @@ -381,7 +401,7 @@ public async Task AC05_Get_WithScopeMissingOpenId_ShouldRespondWith_302Redirect_ [SkippableTheory] [InlineData(Constants.SoftwareProducts.SoftwareProductId, HttpStatusCode.Redirect, true)] - [InlineData(Constants.GuidFoo, HttpStatusCode.Redirect, false)] // Unsuccessful request should redirect back to DR + [InlineData(Constants.GuidFoo, HttpStatusCode.Redirect, false)] // Unsuccessful request should redirect back to DR public async Task AC06_Get_WithInvalidClientID_ShouldRespondWith_302Redirect_ErrorResponse(string softwareProductId, HttpStatusCode expectedStatusCode, bool useSpecificUrl) { if (_authServerOptions.HEADLESSMODE) @@ -425,13 +445,13 @@ public async Task AC06_Get_WithInvalidClientID_ShouldRespondWith_302Redirect_Err response.StatusCode.Should().Be(expectedStatusCode); // Check redirect path - var redirectPath = response?.Headers?.Location?.GetLeftPart(UriPartial.Path); + var redirectPath = response.Headers.Location?.GetLeftPart(UriPartial.Path); redirectPath.Should().Be(expectedRedirectPath); // Check redirect fragment if (expectedRedirectFragment != null) { - var redirectFragment = HttpUtility.UrlDecode(response?.Headers?.Location?.Fragment.TrimStart('#')); + var redirectFragment = HttpUtility.UrlDecode(response.Headers.Location?.Fragment.TrimStart('#')); redirectFragment.Should().StartWith(HttpUtility.UrlDecode(expectedRedirectFragment)); } } @@ -476,7 +496,7 @@ public async Task AC07_Get_WithInvalidRedirectURI_ShouldRespondWith_400BadReques throw new SkipTestException("Test not applicable for headless mode."); } - var expectedError = new InvalidRequestException(""); //TODO: Why doesn't this use a description? Bug 64158 + var expectedError = new InvalidRequestException(string.Empty); // TODO: Why doesn't this use a description? Bug 64158 var expectedRedirectPath = "https://localhost:9001/foo"; // Arrange @@ -545,13 +565,13 @@ public async Task AC08_Get_WithUnsignedRequestBody_ShouldRespondWith_302Redirect response.StatusCode.Should().Be(expectedStatusCode); // Check redirect path - var redirectPath = response?.Headers?.Location?.GetLeftPart(UriPartial.Path); + var redirectPath = response.Headers.Location?.GetLeftPart(UriPartial.Path); redirectPath.Should().Be(expectedRedirectPath); // Check redirect query if (expectedRedirectFragment != null) { - var redirectFragment = HttpUtility.UrlDecode(response?.Headers?.Location?.Fragment.TrimStart('#')); + var redirectFragment = HttpUtility.UrlDecode(response.Headers.Location?.Fragment.TrimStart('#')); redirectFragment.Should().StartWith(HttpUtility.UrlDecode(expectedRedirectFragment)); } } diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US12962_CdrAuthServer_OIDC_Configuration.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US12962_CdrAuthServer_OIDC_Configuration.cs index 680872d..10e9a4a 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US12962_CdrAuthServer_OIDC_Configuration.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US12962_CdrAuthServer_OIDC_Configuration.cs @@ -1,4 +1,4 @@ -#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON +#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON using CdrAuthServer.IntegrationTests.Models; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; @@ -62,18 +62,16 @@ public async Task AC01_FromMDH_Get_ShouldRespondWith_200OK_OIDC_Configuration() actual.revocation_endpoint.Should().Be($"{_options.CDRAUTHSERVER_SECUREBASEURI}/connect/revocation"); actual.cdr_arrangement_revocation_endpoint.Should().Be($"{_options.CDRAUTHSERVER_SECUREBASEURI}/connect/arrangements/revoke"); actual.acr_values_supported.Should().IntersectWith(new[] { "urn:cds.au:cdr:2", "urn:cds.au:cdr:3" }); - actual.id_token_encryption_alg_values_supported.Should().IntersectWith(new[] { "RSA-OAEP", "RSA-OAEP-256" }); - actual.id_token_encryption_enc_values_supported.Should().IntersectWith(new[] { "A128CBC-HS256", "A256GCM" }); actual.tls_client_certificate_bound_access_tokens.Should().Be("true"); - actual.id_token_signing_alg_values_supported.Should().BeEquivalentTo(new[] { "ES256", "PS256" }); - actual.token_endpoint_auth_signing_alg_values_supported.Should().BeEquivalentTo(new[] { "ES256", "PS256" }); - actual.token_endpoint_auth_methods_supported.Should().BeEquivalentTo(new[] { "private_key_jwt" }); - actual.subject_types_supported.Should().BeEquivalentTo(new[] { "pairwise" }); - actual.grant_types_supported.Should().BeEquivalentTo(new[] { "authorization_code", "client_credentials", "refresh_token" }); + actual.id_token_signing_alg_values_supported.Should().BeEquivalentTo(["ES256", "PS256"]); + actual.token_endpoint_auth_signing_alg_values_supported.Should().BeEquivalentTo(["ES256", "PS256"]); + actual.token_endpoint_auth_methods_supported.Should().BeEquivalentTo(["private_key_jwt"]); + actual.subject_types_supported.Should().BeEquivalentTo(["pairwise"]); + actual.grant_types_supported.Should().BeEquivalentTo(["authorization_code", "client_credentials", "refresh_token"]); actual.scopes_supported.Should().Contain(new[] { "openid", "profile", "cdr:registration", "bank:accounts.basic:read", "bank:transactions:read", "common:customer.basic:read", }); actual.claims_supported.Should().Contain(new[] { "name", "given_name", "family_name", "sharing_duration", "iss", "sub", "aud", "acr", "exp", "iat", "nonce", "auth_time", "updated_at" }); - actual.response_types_supported.Should().Contain(new[] { "code id_token" }); - actual.response_modes_supported.Should().Contain(new[] { "form_post", "fragment" }); + actual.response_types_supported.Should().Contain(new[] { "code" }); + actual.response_modes_supported.Should().BeEquivalentTo(new[] { "jwt" }); } } @@ -152,13 +150,10 @@ public async Task AC01_Get_ShouldRespondWith_200OK_OIDC_Configuration() ""pairwise"" ], ""response_modes_supported"": [ - ""fragment"", - ""form_post"", ""jwt"" ], ""response_types_supported"": [ - ""code"", - ""code id_token"" + ""code"" ], ""code_challenge_methods_supported"": [ ""S256"" @@ -186,14 +181,6 @@ public async Task AC01_Get_ShouldRespondWith_200OK_OIDC_Configuration() ""PS256"", ""ES256"" ], - ""id_token_encryption_alg_values_supported"": [ - ""RSA-OAEP"", - ""RSA-OAEP-256"" - ], - ""id_token_encryption_enc_values_supported"": [ - ""A128CBC-HS256"", - ""A256GCM"" - ], ""authorization_signing_alg_values_supported"": [ ""PS256"", ""ES256"" diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US12963_CdrAuthServer_Token.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US12963_CdrAuthServer_Token.cs index aa2229a..38ac0ad 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US12963_CdrAuthServer_Token.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US12963_CdrAuthServer_Token.cs @@ -40,8 +40,10 @@ public class US12963_CdrAuthServer_Token : BaseTest, IClassFixture options, + private string? _clientId { get; set; } + + public US12963_CdrAuthServer_Token( + IOptions options, IOptions authServerOptions, IDataHolderParService dataHolderParService, IDataHolderTokenService dataHolderTokenService, @@ -66,7 +68,7 @@ public US12963_CdrAuthServer_Token(IOptions options, _apiServiceDirector = apiServiceDirector ?? throw new ArgumentNullException(nameof(apiServiceDirector)); } - static void AssertAccessToken(string? accessToken) + private static void AssertAccessToken(string? accessToken) { accessToken.Should().NotBeNullOrEmpty(); if (accessToken != null) @@ -84,7 +86,7 @@ static void AssertAccessToken(string? accessToken) } } - static void AssertIdToken(string? idToken) + private static void AssertIdToken(string? idToken) { string decryptedIdToken; @@ -124,7 +126,7 @@ static void AssertIdToken(string? idToken) } } - #region TEST_SCENARIO_A_IDTOKEN_AND_ACCESSTOKEN + #region TEST_SCENARIO_A_IDTOKEN_AND_ACCESSTOKEN [Fact] public async Task AC01_Auth_Code_Flow_Post_ShouldRespondWith_200OK_IDToken_AccessToken_RefreshToken() { @@ -158,39 +160,6 @@ public async Task AC01_Auth_Code_Flow_Post_ShouldRespondWith_200OK_IDToken_Acces } } - [Fact] - public async Task AC01_Hybrid_Flow_Post_ShouldRespondWith_200OK_IDToken_AccessToken_RefreshToken() - { - // Arrange - var authCode = await GetAuthCode(responseType: ResponseType.CodeIdToken, responseMode: ResponseMode.Fragment); - - // Act - var responseMessage = await _dataHolderTokenService.SendRequest(authCode); - - // Assert - using (new AssertionScope(BaseTestAssertionStrategy)) - { - responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); - - if (responseMessage.StatusCode == HttpStatusCode.OK) - { - Assertions.AssertHasContentTypeApplicationJson(responseMessage.Content); - - var tokenResponse = await _dataHolderTokenService.DeserializeResponse(responseMessage); - tokenResponse.Should().NotBeNull(); - tokenResponse?.TokenType.Should().Be("Bearer"); - tokenResponse?.ExpiresIn.Should().Be(_authServerOptions.ACCESSTOKENLIFETIMESECONDS); - tokenResponse?.CdrArrangementId.Should().NotBeNullOrEmpty(); - tokenResponse?.Scope.Should().Be(SCOPE_TOKEN_ACCOUNTS); - tokenResponse?.AccessToken.Should().NotBeNullOrEmpty(); - tokenResponse?.IdToken.Should().NotBeNullOrEmpty(); - tokenResponse?.RefreshToken.Should().NotBeNullOrEmpty(); - - AssertAccessToken(tokenResponse?.AccessToken); - } - } - } - [Fact] public async Task AC01_Post_WithoutClientId_ShouldRespondWith_200OK_IDToken_AccessToken_RefreshToken() { @@ -290,14 +259,13 @@ public async Task AC03_Post_WithInvalidRequest_GrantType_ShouldRespondWith_400Ba } } - [Fact] public async Task AC03_Post_WithValidRequest_ClientId_Success() { // Arrange var requestClientId = _options.LastRegisteredClientId; - var authCode = await GetAuthCode(); //note that this uses _clientId, so by specifying different values in requestClientId we ensure invalidClientId errors + var authCode = await GetAuthCode(); // note that this uses _clientId, so by specifying different values in requestClientId we ensure invalidClientId errors // Act var responseMessage = await _dataHolderTokenService.SendRequest( @@ -314,9 +282,8 @@ public async Task AC03_Post_WithValidRequest_ClientId_Success() [Fact] public async Task AC03_Post_WithMissingIssuer_ShouldRespondWith_400BadRequest_MissingIssClaimResponse() { - // Arrange - var authCode = await GetAuthCode(); //note that this uses _clientId, so by specifying different values in requestClientId we ensure invalidClientId errors + var authCode = await GetAuthCode(); // note that this uses _clientId, so by specifying different values in requestClientId we ensure invalidClientId errors AuthoriseException expectedError = new MissingIssClaimException(); @@ -333,13 +300,11 @@ public async Task AC03_Post_WithMissingIssuer_ShouldRespondWith_400BadRequest_Mi } } - [Fact] public async Task AC03_Post_WithInvalidClientAndIssuer_ShouldRespondWith_400BadRequest_ClientNotFoundResponse() { - // Arrange - var authCode = await GetAuthCode(); //note that this uses _clientId, so by specifying different values in requestClientId we ensure invalidClientId errors + var authCode = await GetAuthCode(); // note that this uses _clientId, so by specifying different values in requestClientId we ensure invalidClientId errors AuthoriseException expectedError = new ClientNotFoundException(); @@ -378,6 +343,7 @@ public async Task AC03_Post_WithValidRequest_ClientAssertionType_Success() public async Task AC03_Post_WithInvalidRequest_ClientAssertionType_ShouldRespondWith_400BadRequest_InvalidClientErrorResponse(string clientAssertionType) { Log.Information("Running test with Params: {P1}={V1}.", nameof(clientAssertionType), clientAssertionType); + // Arrange var authCode = await GetAuthCode(); @@ -434,7 +400,15 @@ public async Task AC03_Post_WithInvalidRequest_ClientAssertion_ShouldRespondWith } } - public enum AC04_TestType { valid_jwt, omit_iss, omit_aud, omit_exp, omit_jti, exp_backdated } + public enum AC04_TestType + { + valid_jwt, + omit_iss, + omit_aud, + omit_exp, + omit_jti, + exp_backdated + } [Fact] public async Task AC04_Post_WithValidClientAssertion_Success() @@ -473,7 +447,7 @@ public async Task AC04_Post_WithInvalidClientAssertion_ShouldRespondWith_400BadR AC04_TestType.omit_exp => new TokenValidationClientAssertionException(), AC04_TestType.omit_jti => new MissingJtiException(), AC04_TestType.exp_backdated => new ExpiredClientAssertionException(), - _ => throw new NotSupportedException() + _ => throw new NotSupportedException(), }; // Act @@ -511,12 +485,12 @@ public async Task AC05_Post_WithExpiredAuthCode_ShouldRespondWith_400BadRequest_ async Task ExpireAuthCode(string authCode) { using var connection = new SqlConnection(_options.AUTHSERVER_CONNECTIONSTRING); - connection.Open(); + await connection.OpenAsync(); var count = await connection.ExecuteAsync("update grants set expiresat = @expiresAt where [key]=@key", new { expiresAt = DateTime.UtcNow.AddDays(-90), - key = authCode + key = authCode, }); if (count != 1) @@ -544,7 +518,7 @@ async Task ExpireAuthCode(string authCode) } [Fact] - public async Task AC06_Post_WithMissingAuthCode_ShouldRespondWith_400BadRequest_InvalidGrant() //TODO: The title says should return InvalidGrant, but the test checks for InvalidRequest. Logged as Bug 63704 + public async Task AC06_Post_WithMissingAuthCode_ShouldRespondWith_400BadRequest_InvalidGrant() // TODO: The title says should return InvalidGrant, but the test checks for InvalidRequest. Logged as Bug 63704 { // Act var responseMessage = await _dataHolderTokenService.SendRequest(authCode: null); @@ -571,24 +545,6 @@ public async Task AC07_Post_WithInvalidAuthCode_ShouldRespondWith_400BadRequest_ } } - [Fact] - public async Task ACXXX_Post_InvalidClientEncryptionKey_ShouldRespondWith_400BadRequest_InvalidXXX() - { - // Arrange - var authCode = await GetAuthCode(responseType: ResponseType.CodeIdToken, responseMode: ResponseMode.Fragment); - Helpers.AuthServer.UpdateAuthServerClientClaim(_options, _clientId, "id_token_encrypted_response_alg", "RSA-OAEP-256"); - - // Act - var responseMessage = await _dataHolderTokenService.SendRequest(authCode); - - // Assert - var expectedError = new UnexpectedErrorException("Unable to get encryption key required for id_token encryption from client JWKS"); - using (new AssertionScope(BaseTestAssertionStrategy)) - { - await Assertions.AssertErrorAsync(responseMessage, expectedError); - } - } - #endregion #region TEST_SCENARIO_B_IDTOKEN_ACCESSTOKEN_REFRESHTOKEN @@ -633,7 +589,7 @@ public async Task AC08_Post_WithShareDuration_ShouldRespondWith_200OK_IDToken_Ac #region TEST_SCENARIO_C_USE_REFRESHTOKEN_FOR_NEW_ACCESSTOKEN private async Task Test_AC09_AC10(string initalScope, string requestedScope) { - async Task<(string? authCode, string? refreshToken)> GetRefreshToken(string scope) + async Task<(string? AuthCode, string? RefreshToken)> GetRefreshToken(string scope) { // Create grant with specific scope var authCode = await GetAuthCode(100000, scope); @@ -647,16 +603,16 @@ private async Task Test_AC09_AC10(string initalScope, strin } // Just make sure refresh token was issued with correct scope - if (tokenResponse?.Scope != scope) + if (tokenResponse.Scope != scope) { throw new Exception($"{nameof(AC09_Post_WithRefreshToken_AndSameScope_ShouldRespondWith_200OK_AccessToken_RefreshToken)}.{nameof(GetRefreshToken)} - Unexpected scope"); } - return (authCode, tokenResponse?.RefreshToken); + return (authCode, tokenResponse.RefreshToken); } // Get a refresh token with initial scope - var (authCode, refreshToken) = await GetRefreshToken(initalScope); + var (_, refreshToken) = await GetRefreshToken(initalScope); // Use the refresh token to get a new accesstoken and new refreshtoken (with the requested scope) var responseMessage = await _dataHolderTokenService.SendRequest( @@ -778,7 +734,7 @@ public async Task ACX01_Authorise_WithSharingDuration_ShouldRespondWith_AccessTo Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}.", nameof(sharingDuration), sharingDuration, nameof(expectsRefreshToken), expectsRefreshToken); string authCode = await GetAuthCode(sharingDuration); - + var tokenResponse = await _dataHolderTokenService.GetResponse(authCode); // Assert @@ -794,7 +750,6 @@ public async Task ACX01_Authorise_WithSharingDuration_ShouldRespondWith_AccessTo tokenResponse?.RefreshToken.Should().NotBeNullOrEmpty(); var decodedJWT = new JwtSecurityTokenHandler().ReadJwtToken(tokenResponse?.AccessToken); - } else { @@ -811,7 +766,7 @@ public async Task ACX02_UseRefreshTokenMultipleTimes_ShouldRespondWith_AccessTok { Log.Information("Running test with Params: {P1}={V1}.", nameof(usageAttempts), usageAttempts); - // Arrange - Get authcode, + // Arrange - Get authcode, var authCode = await GetAuthCode(100000); // Act - Get access token and refresh token @@ -819,7 +774,7 @@ public async Task ACX02_UseRefreshTokenMultipleTimes_ShouldRespondWith_AccessTok using (new AssertionScope(BaseTestAssertionStrategy)) { - // Assert + // Assert tokenResponse.Should().NotBeNull(); tokenResponse?.AccessToken.Should().NotBeNull(); tokenResponse?.RefreshToken.Should().NotBeNull(); @@ -835,7 +790,7 @@ public async Task ACX02_UseRefreshTokenMultipleTimes_ShouldRespondWith_AccessTok refreshTokenResponse.Should().NotBeNull(); refreshTokenResponse?.AccessToken.Should().NotBeNull(); refreshTokenResponse?.RefreshToken.Should().NotBeNull(); - refreshTokenResponse?.RefreshToken.Should().Be(refreshToken); // same refresh token is returned + refreshTokenResponse?.RefreshToken.Should().Be(refreshToken); // same refresh token is returned refreshToken = refreshTokenResponse?.RefreshToken; } @@ -859,7 +814,7 @@ public async Task ACX03_UseExpiredRefreshToken_ShouldRespondWith_400BadRequest(b using (new AssertionScope(BaseTestAssertionStrategy)) { - // Assert + // Assert tokenResponse.Should().NotBeNull(); tokenResponse?.AccessToken.Should().NotBeNull(); tokenResponse?.RefreshToken.Should().NotBeNull(); @@ -868,7 +823,7 @@ public async Task ACX03_UseExpiredRefreshToken_ShouldRespondWith_400BadRequest(b if (expired) { // Assert - Check that refresh token will expire when we expect it to - var decodedJWT = new JwtSecurityTokenHandler().ReadJwtToken(tokenResponse?.AccessToken); + _ = new JwtSecurityTokenHandler().ReadJwtToken(tokenResponse?.AccessToken); // Arrange - wait until refresh token has expired await Task.Delay((SHARING_DURATION_FOR_EXPIRED_REFRESHTOKEN + 10) * 1000); @@ -882,20 +837,9 @@ public async Task ACX03_UseExpiredRefreshToken_ShouldRespondWith_400BadRequest(b } } - private async Task CallResourceAPI(string accessToken) - { - var api = _apiServiceDirector.BuildCustomerResourceAPI(accessToken); - var response = await api.SendAsync(); - - return response.StatusCode; - } - private async Task GetAuthCode(int? sharingDuration = Constants.AuthServer.SharingDuration, string? scope = null, ResponseType responseType = ResponseType.Code, ResponseMode responseMode = ResponseMode.Jwt) { - if (_clientId == null) - { - _clientId = _options.LastRegisteredClientId; - } + _clientId ??= _options.LastRegisteredClientId; if (scope.IsNullOrWhiteSpace()) { @@ -926,7 +870,7 @@ private string GenerateClientAssertion(AC04_TestType testType) var additionalClaims = new List { new Claim("sub", ISSUER), - new Claim("iat", new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer) + new Claim("iat", new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer), }; if (testType != AC04_TestType.omit_iss) @@ -970,4 +914,4 @@ private string GenerateClientAssertion(AC04_TestType testType) return jwtSecurityTokenHandler.WriteToken(jwt); } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US12964_CDRAuthServer_OIDC_JWKS.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US12964_CDRAuthServer_OIDC_JWKS.cs index 3870d12..0a62c95 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US12964_CDRAuthServer_OIDC_JWKS.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US12964_CDRAuthServer_OIDC_JWKS.cs @@ -1,4 +1,4 @@ -#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON +#undef DEBUG_WRITE_EXPECTED_AND_ACTUAL_JSON using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; @@ -27,14 +27,19 @@ public US12962_CDRAuthServer_OIDC_JWKS( _apiServiceDirector = apiServiceDirector ?? throw new System.ArgumentNullException(nameof(apiServiceDirector)); } + private class AC01_Expected { public class Key { public string? kty { get; set; } + public string? use { get; set; } + public string? kid { get; set; } + public string? e { get; set; } + public string? n { get; set; } } diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US12965_CdrAuthServer_UserInfo.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US12965_CdrAuthServer_UserInfo.cs index 1a872dc..945278b 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US12965_CdrAuthServer_UserInfo.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US12965_CdrAuthServer_UserInfo.cs @@ -1,4 +1,4 @@ -using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions; @@ -42,16 +42,19 @@ public US12965_CdrAuthServer_UserInfo(IOptions options, I _apiServiceDirector = apiServiceDirector ?? throw new ArgumentNullException(nameof(apiServiceDirector)); } - class AC01_AC02_Expected + private class AC01_AC02_Expected { -#pragma warning disable IDE1006 +#pragma warning disable IDE1006 public string? given_name { get; set; } + public string? family_name { get; set; } + public string? name { get; set; } - public string? sub { get; set; } + public string? iss { get; set; } + public string? aud { get; set; } -#pragma warning restore IDE1006 +#pragma warning restore IDE1006 } private async Task Test_AC01_AC02(HttpMethod httpMethod, TokenType tokenType, string expectedName, string expectedGivenName, string expectedFamilyName) @@ -111,7 +114,7 @@ public async Task AC03_Get_WithADRParticipationNotActive_ShouldRespondWith_403Fo { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(status), status, nameof(statusDescription), statusDescription, nameof(expectedStatusCode), expectedStatusCode); - // Arrange + // Arrange var saveStatus = _sqlQueryService.GetStatus(EntityType.LEGALENTITY, Constants.LegalEntities.LegalEntityId); _sqlQueryService.SetStatus(EntityType.LEGALENTITY, Constants.LegalEntities.LegalEntityId, "ACTIVE"); @@ -178,7 +181,7 @@ public async Task AC05_Get_WithADRSoftwareProductNotActive_ShouldRespondWith_403 // Assert - Check error response if (response.StatusCode != HttpStatusCode.OK) { - var error = new AdrStatusNotActiveException($"ERR-GEN-002: Software product status is {statusDescription.ToUpper()}", ""); + var error = new AdrStatusNotActiveException($"ERR-GEN-002: Software product status is {statusDescription.ToUpper()}", string.Empty); var errorList = new ResponseErrorListV2(error.Code, error.Title, error.Detail, null); var expectedContent = JsonConvert.SerializeObject(errorList); @@ -259,10 +262,11 @@ public async Task AC08_Get_WithExpiredAccessToken_ShouldRespondWith_401Unauthori // Assert - Check error response if (response.StatusCode != HttpStatusCode.OK) { - Assertions.AssertHasHeader(@"Bearer error=""invalid_token"", error_description=""The token expired at ", - response.Headers, - "WWW-Authenticate", - true); + Assertions.AssertHasHeader( + @"Bearer error=""invalid_token"", error_description=""The token expired at ", + response.Headers, + "WWW-Authenticate", + true); } } } @@ -292,7 +296,7 @@ public async Task AC09_Get_WithCurrentHolderOfKey_Success() } [Theory] - [InlineData(Constants.Certificates.AdditionalCertificateFilename, Constants.Certificates.AdditionalCertificatePassword)] // Different holder of key + [InlineData(Constants.Certificates.AdditionalCertificateFilename, Constants.Certificates.AdditionalCertificatePassword)] // Different holder of key public async Task AC09_Get_WithDifferentHolderOfKey_ShouldRespondWith_401Unauthorised(string certificateFilename, string certificatePassword) { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}.", nameof(certificateFilename), certificateFilename, nameof(certificatePassword), certificatePassword); diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US12966_CdrAuthServer_Introspection.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US12966_CdrAuthServer_Introspection.cs index f9e60f9..28995fb 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US12966_CdrAuthServer_Introspection.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US12966_CdrAuthServer_Introspection.cs @@ -1,4 +1,4 @@ -using CdrAuthServer.IntegrationTests.Interfaces; +using CdrAuthServer.IntegrationTests.Interfaces; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions; @@ -39,12 +39,15 @@ private void Arrange() Helpers.AuthServer.PurgeAuthServerForDataholder(_options, true); } - class IntrospectionResponse + private class IntrospectionResponse { #pragma warning disable IDE1006 public bool? active { get; set; } + public string? scope { get; set; } + public int? exp { get; set; } + public string? cdr_arrangement_id { get; set; } #pragma warning restore IDE1006 } @@ -61,7 +64,7 @@ class IntrospectionResponse Array.Sort(array); - return String.Join(' ', array); + return string.Join(' ', array); } [Theory] @@ -75,7 +78,7 @@ public async Task AC01_Post_ShouldRespondWith_200OK_IntrospectionInfo(TokenType // Arrange Arrange(); - var approximateGrantTime_Epoch = (Int32)DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds; + var approximateGrantTime_Epoch = (int)DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds; var tokenResponse = await _authorizationService.GetToken(tokenType); // Act @@ -95,9 +98,10 @@ public async Task AC01_Post_ShouldRespondWith_200OK_IntrospectionInfo(TokenType Sort(actual?.scope).Should().Be(Sort(tokenResponse.Scope)); actual?.cdr_arrangement_id.Should().Be(tokenResponse.CdrArrangementId); - // Check expiry of refresh token. + // Check expiry of refresh token. // Since we only know approximate time that refresh token was granted, we can only know approximate time refresh token will expire var approximateExpiryTime_Epoch = approximateGrantTime_Epoch + REFRESHTOKEN_LIFETIME_SECONDS; + // So expiry time is approximated, check that actual expiry is within small window of the approximate expiry time actual?.exp.Should().BeInRange( approximateExpiryTime_Epoch - EXPIRY_GRACE_SECONDS, @@ -118,8 +122,7 @@ public async Task AC02_Post_WithInvalidRefreshToken_ShouldRespondWith_200OK_Acti // Act var response = await _dataHolderIntrospectionService.SendRequest( - token: invalidRefreshToken ? "foo" : tokenResponse.RefreshToken - ); + token: invalidRefreshToken ? "foo" : tokenResponse.RefreshToken); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -148,8 +151,7 @@ public async Task AC03_Post_WithMissingRefreshToken_ShouldRespondWith_200OK_Acti // Act var response = await _dataHolderIntrospectionService.SendRequest( token: missingRefreshToken ? null : tokenResponse.RefreshToken, - tokenTypeHint: missingRefreshToken ? null : "refresh_token" - ); + tokenTypeHint: missingRefreshToken ? null : "refresh_token"); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -213,14 +215,12 @@ public async Task AC06_Post_WithValidClientId_Success() // Arrange Arrange(); var tokenResponse = await _authorizationService.GetToken(TokenType.KamillaSmith); - - var expectedError = new ClientNotFoundException(); + _ = new ClientNotFoundException(); // Act var response = await _dataHolderIntrospectionService.SendRequest( token: tokenResponse.RefreshToken, - clientId: clientId - ); + clientId: clientId); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -245,8 +245,7 @@ public async Task AC06_Post_WithInvalidClientId_ShouldRespondWith_400BadRequest_ // Act var response = await _dataHolderIntrospectionService.SendRequest( token: tokenResponse.RefreshToken, - clientId: clientId - ); + clientId: clientId); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -265,8 +264,7 @@ public async Task AC07_Post_WithValidClientAssertionType_Success() // Act var response = await _dataHolderIntrospectionService.SendRequest( token: tokenResponse.RefreshToken, - clientAssertionType: Constants.ClientAssertionType - ); + clientAssertionType: Constants.ClientAssertionType); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -291,8 +289,7 @@ public async Task AC07_Post_WithInvalidClientAssertionType_ShouldRespondWith_400 // Act var response = await _dataHolderIntrospectionService.SendRequest( token: tokenResponse.RefreshToken, - clientAssertionType: clientAssertionType - ); + clientAssertionType: clientAssertionType); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -311,8 +308,7 @@ public async Task AC08_Post_WithNullClientAssertion_Success() // Act var response = await _dataHolderIntrospectionService.SendRequest( token: tokenResponse.RefreshToken, - clientAssertion: null - ); + clientAssertion: null); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -337,8 +333,7 @@ public async Task AC08_Post_WithInvalidClientAssertion_ShouldRespondWith_400BadR // Act var response = await _dataHolderIntrospectionService.SendRequest( token: tokenResponse.RefreshToken, - clientAssertion: clientAssertion - ); + clientAssertion: clientAssertion); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US12968_CdrAuthServer_PAR.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US12968_CdrAuthServer_PAR.cs index 9f066f8..f46c6fa 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US12968_CdrAuthServer_PAR.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US12968_CdrAuthServer_PAR.cs @@ -1,4 +1,4 @@ -using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.APIs; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions; @@ -51,6 +51,7 @@ public US12968_CdrAuthServer_PAR(IOptions options, IOptio } [Fact] + // Call PAR endpoint, with request, to get a RequestUri public async Task AC01_Post_ShouldRespondWith_201Created_RequestUri() { @@ -59,8 +60,6 @@ public async Task AC01_Post_ShouldRespondWith_201Created_RequestUri() // Act var response = await _dataHolderParService.SendRequest(_options.SCOPE); - //var responseText = await response.Content.ReadAsStringAsync(); - // Assert using (new AssertionScope(BaseTestAssertionStrategy)) { @@ -156,6 +155,7 @@ public async Task AC09_Post_WithInvalidClientAssertionType_ShouldRespondWith_400 // Arrange var expectedErrror = new InvalidClientAssertionTypeException(); + // Act var response = await _dataHolderParService.SendRequest(_options.SCOPE, clientAssertionType: clientAssertionType); @@ -189,7 +189,8 @@ public async Task AC10_Revocation_WithInvalidClientAssertion_ShouldRespondWith_4 public async Task AC11_Post_WitCurrentHolderOfKey_Success_Created() { // Act - var response = await _dataHolderParService.SendRequest(_options.SCOPE, + var response = await _dataHolderParService.SendRequest( + _options.SCOPE, jwtCertificateForClientAssertionFilename: Constants.Certificates.JwtCertificateFilename, jwtCertificateForClientAssertionPassword: Constants.Certificates.JwtCertificatePassword); @@ -206,11 +207,12 @@ public async Task AC11_Post_WithDifferentHolderOfKey_ShouldRespondWith_400BadReq { Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}.", nameof(jwtCertificateFilename), jwtCertificateFilename, nameof(jwtCertificatePassword), jwtCertificatePassword); - //Arrange + // Arrange var expectedError = new TokenValidationClientAssertionException(); // Act - var response = await _dataHolderParService.SendRequest(_options.SCOPE, + var response = await _dataHolderParService.SendRequest( + _options.SCOPE, jwtCertificateForClientAssertionFilename: jwtCertificateFilename, jwtCertificateForClientAssertionPassword: jwtCertificatePassword); @@ -222,16 +224,27 @@ public async Task AC11_Post_WithDifferentHolderOfKey_ShouldRespondWith_400BadReq } [Fact] + // Call Authorisaton endpoint, with requestUri issued by PAR endpoint, to create CdrArrangement public async Task AC02_Post_AuthorisationEndpoint_WithRequestUri_ShouldRespondWith_200OK_CdrArrangementId() { // Arrange var response = await _dataHolderParService.SendRequest(_options.SCOPE); - if (response.StatusCode != HttpStatusCode.Created) throw new Exception("Error with PAR request - StatusCode"); + if (response.StatusCode != HttpStatusCode.Created) + { + throw new Exception("Error with PAR request - StatusCode"); + } + var parResponse = await _dataHolderParService.DeserializeResponse(response); - if (string.IsNullOrEmpty(parResponse?.RequestURI)) throw new Exception("Error with PAR request - RequestURI"); - if (parResponse?.ExpiresIn != 90) throw new Exception("Error with PAR request - ExpiresIn"); + if (string.IsNullOrEmpty(parResponse?.RequestURI)) + { + throw new Exception("Error with PAR request - RequestURI"); + } + if (parResponse?.ExpiresIn != 90) + { + throw new Exception("Error with PAR request - ExpiresIn"); + } // Act - Authorise with PAR RequestURI var authService = await new DataHolderAuthoriseServiceBuilder(_options, _dataHolderParService, _apiServiceDirector, false, _authServerOptions) @@ -241,7 +254,7 @@ public async Task AC02_Post_AuthorisationEndpoint_WithRequestUri_ShouldRespondWi .WithRequestUri(parResponse.RequestURI) .BuildAsync(); - (var authCode, var idToken) = await authService.Authorise(); + (var authCode, _) = await authService.Authorise(); // Assert - Check we got an authCode and idToken using (new AssertionScope(BaseTestAssertionStrategy)) @@ -270,22 +283,33 @@ public async Task AC03_Post_AuthorisationEndpoint_WithRequestUri_After90Seconds_ { var expectedRedirectPath = _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS; var expectedError = new ExpiredRequestUriException(); - + const int PAR_EXPIRY_SECONDS = 90; var clientId = _options.LastRegisteredClientId ?? throw new NullReferenceException("Could not find Client Id."); // Arrange var response = await _dataHolderParService.SendRequest(_options.SCOPE, clientId: clientId); - if (response.StatusCode != HttpStatusCode.Created) throw new Exception("Error with PAR request - StatusCode"); + if (response.StatusCode != HttpStatusCode.Created) + { + throw new Exception("Error with PAR request - StatusCode"); + } + var parResponse = await _dataHolderParService.DeserializeResponse(response); - if (string.IsNullOrEmpty(parResponse?.RequestURI)) throw new Exception("Error with PAR request - RequestURI"); - if (parResponse?.ExpiresIn != PAR_EXPIRY_SECONDS) throw new Exception("Error with PAR request - ExpiresIn"); + if (string.IsNullOrEmpty(parResponse?.RequestURI)) + { + throw new Exception("Error with PAR request - RequestURI"); + } + + if (parResponse.ExpiresIn != PAR_EXPIRY_SECONDS) + { + throw new Exception("Error with PAR request - ExpiresIn"); + } // Wait until PAR expires await Task.Delay((PAR_EXPIRY_SECONDS + 10) * 1000); - //May need to provide a clientId here! + // May need to provide a clientId here! var authorisationURL = new AuthoriseUrl.AuthoriseUrlBuilder(_options) .WithRequestUri(parResponse.RequestURI) .WithClientId(clientId) @@ -307,14 +331,14 @@ public async Task AC03_Post_AuthorisationEndpoint_WithRequestUri_After90Seconds_ authResponse.StatusCode.Should().Be(HttpStatusCode.Redirect); // Check redirect path - var redirectPath = authResponse?.Headers?.Location?.GetLeftPart(UriPartial.Path); + var redirectPath = authResponse.Headers?.Location?.GetLeftPart(UriPartial.Path); redirectPath.Should().Be(expectedRedirectPath); - var queryValues = HttpUtility.ParseQueryString(authResponse?.Headers.Location?.Query ?? throw new NullReferenceException()); + var queryValues = HttpUtility.ParseQueryString(authResponse.Headers?.Location?.Query ?? throw new NullReferenceException()); // Check query has "response" param var queryValueResponse = queryValues["response"]; - + var encodedJwt = queryValueResponse; queryValueResponse.Should().NotBeNullOrEmpty(); @@ -331,15 +355,15 @@ public async Task AC03_Post_AuthorisationEndpoint_WithRequestUri_After90Seconds_ var jwt = new JwtSecurityTokenHandler().ReadJwtToken(encodedJwt); jwt.Claim("error").Value.Should().Be(expectedError.Error); jwt.Claim("error_description").Value.Should().Be(expectedError.ErrorDescription); - } } [Fact] + // Call PAR endpoint, with existing CdrArrangementId, to get requestUri public async Task AC04_Post_WithCdrArrangementId_ShouldRespondWith_201Created_RequestUri() { - // Arrange + // Arrange var cdrArrangementId = await CreateCDRArrangement(); // Act - PAR with existing CdrArrangementId @@ -358,6 +382,7 @@ public async Task AC04_Post_WithCdrArrangementId_ShouldRespondWith_201Created_Re } [Fact] + // Call Authorisaton endpoint, with requestUri issued by PAR endpoint, to update existing CdrArrangement public async Task AC05_Post_AuthorisationEndpoint_WithRequestUri_ShouldRespondWith_200OK_CdrArrangementId() { @@ -365,11 +390,14 @@ public async Task AC05_Post_AuthorisationEndpoint_WithRequestUri_ShouldRespondWi // Create CDR arrangement, call PAR and get RequestURI var cdrArrangementId = await CreateCDRArrangement(); - //call PAR with existing CdrArrangementId and get RequestURI + // call PAR with existing CdrArrangementId and get RequestURI var response = await _dataHolderParService.SendRequest(US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS, cdrArrangementId: cdrArrangementId); var parResponse = await _dataHolderParService.DeserializeResponse(response); - if (string.IsNullOrEmpty(parResponse?.RequestURI)) throw new Exception("requestUri is null"); + if (string.IsNullOrEmpty(parResponse?.RequestURI)) + { + throw new Exception("requestUri is null"); + } // Act - Authorise using requestURI var authService = await new DataHolderAuthoriseServiceBuilder(_options, _dataHolderParService, _apiServiceDirector, false, _authServerOptions) @@ -397,11 +425,12 @@ public async Task AC05_Post_AuthorisationEndpoint_WithRequestUri_ShouldRespondWi } [Fact] + // Call PAR endpoint, with existing CdrArrangementId, to get requestUri public async Task AC04_Post_WithCdrArrangementId_AmendingConsent_ShouldInvalidateRefreshTokenn() { // Create a CDR arrangement - async Task<(string cdrArrangementId, string refreshToken)> CreateCDRArrangement(string cdrArrangementId = null) + async Task<(string CdrArrangementId, string RefreshToken)> CreateCDRArrangement(string cdrArrangementId = null) { // Authorise var authService = await new DataHolderAuthoriseServiceBuilder(_options, _dataHolderParService, _apiServiceDirector, false, _authServerOptions) @@ -415,13 +444,16 @@ public async Task AC04_Post_WithCdrArrangementId_AmendingConsent_ShouldInvalidat // Get token var tokenResponse = await _dataHolderTokenService.GetResponse(authCode, scope: US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS); - if (tokenResponse == null || tokenResponse?.CdrArrangementId == null) throw new Exception("Error getting CDRArrangementId"); + if (tokenResponse == null || tokenResponse?.CdrArrangementId == null) + { + throw new Exception("Error getting CDRArrangementId"); + } // Return CdrArrangementId return (tokenResponse.CdrArrangementId, tokenResponse.RefreshToken); } - // Arrange + // Arrange var (cdrArrangementId, refreshToken) = await CreateCDRArrangement(); // Amend consent. @@ -447,7 +479,10 @@ private async Task CreateCDRArrangement() // Get token var tokenResponse = await _dataHolderTokenService.GetResponse(authCode, scope: US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS); - if (tokenResponse == null || tokenResponse?.CdrArrangementId == null) throw new Exception("Error getting CDRArrangementId"); + if (tokenResponse == null || tokenResponse.CdrArrangementId == null) + { + throw new Exception("Error getting CDRArrangementId"); + } // Return CdrArrangementId return tokenResponse.CdrArrangementId; diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15584_CdrAuthServer_Registration_POST.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15584_CdrAuthServer_Registration_POST.cs index b0ca228..8040de8 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15584_CdrAuthServer_Registration_POST.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15584_CdrAuthServer_Registration_POST.cs @@ -1,4 +1,4 @@ -using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Fixtures; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; @@ -95,8 +95,6 @@ public async Task AC01_Post_WithUnregistedSoftwareProduct_ShouldRespondWith_201C Assert.Equal("PS256", json["token_endpoint_auth_signing_alg"]); Assert.Equal("private_key_jwt", json["token_endpoint_auth_method"]); Assert.Equal("PS256", json["id_token_signed_response_alg"]); - Assert.Equal("RSA-OAEP", json["id_token_encrypted_response_alg"]); - Assert.Equal("A256GCM", json["id_token_encrypted_response_enc"]); Assert.Equal("PS256", json["request_object_signing_alg"]); Assert.Equal("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", json["scope"]); Assert.Equal(ssa, json["software_statement"]); @@ -114,8 +112,8 @@ public async Task AC01_Post_WithUnregistedSoftwareProduct_MandatoryOnlyFields_Sh // Act var registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest( ssa, - applicationType: "", - requestObjectSigningAlg: "", + applicationType: string.Empty, + requestObjectSigningAlg: string.Empty, redirectUris: null); var response = await _dataHolderRegisterService.RegisterSoftwareProduct(registrationRequest); @@ -141,8 +139,6 @@ public async Task AC01_Post_WithUnregistedSoftwareProduct_MandatoryOnlyFields_Sh Assert.Equal("PS256", json["token_endpoint_auth_signing_alg"]); Assert.Equal("private_key_jwt", json["token_endpoint_auth_method"]); Assert.Equal("PS256", json["id_token_signed_response_alg"]); - Assert.Equal("RSA-OAEP", json["id_token_encrypted_response_alg"]); - Assert.Equal("A256GCM", json["id_token_encrypted_response_enc"]); Assert.Equal("PS256", json["request_object_signing_alg"]); Assert.Equal("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", json["scope"]); Assert.Equal(ssa, json["software_statement"]); @@ -241,14 +237,14 @@ string CreateFakeSSA(string ssa) [Fact] public async Task AC06_Post_WithInvalidSSAPayload_400BadRequest_InvalidSSAPayloadResponse() { - // Arrange + // Arrange Helpers.AuthServer.PurgeAuthServerForDataholder(_options); const string RedirectUri = "foo"; var expectedError = new InvalidRedirectUriException(RedirectUri); var ssa = await _registerSSAService.GetSSA(Constants.Brands.BrandId, Constants.SoftwareProducts.SoftwareProductId, _latestSSAVersion); - var registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest(ssa, redirectUris: new string[] { RedirectUri }); + var registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest(ssa, redirectUris: [RedirectUri]); // Act var response = await _dataHolderRegisterService.RegisterSoftwareProduct(registrationRequest); @@ -263,7 +259,7 @@ public async Task AC06_Post_WithInvalidSSAPayload_400BadRequest_InvalidSSAPayloa [Fact] public async Task AC07_Post_WithInvalidMetadata_400BadRequest_InvalidMetadataResponse() { - // Arrange + // Arrange Helpers.AuthServer.PurgeAuthServerForDataholder(_options); var expectedError = new TokenEndpointAuthSigningAlgClaimInvalidException(); @@ -281,4 +277,4 @@ public async Task AC07_Post_WithInvalidMetadata_400BadRequest_InvalidMetadataRes } } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15585_CdrAuthServer_Registration_PUT.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15585_CdrAuthServer_Registration_PUT.cs index 5bc7968..6c3b087 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15585_CdrAuthServer_Registration_PUT.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15585_CdrAuthServer_Registration_PUT.cs @@ -1,4 +1,5 @@ -using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Fixtures; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; @@ -37,10 +38,10 @@ public US15221_US12969_US15585_CdrAuthServer_Registration_PUT(IOptions Arrange() + private async Task<(string Ssa, string Registration, string ClientId)> Arrange() { Helpers.AuthServer.PurgeAuthServerForDataholder(_options); - return await _dataHolderRegisterService.RegisterSoftwareProduct(responseType: "code,code id_token", authorizationSignedResponseAlg: "PS256"); + return await _dataHolderRegisterService.RegisterSoftwareProduct(responseType: ResponseType.Code, authorizationSignedResponseAlg: "PS256"); } [Fact] @@ -52,11 +53,10 @@ public async Task AC11_Put_WithValidSoftwareProduct_ShouldRespondWith_200OK_Upda // Act var registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest( ssa, - responseType: "code,code id_token", + responseType: ResponseType.Code, authorizationSignedResponseAlg: "PS256", authorizationEncryptedResponseAlg: "RSA-OAEP", - authorizationEncryptedResponseEnc: "A128CBC-HS256" - ); + authorizationEncryptedResponseEnc: "A128CBC-HS256"); var accessToken = await new DataHolderAccessToken(clientId, _options.DH_MTLS_GATEWAY_URL, _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS, _authServerOptions.XTLSCLIENTCERTTHUMBPRINT, _authServerOptions.STANDALONE).GetAccessToken(); @@ -90,8 +90,9 @@ public async Task AC12_Put_WithInvalidSoftwareProduct_ShouldRespondWith_400BadRe var expectedError = new InvalidRedirectUriException(RedirectUri); // Act - var registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest(ssa, - redirectUris: new string[] { RedirectUri }); // Invalid redirect uris + var registrationRequest = _dataHolderRegisterService.CreateRegistrationRequest( + ssa, + redirectUris: [RedirectUri]); // Invalid redirect uris var accessToken = await new DataHolderAccessToken(clientId, _options.DH_MTLS_GATEWAY_URL, _options.SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS, _authServerOptions.XTLSCLIENTCERTTHUMBPRINT, _authServerOptions.STANDALONE).GetAccessToken(); @@ -128,7 +129,8 @@ public async Task AC13_Put_WithExpiredAccessToken_ShouldRespondWith_401UnAuthori if (response.StatusCode == HttpStatusCode.Unauthorized) { // Assert - Check WWWAutheticate header - Assertions.AssertHasHeader(@"Bearer error=""invalid_token"", error_description=""The token expired at '05/16/2022 03:04:03'""", + Assertions.AssertHasHeader( + @"Bearer error=""invalid_token"", error_description=""The token expired at '05/16/2022 03:04:03'""", response.Headers, "WWW-Authenticate"); } } @@ -160,10 +162,12 @@ public async Task AC14_Put_WithInvalidOrUnregisteredClientID_ShouldRespondWith_4 if (response.StatusCode == HttpStatusCode.Unauthorized) { // Assert - Check WWWAutheticate header - Assertions.AssertHasHeader(@"Bearer error=""invalid_request"", error_description=""The client is unknown""", - response.Headers, "WWW-Authenticate"); + Assertions.AssertHasHeader( + @"Bearer error=""invalid_request"", error_description=""The client is unknown""", + response.Headers, + "WWW-Authenticate"); } } } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15586_CdrAuthServer_Registration_GET.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15586_CdrAuthServer_Registration_GET.cs index ae3fbb1..e22168c 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15586_CdrAuthServer_Registration_GET.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15586_CdrAuthServer_Registration_GET.cs @@ -1,4 +1,4 @@ -using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Fixtures; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; @@ -18,7 +18,7 @@ public class US15221_US12969_US15586_CdrAuthServer_Registration_GET : BaseTest, private readonly TestAutomationAuthServerOptions _authServerOptions; private readonly ISqlQueryService _sqlQueryService; private readonly IApiServiceDirector _apiServiceDirector; - private readonly string _clientId; + private readonly string _clientId = null!; public US15221_US12969_US15586_CdrAuthServer_Registration_GET(IOptions options, IOptions authServerOptions, ISqlQueryService sqlQueryService, IApiServiceDirector apiServiceDirector, ITestOutputHelperAccessor testOutputHelperAccessor, IConfiguration config) : base(testOutputHelperAccessor, config) @@ -70,7 +70,8 @@ public async Task AC09_Get_WithExpiredAccessToken_ShouldRespondWith_401Unauthori if (response.StatusCode == HttpStatusCode.Unauthorized) { // Assert - Check WWWAutheticate header - Assertions.AssertHasHeader(@"Bearer error=""invalid_token"", error_description=""The token expired at '05/16/2022 03:04:03'""", + Assertions.AssertHasHeader( + @"Bearer error=""invalid_token"", error_description=""The token expired at '05/16/2022 03:04:03'""", response.Headers, "WWW-Authenticate"); } } @@ -93,9 +94,10 @@ public async Task AC10_Get_WithInvalidClientId_ShouldRespondWith_401Unauthorized response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); { - // Assert - Check error response + // Assert - Check error response // TODO - replace with authorise exception - Assertions.AssertHasHeader(@"Bearer error=""invalid_request"", error_description=""The client is unknown""", + Assertions.AssertHasHeader( + @"Bearer error=""invalid_request"", error_description=""The client is unknown""", response.Headers, "WWW-Authenticate", true); // starts with @@ -103,4 +105,4 @@ public async Task AC10_Get_WithInvalidClientId_ShouldRespondWith_401Unauthorized } } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15587_CdrAuthServer_Registration_DELETE.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15587_CdrAuthServer_Registration_DELETE.cs index dac69eb..e9000d6 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15587_CdrAuthServer_Registration_DELETE.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US15221_US12969_US15587_CdrAuthServer_Registration_DELETE.cs @@ -33,7 +33,6 @@ public US15221_US12969_US15587_CdrAuthServer_Registration_DELETE(IOptions Arrange() { @@ -89,10 +88,11 @@ public async Task AC17_Delete_WithExpiredAccessToken_ShouldRespondWith_401Unauth if (response.StatusCode == HttpStatusCode.Unauthorized) { // Assert - Check WWWAutheticate header - Assertions.AssertHasHeader(@"Bearer error=""invalid_token"", error_description=""The token expired at '05/16/2022 03:04:03'""", + Assertions.AssertHasHeader( + @"Bearer error=""invalid_token"", error_description=""The token expired at '05/16/2022 03:04:03'""", response.Headers, "WWW-Authenticate"); } } } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US17652_CdrAuthServer_ArrangementRevocation.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US17652_CdrAuthServer_ArrangementRevocation.cs index 3a7ed25..ae11b56 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US17652_CdrAuthServer_ArrangementRevocation.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US17652_CdrAuthServer_ArrangementRevocation.cs @@ -1,6 +1,4 @@ -#undef FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS // FIXME - MJS - the test using this code needs to be in MDH integration tests since it uses MDH endpoint (ie $"{DH_MTLS_GATEWAY_URL}/cds-au/v1/banking/accounts") - -using CdrAuthServer.IntegrationTests.Interfaces; +using CdrAuthServer.IntegrationTests.Interfaces; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.CdsExceptions; @@ -35,14 +33,15 @@ public class US17652_CdrAuthServer_ArrangementRevocation : BaseTest, IClassFixtu private readonly IDataHolderCDRArrangementRevocationService _cdrArrangementRevocationService; private readonly IApiServiceDirector _apiServiceDirector; - public US17652_CdrAuthServer_ArrangementRevocation(IOptions options, + public US17652_CdrAuthServer_ArrangementRevocation( + IOptions options, IOptions authServerOptions, IDataHolderRegisterService dataHolderRegisterService, IDataHolderParService dataHolderParService, IDataHolderTokenService dataHolderTokenService, ISqlQueryService sqlQueryService, IDataHolderCDRArrangementRevocationService cdrArrangementRevocationService, - IApiServiceDirector apiServiceDirector, + IApiServiceDirector apiServiceDirector, ITestOutputHelperAccessor testOutputHelperAccessor, IConfiguration config) : base(testOutputHelperAccessor, config) @@ -69,7 +68,7 @@ private async Task Arrange() await _dataHolderRegisterService.RegisterSoftwareProduct(); } - int CountPersistedGrant(string persistedGrantType, string? key = null) + private int CountPersistedGrant(string persistedGrantType, string? key = null) { using var connection = new SqlConnection(_options.AUTHSERVER_CONNECTIONSTRING); connection.Open(); @@ -84,12 +83,14 @@ int CountPersistedGrant(string persistedGrantType, string? key = null) { selectCommand = new SqlCommand($"select count(*) from grants where granttype=@type", connection); } + selectCommand.Parameters.AddWithValue("@type", persistedGrantType); return selectCommand.ExecuteScalarInt32(); } [Fact] + // When an arrangement exists, revoking the arrangement should remove the arrangement and revoke any associated tokens public async Task AC01_Post_WithArrangementId_ShouldRespondWith_204NoContent_ArrangementRevoked() { @@ -124,7 +125,7 @@ public async Task AC01_Post_WithArrangementId_ShouldRespondWith_204NoContent_Arr } } - async Task RevokeCdrArrangement(string cdrArrangementId) + private async Task RevokeCdrArrangement(string cdrArrangementId) { var response = await _cdrArrangementRevocationService.SendRequest(cdrArrangementId: cdrArrangementId); if (response.StatusCode != HttpStatusCode.NoContent) @@ -136,6 +137,7 @@ async Task RevokeCdrArrangement(string cdrArrangementId) [Theory] [InlineData(false, HttpStatusCode.OK)] [InlineData(true, HttpStatusCode.BadRequest)] + // When an arrangement has been revoked, trying to use associated access token should result in error (Unauthorised) public async Task AC02_GetAccounts_WithRevokedAccessToken_ShouldRespondWith_401Unauthorised(bool revokeArrangement, HttpStatusCode expectedStatusCode) { @@ -173,7 +175,10 @@ static async Task GetAccounts(string? accessToken) // Arrange - Get token response using authCode var tokenResponse = await _dataHolderTokenService.GetResponse(authCode); - if (tokenResponse == null || tokenResponse.AccessToken == null || tokenResponse.CdrArrangementId == null) throw new Exception("Unexpected token response"); + if (tokenResponse == null || tokenResponse.AccessToken == null || tokenResponse.CdrArrangementId == null) + { + throw new Exception("Unexpected token response"); + } // Arrange - Revoke the arrangement if (revokeArrangement) @@ -195,7 +200,7 @@ static async Task GetAccounts(string? accessToken) await Assertions.Assert_HasNoContent2(response.Content); } } -#endif +#endif } [Fact] @@ -213,15 +218,17 @@ public async Task AC03_GetAccessToken_WithValidRefreshToken_Success() (var authCode, _) = await authService.Authorise(); // Arrange - Get token response using authCode - var tokenResponse = await _dataHolderTokenService.GetResponse(authCode);//, scope: US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS); - if (tokenResponse == null || tokenResponse.RefreshToken == null || tokenResponse.CdrArrangementId == null) throw new Exception("Unexpected token response"); + var tokenResponse = await _dataHolderTokenService.GetResponse(authCode); + if (tokenResponse == null || tokenResponse.RefreshToken == null || tokenResponse.CdrArrangementId == null) + { + throw new Exception("Unexpected token response"); + } - // Act - Use refresh token to get a new access token. The refresh token should have been revoked because the arrangement was revoked + // Act - Use refresh token to get a new access token. The refresh token should have been revoked because the arrangement was revoked var response = await _dataHolderTokenService.SendRequest( grantType: "refresh_token", refreshToken: tokenResponse?.RefreshToken, - scope: US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS - ); + scope: US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -231,6 +238,7 @@ public async Task AC03_GetAccessToken_WithValidRefreshToken_Success() } [Fact] + // When an arrangement has been revoked, trying to use associated refresh token to get newly minted access token should result in error (401Unauthorised) public async Task AC03_GetAccessToken_WithRevokedRefreshToken_ShouldRespondWith_401Unauthorised() { @@ -248,18 +256,20 @@ public async Task AC03_GetAccessToken_WithRevokedRefreshToken_ShouldRespondWith_ (var authCode, _) = await authService.Authorise(); // Arrange - Get token response using authCode - var tokenResponse = await _dataHolderTokenService.GetResponse(authCode);//, scope: US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS); - if (tokenResponse == null || tokenResponse.RefreshToken == null || tokenResponse.CdrArrangementId == null) throw new Exception("Unexpected token response"); + var tokenResponse = await _dataHolderTokenService.GetResponse(authCode); + if (tokenResponse == null || tokenResponse.RefreshToken == null || tokenResponse.CdrArrangementId == null) + { + throw new Exception("Unexpected token response"); + } // Arrange - Revoke the arrangement await RevokeCdrArrangement(tokenResponse.CdrArrangementId); - // Act - Use refresh token to get a new access token. The refresh token should have been revoked because the arrangement was revoked + // Act - Use refresh token to get a new access token. The refresh token should have been revoked because the arrangement was revoked var response = await _dataHolderTokenService.SendRequest( grantType: "refresh_token", refreshToken: tokenResponse?.RefreshToken, - scope: US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS - ); + scope: US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -269,6 +279,7 @@ public async Task AC03_GetAccessToken_WithRevokedRefreshToken_ShouldRespondWith_ } [Fact] + // Calling revocation endpoint with invalid arrangementid should result in error (422UnprocessableEntity) public async Task AC04_POST_WithInvalidArrangementID_ShouldRespondWith_422UnprocessableEntity() { @@ -292,16 +303,19 @@ public async Task AC04_POST_WithInvalidArrangementID_ShouldRespondWith_422Unproc } [Fact] + // Calling revocation endpoint with arrangementid that is not associated with the DataRecipient should result in error (422UnprocessableEntity) public async Task AC05_POST_WithNonAssociatedArrangementID_ShouldRespondWith_422UnprocessableEntity() { async Task ArrangeAdditionalDataRecipient() { // Patch Register for additional data recipient - Helpers.AuthServer.PatchRedirectUriForRegister(_options, + Helpers.AuthServer.PatchRedirectUriForRegister( + _options, Constants.SoftwareProducts.AdditionalSoftwareProductId, _options.ADDITIONAL_SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS); - Helpers.AuthServer.PatchJwksUriForRegister(_options, + Helpers.AuthServer.PatchJwksUriForRegister( + _options, Constants.SoftwareProducts.AdditionalSoftwareProductId, _options.ADDITIONAL_SOFTWAREPRODUCT_JWKS_URI_FOR_INTEGRATION_TESTS); @@ -328,7 +342,7 @@ async Task ArrangeAdditionalDataRecipient() // Arrange - Get authcode and thus create a CDR arrangement for ADDITIONAL_SOFTWAREPRODUCT_ID client await ArrangeAdditionalDataRecipient(); - string additionalClientId = _options.LastRegisteredClientId ?? ""; + string additionalClientId = _options.LastRegisteredClientId ?? string.Empty; var requestUri = await _dataHolderParService.GetRequestUri( scope: US12963_CdrAuthServer_Token.SCOPE_TOKEN_ACCOUNTS, @@ -359,8 +373,7 @@ async Task ArrangeAdditionalDataRecipient() clientId: additionalClientId, redirectUri: _options.ADDITIONAL_SOFTWAREPRODUCT_REDIRECT_URI_FOR_INTEGRATION_TESTS, jwkCertificateFilename: Constants.Certificates.AdditionalJwksCertificateFilename, - jwkCertificatePassword: Constants.Certificates.AdditionalJwksCertificatePassword - ))?.CdrArrangementId; + jwkCertificatePassword: Constants.Certificates.AdditionalJwksCertificatePassword))?.CdrArrangementId; var expectedError = new InvalidArrangementException(additional_cdrArrangementId); var expectedContent = JsonConvert.SerializeObject(new ResponseErrorListV2(expectedError, string.Empty)); @@ -383,6 +396,7 @@ async Task ArrangeAdditionalDataRecipient() } [Fact] + // Calling revocation endpoint with invalid clientid should result in error (401Unauthorised) public async Task AC07_POST_WithInvalidClientId_ShouldRespondWith_400BadRequest() { @@ -415,6 +429,7 @@ public async Task AC07_POST_WithInvalidClientId_ShouldRespondWith_400BadRequest( } [Fact] + // Calling revocation endpoint with invalid clientassertiontype should result in error (401Unauthorised) public async Task AC08a_POST_WithInvalidClientAssertionType_ShouldRespondWith_400BadRequest() { @@ -431,7 +446,6 @@ public async Task AC08a_POST_WithInvalidClientAssertionType_ShouldRespondWith_40 (var authCode, _) = await authService.Authorise(); - // Arrange - Get cdrArrangementId var cdrArrangementId = (await _dataHolderTokenService.GetResponse(authCode))?.CdrArrangementId; @@ -448,6 +462,7 @@ public async Task AC08a_POST_WithInvalidClientAssertionType_ShouldRespondWith_40 } [Fact] + // Calling revocation endpoint with invalid clientassertion should result in error (401Unauthorised) public async Task AC08b_POST_WithInvalidClientAssertion_ShouldRespondWith_400BadRequest() { @@ -510,7 +525,8 @@ public async Task AC09_POST_WithCurrentHolderOfKey_Success_NoContent() } [Theory] - [InlineData(Constants.Certificates.AdditionalCertificateFilename, Constants.Certificates.AdditionalCertificatePassword)] // ie different holder of key + [InlineData(Constants.Certificates.AdditionalCertificateFilename, Constants.Certificates.AdditionalCertificatePassword)] // ie different holder of key + // Calling revocation endpoint with different holder of key should result in error public async Task AC09_POST_WithDifferentHolderOfKey_ShouldRespondWith_400BadRequest(string jwtCertificateFilename, string jwtCertificatePassword) { diff --git a/Source/CdrAuthServer.IntegrationTests/Tests/US18469_CdrAuthServer_TokenRevocation.cs b/Source/CdrAuthServer.IntegrationTests/Tests/US18469_CdrAuthServer_TokenRevocation.cs index 2512407..b18125a 100644 --- a/Source/CdrAuthServer.IntegrationTests/Tests/US18469_CdrAuthServer_TokenRevocation.cs +++ b/Source/CdrAuthServer.IntegrationTests/Tests/US18469_CdrAuthServer_TokenRevocation.cs @@ -1,23 +1,19 @@ -#undef FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS // FIXME - MJS - the test using this code needs to be in MDH integration tests since it uses MDH endpoint (ie $"{DH_MTLS_GATEWAY_URL}/cds-au/v1/banking/accounts") - -using System.Net; -#if FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS -using System.Net.Http; -#endif -using FluentAssertions; -using FluentAssertions.Execution; -using Xunit; +using System.Net; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Enums; +using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Fixtures; -using Constants = ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Constants; -using Microsoft.Extensions.Options; -using static ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services.DataHolderAuthoriseService; -using Xunit.DependencyInjection; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Interfaces; using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Models.Options; +using FluentAssertions; +using FluentAssertions.Execution; using Microsoft.Extensions.Configuration; -using ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Exceptions.AuthoriseExceptions; +using Microsoft.Extensions.Options; using Serilog; +using Xunit; +using Xunit.DependencyInjection; +using static ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Services.DataHolderAuthoriseService; +using Constants = ConsumerDataRight.ParticipantTooling.MockSolution.TestAutomation.Constants; namespace CdrAuthServer.IntegrationTests { @@ -32,7 +28,8 @@ public class US18469_CdrAuthServer_TokenRevocation : BaseTest, IClassFixture options, + public US18469_CdrAuthServer_TokenRevocation( + IOptions options, IOptions authServerOptions, IDataHolderParService dataHolderParService, IDataHolderRegisterService dataHolderRegisterService, @@ -57,14 +54,13 @@ public US18469_CdrAuthServer_TokenRevocation(IOptions opt _sqlQueryService = sqlQueryService ?? throw new ArgumentNullException(nameof(sqlQueryService)); _tokenRevocationService = tokenRevocationService ?? throw new ArgumentNullException(nameof(tokenRevocationService)); _apiServiceDirector = apiServiceDirector ?? throw new ArgumentNullException(nameof(apiServiceDirector)); - } public async Task InitializeAsync() { // Purge Authorisation Server Registrations and create a clean registration for each test to ensure test independance. Helpers.AuthServer.PurgeAuthServerForDataholder(_options); - await _dataHolderRegisterService.RegisterSoftwareProduct(responseType: "code,code id_token"); + await _dataHolderRegisterService.RegisterSoftwareProduct(responseType: ResponseType.Code); } public Task DisposeAsync() @@ -73,13 +69,11 @@ public Task DisposeAsync() } // Call authorise/token endpoints to create access and refresh tokens - private async Task<(string accessToken, string refreshToken)> CreateTokens(string? clientId = null, + private async Task<(string AccessToken, string RefreshToken)> CreateTokens( + string? clientId = null, int sharingDuration = Constants.AuthServer.SharingDuration) { - if (clientId == null) - { - clientId = _options.LastRegisteredClientId; - } + clientId ??= _options.LastRegisteredClientId; var authService = await new DataHolderAuthoriseServiceBuilder(_options, _dataHolderParService, _apiServiceDirector, false, _authServerOptions) .WithUserId(Constants.Users.UserIdKamillaSmith) @@ -93,7 +87,9 @@ public Task DisposeAsync() var tokenResponse = await _dataHolderTokenService.GetResponse(authCode, clientId: clientId); if (tokenResponse == null || tokenResponse.AccessToken == null || tokenResponse.RefreshToken == null) + { throw new Exception("Error getting access/refresh tokens"); + } return (tokenResponse.AccessToken, tokenResponse.RefreshToken); } @@ -125,10 +121,10 @@ private async Task ArrangeAdditionalDataRecipient() Constants.Certificates.AdditionalJwksCertificateFilename, Constants.Certificates.AdditionalJwksCertificatePassword); return clientId; - } [Fact] + // Revoke an access token public async Task AC01_Post_WithAccessToken_ShouldRespondWith_200OK() { @@ -172,6 +168,7 @@ static private async Task CallResourceAPI(string accessToken) [Theory] [InlineData(false, HttpStatusCode.OK)] [InlineData(true, HttpStatusCode.BadRequest)] + // Try to use a revoked access token to call a resource API public async Task AC02_CallResourceAPI_WithRevokedAccessToken_ShouldRespondWith_401Unauthorised(bool revoke, HttpStatusCode expectedResourceAPIStatusCode) { @@ -196,11 +193,12 @@ public async Task AC02_CallResourceAPI_WithRevokedAccessToken_ShouldRespondWith_ #if FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS // Assert - Check call to resource API returns correct status code (await CallResourceAPI(accessToken)).Should().Be(expectedResourceAPIStatusCode); -#endif +#endif } } [Fact] + // Revoke a refresh token public async Task AC03_Post_WithRefreshToken_ShouldRespondWith_200OK() { @@ -222,6 +220,7 @@ public async Task AC03_Post_WithRefreshToken_ShouldRespondWith_200OK() [Theory] [InlineData(false, HttpStatusCode.OK)] [InlineData(true, HttpStatusCode.BadRequest)] + // Try to use a revoked refresh token to get new access and refresh token public async Task AC04_CallTokenAPI_WithRevokedRefreshToken_ShouldRespondWith_400BadRequest(bool revoke, HttpStatusCode expectedStatusCode) { @@ -251,6 +250,7 @@ public async Task AC04_CallTokenAPI_WithRevokedRefreshToken_ShouldRespondWith_40 [Theory] [InlineData("foo", HttpStatusCode.OK)] + // Revoke an invalid access token public async Task AC05_Post_WithInvalidAccessToken_ShouldRespondWith_200OK(string token, HttpStatusCode expectedStatusCode) { @@ -270,6 +270,7 @@ public async Task AC05_Post_WithInvalidAccessToken_ShouldRespondWith_200OK(strin [Theory] [InlineData("foo", HttpStatusCode.OK)] + // Revoke an invalid refresh token public async Task AC06_Post_WithInvalidRefreshToken_ShouldRespondWith_200OK(string token, HttpStatusCode expectedStatusCode) { @@ -289,6 +290,7 @@ public async Task AC06_Post_WithInvalidRefreshToken_ShouldRespondWith_200OK(stri [Theory] [InlineData("foo", HttpStatusCode.OK)] + // Revoke access token with invalid token type hint public async Task AC07_Post_WithInvalidAccessTokenTypeHint_ShouldRespondWith_200OK(string tokenTypeHint, HttpStatusCode expectedStatusCode) { @@ -309,12 +311,13 @@ public async Task AC07_Post_WithInvalidAccessTokenTypeHint_ShouldRespondWith_200 #if FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS (await CallResourceAPI(accessToken)).Should().NotBe(HttpStatusCode.OK, "token should have been revoked"); -#endif +#endif } } [Theory] [InlineData("foo", HttpStatusCode.OK)] + // Revoke refresh token with invalid token type hint public async Task AC08_Post_WithInvalidRefreshTokenTypeHint_ShouldRespondWith_200OK(string tokenTypeHint, HttpStatusCode expectedStatusCode) { @@ -341,6 +344,7 @@ public async Task AC08_Post_WithInvalidRefreshTokenTypeHint_ShouldRespondWith_20 [Theory] [InlineData(Constants.Certificates.AdditionalCertificateFilename, Constants.Certificates.AdditionalCertificatePassword, HttpStatusCode.OK)] // ie different holder of key + // Revoke an access token with different holder of key public async Task AC09_Post_AccessTokenWithDifferentHolderOfKey_ShouldRespondWith_200OK(string jwtCertificateFilename, string jwtCertificatePassword, HttpStatusCode expectedStatusCode) { @@ -348,7 +352,6 @@ public async Task AC09_Post_AccessTokenWithDifferentHolderOfKey_ShouldRespondWit // Then we want to authenticate successfully to the token revocation endpoint, but pass in the access // token that is not owned by the caller. // The token revocation should still return a 200 OK status, but the token should not have been revoked. - Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(jwtCertificateFilename), jwtCertificateFilename, nameof(jwtCertificatePassword), jwtCertificatePassword, nameof(expectedStatusCode), expectedStatusCode); // Arrange @@ -375,7 +378,7 @@ public async Task AC09_Post_AccessTokenWithDifferentHolderOfKey_ShouldRespondWit #if FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS // Assert - Should be able to access resource API since token not revoked (await CallResourceAPI(accessToken)).Should().Be(HttpStatusCode.OK, "token should NOT have been revoked"); -#endif +#endif } } @@ -387,10 +390,9 @@ public async Task AC10_Post_RefreshTokenWithDifferentHolderOfKey_ShouldRespondWi // Then we want to authenticate successfully to the token revocation endpoint, but pass in the access // token that is not owned by the caller. // The token revocation should still return a 200 OK status, but the token should not have been revoked. - Log.Information("Running test with Params: {P1}={V1}, {P2}={V2}, {P3}={V3}.", nameof(jwtCertificateFilename), jwtCertificateFilename, nameof(jwtCertificatePassword), jwtCertificatePassword, nameof(expectedStatusCode), expectedStatusCode); - // Arrange + // Arrange string originalClientId = _sqlQueryService.GetClientId(Constants.SoftwareProducts.SoftwareProductId); string additionalClientId = await ArrangeAdditionalDataRecipient(); @@ -418,6 +420,7 @@ public async Task AC10_Post_RefreshTokenWithDifferentHolderOfKey_ShouldRespondWi } [Fact] + // Revoke an access token with invalid client id public async Task AC12_Post_AccessTokenWithInvalidClientId_ShouldRespondWith_400BadRequest() { @@ -441,11 +444,12 @@ public async Task AC12_Post_AccessTokenWithInvalidClientId_ShouldRespondWith_400 #if FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS // Assert - Should be able to access resource API since token not revoked (await CallResourceAPI(accessToken)).Should().Be(HttpStatusCode.OK, "token should NOT have been revoked"); -#endif +#endif } } [Fact] + // Revoke an access token with invalid client assertion type public async Task AC13_Post_AccessTokenWithInvalidClientAssertionType_ShouldRespondWith_400BadRequest() { @@ -467,11 +471,12 @@ public async Task AC13_Post_AccessTokenWithInvalidClientAssertionType_ShouldResp #if FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS // Assert - Should be able to access resource API since token not revoked (await CallResourceAPI(accessToken)).Should().Be(HttpStatusCode.OK, "token should NOT have been revoked"); -#endif +#endif } } [Fact] + // Revoke an access token with invalid client assertion public async Task AC14_Post_AccessTokenWithInvalidClientAssertion_ShouldRespondWith_401Unauthorised() { @@ -494,11 +499,12 @@ public async Task AC14_Post_AccessTokenWithInvalidClientAssertion_ShouldRespondW #if FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS // Assert - Should be able to access resource API since token not revoked (await CallResourceAPI(accessToken)).Should().Be(HttpStatusCode.OK, "token should NOT have been revoked"); -#endif +#endif } } [Fact] + // Revoke an access token with client assertion signed with invalid certificate public async Task AC15_Post_AccessTokenWithClientAssertionSignedWithInvalidCert_ShouldRespondWith_401Unauthorised() { @@ -512,8 +518,7 @@ public async Task AC15_Post_AccessTokenWithClientAssertionSignedWithInvalidCert_ token: accessToken, tokenTypeHint: "access_token", jwtCertificateFilename: Constants.Certificates.AdditionalCertificateFilename, // ie this is not JWT_CERTIFICATE_FILENAME, hence it's not a valid certificate to sign JWT with - jwtCertificatePassword: Constants.Certificates.AdditionalCertificatePassword - ); + jwtCertificatePassword: Constants.Certificates.AdditionalCertificatePassword); // Assert using (new AssertionScope(BaseTestAssertionStrategy)) @@ -523,9 +528,8 @@ public async Task AC15_Post_AccessTokenWithClientAssertionSignedWithInvalidCert_ #if FIXME_MJS_BELONGS_IN_MDH_INTEGRATION_TESTS // Assert - Should be able to access resource API since token not revoked (await CallResourceAPI(accessToken)).Should().Be(HttpStatusCode.OK, "token should NOT have been revoked"); -#endif +#endif } } - } } diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Orderers/AlphabeticalOrderer.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Orderers/AlphabeticalOrderer.cs index 44dafe5..33ee195 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Orderers/AlphabeticalOrderer.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Orderers/AlphabeticalOrderer.cs @@ -1,11 +1,13 @@ -using Xunit.Abstractions; +using Xunit.Abstractions; using Xunit.Sdk; namespace CdrAuthServer.IntegrationTests.XUnit.Orderers { public class AlphabeticalOrderer : ITestCaseOrderer { - public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase => + public IEnumerable OrderTestCases(IEnumerable testCases) + where TTestCase : ITestCase + => testCases.OrderBy(testCase => testCase.TestMethod.Method.Name); } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/Samples.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/Samples.cs index 22dd9c9..4e484f9 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/Samples.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/Samples.cs @@ -54,4 +54,4 @@ public void DynamicallySkipped(string value) } } } -#endif \ No newline at end of file +#endif diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkipTestException.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkipTestException.cs index 451bdaf..be763f5 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkipTestException.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkipTestException.cs @@ -5,6 +5,8 @@ namespace XUnit_Skippable public class SkipTestException : Exception { public SkipTestException(string reason) - : base(reason) { } + : base(reason) + { + } } } diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkippableFactAttribute.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkippableFactAttribute.cs index b7ed4a0..9b4bc98 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkippableFactAttribute.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkippableFactAttribute.cs @@ -4,5 +4,7 @@ namespace XUnit_Skippable { [XunitTestCaseDiscoverer("CdrAuthServer.IntegrationTests.XUnit_Skippable.SkippableFactDiscoverer", "CdrAuthServer.IntegrationTests")] - public class SkippableFactAttribute : FactAttribute { } + public class SkippableFactAttribute : FactAttribute + { + } } diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkippableTheoryAttribute.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkippableTheoryAttribute.cs index 705f6a4..5fd862d 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkippableTheoryAttribute.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/SkippableTheoryAttribute.cs @@ -4,5 +4,7 @@ namespace XUnit_Skippable { [XunitTestCaseDiscoverer("CdrAuthServer.IntegrationTests.XUnit_Skippable.SkippableTheoryDiscoverer", "CdrAuthServer.IntegrationTests")] - public class SkippableTheoryAttribute : TheoryAttribute { } + public class SkippableTheoryAttribute : TheoryAttribute + { + } } diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactDiscoverer.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactDiscoverer.cs index 61331ef..bd77492 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactDiscoverer.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactDiscoverer.cs @@ -6,7 +6,7 @@ namespace XUnit_Skippable { public class SkippableFactDiscoverer : IXunitTestCaseDiscoverer { - readonly IMessageSink diagnosticMessageSink; + private readonly IMessageSink diagnosticMessageSink; public SkippableFactDiscoverer(IMessageSink diagnosticMessageSink) { diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactMessageBus.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactMessageBus.cs index 3c8537f..bee38ff 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactMessageBus.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactMessageBus.cs @@ -6,7 +6,7 @@ namespace XUnit_Skippable { public class SkippableFactMessageBus : IMessageBus { - readonly IMessageBus innerBus; + private readonly IMessageBus innerBus; public SkippableFactMessageBus(IMessageBus innerBus) { @@ -15,7 +15,9 @@ public SkippableFactMessageBus(IMessageBus innerBus) public int DynamicallySkippedTestCount { get; private set; } - public void Dispose() { } + public void Dispose() + { + } public bool QueueMessage(IMessageSinkMessage message) { diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactTestCase.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactTestCase.cs index c8ff29d..a7fa533 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactTestCase.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableFactTestCase.cs @@ -1,7 +1,4 @@ -using System; -using System.ComponentModel; -using System.Threading; -using System.Threading.Tasks; +using System.ComponentModel; using Xunit.Abstractions; using Xunit.Sdk; @@ -13,16 +10,21 @@ public class SkippableFactTestCase : XunitTestCase { [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableFactTestCase() { } + public SkippableFactTestCase() + { + } public SkippableFactTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[] testMethodArguments = null) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { } + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) + { + } - public override async Task RunAsync(IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) + public override async Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) { var skipMessageBus = new SkippableFactMessageBus(messageBus); var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource); diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableTheoryDiscoverer.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableTheoryDiscoverer.cs index f192867..5e19471 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableTheoryDiscoverer.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableTheoryDiscoverer.cs @@ -7,8 +7,8 @@ namespace XUnit_Skippable { public class SkippableTheoryDiscoverer : IXunitTestCaseDiscoverer { - readonly IMessageSink diagnosticMessageSink; - readonly TheoryDiscoverer theoryDiscoverer; + private readonly IMessageSink diagnosticMessageSink; + private readonly TheoryDiscoverer theoryDiscoverer; public SkippableTheoryDiscoverer(IMessageSink diagnosticMessageSink) { diff --git a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableTheoryTestCase.cs b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableTheoryTestCase.cs index 8da74e8..f26b716 100644 --- a/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableTheoryTestCase.cs +++ b/Source/CdrAuthServer.IntegrationTests/XUnit/Skippable/XunitExtensions/SkippableTheoryTestCase.cs @@ -11,16 +11,21 @@ public class SkippableTheoryTestCase : XunitTheoryTestCase { [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableTheoryTestCase() { } + public SkippableTheoryTestCase() + { + } public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { } + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) + { + } - public override async Task RunAsync(IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) + public override async Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) { // Duplicated code from SkippableFactTestCase. I'm sure we could find a way to de-dup with some thought. var skipMessageBus = new SkippableFactMessageBus(messageBus); diff --git a/Source/CdrAuthServer.Repository/CdrAuthServer.Repository.csproj b/Source/CdrAuthServer.Repository/CdrAuthServer.Repository.csproj index 3eab87a..af6a96d 100644 --- a/Source/CdrAuthServer.Repository/CdrAuthServer.Repository.csproj +++ b/Source/CdrAuthServer.Repository/CdrAuthServer.Repository.csproj @@ -6,11 +6,13 @@ $(Version) $(Version) enable - enable + enable + True + all @@ -21,10 +23,19 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/CdrAuthServer.Repository/CdrRepository.cs b/Source/CdrAuthServer.Repository/CdrRepository.cs index be76c64..30f28e4 100644 --- a/Source/CdrAuthServer.Repository/CdrRepository.cs +++ b/Source/CdrAuthServer.Repository/CdrRepository.cs @@ -25,23 +25,23 @@ public async Task GetSoftwareProduct(string softwareProductId) } public async Task InsertDataRecipients(List softwareProducts) - { + { var newSoftwareProductList = mapper.Map>(softwareProducts); - using (var transaction = cdrAuthServervDatabaseContext.Database.BeginTransaction()) - { - // Bulk insert the new data recipient software products. - await cdrAuthServervDatabaseContext.SoftwareProducts.AddRangeAsync(newSoftwareProductList); - await cdrAuthServervDatabaseContext.SaveChangesAsync(); + using var transaction = await cdrAuthServervDatabaseContext.Database.BeginTransactionAsync(); + + // Bulk insert the new data recipient software products. + await cdrAuthServervDatabaseContext.SoftwareProducts.AddRangeAsync(newSoftwareProductList); + await cdrAuthServervDatabaseContext.SaveChangesAsync(); - // Commit the transaction. - transaction.Commit(); - } + // Commit the transaction. + await transaction.CommitAsync(); } public async Task PurgeDataRecipients() { - using var transaction = cdrAuthServervDatabaseContext.Database.BeginTransaction(); + using var transaction = await cdrAuthServervDatabaseContext.Database.BeginTransactionAsync(); + // Remove the existing data recipients software products. var existingSoftwareProducts = await cdrAuthServervDatabaseContext.SoftwareProducts.AsNoTracking().Where(sp => sp.SoftwareProductId != "cdr-register").ToListAsync(); @@ -50,9 +50,9 @@ public async Task PurgeDataRecipients() cdrAuthServervDatabaseContext.RemoveRange(existingSoftwareProducts); await cdrAuthServervDatabaseContext.SaveChangesAsync(); } + // Commit the transaction. - transaction.Commit(); + await transaction.CommitAsync(); } - } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.Repository/ClientRepository.cs b/Source/CdrAuthServer.Repository/ClientRepository.cs index 8606690..e2e6544 100644 --- a/Source/CdrAuthServer.Repository/ClientRepository.cs +++ b/Source/CdrAuthServer.Repository/ClientRepository.cs @@ -29,14 +29,13 @@ public async Task Create(Client client) var clientEntity = mapper.Map(client); await cdrAuthServervDatabaseContext.Clients.AddAsync(clientEntity); - cdrAuthServervDatabaseContext.SaveChanges(); + await cdrAuthServervDatabaseContext.SaveChangesAsync(); client.ClientId = clientEntity.ClientId.ToString(); return client; - } - catch(Exception ex) + catch (Exception ex) { this.logger.LogError(ex, "Create Client failed for {@Client}", client); throw; @@ -81,7 +80,7 @@ public async Task Delete(string clientId) public async Task GetBySoftwareProductId(string softwareProductId) { var clientClaims = await cdrAuthServervDatabaseContext.ClientClaims.AsNoTracking().Include(cc => cc.Client).FirstOrDefaultAsync(cc => cc.Type == Constants.ClaimNames.SoftwareId && cc.Value == softwareProductId); - + return mapper.Map(clientClaims?.Client); } @@ -95,13 +94,13 @@ public async Task Delete(string clientId) try { - //convert the domain to entity + // convert the domain to entity var domainToEntityClient = mapper.Map(client); - //now map entity to entity + // now map entity to entity mapper.Map(domainToEntityClient, entity); - - //now save the changes. + + // now save the changes. await cdrAuthServervDatabaseContext.SaveChangesAsync(); } catch (Exception ex) diff --git a/Source/CdrAuthServer.Repository/Configuration/ClientClaimsConfiguration.cs b/Source/CdrAuthServer.Repository/Configuration/ClientClaimsConfiguration.cs index b9de6d5..8923990 100644 --- a/Source/CdrAuthServer.Repository/Configuration/ClientClaimsConfiguration.cs +++ b/Source/CdrAuthServer.Repository/Configuration/ClientClaimsConfiguration.cs @@ -16,10 +16,9 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(cc => cc.ClientId, "IX_ClientClaims_ClientId"); - builder.HasData( - new ClientClaims {Id=1, Type = "SoftwareId", Value = "22222222-2222-2222-2222-222222222222", ClientId = "11111111-1111-1111-1111-111111111111" }, - new ClientClaims {Id=2, Type = "JwksUri",Value = "https://localhost:9001/jwks",ClientId = "11111111-1111-1111-1111-111111111111" } - ); + builder.HasData( + new ClientClaims { Id = 1, Type = "SoftwareId", Value = "22222222-2222-2222-2222-222222222222", ClientId = "11111111-1111-1111-1111-111111111111" }, + new ClientClaims { Id = 2, Type = "JwksUri", Value = "https://localhost:9001/jwks", ClientId = "11111111-1111-1111-1111-111111111111" }); } } } diff --git a/Source/CdrAuthServer.Repository/Configuration/ClientConfiguration.cs b/Source/CdrAuthServer.Repository/Configuration/ClientConfiguration.cs index 850ef23..4e6a495 100644 --- a/Source/CdrAuthServer.Repository/Configuration/ClientConfiguration.cs +++ b/Source/CdrAuthServer.Repository/Configuration/ClientConfiguration.cs @@ -12,7 +12,7 @@ public void Configure(EntityTypeBuilder builder) builder.HasMany(c => c.ClientClaims).WithOne(cc => cc.Client).HasForeignKey(c => c.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade); - //seed client + // seed client builder.HasData( new Client() { diff --git a/Source/CdrAuthServer.Repository/Configuration/GrantConfiguration.cs b/Source/CdrAuthServer.Repository/Configuration/GrantConfiguration.cs index 55f9a2b..39ccab2 100644 --- a/Source/CdrAuthServer.Repository/Configuration/GrantConfiguration.cs +++ b/Source/CdrAuthServer.Repository/Configuration/GrantConfiguration.cs @@ -29,15 +29,14 @@ public void Configure(EntityTypeBuilder builder) new Dictionary { { "refresh_token", refreshToken }, - { "account_id", new List {"123", "456", "789" } } - }) + { "account_id", new List { "123", "456", "789" } }, + }), }; builder.HasData( cdrArrangementGrant, - - //RefreshTokenGrant new Grant() { + // RefreshTokenGrant Key = refreshToken, ClientId = "c6327f87-687a-4369-99a4-eaacd3bb8210", CreatedAt = DateTime.UtcNow, @@ -46,15 +45,15 @@ public void Configure(EntityTypeBuilder builder) SubjectId = "customer1", UsedAt = null, Scope = Scopes.AllSectorScopes, - Data = JsonSerializer.Serialize(new Dictionary { - { "response_type", ResponseTypes.Hybrid }, - { "CdrArrangementId", cdrArrangementGrant.Key } - }) + Data = JsonSerializer.Serialize(new Dictionary + { + { "response_type", ResponseTypes.AuthCode }, + { "CdrArrangementId", cdrArrangementGrant.Key }, + }), }, - - //RefreshTokenGrant2 new Grant() { + // RefreshTokenGrant2 Key = "expired-refresh-token", ClientId = "c6327f87-687a-4369-99a4-eaacd3bb8210", CreatedAt = DateTime.UtcNow.AddDays(-366), @@ -63,10 +62,11 @@ public void Configure(EntityTypeBuilder builder) SubjectId = "customer1", UsedAt = null, Scope = Scopes.BankingSectorScopes, - Data = JsonSerializer.Serialize(new Dictionary { - { "response_type", ResponseTypes.Hybrid }, - { "CdrArrangementId", Guid.NewGuid().ToString() } - }) + Data = JsonSerializer.Serialize(new Dictionary + { + { "response_type", ResponseTypes.AuthCode }, + { "CdrArrangementId", Guid.NewGuid().ToString() }, + }), }); } } diff --git a/Source/CdrAuthServer.Repository/Configuration/SoftwareProductConfiguration.cs b/Source/CdrAuthServer.Repository/Configuration/SoftwareProductConfiguration.cs index 0950eea..898c1c9 100644 --- a/Source/CdrAuthServer.Repository/Configuration/SoftwareProductConfiguration.cs +++ b/Source/CdrAuthServer.Repository/Configuration/SoftwareProductConfiguration.cs @@ -7,34 +7,35 @@ internal class SoftwareProductConfiguration : IEntityTypeConfiguration { - private const string _cdrRegister = "cdr-register"; - private const string _dummyUrl = "https://cdrsandbox.gov.au/logo192.png"; + private const string CdrRegister = "cdr-register"; + private const string DummyUrl = "https://cdrsandbox.gov.au/logo192.png"; + public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.SoftwareProductId); - //seed software products + // seed software products builder.HasData( new SoftwareProduct() { SoftwareProductId = "c6327f87-687a-4369-99a4-eaacd3bb8210", SoftwareProductName = "Mock Data Recipient Software Product", SoftwareProductDescription = "Mock Data Recipient Software Product", - LogoUri = _dummyUrl, + LogoUri = DummyUrl, Status = EntityStatus.Active, LegalEntityId = "18B75A76-5821-4C9E-B465-4709291CF0F4", LegalEntityName = "Mock Data Recipient Legal Entity Name", LegalEntityStatus = EntityStatus.Active, BrandId = "FFB1C8BA-279E-44D8-96F0-1BC34A6B436F", BrandName = "Mock Data Recipient Brand Name", - BrandStatus = EntityStatus.Active + BrandStatus = EntityStatus.Active, }, new SoftwareProduct() { SoftwareProductId = "22222222-2222-2222-2222-222222222222", SoftwareProductName = "Active Data Recipient Software Product", SoftwareProductDescription = "Active Data Recipient Software Product", - LogoUri = _dummyUrl, + LogoUri = DummyUrl, Status = EntityStatus.Active, LegalEntityId = "LLLLLLLL-2222-2222-2222-222222222222", LegalEntityName = "Active Data Recipient Legal Entity Name", @@ -48,30 +49,30 @@ public void Configure(EntityTypeBuilder builder) SoftwareProductId = "99999999-9999-9999-9999-999999999999", SoftwareProductName = "Removed Software Product", SoftwareProductDescription = "Removed Software Product", - LogoUri = _dummyUrl, + LogoUri = DummyUrl, Status = EntityStatus.Removed, LegalEntityId = "LLLLLLLL-2222-2222-2222-222222222222", LegalEntityName = "Active Data Recipient Legal Entity Name", LegalEntityStatus = EntityStatus.Active, BrandId = "BBBBBBBB-2222-2222-2222-222222222222", BrandName = "Active Data Recipient Brand Name", - BrandStatus = EntityStatus.Active + BrandStatus = EntityStatus.Active, }, - //This is the software product with which the register is associated. - //we need this for the GetRecipients function app to be able to fetch the access token. new SoftwareProduct() { - SoftwareProductId = _cdrRegister, - SoftwareProductName = _cdrRegister, + // This is the software product with which the register is associated. + // we need this for the GetRecipients function app to be able to fetch the access token. + SoftwareProductId = CdrRegister, + SoftwareProductName = CdrRegister, SoftwareProductDescription = "Mock Register", - LogoUri = _dummyUrl, + LogoUri = DummyUrl, Status = EntityStatus.Active, - LegalEntityId = _cdrRegister, - LegalEntityName = _cdrRegister, + LegalEntityId = CdrRegister, + LegalEntityName = CdrRegister, LegalEntityStatus = EntityStatus.Active, - BrandId = _cdrRegister, - BrandName = _cdrRegister, - BrandStatus = EntityStatus.Active + BrandId = CdrRegister, + BrandName = CdrRegister, + BrandStatus = EntityStatus.Active, }); } } diff --git a/Source/CdrAuthServer.Repository/Configuration/TokenConfiguration.cs b/Source/CdrAuthServer.Repository/Configuration/TokenConfiguration.cs index 1e8ee9a..08e1827 100644 --- a/Source/CdrAuthServer.Repository/Configuration/TokenConfiguration.cs +++ b/Source/CdrAuthServer.Repository/Configuration/TokenConfiguration.cs @@ -8,7 +8,7 @@ internal class TokenConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.HasKey(x => x.Id); + builder.HasKey(x => x.Id); } } } diff --git a/Source/CdrAuthServer.Repository/Entities/Client.cs b/Source/CdrAuthServer.Repository/Entities/Client.cs index e889f8d..1b2f8e3 100644 --- a/Source/CdrAuthServer.Repository/Entities/Client.cs +++ b/Source/CdrAuthServer.Repository/Entities/Client.cs @@ -7,7 +7,7 @@ public Client() this.ClientId = Guid.NewGuid().ToString(); } - public string ClientId { get; set; } = string.Empty; + public string ClientId { get; set; } public long ClientIdIssuedAt { get; set; } @@ -17,6 +17,5 @@ public Client() // every Client has zero or more claims (one-to-many) public virtual ICollection? ClientClaims { get; set; } - } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.Repository/Entities/ClientClaims.cs b/Source/CdrAuthServer.Repository/Entities/ClientClaims.cs index fe21112..6a08cf4 100644 --- a/Source/CdrAuthServer.Repository/Entities/ClientClaims.cs +++ b/Source/CdrAuthServer.Repository/Entities/ClientClaims.cs @@ -5,12 +5,12 @@ public class ClientClaims public int Id { get; set; } // every ClientClaim belongs to exactly one Client using foreign key - public string ClientId { get; set; } = String.Empty; + public string ClientId { get; set; } = string.Empty; public virtual Client Client { get; set; } - public string Type { get; set; } = String.Empty; - - public string Value { get; set; } = String.Empty; + public string Type { get; set; } = string.Empty; + + public string Value { get; set; } = string.Empty; } } diff --git a/Source/CdrAuthServer.Repository/Entities/Grant.cs b/Source/CdrAuthServer.Repository/Entities/Grant.cs index 5a0e717..b5190ca 100644 --- a/Source/CdrAuthServer.Repository/Entities/Grant.cs +++ b/Source/CdrAuthServer.Repository/Entities/Grant.cs @@ -1,13 +1,12 @@ - -namespace CdrAuthServer.Repository.Entities +namespace CdrAuthServer.Repository.Entities { public class Grant { public string Key { get; set; } = string.Empty; - public string GrantType { get; set; } = String.Empty; + public string GrantType { get; set; } = string.Empty; - public string ClientId { get; set; } = String.Empty; + public string ClientId { get; set; } = string.Empty; public string? SubjectId { get; set; } @@ -19,6 +18,6 @@ public class Grant public string? Scope { get; set; } - public string Data { get; set; } = String.Empty; - } + public string Data { get; set; } = string.Empty; + } } diff --git a/Source/CdrAuthServer.Repository/Entities/SoftwareProduct.cs b/Source/CdrAuthServer.Repository/Entities/SoftwareProduct.cs index 42f7de8..362354f 100644 --- a/Source/CdrAuthServer.Repository/Entities/SoftwareProduct.cs +++ b/Source/CdrAuthServer.Repository/Entities/SoftwareProduct.cs @@ -2,26 +2,26 @@ { public class SoftwareProduct { - public string SoftwareProductId { get; set; } = String.Empty; + public string SoftwareProductId { get; set; } = string.Empty; - public string SoftwareProductName { get; set; } = String.Empty; + public string SoftwareProductName { get; set; } = string.Empty; - public string SoftwareProductDescription { get; set; } = String.Empty; + public string SoftwareProductDescription { get; set; } = string.Empty; - public string LogoUri { get; set; } = String.Empty; + public string LogoUri { get; set; } = string.Empty; - public string Status { get; set; } = String.Empty; + public string Status { get; set; } = string.Empty; - public string LegalEntityId { get; set; } = String.Empty; + public string LegalEntityId { get; set; } = string.Empty; - public string LegalEntityName { get; set; } = String.Empty; + public string LegalEntityName { get; set; } = string.Empty; - public string LegalEntityStatus { get; set; } = String.Empty; + public string LegalEntityStatus { get; set; } = string.Empty; - public string BrandId { get; set; } = String.Empty; + public string BrandId { get; set; } = string.Empty; - public string BrandName { get; set; } = String.Empty; + public string BrandName { get; set; } = string.Empty; - public string BrandStatus { get; set; } = String.Empty; + public string BrandStatus { get; set; } = string.Empty; } } diff --git a/Source/CdrAuthServer.Repository/Entities/Token.cs b/Source/CdrAuthServer.Repository/Entities/Token.cs index 5ba7f70..f3a841d 100644 --- a/Source/CdrAuthServer.Repository/Entities/Token.cs +++ b/Source/CdrAuthServer.Repository/Entities/Token.cs @@ -9,6 +9,7 @@ namespace CdrAuthServer.Repository.Entities public class Token { public string Id { get; set; } = string.Empty; + public bool BlackListed { get; set; } } } diff --git a/Source/CdrAuthServer.Repository/GrantRepository.cs b/Source/CdrAuthServer.Repository/GrantRepository.cs index 7168d2c..3a04b43 100644 --- a/Source/CdrAuthServer.Repository/GrantRepository.cs +++ b/Source/CdrAuthServer.Repository/GrantRepository.cs @@ -8,11 +8,9 @@ using CdrAuthServer.Repository.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; - public class GrantRepository : IGrantRepository { - private readonly CdrAuthServerDatabaseContext cdrAuthServervDatabaseContext; private readonly IMapper mapper; private readonly ILogger logger; @@ -31,12 +29,11 @@ public async Task Create(Grant grant) var grantEntity = this.mapper.Map(grant); await cdrAuthServervDatabaseContext.Grants.AddAsync(grantEntity); - cdrAuthServervDatabaseContext.SaveChanges(); + await cdrAuthServervDatabaseContext.SaveChangesAsync(); grant.Key = grantEntity.Key.ToString(); return grant; - } catch (Exception ex) { @@ -77,16 +74,16 @@ public async Task> List(string clientId, string grantType) public async Task Update(Grant grant) { - var _grant = await cdrAuthServervDatabaseContext.Grants.AsNoTracking().FirstOrDefaultAsync(g => g.Key == grant.Key); - - if (_grant == null) + var grantStored = await cdrAuthServervDatabaseContext.Grants.AsNoTracking().FirstOrDefaultAsync(g => g.Key == grant.Key); + + if (grantStored == null) { return null; } try { - var grantToUpdate = mapper.Map(grant); + var grantToUpdate = mapper.Map(grant); cdrAuthServervDatabaseContext.Grants.Update(grantToUpdate); await cdrAuthServervDatabaseContext.SaveChangesAsync(); } diff --git a/Source/CdrAuthServer.Repository/Infrastructure/CdrAuthServerDatabaseContext.cs b/Source/CdrAuthServer.Repository/Infrastructure/CdrAuthServerDatabaseContext.cs index 50d4ba1..e5159d4 100644 --- a/Source/CdrAuthServer.Repository/Infrastructure/CdrAuthServerDatabaseContext.cs +++ b/Source/CdrAuthServer.Repository/Infrastructure/CdrAuthServerDatabaseContext.cs @@ -10,7 +10,8 @@ public CdrAuthServerDatabaseContext() { } - public CdrAuthServerDatabaseContext(DbContextOptions options) : base(options) + public CdrAuthServerDatabaseContext(DbContextOptions options) + : base(options) { } diff --git a/Source/CdrAuthServer.Repository/Infrastructure/MappingProfile.cs b/Source/CdrAuthServer.Repository/Infrastructure/MappingProfile.cs index 6115534..fcd5e65 100644 --- a/Source/CdrAuthServer.Repository/Infrastructure/MappingProfile.cs +++ b/Source/CdrAuthServer.Repository/Infrastructure/MappingProfile.cs @@ -9,7 +9,7 @@ public class MappingProfile : Profile { - const char multiStringSeparator = ';'; + private const char MultiStringSeparator = ';'; public MappingProfile() { @@ -20,7 +20,6 @@ public MappingProfile() .ForMember(dest => dest.ClientClaims, opt => opt.Ignore()) .AfterMap(MapDomainClientToEntityClientClaims); - CreateMap() .ForMember(g => g.Data, cfg => cfg.MapFrom((grant, _) => JsonConvert.DeserializeObject>(grant.Data))); @@ -32,7 +31,6 @@ public MappingProfile() CreateMap() .ForMember(dest => dest.ClientId, cfg => cfg.Ignore()) .ForMember(dest => dest.ClientIdIssuedAt, cfg => cfg.Ignore()); - } private static PropertyInfo? GetProperyByAttributeNameOrProperyName(object obj, string flagName) @@ -42,17 +40,13 @@ public MappingProfile() // iterate the properties var prop = (from property in objType.GetProperties() - // iterate it's attributes from attrib in property.GetCustomAttributes(typeof(JsonPropertyAttribute), false).Cast() - // filter on the name where attrib.PropertyName == flagName - // select the propertyInfo select property).FirstOrDefault(); - - if(prop == null) + if (prop == null) { - //we cannot find a property by attribute name so find by property name + // we cannot find a property by attribute name so find by property name return obj.GetType().GetProperty(flagName); } @@ -61,22 +55,29 @@ from attrib in property.GetCustomAttributes(typeof(JsonPropertyAttribute), false public static void MapEntityClientClaimsToDomainClient(Client source, Domain.Entities.Client destination) { - //map claims to properties on client - foreach (var item in source.ClientClaims) + var clientCliams = source.ClientClaims; + + if (clientCliams == null) + { + return; + } + + // map claims to properties on client + foreach (var item in clientCliams) { var destionationProperty = GetProperyByAttributeNameOrProperyName(destination, item.Type); if (destionationProperty?.PropertyType == typeof(IEnumerable)) { - //split the semicolon separate strings into IEnumerable - var splitString = item.Value.Split(multiStringSeparator, StringSplitOptions.None); + // split the semicolon separate strings into IEnumerable + var splitString = item.Value.Split(MultiStringSeparator, StringSplitOptions.None); - destionationProperty?.SetValue(destination, splitString, null); + destionationProperty.SetValue(destination, splitString, null); continue; } destionationProperty?.SetValue(destination, item.Value, null); - } + } } public static void MapDomainClientToEntityClientClaims(Domain.Entities.Client source, Client destination) @@ -85,40 +86,45 @@ public static void MapDomainClientToEntityClientClaims(Domain.Entities.Client so var sourceProperties = source.GetType().GetProperties(bindingAttr); - //the properties that are there already on entity as considered ignore list - //so we can get the claims and we store them as claims + // the properties that are there already on entity as considered ignore list + // so we can get the claims and we store them as claims List ignoreList = destination.GetType().GetProperties(bindingAttr).Select(p => p.Name).ToList(); var clientClaimsCollection = new List(); foreach (var item in sourceProperties) { - if (!ignoreList.Contains(item.Name)) + if (item?.Name != null && !ignoreList.Contains(item.Name)) { string? value = null; - //IEnumerable + // IEnumerable if (item.PropertyType == typeof(IEnumerable)) { - //split the semicolon separate strings into IEnumberable - var joinedString = string.Join(multiStringSeparator, item.GetValue(source) as IEnumerable); - value = joinedString; + var values = item.GetValue(source) as IEnumerable; + if (values != null) + { + // split the semicolon separate strings into IEnumberable + var joinedString = string.Join(MultiStringSeparator, values); + value = joinedString; + } } - //string type + // string type if (item.PropertyType == typeof(string)) { - value = item?.GetValue(source)?.ToString(); + value = item.GetValue(source)?.ToString(); } - //we save the claim only, if it has a value. + // we save the claim only, if it has a value. if (!string.IsNullOrEmpty(value)) { var clientCliam = new ClientClaims { ClientId = source.ClientId, - //get the jsonproperty name instead of the property name if it is available. - Type = item.GetCustomAttribute()?.PropertyName?? item.Name, - Value = value + + // get the jsonproperty name instead of the property name if it is available. + Type = item.GetCustomAttribute()?.PropertyName ?? item.Name ?? string.Empty, + Value = value, }; clientClaimsCollection.Add(clientCliam); @@ -128,9 +134,6 @@ public static void MapDomainClientToEntityClientClaims(Domain.Entities.Client so var destinationType = destination.GetType(); destinationType.GetProperty("ClientClaims")?.SetValue(destination, clientClaimsCollection, null); - } - } - } diff --git a/Source/CdrAuthServer.Repository/Migrations/20221006235046_V001-InitialCreate.cs b/Source/CdrAuthServer.Repository/Migrations/20221006235046_V001-InitialCreate.cs index 25110b1..f7e6c08 100644 --- a/Source/CdrAuthServer.Repository/Migrations/20221006235046_V001-InitialCreate.cs +++ b/Source/CdrAuthServer.Repository/Migrations/20221006235046_V001-InitialCreate.cs @@ -6,7 +6,9 @@ namespace CdrAuthServer.Repository.Migrations { [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "S1192:Define a constant instead of using this literal 'nvarchar(450)' 5 times", Justification = "Auto-generated migration file.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "CA1861:Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array", Justification = "Auto-generated migration file.")] +#pragma warning disable SA1601 // Partial elements should be documented public partial class V001InitialCreate : Migration +#pragma warning restore SA1601 // Partial elements should be documented { protected override void Up(MigrationBuilder migrationBuilder) { @@ -17,7 +19,7 @@ protected override void Up(MigrationBuilder migrationBuilder) ClientId = table.Column(type: "nvarchar(450)", nullable: false), ClientIdIssuedAt = table.Column(type: "bigint", nullable: false), ClientName = table.Column(type: "nvarchar(max)", nullable: false), - ClientDescription = table.Column(type: "nvarchar(max)", nullable: true) + ClientDescription = table.Column(type: "nvarchar(max)", nullable: true), }, constraints: table => { @@ -36,7 +38,7 @@ protected override void Up(MigrationBuilder migrationBuilder) ExpiresAt = table.Column(type: "datetime2", nullable: false), UsedAt = table.Column(type: "datetime2", nullable: true), Scope = table.Column(type: "nvarchar(max)", nullable: true), - Data = table.Column(type: "nvarchar(max)", nullable: false) + Data = table.Column(type: "nvarchar(max)", nullable: false), }, constraints: table => { @@ -57,7 +59,7 @@ protected override void Up(MigrationBuilder migrationBuilder) LegalEntityStatus = table.Column(type: "nvarchar(max)", nullable: false), BrandId = table.Column(type: "nvarchar(max)", nullable: false), BrandName = table.Column(type: "nvarchar(max)", nullable: false), - BrandStatus = table.Column(type: "nvarchar(max)", nullable: false) + BrandStatus = table.Column(type: "nvarchar(max)", nullable: false), }, constraints: table => { @@ -69,7 +71,7 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: table => new { Id = table.Column(type: "nvarchar(450)", nullable: false), - BlackListed = table.Column(type: "bit", nullable: false) + BlackListed = table.Column(type: "bit", nullable: false), }, constraints: table => { @@ -84,7 +86,7 @@ protected override void Up(MigrationBuilder migrationBuilder) .Annotation("SqlServer:Identity", "1, 1"), ClientId = table.Column(type: "nvarchar(450)", nullable: false), Type = table.Column(type: "nvarchar(max)", nullable: false), - Value = table.Column(type: "nvarchar(max)", nullable: false) + Value = table.Column(type: "nvarchar(max)", nullable: false), }, constraints: table => { @@ -99,38 +101,38 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.InsertData( table: "Clients", - columns: new[] { "ClientId", "ClientDescription", "ClientIdIssuedAt", "ClientName" }, - values: new object[] { "11111111-1111-1111-1111-111111111111", null, 0L, "Software Product 1" }); + columns: ["ClientId", "ClientDescription", "ClientIdIssuedAt", "ClientName"], + values: ["11111111-1111-1111-1111-111111111111", null, 0L, "Software Product 1"]); migrationBuilder.InsertData( table: "Grants", - columns: new[] { "Key", "ClientId", "CreatedAt", "Data", "ExpiresAt", "GrantType", "Scope", "SubjectId", "UsedAt" }, + columns: ["Key", "ClientId", "CreatedAt", "Data", "ExpiresAt", "GrantType", "Scope", "SubjectId", "UsedAt"], values: new object[,] { { "12345678-1234-1234-1234-111122223333", "c6327f87-687a-4369-99a4-eaacd3bb8210", new DateTime(2022, 10, 6, 23, 50, 45, 993, DateTimeKind.Utc).AddTicks(9273), "{\"refresh_token\":\"valid-refresh-token\",\"account_id\":[\"123\",\"456\",\"789\"]}", new DateTime(2023, 10, 6, 23, 50, 45, 993, DateTimeKind.Utc).AddTicks(9277), "cdr_arrangement", "openid profile cdr:registration common:customer.basic:read common:customer.detail:read bank:accounts.basic:read bank:accounts.detail:read bank:transactions:read bank:payees:read bank:regular_payments:read energy:electricity.servicepoints.basic:read energy:electricity.servicepoints.detail:read energy:electricity.usage:read energy:electricity.der:read energy:accounts.basic:read energy:accounts.detail:read energy:accounts.paymentschedule:read energy:accounts.concessions:read energy:billing:read", "customer1", null }, { "expired-refresh-token", "c6327f87-687a-4369-99a4-eaacd3bb8210", new DateTime(2021, 10, 5, 23, 50, 45, 993, DateTimeKind.Utc).AddTicks(9729), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"5331d338-6ee8-4d14-b670-fbdb62d6837f\"}", new DateTime(2022, 10, 5, 23, 50, 45, 993, DateTimeKind.Utc).AddTicks(9730), "refresh_token", "openid profile cdr:registration common:customer.basic:read common:customer.detail:read bank:accounts.basic:read bank:accounts.detail:read bank:transactions:read bank:payees:read bank:regular_payments:read", "customer1", null }, - { "valid-refresh-token", "c6327f87-687a-4369-99a4-eaacd3bb8210", new DateTime(2022, 10, 6, 23, 50, 45, 993, DateTimeKind.Utc).AddTicks(9704), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"12345678-1234-1234-1234-111122223333\"}", new DateTime(2023, 10, 6, 23, 50, 45, 993, DateTimeKind.Utc).AddTicks(9705), "refresh_token", "openid profile cdr:registration common:customer.basic:read common:customer.detail:read bank:accounts.basic:read bank:accounts.detail:read bank:transactions:read bank:payees:read bank:regular_payments:read energy:electricity.servicepoints.basic:read energy:electricity.servicepoints.detail:read energy:electricity.usage:read energy:electricity.der:read energy:accounts.basic:read energy:accounts.detail:read energy:accounts.paymentschedule:read energy:accounts.concessions:read energy:billing:read", "customer1", null } + { "valid-refresh-token", "c6327f87-687a-4369-99a4-eaacd3bb8210", new DateTime(2022, 10, 6, 23, 50, 45, 993, DateTimeKind.Utc).AddTicks(9704), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"12345678-1234-1234-1234-111122223333\"}", new DateTime(2023, 10, 6, 23, 50, 45, 993, DateTimeKind.Utc).AddTicks(9705), "refresh_token", "openid profile cdr:registration common:customer.basic:read common:customer.detail:read bank:accounts.basic:read bank:accounts.detail:read bank:transactions:read bank:payees:read bank:regular_payments:read energy:electricity.servicepoints.basic:read energy:electricity.servicepoints.detail:read energy:electricity.usage:read energy:electricity.der:read energy:accounts.basic:read energy:accounts.detail:read energy:accounts.paymentschedule:read energy:accounts.concessions:read energy:billing:read", "customer1", null }, }); migrationBuilder.InsertData( table: "SoftwareProducts", - columns: new[] { "SoftwareProductId", "BrandId", "BrandName", "BrandStatus", "LegalEntityId", "LegalEntityName", "LegalEntityStatus", "LogoUri", "SoftwareProductDescription", "SoftwareProductName", "Status" }, + columns: ["SoftwareProductId", "BrandId", "BrandName", "BrandStatus", "LegalEntityId", "LegalEntityName", "LegalEntityStatus", "LogoUri", "SoftwareProductDescription", "SoftwareProductName", "Status"], values: new object[,] { { "22222222-2222-2222-2222-222222222222", "BBBBBBBB-2222-2222-2222-222222222222", "Active Data Recipient Brand Name", "ACTIVE", "LLLLLLLL-2222-2222-2222-222222222222", "Active Data Recipient Legal Entity Name", "ACTIVE", "https://cdrsandbox.gov.au/logo192.png", "Active Data Recipient Software Product", "Active Data Recipient Software Product", "ACTIVE" }, { "99999999-9999-9999-9999-999999999999", "BBBBBBBB-2222-2222-2222-222222222222", "Active Data Recipient Brand Name", "ACTIVE", "LLLLLLLL-2222-2222-2222-222222222222", "Active Data Recipient Legal Entity Name", "ACTIVE", "https://cdrsandbox.gov.au/logo192.png", "Removed Software Product", "Removed Software Product", "REMOVED" }, - { "c6327f87-687a-4369-99a4-eaacd3bb8210", "FFB1C8BA-279E-44D8-96F0-1BC34A6B436F", "Mock Data Recipient Brand Name", "ACTIVE", "18B75A76-5821-4C9E-B465-4709291CF0F4", "Mock Data Recipient Legal Entity Name", "ACTIVE", "https://cdrsandbox.gov.au/logo192.png", "Mock Data Recipient Software Product", "Mock Data Recipient Software Product", "ACTIVE" } + { "c6327f87-687a-4369-99a4-eaacd3bb8210", "FFB1C8BA-279E-44D8-96F0-1BC34A6B436F", "Mock Data Recipient Brand Name", "ACTIVE", "18B75A76-5821-4C9E-B465-4709291CF0F4", "Mock Data Recipient Legal Entity Name", "ACTIVE", "https://cdrsandbox.gov.au/logo192.png", "Mock Data Recipient Software Product", "Mock Data Recipient Software Product", "ACTIVE" }, }); migrationBuilder.InsertData( table: "ClientClaims", - columns: new[] { "Id", "ClientId", "Type", "Value" }, - values: new object[] { 1, "11111111-1111-1111-1111-111111111111", "SoftwareId", "22222222-2222-2222-2222-222222222222" }); + columns: ["Id", "ClientId", "Type", "Value"], + values: [1, "11111111-1111-1111-1111-111111111111", "SoftwareId", "22222222-2222-2222-2222-222222222222"]); migrationBuilder.InsertData( table: "ClientClaims", - columns: new[] { "Id", "ClientId", "Type", "Value" }, - values: new object[] { 2, "11111111-1111-1111-1111-111111111111", "JwksUri", "https://localhost:9001/jwks" }); + columns: ["Id", "ClientId", "Type", "Value"], + values: [2, "11111111-1111-1111-1111-111111111111", "JwksUri", "https://localhost:9001/jwks"]); migrationBuilder.CreateIndex( name: "IX_ClientClaims_ClientId", diff --git a/Source/CdrAuthServer.Repository/Migrations/20221205002235_V002-CreateLogEventsDrService.cs b/Source/CdrAuthServer.Repository/Migrations/20221205002235_V002-CreateLogEventsDrService.cs index 5fdeaad..af9aa2b 100644 --- a/Source/CdrAuthServer.Repository/Migrations/20221205002235_V002-CreateLogEventsDrService.cs +++ b/Source/CdrAuthServer.Repository/Migrations/20221205002235_V002-CreateLogEventsDrService.cs @@ -6,10 +6,12 @@ namespace CdrAuthServer.Repository.Migrations { [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "S1192:Define a constant instead of using this literal 'nvarchar(50)' 5 times", Justification = "Auto-generated migration file.")] +#pragma warning disable SA1601 // Partial elements should be documented public partial class V002CreateLogEventsDrService : Migration +#pragma warning restore SA1601 // Partial elements should be documented { protected override void Up(MigrationBuilder migrationBuilder) - { + { migrationBuilder.CreateTable( name: "LogEvents-DrService", columns: table => new @@ -24,7 +26,7 @@ protected override void Up(MigrationBuilder migrationBuilder) ProcessName = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), ThreadId = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), MethodName = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), - SourceContext = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true) + SourceContext = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), }, constraints: table => { diff --git a/Source/CdrAuthServer.Repository/Migrations/20230130011835_V003-RegisterClientSeeddata.cs b/Source/CdrAuthServer.Repository/Migrations/20230130011835_V003-RegisterClientSeeddata.cs index 3d75661..dfe4680 100644 --- a/Source/CdrAuthServer.Repository/Migrations/20230130011835_V003-RegisterClientSeeddata.cs +++ b/Source/CdrAuthServer.Repository/Migrations/20230130011835_V003-RegisterClientSeeddata.cs @@ -2,32 +2,30 @@ using Microsoft.Extensions.Configuration; using System.Configuration; -#nullable disable - namespace CdrAuthServer.Repository.Migrations { [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "S1192:Define a constant instead of using this literal 'cdr-register' 4 times.", Justification = "Auto-generated migration file.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "CA1861:Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array", Justification = "Auto-generated migration file.")] +#pragma warning disable SA1601 // Partial elements should be documented public partial class V003RegisterClientSeeddata : Migration - { +#pragma warning restore SA1601 // Partial elements should be documented + { public static string SSA_JWKS_URI => Configuration["CdrAuthServer:CdrRegister:SsaJwksUri"] ?? throw new ConfigurationErrorsException($"{nameof(SSA_JWKS_URI)} - configuration setting not found"); // SETTING UP CONFIGURATION BUILDER FOR REGISTER SSA JWKS URI - static private IConfigurationRoot? configuration; - static public IConfigurationRoot Configuration + private static IConfigurationRoot? configuration; + + public static IConfigurationRoot Configuration { get { - if (configuration == null) - { - configuration = new ConfigurationBuilder() + configuration ??= new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true) .AddEnvironmentVariables() .Build(); - } return configuration; } @@ -35,24 +33,24 @@ static public IConfigurationRoot Configuration protected override void Up(MigrationBuilder migrationBuilder) { - // Add Dynamic SSA_JWKS_URI for seed data + // Add Dynamic SSA_JWKS_URI for seed data migrationBuilder.InsertData( table: "Clients", - columns: new[] { "ClientId", "ClientDescription", "ClientIdIssuedAt", "ClientName" }, - values: new object[] { "cdr-register", null, 0L, "CDR Register" }); - + columns: ["ClientId", "ClientDescription", "ClientIdIssuedAt", "ClientName"], + values: ["cdr-register", null, 0L, "CDR Register"]); + migrationBuilder.InsertData( table: "ClientClaims", - columns: new[] { "ClientId", "Type", "Value" }, - values: new object[] { "cdr-register", "JwksUri", $"{SSA_JWKS_URI}" }); + columns: ["ClientId", "Type", "Value"], + values: ["cdr-register", "JwksUri", $"{SSA_JWKS_URI}"]); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DeleteData( table: "ClientClaims", - keyColumns: new[] { "ClientId", "Type" }, - keyValues: new object[] { "cdr-register", "JwksUri" }); + keyColumns: ["ClientId", "Type"], + keyValues: ["cdr-register", "JwksUri"]); migrationBuilder.DeleteData( table: "Clients", diff --git a/Source/CdrAuthServer.Repository/Migrations/20230201063622_V004-RegisterClientClaimsSeeddata.cs b/Source/CdrAuthServer.Repository/Migrations/20230201063622_V004-RegisterClientClaimsSeeddata.cs index 62b062f..0b9712e 100644 --- a/Source/CdrAuthServer.Repository/Migrations/20230201063622_V004-RegisterClientClaimsSeeddata.cs +++ b/Source/CdrAuthServer.Repository/Migrations/20230201063622_V004-RegisterClientClaimsSeeddata.cs @@ -6,22 +6,24 @@ namespace CdrAuthServer.Repository.Migrations { [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "CA1861:Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array", Justification = "Auto-generated migration file.")] +#pragma warning disable SA1601 // Partial elements should be documented public partial class V004RegisterClientClaimsSeeddata : Migration +#pragma warning restore SA1601 // Partial elements should be documented { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.InsertData( table: "ClientClaims", - columns: new[] { "ClientId", "Type", "Value" }, - values: new object[] { "cdr-register", "software_id", "cdr-register" }); + columns: ["ClientId", "Type", "Value"], + values: ["cdr-register", "software_id", "cdr-register"]); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DeleteData( table: "ClientClaims", - keyColumns: new[] { "ClientId", "Type" }, - keyValues: new object[] { "cdr-register", "software_id" }); + keyColumns: ["ClientId", "Type"], + keyValues: ["cdr-register", "software_id"]); } } } diff --git a/Source/CdrAuthServer.Repository/Migrations/20230209050525_V005-AddSoftwareProductForCDR-Register.cs b/Source/CdrAuthServer.Repository/Migrations/20230209050525_V005-AddSoftwareProductForCDR-Register.cs index 2228243..e249366 100644 --- a/Source/CdrAuthServer.Repository/Migrations/20230209050525_V005-AddSoftwareProductForCDR-Register.cs +++ b/Source/CdrAuthServer.Repository/Migrations/20230209050525_V005-AddSoftwareProductForCDR-Register.cs @@ -7,7 +7,9 @@ namespace CdrAuthServer.Repository.Migrations { [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "S1192:Define a constant instead of using this literal 'Grants' 6 times.", Justification = "Auto-generated migration file.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "CA1861:Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array", Justification = "Auto-generated migration file.")] +#pragma warning disable SA1601 // Partial elements should be documented public partial class V005AddSoftwareProductForCDRRegister : Migration +#pragma warning restore SA1601 // Partial elements should be documented { protected override void Up(MigrationBuilder migrationBuilder) { @@ -15,27 +17,27 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "Grants", keyColumn: "Key", keyValue: "12345678-1234-1234-1234-111122223333", - columns: new[] { "CreatedAt", "ExpiresAt" }, - values: new object[] { new DateTime(2023, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(474), new DateTime(2024, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(476) }); + columns: ["CreatedAt", "ExpiresAt"], + values: [new DateTime(2023, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(474), new DateTime(2024, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(476)]); migrationBuilder.UpdateData( table: "Grants", keyColumn: "Key", keyValue: "expired-refresh-token", - columns: new[] { "CreatedAt", "Data", "ExpiresAt" }, - values: new object[] { new DateTime(2022, 2, 8, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(901), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"7e265e9f-4af6-4188-b5d5-2b3db35d507c\"}", new DateTime(2023, 2, 8, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(902) }); + columns: ["CreatedAt", "Data", "ExpiresAt"], + values: [new DateTime(2022, 2, 8, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(901), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"7e265e9f-4af6-4188-b5d5-2b3db35d507c\"}", new DateTime(2023, 2, 8, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(902)]); migrationBuilder.UpdateData( table: "Grants", keyColumn: "Key", keyValue: "valid-refresh-token", - columns: new[] { "CreatedAt", "ExpiresAt" }, - values: new object[] { new DateTime(2023, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(870), new DateTime(2024, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(870) }); + columns: ["CreatedAt", "ExpiresAt"], + values: [new DateTime(2023, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(870), new DateTime(2024, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(870)]); migrationBuilder.InsertData( table: "SoftwareProducts", - columns: new[] { "SoftwareProductId", "BrandId", "BrandName", "BrandStatus", "LegalEntityId", "LegalEntityName", "LegalEntityStatus", "LogoUri", "SoftwareProductDescription", "SoftwareProductName", "Status" }, - values: new object[] { "cdr-register", "cdr-register", "cdr-register", "ACTIVE", "cdr-register", "cdr-register", "ACTIVE", "https://cdrsandbox.gov.au/logo192.png", "Mock Register", "cdr-register", "ACITVE" }); + columns: ["SoftwareProductId", "BrandId", "BrandName", "BrandStatus", "LegalEntityId", "LegalEntityName", "LegalEntityStatus", "LogoUri", "SoftwareProductDescription", "SoftwareProductName", "Status"], + values: ["cdr-register", "cdr-register", "cdr-register", "ACTIVE", "cdr-register", "cdr-register", "ACTIVE", "https://cdrsandbox.gov.au/logo192.png", "Mock Register", "cdr-register", "ACITVE"]); } protected override void Down(MigrationBuilder migrationBuilder) @@ -49,22 +51,22 @@ protected override void Down(MigrationBuilder migrationBuilder) table: "Grants", keyColumn: "Key", keyValue: "12345678-1234-1234-1234-111122223333", - columns: new[] { "CreatedAt", "ExpiresAt" }, - values: new object[] { new DateTime(2023, 2, 1, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7040), new DateTime(2024, 2, 1, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7042) }); + columns: ["CreatedAt", "ExpiresAt"], + values: [new DateTime(2023, 2, 1, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7040), new DateTime(2024, 2, 1, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7042)]); migrationBuilder.UpdateData( table: "Grants", keyColumn: "Key", keyValue: "expired-refresh-token", - columns: new[] { "CreatedAt", "Data", "ExpiresAt" }, - values: new object[] { new DateTime(2022, 1, 31, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7411), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"183c4c56-a7bd-4316-8a4d-cbc40dd9ce53\"}", new DateTime(2023, 1, 31, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7411) }); + columns: ["CreatedAt", "Data", "ExpiresAt"], + values: [new DateTime(2022, 1, 31, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7411), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"183c4c56-a7bd-4316-8a4d-cbc40dd9ce53\"}", new DateTime(2023, 1, 31, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7411)]); migrationBuilder.UpdateData( table: "Grants", keyColumn: "Key", keyValue: "valid-refresh-token", - columns: new[] { "CreatedAt", "ExpiresAt" }, - values: new object[] { new DateTime(2023, 2, 1, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7382), new DateTime(2024, 2, 1, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7382) }); + columns: ["CreatedAt", "ExpiresAt"], + values: [new DateTime(2023, 2, 1, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7382), new DateTime(2024, 2, 1, 6, 36, 21, 885, DateTimeKind.Utc).AddTicks(7382)]); } } } diff --git a/Source/CdrAuthServer.Repository/Migrations/20230212220828_V006-Seed-SoftwareProduct-Status.cs b/Source/CdrAuthServer.Repository/Migrations/20230212220828_V006-Seed-SoftwareProduct-Status.cs index 6e91a78..0d59e1c 100644 --- a/Source/CdrAuthServer.Repository/Migrations/20230212220828_V006-Seed-SoftwareProduct-Status.cs +++ b/Source/CdrAuthServer.Repository/Migrations/20230212220828_V006-Seed-SoftwareProduct-Status.cs @@ -7,7 +7,9 @@ namespace CdrAuthServer.Repository.Migrations { [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "S1192:Define a constant instead of using this literal 'Grants' 6 times.", Justification = "Auto-generated migration file.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("SonarAnalyzer", "CA1861:Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array", Justification = "Auto-generated migration file.")] +#pragma warning disable SA1601 // Partial elements should be documented public partial class V006SeedSoftwareProductStatus : Migration +#pragma warning restore SA1601 // Partial elements should be documented { protected override void Up(MigrationBuilder migrationBuilder) { @@ -15,22 +17,22 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "Grants", keyColumn: "Key", keyValue: "12345678-1234-1234-1234-111122223333", - columns: new[] { "CreatedAt", "ExpiresAt" }, - values: new object[] { new DateTime(2023, 2, 12, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8060), new DateTime(2024, 2, 12, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8061) }); + columns: ["CreatedAt", "ExpiresAt"], + values: [new DateTime(2023, 2, 12, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8060), new DateTime(2024, 2, 12, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8061)]); migrationBuilder.UpdateData( table: "Grants", keyColumn: "Key", keyValue: "expired-refresh-token", - columns: new[] { "CreatedAt", "Data", "ExpiresAt" }, - values: new object[] { new DateTime(2022, 2, 11, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8643), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"84290e1a-919d-4ed9-8e88-c47c283c51ea\"}", new DateTime(2023, 2, 11, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8643) }); + columns: ["CreatedAt", "Data", "ExpiresAt"], + values: [new DateTime(2022, 2, 11, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8643), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"84290e1a-919d-4ed9-8e88-c47c283c51ea\"}", new DateTime(2023, 2, 11, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8643)]); migrationBuilder.UpdateData( table: "Grants", keyColumn: "Key", keyValue: "valid-refresh-token", - columns: new[] { "CreatedAt", "ExpiresAt" }, - values: new object[] { new DateTime(2023, 2, 12, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8613), new DateTime(2024, 2, 12, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8614) }); + columns: ["CreatedAt", "ExpiresAt"], + values: [new DateTime(2023, 2, 12, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8613), new DateTime(2024, 2, 12, 22, 8, 27, 831, DateTimeKind.Utc).AddTicks(8614)]); migrationBuilder.UpdateData( table: "SoftwareProducts", @@ -46,22 +48,22 @@ protected override void Down(MigrationBuilder migrationBuilder) table: "Grants", keyColumn: "Key", keyValue: "12345678-1234-1234-1234-111122223333", - columns: new[] { "CreatedAt", "ExpiresAt" }, - values: new object[] { new DateTime(2023, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(474), new DateTime(2024, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(476) }); + columns: ["CreatedAt", "ExpiresAt"], + values: [new DateTime(2023, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(474), new DateTime(2024, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(476)]); migrationBuilder.UpdateData( table: "Grants", keyColumn: "Key", keyValue: "expired-refresh-token", - columns: new[] { "CreatedAt", "Data", "ExpiresAt" }, - values: new object[] { new DateTime(2022, 2, 8, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(901), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"7e265e9f-4af6-4188-b5d5-2b3db35d507c\"}", new DateTime(2023, 2, 8, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(902) }); + columns: ["CreatedAt", "Data", "ExpiresAt"], + values: [new DateTime(2022, 2, 8, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(901), "{\"response_type\":\"code id_token\",\"CdrArrangementId\":\"7e265e9f-4af6-4188-b5d5-2b3db35d507c\"}", new DateTime(2023, 2, 8, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(902)]); migrationBuilder.UpdateData( table: "Grants", keyColumn: "Key", keyValue: "valid-refresh-token", - columns: new[] { "CreatedAt", "ExpiresAt" }, - values: new object[] { new DateTime(2023, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(870), new DateTime(2024, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(870) }); + columns: ["CreatedAt", "ExpiresAt"], + values: [new DateTime(2023, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(870), new DateTime(2024, 2, 9, 5, 5, 24, 902, DateTimeKind.Utc).AddTicks(870)]); migrationBuilder.UpdateData( table: "SoftwareProducts", diff --git a/Source/CdrAuthServer.Repository/TokenRepository.cs b/Source/CdrAuthServer.Repository/TokenRepository.cs index c65efb0..fbd53bd 100644 --- a/Source/CdrAuthServer.Repository/TokenRepository.cs +++ b/Source/CdrAuthServer.Repository/TokenRepository.cs @@ -19,7 +19,7 @@ await cdrAuthServervDatabaseContext.Tokens.AddAsync( new Entities.Token { Id = id, - BlackListed = true + BlackListed = true, }); await cdrAuthServervDatabaseContext.SaveChangesAsync(); @@ -32,4 +32,4 @@ public async Task IsTokenBlacklisted(string id) return token?.BlackListed ?? false; } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.TLS.Gateway/CdrAuthServer.TLS.Gateway.csproj b/Source/CdrAuthServer.TLS.Gateway/CdrAuthServer.TLS.Gateway.csproj index d1e291a..7413281 100644 --- a/Source/CdrAuthServer.TLS.Gateway/CdrAuthServer.TLS.Gateway.csproj +++ b/Source/CdrAuthServer.TLS.Gateway/CdrAuthServer.TLS.Gateway.csproj @@ -6,7 +6,8 @@ $(Version) $(Version) enable - enable + enable + True @@ -26,11 +27,19 @@ - + - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/CdrAuthServer.TLS.Gateway/Program.cs b/Source/CdrAuthServer.TLS.Gateway/Program.cs index a7d5598..7eea5e4 100644 --- a/Source/CdrAuthServer.TLS.Gateway/Program.cs +++ b/Source/CdrAuthServer.TLS.Gateway/Program.cs @@ -1,20 +1,20 @@ -using CdrAuthServer.Infrastructure; +using CdrAuthServer.Infrastructure; using CdrAuthServer.Infrastructure.Certificates; using CdrAuthServer.Infrastructure.Extensions; using Microsoft.AspNetCore.Diagnostics; using Ocelot.DependencyInjection; using Ocelot.Middleware; -using static System.Net.Mime.MediaTypeNames; using Serilog; +using static System.Net.Mime.MediaTypeNames; using ILogger = Serilog.ILogger; var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile("gateway-config.json", false, true); builder.Configuration.AddEnvironmentVariables(); builder.Services.AddScoped(); -builder.Services.ConfigureWebServer( - builder.Configuration, - "Certificates:TlsServerCertificate", +await builder.Services.ConfigureWebServer( + builder.Configuration, + "Certificates:TlsServerCertificate", httpsPort: builder.Configuration.GetValue("CdrAuthServer:tlsGateway:httpsPort", 8081), requireClientCertificate: false); builder.Services.AddOcelot(); @@ -40,7 +40,7 @@ context.Response.StatusCode = StatusCodes.Status502BadGateway; context.Response.ContentType = Text.Plain; - logger?.Error("Caught exception with error: {ex}", ex); + logger?.Error(ex, "Caught exception with error"); await context.Response.WriteAsync($"An error occurred handling the request: {ex?.Message}"); }); }); @@ -54,8 +54,8 @@ // Send through the original host name to the backend service. httpContext.Request.Headers[HttpHeaders.ForwardedHost] = httpContext.Request.Host.ToString(); await next.Invoke(); - } + }, }; app.UseOcelot(pipelineConfiguration).Wait(); -app.Run(); +await app.RunAsync(); diff --git a/Source/CdrAuthServer.UI/package-lock.json b/Source/CdrAuthServer.UI/package-lock.json index d49e8d7..3eb26d4 100644 --- a/Source/CdrAuthServer.UI/package-lock.json +++ b/Source/CdrAuthServer.UI/package-lock.json @@ -5414,9 +5414,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/express": { @@ -6118,148 +6118,148 @@ "dev": true }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -6296,9 +6296,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -6329,15 +6329,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -7228,9 +7219,9 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -7241,7 +7232,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -7404,9 +7395,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -7423,10 +7414,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -7506,14 +7497,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7580,9 +7576,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001576", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", - "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==", + "version": "1.0.30001677", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", + "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", "funding": [ { "type": "opencollective", @@ -7977,9 +7973,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "engines": { "node": ">= 0.6" @@ -8720,17 +8716,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -9098,15 +9097,15 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.628", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.628.tgz", - "integrity": "sha512-2k7t5PHvLsufpP6Zwk0nof62yLOsCf032wZx7/q0mv8gwlXjhcxI3lz6f0jBr0GrnWKcm3burXzI3t5IrcdUxw==", + "version": "1.5.52", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.52.tgz", + "integrity": "sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ==", "dev": true }, "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", + "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -9150,18 +9149,18 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "engines": { "node": ">= 0.8" } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -9257,6 +9256,27 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-iterator-helpers": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", @@ -9331,9 +9351,9 @@ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -10156,37 +10176,37 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -10372,13 +10392,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10863,16 +10883,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11110,12 +11134,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11447,9 +11471,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "dependencies": { "@types/http-proxy": "^1.17.8", @@ -16756,10 +16780,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -16786,12 +16813,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -17097,9 +17124,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "node_modules/normalize-path": { @@ -17586,9 +17613,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true }, "node_modules/path-type": { @@ -17623,9 +17650,9 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -19278,12 +19305,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -20297,9 +20324,9 @@ } }, "node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -20570,9 +20597,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "dependencies": { "debug": "2.6.9", @@ -20608,6 +20635,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -20702,30 +20738,32 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -20829,14 +20867,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -22376,9 +22418,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -22395,8 +22437,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -22552,9 +22594,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -22583,34 +22625,33 @@ } }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { diff --git a/Source/CdrAuthServer.UnitTests/BaseTest.cs b/Source/CdrAuthServer.UnitTests/BaseTest.cs index 8203eaa..a6a6507 100644 --- a/Source/CdrAuthServer.UnitTests/BaseTest.cs +++ b/Source/CdrAuthServer.UnitTests/BaseTest.cs @@ -8,22 +8,22 @@ namespace CdrAuthServer.UnitTests { - abstract public class BaseTest + public abstract class BaseTest { public const string SOFTWARE_PRODUCT_ID_KEY = "CDRAuthServer:softwareProductId"; - public const string BRAND_ID_KEY = "CDRAuthServer:brandId"; + public const string BRAND_ID_KEY = "CDRAuthServer:brandId"; public const string SOFTWAREPRODUCT_ID = "c6327f87-687a-4369-99a4-eaacd3bb8210"; public const string JWT_CERTIFICATE_FILENAME = "Certificates/MDR/jwks.pfx"; public const string JWT_CERTIFICATE_PASSWORD = "#M0ckDataRecipient#"; - public const string DH_MTLS_GATEWAY_URL = "https://localhost:8082"; + public const string DH_MTLS_GATEWAY_URL = "https://localhost:8082"; public const string SOFTWARE_STATEMENT = @"eyJhbGciOiJQUzI1NiIsImtpZCI6IkY0RUEyOTlDNjA3OTQ3RTQ1OUFDNDdFNjlGNzI4OUYxNzRCNUI0REYiLCJ0eXAiOiJKV1QifQ.ewogICJsZWdhbF9lbnRpdHlfaWQiOiAiMThiNzVhNzYtNTgyMS00YzllLWI0NjUtNDcwOTI5MWNmMGY0IiwKICAibGVnYWxfZW50aXR5X25hbWUiOiAiU2FuZGJveCBEYXRhIFJlY2lwaWVudCIsCiAgImlzcyI6ICJjZHItcmVnaXN0ZXIiLAogICJpYXQiOiAxNjY2MDY1NTY4LAogICJleHAiOiAxNjY2MDY2MTY4LAogICJqdGkiOiAiODVjMTBmMTc5YWIzNGVmYzgxMWY1MTEwODQzNzIyYWIiLAogICJvcmdfaWQiOiAiZmZiMWM4YmEtMjc5ZS00NGQ4LTk2ZjAtMWJjMzRhNmI0MzZmIiwKICAib3JnX25hbWUiOiAiU01EUiIsCiAgImNsaWVudF9uYW1lIjogIlNhbmRib3ggRGF0YSBSZWNpcGllbnQgU29mdHdhcmUgUHJvZHVjdCIsCiAgImNsaWVudF9kZXNjcmlwdGlvbiI6ICJBIHByb2R1Y3QgdG8gaW50ZXJhY3Qgd2l0aCB0aGUgZWNvc3lzdGVtIiwKICAiY2xpZW50X3VyaSI6ICJodHRwczovL2RyLmRldi5jZHJzYW5kYm94Lmdvdi5hdSIsCiAgInJlZGlyZWN0X3VyaXMiOiBbCiAgICAiaHR0cHM6Ly9kci5kZXYuY2Ryc2FuZGJveC5nb3YuYXUvY29uc2VudC9jYWxsYmFjayIKICBdLAogICJsb2dvX3VyaSI6ICJodHRwczovL2NkcnNhbmRib3guZ292LmF1L2xvZ28xOTIucG5nIiwKICAidG9zX3VyaSI6ICJodHRwczovL2RyLmRldi5jZHJzYW5kYm94Lmdvdi5hdS90b3MiLAogICJwb2xpY3lfdXJpIjogImh0dHBzOi8vZHIuZGV2LmNkcnNhbmRib3guZ292LmF1L3BvbGljeSIsCiAgImp3a3NfdXJpIjogImh0dHBzOi8vZHIuZGV2LmNkcnNhbmRib3guZ292LmF1L2p3a3MiLAogICJyZXZvY2F0aW9uX3VyaSI6ICJodHRwczovL2RyLmRldi5jZHJzYW5kYm94Lmdvdi5hdS9yZXZvY2F0aW9uIiwKICAicmVjaXBpZW50X2Jhc2VfdXJpIjogImh0dHBzOi8vZHIuZGV2LmNkcnNhbmRib3guZ292LmF1IiwKICAic29mdHdhcmVfaWQiOiAiYzYzMjdmODctNjg3YS00MzY5LTk5YTQtZWFhY2QzYmI4MjEwIiwKICAic29mdHdhcmVfcm9sZXMiOiAiZGF0YS1yZWNpcGllbnQtc29mdHdhcmUtcHJvZHVjdCIsCiAgInNjb3BlIjogIm9wZW5pZCBwcm9maWxlIGNvbW1vbjpjdXN0b21lci5iYXNpYzpyZWFkIGNvbW1vbjpjdXN0b21lci5kZXRhaWw6cmVhZCBiYW5rOmFjY291bnRzLmJhc2ljOnJlYWQgYmFuazphY2NvdW50cy5kZXRhaWw6cmVhZCBiYW5rOnRyYW5zYWN0aW9uczpyZWFkIGJhbms6cmVndWxhcl9wYXltZW50czpyZWFkIGJhbms6cGF5ZWVzOnJlYWQgZW5lcmd5OmFjY291bnRzLmJhc2ljOnJlYWQgZW5lcmd5OmFjY291bnRzLmRldGFpbDpyZWFkIGVuZXJneTphY2NvdW50cy5jb25jZXNzaW9uczpyZWFkIGVuZXJneTphY2NvdW50cy5wYXltZW50c2NoZWR1bGU6cmVhZCBlbmVyZ3k6YmlsbGluZzpyZWFkIGVuZXJneTplbGVjdHJpY2l0eS5zZXJ2aWNlcG9pbnRzLmJhc2ljOnJlYWQgZW5lcmd5OmVsZWN0cmljaXR5LnNlcnZpY2Vwb2ludHMuZGV0YWlsOnJlYWQgZW5lcmd5OmVsZWN0cmljaXR5LmRlcjpyZWFkIGVuZXJneTplbGVjdHJpY2l0eS51c2FnZTpyZWFkIGNkcjpyZWdpc3RyYXRpb24iCn0.DJmLhaM8I8pINgqIHYbpsQ2a63upn_OcliFdWN0iIkJSBP1pmMA-EjZekFojTjlDcCN1boIwOwzYNOjKGRl2Sis10ViiDyJIWN27sxWcLud_y7sm7YMUNBX6aX-p5IvuZ0J3ZmJbjqY9RhcO_0CMIbrpmbbsAqPF3r4XqDwN__XFRBgp2NNL8VRpKEBNMbGl214qwUe_aKTJ3PRPjOTS72RX6OX8dLSxRB--PBHavBUpBnCsdSA2TzjtHLZxbq6VTLswhVExwN8WTrgw8xg1CHRFSxh4L4IJAwYYn0Tn2HmlVGFnaNGjaocxO_EXZrloiF45956CVV1hqmi0EgWWfw"; public const string JWKS_URI = "https://localhost:7000/cdr-register/v1/jwks"; - - /// + + /// /// - /// Add SoftareStatement - /// + /// Add SoftareStatement. + /// jwtToken. public static string GetJwtToken(bool requireSS = false) { string ISSUER = SOFTWAREPRODUCT_ID.ToLower(); @@ -33,7 +33,7 @@ public static string GetJwtToken(bool requireSS = false) var additionalClaims = new List { new Claim("sub", ISSUER), - new Claim("iat", new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer) + new Claim("iat", new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer), }; if (requireSS) @@ -61,7 +61,8 @@ public static string GetJwtToken(bool requireSS = false) return jwtSecturtyToken; } - public static JwtSecurityToken GetJwt(string client_id = "", string RedirectUriValue = "", bool isNbf = false) + public static JwtSecurityToken GetJwt(string client_id = "", string RedirectUriValue = "", bool isNbf = false, + Claim[]? moreClaimsToAdd = null) { string ISSUER = SOFTWAREPRODUCT_ID.ToLower(); @@ -70,25 +71,29 @@ public static JwtSecurityToken GetJwt(string client_id = "", string RedirectUriV var additionalClaims = new List { new Claim("sub", ISSUER), - new Claim("iat", new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer) + new Claim("iat", new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer), }; - if (!String.IsNullOrEmpty(client_id)) + if (!string.IsNullOrEmpty(client_id)) { additionalClaims.Add(new Claim("client_id", client_id)); } - if (!String.IsNullOrEmpty(RedirectUriValue)) + if (!string.IsNullOrEmpty(RedirectUriValue)) { additionalClaims.Add(new Claim(ClaimNames.RedirectUri, RedirectUriValue)); } - + if (isNbf) { var nbfClaim = new Claim(ClaimNames.NotBefore, new DateTimeOffset(DateTime.Now.AddMinutes(5)).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer); additionalClaims.Add(nbfClaim); } + if (moreClaimsToAdd != null) + { + additionalClaims.AddRange(moreClaimsToAdd); + } var expires = now.AddMinutes(10); string? aud = null; @@ -103,8 +108,8 @@ public static JwtSecurityToken GetJwt(string client_id = "", string RedirectUriV additionalClaims, expires: expires, signingCredentials: x509SigningCredentials); - + return jwt; - } + } } } diff --git a/Source/CdrAuthServer.UnitTests/CdrAuthServer.UnitTests.csproj b/Source/CdrAuthServer.UnitTests/CdrAuthServer.UnitTests.csproj index 804d41e..a3d0327 100644 --- a/Source/CdrAuthServer.UnitTests/CdrAuthServer.UnitTests.csproj +++ b/Source/CdrAuthServer.UnitTests/CdrAuthServer.UnitTests.csproj @@ -6,7 +6,8 @@ $(Version) $(Version) enable - false + false + True @@ -37,6 +38,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/CdrAuthServer.UnitTests/CertificateHelper.cs b/Source/CdrAuthServer.UnitTests/CertificateHelper.cs new file mode 100644 index 0000000..4466b99 --- /dev/null +++ b/Source/CdrAuthServer.UnitTests/CertificateHelper.cs @@ -0,0 +1,27 @@ +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace CdrAuthServer.UnitTests +{ + /// + /// Helper functionality for creating certificates for testing. + /// + public static class CertificateHelper + { + /// + /// Creates the for signing. + /// + /// The common name for the subject. + /// A self-signed certificate for testing. + public static X509Certificate2 CreateSigning(string cn = "signing") + { + using var rsa = RSA.Create(); + + var req = new CertificateRequest($"cn={cn}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); + var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddMinutes(1)); + + return cert; + } + } +} diff --git a/Source/CdrAuthServer.UnitTests/Controllers/AdminControllerTests.cs b/Source/CdrAuthServer.UnitTests/Controllers/AdminControllerTests.cs new file mode 100644 index 0000000..93c6adc --- /dev/null +++ b/Source/CdrAuthServer.UnitTests/Controllers/AdminControllerTests.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using CdrAuthServer.Controllers; +using CdrAuthServer.Models; +using CdrAuthServer.Models.Json; +using CdrAuthServer.Models.Register; +using CdrAuthServer.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using static CdrAuthServer.Domain.Constants; + +namespace CdrAuthServer.UnitTests.Controllers +{ + internal class AdminControllerTests + { + private readonly Mock> _logger = new(); + private readonly Mock _registerClientService = new(); + private readonly Mock _clientService = new(); + private readonly Mock _cdrService = new(); + + [Test] + public async Task RefreshDataRecipientsReturnsUnauthorizedForInvalidClient() + { + var context = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal([new ClaimsIdentity([new Claim(ClaimNames.ClientId, "invalid_client")])]), + }, + }; + + var controller = new AdminController(_cdrService.Object, _clientService.Object, _logger.Object, _registerClientService.Object) + { + ControllerContext = context, + }; + + var request = new DataRecipientRequest { Data = new Data { Action = "REFRESH" } }; + + var result = await controller.RefreshDataRecipients(request, default); + + Assert.IsInstanceOf(result); + Assert.NotNull(controller.HttpContext.Response.Headers.WWWAuthenticate); + } + + [Test] + public async Task RefreshDataRecipientsReturnsInternalErrorWhenRefreshFails() + { + var clientId = "valid"; + var context = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal([new ClaimsIdentity([new Claim(ClaimNames.ClientId, clientId)])]), + }, + }; + + _clientService.Setup(x => x.Get(clientId)).ReturnsAsync(new Client()); + + var controller = new AdminController(_cdrService.Object, _clientService.Object, _logger.Object, _registerClientService.Object) + { + ControllerContext = context, + }; + + var request = new DataRecipientRequest { Data = new Data { Action = "REFRESH" } }; + + var result = await controller.RefreshDataRecipients(request, default); + _registerClientService.Verify(x => x.GetDataRecipients(default), Times.Once); + + ResultHelper.AssertInstanceOf(result, out var converted); + Assert.AreEqual(StatusCodes.Status500InternalServerError, converted.StatusCode); + Assert.AreEqual("Data recipient data could not be refreshed.", converted.Value); + } + + [Test] + public async Task RefreshDataRecipientsReturnsInternalErrorWhenRequestActionUnsupported() + { + var clientId = "valid"; + var context = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal([new ClaimsIdentity([new Claim(ClaimNames.ClientId, clientId)])]), + }, + }; + + _clientService.Setup(x => x.Get(clientId)).ReturnsAsync(new Client()); + + var controller = new AdminController(_cdrService.Object, _clientService.Object, _logger.Object, _registerClientService.Object) + { + ControllerContext = context, + }; + + var request = new DataRecipientRequest { Data = new Data { Action = "WRONG'N" } }; + + var result = await controller.RefreshDataRecipients(request, default); + ResultHelper.AssertInstanceOf(result, out var converted); + Assert.AreEqual(StatusCodes.Status500InternalServerError, converted.StatusCode); + Assert.AreEqual("Data recipient data could not be refreshed.", converted.Value); + } + + [Test] + public async Task RefreshDataRecipientsReturnsOkWhenRefreshSucceeds() + { + var clientId = "valid"; + var self = "http://inception/cdr-register/v1/all/data-recipients"; + var context = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal([new ClaimsIdentity([new Claim(ClaimNames.ClientId, clientId)])]), + }, + }; + + _clientService.Setup(x => x.Get(clientId)).ReturnsAsync(new Client()); + _registerClientService.Setup(x => x.GetDataRecipients(default)).ReturnsAsync(new RegisterResponse { Data = GenerateEntities(3), Links = new Links { Self = new Uri(self) } }); + + var controller = new AdminController(_cdrService.Object, _clientService.Object, _logger.Object, _registerClientService.Object) + { + ControllerContext = context, + }; + + var request = new DataRecipientRequest { Data = new Data { Action = "REFRESH" } }; + + var result = await controller.RefreshDataRecipients(request, default); + _cdrService.Verify(x => x.PurgeDataRecipients(), Times.Once); + _cdrService.Verify(x => x.InsertDataRecipients(It.IsAny>()), Times.Once); + ResultHelper.AssertInstanceOf(result, out var converted); + Assert.AreEqual(StatusCodes.Status200OK, converted.StatusCode); + Assert.AreEqual($"Data recipient records refreshed from {self}.", converted.Value); + } + + [Test] + public async Task RefreshDataRecipientsReturnsInternalErrorWhenThereAreNoSoftwareProducts() + { + var clientId = "valid"; + var self = "http://inception/cdr-register/v1/all/data-recipients"; + var context = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal([new ClaimsIdentity([new Claim(ClaimNames.ClientId, clientId)])]), + }, + }; + + _clientService.Setup(x => x.Get(clientId)).ReturnsAsync(new Client()); + _registerClientService.Setup(x => x.GetDataRecipients(default)).ReturnsAsync(new RegisterResponse { Data = GenerateEntities(0), Links = new Links { Self = new Uri(self) } }); + + var controller = new AdminController(_cdrService.Object, _clientService.Object, _logger.Object, _registerClientService.Object) + { + ControllerContext = context, + }; + + var request = new DataRecipientRequest { Data = new Data { Action = "REFRESH" } }; + + var result = await controller.RefreshDataRecipients(request, default); + _cdrService.Verify(x => x.PurgeDataRecipients(), Times.Once); + _cdrService.Verify(x => x.InsertDataRecipients(It.IsAny>()), Times.Never); + + ResultHelper.AssertInstanceOf(result, out var converted); + Assert.AreEqual(StatusCodes.Status500InternalServerError, converted.StatusCode); + Assert.AreEqual("Data recipient data could not be refreshed.", converted.Value); + } + + [TearDown] + public void Reset() + { + _cdrService.Reset(); + _clientService.Reset(); + _registerClientService.Reset(); + } + + private IEnumerable GenerateEntities(int count) + { + for (var i = 1; i <= count; i++) + { + var legalEntityId = Guid.NewGuid().ToString(); + yield return new LegalEntity + { + LegalEntityId = legalEntityId, + LegalEntityName = $"Test Entity {i}", + Status = "Active", + DataRecipientBrands = [new DataRecipientBrand + { + BrandName = $"Brand {i}", + DataRecipientBrandId = i.ToString(), + Status = "Active", + SoftwareProducts = [new SoftwareProduct + { + LegalEntityId = legalEntityId, + LegalEntityName = $"Test Entity {i}", + LegalEntityStatus = "Active", + SoftwareProductId = Guid.NewGuid().ToString(), + SoftwareProductDescription = $"Description {i}", + Status = "Active", + LogoUri = "http://inception/mylogo.png", + } + ], + } + ], + }; + } + } + } +} diff --git a/Source/CdrAuthServer.UnitTests/Controllers/UtilityControllerTests.cs b/Source/CdrAuthServer.UnitTests/Controllers/UtilityControllerTests.cs new file mode 100644 index 0000000..cac0014 --- /dev/null +++ b/Source/CdrAuthServer.UnitTests/Controllers/UtilityControllerTests.cs @@ -0,0 +1,220 @@ +using CdrAuthServer.Controllers; +using CdrAuthServer.Domain.Models; +using CdrAuthServer.Models; +using CdrAuthServer.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using static CdrAuthServer.Domain.Constants; + +namespace CdrAuthServer.UnitTests.Controllers +{ + /// + /// Tests for . + /// + internal class UtilityControllerTests + { + private const string _errorResponse = """ + { + "errors": [ + { + "code": "urn:au-cds:error:cds-all:Authorisation/InvalidArrangement", + "title": "Unable to process revocation", + "detail": "The arrangement is invalid", + } + ] + } + """; + + private readonly Mock> _logger = new(); + private readonly Mock _grantService = new(); + private readonly Mock _clientService = new(); + private readonly Mock _consentRevocationService = new(); + + [Test] + public async Task RemoveArrangementReturnsBadRequestForMissingCdrArrangementId() + { + // Arrange + var cdrArrangementId = string.Empty; + var controller = new UtilityController(_logger.Object, _grantService.Object, _clientService.Object, _consentRevocationService.Object); + + // Act + var result = await controller.RemoveArrangementAndTriggerDataRecipientArrangementRevocation(cdrArrangementId, default); + + // Assert + ResultHelper.AssertInstanceOf(result, out var badRequest); + ResultHelper.AssertErrorExpectation(badRequest, ErrorCodes.Cds.InvalidField, "Invalid Field", "cdrArrangementId"); + } + + [Test] + public async Task RemoveArrangementReturnsBadRequestForCdrArrangementIdNotFound() + { + // Arrange + var cdrArrangementId = Guid.NewGuid().ToString(); + _grantService.Setup(x => x.Get(GrantTypes.CdrArrangement, cdrArrangementId, null)).ReturnsAsync((Grant?)null); + var controller = new UtilityController(_logger.Object, _grantService.Object, _clientService.Object, _consentRevocationService.Object); + + // Act + var result = await controller.RemoveArrangementAndTriggerDataRecipientArrangementRevocation(cdrArrangementId, default); + + // Assert + ResultHelper.AssertInstanceOf(result, out var badRequest); + Assert.AreEqual((int)HttpStatusCode.BadRequest, badRequest.StatusCode); + ResultHelper.AssertErrorExpectation(badRequest, ErrorCodes.Cds.InvalidField, "Invalid Field", "cdrArrangementId"); + } + + [Test] + public async Task RemoveArrangementReturnsInternalServerErrorForGrantNotFound() + { + // Arrange + var cdrArrangementId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + + _grantService.Setup(x => x.Get(GrantTypes.CdrArrangement, cdrArrangementId, null)).ReturnsAsync(new Grant { ClientId = clientId }); + _clientService.Setup(x => x.Get(clientId)).ReturnsAsync((Client?)null); + var controller = new UtilityController(_logger.Object, _grantService.Object, _clientService.Object, _consentRevocationService.Object); + + // Act + var result = await controller.RemoveArrangementAndTriggerDataRecipientArrangementRevocation(cdrArrangementId, default); + + // Assert + ResultHelper.AssertInstanceOf(result, out var objResult); + Assert.AreEqual((int)HttpStatusCode.InternalServerError, objResult.StatusCode); + ResultHelper.AssertErrorExpectation(objResult, ErrorCodes.Cds.InvalidConsentArrangement, "Invalid client_id", clientId); + } + + [TestCase(typeof(TaskCanceledException), "Message", "The operation was cancelled as the ADR did not respond within the timeout period of 30 seconds.")] + [TestCase(typeof(NotImplementedException), "Something else went wrong in client call", "Something else went wrong in client call")] + public async Task RemoveArrangementReturnsOKWithExceptionMessage(Type exceptionType, string exceptionMessage, string expectedResponseContent) + { + // Arrange + var cdrArrangementId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + _grantService.Setup(x => x.Get(GrantTypes.CdrArrangement, cdrArrangementId, null)).ReturnsAsync(new CdrArrangementGrant { ClientId = clientId }); + _clientService.Setup(x => x.Get(clientId)).ReturnsAsync(new Client { ClientId = clientId }); + _consentRevocationService + .Setup(x => x.RevokeAdrArrangement(It.IsAny(), cdrArrangementId, It.IsAny(), It.IsAny())) + .ReturnsAsync(new IConsentRevocationService.OutboundCallDetails(GenerateRequestMessage(), null, (Exception)Activator.CreateInstance(exceptionType, exceptionMessage)!)); + + var controller = new UtilityController(_logger.Object, _grantService.Object, _clientService.Object, _consentRevocationService.Object); + + // Act + var result = await controller.RemoveArrangementAndTriggerDataRecipientArrangementRevocation(cdrArrangementId, default); + + // Assert + ResultHelper.AssertInstanceOf(result, out var objResult); + ResultHelper.AssertJsonInstanceOf(objResult!, out var response); + + AssertRequestIsPopulated(response.ArrangeRevocationRequest); + + // Response should be custom error message + Assert.NotNull(response.ArrangeRevocationResponse); + Assert.AreEqual(expectedResponseContent, response.ArrangeRevocationResponse!.Content); + } + + [TestCase(true)] + [TestCase(false)] + public async Task RemoveArrangementReturnsOkForSuccessfullySendRequest(bool error) + { + // Arrange + var cdrArrangementId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + _grantService.Setup(x => x.Get(GrantTypes.CdrArrangement, cdrArrangementId, null)).ReturnsAsync(new CdrArrangementGrant { ClientId = clientId }); + _clientService.Setup(x => x.Get(clientId)).ReturnsAsync(new Client { ClientId = clientId }); + _consentRevocationService + .Setup(x => x.RevokeAdrArrangement(It.IsAny(), cdrArrangementId, It.IsAny(), It.IsAny())) + .ReturnsAsync(new IConsentRevocationService.OutboundCallDetails(GenerateRequestMessage(), error ? GenerateErrorResponseMessage() : GenerateResponseMessage(), null)); + + var controller = new UtilityController(_logger.Object, _grantService.Object, _clientService.Object, _consentRevocationService.Object); + + // Act + var result = await controller.RemoveArrangementAndTriggerDataRecipientArrangementRevocation(cdrArrangementId, default); + + // Assert + ResultHelper.AssertInstanceOf(result, out var objResult); + ResultHelper.AssertJsonInstanceOf(objResult!, out var response); + + AssertRequestIsPopulated(response.ArrangeRevocationRequest); + AssertResponseIsPopulated(response.ArrangeRevocationResponse, error); + } + + private static HttpRequestMessage GenerateRequestMessage() + { + var request = new HttpRequestMessage + { + RequestUri = new Uri("https://localhost/arrangements/revoke"), + Method = HttpMethod.Post, + Content = new StringContent( + "cdr_arrangement_jwt=eyJhbGciOiJQUzI1NiIsImtpZCI6IkQzRjc3MzE1RjExOTJFOEZFNDhDRjIwMkVBOTU5REYzQzIwNkQ4QTgiLCJ4NXQiOiIwX2R6RmZFWkxvX2tqUElDNnBXZDg4SUcyS2ciLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vbG9jYWxob3N0L2FycmFuZ2VtZW50cy9yZXZva2UiLCJpc3MiOiI4YmM5MTgyMi05Y2Q2LTRkYmMtYTg3Yi01OWZmY2RhODQwNzAiLCJleHAiOjE3Mjk0Nzg5OTgsImNkcl9hcnJhbmdlbWVudF9pZCI6IjE2N2IzZDYxLTM0ZTYtNDI4YS04Zjc2LTM2Zjg1YjQ1MzA5ZiIsInN1YiI6IjhiYzkxODIyLTljZDYtNGRiYy1hODdiLTU5ZmZjZGE4NDA3MCIsImp0aSI6IjA5MjlkM2VlLTI1YTItNGRkNS1iOGEwLWYzZDMyMTQ3MzZmNiIsImlhdCI6MTcyOTQ3ODY5OCwibmJmIjoxNzI5NDc4Njk4fQ.cAaaI_Cu7jARBROJcMmMlJX0yh7dSRzkuDVLf4d8pHHV46tO4XGZIqfl-nTF8tpysq2CZsl0BhAXZzC-N4rdUtcq90Qgv5sYHCgFZzfx4v8xxeywWNxegfCyjIxk5dhe0hu4qh3Izq-lmEgpAbI5TbI66bBwaJl0m8ZC3-1DXKV9Ddbq3k9IwLya5gkwp6Hm1gRhCN9SMiZoTeTvqhj3JCHKclWUsYvodYUncFqh9U9Zm51OlxlXgvSs6z3yJfVnYEIomyvD62aSNXTQWteUA2dnP3auBhhLAOvXO5Obk2M383wz0VDRVhXsMj3WdTZQgdULTJiP4KwosM8Kqn7PnQ", + new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded")), + }; + + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "eyJhbGciOiJQUzI1NiIsImtpZCI6IkQzRjc3MzE1RjExOTJFOEZFNDhDRjIwMkVBOTU5REYzQzIwNkQ4QTgiLCJ4NXQiOiIwX2R6RmZFWkxvX2tqUElDNnBXZDg4SUcyS2ciLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vbG9jYWxob3N0L2FycmFuZ2VtZW50cy9yZXZva2UiLCJpc3MiOiI4YmM5MTgyMi05Y2Q2LTRkYmMtYTg3Yi01OWZmY2RhODQwNzAiLCJleHAiOjE3Mjk0Nzg5OTgsInN1YiI6IjhiYzkxODIyLTljZDYtNGRiYy1hODdiLTU5ZmZjZGE4NDA3MCIsImp0aSI6ImE0MjVkN2QzLTA0ZGItNGYzMi04NjM5LTFmYzFkNmNkODVlYSIsImlhdCI6MTcyOTQ3ODY5OCwibmJmIjoxNzI5NDc4Njk4fQ.lZJxKCHUiDITFNLNLdzaakaiQStx0kq2RGe4HQK6ImhyUfU3-wU6Obdy7bVyWH986DvWHhMm5BTuRW6g2BJ25VqrSHlweCUJ9wJgLmiwTnzr3h33N3cuoFdReos4eraOufdNTwwrvyj60wyBUpMno36DSz_lBPCKOIFriDoa0JtfDWPhJYQaWrjIBk45awLD2GcpeWb6ZIcf2XVdzxF3f6eK7vMtSHkle4qGa4xbbXMzhOogAgYk1KsKcWYSalwB0FAsfVnRKRGtin5T2x-V9znjNthlR0JtfwilM_rN4NRTMl0a9156t4Phd3C9Z-KVrLXDIHrjvWoF2w2468a_vA"); + + return request; + } + + private static HttpResponseMessage GenerateResponseMessage() + { + var response = new HttpResponseMessage + { + StatusCode = HttpStatusCode.NoContent, + }; + + response.Headers.Add("Version", ["1.0"]); + + return response; + } + + private static HttpResponseMessage GenerateErrorResponseMessage() + { + var response = new HttpResponseMessage + { + StatusCode = HttpStatusCode.UnprocessableEntity, + Content = new StringContent(_errorResponse), + }; + + return response; + } + + private static void AssertRequestIsPopulated(ArrangeRevocationRequest? arrangeRevocationRequest) + { + Assert.NotNull(arrangeRevocationRequest); + Assert.NotNull(arrangeRevocationRequest!.Body); + Assert.That(Regex.IsMatch(arrangeRevocationRequest!.Body!, "cdr_arrangement_jwt=[\\w\\.]+")); + Assert.AreEqual("POST", arrangeRevocationRequest.Method); + Assert.AreEqual("https://localhost/arrangements/revoke", arrangeRevocationRequest.Url); + Assert.IsNotEmpty(arrangeRevocationRequest.Headers); + ResultHelper.AssertJsonInstanceOf>(arrangeRevocationRequest.Headers!, out var headers); + + Assert.IsTrue(headers.TryGetValue("Authorization", out var authHeaderValues)); + Assert.AreEqual(1, authHeaderValues!.Count()); + Assert.That(Regex.IsMatch(authHeaderValues![0], "Bearer [\\w\\.]+")); + } + + private static void AssertResponseIsPopulated(ArrangeRevocationResponse? arrangeRevocationResponse, bool error) + { + Assert.NotNull(arrangeRevocationResponse); + + if (error) + { + Assert.AreEqual((int)HttpStatusCode.UnprocessableEntity, arrangeRevocationResponse!.StatusCode); + Assert.AreEqual(_errorResponse, arrangeRevocationResponse.Content); + } + else + { + Assert.AreEqual((int)HttpStatusCode.NoContent, arrangeRevocationResponse!.StatusCode); + } + } + } +} diff --git a/Source/CdrAuthServer.UnitTests/Extensions/StringExtensionsTests.cs b/Source/CdrAuthServer.UnitTests/Extensions/StringExtensionsTests.cs index 00903fe..3887a4d 100644 --- a/Source/CdrAuthServer.UnitTests/Extensions/StringExtensionsTests.cs +++ b/Source/CdrAuthServer.UnitTests/Extensions/StringExtensionsTests.cs @@ -1,5 +1,4 @@ using CdrAuthServer.Domain.Extensions; -using CdrAuthServer.Extensions; using NUnit.Framework; namespace CdrAuthServer.UnitTests.Extensions @@ -15,7 +14,7 @@ public void Setup() public void WhenValueIsNull_HasValue_ShouldReturnFalse() { // Arrange. - string value = null; + string? value = null; bool expected = false; bool actual = false; @@ -101,4 +100,4 @@ public void WhenValueExists_IsNullOrEmpty_ShouldReturnFalse() Assert.AreEqual(expected, actual); } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer.UnitTests/Helpers/HttpHelperTests.cs b/Source/CdrAuthServer.UnitTests/Helpers/HttpHelperTests.cs index c222957..139d84d 100644 --- a/Source/CdrAuthServer.UnitTests/Helpers/HttpHelperTests.cs +++ b/Source/CdrAuthServer.UnitTests/Helpers/HttpHelperTests.cs @@ -29,7 +29,8 @@ public void Setup() public async Task ServerCertificates_ValidationEnabled_ShouldValidateSslConnection( string certName, string certPassword, bool expected, string reason) { - await using (var mockEndpoint = new MockEndpoint("https://localhost:9990", + await using (var mockEndpoint = new MockEndpoint( + "https://localhost:9990", Path.Combine(Directory.GetCurrentDirectory(), "Certificates", "MDR", certName), certPassword)) { @@ -44,23 +45,27 @@ public async Task ServerCertificates_ValidationEnabled_ShouldValidateSslConnecti { Assert.ThrowsAsync(async () => await client.GetAsync("https://localhost:9990"), reason); } + await mockEndpoint.Stop(); } } } public partial class MockEndpoint : IAsyncDisposable - { + { public MockEndpoint(string url, string certificatePath, string certificatePassword) { Url = url; CertificatePath = certificatePath; CertificatePassword = certificatePassword; - } + } public string Url { get; init; } + private int UrlPort => new Uri(Url).Port; + public string CertificatePath { get; init; } + public string CertificatePassword { get; init; } private IWebHost? _host; @@ -72,9 +77,10 @@ public void Start() _host = new WebHostBuilder() .UseKestrel(opts => { - opts.ListenAnyIP(UrlPort, - opts => opts.UseHttps(new X509Certificate2(CertificatePath, CertificatePassword, X509KeyStorageFlags.Exportable))); - }) + opts.ListenAnyIP( + UrlPort, + opts => opts.UseHttps(new X509Certificate2(CertificatePath, CertificatePassword, X509KeyStorageFlags.Exportable))); + }) .UseStartup(_ => new MockEndpointStartup()) .Build(); @@ -91,30 +97,31 @@ public async Task Stop() } } - bool _disposed; + private bool _disposed; + public async ValueTask DisposeAsync() { Log.Information("Calling {FUNCTION} in {ClassName}.", nameof(DisposeAsync), nameof(MockEndpoint)); if (!_disposed) - { + { await Stop(); _disposed = true; - } + } GC.SuppressFinalize(this); - } + } - class MockEndpointStartup - { - public void Configure(IApplicationBuilder app) + private class MockEndpointStartup { + public void Configure(IApplicationBuilder app) + { app.UseHttpsRedirection(); app.UseRouting(); - } + } public static void ConfigureServices(IServiceCollection services) - { + { services.AddRouting(); } } diff --git a/Source/CdrAuthServer.UnitTests/HttpClientHelper.cs b/Source/CdrAuthServer.UnitTests/HttpClientHelper.cs new file mode 100644 index 0000000..02c4ff0 --- /dev/null +++ b/Source/CdrAuthServer.UnitTests/HttpClientHelper.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq.Expressions; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using CdrAuthServer.Extensions; +using Moq; +using Moq.Protected; + +namespace CdrAuthServer.UnitTests +{ + /// + /// Helper functionality for mocking HttpClient. + /// + public static class HttpClientHelper + { + /// + /// Adds a mocked response for calls. + /// + /// The handler mock to decorate. + /// The response code to return. + /// The response content to return, if provided. + /// A message filtering predicate that can be used to apply the behaviour conditionally. For example, based on a route/method, headers, query params etc. + /// + /// This can be used to configure a object that is passed when creating a such as in the following example: + /// + /// var handler = new Mock<HttpClientHandler>(); + /// handler.AddMockedHttpResponse(HttpStatusCode.OK, new StringContent("Hello world"), m => m.RequestUri = new Uri("http://localhost/hello-world")); + /// var httpClient = new Mock<HttpClient>(handler.Object); + /// + /// + public static void AddMockedHttpResponse(this Mock clientHandler, HttpStatusCode responseCode, HttpContent? responseContent, Expression>? messageFilter = null) + { + messageFilter ??= _ => true; // match anything by default + + clientHandler.Protected() + .Setup>("SendAsync", ItExpr.Is(messageFilter), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = responseCode, + Content = responseContent, + }); + } + + /// + /// Adds a mocked JSON response for calls. + /// + /// + /// This can be used to configure a object that is passed when creating a such as in the following example: + /// + /// var handler = new Mock<HttpClientHandler>(); + /// handler.AddMockedHttpResponseJson(HttpStatusCode.OK, new { Hello = "world"}, m => m.RequestUri = new Uri("http://localhost/hello-world")); + /// var httpClient = new Mock<HttpClient>(handler.Object); + /// + /// + /// The type of the response object. + /// The handler mock to decorate. + /// The response code to return. + /// The object to that will be serialised as JSON and form the response content. + /// A message filtering predicate that can be used to apply the behaviour conditionally. For example, based on a route/method, headers, query params etc. + public static void AddMockedHttpResponseJson(this Mock clientHandler, HttpStatusCode responseCode, T response, Expression>? messageFilter = null) + { + var content = new StringContent(response!.ToJson(), new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); + AddMockedHttpResponse(clientHandler, responseCode, content, messageFilter); + } + + /// + /// Adds a mocked exception to be thrown on to emulate errors for negative testing. + /// + /// + /// This can be used to configure a object that is passed when creating a such as in the following example: + /// + /// var handler = new Mock<HttpClientHandler>(); + /// handler.AddMockedException<NotImplementException>(new("No hello"), m => m.RequestUri = new Uri("http://localhost/hello-world")); + /// var httpClient = new Mock<HttpClient>(handler.Object); + /// + /// + /// The type of the exception that will be returned. + /// The handler mock to decorate. + /// The exception to throw. + /// A message filtering predicate that can be used to apply the behaviour conditionally. For example, based on a route/method, headers, query params etc. + public static void AddMockedException(this Mock clientHandler, TException exception, Expression>? messageFilter = null) + where TException : Exception + { + messageFilter ??= _ => true; // match anything by default + + clientHandler.Protected() + .Setup>("SendAsync", ItExpr.Is(messageFilter), ItExpr.IsAny()) + .ThrowsAsync(exception); + } + } +} diff --git a/Source/CdrAuthServer.UnitTests/ResultHelper.cs b/Source/CdrAuthServer.UnitTests/ResultHelper.cs new file mode 100644 index 0000000..49b7db8 --- /dev/null +++ b/Source/CdrAuthServer.UnitTests/ResultHelper.cs @@ -0,0 +1,59 @@ +using CdrAuthServer.Domain.Models; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace CdrAuthServer.UnitTests +{ + internal static class ResultHelper + { + public static void AssertInstanceOf(object obj, out T converted) + where T : class + { + Assert.IsInstanceOf(obj); + converted = (T)obj; + } + + public static void AssertJsonInstanceOf(ObjectResult objectResult, out T converted) + where T : class, new() + { + Assert.IsNotNull(objectResult.Value); + Assert.IsAssignableFrom(objectResult.Value!); + try + { + converted = JsonConvert.DeserializeObject((string)objectResult.Value!)!; + } + catch (JsonException) + { + Assert.Fail($"{nameof(objectResult)} cannot be deserialised to {typeof(T).Name}"); + converted = new T(); + } + } + + public static void AssertJsonInstanceOf(string value, out T instance) + where T : class, new() + { + try + { + instance = JsonConvert.DeserializeObject(value)!; + } + catch (JsonException) + { + Assert.Fail($"{nameof(value)} cannot be deserialised to {typeof(T).Name}"); + instance = new T(); + } + } + + public static void AssertErrorExpectation(ObjectResult objectResult, string errorCode, string errorTitle, string errorDetail) + { + Assert.IsInstanceOf(objectResult!.Value); + var errors = objectResult.Value as ResponseErrorList; + Assert.IsNotNull(errors); + Assert.AreEqual(1, errors!.Errors.Count); + var error = errors.Errors[0]; + Assert.AreEqual(errorCode, error.Code); + Assert.AreEqual(errorTitle, error.Title); + Assert.AreEqual(errorDetail, error.Detail); + } + } +} diff --git a/Source/CdrAuthServer.UnitTests/Services/ConsentRevocationServiceTests.cs b/Source/CdrAuthServer.UnitTests/Services/ConsentRevocationServiceTests.cs new file mode 100644 index 0000000..9ded4f7 --- /dev/null +++ b/Source/CdrAuthServer.UnitTests/Services/ConsentRevocationServiceTests.cs @@ -0,0 +1,237 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using CdrAuthServer.Configuration; +using CdrAuthServer.Infrastructure.Certificates; +using CdrAuthServer.Models; +using CdrAuthServer.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Moq; +using NUnit.Framework; + +namespace CdrAuthServer.UnitTests.Services +{ + internal class ConsentRevocationServiceTests + { + private readonly IOptions _configurationOptions = Options.Create(new ConfigurationOptions { BrandId = Guid.NewGuid().ToString() }); + + private readonly Mock _mockHttpClient = new(); + + private readonly Mock> _logger = new(); + + private readonly Mock _certificateLoader = new(); + + private readonly Client _client = new() { ClientId = Guid.NewGuid().ToString(), RecipientBaseUri = "http://localhost" }; + + private readonly string _arrangementId = Guid.NewGuid().ToString(); + + private readonly X509Certificate2 _ps256SigningCertificate = CertificateHelper.CreateSigning(); + + public ConsentRevocationServiceTests() + { + _certificateLoader.Setup(x => x.Load(It.IsAny())).ReturnsAsync(_ps256SigningCertificate); + } + + /// + /// Ensure that when the request is successfully sent the appropriate request/response pair is returned. + /// + /// Expect no exception, only a request and response. + [Test] + public async Task RevokeAdrArrangementReturnsRequestResponsePairForSuccessfulRequest() + { + // Arrange + _mockHttpClient + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpResponseMessage(System.Net.HttpStatusCode.NoContent) + { + Content = new StringContent(string.Empty), + }); + + var service = new ConsentRevocationService(_mockHttpClient.Object, _configurationOptions, _certificateLoader.Object, _logger.Object); + + // Act + var (request, response, exception) = await service.RevokeAdrArrangement(_client, _arrangementId, TimeSpan.FromSeconds(5)); + + // Assert + Assert.NotNull(request); + Assert.NotNull(response); + Assert.IsNull(exception); + + await AssertBearerTokenIsValid(request); + await AssertArrangementIsValid(request); + } + + /// + /// Ensure that when an exception is thrown that is related to cancellation that it is handled appropriately. + /// + /// Expect an appropriate Exception to be returned, and no response. + [Test] + public async Task RevokeAdrArrangementReturnsRequestExceptionPairForThrownException() + { + // Arrange + _mockHttpClient + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new NotImplementedException("Emulate an exception thrown that isn't due to cancellation")); + + var service = new ConsentRevocationService(_mockHttpClient.Object, _configurationOptions, _certificateLoader.Object, _logger.Object); + + // Act + var (request, response, exception) = await service.RevokeAdrArrangement(_client, _arrangementId, TimeSpan.FromSeconds(5)); + + // Assert + Assert.NotNull(request); + Assert.NotNull(request.Content); + Assert.IsNull(response); + Assert.NotNull(exception); + } + + /// + /// Ensure that when the request is not responded to by the time the timeout expires it is cancelled. + /// + /// Expect a TaskCancellationException to be returned, and no response. + [Test] + public async Task RevokeAdrArrangementCancelsOnTimeout() + { + var stopWatch = new Stopwatch(); + + // The round trip to be more than the call timeout so that the task gets cancelled. + int roundTripDurationMs = 500, callTimeoutMs = 200; + + // Arrange + _mockHttpClient + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .Returns(async (message, token) => + { + // Wait until the token is cancelled + await Task.Delay(roundTripDurationMs, token); + return new HttpResponseMessage(); + }); + + var service = new ConsentRevocationService(_mockHttpClient.Object, _configurationOptions, _certificateLoader.Object, _logger.Object); + + // Act + stopWatch.Start(); + var (request, response, exception) = await service.RevokeAdrArrangement(_client, _arrangementId, TimeSpan.FromMilliseconds(callTimeoutMs)); + stopWatch.Stop(); + + // Assert + Assert.NotNull(request); + Assert.NotNull(request.Content); + Assert.IsNull(response); + Assert.NotNull(exception); + Assert.IsAssignableFrom(typeof(TaskCanceledException), exception); + Assert.LessOrEqual(stopWatch.ElapsedMilliseconds, roundTripDurationMs, "Request is expected to be cancelled after {0} but before {1}", callTimeoutMs, roundTripDurationMs); + } + + /// + /// Ensure that when the parent cancellation token (i.e. incoming token from the caller) is cancelled the request is cancelled. + /// + /// Expect a TaskCancellationException to be returned, and no response. + [Test] + public async Task RevokeAdrArrangementCancelsOnParent() + { + var stopWatch = new Stopwatch(); + + // The round trip to be less than the call timeout but longer than the parent token timeout in order for the parent cancellation token to cancel first. + int roundTripDurationMs = 300, callTimeoutMs = 500, requestorTimeoutMs = 100; + + // Arrange + _mockHttpClient + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .Returns(async (message, token) => + { + // Wait until the token is cancelled + await Task.Delay(roundTripDurationMs, token); + return new HttpResponseMessage(); + }); + + var service = new ConsentRevocationService(_mockHttpClient.Object, _configurationOptions, _certificateLoader.Object, _logger.Object); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(requestorTimeoutMs); + + // Act + stopWatch.Start(); + var (request, response, exception) = await service.RevokeAdrArrangement(_client, _arrangementId, TimeSpan.FromSeconds(callTimeoutMs), cts.Token); + stopWatch.Stop(); + + // Assert + Assert.NotNull(request); + Assert.NotNull(request.Content); + Assert.IsNull(response); + Assert.NotNull(exception); + Assert.IsAssignableFrom(typeof(TaskCanceledException), exception); + Assert.LessOrEqual(stopWatch.ElapsedMilliseconds, roundTripDurationMs, "Request is expected to be cancelled after {0} but before {1}", requestorTimeoutMs, callTimeoutMs); + } + + /// + /// Ensure that the bearer token sent has the expected details and signature. + /// + private async Task AssertBearerTokenIsValid(HttpRequestMessage request) + { + var handler = new JsonWebTokenHandler(); + var publicKey = new X509Certificate2(_ps256SigningCertificate.GetRawCertData()); + + var authHeader = request.Headers.Authorization; + + Assert.NotNull(authHeader); + Assert.AreEqual("Bearer", authHeader!.Scheme); + + var validationResult = await handler.ValidateTokenAsync( + authHeader.Parameter, + new TokenValidationParameters + { + IssuerSigningKey = new X509SecurityKey(publicKey), + ValidAudience = _client.RecipientBaseUri + "/arrangements/revoke", + ValidIssuer = _configurationOptions.Value.BrandId, + ValidateIssuerSigningKey = true, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateTokenReplay = true, + }); + + Assert.IsTrue(validationResult.IsValid, validationResult.Exception != null ? validationResult.Exception.Message : string.Empty); + + var subjectIsValid = validationResult.Claims.TryGetValue(JwtRegisteredClaimNames.Sub, out var subject) && (string)subject == _configurationOptions.Value.BrandId; + var jtiIsValid = validationResult.Claims.TryGetValue(JwtRegisteredClaimNames.Jti, out var jti) && !string.IsNullOrEmpty((string)jti); + + Assert.IsTrue(subjectIsValid, $"Claim 'subject' was not provided or did not match {_configurationOptions.Value.BrandId}"); + Assert.IsTrue(jtiIsValid, "Claim 'jti' must be provided"); + } + + /// + /// Ensure that the arrangement has the expected details and signature. + /// + private async Task AssertArrangementIsValid(HttpRequestMessage request) + { + var handler = new JsonWebTokenHandler(); + var publicKey = new X509Certificate2(_ps256SigningCertificate.GetRawCertData()); + + Assert.AreEqual("application/x-www-form-urlencoded", request.Content?.Headers.ContentType?.MediaType); + + using var reader = new Microsoft.AspNetCore.WebUtilities.FormReader(await request.Content!.ReadAsStreamAsync()); + var formValues = await reader.ReadFormAsync(); + + Assert.True(formValues.TryGetValue("cdr_arrangement_jwt", out StringValues cdrArrangementJwt)); + + var validationResult = await handler.ValidateTokenAsync(cdrArrangementJwt, new TokenValidationParameters + { + ValidIssuer = _configurationOptions.Value.BrandId, + ValidAudience = _client.RecipientBaseUri + "/arrangements/revoke", + IssuerSigningKey = new X509SecurityKey(publicKey), + }); + + Assert.IsTrue(validationResult.IsValid, validationResult.Exception != null ? validationResult.Exception.Message : string.Empty); + Assert.AreEqual(_arrangementId, validationResult.Claims.FirstOrDefault(x => x.Key == "cdr_arrangement_id").Value); + } + } +} diff --git a/Source/CdrAuthServer.UnitTests/Services/JwksServiceTests.cs b/Source/CdrAuthServer.UnitTests/Services/JwksServiceTests.cs new file mode 100644 index 0000000..915157c --- /dev/null +++ b/Source/CdrAuthServer.UnitTests/Services/JwksServiceTests.cs @@ -0,0 +1,207 @@ +using CdrAuthServer.Exceptions; +using CdrAuthServer.Services; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; + +namespace CdrAuthServer.UnitTests.Services +{ + internal class JwksServiceTests + { + private readonly JsonWebKeySet _jwksCached; + private readonly JsonWebKeySet _jwks; + private readonly JsonWebKey jwk1 = JsonWebKeyConverter.ConvertFromRSASecurityKey(new RsaSecurityKey(RSA.Create())); + private readonly JsonWebKey jwk2 = JsonWebKeyConverter.ConvertFromRSASecurityKey(new RsaSecurityKey(RSA.Create())); + + private readonly Uri _cachedJwksUri = new("https://localhost/cached"); + private readonly Uri _uncachedJwksUri = new("https://localhost/not-cached"); + + private readonly Mock> _loggerMock = new(); + private readonly Mock _httpClientHandlerMock = new(); + private readonly Mock _cacheMock = new(); + private readonly IConfiguration _config; + + public JwksServiceTests() + { + _jwks = new JsonWebKeySet { Keys = { jwk1, jwk2 } }; + _jwksCached = new JsonWebKeySet { Keys = { jwk1 } }; + _config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "CacheExpiryMinutes", "5" } }) + .Build(); + } + + /// + /// Asserts that hits the cache for cached JWKS. + /// + [Test] + public async Task GetJwksWhenCachedReturnsFromCache() + { + var httpClient = new Mock(_httpClientHandlerMock.Object); + object? jwks = _jwksCached; + object? nullSet = null; + _cacheMock.Setup(x => x.TryGetValue(_cachedJwksUri.ToString(), out jwks)).Returns(true); + _cacheMock.Setup(x => x.TryGetValue(_uncachedJwksUri.ToString(), out nullSet)).Returns(true); + + var service = new JwksService(_loggerMock.Object, _config, httpClient.Object, _cacheMock.Object); + + var result = await service.GetJwks(_cachedJwksUri); + httpClient.Verify(x => x.SendAsync(It.IsAny(), It.IsAny()), Times.Never); + + Assert.IsNotNull(result); + Assert.Greater(result!.Keys.Count, 0); + } + + /// + /// Asserts that refreshes for uncached JWKS. + /// + [Test] + public async Task GetJwksWhenNotCachedFetchesThenCaches() + { + // Arrange + object? jwks = _jwksCached; + object? nullSet = null; + _cacheMock.Setup(x => x.TryGetValue(_cachedJwksUri.ToString(), out jwks)).Returns(true); + _cacheMock.Setup(x => x.TryGetValue(_uncachedJwksUri.ToString(), out nullSet)).Returns(true); + _cacheMock.Setup(x => x.CreateEntry(It.IsAny())).Returns(new Mock().Object); + + _httpClientHandlerMock.AddMockedHttpResponseJson(HttpStatusCode.OK, _jwks, msg => msg.RequestUri == _uncachedJwksUri); + var httpClient = new Mock(_httpClientHandlerMock.Object); + var service = new JwksService(_loggerMock.Object, _config, httpClient.Object, _cacheMock.Object); + + // Act + var result = await service.GetJwks(_uncachedJwksUri); + + // Assert + httpClient.VerifyAll(); + Assert.IsNotNull(result); + Assert.Greater(result!.Keys.Count, 0); + } + + /// + /// Asserts that throws an exception if the JWKS endpoint returns with an unsuccessful error code. + /// + /// The HTTP status code to return to indicate an error fetching the JWKS. + /// The expected message for the exception which is expected to be thrown. + [TestCase(HttpStatusCode.NotFound, "https://localhost/not-cached returned 404.")] + [TestCase(HttpStatusCode.ServiceUnavailable, "https://localhost/not-cached returned " + nameof(HttpStatusCode.ServiceUnavailable))] + public void GetJwksThrowsExceptionForStatusCode(HttpStatusCode status, string exceptionMessage) + { + // Arrange + object? jwks = _jwksCached; + object? nullSet = null; + _cacheMock.Setup(x => x.TryGetValue(_cachedJwksUri.ToString(), out jwks)).Returns(true); + _cacheMock.Setup(x => x.TryGetValue(_uncachedJwksUri.ToString(), out nullSet)).Returns(true); + _cacheMock.Setup(x => x.CreateEntry(It.IsAny())).Returns(new Mock().Object); + + _httpClientHandlerMock.AddMockedHttpResponse(status, new StringContent("this is not the JWKS you are looking for"), msg => msg.RequestUri == _uncachedJwksUri); + var httpClient = new Mock(_httpClientHandlerMock.Object); + var service = new JwksService(_loggerMock.Object, _config, httpClient.Object, _cacheMock.Object); + + // Act & Assert + var result = Assert.ThrowsAsync(async () => await service.GetJwks(_uncachedJwksUri)); + Assert.True(result!.Message.StartsWith(exceptionMessage)); + } + + /// + /// Asserts that throws an exception if the call to fetch JWKS from the endpoint failed with an exception. + /// + [Test] + public void GetJwksWhenNotCachedThrowsExceptionForClientException() + { + // Arrange + object? jwks = _jwksCached; + object? nullSet = null; + _cacheMock.Setup(x => x.TryGetValue(_cachedJwksUri.ToString(), out jwks)).Returns(true); + _cacheMock.Setup(x => x.TryGetValue(_uncachedJwksUri.ToString(), out nullSet)).Returns(true); + _cacheMock.Setup(x => x.CreateEntry(It.IsAny())).Returns(new Mock().Object); + + var expectedException = new NotImplementedException("Emulate an exception thrown"); + _httpClientHandlerMock.AddMockedException(expectedException); + var httpClient = new Mock(_httpClientHandlerMock.Object); + var service = new JwksService(_loggerMock.Object, _config, httpClient.Object, _cacheMock.Object); + + // Act & Assert + var result = Assert.ThrowsAsync(async () => await service.GetJwks(_uncachedJwksUri)); + Assert.AreEqual("An error occurred retrieving JWKS from https://localhost/not-cached - Emulate an exception thrown", result!.Message); + } + + /// + /// Asserts that throws an exception if the JWKS payload is not well-formed. + /// + [Test] + public void GetJwksWhenNotCachedThrowsExceptionForInvalidResponsePayload() + { + // Arrange + object? jwks = _jwksCached; + object? nullSet = null; + _cacheMock.Setup(x => x.TryGetValue(_cachedJwksUri.ToString(), out jwks)).Returns(true); + _cacheMock.Setup(x => x.TryGetValue(_uncachedJwksUri.ToString(), out nullSet)).Returns(true); + _cacheMock.Setup(x => x.CreateEntry(It.IsAny())).Returns(new Mock().Object); + + _httpClientHandlerMock.AddMockedHttpResponse(HttpStatusCode.OK, new StringContent("this is not the JWKS you are looking for"), msg => msg.RequestUri == _uncachedJwksUri); + var httpClient = new Mock(_httpClientHandlerMock.Object); + var service = new JwksService(_loggerMock.Object, _config, httpClient.Object, _cacheMock.Object); + + // Act & Assert + var result = Assert.ThrowsAsync(async () => await service.GetJwks(_uncachedJwksUri)); + Assert.AreEqual("No valid JWKS found from " + _uncachedJwksUri, result!.Message); + } + + /// + /// Asserts that hits the cache for cached JWKS. + /// + [Test] + public async Task GetJwksByKidWhenCachedReturnsFromCache() + { + var httpClient = new Mock(_httpClientHandlerMock.Object); + object? jwks = _jwksCached; + object? nullSet = null; + _cacheMock.Setup(x => x.TryGetValue(_cachedJwksUri.ToString(), out jwks)).Returns(true); + _cacheMock.Setup(x => x.TryGetValue(_uncachedJwksUri.ToString(), out nullSet)).Returns(true); + + var service = new JwksService(_loggerMock.Object, _config, httpClient.Object, _cacheMock.Object); + + var result = await service.GetJwks(_cachedJwksUri, _jwks.Keys[0].KeyId); + httpClient.Verify(x => x.SendAsync(It.IsAny(), It.IsAny()), Times.Never); + + Assert.IsNotNull(result); + Assert.Greater(result!.Keys.Count, 0); + } + + /// + /// Asserts that refreshes for uncached JWKS. + /// + [Test] + public async Task GetJwksByKidWhenNotCachedFetchesThenCaches() + { + // Arrange + object? jwks = _jwksCached; + object? nullSet = null; + _cacheMock.Setup(x => x.TryGetValue(_cachedJwksUri.ToString(), out jwks)).Returns(true); + _cacheMock.Setup(x => x.TryGetValue(_uncachedJwksUri.ToString(), out nullSet)).Returns(false); + _cacheMock.Setup(x => x.CreateEntry(It.IsAny())).Returns(new Mock().Object); + + _httpClientHandlerMock.AddMockedHttpResponseJson(HttpStatusCode.OK, _jwks, msg => msg.RequestUri == _uncachedJwksUri); + var httpClient = new Mock(_httpClientHandlerMock.Object); + var service = new JwksService(_loggerMock.Object, _config, httpClient.Object, _cacheMock.Object); + + // Act + var result = await service.GetJwks(_uncachedJwksUri, _jwks.Keys[1].KeyId); + + // Assert + httpClient.VerifyAll(); + Assert.IsNotNull(result); + Assert.Greater(result!.Keys.Count, 0); + } + } +} diff --git a/Source/CdrAuthServer.UnitTests/Services/RegisterClientServiceTests.cs b/Source/CdrAuthServer.UnitTests/Services/RegisterClientServiceTests.cs new file mode 100644 index 0000000..d64f2a0 --- /dev/null +++ b/Source/CdrAuthServer.UnitTests/Services/RegisterClientServiceTests.cs @@ -0,0 +1,126 @@ +using CdrAuthServer.Configuration; +using CdrAuthServer.Services; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace CdrAuthServer.UnitTests.Services +{ + internal class RegisterClientServiceTests + { + private readonly IOptions _options = Options.Create(new CdrRegisterConfiguration { Version = 3, GetDataRecipientsEndpoint = "https://localhost/cdr-register/v1/all/data-recipients" }); + + private readonly Mock _mockHttpClient = new(); + + [Test] + public async Task GetDataRecipientsReturnsNullForFailedRequest() + { + // Arrange + _mockHttpClient + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)) + .Verifiable(Times.Once); + + var service = new RegisterClientService(_mockHttpClient.Object, _options); + + // Act + var result = await service.GetDataRecipients(); + + // Assert + Assert.IsNull(result); + _mockHttpClient.VerifyAll(); + } + + [Test] + public async Task GetDataRecipientsSendsCorrectHeaders() + { + HttpRequestHeaders? headers = null; + + // Arrange + _mockHttpClient + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .Callback((req, _) => headers = req.Headers) + .ReturnsAsync(new HttpResponseMessage(System.Net.HttpStatusCode.NotImplemented)); + + var service = new RegisterClientService(_mockHttpClient.Object, _options); + + // Act + _ = await service.GetDataRecipients(); + + // Assert + Assert.IsNotNull(headers); + Assert.IsTrue(headers!.TryGetValues("x-v", out var versionHeader)); + Assert.Contains(_options.Value.Version.ToString(), versionHeader!.ToList()); + } + + [Test] + public async Task GetDataRecipientsReturnsLegalEntitiesForSuccessfulRequest() + { + var httpResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent( + """ + { + "data": [ + { + "legalEntityId": "string", + "legalEntityName": "string", + "accreditationNumber": "string", + "accreditationLevel": "UNRESTRICTED", + "logoUri": "string", + "dataRecipientBrands": [ + { + "dataRecipientBrandId": "string", + "brandName": "string", + "logoUri": "string", + "softwareProducts": [ + { + "softwareProductId": "string", + "softwareProductName": "string", + "softwareProductDescription": "string", + "logoUri": "string", + "status": "ACTIVE" + } + ], + "status": "ACTIVE" + } + ], + "status": "ACTIVE", + "lastUpdated": "string" + } + ], + "links": { + "self": "https://localhost/cdr-register/v1/all/data-recipients" + }, + "meta": {} + } + """), + }; + + httpResponse.Headers.Add("x-v", "3"); + + // Arrange + _mockHttpClient + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(httpResponse) + .Verifiable(Times.Once); + + var service = new RegisterClientService(_mockHttpClient.Object, _options); + + // Act + var result = await service.GetDataRecipients(); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result!.Data.Count()); + Assert.AreEqual("https://localhost/cdr-register/v1/all/data-recipients", result!.Links.Self?.AbsoluteUri); + _mockHttpClient.VerifyAll(); + } + } +} diff --git a/Source/CdrAuthServer.UnitTests/Validators/AuthorizeRequestValidatorTests.cs b/Source/CdrAuthServer.UnitTests/Validators/AuthorizeRequestValidatorTests.cs index d0f249e..60c0201 100644 --- a/Source/CdrAuthServer.UnitTests/Validators/AuthorizeRequestValidatorTests.cs +++ b/Source/CdrAuthServer.UnitTests/Validators/AuthorizeRequestValidatorTests.cs @@ -1,4 +1,6 @@ -using CdrAuthServer.Extensions; +using System; +using System.Threading.Tasks; +using CdrAuthServer.Extensions; using CdrAuthServer.Models; using CdrAuthServer.Services; using CdrAuthServer.Validation; @@ -6,24 +8,21 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -using System; -using System.Threading.Tasks; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.UnitTests.Validators { - [TestFixture] public class AuthorizeRequestValidatorTests - { - private const string RESPONSETYPE = "code id_token"; - private ILogger logger; + { + private const string RESPONSETYPE = "code"; + private ILogger logger = null!; private IConfiguration configuration = null!; private Mock clientService = null!; private Mock grantService = null!; private Mock cdrService = null!; private AuthorizeRequestValidator autherizeRequestValidator = null!; - + [SetUp] public void Setup() { @@ -37,16 +36,17 @@ public void Setup() .AddEnvironmentVariables() .Build(); - autherizeRequestValidator = new AuthorizeRequestValidator(logger, - clientService.Object, - grantService.Object, - cdrService.Object); + autherizeRequestValidator = new AuthorizeRequestValidator( + logger, + clientService.Object, + grantService.Object, + cdrService.Object); } - [TestCase("", "", RESPONSETYPE, "", "", "client_id is missing", ErrorCodes.Generic.InvalidRequest)] + [TestCase("", "", RESPONSETYPE, "", "", "client_id is missing", ErrorCodes.Generic.InvalidRequest)] [TestCase("foo", "", RESPONSETYPE, "", "", "Invalid client_id", ErrorCodes.Generic.InvalidRequest)] [TestCase("fooClient", "", RESPONSETYPE, "", "", "request_uri is missing", ErrorCodes.Generic.InvalidRequest)] - [TestCase("fooClient", "https://server/uri", RESPONSETYPE, "", "", "Invalid request_uri", ErrorCodes.Generic.InvalidRequest)] + [TestCase("fooClient", "https://server/uri", RESPONSETYPE, "", "", "Invalid request_uri", ErrorCodes.Generic.InvalidRequest)] [TestCase("fooClient", "https://server/uri", RESPONSETYPE, "", "", "request_uri has expired", ErrorCodes.Generic.InvalidRequestUri)] [TestCase("fooClient", "https://server/uri", RESPONSETYPE, "", "", "request_uri has already been used", ErrorCodes.Generic.InvalidRequestUri)] [TestCase("fooClient", "https://server/uri", RESPONSETYPE, "", "", "client_id does not match request_uri client_id", ErrorCodes.Generic.InvalidRequest)] @@ -58,37 +58,36 @@ public void Setup() [TestCase("78273140-cfa2-4073-b248-0eb41940e4c3", "https://server/uri", RESPONSETYPE, "", "", "openid scope is missing", ErrorCodes.Generic.InvalidRequest)] [TestCase("78273140-cfa2-4073-b248-0eb41940e4c3", "https://server/uri", RESPONSETYPE, "openid scope", "foo", "response_mode is not supported", ErrorCodes.Generic.InvalidRequest)] [TestCase("78273140-cfa2-4073-b248-0eb41940e4c3", "https://server/uri", RESPONSETYPE, "openid scope", "form_post", "Software product not found", ErrorCodes.Generic.InvalidClient)] - [TestCase("78273140-cfa2-4073-b248-0eb41940e4c3", "https://server/uri", RESPONSETYPE, "openid scope", "form_post", "Software product status is INACTIVE", ErrorCodes.Cds.AdrStatusNotActive)] + [TestCase("78273140-cfa2-4073-b248-0eb41940e4c3", "https://server/uri", RESPONSETYPE, "openid scope", "form_post", "Software product status is INACTIVE", ErrorCodes.Cds.AdrStatusNotActive)] public async Task Should_Return_InvalidRequest_With_ErrorCodes( - string client_id, - string request_uri, - string response_type, - string scope, + string client_id, + string request_uri, + string response_type, + string scope, string response_mode, - string expectedErrorDescription, + string expectedErrorDescription, string expectedError) { - //Arrange + // Arrange var configOptions = this.configuration.GetConfigurationOptions(); - AuthorizeRequest authRequest = new AuthorizeRequest() + var authRequest = new AuthorizeRequest() { - client_id = client_id, - request_uri = request_uri, - scope = scope, - redirect_uri = "https://dr.dev.cdrsandbox.gov.au/consent/callback", - response_type = response_type, - nonce = "", - response_mode = response_mode + Client_id = client_id, + Request_uri = request_uri, + Scope = scope, + Redirect_uri = "https://dr.dev.cdrsandbox.gov.au/consent/callback", + Response_type = response_type, + Nonce = string.Empty, + Response_mode = response_mode, }; var grantType = "request_uri"; - - //""scope"": ""openid profile common:customer.basic:read bank:accounts.basic:read bank:transactions:read cdr:registration"", + // ""scope"": ""openid profile common:customer.basic:read bank:accounts.basic:read bank:transactions:read cdr:registration"", var requestObjectJson = @"{ - ""response_type"": ""code id_token"", + ""response_type"": ""code"", ""client_id"": ""78273140-cfa2-4073-b248-0eb41940e4c3"", ""redirect_uri"": ""https://dr.dev.cdrsandbox.gov.au/consent/callback"", - ""response_mode"": ""form_post"", + ""response_mode"": ""jwt"", ""scope"": ""openid profile"", ""state"": ""81afd662-b1e7-474b-a01c-8b7fa6ee0b0a"", ""nonce"": ""91d02897-e8e1-4768-bf88-e5cd5018bd0b"", @@ -117,112 +116,123 @@ public async Task Should_Return_InvalidRequest_With_ErrorCodes( ""aud"": ""https://dh-bank.idp.dev.cdrsandbox.gov.au"" }"; - if (!String.Equals("Invalid client_id", expectedErrorDescription)) + if (!string.Equals("Invalid client_id", expectedErrorDescription)) { - var client = new Client() { ClientId = authRequest.client_id, Scope = authRequest.scope, SoftwareId = "77773140-cfa2-4073-b248-0eb51540e5c6" }; - clientService.Setup(x => x.Get(client_id)).ReturnsAsync(client); + var client = new Client() { ClientId = authRequest.Client_id, Scope = authRequest.Scope, SoftwareId = "77773140-cfa2-4073-b248-0eb51540e5c6" }; + _ = clientService.Setup(x => x.Get(client_id)).ReturnsAsync(client); - if (String.Equals("Invalid request_uri", expectedErrorDescription)) + if (string.Equals("Invalid request_uri", expectedErrorDescription)) { - grantService.Setup(x => x.Get(grantType, authRequest.request_uri, authRequest.client_id)).ReturnsAsync(value: null as RequestUriGrant); + _ = grantService.Setup(x => x.Get(grantType, authRequest.Request_uri, authRequest.Client_id)).ReturnsAsync(value: null); } - else + else { - var requestUriGrant = new RequestUriGrant() { GrantType = grantType, ClientId = authRequest.client_id }; - - if (String.Equals("request_uri has expired", expectedErrorDescription)) + var requestUriGrant = new RequestUriGrant() { GrantType = grantType, ClientId = authRequest.Client_id }; + + if (string.Equals("request_uri has expired", expectedErrorDescription)) { - requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(-30); + requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(-30); } - if (String.Equals("request_uri has already been used", expectedErrorDescription)) + + if (string.Equals("request_uri has already been used", expectedErrorDescription)) { - requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); - requestUriGrant.UsedAt = DateTime.UtcNow; + requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); + requestUriGrant.UsedAt = DateTime.UtcNow; } - if (String.Equals("client_id does not match request_uri client_id", expectedErrorDescription) || - String.Equals("Invalid redirect_uri for client", expectedErrorDescription)) + + if (string.Equals("client_id does not match request_uri client_id", expectedErrorDescription) || + string.Equals("Invalid redirect_uri for client", expectedErrorDescription)) { client.RedirectUris = ["http://server/redirecturi1", "http://server/redirecturi2"]; - requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); - requestUriGrant.Request = requestObjectJson; + requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); + requestUriGrant.Request = requestObjectJson; } - if (String.Equals("response_type is missing", expectedErrorDescription)) + + if (string.Equals("response_type is missing", expectedErrorDescription)) { client.RedirectUris = ["https://dr.dev.cdrsandbox.gov.au/consent/callback", "http://server/redirecturi2"]; - requestObjectJson = requestObjectJson.Replace("code id_token", ""); + requestObjectJson = requestObjectJson.Replace("code", string.Empty); requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); - requestUriGrant.Request = requestObjectJson; + requestUriGrant.Request = requestObjectJson; } - if (String.Equals("response_type is not supported", expectedErrorDescription)) + + if (string.Equals("response_type is not supported", expectedErrorDescription)) { client.RedirectUris = ["https://dr.dev.cdrsandbox.gov.au/consent/callback", "http://server/redirecturi2"]; - requestObjectJson = requestObjectJson.Replace("code id_token", "foo"); + requestObjectJson = requestObjectJson.Replace("code", "foo"); requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); - requestUriGrant.Request = requestObjectJson; + requestUriGrant.Request = requestObjectJson; } - if (String.Equals("response_type does not match request_uri response_type", expectedErrorDescription)) + + if (string.Equals("response_type does not match request_uri response_type", expectedErrorDescription)) { client.RedirectUris = ["https://dr.dev.cdrsandbox.gov.au/consent/callback", "http://server/redirecturi2"]; - requestObjectJson = requestObjectJson.Replace("code id_token", "code"); requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); - requestUriGrant.Request = requestObjectJson; + requestUriGrant.Request = requestObjectJson; + authRequest.Response_type = "foo"; } - if (String.Equals("scope is missing", expectedErrorDescription)) + + if (string.Equals("scope is missing", expectedErrorDescription)) { client.RedirectUris = ["https://dr.dev.cdrsandbox.gov.au/consent/callback", "http://server/redirecturi2"]; - requestObjectJson = requestObjectJson.Replace("openid profile", ""); + requestObjectJson = requestObjectJson.Replace("openid profile", string.Empty); requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); requestUriGrant.Request = requestObjectJson; } - if (String.Equals("openid scope is missing", expectedErrorDescription)) + + if (string.Equals("openid scope is missing", expectedErrorDescription)) { client.RedirectUris = ["https://dr.dev.cdrsandbox.gov.au/consent/callback", "http://server/redirecturi2"]; requestObjectJson = requestObjectJson.Replace("openid profile", "profile"); requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); requestUriGrant.Request = requestObjectJson; } - if (String.Equals("response_mode is not supported", expectedErrorDescription)) + + if (string.Equals("response_mode is not supported", expectedErrorDescription)) { client.RedirectUris = ["https://dr.dev.cdrsandbox.gov.au/consent/callback", "http://server/redirecturi2"]; - requestObjectJson = requestObjectJson.Replace("form_post", "foo"); + requestObjectJson = requestObjectJson.Replace("jwt", "foo"); requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); requestUriGrant.Request = requestObjectJson; } - if (String.Equals("Software product not found", expectedErrorDescription)) + if (string.Equals("Software product not found", expectedErrorDescription)) { - client.RedirectUris = ["https://dr.dev.cdrsandbox.gov.au/consent/callback", "http://server/redirecturi2"]; + client.RedirectUris = ["https://dr.dev.cdrsandbox.gov.au/consent/callback", "http://server/redirecturi2"]; requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); requestUriGrant.Request = requestObjectJson; } - if (String.Equals("Software product status is INACTIVE", expectedErrorDescription)) + if (string.Equals("Software product status is INACTIVE", expectedErrorDescription)) { client.RedirectUris = ["https://dr.dev.cdrsandbox.gov.au/consent/callback", "http://server/redirecturi2"]; requestUriGrant.ExpiresAt = DateTime.UtcNow.AddMinutes(10); requestUriGrant.Request = requestObjectJson; - SoftwareProduct softwareProduct = new SoftwareProduct() { SoftwareProductId = "77773140-cfa2-4073-b248-0eb51540e5c6" , - BrandName = "Foo brand name", - BrandStatus="ACTIVE", Status="ACTIVE", LegalEntityStatus="INACTIVE" }; + var softwareProduct = new SoftwareProduct() + { + SoftwareProductId = "77773140-cfa2-4073-b248-0eb51540e5c6", + BrandName = "Foo brand name", + BrandStatus = "ACTIVE", + Status = "ACTIVE", + LegalEntityStatus = "INACTIVE", + }; - cdrService.Setup(x => x.GetSoftwareProduct(client.SoftwareId)).ReturnsAsync(softwareProduct); + _ = cdrService.Setup(x => x.GetSoftwareProduct(client.SoftwareId)).ReturnsAsync(softwareProduct); } - - - grantService.Setup(x => x.Get(grantType, authRequest.request_uri, authRequest.client_id)).ReturnsAsync(requestUriGrant); - } + _ = grantService.Setup(x => x.Get(grantType, authRequest.Request_uri, authRequest.Client_id)).ReturnsAsync(requestUriGrant); + } } - //Act + // Act var result = await autherizeRequestValidator.Validate(authRequest, configOptions); - //Assert + // Assert Assert.IsNotNull(result); - Assert.AreEqual(false, result.IsValid); + Assert.IsFalse(result.IsValid); Assert.AreEqual(expectedError, result.Error); - Assert.IsTrue(result.ErrorDescription?.Contains(expectedErrorDescription)); + Assert.IsTrue(result.ErrorDescription?.Contains(expectedErrorDescription)); } } } diff --git a/Source/CdrAuthServer.UnitTests/Validators/ClientAssertionValidatorTests.cs b/Source/CdrAuthServer.UnitTests/Validators/ClientAssertionValidatorTests.cs index c5e4c50..d40a675 100644 --- a/Source/CdrAuthServer.UnitTests/Validators/ClientAssertionValidatorTests.cs +++ b/Source/CdrAuthServer.UnitTests/Validators/ClientAssertionValidatorTests.cs @@ -1,4 +1,11 @@ -using CdrAuthServer.Models; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using CdrAuthServer.Extensions; +using CdrAuthServer.Models; using CdrAuthServer.Services; using CdrAuthServer.Validation; using Microsoft.Extensions.Configuration; @@ -6,36 +13,28 @@ using Microsoft.IdentityModel.Tokens; using Moq; using NUnit.Framework; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Security.Cryptography.X509Certificates; -using System; -using System.Threading.Tasks; using static CdrAuthServer.Domain.Constants; -using CdrAuthServer.Extensions; namespace CdrAuthServer.UnitTests.Validators { public class ClientAssertionValidatorTests { - private Mock> logger = null!; + private const string ClientAssertion = @"eyJraWQiOiJCNTQ4QzkxNEEwMjc4N0EzQjVGMTU1ODNDOEVCMDMwRDk0QkMyNDI0IiwiYWxnIjoiUFMyNTYifQ.eyJzdWIiOiIzZTZjNWYzZC1iZDU4LTRhYWEtOGMyMy1hY2ZlYzgzN2I1MDYiLCJhdWQiOiJodHRwczpcL1wvZGgtdGVzdC5pZHAuZGV2LmNkcnNhbmRib3guZ292LmF1XC9kaC1lbmVyZ3ktNSIsImlzcyI6IjNlNmM1ZjNkLWJkNTgtNGFhYS04YzIzLWFjZmVjODM3YjUwNiIsImV4cCI6MTY1MjM0MDUzOCwiaWF0IjoxNjUyMzQwNDc4LCJqdGkiOiJMd3J0YTJLU2RhNGpPWVYwSDVwUiJ9.SjGr9X5vxnYywoVU1GAcG6N4taPniDJPYuEme1wPD2tvNjK4D-huQsb4BuaLJZem1MBbIDZprmvMk8_YkL50qOdvdaFYflqIif6SfFlaAIzN5B-9pzSM3iOC7Q0bt26xjr-C8MZaprc3O7LhsdpSynWIWiqle9I248-quikMsqyTDXhiVm_PtKnDs-DwzdfXvcp4JJcgN4Gk_fb431n2UGeQzFHAT-SCasvDVO7i9Zhw72bS8orWo7-ybiAUFjK8-B38lCih6LZg7mjDQdJWnXmkO4tqTYCIJgVEQteiaxUJRmsPlPX6Uvh0jC22pj3VTqGRIW4yukzeKgtB4q2HyQ"; + public const string SOFTWAREPRODUCT_ID = "c6327f87-687a-4369-99a4-eaacd3bb8210"; + public const string JWT_CERTIFICATE_FILENAME = "Certificates/MDR/jwks.pfx"; + public const string JWT_CERTIFICATE_PASSWORD = "#M0ckDataRecipient#"; + public static string DH_MTLS_GATEWAY_URL; + + private Mock> logger = null!; private Mock clientService = null!; private Mock tokenService = null!; private Mock jwtValidator = null!; - private IConfiguration configuration = null!; private ClientAssertionValidator clientAssertionValidator = null!; - - private const string ClientAssertion = @"eyJraWQiOiJCNTQ4QzkxNEEwMjc4N0EzQjVGMTU1ODNDOEVCMDMwRDk0QkMyNDI0IiwiYWxnIjoiUFMyNTYifQ.eyJzdWIiOiIzZTZjNWYzZC1iZDU4LTRhYWEtOGMyMy1hY2ZlYzgzN2I1MDYiLCJhdWQiOiJodHRwczpcL1wvZGgtdGVzdC5pZHAuZGV2LmNkcnNhbmRib3guZ292LmF1XC9kaC1lbmVyZ3ktNSIsImlzcyI6IjNlNmM1ZjNkLWJkNTgtNGFhYS04YzIzLWFjZmVjODM3YjUwNiIsImV4cCI6MTY1MjM0MDUzOCwiaWF0IjoxNjUyMzQwNDc4LCJqdGkiOiJMd3J0YTJLU2RhNGpPWVYwSDVwUiJ9.SjGr9X5vxnYywoVU1GAcG6N4taPniDJPYuEme1wPD2tvNjK4D-huQsb4BuaLJZem1MBbIDZprmvMk8_YkL50qOdvdaFYflqIif6SfFlaAIzN5B-9pzSM3iOC7Q0bt26xjr-C8MZaprc3O7LhsdpSynWIWiqle9I248-quikMsqyTDXhiVm_PtKnDs-DwzdfXvcp4JJcgN4Gk_fb431n2UGeQzFHAT-SCasvDVO7i9Zhw72bS8orWo7-ybiAUFjK8-B38lCih6LZg7mjDQdJWnXmkO4tqTYCIJgVEQteiaxUJRmsPlPX6Uvh0jC22pj3VTqGRIW4yukzeKgtB4q2HyQ"; - public const string SOFTWAREPRODUCT_ID = "c6327f87-687a-4369-99a4-eaacd3bb8210"; - public const string JWT_CERTIFICATE_FILENAME = "Certificates/MDR/jwks.pfx"; - public const string JWT_CERTIFICATE_PASSWORD = "#M0ckDataRecipient#"; - public static string DH_MTLS_GATEWAY_URL; [SetUp] public void Setup() - { + { logger = new Mock>(); clientService = new Mock(); tokenService = new Mock(); @@ -46,50 +45,50 @@ public void Setup() .AddEnvironmentVariables() .Build(); - DH_MTLS_GATEWAY_URL = configuration["DHSecureInfosecBaseUri:DH_MTLS_Gateway"] ?? ""; + DH_MTLS_GATEWAY_URL = configuration["DHSecureInfosecBaseUri:DH_MTLS_Gateway"] ?? string.Empty; clientAssertionValidator = new ClientAssertionValidator(logger.Object, clientService.Object, tokenService.Object, jwtValidator.Object); } - [TestCase("", "", "", "", "", false, false, "client_assertion not provided", ErrorCodes.Generic.InvalidClient)] [TestCase("foo", "", "", "", "", false, false, "client_assertion_type not provided", ErrorCodes.Generic.InvalidClient)] [TestCase("foo", "foo", "", "", "", false, false, "client_assertion_type must be urn:ietf:params:oauth:client-assertion-type:jwt-bearer", ErrorCodes.Generic.InvalidClient)] [TestCase("foo", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "", "", "", true, false, "grant_type not provided", ErrorCodes.Generic.UnsupportedGrantType)] [TestCase("foo", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "", "foo", "", true, false, "unsupported grant_type", ErrorCodes.Generic.UnsupportedGrantType)] - [TestCase("foo", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "", "authorization_code", "", true, false, "Cannot read client_assertion. Invalid format.", ErrorCodes.Generic.InvalidClient)] //grant type: "authorization_code", "client_credentials", "refresh_token" + [TestCase("foo", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "", "authorization_code", "", true, false, "Cannot read client_assertion. Invalid format.", ErrorCodes.Generic.InvalidClient)] // grant type: "authorization_code", "client_credentials", "refresh_token" [TestCase(ClientAssertion, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "", "authorization_code", "", true, false, "Client not found", ErrorCodes.Generic.InvalidClient)] [TestCase("validate_assertion1", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "", "authorization_code", "", true, false, "Client not found", ErrorCodes.Generic.InvalidClient)] [TestCase("validate_assertion2", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "", "authorization_code", "", true, false, "Client not found", ErrorCodes.Generic.InvalidClient)] - //[TestCase("validate_assertion3", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", SOFTWAREPRODUCT_ID, "authorization_code", "", true, false, "Client not found", ErrorCodes.Generic.InvalidClient)] + + // [TestCase("validate_assertion3", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", SOFTWAREPRODUCT_ID, "authorization_code", "", true, false, "Client not found", ErrorCodes.Generic.InvalidClient)] public async Task Validate_ClientAssertionRequest_InvalidClient_Test( - string? clientAssertion, - string? clientAssertionType, - string? clientId, - string? grantType, - string? scope, + string? clientAssertion, + string? clientAssertionType, + string? clientId, + string? grantType, + string? scope, bool isTokenEndpoint, - bool isValid, - string expectedErrorDescription, - string expectedError) + bool isValid, + string expectedErrorDescription, + string expectedError) { - //Arrange + // Arrange var configOptions = this.configuration.GetConfigurationOptions(); var clientAssertionRequest = new ClientAssertionRequest() { - ClientAssertion = clientAssertion, - ClientAssertionType = clientAssertionType , - ClientId = clientId, - GrantType = grantType, - Scope = scope, + ClientAssertion = clientAssertion, + ClientAssertionType = clientAssertionType, + ClientId = clientId, + GrantType = grantType, + Scope = scope, }; var errorFailure = ValidationResult.Fail(expectedError, expectedErrorDescription); - if (string.Equals("validate_assertion1", clientAssertionRequest.ClientAssertion) || - string.Equals("validate_assertion2", clientAssertionRequest.ClientAssertion) || + if (string.Equals("validate_assertion1", clientAssertionRequest.ClientAssertion) || + string.Equals("validate_assertion2", clientAssertionRequest.ClientAssertion) || string.Equals("validate_assertion3", clientAssertionRequest.ClientAssertion)) - { + { if (string.Equals("validate_assertion2", clientAssertionRequest.ClientAssertion)) { clientService.Setup(x => x.Get(clientId)).ReturnsAsync(value: null as CdrAuthServer.Models.Client); @@ -108,7 +107,7 @@ public async Task Validate_ClientAssertionRequest_InvalidClient_Test( var additionalClaims = new List { new Claim("sub", ISSUER), - new Claim("iat", new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer) + new Claim("iat", new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer), }; var expires = now.AddMinutes(10); @@ -128,17 +127,16 @@ public async Task Validate_ClientAssertionRequest_InvalidClient_Test( var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); clientAssertionRequest.ClientAssertion = jwtSecurityTokenHandler.WriteToken(jwt); - } - //Act + // Act var result = await clientAssertionValidator.ValidateClientAssertionRequest(clientAssertionRequest, configOptions, isTokenEndpoint); - - //Assert + + // Assert Assert.IsNotNull(result); - Assert.AreEqual(result.Item1.IsValid, isValid); - Assert.AreEqual(result.Item1.Error, errorFailure.Error); - Assert.IsTrue(result.Item1.ErrorDescription?.Contains(expectedErrorDescription)); + Assert.AreEqual(result.Result.IsValid, isValid); + Assert.AreEqual(result.Result.Error, errorFailure.Error); + Assert.IsTrue(result.Result.ErrorDescription?.Contains(expectedErrorDescription)); } } } diff --git a/Source/CdrAuthServer.UnitTests/Validators/ClientRegistrationValidatorTests.cs b/Source/CdrAuthServer.UnitTests/Validators/ClientRegistrationValidatorTests.cs index 0f06cea..4f01b7b 100644 --- a/Source/CdrAuthServer.UnitTests/Validators/ClientRegistrationValidatorTests.cs +++ b/Source/CdrAuthServer.UnitTests/Validators/ClientRegistrationValidatorTests.cs @@ -20,7 +20,6 @@ public class ClientRegistrationValidatorTests : BaseTest private IConfiguration configuration = null!; private Mock httpContext = null!; - public SoftwareProduct softwareProduct { get; private set; } = null!; private IClientRegistrationValidator? clientRegistrationValidator = null; @@ -40,79 +39,82 @@ public void Setup() .Build(); softwareProduct = new SoftwareProduct(); - softwareProduct.SoftwareProductId = configuration[SOFTWARE_PRODUCT_ID_KEY] ?? ""; - softwareProduct.BrandId = configuration[BRAND_ID_KEY] ?? ""; + softwareProduct.SoftwareProductId = configuration[SOFTWARE_PRODUCT_ID_KEY] ?? string.Empty; + softwareProduct.BrandId = configuration[BRAND_ID_KEY] ?? string.Empty; clientRegistrationValidator = new ClientRegistrationValidator(configuration, logger.Object, jwksService.Object, httpContext.Object); } - - //TODO - // Assert - Validate SSA Signature + // TODO + // Assert - Validate SSA Signature [TestCase("empty_client_registration", false, "Registration request is empty", ErrorCodes.Generic.InvalidClientMetadata)] [TestCase("SSA_validation_without_SS", false, "The software_statement is empty or invalid", ErrorCodes.Generic.InvalidSoftwareStatement)] - [TestCase("SSA_validation_with_SS", false, "Could not load SSA JWKS from Register endpoint: "+ JWKS_URI, ErrorCodes.Generic.InvalidSoftwareStatement)] - [TestCase("SSA_validation_with_SS_and_Null_JWKS", false, "Could not load SSA JWKS from Register endpoint: "+ JWKS_URI, ErrorCodes.Generic.InvalidSoftwareStatement)] - [TestCase("SSA_validation_with_SS_and_JWKS", false, "SSA validation failed.", ErrorCodes.Generic.InvalidSoftwareStatement)] - public async Task Validate_ClientRegistrationRequest_InvalidClient_Test(string testCaseType, bool isvalid, - string expectErrorDescription, - string expectedError) + [TestCase("SSA_validation_with_SS", false, "Could not load SSA JWKS from Register endpoint: " + JWKS_URI, ErrorCodes.Generic.InvalidSoftwareStatement)] + [TestCase("SSA_validation_with_SS_and_Null_JWKS", false, "Could not load SSA JWKS from Register endpoint: " + JWKS_URI, ErrorCodes.Generic.InvalidSoftwareStatement)] + [TestCase("SSA_validation_with_SS_and_JWKS", false, "SSA validation failed.", ErrorCodes.Generic.InvalidSoftwareStatement)] + public async Task Validate_ClientRegistrationRequest_InvalidClient_Test(string testCaseType, bool isvalid, + string expectErrorDescription, + string expectedError) { - //Arrange + // Arrange var configOptions = this.configuration.GetConfigurationOptions(); ClientRegistrationRequest? clientRegistrationRequest = null; - if (String.Equals("empty_client_registration", testCaseType)) + if (string.Equals("empty_client_registration", testCaseType)) { clientRegistrationRequest = null; } - if (String.Equals("SSA_validation_without_SS", testCaseType)) + + if (string.Equals("SSA_validation_without_SS", testCaseType)) { clientRegistrationRequest = new ClientRegistrationRequest(GetJwtToken()); } - if (String.Equals("SSA_validation_with_SS", testCaseType)) + + if (string.Equals("SSA_validation_with_SS", testCaseType)) { - clientRegistrationRequest = new ClientRegistrationRequest(GetJwtToken(requireSS:true)); - //SoftwareStatementJwt is readonly - //clientRegistrationRequest.SoftwareStatementJwt; + clientRegistrationRequest = new ClientRegistrationRequest(GetJwtToken(requireSS: true)); - var softwareStatement = new SoftwareStatement(clientRegistrationRequest.SoftwareStatementJwt ?? ""); + // SoftwareStatementJwt is readonly + // clientRegistrationRequest.SoftwareStatementJwt; + var softwareStatement = new SoftwareStatement(clientRegistrationRequest.SoftwareStatementJwt ?? string.Empty); _ = softwareStatement.ValidFrom; _ = softwareStatement.ValidTo; - clientRegistrationRequest.SoftwareStatement = softwareStatement; + clientRegistrationRequest.SoftwareStatement = softwareStatement; } - if (String.Equals("SSA_validation_with_SS_and_Null_JWKS", testCaseType)) + + if (string.Equals("SSA_validation_with_SS_and_Null_JWKS", testCaseType)) { - clientRegistrationRequest = new ClientRegistrationRequest(GetJwtToken(requireSS: true)); - var softwareStatement = new SoftwareStatement(clientRegistrationRequest.SoftwareStatementJwt ?? ""); + clientRegistrationRequest = new ClientRegistrationRequest(GetJwtToken(requireSS: true)); + var softwareStatement = new SoftwareStatement(clientRegistrationRequest.SoftwareStatementJwt ?? string.Empty); _ = softwareStatement.ValidFrom; _ = softwareStatement.ValidTo; clientRegistrationRequest.SoftwareStatement = softwareStatement; - + var jwksURI = new Uri(JWKS_URI); jwksService.Setup(x => x.GetJwks(jwksURI)).ReturnsAsync(value: null as Microsoft.IdentityModel.Tokens.JsonWebKeySet); } - if (String.Equals("SSA_validation_with_SS_and_JWKS", testCaseType)) + + if (string.Equals("SSA_validation_with_SS_and_JWKS", testCaseType)) { clientRegistrationRequest = new ClientRegistrationRequest(GetJwtToken(requireSS: true)); - var softwareStatement = new SoftwareStatement(clientRegistrationRequest.SoftwareStatementJwt ?? ""); + var softwareStatement = new SoftwareStatement(clientRegistrationRequest.SoftwareStatementJwt ?? string.Empty); _ = softwareStatement.ValidFrom; _ = softwareStatement.ValidTo; clientRegistrationRequest.SoftwareStatement = softwareStatement; - + var jwksURI = new Uri(JWKS_URI); - Microsoft.IdentityModel.Tokens.JsonWebKeySet jsonWebKeySet = new Microsoft.IdentityModel.Tokens.JsonWebKeySet(); + Microsoft.IdentityModel.Tokens.JsonWebKeySet jsonWebKeySet = new Microsoft.IdentityModel.Tokens.JsonWebKeySet(); jsonWebKeySet.Keys.Add(new Microsoft.IdentityModel.Tokens.JsonWebKey()); jwksService.Setup(x => x.GetJwks(jwksURI, softwareStatement.Header["kid"].ToString())).ReturnsAsync(jsonWebKeySet); } - //Act + // Act var result = await clientRegistrationValidator.Validate(clientRegistrationRequest, configOptions); - //Assert + // Assert Assert.IsNotNull(result); Assert.AreEqual(result.IsValid, isvalid); - Assert.AreEqual(result.Error, expectedError); + Assert.AreEqual(result.Error, expectedError); Assert.IsTrue(result.ErrorDescription?.Contains(expectErrorDescription)); } } diff --git a/Source/CdrAuthServer.UnitTests/Validators/JwtValidatorTests.cs b/Source/CdrAuthServer.UnitTests/Validators/JwtValidatorTests.cs index d798d93..7842219 100644 --- a/Source/CdrAuthServer.UnitTests/Validators/JwtValidatorTests.cs +++ b/Source/CdrAuthServer.UnitTests/Validators/JwtValidatorTests.cs @@ -16,21 +16,20 @@ namespace CdrAuthServer.UnitTests.Validators { public class JwtValidatorTest : BaseTest { - private Mock> logger = null!; + private Mock> logger = null!; private Mock clientService = null!; - private IConfiguration configuration = null!; + private IConfiguration configuration = null!; private IJwtValidator JwtValidator = null!; private (ValidationResult, JwtSecurityToken) mockJwtValidatorResult; public Client client { get; private set; } = null!; - [SetUp] public void Setup() { logger = new Mock>(); clientService = new Mock(); - + configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.UnitTest.json") .AddEnvironmentVariables() @@ -38,47 +37,47 @@ public void Setup() JwtValidator = new JwtValidator(logger.Object, configuration, clientService.Object); } - - - [TestCase("jwt_validator_invalid_token", "foo", "", false, "request - token validation error", ErrorCodes.Generic.InvalidClient)] - public async Task Validate_Jwt_Validator_InvalidClient_Test(string testCaseType, string client_id, string jwt, - bool isvalid, - string expectErrorDescription, - string expectedError) + + [TestCase("jwt_validator_invalid_token", "foo", "", false, "ERR-JWT-004: request - token validation error", ErrorCodes.Generic.InvalidClient)] + public async Task Validate_Jwt_Validator_InvalidClient_Test( + string testCaseType, + string client_id, + string jwt, + bool isvalid, + string expectErrorDescription, + string expectedError) { - //Arrange + // Arrange var configOptions = this.configuration.GetConfigurationOptions(); List? audiences = null; List? validAlgos = null; - if (String.Equals("jwt_validator_invalid_token", testCaseType)) + if (string.Equals("jwt_validator_invalid_token", testCaseType)) { jwt = GetJwtToken(); GetClient(client_id); - audiences = new List() { "https://localhost:8081", "https://localhost:8082/connect/token" }; - validAlgos = new List() { Algorithms.Signing.PS256, Algorithms.Signing.ES256 }; - ValidationResult validationResult = new ValidationResult(isvalid); - mockJwtValidatorResult = (validationResult, GetJwt()); + audiences = ["https://localhost:8081", "https://localhost:8082/connect/token"]; + validAlgos = [Algorithms.Signing.PS256, Algorithms.Signing.ES256]; + ValidationResult validationResult = new ValidationResult(isvalid); + mockJwtValidatorResult = (validationResult, GetJwt()); } - - //Act - var result = await JwtValidator.Validate(jwt, client, JwtValidationContext.request, configOptions, audiences, validAlgos); + // Act + var result = await JwtValidator.Validate(jwt, client, JwtValidationContext.Request, configOptions, audiences, validAlgos); - //Assert + // Assert Assert.IsNotNull(result); - Assert.AreEqual(result.Item1.IsValid, isvalid); - Assert.AreEqual(result.Item1.Error, expectedError); - Assert.IsTrue(result.Item1.ErrorDescription.Contains(expectErrorDescription)); + Assert.AreEqual(result.ValidationResult.IsValid, isvalid); + Assert.AreEqual(result.ValidationResult.Error, expectedError); + Assert.IsTrue(result.ValidationResult.ErrorDescription?.Contains(expectErrorDescription)); } private void GetClient(string client_id) { client = new Client { - ClientId = client_id + ClientId = client_id, }; clientService.Setup(x => x.Get(client_id)).ReturnsAsync(client); } } } - diff --git a/Source/CdrAuthServer.UnitTests/Validators/ParValidatorTest.cs b/Source/CdrAuthServer.UnitTests/Validators/ParValidatorTest.cs new file mode 100644 index 0000000..a83612f --- /dev/null +++ b/Source/CdrAuthServer.UnitTests/Validators/ParValidatorTest.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Threading.Tasks; +using CdrAuthServer.Configuration; +using CdrAuthServer.Extensions; +using CdrAuthServer.Models; +using CdrAuthServer.Services; +using CdrAuthServer.Validation; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using static CdrAuthServer.Domain.Constants; + +namespace CdrAuthServer.UnitTests.Validators +{ + public class ParValidatorTest : BaseTest + { + private Mock> logger = null!; + private Mock clientService = null!; + + private Mock jwtValidator = null!; + + private IConfiguration configuration = null!; + + private ParValidator parValidator = null!; + + public Client client { get; private set; } = null!; + + private (ValidationResult, JwtSecurityToken) mockJwtValidatorResult; + + [SetUp] + public void Setup() + { + logger = new Mock>(); + + clientService = new Mock(); + jwtValidator = new Mock(); + + var grantService = new Mock(); + var requestObjectValidatorLogger = new Mock>(); + var requestObjectValidator = new RequestObjectValidator(requestObjectValidatorLogger.Object, clientService.Object, grantService.Object); + + configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.UnitTest.json") + .AddEnvironmentVariables() + .Build(); + + parValidator = new ParValidator(logger.Object, jwtValidator.Object, requestObjectValidator, clientService.Object); + } + + [TestCase("par_request_client_id_missing", "", "", false, "request is not a well-formed JWT", ErrorCodes.Generic.InvalidRequest)] + [TestCase("par_request_client_invalid_jwt", "foo", "", false, "", ErrorCodes.Generic.InvalidRequestObject)] + [TestCase("par_request_client_jwt_validator", "foo", "", false, "", ErrorCodes.Generic.InvalidRequestObject)] + public async Task Validate_Par_Request_InvalidClient_Test( + string testCaseType, + string client_id, + string requestObject, + bool isvalid, + string expectErrorDescription, + string expectedError) + { + // Arrange + var configOptions = this.configuration.GetConfigurationOptions(); + if (string.Equals("par_request_client_invalid_jwt", testCaseType)) + { + requestObject = GetJwtToken(); + GetClient(client_id); + } + + if (string.Equals("par_request_client_jwt_validator", testCaseType)) + { + requestObject = GetJwtToken(); + GetClient(client_id); + + var audiences = new List() { "https://localhost:8081", "https://localhost:8082/connect/token" }; + var validAlgos = new List() { "PS256", "ES256" }; + ValidationResult validationResult = new(isvalid); + + mockJwtValidatorResult = (validationResult, GetJwt()); + jwtValidator.Setup(x => x.Validate(requestObject, client, JwtValidationContext.Request, configOptions, audiences, validAlgos)).ReturnsAsync(mockJwtValidatorResult); + } + + // Act + var result = await parValidator.Validate(client_id, requestObject, configOptions); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(result.ValidationResult.IsValid, isvalid); + Assert.AreEqual(result.ValidationResult.Error, expectedError); + Assert.IsTrue(result.ValidationResult.ErrorDescription?.Contains(expectErrorDescription)); + } + + [TestCase("valid_claims_json", "{\"sharing_duration\":30000,\"id_token\":{\"acr\":{\"essential\":true,\"values\":[\"urn:cds.au:cdr:3\"]}}}", true)] + [TestCase("invalid_claims_json_malformed", "{\"sharing_duration\":30000,\"id_token\":{\"acr\":{\"essential\":true,\"values\":[\"urn:cds.au:cdr:3\"]}}", false)] + [TestCase("invalid_claims_missing_id_token", "{\"sharing_duration\":30000}", false)] + [TestCase("invalid_claims_missing_id_token_acr", "{\"sharing_duration\":30000,\"id_token\":{}}", false)] + [TestCase("invalid_claims_missing_response_mode", "{\"sharing_duration\":30000,\"id_token\":{\"acr\":{\"essential\":true,\"values\":[\"urn:cds.au:cdr:3\"]}}}", false, "")] + public async Task Validate_Par_Request_ValidateClaimsJsonString_Test(string testCaseType, string claimsString, bool isValid, string responseMode = ResponseModes.Jwt) + { + // Arrange + var requestObject = GetJwtToken(); // This just needs to be in correct format and the data inside doesn't matter. + var configOptions = this.configuration.GetConfigurationOptions(); + var clienId = "client_id"; + var responseType = "code"; + var redirectUri = "https://redirect.uri"; + GetClient(clienId); + client.RedirectUris = [redirectUri]; + client.ResponseTypes = [responseType]; + + // Add additional claims that are required for the validations. + Claim[] claims = + [ + new Claim(ClaimNames.ResponseType, responseType), + new Claim(ClaimNames.ResponseMode, responseMode), + new Claim(ClaimNames.CodeChallengeMethod, "S256"), + new Claim(ClaimNames.CodeChallenge, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"), // Any string with length to be between 43 - 128 + new Claim(ClaimNames.Scope, "openid"), // Minimum required scope + new Claim(ClaimNames.Nonce, "nonce"), + new Claim(ClaimNames.Claims, claimsString), + ]; + ValidationResult validationResult = new(true); + mockJwtValidatorResult = (validationResult, GetJwt(clienId, redirectUri, true, claims)); + + // Mock supporting methods + jwtValidator + .Setup(x => x.Validate(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>())) + .ReturnsAsync(mockJwtValidatorResult); + clientService.Setup(x => x.Get(It.IsAny())).ReturnsAsync(client); + + // Act + var result = await parValidator.Validate(clienId, requestObject, configOptions); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(result.ValidationResult.IsValid, isValid); + } + + private void GetClient(string client_id) + { + client = new Client(); + client.ClientId = client_id; + clientService.Setup(x => x.Get(client_id)).ReturnsAsync(client); + } + } +} diff --git a/Source/CdrAuthServer.UnitTests/Validators/ParValidatorTests.cs b/Source/CdrAuthServer.UnitTests/Validators/ParValidatorTests.cs deleted file mode 100644 index 687b850..0000000 --- a/Source/CdrAuthServer.UnitTests/Validators/ParValidatorTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -using CdrAuthServer.Extensions; -using CdrAuthServer.Models; -using CdrAuthServer.Services; -using CdrAuthServer.Validation; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Threading.Tasks; -using static CdrAuthServer.Domain.Constants; - -namespace CdrAuthServer.UnitTests.Validators -{ - public class ParValidatorTest : BaseTest - { - private Mock> logger = null!; - private Mock clientService = null!; - - private Mock jwtValidator = null!; - private Mock requestObjectValidator = null!; - - private IConfiguration configuration = null!; - - private ParValidator parValidator = null!; - - public Client client { get; private set; } = null!; - private (ValidationResult, JwtSecurityToken) mockJwtValidatorResult; - - [SetUp] - public void Setup() - { - logger = new Mock>(); - - clientService = new Mock(); - jwtValidator = new Mock(); - requestObjectValidator = new Mock(); - - configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.UnitTest.json") - .AddEnvironmentVariables() - .Build(); - - parValidator = new ParValidator(logger.Object, jwtValidator.Object, requestObjectValidator.Object, clientService.Object); - } - - [TestCase("par_request_client_id_missing", "", "", false, "request is not a well-formed JWT", ErrorCodes.Generic.InvalidRequest)] - [TestCase("par_request_client_invalid_jwt", "foo", "", false, "", ErrorCodes.Generic.InvalidRequestObject)] - [TestCase("par_request_client_jwt_validator", "foo", "", false, "", ErrorCodes.Generic.InvalidRequestObject)] - public async Task Validate_Par_Request_InvalidClient_Test(string testCaseType, string client_id, string requestObject, - bool isvalid, - string expectErrorDescription, - string expectedError) - { - //Arrange - var configOptions = this.configuration.GetConfigurationOptions(); - if (String.Equals("par_request_client_invalid_jwt", testCaseType)) - { - requestObject = GetJwtToken(); - GetClient(client_id); - } - if (String.Equals("par_request_client_jwt_validator", testCaseType)) - { - requestObject = GetJwtToken(); - GetClient(client_id); - - var audiences = new List() { "https://localhost:8081", "https://localhost:8082/connect/token" }; - var validAlgos = new List() { "PS256", "ES256" }; - ValidationResult validationResult = new(isvalid); - - mockJwtValidatorResult = (validationResult, GetJwt()); - jwtValidator.Setup(x => x.Validate(requestObject, client, JwtValidationContext.request, configOptions, audiences, validAlgos)).ReturnsAsync(mockJwtValidatorResult); - } - - //Act - var result = await parValidator.Validate(client_id, requestObject, configOptions); - - //Assert - Assert.IsNotNull(result); - Assert.AreEqual(result.Item1.IsValid, isvalid); - Assert.AreEqual(result.Item1.Error, expectedError); - Assert.IsTrue(result.Item1.ErrorDescription?.Contains(expectErrorDescription)); - } - - private void GetClient(string client_id) - { - client = new Client(); - client.ClientId = client_id; - clientService.Setup(x => x.Get(client_id)).ReturnsAsync(client); - } - } -} diff --git a/Source/CdrAuthServer.UnitTests/Validators/RequestObjectValidatorTests.cs b/Source/CdrAuthServer.UnitTests/Validators/RequestObjectValidatorTests.cs index 3febc53..423d44d 100644 --- a/Source/CdrAuthServer.UnitTests/Validators/RequestObjectValidatorTests.cs +++ b/Source/CdrAuthServer.UnitTests/Validators/RequestObjectValidatorTests.cs @@ -1,4 +1,8 @@ -using CdrAuthServer.Extensions; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Threading.Tasks; +using CdrAuthServer.Extensions; using CdrAuthServer.Models; using CdrAuthServer.Services; using CdrAuthServer.Validation; @@ -6,23 +10,18 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Threading.Tasks; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.UnitTests.Validators { public class RequestObjectValidatorTest : BaseTest { - private Mock> logger = null!; + private Mock> logger = null!; private Mock clientService = null!; private Mock grantService = null!; private IConfiguration configuration = null!; - + private RequestObjectValidator requestObjectValidator = null!; [SetUp] @@ -40,65 +39,69 @@ public void Setup() requestObjectValidator = new RequestObjectValidator(logger.Object, clientService.Object, grantService.Object); } - - [TestCase("missing_client_id","foo", false, "client_id is missing", ErrorCodes.Generic.InvalidRequest)] - [TestCase("jwt_test_case1", "foo", false, "client_id does not match client_id in request object JWT", ErrorCodes.Generic.UnauthorizedClient)] + + [TestCase("missing_client_id", "foo", false, "client_id is missing", ErrorCodes.Generic.InvalidRequest)] + [TestCase("jwt_test_case1", "foo", false, "client_id does not match client_id in request object JWT", ErrorCodes.Generic.UnauthorizedClient)] [TestCase("jwt_test_case2", "foo", false, "redirect_uri missing from request object JWT", ErrorCodes.Generic.InvalidRequestObject)] [TestCase("jwt_test_case3", "foo", false, "Invalid redirect_uri", ErrorCodes.Generic.InvalidRequest)] [TestCase("jwt_test_case4", "foo", false, "Invalid redirect_uri for client", ErrorCodes.Generic.InvalidRequest)] [TestCase("jwt_test_redirect_uri_match", "foo", false, "Invalid request - nbf is missing", ErrorCodes.Generic.InvalidRequestObject)] [TestCase("jwt_test_redirect_uri_match_with_nbf", "foo", false, "response_type is missing", ErrorCodes.Generic.InvalidRequest)] public async Task Validate_RequestObject_InvalidClient_Test( - string testCaseType, - string client_id, + string testCaseType, + string client_id, bool isvalid, - string expectErrorDescription, - string expectedError) + string expectErrorDescription, + string expectedError) { - //Arrange + // Arrange var configOptions = this.configuration.GetConfigurationOptions(); JwtSecurityToken requestObject = new JwtSecurityToken(); - if (String.Equals("jwt_test_case1", testCaseType)) + if (string.Equals("jwt_test_case1", testCaseType)) { requestObject = GetJwt(client_id: "unknown"); } - if (String.Equals("jwt_test_case2", testCaseType)) + + if (string.Equals("jwt_test_case2", testCaseType)) { requestObject = GetJwt(client_id: client_id); } - if (String.Equals("jwt_test_case3", testCaseType)) + + if (string.Equals("jwt_test_case3", testCaseType)) { - requestObject = GetJwt(client_id: client_id, RedirectUriValue: "foo/cburi") ; + requestObject = GetJwt(client_id: client_id, RedirectUriValue: "foo/cburi"); } - if (String.Equals("jwt_test_case4", testCaseType)) + + if (string.Equals("jwt_test_case4", testCaseType)) { requestObject = GetJwt(client_id, RedirectUriValue: "https://foo/cb"); } - if (String.Equals("jwt_test_redirect_uri_match", testCaseType)) + + if (string.Equals("jwt_test_redirect_uri_match", testCaseType)) { var client = new Client(); client.RedirectUris = new List() { "https://foo/cb" }.AsEnumerable(); - requestObject = GetJwt(client_id, RedirectUriValue: "https://foo/cb" ); + requestObject = GetJwt(client_id, RedirectUriValue: "https://foo/cb"); clientService.Setup(x => x.Get(client_id)).ReturnsAsync(client); } - if (String.Equals("jwt_test_redirect_uri_match_with_nbf", testCaseType)) + + if (string.Equals("jwt_test_redirect_uri_match_with_nbf", testCaseType)) { var client = new Client(); client.RedirectUris = new List() { "https://foo/cb" }.AsEnumerable(); - requestObject = GetJwt(client_id, RedirectUriValue: "https://foo/cb", isNbf:true); + requestObject = GetJwt(client_id, RedirectUriValue: "https://foo/cb", isNbf: true); clientService.Setup(x => x.Get(client_id)).ReturnsAsync(client); } - - //Act + // Act var result = await requestObjectValidator.Validate(client_id, requestObject, configOptions); - //Assert + // Assert Assert.IsNotNull(result); - Assert.AreEqual(isvalid, result.Item1.IsValid); - Assert.AreEqual(expectedError, result.Item1.Error); - Assert.IsTrue(result.Item1.ErrorDescription?.Contains(expectErrorDescription)); + Assert.AreEqual(isvalid, result.ValidationResult.IsValid); + Assert.AreEqual(expectedError, result.ValidationResult.Error); + Assert.IsTrue(result.ValidationResult.ErrorDescription?.Contains(expectErrorDescription)); } } } diff --git a/Source/CdrAuthServer.UnitTests/Validators/TokenRequestValidatorTests.cs b/Source/CdrAuthServer.UnitTests/Validators/TokenRequestValidatorTests.cs index e10612a..9511d53 100644 --- a/Source/CdrAuthServer.UnitTests/Validators/TokenRequestValidatorTests.cs +++ b/Source/CdrAuthServer.UnitTests/Validators/TokenRequestValidatorTests.cs @@ -14,14 +14,14 @@ namespace CdrAuthServer.UnitTests.Validators { public class TokenRequestValidatorTest : BaseTest { - private Mock> logger = null!; + private Mock> logger = null!; private Mock clientService = null!; private Mock grantService = null!; private Mock tokenService = null!; private Mock cdrService = null!; private IConfiguration configuration = null!; - + private TokenRequestValidator TokenRequestValidator = null!; [SetUp] @@ -41,12 +41,12 @@ public void Setup() TokenRequestValidator = new TokenRequestValidator(grantService.Object, tokenService.Object, clientService.Object, cdrService.Object); } - + [TestCase("token_client_id_missing", "", "", "", "", "", false, "client_id is missing", ErrorCodes.Generic.InvalidRequest)] [TestCase("token_is_missing", "foo", "", "", "", "", false, "invalid token request", ErrorCodes.Generic.InvalidRequest)] [TestCase("token_granttype_missing", "foo", "", "", "", "", false, "grant_type is missing", ErrorCodes.Generic.InvalidRequest)] [TestCase("token_granttype_missing", "foo", "foo", "", "", "", false, "unsupported grant_type", ErrorCodes.Generic.UnsupportedGrantType)] - [TestCase("token_granttype_supported", "foo", "refresh_token", "", "", "", false, "Could not retrieve client metadata", ErrorCodes.Generic.InvalidRequest)] //authorization_code refresh_token client_credentials + [TestCase("token_granttype_supported", "foo", "refresh_token", "", "", "", false, "Could not retrieve client metadata", ErrorCodes.Generic.InvalidRequest)] // authorization_code refresh_token client_credentials [TestCase("token_client_id_unmatched", "foo", "refresh_token", "", "", "", false, "client_id does not match", ErrorCodes.Generic.InvalidRequest)] [TestCase("token_software_product_id_empty", "foo", "refresh_token", "", "", "", false, "Could not retrieve client metadata", ErrorCodes.Generic.InvalidRequest)] [TestCase("token_software_product_id_foo", "foo", "refresh_token", "foo", "", "", false, "Software product not found", ErrorCodes.Generic.InvalidClient)] @@ -55,97 +55,109 @@ public void Setup() [TestCase("token_redirect_uri_missing", "foo", "authorization_code", "foo", "ACTIVE", "foo", false, "redirect_uri is missing", ErrorCodes.Generic.InvalidRequest)] [TestCase("token_code_verifier_missing", "foo", "authorization_code", "foo", "ACTIVE", "foo", false, "code_verifier is missing", ErrorCodes.Generic.InvalidGrant)] [TestCase("token_code_verifier_foo", "foo", "authorization_code", "foo", "ACTIVE", "foo", false, "authorization code is invalid", ErrorCodes.Generic.InvalidGrant)] - [TestCase("token_grant_expired", "foo", "authorization_code", "foo", "ACTIVE", "foo", false, "authorization code has expired", ErrorCodes.Generic.InvalidGrant)] - [TestCase("token_refresh_token_missing", "foo", "refresh_token", "foo", "ACTIVE", "", false, "refresh_token is missing", ErrorCodes.Generic.InvalidGrant)] - public async Task Validate_Token_Request_InvalidClient_Test(string testCaseType, string client_id, string grant_type, string softwareProductId, string brandStatus, string code, - bool isvalid, - string expectErrorDescription, - string expectedError) + [TestCase("token_grant_expired", "foo", "authorization_code", "foo", "ACTIVE", "foo", false, "authorization code has expired", ErrorCodes.Generic.InvalidGrant)] + [TestCase("token_refresh_token_missing", "foo", "refresh_token", "foo", "ACTIVE", "", false, "refresh_token is missing", ErrorCodes.Generic.InvalidGrant)] + public async Task Validate_Token_Request_InvalidClient_Test( + string testCaseType, + string client_id, + string grant_type, + string softwareProductId, + string brandStatus, + string code, + bool isvalid, + string expectErrorDescription, + string expectedError) { - //Arrange + // Arrange var configOptions = this.configuration.GetConfigurationOptions(); TokenRequest? tokenRequest = new() { - grant_type = grant_type, + Grant_type = grant_type, }; - - if (String.Equals("token_is_missing", testCaseType)) + + if (string.Equals("token_is_missing", testCaseType)) { tokenRequest = null; - } - if (String.Equals("token_client_id_unmatched", testCaseType)) + } + + if (string.Equals("token_client_id_unmatched", testCaseType)) { - tokenRequest!.client_id = "unknown"; + tokenRequest!.Client_id = "unknown"; } - if (String.Equals("token_software_product_id_empty", testCaseType) || String.Equals("token_software_product_id_foo", testCaseType)) + + if (string.Equals("token_software_product_id_empty", testCaseType) || string.Equals("token_software_product_id_foo", testCaseType)) { GetClient(client_id, softwareProductId); } - - if (String.Equals("token_software_product_inactive", testCaseType)) - { - var softwareProduct = new SoftwareProduct(); + + if (string.Equals("token_software_product_inactive", testCaseType)) + { + _ = new SoftwareProduct(); GetSoftwareProduct(softwareProductId, brandStatus); GetClient(client_id, softwareProductId); } - if (String.Equals("token_request_code_missing", testCaseType) || String.Equals("token_redirect_uri_missing", testCaseType)) + + if (string.Equals("token_request_code_missing", testCaseType) || string.Equals("token_redirect_uri_missing", testCaseType)) { - tokenRequest!.code = code; + tokenRequest!.Code = code; GetClient(client_id, softwareProductId); GetSoftwareProduct(softwareProductId, brandStatus); - } - if (String.Equals("token_code_verifier_missing", testCaseType)) + } + + if (string.Equals("token_code_verifier_missing", testCaseType)) { - tokenRequest!.code = code; - tokenRequest.redirect_uri = "foo"; - tokenRequest.code_verifier = String.Empty; + tokenRequest!.Code = code; + tokenRequest.Redirect_uri = "foo"; + tokenRequest.Code_verifier = string.Empty; GetClient(client_id, softwareProductId); GetSoftwareProduct(softwareProductId, brandStatus); } - if (String.Equals("token_code_verifier_foo", testCaseType)) + + if (string.Equals("token_code_verifier_foo", testCaseType)) { - tokenRequest!.code = code; - tokenRequest.redirect_uri = "foo"; - tokenRequest.code_verifier = "foo"; + tokenRequest!.Code = code; + tokenRequest.Redirect_uri = "foo"; + tokenRequest.Code_verifier = "foo"; GetClient(client_id, softwareProductId); GetSoftwareProduct(softwareProductId, brandStatus); } - if (String.Equals("token_grant_expired", testCaseType)) + + if (string.Equals("token_grant_expired", testCaseType)) { - tokenRequest!.code = code; - tokenRequest.redirect_uri = "foo"; - tokenRequest.code_verifier = "foo"; - + tokenRequest!.Code = code; + tokenRequest.Redirect_uri = "foo"; + tokenRequest.Code_verifier = "foo"; + GetClient(client_id, softwareProductId); GetSoftwareProduct(softwareProductId, brandStatus); - var grant= new AuthorizationCodeGrant(); - grant.ExpiresAt = DateTime.UtcNow.AddMinutes(-10); - grantService.Setup(x => x.Get(GrantTypes.AuthCode, tokenRequest.code, client_id)).ReturnsAsync(grant); + var grant = new AuthorizationCodeGrant(); + grant.ExpiresAt = DateTime.UtcNow.AddMinutes(-10); + grantService.Setup(x => x.Get(GrantTypes.AuthCode, tokenRequest.Code, client_id)).ReturnsAsync(grant); } - - if (String.Equals("token_refresh_token_missing", testCaseType)) + + if (string.Equals("token_refresh_token_missing", testCaseType)) { - var softwareProduct = new SoftwareProduct + _ = new SoftwareProduct { - SoftwareProductId = softwareProductId + SoftwareProductId = softwareProductId, }; - tokenRequest!.refresh_token = code; + tokenRequest!.Refresh_token = code; GetClient(client_id, softwareProductId); - GetSoftwareProduct(softwareProductId, brandStatus); - } + GetSoftwareProduct(softwareProductId, brandStatus); + } - //Act + // Act var result = await TokenRequestValidator.Validate(client_id, tokenRequest!, configOptions); - //Assert + // Assert Assert.IsNotNull(result); - Assert.AreEqual(result.IsValid, isvalid); - Assert.AreEqual(result.Error, expectedError); + Assert.AreEqual(result.IsValid, isvalid); + Assert.AreEqual(result.Error, expectedError); Assert.IsTrue(result.ErrorDescription?.Contains(expectErrorDescription), "Expected: {0}, Actual: {1}", expectErrorDescription, result.ErrorDescription); } @@ -163,7 +175,7 @@ private void GetClient(string client_id, string softwareProductId) { var client = new Client { - SoftwareId = softwareProductId + SoftwareId = softwareProductId, }; clientService.Setup(x => x.Get(client_id)).ReturnsAsync(client); } diff --git a/Source/CdrAuthServer.mTLS.Gateway/CdrAuthServer.mTLS.Gateway.csproj b/Source/CdrAuthServer.mTLS.Gateway/CdrAuthServer.mTLS.Gateway.csproj index 64789ba..b495286 100644 --- a/Source/CdrAuthServer.mTLS.Gateway/CdrAuthServer.mTLS.Gateway.csproj +++ b/Source/CdrAuthServer.mTLS.Gateway/CdrAuthServer.mTLS.Gateway.csproj @@ -7,6 +7,7 @@ $(Version) enable enable + True @@ -28,10 +29,18 @@ - + - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/CdrAuthServer.mTLS.Gateway/Certificates/CertificateValidator.cs b/Source/CdrAuthServer.mTLS.Gateway/Certificates/CertificateValidator.cs index de3bd5c..c633479 100644 --- a/Source/CdrAuthServer.mTLS.Gateway/Certificates/CertificateValidator.cs +++ b/Source/CdrAuthServer.mTLS.Gateway/Certificates/CertificateValidator.cs @@ -25,8 +25,14 @@ public void ValidateClientCertificate(X509Certificate2 clientCert) throw new ArgumentNullException(nameof(clientCert)); } + var rootCertLocation = _config.GetValue("Certificates:RootCACertificate:Location"); + if (string.IsNullOrEmpty(rootCertLocation)) + { + throw new InvalidOperationException("Root certificate location is not configured."); + } + // Validate that the certificate has been issued by the Mock CDR CA. - var rootCACertificate = new X509Certificate2(_config.GetValue("Certificates:RootCACertificate:Location")); + var rootCACertificate = new X509Certificate2(rootCertLocation); _logger.LogDebug("Validating client certificate using: {RootCACertificate}", rootCACertificate); var ch = new X509Chain(); @@ -48,8 +54,8 @@ public void ValidateClientCertificate(X509Certificate2 clientCert) if (ch.ChainStatus.Any()) { - _logger.LogError("An error occurred validating the client certificate: {Status}", ch.ChainStatus.First().StatusInformation); - throw new ClientCertificateException(ch.ChainStatus.First().StatusInformation); + _logger.LogError("An error occurred validating the client certificate: {Status}", ch.ChainStatus[0].StatusInformation); + throw new ClientCertificateException(ch.ChainStatus[0].StatusInformation); } } } diff --git a/Source/CdrAuthServer.mTLS.Gateway/Certificates/ClientCertificateException.cs b/Source/CdrAuthServer.mTLS.Gateway/Certificates/ClientCertificateException.cs index 8866f0e..50fe2af 100644 --- a/Source/CdrAuthServer.mTLS.Gateway/Certificates/ClientCertificateException.cs +++ b/Source/CdrAuthServer.mTLS.Gateway/Certificates/ClientCertificateException.cs @@ -1,22 +1,15 @@ -using System; -using System.Runtime.Serialization; - -namespace CdrAuthServer.mTLS.Gateway.Certificates +namespace CdrAuthServer.mTLS.Gateway.Certificates { - [Serializable] public class ClientCertificateException : Exception { - public ClientCertificateException(string message) : base($"An error occurred validating the client certificate: {message}") + public ClientCertificateException(string message) + : base($"An error occurred validating the client certificate: {message}") { } - public ClientCertificateException(string message, Exception ex) : base($"An error occurred validating the client certificate: {message}", ex) - { - } - - protected ClientCertificateException(SerializationInfo info, StreamingContext context) : base(info, context) + public ClientCertificateException(string message, Exception ex) + : base($"An error occurred validating the client certificate: {message}", ex) { } } - } diff --git a/Source/CdrAuthServer.mTLS.Gateway/Extensions/WebServerExtensions.cs b/Source/CdrAuthServer.mTLS.Gateway/Extensions/WebServerExtensions.cs index b029f8d..4602e04 100644 --- a/Source/CdrAuthServer.mTLS.Gateway/Extensions/WebServerExtensions.cs +++ b/Source/CdrAuthServer.mTLS.Gateway/Extensions/WebServerExtensions.cs @@ -1,9 +1,4 @@ -using CdrAuthServer.Infrastructure.Certificates; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using System.Net.Security; +using Microsoft.AspNetCore.Server.Kestrel.Core; namespace CdrAuthServer.mTLS.Gateway.Extensions { @@ -17,22 +12,6 @@ public static void ConfigureCipherSuites( options.ConfigureHttpsDefaults(httpsOptions => { httpsOptions.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13; - - // On non-Windows platform the CipherSuitesPolicy can be set. - if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) - { - httpsOptions.OnAuthenticate = (context, sslOptions) => - { - // Set the cipher suites dictated by the CDS. - //sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy( - // new[] { - // TlsCipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, - // TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - // TlsCipherSuite.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, - // TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 - // }); - }; - } }); }); } diff --git a/Source/CdrAuthServer.mTLS.Gateway/Program.cs b/Source/CdrAuthServer.mTLS.Gateway/Program.cs index 662a71e..68b3197 100644 --- a/Source/CdrAuthServer.mTLS.Gateway/Program.cs +++ b/Source/CdrAuthServer.mTLS.Gateway/Program.cs @@ -1,36 +1,38 @@ +using System.Security.Cryptography.X509Certificates; using CdrAuthServer.Infrastructure; using CdrAuthServer.Infrastructure.Certificates; +using CdrAuthServer.Infrastructure.Extensions; using CdrAuthServer.mTLS.Gateway.Certificates; using Microsoft.AspNetCore.Authentication.Certificate; using Microsoft.AspNetCore.Diagnostics; using Ocelot.DependencyInjection; using Ocelot.Middleware; -using System.Security.Cryptography.X509Certificates; -using static System.Net.Mime.MediaTypeNames; using Serilog; -using CdrAuthServer.mTLS.Gateway.Extensions; -using CdrAuthServer.Infrastructure.Extensions; +using static System.Net.Mime.MediaTypeNames; var builder = WebApplication.CreateBuilder(args); + builder.Configuration.AddJsonFile("gateway-config.json", false, true); builder.Configuration.AddEnvironmentVariables(); -builder.Services.ConfigureWebServer( - builder.Configuration, - "Certificates:MtlsServerCertificate", - httpsPort: builder.Configuration.GetValue("CdrAuthServer:mtlsGateway:httpsPort", 8082), + +await builder.Services.ConfigureWebServer( + builder.Configuration, + "Certificates:MtlsServerCertificate", + httpsPort: builder.Configuration.GetValue("CdrAuthServer:mtlsGateway:httpsPort", 8082), requireClientCertificate: true); -//builder.Services.ConfigureCipherSuites(); // Add logging provider. var logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .Enrich.FromLogContext() .CreateLogger(); + builder.Logging.ClearProviders(); builder.Logging.AddSerilog(logger); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme) .AddCertificate(options => { @@ -59,12 +61,14 @@ { context.Fail("invalid client certificate"); throw context.Exception; - } + }, }; }) + // Adding an ICertificateValidationCache results in certificate auth caching the results. // The default implementation uses a memory cache. .AddCertificateCache(); + builder.Services.AddAuthorization(); builder.Services.AddOcelot(); @@ -113,8 +117,9 @@ httpContext.Request.Headers[HttpHeaders.ForwardedHost] = httpContext.Request.Host.ToString(); await next.Invoke(); - } + }, }; + app.UseOcelot(pipelineConfiguration).Wait(); -app.Run(); +await app.RunAsync(); diff --git a/Source/CdrAuthServer.sln b/Source/CdrAuthServer.sln index 5bc92b1..a3ee4e0 100644 --- a/Source/CdrAuthServer.sln +++ b/Source/CdrAuthServer.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Artefacts", "Artefacts", "{2BA365FC-FFCC-449A-96B5-BC73D9216FB1}" ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore + .editorconfig = .editorconfig ..\.gitignore = ..\.gitignore docker-compose.E2ETests.Standalone.yml = docker-compose.E2ETests.Standalone.yml docker-compose.E2ETests.yml = docker-compose.E2ETests.yml diff --git a/Source/CdrAuthServer/Authorisation/AccessTokenHandler.cs b/Source/CdrAuthServer/Authorisation/AccessTokenHandler.cs index 3cfd377..dacd150 100644 --- a/Source/CdrAuthServer/Authorisation/AccessTokenHandler.cs +++ b/Source/CdrAuthServer/Authorisation/AccessTokenHandler.cs @@ -65,14 +65,14 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext } // Introspect the access token. - var accessToken = authHeader.ToString().Replace("Bearer ", ""); + var accessToken = authHeader.ToString().Replace("Bearer ", string.Empty); var endpoint = _config["AccessTokenIntrospectionEndpoint"]; var httpClient = new HttpClient(HttpHelper.CreateHttpClientHandler(_config)); var formFields = new List> { - new("token", accessToken) + new("token", accessToken), }; var response = await httpClient.PostAsync(endpoint, new FormUrlEncodedContent(formFields)); diff --git a/Source/CdrAuthServer/Authorisation/AuthorisationPolicy.cs b/Source/CdrAuthServer/Authorisation/AuthorisationPolicy.cs index 42496eb..94f4fbb 100644 --- a/Source/CdrAuthServer/Authorisation/AuthorisationPolicy.cs +++ b/Source/CdrAuthServer/Authorisation/AuthorisationPolicy.cs @@ -6,6 +6,6 @@ public enum AuthorisationPolicy UserInfo, GetCustomerBasic, GetBankingAccounts, - AdminMetadataUpdate + AdminMetadataUpdate, } } diff --git a/Source/CdrAuthServer/Authorisation/HolderOfKeyHandler.cs b/Source/CdrAuthServer/Authorisation/HolderOfKeyHandler.cs index 0f9ad2b..8e06a3d 100644 --- a/Source/CdrAuthServer/Authorisation/HolderOfKeyHandler.cs +++ b/Source/CdrAuthServer/Authorisation/HolderOfKeyHandler.cs @@ -15,7 +15,7 @@ public class HolderOfKeyHandler : AuthorizationHandler public HolderOfKeyHandler( IConfiguration config, - IHttpContextAccessor httpContextAccessor, + IHttpContextAccessor httpContextAccessor, ILogger logger) { _httpContextAccessor = httpContextAccessor; @@ -31,10 +31,8 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte return Task.CompletedTask; } - // - // Check that the thumbprint of the client cert used for TLS MA is the same - // as the one expected by the cnf:x5t#S256 claim in the access token - // + // Check that the thumbprint of the client cert used for TLS MA is the same + // as the one expected by the cnf:x5t#S256 claim in the access token string? requestHeaderClientCertThumprint = null; if (_httpContextAccessor.HttpContext?.Request.Headers.TryGetValue(_configOptions.ClientCertificateThumbprintHttpHeaderName, out StringValues headerThumbprints) is true) { diff --git a/Source/CdrAuthServer/Authorisation/PolicyAuthorizeAttribute.cs b/Source/CdrAuthServer/Authorisation/PolicyAuthorizeAttribute.cs index 1469890..97c3606 100644 --- a/Source/CdrAuthServer/Authorisation/PolicyAuthorizeAttribute.cs +++ b/Source/CdrAuthServer/Authorisation/PolicyAuthorizeAttribute.cs @@ -9,11 +9,11 @@ namespace CdrAuthServer.Authorisation [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class PolicyAuthorizeAttribute : AuthorizeAttribute, IAsyncAuthorizationFilter { - public readonly AuthServerAuthorisationPolicyAttribute policy; + public AuthServerAuthorisationPolicyAttribute PolicyName { get; } public PolicyAuthorizeAttribute(AuthServerAuthorisationPolicyAttribute policy) { - this.policy = policy; + this.PolicyName = policy; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) @@ -23,12 +23,15 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { return; } - var authorizationResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, policy.ToString()); + + var authorizationResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, PolicyName.ToString()); var logger = context.HttpContext.RequestServices.GetService(typeof(ILogger)) as ILogger; if (authorizationResult.Succeeded) + { return; + } if (authorizationResult.Failure?.FailedRequirements.Any(r => r.GetType() == typeof(HolderOfKeyRequirement)) is true) { @@ -55,7 +58,6 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) logger?.LogError("invalid_token - insufficient_scope"); context.Result = ErrorCatalogue.Catalogue().GetErrorResponse(ErrorCatalogue.AUTHORIZATION_INSUFFICIENT_SCOPE); context.Result = new JsonResult(new Error("insufficient_scope")) { StatusCode = 403 }; - } } } diff --git a/Source/CdrAuthServer/Authorisation/ScopeHandler.cs b/Source/CdrAuthServer/Authorisation/ScopeHandler.cs index 75e73ec..bc12e04 100644 --- a/Source/CdrAuthServer/Authorisation/ScopeHandler.cs +++ b/Source/CdrAuthServer/Authorisation/ScopeHandler.cs @@ -8,8 +8,6 @@ namespace CdrAuthServer.Authorisation public class ScopeHandler : AuthorizationHandler { private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IConfiguration _config; private readonly ConfigurationOptions _configOptions; public ScopeHandler( @@ -18,9 +16,7 @@ public ScopeHandler( IHttpContextAccessor httpContextAccessor) { _logger = logger; - _httpContextAccessor = httpContextAccessor; - _config = config; - _configOptions = _config.GetConfigurationOptions(_httpContextAccessor.HttpContext); + _configOptions = config.GetConfigurationOptions(httpContextAccessor.HttpContext); } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ScopeRequirement requirement) diff --git a/Source/CdrAuthServer/CdrAuthServer.csproj b/Source/CdrAuthServer/CdrAuthServer.csproj index 31e0557..9e2fd40 100644 --- a/Source/CdrAuthServer/CdrAuthServer.csproj +++ b/Source/CdrAuthServer/CdrAuthServer.csproj @@ -10,7 +10,15 @@ d57d2c28-46ab-4ea5-9327-daba2552a167 Linux True - ConsumerDataRight.ParticipantTooling.MockAuthServer.API.xml + bin\$(MSBuildProjectName).xml + + + + $(NoWarn);1591;1516 + + + + $(NoWarn);1591;1516 @@ -51,6 +59,14 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/CdrAuthServer/Configuration/CdrRegisterConfiguration.cs b/Source/CdrAuthServer/Configuration/CdrRegisterConfiguration.cs index dc444aa..7008750 100644 --- a/Source/CdrAuthServer/Configuration/CdrRegisterConfiguration.cs +++ b/Source/CdrAuthServer/Configuration/CdrRegisterConfiguration.cs @@ -2,9 +2,12 @@ { public class CdrRegisterConfiguration { - public string SsaJwksUri { get; set; } = string.Empty; + public string SsaJwksUri { get; set; } = string.Empty; + public bool CheckSoftwareProductStatus { get; set; } = true; + public string GetDataRecipientsEndpoint { get; set; } = string.Empty; + public int Version { get; set; } } } diff --git a/Source/CdrAuthServer/Configuration/ConfigurationOptions.cs b/Source/CdrAuthServer/Configuration/ConfigurationOptions.cs index c3ec4f6..ed9e710 100644 --- a/Source/CdrAuthServer/Configuration/ConfigurationOptions.cs +++ b/Source/CdrAuthServer/Configuration/ConfigurationOptions.cs @@ -1,50 +1,93 @@ using CdrAuthServer.Infrastructure; using CdrAuthServer.Infrastructure.Certificates; +using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Configuration { public class ConfigurationOptions { + public const string ConfigurationSectionName = "CdrAuthServer"; + + public const string ScopesProfileAll = "all"; + public const string ScopesProfileBanking = "banking"; + public const string ScopesProfileEnergy = "energy"; + public string BrandId { get; set; } = string.Empty; + public string BrandName { get; set; } = string.Empty; + public string BrandAbn { get; set; } = string.Empty; + public string BaseUri { get; set; } = string.Empty; + public string SecureBaseUri { get; set; } = string.Empty; + public string BasePath { get; set; } = string.Empty; + public string Issuer { get; set; } = string.Empty; + public string JwksUri { get; set; } = string.Empty; + public string RegistrationEndpoint { get; set; } = string.Empty; + public string AuthorizationEndpoint { get; set; } = string.Empty; + public string TokenEndpoint { get; set; } = string.Empty; + public string UserinfoEndpoint { get; set; } = string.Empty; + public string IntrospectionEndpoint { get; set; } = string.Empty; + public string ArrangementRevocationEndpoint { get; set; } = string.Empty; + public string RevocationEndpoint { get; set; } = string.Empty; + public string PushedAuthorizationEndpoint { get; set; } = string.Empty; + public string DefaultAcrValue { get; set; } = string.Empty; + public IList? AcrValuesSupported { get; set; } + public IList? ClaimsSupported { get; set; } + public IList? CodeChallengeMethodsSupported { get; set; } + public IList? GrantTypesSupported { get; set; } + public string ScopesProfile { get; set; } = string.Empty; + public IList? ScopesSupported { get; set; } + public IList? BankingScopesSupported { get; set; } + public IList? EnergyScopesSupported { get; set; } + public IList? ClientCredentialScopesSupported { get; set; } + public IList? ResponseModesSupported { get; set; } - public IList? ResponseTypesSupported { get; set; } + + // ResponseTypesSupported is always "code" as Hybrid flow is decommissioned. + public IList? ResponseTypesSupported + { + get { return [ResponseTypes.AuthCode]; } + } + public IList? SubjectTypesSupported { get; set; } + public IList? RequestObjectSigningAlgValuesSupported { get; set; } + public IList? TokenEndpointAuthMethodsSupported { get; set; } + public IList? TokenEndpointAuthSigningAlgValuesSupported { get; set; } + public IList? IdTokenSigningAlgValuesSupported { get; set; } - public IList? IdTokenEncryptionAlgValuesSupported { get; set; } - public IList? IdTokenEncryptionEncValuesSupported { get; set; } - public bool AlwaysEncryptIdTokens { get; set; } + public bool UseMtlsEndpointAliases { get; set; } + public IList? AuthorizationSigningAlgValuesSupported { get; set; } public string AuthorizationEncryptionAlgValuesSupported { get; set; } = string.Empty; + public IList? AuthorizationEncryptionAlgValuesSupportedList { get @@ -55,6 +98,7 @@ public IList? AuthorizationEncryptionAlgValuesSupportedList } public string AuthorizationEncryptionEncValuesSupported { get; set; } = string.Empty; + public IList? AuthorizationEncryptionEncValuesSupportedList { get @@ -65,29 +109,43 @@ public IList? AuthorizationEncryptionEncValuesSupportedList } public CertificateLoadDetails? PS256SigningCertificate { get; set; } + public CertificateLoadDetails? ES256SigningCertificate { get; set; } + public int RequestUriExpirySeconds { get; set; } = 90; + public int AccessTokenExpirySeconds { get; set; } = 300; + public int IdTokenExpirySeconds { get; set; } = 300; + public int ClockSkewSeconds { get; set; } = 0; + public CdrRegisterConfiguration? CdrRegister { get; set; } + public bool HeadlessMode { get; set; } + public bool HeadlessModeRequiresConfirmation { get; set; } + public bool ValidateResourceEndpoint { get; set; } = true; + public bool AllowDuplicateRegistrations { get; set; } = false; + public bool SupportJarmEncryption { get; set; } = false; + public string ClientCertificateThumbprintHttpHeaderName { get; set; } = HttpHeaders.ClientCertificateThumbprint; + public string ClientCertificateCommonNameHttpHeaderName { get; set; } = HttpHeaders.ClientCertificateCommonName; + public string ClientCertificateHttpHeaderName { get; set; } = HttpHeaders.ClientCertificate; + public IList? OverrideMtlsChecks { get; set; } + public string AutoFillCustomerId { get; set; } = string.Empty; + public string AutoFillOtp { get; set; } = string.Empty; + public string AuthUiBaseUri { get; set; } = string.Empty; - public const string scopesProfileAll = "all"; - public const string scopesProfileBanking = "banking"; - public const string scopesProfileEnergy = "energy"; public bool EnableServerCertificateValidation { get; set; } = false; - } } diff --git a/Source/CdrAuthServer/Configuration/Keys.cs b/Source/CdrAuthServer/Configuration/Keys.cs index 44eb4ec..3f8d9a6 100644 --- a/Source/CdrAuthServer/Configuration/Keys.cs +++ b/Source/CdrAuthServer/Configuration/Keys.cs @@ -26,6 +26,6 @@ public static class Keys public const string IdTokenEncryptionEncValuesSupported = "CdrAuthServer:IdTokenEncryptionEncValuesSupported"; public const string PS256SigningCertificate = "CdrAuthServer:PS256SigningCertificate"; public const string ES256SigningCertificate = "CdrAuthServer:ES256SigningCertificate"; - public const string SeedDataFilePath = "CdrAuthServer:SeedData:FilePath"; + public const string SeedDataFilePath = "CdrAuthServer:SeedData:FilePath"; } } diff --git a/Source/CdrAuthServer/Configuration/ServerCertificateValidationConfiguration.cs b/Source/CdrAuthServer/Configuration/ServerCertificateValidationConfiguration.cs index af5dc33..a2bd4b2 100644 --- a/Source/CdrAuthServer/Configuration/ServerCertificateValidationConfiguration.cs +++ b/Source/CdrAuthServer/Configuration/ServerCertificateValidationConfiguration.cs @@ -3,7 +3,9 @@ public class ServerCertificateValidationConfiguration { public bool Enabled { get; set; } + public string RootCertificate { get; set; } = string.Empty; + public string IntermediateCertificate { get; set; } = string.Empty; } } diff --git a/Source/CdrAuthServer/ConsumerDataRight.ParticipantTooling.MockAuthServer.API.xml b/Source/CdrAuthServer/ConsumerDataRight.ParticipantTooling.MockAuthServer.API.xml deleted file mode 100644 index d82dc75..0000000 --- a/Source/CdrAuthServer/ConsumerDataRight.ParticipantTooling.MockAuthServer.API.xml +++ /dev/null @@ -1,559 +0,0 @@ - - - - CdrAuthServer - - - - - This is used to generate html that can be used to Confirm/Cancel an authorisation - request when in HeadlessMode. - - IActionResult - - - - This controller action is used to check the validity of an access_token only. - It should not be called by an external participant (i.e. ADR) but is consumed internally - by the resource API of the mock data holder. - In the CDS, the introspection endpoint only supports the introspection of refresh tokens. - - Access token to check - IntrospectionResult - - There is currently no auth on this endpoint. - This could be added in the future to only allow the calls from the Mock Data Holder Resource API. - - - - - Generate a JWKS for the Auth Server. - - JsonWebKeySet - - - - This controller is used to provide a resource endpoint for testing purposes, to - ensure that the auth server is issuing access tokens correctly. - - - - - This controller method is provided to delete the arrangement and refreshtoken in authserver and - trigger an arrangement revocation at a data recipient. - Normally, this would be done from the DH dashboard. - However, until a dashboard is in place this method can be used to trigger a request. - - IActionResult - - Note: this controller action would not be implemented in a production system and is provided for testing purposes. - - - - - Creates a SHA256 hash of the specified input. - - The input. - A hash - - - - Compresses a byte array and returns a deflate compressed, byte array. - - String to compress - - - - Decompresses a deflate compressed, byte array and returns an uncompressed byte array. - - String to decompress. - - - - Id Permanence Helper - - - - - Encrypt an ID to meet ID Permanence rules. - - Internal ID (i.e. accountId, transactionId) to encrypt - IdPermanenceParameters - Private Key - Encrypted ID - - - - Decrypt an encrypted ID back to the internal value. - - Encrypted ID to decrypt back to internal value - IdPermanenceParameters - Private Key - Internal ID - - - - Encrypt the internal customer id for inclusion as the "sub" claim in id_token and access_token. - - Internal Customer Id - SubPermanenceParameters - Private Key - Encrypted customer id to be included in sub claim - - - - Decrypt the encrypted sub claim value from the access_token into the internal customer id. - - Encrypted Customer Id found in sub claim of the access_token - SubPermanenceParameters - Private Key - Internal Customer Id - - - - Id Permanence Manager - - - - - Method to create permanence ids for specified properties in a list of objects - - The type of list - The list - The permanence id parameters - The specified id properties to create permanence ids for - The list with permanence ids set - - - - Encrypt an ID to meet ID Permanence rules. - - Internal ID (i.e. accountId, transactionId) to encrypt - IdPermanenceParameters - Encrypted ID - - - - Decrypt an encrypted ID back to the internal value. - - Encrypted ID to decrypt back to internal value - IdPermanenceParameters - Internal ID - - - - Encrypt the internal customer id for inclusion as the "sub" claim in id_token and access_token. - - Internal Customer Id - SubPermanenceParameters - Encrypted customer id to be included in sub claim - - - - Decrypt the encrypted sub claim value from the access_token into the internal customer id. - - Encrypted Customer Id found in sub claim of the access_token - SubPermanenceParameters - Internal Customer Id - - - - Gets a Data Holder issued client identifier string. - - - - - Gets a time at which the client identifier was issued expressed as seconds since 1970-01-01T00:00:00Z as measured in UTC. - - - - - Gets a human-readable string name of the software product to be presented to the end-user during authorization. - - - - - Gets a human-readable string name of the software product description to be presented to the end user during authorization. - - - - - URL string of a web page providing information about the client. - - - - - Legal Entity Identifier. - - - - - Legal Entity Name. - - - - - Gets a unique identifier string assigned by the CDR Register that identifies the Accredited Data Recipient Brand. - - - - - Gets a human-readable string name of the Accredited Data Recipient to be presented to the end user during authorization. - - - - - Gets an array of redirection URI strings for use in redirect-based flows. - - - - - Gets a URL string that references a logo for the client. If present, the server SHOULD display this image to the end-user during approval. - - - - - Gets a URL string that points to a human-readable terms of service document for the Software Product. - - - - - Gets a URL string that points to a human-readable policy document for the Software Product. - - - - - Gets a URL string referencing the client JSON Web Key (JWK) Set [RFC7517] document, which contains the client public keys. - - - - - Gets a URI string that references the location of the Software Product consent revocation endpoint. - - - - - Gets or sets a URI string that references the location of the Software Product recipient base uri endpoint. - - - - - Gets the requested authentication method for the token endpoint. - - - - - Gets the algorithm used for signing the JWT. - - - - - Gets an array of OAuth 2.0 grant type strings that the client can use at the token endpoint. - - - - - Gets an array of the OAuth 2.0 response type strings that the client can use at the authorization endpoint. - - - - - Gets the kind of the application. The only supported application type will be 'web'. - - - - - Gets an algorithm with which an id_token is to be signed. - - - - - Gets a JWE `alg` algorithm with which an id_token is to be encrypted. - - - - - Gets a JWE `enc` algorithm with which an id_token is to be encrypted. - - - - - Gets an algorithm which the ADR expects to sign the request object if a request object will be part of the authorization request sent to the Data Holder. - - - - - Gets the Software Statement Assertion, as defined in [Dynamic Client Registration](https://cdr-register.github.io/register/#dynamic-client-registration). - - - - - Gets a string representing a unique identifier assigned by the ACCC Register and used by registration endpoints to identify the software product to be dynamically registered. </br></br>The \"software_id\" will remain the same for the lifetime of the product, across multiple updates and versions. - - - - - Gets a string representing the software roles as outlined in the SSA. - - - - - Gets a string containing a space-separated list of scope values that the client can use when requesting access tokens. - - - - - Get the sector identifier uri used in PPID calculations. - - - - - Gets an algorithm with which an id_token is to be signed. - - - - - Gets a JWE `alg` algorithm with which an id_token is to be encrypted. - - - - - Gets a JWE `enc` algorithm with which an id_token is to be encrypted. - - - - - Gets a Software Statement Assertion, as defined in [Dynamic Client Registration](https://cdr-register.github.io/register/#dynamic-client-registration). - - - - - Gets the Key Identifier of this JWT. - - - - - Gets the time at which the request was issued by the TPP expressed as seconds since 1970-01-01T00:00:00Z as measured in UTC. - - - - - Gets the time at which the request expires expressed as seconds since 1970-01-01T00:00:00Z as measured in UTC. - - - - - Gets a unique identifier for the Data Holder issued by the CDR Register. - - - - - Gets a unique identifier for the JWT, used to prevent replay of the token. - - - - - Gets the audience for the request. This should be the Data Holder authorisation server URI. - - - - - Gets an array of redirection URI strings for use in redirect-based flows. - - - - - Gets the requested authentication method for the token endpoint. - - - - - Gets the algorithm used for signing the JWT. - - - - - Gets an array of OAuth 2.0 grant type strings that the client can use at the token endpoint. - - - - - Gets an array of the OAuth 2.0 response type strings that the client can use at the authorization endpoint. - - - - - Gets the kind of the application. The only supported application type will be 'web'. - - - - - Gets a algorithm with which an id_token is to be signed. - - - - - Gets a JWE `alg` algorithm with which an id_token is to be encrypted. - - - - - Gets a JWE `enc` algorithm with which an id_token is to be encrypted. - - - - - Gets an algorithm which the ADR expects to sign the request object if a request object will be part of the authorization request sent to the Data Holder. - - - - - Gets the Software Statement Assertion - - - - - Gets a algorithm with which an auth request is to be signed (JARM). - - - - - Gets a JWE `alg` algorithm with which an auth request is to be encrypted (JARM). - - - - - Gets a JWE `enc` algorithm with which an auth request is to be encrypted (JARM). - - - - - Gets a unique identifier assigned by the CDR Register that identifies the Accredited Data Recipient Legal Entity. - - - - - Gets a Human-readable string name of the Accredited Data Recipient Legal Entity. - - - - - Gets a unique identifier string assigned by the CDR Register that identifies the Accredited Data Recipient Brand. - - - - - Gets a Human-readable string name of the Accredited Data Recipient to be presented to the end user during authorization. - - - - - Gets a Human-readable string name of the software product to be presented to the end-user during authorization. - - - - - Gets a Human-readable string name of the software product description to be presented to the end user during authorization. - - - - - Gets a URL string of a web page providing information about the client. - - - - - Gets a URL string that references a logo for the client. If present, the server SHOULD display this image to the end-user during approval - - - - - URL string that points to a human-readable terms of service document for the Software Product. - - URL string that points to a human-readable terms of service document for the Software Product - - - - Gets a URL string that points to a human-readable policy document for the Software Product. - - - - - Gets a URL string referencing the client JSON Web Key (JWK) Set [RFC7517] document, which contains the client public keys. - - - - - Gets a URI string that references the location of the Software Product consent revocation endpoint. - - - - - Gets a URI string that references the location of the Software Product recipient base uri endpoint. - - - - - Gets a String representing a unique identifier assigned by the ACCC Register and used by registration endpoints to identify the software product to be dynamically registered. </br></br>The \"software_id\" will remain the same for the lifetime of the product, across multiple updates and versions - - - - - Gets a String containing a the software role, e.g. data-recipient-software-product - - - - - Gets a String containing a space-separated list of scope values that the client can use when requesting access tokens. - - - - - Gets an Array of redirection URI strings for use in redirect-based flows. - - - - - Sector Identifier Uri used in PPID calculations. - - - - - The purpose of those filters is to get the auto-generated swagger into a consistent order to aid in comparison with the json schema defined at https://consumerdatastandardsaustralia.github.io/standards/includes/swagger/cds_register.json - These orders don't neccessarily match yet, but we have the aim to better match them in the future and until they do, it's helpful to order them alphabetically to aid in searching for property manually. - We are also exposing properties to the swagger auto-generation that are already set within the application, but don't show up on the autogeneration without this code. - - - - - Validates an auth request using the requestObject (from PAR endpoint) and - the parameters passed directly to the auth endpoint (authRequest). - - Parameters received on authorization endpoint - configuration - checkGrantExpiredOrUsed - - AuthorizeRequestValidationResult containing a validated auth request object. - - - - The requestObject values take precedence over the authRequest values. - - client_id and response_type must match the values in the request object. - - - - - Validate the sectory identifier url. - Currently it is only required to call this endpoint and we do not validate the output. - - - - - Validates a JWT against the JWKS endpoint of the client. - - JWT to validate - Client - client_assertion, request or access_token - ValidationResult and JwtSecurityToken - - - diff --git a/Source/CdrAuthServer/Controllers/AdminController.cs b/Source/CdrAuthServer/Controllers/AdminController.cs index 1739443..064ffe0 100644 --- a/Source/CdrAuthServer/Controllers/AdminController.cs +++ b/Source/CdrAuthServer/Controllers/AdminController.cs @@ -1,7 +1,4 @@ using CdrAuthServer.Authorisation; -using CdrAuthServer.Configuration; -using CdrAuthServer.Extensions; -using CdrAuthServer.Helpers; using CdrAuthServer.Infrastructure; using CdrAuthServer.Infrastructure.Attributes; using CdrAuthServer.Infrastructure.Authorisation; @@ -12,8 +9,6 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Controllers @@ -21,30 +16,30 @@ namespace CdrAuthServer.Controllers [ApiController] public class AdminController : ControllerBase { - private readonly ConfigurationOptions _configOptions; private readonly ICdrService _cdrService; private readonly IClientService _clientService; private readonly ILogger _logger; + private readonly IRegisterClientService _registerClientService; public AdminController( - IConfiguration config, ICdrService cdrService, IClientService clientService, - ILogger logger) + ILogger logger, + IRegisterClientService registerClientService) { _cdrService = cdrService; _clientService = clientService; _logger = logger; - _configOptions = config.GetConfigurationOptions(); + _registerClientService = registerClientService; } - [HttpPost] + [HttpPost] [Route("cds-au/v1/admin/register/metadata")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [PolicyAuthorize(AuthServerAuthorisationPolicyAttribute.AdminMetadataUpdate)] [ApiVersion("1")] [ReturnXV("1")] - public async Task RefreshDataRecipients(DataRecipientRequest dataRecipientRequest) + public async Task RefreshDataRecipients(DataRecipientRequest dataRecipientRequest, CancellationToken cancellationToken = default) { if (dataRecipientRequest is null) { @@ -63,99 +58,57 @@ public async Task RefreshDataRecipients(DataRecipientRequest data return new UnauthorizedObjectResult(new { error = clientIdResult.Error, error_description = clientIdResult.ErrorDescription }); } - if (string.Equals( dataRecipientRequest.Data?.Action, "REFRESH",StringComparison.OrdinalIgnoreCase)) + if (string.Equals(dataRecipientRequest.Data?.Action, "REFRESH", StringComparison.OrdinalIgnoreCase)) { - var getDataRecipientsEndpoint = _configOptions.CdrRegister?.GetDataRecipientsEndpoint; - if (!string.IsNullOrEmpty(getDataRecipientsEndpoint)) - { - - // Call the Register to get the data recipients list. - // With new endpoints x-v header version should be 3 - var jsonResponse = await GetData(getDataRecipientsEndpoint, _configOptions.CdrRegister.Version); - - if (!string.IsNullOrEmpty(jsonResponse)) - { - var data = JsonConvert.DeserializeObject(jsonResponse); - if (data != null) - { - var dataRecipients = data["data"].ToObject(); + var response = await _registerClientService.GetDataRecipients(cancellationToken); - List softwareProducts = await MapSoftwareProductList(dataRecipients?.ToList()); + if (response != null) + { + List softwareProducts = MapSoftwareProductList(response.Data.ToList()); - // Purge Data Recipients - await _cdrService.PurgeDataRecipients(); + // Purge Data Recipients + await _cdrService.PurgeDataRecipients(); - // If data was retrieved, then insert it in our repository. - if (softwareProducts.Count > 0) - { - await _cdrService.InsertDataRecipients(softwareProducts); - return Ok($"Data recipient records refreshed from {getDataRecipientsEndpoint}."); - } - } + // If data was retrieved, then insert it in our repository. + if (softwareProducts.Count > 0) + { + await _cdrService.InsertDataRecipients(softwareProducts); + return Ok($"Data recipient records refreshed from {response.Links.Self}."); } } - } + } + return StatusCode(StatusCodes.Status500InternalServerError, "Data recipient data could not be refreshed."); } - private async Task> MapSoftwareProductList(List dataRecipients) + private static List MapSoftwareProductList(List dataRecipients) { - List softwareProducts = []; - - if (dataRecipients.Count != 0) - { - foreach (LegalEntity legalEntity in dataRecipients) - { - foreach (var dataRecipientBrand in legalEntity.DataRecipientBrands) - { - foreach (var sp in dataRecipientBrand.SoftwareProducts) - { - SoftwareProduct softwareProduct = new SoftwareProduct(); - softwareProduct.SoftwareProductId = sp.SoftwareProductId; - softwareProduct.SoftwareProductName = sp.SoftwareProductName; - softwareProduct.SoftwareProductDescription = sp.SoftwareProductDescription; - softwareProduct.LogoUri = sp.LogoUri; - softwareProduct.Status = sp.Status; - - softwareProduct.LegalEntityId = legalEntity.LegalEntityId; - softwareProduct.LegalEntityName = legalEntity.LegalEntityName; - softwareProduct.LegalEntityStatus = legalEntity.Status; - - softwareProduct.BrandId = dataRecipientBrand.DataRecipientBrandId; - softwareProduct.BrandName = dataRecipientBrand.BrandName; - softwareProduct.BrandStatus = dataRecipientBrand.Status; - - softwareProducts.Add(softwareProduct); - } - } - } - } - - return softwareProducts; + return dataRecipients + .SelectMany(legalEntity => legalEntity.DataRecipientBrands + .SelectMany(dataRecipientBrand => dataRecipientBrand.SoftwareProducts + .Select(sp => new SoftwareProduct + { + SoftwareProductId = sp.SoftwareProductId, + SoftwareProductName = sp.SoftwareProductName, + SoftwareProductDescription = sp.SoftwareProductDescription, + LogoUri = sp.LogoUri, + Status = sp.Status, + + LegalEntityId = legalEntity.LegalEntityId, + LegalEntityName = legalEntity.LegalEntityName, + LegalEntityStatus = legalEntity.Status, + + BrandId = dataRecipientBrand.DataRecipientBrandId, + BrandName = dataRecipientBrand.BrandName, + BrandStatus = dataRecipientBrand.Status, + }))) + .ToList(); } - private async Task GetData(string endpoint, int version) - { - _logger.LogInformation("Retrieving data from {Endpoint} (x-v: {Version})...", endpoint, version); - - var httpClient = new HttpClient(HttpHelper.CreateHttpClientHandler(_configOptions.EnableServerCertificateValidation)); - httpClient.DefaultRequestHeaders.Add("x-v", version.ToString()); - var response = await httpClient.GetAsync(endpoint); - - _logger.LogInformation("Status code: {StatusCode}", response.StatusCode); - - if (response.IsSuccessStatusCode) - { - return await response.Content.ReadAsStringAsync(); - } - - return null; - } - private async Task ValidateClientId() { var clientIdClaimValue = this.User.Claims.FirstOrDefault(c => c.Type == ClaimNames.ClientId); - if (clientIdClaimValue == null + if (clientIdClaimValue == null || (await _clientService.Get(clientIdClaimValue.Value) == null)) { return ValidationResult.Fail(ErrorCodes.Generic.InvalidRequest, "The client is unknown"); diff --git a/Source/CdrAuthServer/Controllers/ArrangementRevocationController.cs b/Source/CdrAuthServer/Controllers/ArrangementRevocationController.cs index d60093d..fefd0b1 100644 --- a/Source/CdrAuthServer/Controllers/ArrangementRevocationController.cs +++ b/Source/CdrAuthServer/Controllers/ArrangementRevocationController.cs @@ -69,6 +69,7 @@ public async Task RevokeArrangement( _logger.LogError("Software Product not found {SoftwareProductId}", softwareProductId); return ErrorCatalogue.Catalogue().GetErrorResponse(ErrorCatalogue.SOFTWARE_PRODUCT_NOT_FOUND); } + if (softwareProduct.Status.Equals("REMOVED", StringComparison.OrdinalIgnoreCase)) { _logger.LogError("Software product status is removed - consents cannot be revoked {SoftwareProductId}", softwareProductId); @@ -83,11 +84,10 @@ public async Task RevokeArrangement( } // Delete the grants. - await _grantService.Delete(client.ClientId, GrantTypes.RefreshToken, cdrArrangementGrant.RefreshToken ?? ""); + await _grantService.Delete(client.ClientId, GrantTypes.RefreshToken, cdrArrangementGrant.RefreshToken ?? string.Empty); await _grantService.Delete(client.ClientId, GrantTypes.CdrArrangement, cdrArrangementGrant.Key); return NoContent(); } - } } diff --git a/Source/CdrAuthServer/Controllers/AuthorisationController.cs b/Source/CdrAuthServer/Controllers/AuthorisationController.cs index 719fd17..b311568 100644 --- a/Source/CdrAuthServer/Controllers/AuthorisationController.cs +++ b/Source/CdrAuthServer/Controllers/AuthorisationController.cs @@ -1,13 +1,13 @@ -using CdrAuthServer.Configuration; +using System.Security.Claims; +using System.Text; +using System.Web; +using CdrAuthServer.Configuration; using CdrAuthServer.Extensions; using CdrAuthServer.Models; using CdrAuthServer.Services; using CdrAuthServer.Validation; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; -using System.Security.Claims; -using System.Text; -using System.Web; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Controllers @@ -50,15 +50,16 @@ public async Task Authorise( if (!validationResult.IsValid) { _logger.LogInformation("Authorization failed {@ValidationResult}", validationResult); - return await CallbackErrorResponse(validationResult, authRequest.client_id, configOptions); + return await CallbackErrorResponse(validationResult, authRequest.Client_id, configOptions); } // Retrieve the request uri grant, this has already been validated in the validator. - if (await _grantService.Get(GrantTypes.RequestUri, authRequest.request_uri, authRequest.client_id) is not RequestUriGrant requestUriGrant) + if (await _grantService.Get(GrantTypes.RequestUri, authRequest.Request_uri, authRequest.Client_id) is not RequestUriGrant requestUriGrant) { - _logger.LogError("requestUriGrant for request_uri:{Uri} for client:{Id} not found", authRequest.request_uri, authRequest.client_id); + _logger.LogError("requestUriGrant for request_uri:{Uri} for client:{Id} not found", authRequest.Request_uri, authRequest.Client_id); throw new InvalidOperationException($"requestUriGrant is null or not found"); } + requestUriGrant.UsedAt = DateTime.UtcNow; await _grantService.Update(requestUriGrant); @@ -66,7 +67,7 @@ public async Task Authorise( { if (configOptions.HeadlessModeRequiresConfirmation) { - return OutputAuthConfirmation($"{configOptions.BaseUri}{configOptions.BasePath}/connect/authorize-confirm", authRequest.request_uri, authRequest.client_id); + return OutputAuthConfirmation($"{configOptions.BaseUri}{configOptions.BasePath}/connect/authorize-confirm", authRequest.Request_uri, authRequest.Client_id); } var user = new HeadlessModeUser(); @@ -74,10 +75,10 @@ public async Task Authorise( } // Create params to redirect to the auth UI - var client = await _clientService.Get(authRequest.client_id); + var client = await _clientService.Get(authRequest.Client_id); - //get sharing_duration - var parRequestData = JsonConvert.DeserializeObject(requestUriGrant.Data["request"] as string); + // get sharing_duration + var parRequestData = JsonConvert.DeserializeObject(requestUriGrant.Data["request"] as string ?? string.Empty); var authorizeRedirectRequest = new AuthorizeRedirectRequest { @@ -89,12 +90,12 @@ public async Task Authorise( Otp = configOptions.AutoFillOtp, AuthorizeRequest = authRequest, Scope = validationResult.ValidatedAuthorizationRequestObject.Scope, - SharingDuration = parRequestData?.Claims.SharingDuration + SharingDuration = parRequestData?.Claims.SharingDuration, }; var jwtToken = await _tokenService.CreateToken( new List() { - new Claim("login_params", JsonConvert.SerializeObject(authorizeRedirectRequest)) + new Claim("login_params", JsonConvert.SerializeObject(authorizeRedirectRequest)), }, client?.ClientId ?? string.Empty, TokenTypes.Jwt, @@ -115,20 +116,20 @@ public async Task AuthoriseCallBack( if (!validationResult.IsValid) { _logger.LogInformation("Authorization failed {@ValidationResult}", validationResult); - return await CallbackErrorResponse(validationResult, authCallbackRequest.client_id, configOptions); + return await CallbackErrorResponse(validationResult, authCallbackRequest.Client_id, configOptions); } // Retrieve the request uri grant, this has already been validated in the validator. - if (await _grantService.Get(GrantTypes.RequestUri, authCallbackRequest.request_uri, authCallbackRequest.client_id) is not RequestUriGrant requestUriGrant) + if (await _grantService.Get(GrantTypes.RequestUri, authCallbackRequest.Request_uri, authCallbackRequest.Client_id) is not RequestUriGrant) { - _logger.LogError("requestUriGrant for request_uri:{Uri} for client:{Id} not found", authCallbackRequest.request_uri, authCallbackRequest.client_id); + _logger.LogError("requestUriGrant for request_uri:{Uri} for client:{Id} not found", authCallbackRequest.Request_uri, authCallbackRequest.Client_id); throw new InvalidOperationException($"requestUriGrant is null or not found"); } return await ProcessAuthResponse( validationResult.ValidatedAuthorizationRequestObject, - authCallbackRequest.subject_id, - authCallbackRequest.account_ids.Split(','), + authCallbackRequest.Subject_id, + authCallbackRequest.Account_ids.Split(','), configOptions); } @@ -151,23 +152,24 @@ public async Task AuthoriseConfirm( var authRequest = new AuthorizeRequest() { - client_id = clientId, - request_uri = requestUri, + Client_id = clientId, + Request_uri = requestUri, }; var validationResult = await _authorizeRequestValidator.Validate(authRequest, configOptions, false); if (!validationResult.IsValid) { _logger.LogInformation("Authorization failed {@ValidationResult}", validationResult); - return await CallbackErrorResponse(validationResult, authRequest.client_id, configOptions); + return await CallbackErrorResponse(validationResult, authRequest.Client_id, configOptions); } // Retrieve the request uri grant, this has already been validated in the validator. var grant = await _grantService.Get(GrantTypes.RequestUri, requestUri, clientId) as RequestUriGrant; if (grant == null) { - _logger.LogError("requestUriGrant for request_uri:{Uri} for client:{Id} not found", - requestUri.Replace(Environment.NewLine, ""), - clientId.Replace(Environment.NewLine, "")); + _logger.LogError( + "requestUriGrant for request_uri:{Uri} for client:{Id} not found", + requestUri.Replace(Environment.NewLine, string.Empty), + clientId.Replace(Environment.NewLine, string.Empty)); throw new InvalidOperationException("requestUriGrant is null or not found"); } @@ -210,28 +212,14 @@ private async Task ProcessAuthResponse( Key = Guid.NewGuid().ToString(), Scope = FilterScopes(authRequestObject.Scope, configOptions), SubjectId = subjectId, - AccountIdDelimitedList = String.Join(',', accountIds) + AccountIdDelimitedList = string.Join(',', accountIds), }; await _grantService.Create(grant); _logger.LogInformation("created AuthorizationCodeGrant for client:{Id} subjectid:{Subid}", authRequestObject.ClientId, subjectId); string? idToken = null; - // If using hybrid flow then we need to generate an id_token to return from authorisation endpoint. - if (authRequestObject.ResponseType == ResponseTypes.Hybrid) - { - idToken = await _tokenService.IssueIdToken( - authRequestObject.ClientId, - subjectId, - configOptions, - true, - state: authRequestObject.State, - nonce: authRequestObject.Nonce, - authCode: grant.Key); - _logger.LogInformation("created Id token for client:{Id} subjectid:{Subid}", authRequestObject.ClientId, subjectId); - } - - var client = await _clientService.Get(authRequestObject.ClientId); + var client = await _clientService.Get(authRequestObject.ClientId) ?? throw new InvalidDataException("client not found"); return await CallbackResponse(authRequestObject, client, configOptions, grant, idToken); } @@ -239,32 +227,19 @@ private static string FilterScopes(string scope, ConfigurationOptions configOpti { var scopes = scope.Split(' '); return string.Join(" ", scopes.Where( - s => configOptions.ScopesSupported.Contains(s) && !configOptions.ClientCredentialScopesSupported.Contains(s))); + s => configOptions.ScopesSupported != null && configOptions.ScopesSupported.Contains(s) && configOptions.ClientCredentialScopesSupported != null && !configOptions.ClientCredentialScopesSupported.Contains(s))); } private async Task CallbackResponse( - AuthorizationRequestObject requestObject, - Client client, - ConfigurationOptions configOptions, - AuthorizationCodeGrant grant, - string? idToken = null) + AuthorizationRequestObject requestObject, + Client client, + ConfigurationOptions configOptions, + AuthorizationCodeGrant grant, + string? idToken = null) { // Send the error back to the client using query mode. - if (requestObject.ResponseMode.StartsWith("query")) - { - var queryUri = await BuildQueryUri(requestObject, client, configOptions, grant.Key, idToken); - return Redirect(queryUri); - } - - // Send the error back to the client using form_post mode. - if (requestObject.ResponseMode.StartsWith("form_post")) - { - return await FormPostCallback(requestObject, client, configOptions, grant.Key, idToken); - } - - // Send the error back to the client using fragment mode. - var fragmentUri = await BuildFragmentUri(requestObject, client, configOptions, grant.Key, idToken); - return Redirect(fragmentUri); + var queryUri = await BuildQueryUri(requestObject, client, configOptions, grant.Key, idToken); + return Redirect(queryUri); } private async Task CallbackErrorResponse( @@ -279,36 +254,11 @@ private async Task CallbackErrorResponse( } // Get the client so the registration metadata can be used. - var client = await _clientService.Get(clientId); + var client = await _clientService.Get(clientId) ?? throw new InvalidDataException("client not found"); // Send the error back to the client using query mode. - if (result.ValidatedAuthorizationRequestObject.ResponseMode.StartsWith("query")) - { - var queryUri = await BuildQueryUri(result.ValidatedAuthorizationRequestObject, client, configOptions, error: result.Error, errorDescription: result.ErrorDescription); - return Redirect(queryUri); - } - - // Send the error back to the client using form_post mode. - if (result.ValidatedAuthorizationRequestObject.ResponseMode.StartsWith("form_post")) - { - return await FormPostCallback(result.ValidatedAuthorizationRequestObject, client, configOptions, error: result.Error, errorDescription: result.ErrorDescription); - } - - // Send the error back to the client using fragment mode. - var fragmentUri = await BuildFragmentUri(result.ValidatedAuthorizationRequestObject, client, configOptions, error: result.Error, errorDescription: result.ErrorDescription); - return Redirect(fragmentUri); - } - - private async Task BuildFragmentUri( - AuthorizationRequestObject requestObject, - Client client, - ConfigurationOptions configOptions, - string? authCode = null, - string? idToken = null, - string? error = null, - string? errorDescription = null) - { - return $"{requestObject.RedirectUri}#{await BuildQueryString(requestObject, client, configOptions, authCode, idToken, error, errorDescription)}"; + var queryUri = await BuildQueryUri(result.ValidatedAuthorizationRequestObject, client, configOptions, error: result.Error, errorDescription: result.ErrorDescription); + return Redirect(queryUri); } private async Task BuildQueryUri( @@ -325,66 +275,8 @@ private async Task BuildQueryUri( { delimiter = '&'; } - return $"{requestObject.RedirectUri}{delimiter}{await BuildQueryString(requestObject, client, configOptions, authCode, idToken, error, errorDescription)}"; - } - - private async Task FormPostCallback( - AuthorizationRequestObject requestObject, - Client client, - ConfigurationOptions configOptions, - string? authCode = null, - string? idToken = null, - string? error = null, - string? errorDescription = null) - { - var html = new StringBuilder(); - html.AppendLine(""); - html.AppendLine(""); - html.AppendLine(""); - - // Build the form to post back to the callback. - html.AppendLine($@"
"); - - // Add hidden fields that will be posted back to the callback. - if (requestObject.ResponseMode.EndsWith("jwt")) - { - // JARM response. - html.AppendLine($@""); - } - else - { - if (!string.IsNullOrEmpty(authCode)) - { - html.AppendLine($@""); - } - if (!string.IsNullOrEmpty(idToken)) - { - html.AppendLine($@""); - } - - if (!string.IsNullOrEmpty(error)) - { - html.AppendLine($@""); - } - - if (!string.IsNullOrEmpty(errorDescription)) - { - html.AppendLine($@""); - } - - if (!string.IsNullOrEmpty(requestObject.State)) - { - html.AppendLine($@""); - } - } - - html.AppendLine(@""); - html.AppendLine("
"); - html.AppendLine(""); - html.AppendLine(""); - html.AppendLine(""); - return Content(html.ToString(), "text/html"); + return $"{requestObject.RedirectUri}{delimiter}{await BuildQueryString(requestObject, client, configOptions, authCode, idToken, error, errorDescription)}"; } private async Task BuildQueryString( @@ -399,7 +291,7 @@ private async Task BuildQueryString( // JARM response. if (requestObject.ResponseMode.EndsWith("jwt")) { - return $"response={(await BuildJarmResponse(requestObject, client, configOptions, authCode, error, errorDescription))}"; + return $"response={await BuildJarmResponse(requestObject, client, configOptions, authCode, error, errorDescription)}"; } var queryString = new StringBuilder(); @@ -472,7 +364,7 @@ private async Task BuildJarmResponse( { // Get the client enc jwk. var jwks = await _clientService.GetJwks(client); - clientJwk = jwks.Keys.First(jwk => jwk.Alg == client.AuthorizationEncryptedResponseAlg); + clientJwk = jwks?.Keys.First(jwk => jwk.Alg == client.AuthorizationEncryptedResponseAlg); encryptedResponseAlg = client.AuthorizationEncryptedResponseAlg; encryptedResponseEnc = client.AuthorizationEncryptedResponseEnc; } @@ -483,7 +375,7 @@ private async Task BuildJarmResponse( TokenTypes.Jwt, 300, configOptions, - signingAlg: client.AuthorizationSignedResponseAlg, + signingAlg: client.AuthorizationSignedResponseAlg ?? string.Empty, encryptedResponseAlg: encryptedResponseAlg, encryptedResponseEnc: encryptedResponseEnc, clientJwk: clientJwk); @@ -511,7 +403,7 @@ private IActionResult ShowError(AuthorizeRequestValidationResult result) /// This is used to generate html that can be used to Confirm/Cancel an authorisation /// request when in HeadlessMode. /// - /// IActionResult + /// IActionResult. private IActionResult OutputAuthConfirmation( string confirmationUri, string requestUri, diff --git a/Source/CdrAuthServer/Controllers/DiscoveryController.cs b/Source/CdrAuthServer/Controllers/DiscoveryController.cs index aa14395..30ac529 100644 --- a/Source/CdrAuthServer/Controllers/DiscoveryController.cs +++ b/Source/CdrAuthServer/Controllers/DiscoveryController.cs @@ -1,7 +1,8 @@ using CdrAuthServer.Extensions; using CdrAuthServer.Models; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Controllers { @@ -39,8 +40,8 @@ public JsonResult GetDiscoveryDocument() ClaimsSupported = configOptions.ClaimsSupported, CodeChallengeMethodsSupported = configOptions.CodeChallengeMethodsSupported, GrantTypesSupported = configOptions.GrantTypesSupported, - ScopesSupported = configOptions.ScopesSupported.Union(configOptions.ClientCredentialScopesSupported).ToList(), - ResponseModesSupported = configOptions.ResponseModesSupported, + ScopesSupported = (configOptions.ScopesSupported ?? Enumerable.Empty()).Union(configOptions.ClientCredentialScopesSupported ?? Enumerable.Empty()).ToList(), + ResponseModesSupported = [ResponseModes.Jwt], ResponseTypesSupported = configOptions.ResponseTypesSupported, SubjectTypesSupported = configOptions.SubjectTypesSupported, RequirePushedAuthorizationRequests = true, @@ -51,9 +52,7 @@ public JsonResult GetDiscoveryDocument() TokenEndpointAuthMethodsSupported = configOptions.TokenEndpointAuthMethodsSupported, TokenEndpointAuthSigningAlgValuesSupported = configOptions.TokenEndpointAuthSigningAlgValuesSupported, IdTokenSigningAlgValuesSupported = configOptions.IdTokenSigningAlgValuesSupported, - IdTokenEncryptionAlgValuesSupported = configOptions.IdTokenEncryptionAlgValuesSupported, - IdTokenEncryptionEncValuesSupported = configOptions.IdTokenEncryptionEncValuesSupported, - AuthorizationSigningAlgValuesSupported = configOptions.AuthorizationSigningAlgValuesSupported, + AuthorizationSigningAlgValuesSupported = configOptions.AuthorizationSigningAlgValuesSupported, }; if (configOptions.SupportJarmEncryption) @@ -63,7 +62,7 @@ public JsonResult GetDiscoveryDocument() } // Switch to out the "mtls_endpoint_aliases" property in the discovery document. - if (configOptions.UseMtlsEndpointAliases) + if (configOptions.UseMtlsEndpointAliases) { model.MtlsEndpointAliases = new Dictionary { @@ -72,7 +71,7 @@ public JsonResult GetDiscoveryDocument() { "introspection_endpoint", model.IntrospectionEndpoint }, { "pushed_authorization_request_endpoint", model.PushedAuthorizationEndpoint }, { "registration_endpoint", model.RegistrationEndpoint }, - { "userinfo_endpoint", model.UserinfoEndpoint } + { "userinfo_endpoint", model.UserinfoEndpoint }, }; // Set the mTLS endpoints back to their TLS equivalents. @@ -84,7 +83,7 @@ public JsonResult GetDiscoveryDocument() model.UserinfoEndpoint = model.UserinfoEndpoint.Replace(configOptions.SecureBaseUri, configOptions.BaseUri); } - return new JsonResult(model); + return new JsonResult(model, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); } } } diff --git a/Source/CdrAuthServer/Controllers/IntrospectionController.cs b/Source/CdrAuthServer/Controllers/IntrospectionController.cs index 7bfefd1..b7d04c0 100644 --- a/Source/CdrAuthServer/Controllers/IntrospectionController.cs +++ b/Source/CdrAuthServer/Controllers/IntrospectionController.cs @@ -1,11 +1,10 @@ -using CdrAuthServer.Configuration; +using System.ComponentModel.DataAnnotations; +using System.IdentityModel.Tokens.Jwt; using CdrAuthServer.Extensions; using CdrAuthServer.Models; using CdrAuthServer.Services; using CdrAuthServer.Validation; using Microsoft.AspNetCore.Mvc; -using System.ComponentModel.DataAnnotations; -using System.IdentityModel.Tokens.Jwt; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Controllers @@ -71,7 +70,7 @@ public async Task Introspect([FromForm] string token) IsActive = true, CdrArrangementId = grant.CdrArrangementId ?? string.Empty, Expiry = grant.ExpiresAt.ToEpoch(), - Scope = grant.Scope + Scope = grant.Scope, }); } @@ -88,10 +87,10 @@ public async Task Introspect([FromForm] string token) /// by the resource API of the mock data holder. /// In the CDS, the introspection endpoint only supports the introspection of refresh tokens. /// - /// Access token to check - /// IntrospectionResult + /// Access token to check. + /// IntrospectionResult. /// - /// There is currently no auth on this endpoint. + /// There is currently no auth on this endpoint. /// This could be added in the future to only allow the calls from the Mock Data Holder Resource API. /// [HttpPost] @@ -141,7 +140,7 @@ public async Task IntrospectInternal([Required, FromForm] string // Check software product status (if configured). var configOptions = _config.GetConfigurationOptions(this.HttpContext); - if (configOptions.CdrRegister.CheckSoftwareProductStatus) + if (configOptions.CdrRegister != null && configOptions.CdrRegister.CheckSoftwareProductStatus) { var client = await _clientService.Get(clientIdFromAccessToken); if (client == null) @@ -154,6 +153,7 @@ public async Task IntrospectInternal([Required, FromForm] string { return ErrorCatalogue.Catalogue().GetErrorResponse(ErrorCatalogue.SOFTWARE_PRODUCT_NOT_FOUND); } + if (!softwareProduct.IsActive()) { return ErrorCatalogue.Catalogue().GetErrorResponse(ErrorCatalogue.SOFTWARE_PRODUCT_STATUS_INACTIVE, softwareProduct.GetStatusDescription()); @@ -173,7 +173,7 @@ public async Task IntrospectInternal([Required, FromForm] string _logger.LogError("arrangement was not found: {CdrArrangementId}", cdrArrangementId); return new JsonResult(new Introspection { - IsActive = false + IsActive = false, }); } @@ -182,21 +182,21 @@ public async Task IntrospectInternal([Required, FromForm] string _logger.LogError("arrangement version ({Version}) does not match arrangement version in the access token: {CdrArrangementVersion}", arrangement.Version, cdrArrangementVersion); return new JsonResult(new Introspection { - IsActive = false + IsActive = false, }); } // If the arrangement was not found, or has expired, or does not match the client id in the access token. return new JsonResult(new Introspection { - IsActive = (arrangement != null && !arrangement.IsExpired && !securityToken.IsExpired()) + IsActive = !arrangement.IsExpired && !securityToken.IsExpired(), }); } } return new JsonResult(new Introspection { - IsActive = true + IsActive = true, }); } } diff --git a/Source/CdrAuthServer/Controllers/JwksController.cs b/Source/CdrAuthServer/Controllers/JwksController.cs index 989d3b4..3f5b778 100644 --- a/Source/CdrAuthServer/Controllers/JwksController.cs +++ b/Source/CdrAuthServer/Controllers/JwksController.cs @@ -38,7 +38,7 @@ public async Task GetJwks() /// /// Generate a JWKS for the Auth Server. /// - /// JsonWebKeySet + /// JsonWebKeySet. private async Task GenerateJwks() { _logger.LogInformation("{JwksController}.{GenerateJwks}", nameof(JwksController), nameof(GenerateJwks)); @@ -57,7 +57,7 @@ public async Task GetJwks() var jwks = new Models.JsonWebKeySet() { - keys = [ps256jwk, es256jwk] + keys = [ps256jwk, es256jwk], }; // Add the jwks to the cache. @@ -74,9 +74,9 @@ public async Task GetJwks() // Get credentials from certificate var securityKey = new X509SecurityKey(cert); var signingCredentials = new X509SigningCredentials(cert, SecurityAlgorithms.RsaSsaPssSha256); - var rsaParams = signingCredentials.Certificate.GetRSAPublicKey().ExportParameters(false); - var e = Base64UrlEncoder.Encode(rsaParams.Exponent); - var n = Base64UrlEncoder.Encode(rsaParams.Modulus); + var rsaParams = signingCredentials.Certificate.GetRSAPublicKey()?.ExportParameters(false); + var e = Base64UrlEncoder.Encode(rsaParams?.Exponent); + var n = Base64UrlEncoder.Encode(rsaParams?.Modulus); var jwk = new Models.JsonWebKey() { @@ -87,16 +87,15 @@ public async Task GetJwks() n = n, e = e, x5t = securityKey.X5t, - x5c = [Convert.ToBase64String(cert.RawData)] + x5c = cert != null ? [Convert.ToBase64String(cert.RawData)] : [], }; return jwk; } private async Task GetES256Jwk() { - var cert = await _config.GetES256SigningCertificate(); - - var ecdsa = cert.GetECDsaPrivateKey(); + var cert = await _config.GetES256SigningCertificate() ?? throw new InvalidOperationException("Signing certificate not found."); + var ecdsa = cert.GetECDsaPrivateKey() ?? throw new InvalidOperationException("ECDSA private key not found."); var securityKey = new ECDsaSecurityKey(ecdsa) { KeyId = cert.Thumbprint }; var parameters = ecdsa.ExportParameters(false); var x = Base64UrlEncoder.Encode(parameters.Q.X); @@ -112,7 +111,7 @@ public async Task GetJwks() y = y, crv = GetCrvValue(parameters.Curve), x5t = Base64UrlEncoder.Encode(cert.GetCertHash()), - x5c = new string[] { Convert.ToBase64String(cert.RawData) } + x5c = [Convert.ToBase64String(cert.RawData)], }; return jwk; @@ -128,12 +127,11 @@ private string GetCrvValue(ECCurve curve) return JsonWebKeyECTypes.P384; case "1.3.132.0.35": return JsonWebKeyECTypes.P521; - }; + } _logger.LogError("Unsupported curve type of {Value} - {FriendlyName}", curve.Oid.Value, curve.Oid.FriendlyName); throw new InvalidOperationException($"Unsupported curve type of {curve.Oid.Value} - {curve.Oid.FriendlyName}"); } - } } diff --git a/Source/CdrAuthServer/Controllers/PushedAuthorisationRequestController.cs b/Source/CdrAuthServer/Controllers/PushedAuthorisationController.cs similarity index 93% rename from Source/CdrAuthServer/Controllers/PushedAuthorisationRequestController.cs rename to Source/CdrAuthServer/Controllers/PushedAuthorisationController.cs index ca37e8f..7ba9398 100644 --- a/Source/CdrAuthServer/Controllers/PushedAuthorisationRequestController.cs +++ b/Source/CdrAuthServer/Controllers/PushedAuthorisationController.cs @@ -60,11 +60,11 @@ public async Task PushedAuthorisationRequest( var clientId = User.GetClientId(); var configOptions = _config.GetConfigurationOptions(this.HttpContext); - var (result, validatedRequest) = await _parValidator.Validate(clientId, request, configOptions); + var (result, validatedRequest) = await _parValidator.Validate(clientId ?? string.Empty, request, configOptions); if (!result.IsValid) { _logger.LogError("parvalidator returned error:{Error} errordescription:{Desc}", result.Error, result.ErrorDescription); - return new JsonResult(new Error(result.Error, result.ErrorDescription)) { StatusCode = result.StatusCode }; + return new JsonResult(new Error(result.Error ?? string.Empty, result.ErrorDescription)) { StatusCode = result.StatusCode }; } // Create the par grant. @@ -72,10 +72,10 @@ public async Task PushedAuthorisationRequest( { GrantType = GrantTypes.RequestUri, Key = $"urn:{Guid.NewGuid()}", - ClientId = clientId, + ClientId = clientId ?? string.Empty, CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddSeconds(options.RequestUriExpirySeconds), - Request = JsonConvert.SerializeObject(validatedRequest) + Request = JsonConvert.SerializeObject(validatedRequest), }; await _grantService.Create(parGrant); @@ -88,6 +88,5 @@ public async Task PushedAuthorisationRequest( return new JsonResult(parCreatedResponse) { StatusCode = 201 }; } - } } diff --git a/Source/CdrAuthServer/Controllers/RegistrationController.cs b/Source/CdrAuthServer/Controllers/RegistrationController.cs index 9a67edf..7bc4323 100644 --- a/Source/CdrAuthServer/Controllers/RegistrationController.cs +++ b/Source/CdrAuthServer/Controllers/RegistrationController.cs @@ -1,4 +1,5 @@ -using CdrAuthServer.Authorisation; +using System.ComponentModel.DataAnnotations; +using CdrAuthServer.Authorisation; using CdrAuthServer.Configuration; using CdrAuthServer.Extensions; using CdrAuthServer.Infrastructure; @@ -10,7 +11,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; -using System.ComponentModel.DataAnnotations; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Controllers @@ -76,13 +76,13 @@ public async Task CreateRegistration(ClientRegistrationRequest re var result = await _clientRegistrationValidator.Validate(request, configOptions); if (!result.IsValid) { - return new BadRequestObjectResult(new Error(result.Error, result.ErrorDescription)); + return new BadRequestObjectResult(new Error(result.Error ?? string.Empty, result.ErrorDescription)); } // Check if the software product has already been registered. if (!configOptions.AllowDuplicateRegistrations) { - var existingClient = await _clientService.GetClientBySoftwareProductId(request.SoftwareStatement.SoftwareId); + var existingClient = await _clientService.GetClientBySoftwareProductId(request.SoftwareStatement?.SoftwareId ?? string.Empty); if (existingClient != null) { return ErrorCatalogue.Catalogue().GetErrorResponse(ErrorCatalogue.DUPLICATE_REGISTRATION); @@ -130,7 +130,7 @@ public async Task UpdateRegistration( if (!result.IsValid) { _logger.LogError("client registration validation failed with error:{Error} errordescription:{Desc}", clientIdResult.Error, clientIdResult.ErrorDescription); - return new BadRequestObjectResult(new Error(result.Error, result.ErrorDescription)); + return new BadRequestObjectResult(new Error(result.Error ?? string.Empty, result.ErrorDescription)); } var client = Map(request, clientId, configOptions); @@ -190,34 +190,32 @@ private Client Map(ClientRegistrationRequest request, string clientId, Configura var client = new Client() { ClientId = clientId, - ClientName = request.SoftwareStatement.ClientName ?? string.Empty, - ClientDescription = request.SoftwareStatement.ClientDescription ?? string.Empty, + ClientName = request.SoftwareStatement?.ClientName ?? string.Empty, + ClientDescription = request.SoftwareStatement?.ClientDescription ?? string.Empty, ApplicationType = request.ApplicationType ?? "web", - ClientUri = request.SoftwareStatement.ClientUri ?? string.Empty, + ClientUri = request.SoftwareStatement?.ClientUri ?? string.Empty, GrantTypes = request.GrantTypes, ResponseTypes = request.ResponseTypes, - RedirectUris = (request.RedirectUris == null || !request.RedirectUris.Any()) ? request.SoftwareStatement.RedirectUris : request.RedirectUris, - Scope = request.SoftwareStatement.Scope ?? "openid profile cdr:registration", - LegalEntityId = request.SoftwareStatement.LegalEntityId ?? string.Empty, - LegalEntityName = request.SoftwareStatement.LegalEntityName ?? string.Empty, - OrgId = request.SoftwareStatement.OrgId ?? string.Empty, - OrgName = request.SoftwareStatement.OrgName ?? string.Empty, - LogoUri = request.SoftwareStatement.LogoUri ?? string.Empty, - TosUri = request.SoftwareStatement.TosUri ?? string.Empty, - PolicyUri = request.SoftwareStatement.PolicyUri ?? string.Empty, - RecipientBaseUri = request.SoftwareStatement.RecipientBaseUri ?? string.Empty, - SectorIdentifierUri = request.SoftwareStatement.SectorIdentifierUri ?? null, - RevocationUri = request.SoftwareStatement.RevocationUri ?? string.Empty, - JwksUri = request.SoftwareStatement.JwksUri ?? string.Empty, - SoftwareId = request.SoftwareStatement.SoftwareId ?? string.Empty, - RequestObjectSigningAlg = request.RequestObjectSigningAlg ?? configOptions.RequestObjectSigningAlgValuesSupported.First(), - IdTokenEncryptedResponseAlg = request.IdTokenEncryptedResponseAlg, - IdTokenEncryptedResponseEnc = request.IdTokenEncryptedResponseEnc, - IdTokenSignedResponseAlg = request.IdTokenSignedResponseAlg ?? configOptions.IdTokenSigningAlgValuesSupported.First(), - TokenEndpointAuthMethod = request.TokenEndpointAuthMethod ?? configOptions.TokenEndpointAuthMethodsSupported.First(), - TokenEndpointAuthSigningAlg = request.TokenEndpointAuthSigningAlg ?? configOptions.TokenEndpointAuthSigningAlgValuesSupported.First(), + RedirectUris = (request.RedirectUris == null || !request.RedirectUris.Any()) ? (request.SoftwareStatement?.RedirectUris ?? []) : request.RedirectUris, + Scope = request.SoftwareStatement?.Scope ?? "openid profile cdr:registration", + LegalEntityId = request.SoftwareStatement?.LegalEntityId ?? string.Empty, + LegalEntityName = request.SoftwareStatement?.LegalEntityName ?? string.Empty, + OrgId = request.SoftwareStatement?.OrgId ?? string.Empty, + OrgName = request.SoftwareStatement?.OrgName ?? string.Empty, + LogoUri = request.SoftwareStatement?.LogoUri ?? string.Empty, + TosUri = request.SoftwareStatement?.TosUri ?? string.Empty, + PolicyUri = request.SoftwareStatement?.PolicyUri ?? string.Empty, + RecipientBaseUri = request.SoftwareStatement?.RecipientBaseUri ?? string.Empty, + SectorIdentifierUri = request.SoftwareStatement?.SectorIdentifierUri ?? null, + RevocationUri = request.SoftwareStatement?.RevocationUri ?? string.Empty, + JwksUri = request.SoftwareStatement?.JwksUri ?? string.Empty, + SoftwareId = request.SoftwareStatement?.SoftwareId ?? string.Empty, + RequestObjectSigningAlg = request.RequestObjectSigningAlg ?? (configOptions.RequestObjectSigningAlgValuesSupported != null ? configOptions.RequestObjectSigningAlgValuesSupported[0] : string.Empty), + IdTokenSignedResponseAlg = request.IdTokenSignedResponseAlg ?? (configOptions.IdTokenSigningAlgValuesSupported != null ? configOptions.IdTokenSigningAlgValuesSupported[0] : string.Empty), + TokenEndpointAuthMethod = request.TokenEndpointAuthMethod ?? (configOptions.TokenEndpointAuthMethodsSupported != null ? configOptions.TokenEndpointAuthMethodsSupported[0] : string.Empty), + TokenEndpointAuthSigningAlg = request.TokenEndpointAuthSigningAlg ?? (configOptions.TokenEndpointAuthSigningAlgValuesSupported != null ? configOptions.TokenEndpointAuthSigningAlgValuesSupported[0] : string.Empty), SoftwareStatementJwt = request.SoftwareStatementJwt ?? string.Empty, - SoftwareRoles = request.SoftwareStatement.SoftwareRoles ?? string.Empty + SoftwareRoles = request.SoftwareStatement?.SoftwareRoles ?? string.Empty, }; client.AuthorizationSignedResponseAlg = null; @@ -232,8 +230,8 @@ private Client Map(ClientRegistrationRequest request, string clientId, Configura // If JARM encryption is supported. if (configOptions.SupportJarmEncryption) { - client.AuthorizationEncryptedResponseAlg = String.IsNullOrEmpty(request.AuthorizationEncryptedResponseAlg) ? configOptions.AuthorizationEncryptionAlgValuesSupportedList.FirstOrDefault() : request.AuthorizationEncryptedResponseAlg; - client.AuthorizationEncryptedResponseEnc = String.IsNullOrEmpty(request.AuthorizationEncryptedResponseEnc) ? configOptions.AuthorizationEncryptionEncValuesSupportedList.FirstOrDefault() : request.AuthorizationEncryptedResponseEnc; + client.AuthorizationEncryptedResponseAlg = string.IsNullOrEmpty(request.AuthorizationEncryptedResponseAlg) ? configOptions.AuthorizationEncryptionAlgValuesSupportedList?[0] : request.AuthorizationEncryptedResponseAlg; + client.AuthorizationEncryptedResponseEnc = string.IsNullOrEmpty(request.AuthorizationEncryptedResponseEnc) ? configOptions.AuthorizationEncryptionEncValuesSupportedList?[0] : request.AuthorizationEncryptedResponseEnc; } } diff --git a/Source/CdrAuthServer/Controllers/ResourceController.cs b/Source/CdrAuthServer/Controllers/ResourceController.cs index 1c3d65e..9415aa8 100644 --- a/Source/CdrAuthServer/Controllers/ResourceController.cs +++ b/Source/CdrAuthServer/Controllers/ResourceController.cs @@ -13,7 +13,7 @@ namespace CdrAuthServer.Controllers /// ensure that the auth server is issuing access tokens correctly. /// [ApiController] - public class ResourceController : Controller + public class ResourceController : ControllerBase { private readonly ILogger _logger; private readonly IConfiguration _config; @@ -38,7 +38,7 @@ public IActionResult GetCustomer() if (_config.GetValue("CdrAuthServer:ValidateResourceEndpoint", true)) { - // Add validation for the resource endpoint. + // Add validation for the resource endpoint. var (isValidAuthDate, authDateError, authDateStatusCode) = HttpContext.Request.Headers.ValidateAuthDate(); if (!isValidAuthDate) { @@ -75,7 +75,8 @@ public IActionResult GetCustomer() { xFapiInterationId = Request.Headers["x-fapi-interaction-id"].ToString(); } - Response.Headers.Add("x-fapi-interaction-id", xFapiInterationId); + + Response.Headers.Append("x-fapi-interaction-id", xFapiInterationId); return Content(body, "application/json"); } @@ -93,9 +94,7 @@ public IActionResult GetAccounts(string industry) if (_config.GetValue("CdrAuthServer:ValidateResourceEndpoint", true)) { - // // Add validation for the resource endpoint. - // var (isValidAuthDate, authDateError, authDateStatusCode) = HttpContext.Request.Headers.ValidateAuthDate(); if (!isValidAuthDate) { @@ -120,7 +119,8 @@ public IActionResult GetAccounts(string industry) { xFapiInterationId = Request.Headers["x-fapi-interaction-id"].ToString(); } - Response.Headers.Add("x-fapi-interaction-id", xFapiInterationId); + + Response.Headers.Append("x-fapi-interaction-id", xFapiInterationId); return Content(body, "application/json"); } } diff --git a/Source/CdrAuthServer/Controllers/RevocationController.cs b/Source/CdrAuthServer/Controllers/RevocationController.cs index bb4b8c7..6b36ba6 100644 --- a/Source/CdrAuthServer/Controllers/RevocationController.cs +++ b/Source/CdrAuthServer/Controllers/RevocationController.cs @@ -1,8 +1,8 @@ -using CdrAuthServer.Extensions; +using System.IdentityModel.Tokens.Jwt; +using CdrAuthServer.Extensions; using CdrAuthServer.Services; using CdrAuthServer.Validation; using Microsoft.AspNetCore.Mvc; -using System.IdentityModel.Tokens.Jwt; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Controllers @@ -40,6 +40,7 @@ public async Task RevokeToken( if (refreshTokenGrant != null) { _logger.LogInformation("Revoked the refresh token by removing the refresh token grant for clientId:{Id}", clientId); + // Revoke the refresh token by removing the refresh token grant. await _grantService.Delete(clientId, GrantTypes.RefreshToken, token); return Ok(); @@ -67,8 +68,13 @@ public async Task RevokeToken( if (clientId != null && clientId.Equals(clientIdFromAccessToken, StringComparison.OrdinalIgnoreCase)) { - _logger.LogDebug("Revoking access token: {token}", token); - await _tokenService.AddToBlacklist(securityToken.Claims.GetClaimValue(ClaimNames.JwtId)); + _logger.LogDebug("Revoking access token: {Token}", token); + var cliamValue = securityToken.Claims.GetClaimValue(ClaimNames.JwtId); + + if (cliamValue != null) + { + await _tokenService.AddToBlacklist(cliamValue); + } return Ok(); } @@ -76,12 +82,11 @@ public async Task RevokeToken( } catch (Exception ex) { - _logger.LogError(ex, "An error occurred while revoking the access token: {token}", token); + _logger.LogError(ex, "An error occurred while revoking the access token: {Token}", token); } // Always return 200 OK. return Ok(); } - } } diff --git a/Source/CdrAuthServer/Controllers/TokenController.cs b/Source/CdrAuthServer/Controllers/TokenController.cs index ca65c7f..3d8f98d 100644 --- a/Source/CdrAuthServer/Controllers/TokenController.cs +++ b/Source/CdrAuthServer/Controllers/TokenController.cs @@ -47,14 +47,14 @@ public async Task IssueTokens([FromForm] TokenRequest tokenReques if (!validationResult.IsValid) { _logger.LogInformation("Validation failed - {@ValidationResult}", validationResult); - return new JsonResult(new Error(validationResult.Error, validationResult.ErrorDescription)) { StatusCode = validationResult.StatusCode ?? 400 }; + return new JsonResult(new Error(validationResult.Error ?? string.Empty, validationResult.ErrorDescription)) { StatusCode = validationResult.StatusCode ?? 400 }; } // Client Id is optional in the token request from the client but is required in the token repsonse. // If client Id is not provided in the request then use the client Id that was extracted from the client assertion. - if (string.IsNullOrEmpty(tokenRequest.client_id) && !string.IsNullOrEmpty(clientId)) + if (string.IsNullOrEmpty(tokenRequest.Client_id) && !string.IsNullOrEmpty(clientId)) { - tokenRequest.client_id = clientId; + tokenRequest.Client_id = clientId; } var cnf = GetClientCertificateThumbprint(); @@ -74,10 +74,10 @@ private string GetClientCertificateThumbprint() var configOptions = _config.GetConfigurationOptions(this.HttpContext); if (this.HttpContext.Request.Headers.TryGetValue(configOptions.ClientCertificateThumbprintHttpHeaderName, out StringValues headerThumbprints)) { - return headerThumbprints.First(); + return headerThumbprints[0] ?? string.Empty; } - return ""; + return string.Empty; } } } diff --git a/Source/CdrAuthServer/Controllers/UserInfoController.cs b/Source/CdrAuthServer/Controllers/UserInfoController.cs index 85da9c2..efc0292 100644 --- a/Source/CdrAuthServer/Controllers/UserInfoController.cs +++ b/Source/CdrAuthServer/Controllers/UserInfoController.cs @@ -50,7 +50,7 @@ public async Task GetUserInfo() } // Check software product status (if configured). - if (configOptions.CdrRegister.CheckSoftwareProductStatus) + if (configOptions.CdrRegister != null && configOptions.CdrRegister.CheckSoftwareProductStatus) { var softwareProductId = client.SoftwareId; var softwareProduct = await _cdrService.GetSoftwareProduct(softwareProductId); @@ -59,6 +59,7 @@ public async Task GetUserInfo() _logger.LogInformation("Software Product not found {SoftwareProductId}", softwareProductId); return ErrorCatalogue.Catalogue().GetErrorResponse(ErrorCatalogue.SOFTWARE_PRODUCT_NOT_FOUND); } + if (!softwareProduct.IsActive()) { _logger.LogInformation("Software product status is removed - consents cannot be revoked {SoftwareProductId}", softwareProductId); @@ -70,7 +71,7 @@ public async Task GetUserInfo() { Audience = User.GetClientId() ?? string.Empty, Issuer = User.GetIssuer() ?? string.Empty, - Subject = User.GetSubject() ?? string.Empty + Subject = User.GetSubject() ?? string.Empty, }; if (configOptions.HeadlessMode) @@ -84,11 +85,11 @@ public async Task GetUserInfo() } else { - var subjectId = User.GetSubject() + var subjectId = User?.GetSubject()? .DecryptSub(client, _config); // Get customer login details from seed data file instead - var customer = await _customerService.Get(subjectId); + var customer = await _customerService.Get(subjectId ?? string.Empty); userInfo.FamilyName = customer.FamilyName; userInfo.GivenName = customer.GivenName; userInfo.Name = customer.Name; diff --git a/Source/CdrAuthServer/Controllers/UtilityController.cs b/Source/CdrAuthServer/Controllers/UtilityController.cs index 548fa90..feab689 100644 --- a/Source/CdrAuthServer/Controllers/UtilityController.cs +++ b/Source/CdrAuthServer/Controllers/UtilityController.cs @@ -1,59 +1,51 @@ -using CdrAuthServer.Domain.Models; +using System.Net; +using CdrAuthServer.Domain.Models; using CdrAuthServer.Extensions; -using CdrAuthServer.Infrastructure.Models; using CdrAuthServer.Models; using CdrAuthServer.Services; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using System.IdentityModel.Tokens.Jwt; -using System.Net; -using System.Net.Http.Headers; -using System.Security.Claims; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Controllers { [ApiController] [Route("utility")] - public class UtilityController : Controller + public class UtilityController : ControllerBase { - private readonly IConfiguration _config; private readonly ILogger _logger; private readonly IGrantService _grantService; private readonly IClientService _clientService; - private readonly HttpClient _httpClient; + private readonly IConsentRevocationService _consentRevocationService; + private readonly TimeSpan _timeout = TimeSpan.FromSeconds(30); public UtilityController( - IConfiguration config, ILogger logger, - HttpClient httpClient, IGrantService grantService, - IClientService clientService) + IClientService clientService, + IConsentRevocationService consentRevocationService) { - _config = config; _logger = logger; - _httpClient = httpClient; _grantService = grantService; _clientService = clientService; + _consentRevocationService = consentRevocationService; } /// /// This controller method is provided to delete the arrangement and refreshtoken in authserver and /// trigger an arrangement revocation at a data recipient. - /// Normally, this would be done from the DH dashboard. + /// Normally, this would be done from the DH dashboard. /// However, until a dashboard is in place this method can be used to trigger a request. /// - /// IActionResult + /// IActionResult. /// /// Note: this controller action would not be implemented in a production system and is provided for testing purposes. /// [HttpGet] [Route("dr/revoke-arrangement-jwt/{cdrArrangementId}")] [ApiVersionNeutral] - public async Task RemoveArrangementAndTriggerDataRecipientArrangementRevocation(string cdrArrangementId) + public async Task RemoveArrangementAndTriggerDataRecipientArrangementRevocation(string cdrArrangementId, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(cdrArrangementId)) { @@ -61,15 +53,18 @@ public async Task RemoveArrangementAndTriggerDataRecipientArrange return BadRequest(new ResponseErrorList().AddInvalidField(nameof(cdrArrangementId))); } - var (adrRevocationResponse, cdsErrorList, httpStatusCode) = await RemoveArrangementAndSendRevocationRequestAsync(cdrArrangementId); + var (adrRevocationResponse, cdsErrorList, httpStatusCode) = await RemoveArrangementAndSendRevocationRequestAsync(cdrArrangementId, cancellationToken); - if (cdsErrorList?.Errors.Count!=0) + if (cdsErrorList?.Errors.Count > 0) { return StatusCode((int)httpStatusCode, cdsErrorList); } - var jsonResponse = JsonConvert.SerializeObject(adrRevocationResponse, Formatting.Indented, + var jsonResponse = JsonConvert.SerializeObject( + adrRevocationResponse, + Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Include, ContractResolver = new CamelCasePropertyNamesContractResolver() }); + return Ok(jsonResponse); } @@ -78,18 +73,20 @@ private static AdrArrangementRevocationResponse GetRevocationResponse(ArrangeRev return new() { ArrangeRevocationRequest = request, - ArrangeRevocationResponse = response + ArrangeRevocationResponse = response, }; } - private async Task<(AdrArrangementRevocationResponse?, ResponseErrorList?, HttpStatusCode statusCode)> RemoveArrangementAndSendRevocationRequestAsync(string cdrArrangementId) + private async Task<(AdrArrangementRevocationResponse? AdrRevocationResponse, ResponseErrorList? CdsErrorList, HttpStatusCode StatusCode)> RemoveArrangementAndSendRevocationRequestAsync(string cdrArrangementId, CancellationToken cancellationToken) { - HttpResponseMessage? httpResponse = null; + HttpResponseMessage? revocationResponseMessage; + ArrangeRevocationRequest? revocationRequestInfo; + string? errorMessage = null; // Find the CDR Arrangement Grant. var grant = await _grantService.Get(GrantTypes.CdrArrangement, cdrArrangementId); - // "cdr_arrangement_grant" grant not found for given id. + // "cdr_arrangement_grant" grant not found for given id. if (grant == null) { _logger.LogError("cdrArrangementId:{Id} not found to revoke", cdrArrangementId); @@ -101,107 +98,49 @@ private static AdrArrangementRevocationResponse GetRevocationResponse(ArrangeRev if (client == null) { _logger.LogError("client with Id:{Id} in the grant, not found", grant.ClientId); - return (null, new ResponseErrorList { Errors = { new CdsError(Domain.Constants.ErrorCodes.Cds.InvalidConsentArrangement, "Invalid client_id", grant.ClientId) } }, HttpStatusCode.InternalServerError); + return (null, new ResponseErrorList { Errors = { new CdsError(ErrorCodes.Cds.InvalidConsentArrangement, "Invalid client_id", grant.ClientId) } }, HttpStatusCode.InternalServerError); } - //get the httprequest with the values populated - var (revokeRequest, urlEncodedContent) = await PopulateRequestMessageForRevocationCall(client, cdrArrangementId); + // Revoke the arrangement locally + await RemoveArrangement(cdrArrangementId, client, (CdrArrangementGrant)grant); + + // Notify the data recipient + (var revocationRequestMessage, revocationResponseMessage, var exception) = await _consentRevocationService.RevokeAdrArrangement(client, cdrArrangementId, _timeout, cancellationToken); - using (revokeRequest) + // Add request and response details sent to ADR so that the invoker of this endpoint has this information for it's own logs. + revocationRequestInfo = await GetAdrRevokeRequestInfoAsync(revocationRequestMessage); + + if (exception is not null) { - //add request and response details sent to ADR to custom headers. - var revocRequestInfo = await GetAdrRevokeRequestInfoAsync(revokeRequest, _httpClient.DefaultRequestHeaders, urlEncodedContent); - - try - { - //Revoke the arrangement before notifying the Data Recipient - await RemoveArrangement(cdrArrangementId, client, (CdrArrangementGrant)grant); - - // Call the DR's arrangement revocation endpoint. - using var cts = new CancellationTokenSource(new TimeSpan(0, 0, 30)); - httpResponse = await _httpClient.SendAsync(revokeRequest, cts.Token).ConfigureAwait(false); - - _logger.LogInformation("Response from DR arrangement revocation endpoint: {HttpResponse}", httpResponse); - - return (GetRevocationResponse(revocRequestInfo, await GetAdrRevokeResponseInfoAsync(httpResponse)), null, HttpStatusCode.OK); - } - catch (TaskCanceledException ex) - { - //return custom message when the httpclient timesout after set timeout of 30seconds instead of - //ex.Message of task canceled message. - _logger.LogError(ex, "Error revoking arrangement {ExceptionMessage}", ex.Message); - - var errorMessage = "The operation was cancelled as the ADR did not respond within the timeout period of 30 seconds."; - - return (GetRevocationResponse(revocRequestInfo, await GetAdrRevokeResponseInfoAsync(httpResponse, errorMessage)), null, HttpStatusCode.OK); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error revoking arrangement {ExceptionMessage}", ex.Message); - return (GetRevocationResponse(revocRequestInfo, await GetAdrRevokeResponseInfoAsync(httpResponse, ex.Message)), null, HttpStatusCode.OK); - } + _logger.LogError(exception, "Error revoking arrangement {ExceptionMessage}", exception.Message); + + // Change the error message when the request was cancelled due to timeout being exceeded or client cancelling the request. + errorMessage = exception is TaskCanceledException + ? $"The operation was cancelled as the ADR did not respond within the timeout period of {_timeout.Seconds} seconds." + : exception.Message; } + return (GetRevocationResponse(revocationRequestInfo, await GetAdrRevokeResponseInfoAsync(revocationResponseMessage, errorMessage)), null, HttpStatusCode.OK); } - //Remove arrangement and token from AuthServer DB + // Remove arrangement and token from AuthServer DB private async Task RemoveArrangement(string cdrArrangementId, Client client, CdrArrangementGrant cdrArrangementGrant) { // Delete the grants. - await _grantService.Delete(client.ClientId, GrantTypes.RefreshToken, cdrArrangementGrant.RefreshToken ?? ""); + await _grantService.Delete(client.ClientId, GrantTypes.RefreshToken, cdrArrangementGrant.RefreshToken ?? string.Empty); await _grantService.Delete(client.ClientId, GrantTypes.CdrArrangement, cdrArrangementGrant.Key); - _logger.LogInformation("Removed arrangement {Arrangmentid} and refresh token for client {Clientid}", cdrArrangementId, client.ClientId); + _logger.LogInformation("Removed arrangement {ArrangementId} and refresh token for client {ClientId}", cdrArrangementId, client.ClientId); } - private async Task GetSignedJwt(JwtPayload jwtPayload) - { - var cert = await _config.GetPS256SigningCertificate(); - var signingCredentials = new X509SigningCredentials(cert, SecurityAlgorithms.RsaSsaPssSha256); - - var jwtHeader = new JwtHeader( - signingCredentials: signingCredentials, - outboundAlgorithmMap: null, - tokenType: TokenTypes.Jwt); - - var jwt = new JwtSecurityToken(jwtHeader, jwtPayload); - var tokenHandler = new JwtSecurityTokenHandler(); - return tokenHandler.WriteToken(jwt); - } - - private async Task> GetFormValues( - string cdrArrangementId, - string brandId, - string audience) - { - var formValues = new Dictionary(); - var jwt = new JwtPayload( - issuer: brandId, - audience: audience, - notBefore: DateTime.UtcNow, - issuedAt: DateTime.UtcNow, - expires: DateTime.UtcNow.AddMinutes(5), - claims: new Claim[] { - new(ClaimNames.CdrArrangementId, cdrArrangementId), - new(ClaimNames.Subject, brandId), - new(ClaimNames.JwtId, Guid.NewGuid().ToString()) - }); - - formValues.Add("cdr_arrangement_jwt", (await GetSignedJwt(jwt))); - - _logger.LogInformation("Arrangement revocation request using {Form}:{Form_value} ", formValues.First().Key, formValues.First().Value); - - return formValues; - } - - private static async Task GetAdrRevokeRequestInfoAsync(HttpRequestMessage requestMessage, HttpHeaders requestHeaders, FormUrlEncodedContent? urlEncodeContent) + private static async Task GetAdrRevokeRequestInfoAsync(HttpRequestMessage requestMessage) { return new ArrangeRevocationRequest { - Body = await (urlEncodeContent?.ReadAsStringAsync() ?? Task.FromResult("")), - Headers = JsonConvert.SerializeObject(requestHeaders.ToDictionary(a => a.Key, a => a.Value)) ?? null, + Body = await (requestMessage.Content?.ReadAsStringAsync() ?? Task.FromResult(string.Empty)), + Headers = requestMessage.Headers?.ToDictionary(a => a.Key, a => a.Value).ToJson(), ContentType = requestMessage.Content?.Headers.ContentType?.MediaType ?? null, Url = requestMessage.RequestUri?.ToString() ?? null, - Method = requestMessage.Method.ToString() + Method = requestMessage.Method.ToString(), }; } @@ -209,43 +148,12 @@ private static async Task GetAdrRevokeResponseInfoAsy { var response = new ArrangeRevocationResponse { - Content = responseContent ?? await (httpResponse?.Content.ReadAsStringAsync() ?? Task.FromResult("")), - Headers = httpResponse == null ? null : JsonConvert.SerializeObject(httpResponse.Headers.ToDictionary(a => a.Key, a => a.Value)), - StatusCode = httpResponse?.StatusCode.ToJson() + Content = responseContent ?? await (httpResponse?.Content.ReadAsStringAsync() ?? Task.FromResult(string.Empty)), + Headers = httpResponse?.Headers.ToDictionary(a => a.Key, a => a.Value).ToJson(), + StatusCode = (int?)httpResponse?.StatusCode, }; return response; } - - private async Task<(HttpRequestMessage, FormUrlEncodedContent?)> PopulateRequestMessageForRevocationCall(Client client, string cdrArrangementId) - { - // Build the parameters for the call to the DR's arrangement revocation endpoint. - var configOptions = _config.GetConfigurationOptions(this.HttpContext); - var revocationUri = new Uri($"{client.RecipientBaseUri}/arrangements/revoke"); - var brandId = configOptions.BrandId; - var jwtPayload = new JwtPayload( - claims: new Claim[] - { - new(ClaimNames.Subject, brandId), - new(ClaimNames.JwtId, Guid.NewGuid().ToString()), - }, - issuer: brandId, - audience: revocationUri.ToString(), - notBefore: DateTime.UtcNow, - issuedAt: DateTime.UtcNow, - expires: DateTime.UtcNow.AddMinutes(5)); - var signedBearerTokenJwt = await GetSignedJwt(jwtPayload); - - _logger.LogInformation("Calling DR arrangement revocation endpoint ({RevocationUri})...", revocationUri); - - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", signedBearerTokenJwt); - var formValues = await GetFormValues(cdrArrangementId, brandId, revocationUri.ToString()); - var urlEncodedContent = new FormUrlEncodedContent(formValues); - - //Build the httprequestmessage with all the parameters. - HttpRequestMessage revokeRequest = new(HttpMethod.Post, revocationUri) { Content = urlEncodedContent }; - - return (revokeRequest, urlEncodedContent); - } } } diff --git a/Source/CdrAuthServer/Exceptions/ClientMetadataException.cs b/Source/CdrAuthServer/Exceptions/ClientMetadataException.cs index 0e19c90..c4cd880 100644 --- a/Source/CdrAuthServer/Exceptions/ClientMetadataException.cs +++ b/Source/CdrAuthServer/Exceptions/ClientMetadataException.cs @@ -4,11 +4,13 @@ namespace CdrAuthServer.Exceptions { public class ClientMetadataException : Exception { - public ClientMetadataException(string field) : base($"Client metadata error: {field}") + public ClientMetadataException(string field) + : base($"Client metadata error: {field}") { } - public ClientMetadataException(string field, string message) : base($"{field}: {message}") + public ClientMetadataException(string field, string message) + : base($"{field}: {message}") { } } diff --git a/Source/CdrAuthServer/Exceptions/JwksException.cs b/Source/CdrAuthServer/Exceptions/JwksException.cs index 09516c6..c97c581 100644 --- a/Source/CdrAuthServer/Exceptions/JwksException.cs +++ b/Source/CdrAuthServer/Exceptions/JwksException.cs @@ -2,11 +2,13 @@ { public class JwksException : Exception { - public JwksException() : base() + public JwksException() + : base() { } - public JwksException(string message) : base(message) + public JwksException(string message) + : base(message) { } } diff --git a/Source/CdrAuthServer/Extensions/AuthorizationRequestObjectExtensions.cs b/Source/CdrAuthServer/Extensions/AuthorizationRequestObjectExtensions.cs deleted file mode 100644 index 474af20..0000000 --- a/Source/CdrAuthServer/Extensions/AuthorizationRequestObjectExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using CdrAuthServer.Models; - -namespace CdrAuthServer.Extensions -{ - public static class AuthorizationRequestObjectExtensions - { - public static bool IsHybridFlow(this AuthorizationRequestObject authorizationRequestObject) - { - if (authorizationRequestObject == null || string.IsNullOrEmpty(authorizationRequestObject.ResponseType)) - { - return false; - } - - return authorizationRequestObject.ResponseType.IsHybridFlow(); - } - - public static bool IsHybridFlow(this string responseType) - { - var responseTypeValues = responseType.Split(' '); - return responseTypeValues.Contains("code") && responseTypeValues.Contains("id_token"); - } - } -} diff --git a/Source/CdrAuthServer/Extensions/ClaimsPrincipalExtensions.cs b/Source/CdrAuthServer/Extensions/ClaimsPrincipalExtensions.cs index 3ebeb4c..3995b6f 100644 --- a/Source/CdrAuthServer/Extensions/ClaimsPrincipalExtensions.cs +++ b/Source/CdrAuthServer/Extensions/ClaimsPrincipalExtensions.cs @@ -1,7 +1,6 @@ -using CdrAuthServer.IdPermanence; -using CdrAuthServer.Infrastructure; +using System.Security.Claims; +using CdrAuthServer.IdPermanence; using CdrAuthServer.Models; -using System.Security.Claims; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Extensions @@ -50,7 +49,7 @@ public static class ClaimsPrincipalExtensions var param = new SubPermanenceParameters() { SoftwareProductId = client.SoftwareId, - SectorIdentifierUri = client.SectorIdentifierUri ?? "" + SectorIdentifierUri = client.SectorIdentifierUri ?? string.Empty, }; return idPermanenceManager.DecryptSub(subject, param); diff --git a/Source/CdrAuthServer/Extensions/ConfigExtensions.cs b/Source/CdrAuthServer/Extensions/ConfigExtensions.cs index 0c7b566..2100e54 100644 --- a/Source/CdrAuthServer/Extensions/ConfigExtensions.cs +++ b/Source/CdrAuthServer/Extensions/ConfigExtensions.cs @@ -1,15 +1,15 @@ -using CdrAuthServer.Configuration; +using System.Security.Cryptography.X509Certificates; +using CdrAuthServer.Configuration; using CdrAuthServer.Infrastructure; using CdrAuthServer.Infrastructure.Certificates; using Microsoft.IdentityModel.Tokens; -using System.Security.Cryptography.X509Certificates; namespace CdrAuthServer.Extensions { public static class ConfigExtensions { private static IConfiguration? _config; - private static ConfigurationOptions _configurationOptions; + private static ConfigurationOptions _configurationOptions = null!; public static ConfigurationOptions GetConfigurationOptions(this IConfiguration config, HttpContext? context = null) { @@ -46,7 +46,7 @@ public static ConfigurationOptions GetConfigurationOptions(this IConfiguration c return null; } - private static void SetDefaults(HttpContext context) + private static void SetDefaults(HttpContext? context) { // Default endpoints. var basePath = (context != null && context.Request.PathBase.HasValue) ? context.Request.PathBase.Value : _configurationOptions.BasePath; @@ -91,30 +91,23 @@ private static void SetDefaults(HttpContext context) "client_credentials" ]); var scopesSupported = _configurationOptions.ScopesSupported; - if (_configurationOptions.ScopesProfile == ConfigurationOptions.scopesProfileAll || _configurationOptions.ScopesProfile == ConfigurationOptions.scopesProfileBanking) + if (_configurationOptions.ScopesProfile == ConfigurationOptions.ScopesProfileAll || _configurationOptions.ScopesProfile == ConfigurationOptions.ScopesProfileBanking) { scopesSupported = scopesSupported!.Union(_configurationOptions.BankingScopesSupported!).ToList(); } - if (_configurationOptions.ScopesProfile == ConfigurationOptions.scopesProfileAll || _configurationOptions.ScopesProfile == ConfigurationOptions.scopesProfileEnergy) + + if (_configurationOptions.ScopesProfile == ConfigurationOptions.ScopesProfileAll || _configurationOptions.ScopesProfile == ConfigurationOptions.ScopesProfileEnergy) { scopesSupported = scopesSupported!.Union(_configurationOptions.EnergyScopesSupported!).ToList(); } + _configurationOptions.ScopesSupported = SetDefault(scopesSupported, [ "openid", "profile", "cdr:registration" ]); _configurationOptions.ResponseModesSupported = SetDefault(_configurationOptions.ResponseModesSupported, [ - "fragment", - "form_post", "jwt", - "form_post.jwt", - "fragment.jwt", - "query.jwt", - ]); - _configurationOptions.ResponseTypesSupported = SetDefault(_configurationOptions.ResponseTypesSupported, [ - "code", - "code id_token" ]); _configurationOptions.SubjectTypesSupported = SetDefault(_configurationOptions.SubjectTypesSupported, [ "pairwise" @@ -134,21 +127,15 @@ private static void SetDefaults(HttpContext context) SecurityAlgorithms.RsaSsaPssSha256, SecurityAlgorithms.EcdsaSha256 ]); - _configurationOptions.IdTokenEncryptionAlgValuesSupported = SetDefault(_configurationOptions.IdTokenEncryptionAlgValuesSupported, [ - "RSA-OAEP", - "RSA-OAEP-256" - ]); - _configurationOptions.IdTokenEncryptionEncValuesSupported = SetDefault(_configurationOptions.IdTokenEncryptionEncValuesSupported, [ - "A128CBC-HS256", - "A256GCM" - ]); _configurationOptions.AuthorizationSigningAlgValuesSupported = SetDefault(_configurationOptions.AuthorizationSigningAlgValuesSupported, [ SecurityAlgorithms.RsaSsaPssSha256, SecurityAlgorithms.EcdsaSha256 ]); - _configurationOptions.AuthorizationEncryptionAlgValuesSupported = SetDefault(_configurationOptions.AuthorizationEncryptionAlgValuesSupported, + _configurationOptions.AuthorizationEncryptionAlgValuesSupported = SetDefault( + _configurationOptions.AuthorizationEncryptionAlgValuesSupported, "RSA-OAEP,RSA-OAEP-256"); - _configurationOptions.AuthorizationEncryptionEncValuesSupported = SetDefault(_configurationOptions.AuthorizationEncryptionEncValuesSupported, + _configurationOptions.AuthorizationEncryptionEncValuesSupported = SetDefault( + _configurationOptions.AuthorizationEncryptionEncValuesSupported, "A128CBC-HS256,A256GCM"); _configurationOptions.BrandId = SetDefault(_configurationOptions.BrandId, "00000000-0000-0000-0000-000000000000"); _configurationOptions.HeadlessMode = SetDefault(_configurationOptions.HeadlessMode, false); @@ -159,7 +146,6 @@ private static void SetDefaults(HttpContext context) _configurationOptions.RequestUriExpirySeconds = SetDefault(_configurationOptions.RequestUriExpirySeconds, 90); _configurationOptions.AccessTokenExpirySeconds = SetDefault(_configurationOptions.AccessTokenExpirySeconds, 300); _configurationOptions.IdTokenExpirySeconds = SetDefault(_configurationOptions.IdTokenExpirySeconds, 300); - _configurationOptions.AlwaysEncryptIdTokens = SetDefault(_configurationOptions.AlwaysEncryptIdTokens, false); _configurationOptions.UseMtlsEndpointAliases = SetDefault(_configurationOptions.UseMtlsEndpointAliases, false); _configurationOptions.ClockSkewSeconds = SetDefault(_configurationOptions.ClockSkewSeconds, 0); _configurationOptions.ClientCertificateThumbprintHttpHeaderName = SetDefault(_configurationOptions.ClientCertificateThumbprintHttpHeaderName, HttpHeaders.ClientCertificateThumbprint); @@ -167,8 +153,8 @@ private static void SetDefaults(HttpContext context) _configurationOptions.ClientCertificateHttpHeaderName = SetDefault(_configurationOptions.ClientCertificateHttpHeaderName, HttpHeaders.ClientCertificate); // If needing to turn off mtls checking at specific endpoints, such as for FAPI JARM testing with PAR endpoint. - var endpointList = _config.GetValue("CdrAuthServer:OverrideMtlsCheckEndpointList", ""); - _configurationOptions.OverrideMtlsChecks = SetDefault(_configurationOptions.OverrideMtlsChecks, endpointList.Split(',')); + var endpointList = _config?.GetValue("CdrAuthServer:OverrideMtlsCheckEndpointList", string.Empty); + _configurationOptions.OverrideMtlsChecks = SetDefault(_configurationOptions.OverrideMtlsChecks, endpointList?.Split(',') ?? []); } private static string SetDefault(string option, string defaultValue) diff --git a/Source/CdrAuthServer/Extensions/DateTimeExtensions.cs b/Source/CdrAuthServer/Extensions/DateTimeExtensions.cs index b9b167c..e1d3bce 100644 --- a/Source/CdrAuthServer/Extensions/DateTimeExtensions.cs +++ b/Source/CdrAuthServer/Extensions/DateTimeExtensions.cs @@ -2,14 +2,14 @@ { public static class DateTimeExtensions { - private static readonly DateTime _epochTime = new DateTime(1970, 1, 1); + private static readonly DateTime _epochTime = DateTime.UnixEpoch; public static bool HasExpired(this DateTime creationTime, int seconds, DateTime now) => creationTime.AddSeconds(seconds) < now; public static int ToEpoch(this DateTime time) => (int)(time - _epochTime).TotalSeconds; - public static DateTime? FromEpoch(this int? time) + public static DateTime? FromEpoch(this long? time) { if (time == null) { diff --git a/Source/CdrAuthServer/Extensions/EnumExtensions.cs b/Source/CdrAuthServer/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..0498738 --- /dev/null +++ b/Source/CdrAuthServer/Extensions/EnumExtensions.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace CdrAuthServer.Extensions +{ + public static class EnumExtensions + { + public static string? GetDisplayName(this Enum enumValue) + { + return enumValue.GetType() + .GetMember(enumValue.ToString())[0] + .GetCustomAttribute() + ?.GetName(); + } + } +} diff --git a/Source/CdrAuthServer/Extensions/HashExtensions.cs b/Source/CdrAuthServer/Extensions/HashExtensions.cs index 28b7b52..07be3a9 100644 --- a/Source/CdrAuthServer/Extensions/HashExtensions.cs +++ b/Source/CdrAuthServer/Extensions/HashExtensions.cs @@ -1,6 +1,6 @@ -using CdrAuthServer.Domain.Extensions; -using System.Security.Cryptography; +using System.Security.Cryptography; using System.Text; +using CdrAuthServer.Domain.Extensions; namespace CdrAuthServer.Extensions { @@ -10,10 +10,14 @@ public static class HashExtensions /// Creates a SHA256 hash of the specified input. /// /// The input. - /// A hash + /// A hash. public static string Sha256(this string input) { - if (input.IsNullOrEmpty()) return string.Empty; + if (input.IsNullOrEmpty()) + { + return string.Empty; + } + var bytes = Encoding.UTF8.GetBytes(input); var hash = SHA256.HashData(bytes); diff --git a/Source/CdrAuthServer/Extensions/JwtExtensions.cs b/Source/CdrAuthServer/Extensions/JwtExtensions.cs index 555f1d5..38291df 100644 --- a/Source/CdrAuthServer/Extensions/JwtExtensions.cs +++ b/Source/CdrAuthServer/Extensions/JwtExtensions.cs @@ -1,6 +1,4 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.IdentityModel.Tokens.Jwt; +using System.IdentityModel.Tokens.Jwt; namespace CdrAuthServer.Extensions { @@ -15,7 +13,7 @@ public static class JwtExtensions if (jwt.Payload.TryGetValue(claimType, out var claimValue)) { - return (claimValue == null ? null : claimValue.ToString()); + return claimValue == null ? null : claimValue.ToString(); } return null; diff --git a/Source/CdrAuthServer/Extensions/RequestHeadersExtensions.cs b/Source/CdrAuthServer/Extensions/RequestHeadersExtensions.cs index 097d6d8..3cca8bd 100644 --- a/Source/CdrAuthServer/Extensions/RequestHeadersExtensions.cs +++ b/Source/CdrAuthServer/Extensions/RequestHeadersExtensions.cs @@ -1,13 +1,11 @@ -using CdrAuthServer.Domain.Models; -using CdrAuthServer.Infrastructure.Models; -using CdrAuthServer.Models; -using System.Globalization; +using System.Globalization; +using CdrAuthServer.Domain.Models; namespace CdrAuthServer.Extensions { public static class RequestHeadersExtensions { - public static (bool isValid, ResponseErrorList? error, int? statusCode) ValidateAuthDate(this IHeaderDictionary headers) + public static (bool IsValid, ResponseErrorList? Error, int? StatusCode) ValidateAuthDate(this IHeaderDictionary headers) { // Get x-fapi-auth-date from request header var authDateValue = headers["x-fapi-auth-date"]; @@ -16,8 +14,7 @@ public static (bool isValid, ResponseErrorList? error, int? statusCode) Validate return MissingRequiredHeaderError("x-fapi-auth-date"); } - if (authDateValue.Count > 0 && - !DateTime.TryParseExact(authDateValue, CultureInfo.CurrentCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.CurrentCulture.DateTimeFormat, DateTimeStyles.None, out DateTime authDate)) + if (!DateTime.TryParseExact(authDateValue, CultureInfo.CurrentCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.CurrentCulture.DateTimeFormat, DateTimeStyles.None, out _)) { return InvalidHeaderError("x-fapi-auth-date"); } @@ -25,13 +22,13 @@ public static (bool isValid, ResponseErrorList? error, int? statusCode) Validate return (true, null, null); } - private static (bool isValid, ResponseErrorList? error, int? statusCode) MissingRequiredHeaderError(string headerName) + private static (bool IsValid, ResponseErrorList? Error, int? StatusCode) MissingRequiredHeaderError(string headerName) { var errors = new ResponseErrorList().AddMissingRequiredHeader(headerName); return (false, errors, 400); } - private static (bool isValid, ResponseErrorList? error, int? statusCode) InvalidHeaderError(string headerName) + private static (bool IsValid, ResponseErrorList? Error, int? StatusCode) InvalidHeaderError(string headerName) { var errors = new ResponseErrorList().AddInvalidHeader(headerName); return (false, errors, 400); diff --git a/Source/CdrAuthServer/Extensions/SwaggerExtensions.cs b/Source/CdrAuthServer/Extensions/SwaggerExtensions.cs index 384bfe4..fb66c19 100644 --- a/Source/CdrAuthServer/Extensions/SwaggerExtensions.cs +++ b/Source/CdrAuthServer/Extensions/SwaggerExtensions.cs @@ -20,7 +20,7 @@ public static IServiceCollection AddCdrSwaggerGen(this IServiceCollection servic { services.AddTransient, ConfigureSwaggerOptions>(); - //Required for our Swagger setup to work when endpoints have been versioned + // Required for our Swagger setup to work when endpoints have been versioned services.AddVersionedApiExplorer(opt => { opt.GroupNameFormat = options.VersionedApiGroupNameFormat; diff --git a/Source/CdrAuthServer/Formatters/JwtInputFormatter.cs b/Source/CdrAuthServer/Formatters/JwtInputFormatter.cs index 6e99100..a69e102 100644 --- a/Source/CdrAuthServer/Formatters/JwtInputFormatter.cs +++ b/Source/CdrAuthServer/Formatters/JwtInputFormatter.cs @@ -20,8 +20,9 @@ public JwtInputFormatter() public override bool CanRead(InputFormatterContext context) { - return context.HttpContext.Request.ContentType.Equals("application/jwt", StringComparison.OrdinalIgnoreCase) - || context.HttpContext.Request.ContentType.StartsWith("application/jwt;"); + return context.HttpContext.Request.ContentType != null && + (context.HttpContext.Request.ContentType.Equals("application/jwt", StringComparison.OrdinalIgnoreCase) + || context.HttpContext.Request.ContentType.StartsWith("application/jwt;")); } public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) @@ -47,7 +48,7 @@ public override async Task ReadRequestBodyAsync(InputForma try { - var softwareStatement = new SoftwareStatement(clientRegistrationRequest.SoftwareStatementJwt); + var softwareStatement = new SoftwareStatement(clientRegistrationRequest.SoftwareStatementJwt ?? string.Empty); _ = softwareStatement.ValidFrom; _ = softwareStatement.ValidTo; @@ -57,7 +58,7 @@ public override async Task ReadRequestBodyAsync(InputForma catch (Exception ex) { // Cant log actual Exception as Azure AppService Logging crashes in some instances - logger?.LogError("Error processing the SSA JWT {ExceptionMessage} {StackTrace}", ex.Message, ex.StackTrace); + logger?.LogError(ex, "Error processing the SSA JWT {ExceptionMessage} {StackTrace}", ex.Message, ex.StackTrace); context.ModelState.AddModelError(Constants.ClientMetadata.SoftwareStatement, "SSA is an invalid JWT"); return await InputFormatterResult.FailureAsync(); } @@ -65,7 +66,7 @@ public override async Task ReadRequestBodyAsync(InputForma catch (Exception ex) { // Cant log actual Exception as Azure AppService Logging crashes in some instances - logger?.LogError("Error processing the Request JWT {ExceptionMessage} {StackTrace}", ex.Message, ex.StackTrace); + logger?.LogError(ex, "Error processing the Request JWT {ExceptionMessage} {StackTrace}", ex.Message, ex.StackTrace); return await InputFormatterResult.FailureAsync(); } diff --git a/Source/CdrAuthServer/Helpers/HttpHelper.cs b/Source/CdrAuthServer/Helpers/HttpHelper.cs index e0ca796..bccf1fa 100644 --- a/Source/CdrAuthServer/Helpers/HttpHelper.cs +++ b/Source/CdrAuthServer/Helpers/HttpHelper.cs @@ -10,7 +10,7 @@ public static HttpClientHandler CreateHttpClientHandler(IConfiguration configura { return new HttpClientHandler { - ServerCertificateCustomValidationCallback = ServerCertificateCustomValidationCallback(configuration) + ServerCertificateCustomValidationCallback = ServerCertificateCustomValidationCallback(configuration), }; } @@ -18,7 +18,7 @@ public static HttpClientHandler CreateHttpClientHandler(bool isServerCertificate { return new HttpClientHandler { - ServerCertificateCustomValidationCallback = ServerCertificateCustomValidationCallback(isServerCertificateValidationEnabled) + ServerCertificateCustomValidationCallback = ServerCertificateCustomValidationCallback(isServerCertificateValidationEnabled), }; } diff --git a/Source/CdrAuthServer/HttpPipeline/HttpLoggingDelegatingHandler.cs b/Source/CdrAuthServer/HttpPipeline/HttpLoggingDelegatingHandler.cs new file mode 100644 index 0000000..c1b6274 --- /dev/null +++ b/Source/CdrAuthServer/HttpPipeline/HttpLoggingDelegatingHandler.cs @@ -0,0 +1,77 @@ +namespace CdrAuthServer.HttpPipeline +{ + /// + /// Logs request/responses to third parties when added to pipeline. + /// + /// + /// If the default logging level is Information or higher this will not log anything and will require the following environment variable to be set + /// Logging__LogLevel__CdrAuthServer__HttpPipeline=Debug. + /// + /// The logger. + public class HttpLoggingDelegatingHandler(ILogger logger) : DelegatingHandler + { + private const string RequestMessage = """ + Sending request: + Method: { Request.Method } + URI: { Request.Uri } + ContentType: { Request.MediaType } + Headers: { Request.Headers } + Body: + { Request.Body } + """; + + private const string ResponseMessage = """ + Received response: + StatusCode: { Response.StatusCode } + Headers: { Response.Headers } + Body: + { Response.Body } + """; + + /// + /// Send the request and log the request/response details. + /// + /// The request to send. + /// The cancellation token to be forwarded to downstream calls. + /// The response. + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpResponseMessage? response = null; + + using (logger.BeginScope("Calling {Method}{Uri}", request.Method, request.RequestUri)) + { + await Log(request, cancellationToken); + + response = await base.SendAsync(request, cancellationToken); + + await Log(response, cancellationToken); + } + + return response; + } + + /// + /// Log the response. + /// + /// The response. + /// The cancellation token to be forwarded to downstream calls. + private async Task Log(HttpResponseMessage response, CancellationToken cancellationToken) + { + var content = await (response.Content?.ReadAsStringAsync(cancellationToken) ?? Task.FromResult(string.Empty)); + + logger.LogInformation(ResponseMessage, response.StatusCode, response.Headers, content); + } + + /// + /// Log the request. + /// + /// The request. + /// The cancellation token to be forwarded to downstream calls. + private async Task Log(HttpRequestMessage request, CancellationToken cancellationToken) + { + var content = await (request.Content?.ReadAsStringAsync(cancellationToken) ?? Task.FromResult(string.Empty)); + + logger.LogInformation(RequestMessage, request.Method, request.RequestUri, request.Content?.Headers.ContentType?.MediaType, request.Headers, content); + } + } +} diff --git a/Source/CdrAuthServer/IdPermanence/AesEncryptor.cs b/Source/CdrAuthServer/IdPermanence/AesEncryptor.cs index 92a7ecf..f183c36 100644 --- a/Source/CdrAuthServer/IdPermanence/AesEncryptor.cs +++ b/Source/CdrAuthServer/IdPermanence/AesEncryptor.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Linq; -using System.Security.Cryptography; +using System.Security.Cryptography; using System.Text; namespace CdrAuthServer.IdPermanence @@ -45,7 +43,7 @@ public static string DecryptString(string key, byte[] cipherText) using (var encryptedStream = new MemoryStream(buffer)) { - //stream where decrypted contents will be stored + // stream where decrypted contents will be stored using (var decryptedStream = new MemoryStream()) { using (var aes = Aes.Create()) @@ -56,18 +54,20 @@ public static string DecryptString(string key, byte[] cipherText) using (var decryptor = aes.CreateDecryptor()) { - //decrypt stream and write it to parent stream + // decrypt stream and write it to parent stream using (var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read)) { int data; while ((data = cryptoStream.ReadByte()) != -1) + { decryptedStream.WriteByte((byte)data); + } } } } - //reset position in prep for reading + // reset position in prep for reading decryptedStream.Position = 0; var payloadBytes = decryptedStream.ToArray(); diff --git a/Source/CdrAuthServer/IdPermanence/CompressionExtensions.cs b/Source/CdrAuthServer/IdPermanence/CompressionExtensions.cs index 3a9d0c5..37808ac 100644 --- a/Source/CdrAuthServer/IdPermanence/CompressionExtensions.cs +++ b/Source/CdrAuthServer/IdPermanence/CompressionExtensions.cs @@ -8,7 +8,7 @@ public static class CompressionExtensions /// /// Compresses a byte array and returns a deflate compressed, byte array. /// - /// String to compress + /// String to compress. public static byte[] Compress(this byte[] uncompressedString) { byte[] compressedBytes; diff --git a/Source/CdrAuthServer/IdPermanence/IIdPermanenceManager.cs b/Source/CdrAuthServer/IdPermanence/IIdPermanenceManager.cs index a2d27b5..0d1e4e3 100644 --- a/Source/CdrAuthServer/IdPermanence/IIdPermanenceManager.cs +++ b/Source/CdrAuthServer/IdPermanence/IIdPermanenceManager.cs @@ -7,9 +7,13 @@ namespace CdrAuthServer.IdPermanence public interface IIdPermanenceManager { string EncryptId(string internalId, IdPermanenceParameters idParameters); - IEnumerable EncryptIds(IEnumerable list, IdPermanenceParameters idParameters, params Expression>[] idProperties); + + IEnumerable? EncryptIds(IEnumerable list, IdPermanenceParameters idParameters, params Expression>[] idProperties); + string DecryptId(string encryptedId, IdPermanenceParameters idParameters); + string EncryptSub(string customerId, SubPermanenceParameters subParameters); + string DecryptSub(string sub, SubPermanenceParameters subParameters); } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer/IdPermanence/IdPermanenceHelper.cs b/Source/CdrAuthServer/IdPermanence/IdPermanenceHelper.cs index b3fcca7..8ecfe62 100644 --- a/Source/CdrAuthServer/IdPermanence/IdPermanenceHelper.cs +++ b/Source/CdrAuthServer/IdPermanence/IdPermanenceHelper.cs @@ -7,34 +7,43 @@ namespace CdrAuthServer.IdPermanence { /// - /// Id Permanence Helper + /// Id Permanence Helper. /// public static class IdPermanenceHelper { - /// /// Encrypt an ID to meet ID Permanence rules. /// - /// Internal ID (i.e. accountId, transactionId) to encrypt - /// IdPermanenceParameters - /// Private Key - /// Encrypted ID + /// Internal ID (i.e. accountId, transactionId) to encrypt. + /// IdPermanenceParameters. + /// Private Key. + /// Encrypted ID. public static string EncryptId(string internalId, IdPermanenceParameters idParameters, string privateKey) { if (string.IsNullOrEmpty(internalId)) + { throw new ArgumentException("Value is null or empty", nameof(internalId)); + } if (string.IsNullOrEmpty(privateKey)) + { throw new ArgumentException("Value is null or empty", nameof(privateKey)); + } if (idParameters == null) + { throw new ArgumentNullException(nameof(idParameters)); + } if (string.IsNullOrEmpty(idParameters.SoftwareProductId)) + { throw new ArgumentException($"{nameof(idParameters)}.{nameof(idParameters.SoftwareProductId)} is not supplied."); + } if (string.IsNullOrEmpty(idParameters.CustomerId)) + { throw new ArgumentException($"{nameof(idParameters)}.{nameof(idParameters.CustomerId)} is not supplied."); + } var textToEncrypt = $"{idParameters.CustomerId}{internalId}"; var encryptionKey = $"{idParameters.SoftwareProductId}{privateKey}"; @@ -44,26 +53,36 @@ public static string EncryptId(string internalId, IdPermanenceParameters idParam /// /// Decrypt an encrypted ID back to the internal value. /// - /// Encrypted ID to decrypt back to internal value - /// IdPermanenceParameters - /// Private Key - /// Internal ID + /// Encrypted ID to decrypt back to internal value. + /// IdPermanenceParameters. + /// Private Key. + /// Internal ID. public static string DecryptId(string encryptedId, IdPermanenceParameters idParameters, string privateKey) { if (string.IsNullOrEmpty(encryptedId)) + { throw new ArgumentException("Value is null or empty", nameof(encryptedId)); + } if (string.IsNullOrEmpty(privateKey)) + { throw new ArgumentException("Value is null or empty", nameof(privateKey)); + } if (idParameters == null) + { throw new ArgumentNullException(nameof(idParameters)); + } if (string.IsNullOrEmpty(idParameters.SoftwareProductId)) + { throw new ArgumentException($"{nameof(idParameters)}.{nameof(idParameters.SoftwareProductId)} is not supplied."); + } if (string.IsNullOrEmpty(idParameters.CustomerId)) + { throw new ArgumentException($"{nameof(idParameters)}.{nameof(idParameters.CustomerId)} is not supplied."); + } var encryptionKey = $"{idParameters.SoftwareProductId}{privateKey}"; var decryptedStr = Decrypt(Decode(encryptedId), encryptionKey); @@ -75,23 +94,31 @@ public static string DecryptId(string encryptedId, IdPermanenceParameters idPara /// /// Encrypt the internal customer id for inclusion as the "sub" claim in id_token and access_token. /// - /// Internal Customer Id - /// SubPermanenceParameters - /// Private Key - /// Encrypted customer id to be included in sub claim + /// Internal Customer Id. + /// SubPermanenceParameters. + /// Private Key. + /// Encrypted customer id to be included in sub claim. public static string EncryptSub(string customerId, SubPermanenceParameters subParameters, string privateKey) { if (string.IsNullOrEmpty(customerId)) + { throw new ArgumentException("Value is null or empty", nameof(customerId)); + } if (string.IsNullOrEmpty(privateKey)) + { throw new ArgumentException("Value is null or empty", nameof(privateKey)); + } if (subParameters == null) + { throw new ArgumentNullException(nameof(subParameters)); + } if (string.IsNullOrEmpty(subParameters.SoftwareProductId)) + { throw new ArgumentException($"{nameof(subParameters)}.{nameof(subParameters.SoftwareProductId)} is not supplied."); + } var encryptionKey = $"{subParameters.SoftwareProductId}{privateKey}"; return Encrypt(customerId, encryptionKey); @@ -100,23 +127,31 @@ public static string EncryptSub(string customerId, SubPermanenceParameters subPa /// /// Decrypt the encrypted sub claim value from the access_token into the internal customer id. /// - /// Encrypted Customer Id found in sub claim of the access_token - /// SubPermanenceParameters - /// Private Key - /// Internal Customer Id + /// Encrypted Customer Id found in sub claim of the access_token. + /// SubPermanenceParameters. + /// Private Key. + /// Internal Customer Id. public static string DecryptSub(string sub, SubPermanenceParameters subParameters, string privateKey) { if (string.IsNullOrEmpty(sub)) + { throw new ArgumentException("Value is null or empty", nameof(sub)); + } if (string.IsNullOrEmpty(privateKey)) + { throw new ArgumentException("Value is null or empty", nameof(privateKey)); + } if (subParameters == null) + { throw new ArgumentNullException(nameof(subParameters)); + } if (string.IsNullOrEmpty(subParameters.SoftwareProductId)) + { throw new ArgumentException($"{nameof(subParameters)}.{nameof(subParameters.SoftwareProductId)} is not supplied."); + } var encryptionKey = $"{subParameters.SoftwareProductId}{privateKey}"; return Decrypt(sub, encryptionKey); @@ -158,7 +193,7 @@ private static string Decode(string value) public static string GetPrivateKey(IConfiguration config) { - string privateKey = config["CdrAuthServer:IdPermanence:PrivateKey"] ?? ""; + string privateKey = config["CdrAuthServer:IdPermanence:PrivateKey"] ?? string.Empty; // Private key was found, so return. if (!string.IsNullOrEmpty(privateKey)) diff --git a/Source/CdrAuthServer/IdPermanence/IdPermanenceManager.cs b/Source/CdrAuthServer/IdPermanence/IdPermanenceManager.cs index 8b2c765..db12666 100644 --- a/Source/CdrAuthServer/IdPermanence/IdPermanenceManager.cs +++ b/Source/CdrAuthServer/IdPermanence/IdPermanenceManager.cs @@ -1,21 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Linq; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Configuration; namespace CdrAuthServer.IdPermanence { /// - /// Id Permanence Manager + /// Id Permanence Manager. /// public class IdPermanenceManager : IIdPermanenceManager { - private string _privateKey; private readonly IConfiguration _config; + private string _privateKey = string.Empty; public IdPermanenceManager(IConfiguration config) { @@ -23,14 +17,14 @@ public IdPermanenceManager(IConfiguration config) } /// - /// Method to create permanence ids for specified properties in a list of objects + /// Method to create permanence ids for specified properties in a list of objects. /// - /// The type of list - /// The list - /// The permanence id parameters - /// The specified id properties to create permanence ids for - /// The list with permanence ids set - public IEnumerable EncryptIds(IEnumerable list, IdPermanenceParameters idParameters, params Expression>[] idProperties) + /// The type of list. + /// The list. + /// The permanence id parameters. + /// The specified id properties to create permanence ids for. + /// The list with permanence ids set. + public IEnumerable? EncryptIds(IEnumerable list, IdPermanenceParameters idParameters, params Expression>[] idProperties) { var privateKey = GetPrivateKey(); @@ -39,7 +33,8 @@ public IEnumerable EncryptIds(IEnumerable list, IdPermanenceParameters idProperties.ToList().ForEach(idProperty => { var id = idProperty.Compile()(item); - //Generate Permanence Id + + // Generate Permanence Id id = IdPermanenceManager.EncryptId(id, idParameters, privateKey); var memberSelectorExpression = idProperty.Body as MemberExpression; @@ -60,9 +55,9 @@ public IEnumerable EncryptIds(IEnumerable list, IdPermanenceParameters /// /// Encrypt an ID to meet ID Permanence rules. /// - /// Internal ID (i.e. accountId, transactionId) to encrypt - /// IdPermanenceParameters - /// Encrypted ID + /// Internal ID (i.e. accountId, transactionId) to encrypt. + /// IdPermanenceParameters. + /// Encrypted ID. public string EncryptId(string internalId, IdPermanenceParameters idParameters) { var privateKey = GetPrivateKey(); @@ -77,9 +72,9 @@ private static string EncryptId(string internalId, IdPermanenceParameters idPara /// /// Decrypt an encrypted ID back to the internal value. /// - /// Encrypted ID to decrypt back to internal value - /// IdPermanenceParameters - /// Internal ID + /// Encrypted ID to decrypt back to internal value. + /// IdPermanenceParameters. + /// Internal ID. public string DecryptId(string encryptedId, IdPermanenceParameters idParameters) { var privateKey = GetPrivateKey(); @@ -89,9 +84,9 @@ public string DecryptId(string encryptedId, IdPermanenceParameters idParameters) /// /// Encrypt the internal customer id for inclusion as the "sub" claim in id_token and access_token. /// - /// Internal Customer Id - /// SubPermanenceParameters - /// Encrypted customer id to be included in sub claim + /// Internal Customer Id. + /// SubPermanenceParameters. + /// Encrypted customer id to be included in sub claim. public string EncryptSub(string customerId, SubPermanenceParameters subParameters) { var privateKey = GetPrivateKey(); @@ -101,9 +96,9 @@ public string EncryptSub(string customerId, SubPermanenceParameters subParameter /// /// Decrypt the encrypted sub claim value from the access_token into the internal customer id. /// - /// Encrypted Customer Id found in sub claim of the access_token - /// SubPermanenceParameters - /// Internal Customer Id + /// Encrypted Customer Id found in sub claim of the access_token. + /// SubPermanenceParameters. + /// Internal Customer Id. public string DecryptSub(string sub, SubPermanenceParameters subParameters) { var privateKey = GetPrivateKey(); diff --git a/Source/CdrAuthServer/IdPermanence/IdPermanenceParameters.cs b/Source/CdrAuthServer/IdPermanence/IdPermanenceParameters.cs index dfff4c0..0ce647b 100644 --- a/Source/CdrAuthServer/IdPermanence/IdPermanenceParameters.cs +++ b/Source/CdrAuthServer/IdPermanence/IdPermanenceParameters.cs @@ -5,6 +5,7 @@ namespace CdrAuthServer.IdPermanence public class IdPermanenceParameters { public string SoftwareProductId { get; set; } = string.Empty; + public string CustomerId { get; set; } = string.Empty; } } diff --git a/Source/CdrAuthServer/IdPermanence/SubPermanenceParameters.cs b/Source/CdrAuthServer/IdPermanence/SubPermanenceParameters.cs index ee366df..54421b3 100644 --- a/Source/CdrAuthServer/IdPermanence/SubPermanenceParameters.cs +++ b/Source/CdrAuthServer/IdPermanence/SubPermanenceParameters.cs @@ -5,6 +5,7 @@ namespace CdrAuthServer.IdPermanence public class SubPermanenceParameters { public string SoftwareProductId { get; set; } = string.Empty; + public string SectorIdentifierUri { get; set; } = string.Empty; } } diff --git a/Source/CdrAuthServer/Models/Acr.cs b/Source/CdrAuthServer/Models/Acr.cs new file mode 100644 index 0000000..b31b133 --- /dev/null +++ b/Source/CdrAuthServer/Models/Acr.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace CdrAuthServer.Models +{ + public class Acr + { + [JsonProperty(PropertyName = "essential")] + public bool Essential { get; set; } + + [JsonProperty(PropertyName = "values")] + public string[] Values { get; set; } = []; + + [JsonProperty(PropertyName = "value")] + public string Value { get; set; } = string.Empty; + } +} diff --git a/Source/CdrAuthServer/Models/AdrArrangementRevocationResponse.cs b/Source/CdrAuthServer/Models/AdrArrangementRevocationResponse.cs new file mode 100644 index 0000000..f4a6db3 --- /dev/null +++ b/Source/CdrAuthServer/Models/AdrArrangementRevocationResponse.cs @@ -0,0 +1,9 @@ +namespace CdrAuthServer.Models +{ + public record AdrArrangementRevocationResponse + { + public ArrangeRevocationRequest? ArrangeRevocationRequest { get; set; } + + public ArrangeRevocationResponse? ArrangeRevocationResponse { get; set; } + } +} diff --git a/Source/CdrAuthServer/Models/ArrangeRevocationRequest.cs b/Source/CdrAuthServer/Models/ArrangeRevocationRequest.cs new file mode 100644 index 0000000..f18a050 --- /dev/null +++ b/Source/CdrAuthServer/Models/ArrangeRevocationRequest.cs @@ -0,0 +1,15 @@ +namespace CdrAuthServer.Models +{ + public record ArrangeRevocationRequest + { + public string? Body { get; set; } + + public string? Headers { get; set; } + + public string? Url { get; set; } + + public string? Method { get; set; } + + public string? ContentType { get; set; } + } +} diff --git a/Source/CdrAuthServer/Models/ArrangeRevocationResponse.cs b/Source/CdrAuthServer/Models/ArrangeRevocationResponse.cs new file mode 100644 index 0000000..989e5f0 --- /dev/null +++ b/Source/CdrAuthServer/Models/ArrangeRevocationResponse.cs @@ -0,0 +1,11 @@ +namespace CdrAuthServer.Models +{ + public record ArrangeRevocationResponse + { + public string? Content { get; set; } + + public string? Headers { get; set; } + + public int? StatusCode { get; set; } + } +} diff --git a/Source/CdrAuthServer/Models/ArrangementRevocationResponse.cs b/Source/CdrAuthServer/Models/ArrangementRevocationResponse.cs deleted file mode 100644 index 39431f8..0000000 --- a/Source/CdrAuthServer/Models/ArrangementRevocationResponse.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace CdrAuthServer.Models -{ - public record ArrangeRevocationRequest - { - public string? Body { get; set; } - public string? Headers { get; set; } - public string? Url { get; set; } - public string? Method { get; set; } - public string? ContentType { get; set; } - } - - public record ArrangeRevocationResponse - { - public string? Content { get; set; } - public string? Headers { get; set; } - public string? StatusCode { get; set; } - } - - public record AdrArrangementRevocationResponse - { - public ArrangeRevocationRequest? ArrangeRevocationRequest { get; set; } - public ArrangeRevocationResponse? ArrangeRevocationResponse { get; set; } - } - -} \ No newline at end of file diff --git a/Source/CdrAuthServer/Models/AuthorizationCodeGrant.cs b/Source/CdrAuthServer/Models/AuthorizationCodeGrant.cs new file mode 100644 index 0000000..4434c10 --- /dev/null +++ b/Source/CdrAuthServer/Models/AuthorizationCodeGrant.cs @@ -0,0 +1,77 @@ +using static CdrAuthServer.Domain.Constants; + +namespace CdrAuthServer.Models +{ + public class AuthorizationCodeGrant : Grant + { + private string? _request; + + public string? Request + { + get + { + if (!string.IsNullOrEmpty(_request)) + { + return _request; + } + + if (!this.Data.Any()) + { + return null; + } + + _request = GetDataItem(ClaimNames.Request) as string; + return _request; + } + + set + { + _request = value; + SetData(ClaimNames.Request, value); + } + } + + private string? _accountIdDelimitedList; + + public string? AccountIdDelimitedList + { + get + { + if (!string.IsNullOrEmpty(_accountIdDelimitedList)) + { + return _accountIdDelimitedList; + } + + if (!this.Data.Any()) + { + return null; + } + + _accountIdDelimitedList = GetDataItem(ClaimNames.AccountId) as string; + return _accountIdDelimitedList; + } + + set + { + _accountIdDelimitedList = value ?? string.Empty; + SetData(ClaimNames.AccountId, value); + } + } + + public List GetAccountIds() + { + if (!string.IsNullOrEmpty(_accountIdDelimitedList)) + { + return _accountIdDelimitedList.Split(',').ToList(); + } + + if (!this.Data.Any()) + { + return new List(); + } + + _accountIdDelimitedList = GetDataItem(ClaimNames.AccountId) as string; + return !string.IsNullOrEmpty(_accountIdDelimitedList) ? _accountIdDelimitedList.Split(',').ToList() : new List(); + } + } +} diff --git a/Source/CdrAuthServer/Models/AuthorizationRequestObject.cs b/Source/CdrAuthServer/Models/AuthorizationRequestObject.cs index 5f84a77..a1ce73b 100644 --- a/Source/CdrAuthServer/Models/AuthorizationRequestObject.cs +++ b/Source/CdrAuthServer/Models/AuthorizationRequestObject.cs @@ -40,8 +40,8 @@ public class AuthorizationRequestObject [JsonProperty("scopes")] public IEnumerable Scopes { - get - { + get + { if (string.IsNullOrEmpty(Scope)) { return Array.Empty(); @@ -51,34 +51,4 @@ public IEnumerable Scopes } } } - - public class AuthorizeClaims - { - [JsonProperty(PropertyName = "cdr_arrangement_id")] - public string CdrArrangementId { get; set; } = string.Empty; - - [JsonProperty(PropertyName = "sharing_duration")] - public int? SharingDuration { get; set; } - - [JsonProperty(PropertyName = "id_token", Required = Required.Always)] - public IdToken IdToken { get; set; } - } - - public class IdToken - { - [JsonProperty(PropertyName = "acr", Required = Required.Always)] - public Acr Acr { get; set; } - } - - public class Acr - { - [JsonProperty(PropertyName = "essential")] - public bool Essential { get; set; } - - [JsonProperty(PropertyName = "values")] - public string[] Values { get; set; } = []; - - [JsonProperty(PropertyName = "value")] - public string Value { get; set; } = string.Empty; - } } diff --git a/Source/CdrAuthServer/Models/AuthorizeCallbackRequest.cs b/Source/CdrAuthServer/Models/AuthorizeCallbackRequest.cs index c8cc872..a61ba53 100644 --- a/Source/CdrAuthServer/Models/AuthorizeCallbackRequest.cs +++ b/Source/CdrAuthServer/Models/AuthorizeCallbackRequest.cs @@ -3,14 +3,14 @@ namespace CdrAuthServer.Models { public class AuthorizeCallbackRequest : AuthorizeRequest - { - [JsonProperty(nameof(subject_id))] - public string subject_id { get; set; } = string.Empty; + { + [JsonProperty(nameof(Subject_id))] + public string Subject_id { get; set; } = string.Empty; - [JsonProperty(nameof(account_ids))] - public string account_ids { get; set; } = string.Empty; + [JsonProperty(nameof(Account_ids))] + public string Account_ids { get; set; } = string.Empty; - [JsonProperty(nameof(error_code))] - public string error_code { get; set; } = string.Empty; + [JsonProperty(nameof(Error_code))] + public string Error_code { get; set; } = string.Empty; } } diff --git a/Source/CdrAuthServer/Models/AuthorizeClaims.cs b/Source/CdrAuthServer/Models/AuthorizeClaims.cs new file mode 100644 index 0000000..aef03d9 --- /dev/null +++ b/Source/CdrAuthServer/Models/AuthorizeClaims.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace CdrAuthServer.Models +{ + public class AuthorizeClaims + { + [JsonProperty(PropertyName = "cdr_arrangement_id")] + public string CdrArrangementId { get; set; } = string.Empty; + + [JsonProperty(PropertyName = "sharing_duration")] + public int? SharingDuration { get; set; } + + [JsonProperty(PropertyName = "id_token", Required = Required.Always)] + public IdToken IdToken { get; set; } = null!; + } +} diff --git a/Source/CdrAuthServer/Models/AuthorizeRedirectRequest.cs b/Source/CdrAuthServer/Models/AuthorizeRedirectRequest.cs index cfddb97..fc33d50 100644 --- a/Source/CdrAuthServer/Models/AuthorizeRedirectRequest.cs +++ b/Source/CdrAuthServer/Models/AuthorizeRedirectRequest.cs @@ -1,7 +1,4 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Security.Claims; -using System.Text; namespace CdrAuthServer.Models { @@ -15,6 +12,7 @@ public class AuthorizeRedirectRequest [JsonProperty("dh_brand_name")] public string DhBrandName { get; set; } = string.Empty; + [JsonProperty("dh_brand_abn")] public string DhBrandAbn { get; set; } = string.Empty; diff --git a/Source/CdrAuthServer/Models/AuthorizeRequest.cs b/Source/CdrAuthServer/Models/AuthorizeRequest.cs index 5517329..c037551 100644 --- a/Source/CdrAuthServer/Models/AuthorizeRequest.cs +++ b/Source/CdrAuthServer/Models/AuthorizeRequest.cs @@ -5,25 +5,25 @@ namespace CdrAuthServer.Models { public class AuthorizeRequest { - [JsonProperty(nameof(request_uri))] - public string request_uri { get; set; } = string.Empty; + [JsonProperty(nameof(Request_uri))] + public string Request_uri { get; set; } = string.Empty; - [JsonProperty(nameof(response_type))] - public string response_type { get; set; } = string.Empty; + [JsonProperty(nameof(Response_type))] + public string Response_type { get; set; } = string.Empty; - [JsonProperty(nameof(response_mode))] - public string response_mode { get; set; } = string.Empty; + [JsonProperty(nameof(Response_mode))] + public string Response_mode { get; set; } = string.Empty; - [JsonProperty(nameof(client_id))] - public string client_id { get; set; } = string.Empty; + [JsonProperty(nameof(Client_id))] + public string Client_id { get; set; } = string.Empty; - [JsonProperty(nameof(redirect_uri))] - public string redirect_uri { get; set; } = string.Empty; + [JsonProperty(nameof(Redirect_uri))] + public string Redirect_uri { get; set; } = string.Empty; - [JsonProperty(nameof(scope))] - public string scope { get; set; } = string.Empty; + [JsonProperty(nameof(Scope))] + public string Scope { get; set; } = string.Empty; - [JsonProperty(nameof(nonce))] - public string nonce { get; set; } = string.Empty; + [JsonProperty(nameof(Nonce))] + public string Nonce { get; set; } = string.Empty; } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer/Models/AuthorizeResponse.cs b/Source/CdrAuthServer/Models/AuthorizeResponse.cs index 184cb22..75c7fc3 100644 --- a/Source/CdrAuthServer/Models/AuthorizeResponse.cs +++ b/Source/CdrAuthServer/Models/AuthorizeResponse.cs @@ -12,5 +12,4 @@ public class AuthorizeResponse [JsonProperty(ClaimNames.State)] public string State { get; set; } = string.Empty; } - } diff --git a/Source/CdrAuthServer/Models/CdrArrangementGrant.cs b/Source/CdrAuthServer/Models/CdrArrangementGrant.cs new file mode 100644 index 0000000..d447db5 --- /dev/null +++ b/Source/CdrAuthServer/Models/CdrArrangementGrant.cs @@ -0,0 +1,62 @@ +using CdrAuthServer.Extensions; +using static CdrAuthServer.Domain.Constants; + +namespace CdrAuthServer.Models +{ + public class CdrArrangementGrant : Grant + { + public List AccountIds + { + get + { + if (!this.Data.Any()) + { + return new List(); + } + + return (GetDataItem(ClaimNames.AccountId) as List) ?? new List(); + } + + set + { + SetData(ClaimNames.AccountId, value); + } + } + + public string? RefreshToken + { + get + { + if (!this.Data.Any()) + { + return null; + } + + return GetDataItem(ClaimNames.RefreshToken) as string; + } + + set + { + SetData(ClaimNames.RefreshToken, value); + } + } + + public int Version + { + get + { + if (!this.Data.Any()) + { + return 1; + } + + return Convert.ToInt32(GetDataItem(ClaimNames.CdrArrangementVersion)); + } + + set + { + SetData(ClaimNames.CdrArrangementVersion, value); + } + } + } +} diff --git a/Source/CdrAuthServer/Models/Client.cs b/Source/CdrAuthServer/Models/Client.cs index 09cff25..3bdb7b1 100644 --- a/Source/CdrAuthServer/Models/Client.cs +++ b/Source/CdrAuthServer/Models/Client.cs @@ -1,6 +1,4 @@ using Newtonsoft.Json; -using System.Collections.Generic; -using System.Text.Json.Serialization; namespace CdrAuthServer.Models { @@ -138,18 +136,6 @@ public class Client [JsonProperty("id_token_signed_response_alg")] public string IdTokenSignedResponseAlg { get; set; } = string.Empty; - /// - /// Gets a JWE `alg` algorithm with which an id_token is to be encrypted. - /// - [JsonProperty("id_token_encrypted_response_alg")] - public string IdTokenEncryptedResponseAlg { get; set; } = string.Empty; - - /// - /// Gets a JWE `enc` algorithm with which an id_token is to be encrypted. - /// - [JsonProperty("id_token_encrypted_response_enc")] - public string IdTokenEncryptedResponseEnc { get; set; } = string.Empty; - /// /// Gets an algorithm which the ADR expects to sign the request object if a request object will be part of the authorization request sent to the Data Holder. /// @@ -204,4 +190,4 @@ public class Client [JsonProperty("authorization_encrypted_response_enc")] public string? AuthorizationEncryptedResponseEnc { get; set; } = null; } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer/Models/ClientRegistrationRequest.cs b/Source/CdrAuthServer/Models/ClientRegistrationRequest.cs index 08bfc0f..8b9da47 100644 --- a/Source/CdrAuthServer/Models/ClientRegistrationRequest.cs +++ b/Source/CdrAuthServer/Models/ClientRegistrationRequest.cs @@ -10,7 +10,8 @@ public ClientRegistrationRequest() { } - public ClientRegistrationRequest(string jwt) : base(jwt) + public ClientRegistrationRequest(string jwt) + : base(jwt) { ClientRegistrationRequestJwt = jwt; } @@ -19,9 +20,9 @@ public ClientRegistrationRequest(string jwt) : base(jwt) /// Gets a Software Statement Assertion, as defined in [Dynamic Client Registration](https://cdr-register.github.io/register/#dynamic-client-registration). /// [Display(Name = "software_statement")] - public SoftwareStatement SoftwareStatement { get; set; } + public SoftwareStatement? SoftwareStatement { get; set; } - public string ClientRegistrationRequestJwt { get; } + public string ClientRegistrationRequestJwt { get; } = string.Empty; /// /// Gets the Key Identifier of this JWT. @@ -125,18 +126,6 @@ public string? TokenEndpointAuthMethod [Display(Name = "id_token_signed_response_alg")] public string? IdTokenSignedResponseAlg => Claims.FirstOrDefault(x => x.Type == ClientMetadata.IdTokenSignedResponseAlg)?.Value; - /// - /// Gets a JWE `alg` algorithm with which an id_token is to be encrypted. - /// - [Display(Name = "id_token_encrypted_response_alg")] - public string? IdTokenEncryptedResponseAlg => Claims.FirstOrDefault(x => x.Type == ClientMetadata.IdTokenEncryptedResponseAlg)?.Value; - - /// - /// Gets a JWE `enc` algorithm with which an id_token is to be encrypted. - /// - [Display(Name = "id_token_encrypted_response_enc")] - public string? IdTokenEncryptedResponseEnc => Claims.FirstOrDefault(x => x.Type == ClientMetadata.IdTokenEncryptedResponseEnc)?.Value; - /// /// Gets an algorithm which the ADR expects to sign the request object if a request object will be part of the authorization request sent to the Data Holder. /// @@ -144,7 +133,7 @@ public string? TokenEndpointAuthMethod public string? RequestObjectSigningAlg => Claims.FirstOrDefault(x => x.Type == ClientMetadata.RequestObjectSigningAlg)?.Value; /// - /// Gets the Software Statement Assertion + /// Gets the Software Statement Assertion. /// [Display(Name = "software_statement")] public string? SoftwareStatementJwt => Claims.FirstOrDefault(x => x.Type == ClientMetadata.SoftwareStatement)?.Value; @@ -166,6 +155,5 @@ public string? TokenEndpointAuthMethod /// [Display(Name = "authorization_encrypted_response_enc")] public string? AuthorizationEncryptedResponseEnc => Claims.FirstOrDefault(x => x.Type == ClientMetadata.AuthorizationEncryptedResponseEnc)?.Value; - } } diff --git a/Source/CdrAuthServer/Models/ClientRegistrationResponse.cs b/Source/CdrAuthServer/Models/ClientRegistrationResponse.cs index 21ffee0..26408e3 100644 --- a/Source/CdrAuthServer/Models/ClientRegistrationResponse.cs +++ b/Source/CdrAuthServer/Models/ClientRegistrationResponse.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace CdrAuthServer.Models +namespace CdrAuthServer.Models { public class ClientRegistrationResponse : Client { } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer/Models/Customer.cs b/Source/CdrAuthServer/Models/Customer.cs new file mode 100644 index 0000000..ecad8db --- /dev/null +++ b/Source/CdrAuthServer/Models/Customer.cs @@ -0,0 +1,9 @@ +namespace CdrAuthServer.Models +{ + public class Customer + { + public string LoginId { get; set; } = string.Empty; + + public Person? Person { get; set; } + } +} diff --git a/Source/CdrAuthServer/Models/Data.cs b/Source/CdrAuthServer/Models/Data.cs new file mode 100644 index 0000000..c71917b --- /dev/null +++ b/Source/CdrAuthServer/Models/Data.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace CdrAuthServer.Models +{ + public class Data + { + [JsonPropertyName("action")] + public string Action { get; set; } = string.Empty; + } +} diff --git a/Source/CdrAuthServer/Models/DataHolderCustomer.cs b/Source/CdrAuthServer/Models/DataHolderCustomer.cs index 18f1a1a..d19040a 100644 --- a/Source/CdrAuthServer/Models/DataHolderCustomer.cs +++ b/Source/CdrAuthServer/Models/DataHolderCustomer.cs @@ -2,19 +2,6 @@ { public class DataHolderCustomer { - public List Customers { get; set; } - } - - public class Customer - { - public string LoginId { get; set; } - public Person Person { get; set; } - } - - public class Person - { - - public string FirstName { get; set; } - public string LastName { get; set; } + public List Customers { get; set; } = []; } } diff --git a/Source/CdrAuthServer/Models/DataRecipientBrand.cs b/Source/CdrAuthServer/Models/DataRecipientBrand.cs index 7f8b5ee..d20a9ce 100644 --- a/Source/CdrAuthServer/Models/DataRecipientBrand.cs +++ b/Source/CdrAuthServer/Models/DataRecipientBrand.cs @@ -13,21 +13,6 @@ public class DataRecipientBrand [JsonPropertyName("softwareProducts")] public List SoftwareProducts { get; set; } = []; - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; - } - - public class LegalEntity - { - [JsonPropertyName("legalEntityId")] - public string LegalEntityId { get; set; } = string.Empty ; - - [JsonPropertyName("legalEntityName")] - public string LegalEntityName { get; set; } = string.Empty; - - [JsonPropertyName("dataRecipientBrands")] - public List DataRecipientBrands { get; set; } = []; - [JsonPropertyName("status")] public string Status { get; set; } = string.Empty; } diff --git a/Source/CdrAuthServer/Models/DataRecipientRequest.cs b/Source/CdrAuthServer/Models/DataRecipientRequest.cs index 8cdac0d..47040bb 100644 --- a/Source/CdrAuthServer/Models/DataRecipientRequest.cs +++ b/Source/CdrAuthServer/Models/DataRecipientRequest.cs @@ -2,12 +2,6 @@ namespace CdrAuthServer.Models { - public class Data - { - [JsonPropertyName("action")] - public string Action { get; set; } - } - public class DataRecipientRequest { [JsonPropertyName("data")] diff --git a/Source/CdrAuthServer/Models/Discovery.cs b/Source/CdrAuthServer/Models/Discovery.cs index 1fb1b64..51b859e 100644 --- a/Source/CdrAuthServer/Models/Discovery.cs +++ b/Source/CdrAuthServer/Models/Discovery.cs @@ -85,12 +85,6 @@ public class Discovery [JsonProperty("id_token_signing_alg_values_supported")] public IList? IdTokenSigningAlgValuesSupported { get; set; } - [JsonProperty("id_token_encryption_alg_values_supported")] - public IList? IdTokenEncryptionAlgValuesSupported { get; set; } - - [JsonProperty("id_token_encryption_enc_values_supported")] - public IList? IdTokenEncryptionEncValuesSupported { get; set; } - [JsonProperty("authorization_signing_alg_values_supported")] public IList? AuthorizationSigningAlgValuesSupported { get; set; } @@ -102,6 +96,5 @@ public class Discovery [JsonProperty("mtls_endpoint_aliases")] public IDictionary? MtlsEndpointAliases { get; set; } - } } diff --git a/Source/CdrAuthServer/Models/Error.cs b/Source/CdrAuthServer/Models/Error.cs index e6066f9..eacff55 100644 --- a/Source/CdrAuthServer/Models/Error.cs +++ b/Source/CdrAuthServer/Models/Error.cs @@ -4,7 +4,6 @@ namespace CdrAuthServer.Models { public class Error { - [JsonProperty("error")] public string Code { get; set; } diff --git a/Source/CdrAuthServer/Models/ErrorCatalogue.cs b/Source/CdrAuthServer/Models/ErrorCatalogue.cs index 4a5a358..075c480 100644 --- a/Source/CdrAuthServer/Models/ErrorCatalogue.cs +++ b/Source/CdrAuthServer/Models/ErrorCatalogue.cs @@ -8,9 +8,9 @@ namespace CdrAuthServer { public class ErrorCatalogue { - static ErrorCatalogue? _instance; - private static readonly object locker = new(); + private static readonly object Locker = new(); private static readonly Dictionary _errorCatalogue = []; + private static ErrorCatalogue? _instance; public static class Categories { @@ -43,7 +43,7 @@ public static class Categories public const string MTLS_CERT_OCSP_FAILED = "ERR-MTLS-003"; public const string MTLS_CERT_OCSP_ERROR = "ERR-MTLS-004"; - //Auth + // Auth public const string AUTHORIZATION_HOLDER_OF_KEY_CHECK_FAILED = "ERR-AUTH-001"; public const string AUTHORIZATION_ACCESS_TOKEN_REVOKED = "ERR-AUTH-002"; public const string AUTHORIZATION_INSUFFICIENT_SCOPE = "ERR-AUTH-003"; @@ -54,7 +54,7 @@ public static class Categories public const string REQUEST_URI_MISSING = "ERR-AUTH-008"; public const string ACCESS_DENIED = "ERR-AUTH-009"; - //Client Assertion + // Client Assertion public const string CLIENT_ASSERTION_TYPE_NOT_PROVIDED = "ERR-CLIENT_ASSERTION-002"; public const string INVALID_CLIENT_ASSERTION_TYPE = "ERR-CLIENT_ASSERTION-003"; public const string CLIENT_ASSERTION_CLIENT_ID_MISMATCH = "ERR-CLIENT_ASSERTION-004"; @@ -64,17 +64,16 @@ public static class Categories public const string CLIENT_ASSERTION_SUBJECT_ISS_NOT_SAME_VALUE = "ERR-CLIENT_ASSERTION-008"; public const string CLIENT_ASSERTION_MISSING_ISS_CLAIM = "ERR-CLIENT_ASSERTION-009"; - - //CDR Arrangement + // CDR Arrangement public const string INVALID_CONSENT_CDR_ARRANGEMENT = "ERR-ARR-001"; - //JWT + // JWT public const string JWT_INVALID_AUDIENCE = "ERR-JWT-001"; public const string JWT_EXPIRED = "ERR-JWT-002"; public const string JWKS_ERROR = "ERR-JWT-003"; public const string JWT_VALIDATION_ERROR = "ERR-JWT-004"; - //DCR + // DCR public const string DUPLICATE_REGISTRATION = "ERR-DCR-001"; public const string EMPTY_REGISTRATION_REQUEST = "ERR-DCR-002"; public const string REGISTRATION_REQUEST_INVALID_REDIRECT_URI = "ERR-DCR-003"; @@ -83,8 +82,7 @@ public static class Categories public const string SOFTWARE_STATEMENT_INVALID_OR_EMPTY = "ERR-DCR-006"; public const string INVALID_SECTOR_IDENTIFIER_URI = "ERR-DCR-007"; - - //Token + // Token public const string REFRESH_TOKEN_EXPIRED = "ERR-TKN-001"; public const string INVALID_REFRESH_TOKEN = "ERR-TKN-002"; public const string REFRESH_TOKEN_MISSING = "ERR-TKN-003"; @@ -93,8 +91,7 @@ public static class Categories public const string CODE_VERIFIER_IS_MISSING = "ERR-TKN-006"; public const string INVALID_AUTHORIZATION_CODE = "ERR-TKN-007"; - - //General + // General public const string SOFTWARE_PRODUCT_NOT_FOUND = "ERR-GEN-001"; public const string SOFTWARE_PRODUCT_STATUS_INACTIVE = "ERR-GEN-002"; public const string SOFTWARE_PRODUCT_REMOVED = "ERR-GEN-003"; @@ -157,7 +154,7 @@ protected ErrorCatalogue() // Authorization errors. AddToCatalogue(AUTHORIZATION_HOLDER_OF_KEY_CHECK_FAILED, Categories.Authorization, "invalid_token", "Holder of Key check failed", StatusCodes.Status401Unauthorized); AddToCatalogue(AUTHORIZATION_ACCESS_TOKEN_REVOKED, Categories.Authorization, "invalid_token", "Access Token check failed - it has been revoked", StatusCodes.Status401Unauthorized); - AddToCatalogue(AUTHORIZATION_INSUFFICIENT_SCOPE, Categories.Authorization, "insufficient_scope", "", StatusCodes.Status403Forbidden); + AddToCatalogue(AUTHORIZATION_INSUFFICIENT_SCOPE, Categories.Authorization, "insufficient_scope", string.Empty, StatusCodes.Status403Forbidden); AddToCatalogue(REQUEST_URI_ALREADY_USED, Categories.Authorization, ErrorCodes.Generic.InvalidRequestUri, "request_uri has already been used", StatusCodes.Status400BadRequest); AddToCatalogue(REQUEST_URI_CLIENT_ID_MISMATCH, Categories.Authorization, ErrorCodes.Generic.InvalidRequest, "client_id does not match request_uri client_id", StatusCodes.Status400BadRequest); AddToCatalogue(REQUEST_URI_EXPIRED, Categories.Authorization, ErrorCodes.Generic.InvalidRequestUri, "request_uri has expired", StatusCodes.Status400BadRequest); @@ -173,10 +170,9 @@ protected ErrorCatalogue() AddToCatalogue(SSA_VALIDATION_FAILED, Categories.DCR, ErrorCodes.Generic.InvalidSoftwareStatement, "SSA validation failed.", StatusCodes.Status401Unauthorized); AddToCatalogue(SOFTWARE_STATEMENT_INVALID_OR_EMPTY, Categories.DCR, ErrorCodes.Generic.InvalidSoftwareStatement, "The software_statement is empty or invalid", StatusCodes.Status401Unauthorized); - //Client Assertion errors + // Client Assertion errors - - //token errors + // token errors AddToCatalogue(CLIENT_NOT_FOUND, Categories.Token, ErrorCodes.Generic.InvalidClient, "Client not found", StatusCodes.Status400BadRequest); AddToCatalogue(REFRESH_TOKEN_EXPIRED, Categories.Token, ErrorCodes.Generic.InvalidGrant, "refresh_token has expired", StatusCodes.Status400BadRequest); AddToCatalogue(INVALID_REFRESH_TOKEN, Categories.Token, ErrorCodes.Generic.InvalidGrant, "refresh_token is invalid", StatusCodes.Status400BadRequest); @@ -186,16 +182,16 @@ protected ErrorCatalogue() AddToCatalogue(CODE_VERIFIER_IS_MISSING, Categories.Token, ErrorCodes.Generic.InvalidGrant, "code_verifier is missing", StatusCodes.Status400BadRequest); AddToCatalogue(INVALID_AUTHORIZATION_CODE, Categories.Token, ErrorCodes.Generic.InvalidGrant, "authorization code is invalid", StatusCodes.Status400BadRequest); - //arrangement errors + // arrangement errors AddToCatalogue(INVALID_CONSENT_CDR_ARRANGEMENT, Categories.Arrangement, ErrorCodes.Cds.InvalidConsentArrangement, "Invalid Consent Arrangement", StatusCodes.Status422UnprocessableEntity); - //jwt errors + // jwt errors AddToCatalogue(JWT_INVALID_AUDIENCE, Categories.JWT, ErrorCodes.Generic.InvalidClient, @"{0} - Invalid audience", StatusCodes.Status400BadRequest); AddToCatalogue(JWT_EXPIRED, Categories.JWT, ErrorCodes.Generic.InvalidClient, @"{0} has expired", StatusCodes.Status400BadRequest); AddToCatalogue(JWKS_ERROR, Categories.JWT, ErrorCodes.Generic.InvalidClient, @"{0} - jwks error", StatusCodes.Status400BadRequest); AddToCatalogue(JWT_VALIDATION_ERROR, Categories.JWT, ErrorCodes.Generic.InvalidClient, @"{0} - token validation error", StatusCodes.Status400BadRequest); - //general errors + // general errors AddToCatalogue(SOFTWARE_PRODUCT_NOT_FOUND, Categories.General, ErrorCodes.Generic.InvalidClient, "Software product not found", StatusCodes.Status403Forbidden, true); AddToCatalogue(SOFTWARE_PRODUCT_STATUS_INACTIVE, Categories.General, ErrorCodes.Cds.AdrStatusNotActive, "Software product status is {0}", StatusCodes.Status403Forbidden, true, "ADR Status Is Not Active"); AddToCatalogue(SOFTWARE_PRODUCT_REMOVED, Categories.General, ErrorCodes.Cds.AdrStatusNotActive, "Software product status is removed - consents cannot be revoked", StatusCodes.Status403Forbidden, true, "ADR Status Is Not Active"); @@ -260,15 +256,16 @@ public static ErrorCatalogue Catalogue() { if (_instance == null) { - lock (locker) + lock (Locker) { _instance ??= new ErrorCatalogue(); } } + return _instance; } - public (Error, int) GetError(string code) + public static (Error Error, int StatusCode) GetError(string code) { var errorDefinition = _errorCatalogue[code]; if (errorDefinition == null) @@ -279,7 +276,7 @@ public static ErrorCatalogue Catalogue() return (new Error(errorDefinition.Error, errorDefinition.ErrorDescription), errorDefinition.StatusCode); } - public (Error, int) GetError(string code, string? context) + public (Error Error, int StatusCode) GetError(string code, string? context) { var errorDefinition = GetErrorDefinition(code); var errorDescription = errorDefinition.ErrorDescription; @@ -310,7 +307,6 @@ public ErrorDefinition GetErrorDefinition(string code) return errorDefinition; } - public int GetStatusCode(string code) { var errorDefinition = _errorCatalogue[code]; @@ -333,7 +329,7 @@ public JsonResult GetErrorResponse(string code, string? context = null) if (errorDefinition.IsCdsError) { - var cdsError = new CdsError(errorDefinition.Error, errorDefinition.ErrorTitle ?? "", errorDescription); + var cdsError = new CdsError(errorDefinition.Error, errorDefinition.ErrorTitle ?? string.Empty, errorDescription); return new JsonResult(new ResponseErrorList(cdsError)) { StatusCode = errorDefinition.StatusCode }; } @@ -356,11 +352,17 @@ public ValidationResult GetValidationResult(string code, string context) public class ErrorDefinition { public string Category { get; private set; } + public string Code { get; private set; } + public string Error { get; private set; } + public string ErrorDescription { get; private set; } + public string? ErrorTitle { get; private set; } + public int StatusCode { get; private set; } + public bool IsCdsError { get; private set; } = false; public ErrorDefinition( @@ -381,6 +383,5 @@ public ErrorDefinition( this.ErrorTitle = errorTitle; } } - } } diff --git a/Source/CdrAuthServer/Models/Grant.cs b/Source/CdrAuthServer/Models/Grant.cs index d85b8a2..d91d97a 100644 --- a/Source/CdrAuthServer/Models/Grant.cs +++ b/Source/CdrAuthServer/Models/Grant.cs @@ -5,14 +5,27 @@ namespace CdrAuthServer.Models { public class Grant { - public string Key { get; set; } = String.Empty; - public string GrantType { get; set; } = String.Empty; - public string ClientId { get; set; } = String.Empty; - public string SubjectId { get; set; } = String.Empty; + public Grant() + { + this.Data = new Dictionary(); + } + + public string Key { get; set; } = string.Empty; + + public string GrantType { get; set; } = string.Empty; + + public string ClientId { get; set; } = string.Empty; + + public string SubjectId { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + public DateTime ExpiresAt { get; set; } + public DateTime? UsedAt { get; set; } - public string Scope { get; set; } = String.Empty; + + public string Scope { get; set; } = string.Empty; + public virtual IDictionary Data { get; set; } public bool IsExpired @@ -23,11 +36,6 @@ public bool IsExpired } } - public Grant() - { - this.Data = new Dictionary(); - } - public object? GetDataItem(string dataItemKey) { if (Data.TryGetValue(dataItemKey, out var value)) @@ -50,238 +58,4 @@ protected void SetData(string key, object? value) } } } - - public class RefreshTokenGrant : Grant - { - private string? _cdrArrangementId; - - public string? CdrArrangementId - { - get - { - if (!string.IsNullOrEmpty(_cdrArrangementId)) - { - return _cdrArrangementId; - } - - if (!base.Data.Any()) - { - return null; - } - - _cdrArrangementId = GetDataItem(ClaimNames.CdrArrangementId) as string; - return _cdrArrangementId; - } - set - { - _cdrArrangementId = value; - SetData(ClaimNames.CdrArrangementId, value); - } - } - - private string? _responseType; - - public string? ResponseType - { - get - { - if (!string.IsNullOrEmpty(_responseType)) - { - return _responseType; - } - - if (!base.Data.Any()) - { - return null; - } - - _responseType = GetDataItem(ClaimNames.ResponseType) as string; - return _responseType; - } - set - { - _responseType = value; - SetData(ClaimNames.ResponseType, value); - } - } - - private string? _authorizationCode; - - public string? AuthorizationCode - { - get - { - if (!string.IsNullOrEmpty(_authorizationCode)) - { - return _authorizationCode; - } - - if (!base.Data.Any()) - { - return null; - } - - _authorizationCode = GetDataItem(ClaimNames.AuthorizationCode) as string; - return _authorizationCode; - } - set - { - _authorizationCode = value; - SetData(ClaimNames.AuthorizationCode, value); - } - } - } - - public class AuthorizationCodeGrant : Grant - { - private string? _request; - public string? Request - { - get - { - if (!string.IsNullOrEmpty(_request)) - { - return _request; - } - - if (!base.Data.Any()) - { - return null; - } - - _request = GetDataItem(ClaimNames.Request) as string; - return _request; - } - set - { - _request = value; - SetData(ClaimNames.Request, value); - } - } - - private string? _accountIdDelimitedList; - public string? AccountIdDelimitedList - { - get - { - if (!string.IsNullOrEmpty(_accountIdDelimitedList)) - { - return _accountIdDelimitedList; - } - - if (!base.Data.Any()) - { - return null; - } - - _accountIdDelimitedList = GetDataItem(ClaimNames.AccountId) as string; - return _accountIdDelimitedList; - } - set - { - _accountIdDelimitedList = value ?? string.Empty; - SetData(ClaimNames.AccountId, value); - } - } - - public List GetAccountIds() - { - if (!string.IsNullOrEmpty(_accountIdDelimitedList)) - { - return _accountIdDelimitedList.Split(',').ToList(); - } - - if (!base.Data.Any()) - { - return new List(); - } - - _accountIdDelimitedList = GetDataItem(ClaimNames.AccountId) as string; - return !string.IsNullOrEmpty(_accountIdDelimitedList) ? _accountIdDelimitedList.Split(',').ToList() : new List(); - } - } - - public class RequestUriGrant : Grant - { - private string? _request; - - public string? Request - { - get - { - if (!string.IsNullOrEmpty(_request)) - { - return _request; - } - - if (!base.Data.Any()) - { - return null; - } - - _request = GetDataItem(ClaimNames.Request) as string; - return _request; - } - set - { - _request = value; - SetData(ClaimNames.Request, value); - } - } - } - - public class CdrArrangementGrant : Grant - { - public List AccountIds - { - get - { - if (!base.Data.Any()) - { - return new List(); - } - - return (GetDataItem(ClaimNames.AccountId) as List) ?? new List(); - } - set - { - SetData(ClaimNames.AccountId, value); - } - } - - public string? RefreshToken - { - get - { - if (!base.Data.Any()) - { - return null; - } - - return GetDataItem(ClaimNames.RefreshToken) as string; - } - set - { - SetData(ClaimNames.RefreshToken, value); - } - } - - public int Version - { - get - { - if (!base.Data.Any()) - { - return 1; - } - - return Convert.ToInt32(GetDataItem(ClaimNames.CdrArrangementVersion)); - } - set - { - SetData(ClaimNames.CdrArrangementVersion, value); - } - } - - } } diff --git a/Source/CdrAuthServer/Models/HeadlessModeUser.cs b/Source/CdrAuthServer/Models/HeadlessModeUser.cs index 86b8cca..acf989c 100644 --- a/Source/CdrAuthServer/Models/HeadlessModeUser.cs +++ b/Source/CdrAuthServer/Models/HeadlessModeUser.cs @@ -17,6 +17,6 @@ public class HeadlessModeUser public string Subject { get; } = "ksmith"; [JsonProperty("accounts")] - public string[] Accounts { get; } = { "123456", "987654" }; + public string[] Accounts { get; } = ["123456", "987654"]; } } diff --git a/Source/CdrAuthServer/Models/IdToken.cs b/Source/CdrAuthServer/Models/IdToken.cs new file mode 100644 index 0000000..7b7a7bf --- /dev/null +++ b/Source/CdrAuthServer/Models/IdToken.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace CdrAuthServer.Models +{ + public class IdToken + { + [JsonProperty(PropertyName = "acr", Required = Required.Always)] + public Acr? Acr { get; set; } + } +} diff --git a/Source/CdrAuthServer/Models/JsonWebKey.cs b/Source/CdrAuthServer/Models/JsonWebKey.cs new file mode 100644 index 0000000..b8a5767 --- /dev/null +++ b/Source/CdrAuthServer/Models/JsonWebKey.cs @@ -0,0 +1,27 @@ +namespace CdrAuthServer.Models +{ + public class JsonWebKey + { + public string kty { get; set; } = string.Empty; + + public string use { get; set; } = string.Empty; + + public string kid { get; set; } = string.Empty; + + public string x5t { get; set; } = string.Empty; + + public string? e { get; set; } + + public string? n { get; set; } + + public IList x5c { get; set; } = []; + + public string alg { get; set; } = string.Empty; + + public string? x { get; set; } + + public string? y { get; set; } + + public string? crv { get; set; } + } +} diff --git a/Source/CdrAuthServer/Models/JsonWebKeySet.cs b/Source/CdrAuthServer/Models/JsonWebKeySet.cs index e3bcc84..0bc27c1 100644 --- a/Source/CdrAuthServer/Models/JsonWebKeySet.cs +++ b/Source/CdrAuthServer/Models/JsonWebKeySet.cs @@ -2,22 +2,6 @@ { public class JsonWebKeySet { - public JsonWebKey[] keys { get; set; } + public JsonWebKey[] keys { get; set; } = []; } - - public class JsonWebKey - { - public string kty { get; set; } = string.Empty; - public string use { get; set; } = string.Empty; - public string kid { get; set; } = string.Empty; - public string x5t { get; set; } = string.Empty; - public string? e { get; set; } - public string? n { get; set; } - public IList x5c { get; set; } - public string alg { get; set; } = string.Empty; - public string? x { get; set; } - public string? y { get; set; } - public string? crv { get; set; } - } - } diff --git a/Source/CdrAuthServer/Models/LegalEntity.cs b/Source/CdrAuthServer/Models/LegalEntity.cs new file mode 100644 index 0000000..7575a1c --- /dev/null +++ b/Source/CdrAuthServer/Models/LegalEntity.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace CdrAuthServer.Models.Json +{ + public class LegalEntity + { + [JsonPropertyName("legalEntityId")] + public string LegalEntityId { get; set; } = string.Empty; + + [JsonPropertyName("legalEntityName")] + public string LegalEntityName { get; set; } = string.Empty; + + [JsonPropertyName("dataRecipientBrands")] + public List DataRecipientBrands { get; set; } = []; + + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + } +} diff --git a/Source/CdrAuthServer/Models/Person.cs b/Source/CdrAuthServer/Models/Person.cs new file mode 100644 index 0000000..628acb7 --- /dev/null +++ b/Source/CdrAuthServer/Models/Person.cs @@ -0,0 +1,9 @@ +namespace CdrAuthServer.Models +{ + public class Person + { + public string FirstName { get; set; } = string.Empty; + + public string LastName { get; set; } = string.Empty; + } +} diff --git a/Source/CdrAuthServer/Models/RefreshTokenGrant.cs b/Source/CdrAuthServer/Models/RefreshTokenGrant.cs new file mode 100644 index 0000000..539a2db --- /dev/null +++ b/Source/CdrAuthServer/Models/RefreshTokenGrant.cs @@ -0,0 +1,88 @@ +using static CdrAuthServer.Domain.Constants; + +namespace CdrAuthServer.Models +{ + public class RefreshTokenGrant : Grant + { + private string? _cdrArrangementId; + + public string? CdrArrangementId + { + get + { + if (!string.IsNullOrEmpty(_cdrArrangementId)) + { + return _cdrArrangementId; + } + + if (!this.Data.Any()) + { + return null; + } + + _cdrArrangementId = GetDataItem(ClaimNames.CdrArrangementId) as string; + return _cdrArrangementId; + } + + set + { + _cdrArrangementId = value; + SetData(ClaimNames.CdrArrangementId, value); + } + } + + private string? _responseType; + + public string? ResponseType + { + get + { + if (!string.IsNullOrEmpty(_responseType)) + { + return _responseType; + } + + if (!this.Data.Any()) + { + return null; + } + + _responseType = GetDataItem(ClaimNames.ResponseType) as string; + return _responseType; + } + + set + { + _responseType = value; + SetData(ClaimNames.ResponseType, value); + } + } + + private string? _authorizationCode; + + public string? AuthorizationCode + { + get + { + if (!string.IsNullOrEmpty(_authorizationCode)) + { + return _authorizationCode; + } + + if (!this.Data.Any()) + { + return null; + } + + _authorizationCode = GetDataItem(ClaimNames.AuthorizationCode) as string; + return _authorizationCode; + } + + set + { + _authorizationCode = value; + SetData(ClaimNames.AuthorizationCode, value); + } + } + } +} diff --git a/Source/CdrAuthServer/Models/Register/Links.cs b/Source/CdrAuthServer/Models/Register/Links.cs new file mode 100644 index 0000000..373ef57 --- /dev/null +++ b/Source/CdrAuthServer/Models/Register/Links.cs @@ -0,0 +1,14 @@ +namespace CdrAuthServer.Models.Register +{ + /// + /// Links related to the contextual API call. + /// + /// . + public class Links + { + /// + /// Fully qualified link to the contextual API call. + /// + public Uri? Self { get; set; } + } +} diff --git a/Source/CdrAuthServer/Models/Register/LinksPaginated.cs b/Source/CdrAuthServer/Models/Register/LinksPaginated.cs new file mode 100644 index 0000000..93ef6ae --- /dev/null +++ b/Source/CdrAuthServer/Models/Register/LinksPaginated.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; +using static System.Net.WebRequestMethods; + +namespace CdrAuthServer.Models.Register +{ + /// + /// Links related to the contextual API call and paging. + /// + /// . + public class LinksPaginated : Links + { + /// + /// URI to the first page of this set. Mandatory if this response is not the first page. + /// + public Uri? First { get; set; } + + /// + /// URI to the last page of this set. Mandatory if this response is not the last page. + /// + public Uri? Last { get; set; } + + /// + /// URI to the next page of this set. Mandatory if this response is not the last page. + /// + public Uri? Next { get; set; } + + /// + /// URI to the previous page of this set. Mandatory if this response is not the first page. + /// + [JsonPropertyName("prev")] + public Uri? Previous { get; set; } + } +} diff --git a/Source/CdrAuthServer/Models/Register/Meta.cs b/Source/CdrAuthServer/Models/Register/Meta.cs new file mode 100644 index 0000000..a505336 --- /dev/null +++ b/Source/CdrAuthServer/Models/Register/Meta.cs @@ -0,0 +1,15 @@ +using static System.Net.WebRequestMethods; + +namespace CdrAuthServer.Models.Register +{ + /// + /// Metadata. + /// + /// . +#pragma warning disable S2094 // Classes should not be empty + public class Meta +#pragma warning restore S2094 // Classes should not be empty + { + // Intentionally empty, reflects the standards + } +} diff --git a/Source/CdrAuthServer/Models/Register/MetaPaginated.cs b/Source/CdrAuthServer/Models/Register/MetaPaginated.cs new file mode 100644 index 0000000..e12fdad --- /dev/null +++ b/Source/CdrAuthServer/Models/Register/MetaPaginated.cs @@ -0,0 +1,19 @@ +namespace CdrAuthServer.Models.Register +{ + /// + /// Paging metadata. + /// + /// . + public class MetaPaginated : Meta + { + /// + /// The total number of pages in the full set. + /// + public int TotalPages { get; set; } + + /// + /// The total number of records in the full set. + /// + public int TotalRecords { get; set; } + } +} diff --git a/Source/CdrAuthServer/Models/Register/RegisterResponse.cs b/Source/CdrAuthServer/Models/Register/RegisterResponse.cs new file mode 100644 index 0000000..7c778b8 --- /dev/null +++ b/Source/CdrAuthServer/Models/Register/RegisterResponse.cs @@ -0,0 +1,25 @@ +namespace CdrAuthServer.Models.Register +{ + /// + /// Represents a response from the CDR Register (without pagination). + /// + /// The data type to include in the response. + public class RegisterResponse + where TData : class, new() + { + /// + /// The response data for the query. + /// + public IEnumerable Data { get; set; } = []; + + /// + /// Links. + /// + public Links Links { get; set; } = new(); + + /// + /// Metadata. + /// + public Meta Meta { get; set; } = new(); + } +} diff --git a/Source/CdrAuthServer/Models/Register/RegisterResponsePaginated.cs b/Source/CdrAuthServer/Models/Register/RegisterResponsePaginated.cs new file mode 100644 index 0000000..d2bbfa8 --- /dev/null +++ b/Source/CdrAuthServer/Models/Register/RegisterResponsePaginated.cs @@ -0,0 +1,25 @@ +namespace CdrAuthServer.Models.Register +{ + /// + /// Represents a response from the CDR register (with pagination). + /// + /// The data type to include in the response. + public class RegisterResponsePaginated + where TData : class, new() + { + /// + /// The response data for the query. + /// + public IEnumerable Data { get; set; } = []; + + /// + /// Paging Links. + /// + public LinksPaginated Links { get; set; } = new(); + + /// + /// Paging Metadata. + /// + public MetaPaginated Meta { get; set; } = new(); + } +} diff --git a/Source/CdrAuthServer/Models/RequestUriGrant.cs b/Source/CdrAuthServer/Models/RequestUriGrant.cs new file mode 100644 index 0000000..8cf4120 --- /dev/null +++ b/Source/CdrAuthServer/Models/RequestUriGrant.cs @@ -0,0 +1,34 @@ +using static CdrAuthServer.Domain.Constants; + +namespace CdrAuthServer.Models +{ + public class RequestUriGrant : Grant + { + private string? _request; + + public string? Request + { + get + { + if (!string.IsNullOrEmpty(_request)) + { + return _request; + } + + if (!this.Data.Any()) + { + return null; + } + + _request = GetDataItem(ClaimNames.Request) as string; + return _request; + } + + set + { + _request = value; + SetData(ClaimNames.Request, value); + } + } + } +} diff --git a/Source/CdrAuthServer/Models/SoftwareProduct.cs b/Source/CdrAuthServer/Models/SoftwareProduct.cs index 29c57e5..d1543eb 100644 --- a/Source/CdrAuthServer/Models/SoftwareProduct.cs +++ b/Source/CdrAuthServer/Models/SoftwareProduct.cs @@ -3,15 +3,25 @@ public class SoftwareProduct { public string LegalEntityId { get; set; } = string.Empty; + public string LegalEntityName { get; set; } = string.Empty; + public string LegalEntityStatus { get; set; } = string.Empty; + public string BrandId { get; set; } = string.Empty; + public string BrandName { get; set; } = string.Empty; + public string BrandStatus { get; set; } = string.Empty; + public string SoftwareProductId { get; set; } = string.Empty; + public string SoftwareProductName { get; set; } = string.Empty; + public string SoftwareProductDescription { get; set; } = string.Empty; + public string LogoUri { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; } } diff --git a/Source/CdrAuthServer/Models/SoftwareStatement.cs b/Source/CdrAuthServer/Models/SoftwareStatement.cs index f204bd2..b9e34c1 100644 --- a/Source/CdrAuthServer/Models/SoftwareStatement.cs +++ b/Source/CdrAuthServer/Models/SoftwareStatement.cs @@ -55,7 +55,7 @@ public SoftwareStatement(string jwtEncodedString) public string? ClientUri => Claims.FirstOrDefault(x => x.Type == "client_uri")?.Value; /// - /// Gets a URL string that references a logo for the client. If present, the server SHOULD display this image to the end-user during approval + /// Gets a URL string that references a logo for the client. If present, the server SHOULD display this image to the end-user during approval. /// [JsonPropertyName("logo_uri")] public string? LogoUri => Claims.FirstOrDefault(x => x.Type == "logo_uri")?.Value; @@ -63,7 +63,7 @@ public SoftwareStatement(string jwtEncodedString) /// /// URL string that points to a human-readable terms of service document for the Software Product. /// - /// URL string that points to a human-readable terms of service document for the Software Product + /// URL string that points to a human-readable terms of service document. [JsonPropertyName("tos_uri")] public string? TosUri => Claims.FirstOrDefault(x => x.Type == "tos_uri")?.Value; @@ -92,13 +92,13 @@ public SoftwareStatement(string jwtEncodedString) public string? RecipientBaseUri => Claims.FirstOrDefault(x => x.Type == "recipient_base_uri")?.Value; /// - /// Gets a String representing a unique identifier assigned by the ACCC Register and used by registration endpoints to identify the software product to be dynamically registered. </br></br>The \"software_id\" will remain the same for the lifetime of the product, across multiple updates and versions + /// Gets a String representing a unique identifier assigned by the ACCC Register and used by registration endpoints to identify the software product to be dynamically registered. </br></br>The \"software_id\" will remain the same for the lifetime of the product, across multiple updates and versions. /// [JsonPropertyName("software_id")] public string? SoftwareId => Claims.FirstOrDefault(x => x.Type == "software_id")?.Value; /// - /// Gets a String containing a the software role, e.g. data-recipient-software-product + /// Gets a String containing a the software role, e.g. data-recipient-software-product. /// [JsonPropertyName("software_roles")] public string? SoftwareRoles => Claims.FirstOrDefault(x => x.Type == "software_roles")?.Value; @@ -116,7 +116,7 @@ public SoftwareStatement(string jwtEncodedString) public IEnumerable RedirectUris => Claims.Where(x => x.Type == "redirect_uris").Select(x => x.Value); /// - ///Sector Identifier Uri used in PPID calculations. + /// Sector Identifier Uri used in PPID calculations. /// [JsonPropertyName("sector_identifier_uri")] public string? SectorIdentifierUri => Claims.FirstOrDefault(x => x.Type == "sector_identifier_uri")?.Value; diff --git a/Source/CdrAuthServer/Models/TokenRequest.cs b/Source/CdrAuthServer/Models/TokenRequest.cs index 6ec8c7c..dbbeabd 100644 --- a/Source/CdrAuthServer/Models/TokenRequest.cs +++ b/Source/CdrAuthServer/Models/TokenRequest.cs @@ -6,24 +6,24 @@ namespace CdrAuthServer.Models public class TokenRequest { [JsonProperty(ClaimNames.GrantType)] - public string grant_type { get; set; } = string.Empty; + public string Grant_type { get; set; } = string.Empty; [JsonProperty(ClaimNames.Code)] - public string code { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; [JsonProperty(ClaimNames.CodeVerifier)] - public string code_verifier { get; set; } = string.Empty; + public string Code_verifier { get; set; } = string.Empty; [JsonProperty(ClaimNames.RedirectUri)] - public string redirect_uri { get; set; } = string.Empty; + public string Redirect_uri { get; set; } = string.Empty; [JsonProperty(ClaimNames.ClientId)] - public string client_id { get; set; } = string.Empty; + public string Client_id { get; set; } = string.Empty; [JsonProperty(ClaimNames.Scope)] - public string scope { get; set; } = string.Empty; + public string Scope { get; set; } = string.Empty; [JsonProperty(ClaimNames.RefreshToken)] - public string refresh_token { get; set; } = string.Empty; + public string Refresh_token { get; set; } = string.Empty; } } diff --git a/Source/CdrAuthServer/Models/TokenResponse.cs b/Source/CdrAuthServer/Models/TokenResponse.cs index e8c2f48..0ec44e2 100644 --- a/Source/CdrAuthServer/Models/TokenResponse.cs +++ b/Source/CdrAuthServer/Models/TokenResponse.cs @@ -28,6 +28,5 @@ public class TokenResponse [JsonIgnore] public Error? Error { get; set; } - } } diff --git a/Source/CdrAuthServer/Program.cs b/Source/CdrAuthServer/Program.cs index ef75728..e5caa60 100644 --- a/Source/CdrAuthServer/Program.cs +++ b/Source/CdrAuthServer/Program.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using CdrAuthServer; using CdrAuthServer.API.Logger; using CdrAuthServer.Authorisation; @@ -6,6 +7,7 @@ using CdrAuthServer.Domain.Repositories; using CdrAuthServer.Extensions; using CdrAuthServer.Helpers; +using CdrAuthServer.HttpPipeline; using CdrAuthServer.Infrastructure.Authorisation; using CdrAuthServer.Infrastructure.Certificates; using CdrAuthServer.Infrastructure.Extensions; @@ -25,25 +27,23 @@ using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using Serilog; -using System.Text.RegularExpressions; +using Serilog.Settings.Configuration; using static CdrAuthServer.Infrastructure.Constants; var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddEnvironmentVariables(); -builder.Services.AddScoped(); -builder.Services.ConfigureWebServer( +builder.Services.AddSingleton(); + +await builder.Services.ConfigureWebServer( builder.Configuration, "Certificates:TlsInternalCertificate", httpPort: builder.Configuration.GetValue("CdrAuthServer:HttpPort", 8080), httpsPort: builder.Configuration.GetValue("CdrAuthServer:HttpsPort", 8001)); // Add logging provider. -var logger = new LoggerConfiguration() - .ReadFrom.Configuration(builder.Configuration) - .Enrich.FromLogContext() - .CreateLogger(); +ConfigureSerilog(builder.Configuration); builder.Logging.ClearProviders(); -builder.Logging.AddSerilog(logger); +builder.Logging.AddSerilog(); IdentityModelEventSource.ShowPII = true; // Turn off default model validation so that it can be handled according to standards. @@ -52,8 +52,32 @@ options.SuppressModelStateInvalidFilter = true; }); +builder.Services + .AddOptions() + .ValidateDataAnnotations() + .Configure((options, config) => config.GetSection(ConfigurationOptions.ConfigurationSectionName).Bind(options)); + +builder.Services + .AddOptions() + .ValidateDataAnnotations() + .Configure((options, config) => config + .GetSection(ConfigurationOptions.ConfigurationSectionName) + .GetSection(nameof(ConfigurationOptions.CdrRegister)) + .Bind(options)); + +builder.Services.AddTransient(); + builder.Services.AddHttpClient() - .ConfigurePrimaryHttpMessageHandler(s => HttpHelper.CreateHttpClientHandler(builder.Configuration)); + .ConfigurePrimaryHttpMessageHandler(s => HttpHelper.CreateHttpClientHandler(builder.Configuration)) + .AddHttpMessageHandler(); + +builder.Services.AddHttpClient() + .ConfigurePrimaryHttpMessageHandler(s => HttpHelper.CreateHttpClientHandler(builder.Configuration)) + .AddHttpMessageHandler(); + +builder.Services.AddHttpClient() + .ConfigurePrimaryHttpMessageHandler(s => HttpHelper.CreateHttpClientHandler(builder.Configuration)) + .AddHttpMessageHandler(); builder.Services.AddMemoryCache(); @@ -93,13 +117,13 @@ options.Configuration = new OpenIdConnectConfiguration() { JwksUri = $"{metadataAddress}/jwks", - JsonWebKeySet = await LoadJwks($"{metadataAddress}/jwks", HttpHelper.CreateHttpClientHandler(builder.Configuration)) + JsonWebKeySet = await LoadJwks($"{metadataAddress}/jwks", HttpHelper.CreateHttpClientHandler(builder.Configuration)), }; options.TokenValidationParameters = BuildTokenValidationParameters(options, validIssuers, validAudiences, clockSkew); // Ignore server certificate issues when retrieving OIDC configuration and JWKS. - options.BackchannelHttpHandler = HttpHelper.CreateHttpClientHandler(builder.Configuration); + options.BackchannelHttpHandler = HttpHelper.CreateHttpClientHandler(builder.Configuration); }); builder.Services.AddAuthorization(); @@ -109,7 +133,7 @@ { var allAuthPolicies = AuthorisationPolicies.GetAllPolicies(); - //Apply all listed policities from a single source of truth that is also used for self-documentation + // Apply all listed policities from a single source of truth that is also used for self-documentation foreach (var pol in allAuthPolicies) { options.AddPolicy(pol.Name, policy => @@ -118,14 +142,17 @@ { policy.Requirements.Add(new ScopeRequirement(pol.ScopeRequirement)); } + if (pol.HasMtlsRequirement) { - //policy.Requirements.Add(new MtlsRequirement()); //Currently not in CdrAuthServer but kept to later align with Mock Register + // policy.Requirements.Add(new MtlsRequirement()); //Currently not in CdrAuthServer but kept to later align with Mock Register } + if (pol.HasHolderOfKeyRequirement) { policy.Requirements.Add(new HolderOfKeyRequirement()); } + if (pol.HasAccessTokenRequirement) { policy.Requirements.Add(new AccessTokenRequirement()); @@ -137,7 +164,8 @@ // Add CORS Policy builder.Services.AddCors(options => { - options.AddPolicy("AllOrigins", + options.AddPolicy( + "AllOrigins", policy => { policy.WithOrigins("*") @@ -176,21 +204,20 @@ var enableSwagger = builder.Configuration.GetValue(ConfigurationKeys.EnableSwagger); if (enableSwagger) { - builder.Services.AddCdrSwaggerGen(opt => - { - opt.SwaggerTitle = "Consumer Data Right (CDR) Participant Tooling - Mock Auth Server API"; - opt.IncludeAuthentication = true; - }, false); + builder.Services.AddCdrSwaggerGen( + opt => + { + opt.SwaggerTitle = "Consumer Data Right (CDR) Participant Tooling - Mock Auth Server API"; + opt.IncludeAuthentication = true; + }, + false); } var connectionString = builder.Configuration.GetConnectionString(DbConstants.ConnectionStrings.Default); builder.Services.AddDbContext(options => options.UseSqlServer(connectionString)); -if (builder.Configuration.GetSection("SerilogRequestResponseLogger") != null) -{ - Log.Logger.Information("Adding request response logging middleware"); - builder.Services.AddRequestResponseLogging(); -} +Log.Logger.Information("Adding request response logging middleware"); +builder.Services.AddRequestResponseLogging(); // Add services to the container. builder.Services @@ -202,7 +229,7 @@ }) .AddNewtonsoftJson(options => { - options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); bool healthCheckMigration = false; @@ -212,8 +239,8 @@ builder.Services .AddHealthChecks() - .AddCheck("migration", () => healthCheckMigration ? HealthCheckResult.Healthy(healthCheckMigrationMessage) : HealthCheckResult.Unhealthy(healthCheckMigrationMessage)) - .AddCheck("seed-data", () => healthCheckSeedData ? HealthCheckResult.Healthy(healthCheckSeedDataMessage) : HealthCheckResult.Unhealthy(healthCheckSeedDataMessage)); + .AddCheck("migration", () => healthCheckMigration ? HealthCheckResult.Healthy(healthCheckMigrationMessage) : HealthCheckResult.Unhealthy(healthCheckMigrationMessage)) + .AddCheck("seed-data", () => healthCheckSeedData ? HealthCheckResult.Healthy(healthCheckSeedDataMessage) : HealthCheckResult.Unhealthy(healthCheckSeedDataMessage)); var app = builder.Build(); app.UseStaticFiles(); @@ -236,7 +263,7 @@ var path = matches[0].Groups[0].Value; var remainder = matches[0].Groups[1].Value; context.Request.Path = $"/{remainder}"; - context.Request.PathBase = path.Replace(remainder, "").TrimEnd('/'); + context.Request.PathBase = path.Replace(remainder, string.Empty).TrimEnd('/'); } return next(context); @@ -266,7 +293,7 @@ // Try and retrieve the error from the ExceptionHandler middleware var exceptionDetails = context.Features.Get(); var ex = exceptionDetails?.Error; - var error = new ResponseErrorList(CdrAuthServer.Domain.Constants.ErrorCodes.Cds.UnexpectedError, CdrAuthServer.Domain.Constants.ErrorTitles.UnexpectedError, ex?.Message); + var error = new ResponseErrorList(CdrAuthServer.Domain.Constants.ErrorCodes.Cds.UnexpectedError, CdrAuthServer.Domain.Constants.ErrorTitles.UnexpectedError, ex?.Message ?? string.Empty); context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject(error)); @@ -280,17 +307,33 @@ healthCheckMigration = true; healthCheckMigrationMessage = "Migration completed"; +// Reconfigure Serilog with DB +ConfigureSerilog(builder.Configuration, true); + healthCheckSeedData = true; healthCheckSeedDataMessage = "Seeding of data completed"; app.UseHealthChecks("/health", new HealthCheckOptions() { - ResponseWriter = CustomResponseWriter + ResponseWriter = CustomResponseWriter, }); -app.Run(); +await app.RunAsync(); +static void ConfigureSerilog(IConfiguration configuration, bool isDatabaseReady = false) +{ + var loggerConfiguration = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .Enrich.FromLogContext(); + // If the database is ready, configure the SQL Server sink + if (isDatabaseReady) + { + loggerConfiguration.ReadFrom.Configuration(configuration, new ConfigurationReaderOptions() { SectionName = "SerilogMSSqlServerWriteTo" }); + } + + Log.Logger = loggerConfiguration.CreateLogger(); +} static Task CustomResponseWriter(HttpContext context, HealthReport healthReport) { @@ -301,26 +344,27 @@ static Task CustomResponseWriter(HttpContext context, HealthReport healthReport) { key = e.Key, value = e.Value.Status.ToString(), - }) + }), }); return context.Response.WriteAsync(result); } -static async Task LoadJwks(string jwksUri, HttpMessageHandler httpMessageHandler) +static async Task LoadJwks(string jwksUri, HttpMessageHandler httpMessageHandler) { var httpClient = new HttpClient(httpMessageHandler); var httpResponse = await httpClient.GetAsync(jwksUri); - return await httpResponse.Content.ReadAsJson(); + return await httpResponse.Content.ReadAsJson(); } void MigrateDatabase() { var optionsBuilder = new DbContextOptionsBuilder(); + // Run migrations if the DBO connection string is set. var migrationsConnectionString = builder.Configuration.GetConnectionString(DbConstants.ConnectionStrings.Migrations); if (!string.IsNullOrEmpty(migrationsConnectionString)) { - logger.Information("Found connection string for migrations dbo, migrating database"); + Log.Logger.Information("Found connection string for migrations dbo, migrating database"); optionsBuilder.UseSqlServer(migrationsConnectionString); using var dbContext = new CdrAuthServerDatabaseContext(optionsBuilder.Options); dbContext.Database.Migrate(); @@ -353,37 +397,36 @@ static TokenValidationParameters BuildTokenValidationParameters( string errorMessage = $"IDX10205: Issuer validation failed. Issuer: '{issuer}'. Did not match: '{string.Join(',', validationParameters.ValidIssuers)}'."; throw new SecurityTokenInvalidIssuerException(errorMessage) { - InvalidIssuer = issuer + InvalidIssuer = issuer, }; }, ValidateAudience = true, - ValidAudiences = validAudiences, + ValidAudiences = validAudiences, AudienceValidator = (IEnumerable audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) => - { - var validAudiences = new HashSet(validationParameters.ValidAudiences, StringComparer.OrdinalIgnoreCase); + { + var validAudiences = new HashSet(validationParameters.ValidAudiences, StringComparer.OrdinalIgnoreCase); - bool isValid = audiences.Any(audience => - validAudiences.Contains(audience) || - validAudiences.Any(validAudience => audience.StartsWith(validAudience, StringComparison.OrdinalIgnoreCase)) - ); + bool isValid = audiences.Any(audience => + validAudiences.Contains(audience) || + validAudiences.Any(validAudience => audience.StartsWith(validAudience, StringComparison.OrdinalIgnoreCase))); - if (!isValid) - { - string errorMessage = $"IDX10214: Audience validation failed. Audiences: '{string.Join(',', audiences)}'. Did not match: '{string.Join(',', validationParameters.ValidAudiences)}'."; - throw new SecurityTokenInvalidAudienceException(errorMessage) - { - InvalidAudience = string.Join(',', audiences) - }; - } + if (!isValid) + { + string errorMessage = $"IDX10214: Audience validation failed. Audiences: '{string.Join(',', audiences)}'. Did not match: '{string.Join(',', validationParameters.ValidAudiences)}'."; + throw new SecurityTokenInvalidAudienceException(errorMessage) + { + InvalidAudience = string.Join(',', audiences), + }; + } - return isValid; - }, + return isValid; + }, ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(clockSkew), RequireSignedTokens = true, - IssuerSigningKeys = options.Configuration!.JsonWebKeySet.Keys + IssuerSigningKeys = options.Configuration!.JsonWebKeySet.Keys, }; } diff --git a/Source/CdrAuthServer/MappingProfile.cs b/Source/CdrAuthServer/ServiceMappingProfile.cs similarity index 100% rename from Source/CdrAuthServer/MappingProfile.cs rename to Source/CdrAuthServer/ServiceMappingProfile.cs diff --git a/Source/CdrAuthServer/Services/CdrService.cs b/Source/CdrAuthServer/Services/CdrService.cs index 32321bf..afd7729 100644 --- a/Source/CdrAuthServer/Services/CdrService.cs +++ b/Source/CdrAuthServer/Services/CdrService.cs @@ -23,19 +23,18 @@ public async Task GetSoftwareProduct(string softwareProductId) return mapper.Map(entity); } - public async Task InsertDataRecipients(List softwareProducts) + public async Task InsertDataRecipients(List softwareProducts) { if (softwareProducts.Count > 0) { var softwareProductList = mapper.Map>(softwareProducts); - await cdrRepository.InsertDataRecipients(softwareProductList); - } + await cdrRepository.InsertDataRecipients(softwareProductList); + } } public async Task PurgeDataRecipients() - { - await cdrRepository.PurgeDataRecipients(); + { + await cdrRepository.PurgeDataRecipients(); } - } } diff --git a/Source/CdrAuthServer/Services/ClientService.cs b/Source/CdrAuthServer/Services/ClientService.cs index af7c717..3ac0de9 100644 --- a/Source/CdrAuthServer/Services/ClientService.cs +++ b/Source/CdrAuthServer/Services/ClientService.cs @@ -34,7 +34,7 @@ public ClientService( } var client = await _clientRepository.Get(clientId); - return _mapper.Map(client); + return _mapper.Map(client); } public async Task GetClientBySoftwareProductId(string softwareProductId) @@ -67,7 +67,7 @@ public async Task Delete(string clientId) _logger.LogInformation("deleted client with id:{Id}", clientId); } - public async Task GetJwks(Client client) + public async Task GetJwks(Client client) { if (string.IsNullOrEmpty(client.JwksUri)) { @@ -95,6 +95,5 @@ public async Task> GetSigningKeys(Client client) return keys; } - } } diff --git a/Source/CdrAuthServer/Services/ConsentRevocationService.cs b/Source/CdrAuthServer/Services/ConsentRevocationService.cs new file mode 100644 index 0000000..b4b9290 --- /dev/null +++ b/Source/CdrAuthServer/Services/ConsentRevocationService.cs @@ -0,0 +1,162 @@ +using System.Net.Http.Headers; +using CdrAuthServer.Configuration; +using CdrAuthServer.Infrastructure.Certificates; +using CdrAuthServer.Models; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using static CdrAuthServer.Domain.Constants; +using static CdrAuthServer.Services.IConsentRevocationService; + +namespace CdrAuthServer.Services +{ + /// + /// DH initiated consent revocation functionality. + /// + /// The managed http client. + /// The configuration. + /// The logger. + public class ConsentRevocationService(HttpClient httpClient, IOptions configurationOptions, ICertificateLoader certificateLoader, ILogger logger) : IConsentRevocationService + { + private readonly ConfigurationOptions _configurationOptions = configurationOptions.Value; + private readonly Task signingCredentialsTask = CreateSigningCredentials(configurationOptions, certificateLoader); + + /// + /// Create signing credentials from configuration. + /// + /// The configuration which will be used to fetch the PS256 signing certificate details. + /// The certificate loader to read the certificate based on the configuration. + /// A signing credentials generated from the certificate specified in configuration. + private static async Task CreateSigningCredentials(IOptions configurationOptions, ICertificateLoader certificateLoader) + { + var certificate = await certificateLoader.Load(configurationOptions.Value.PS256SigningCertificate!); + return new X509SigningCredentials(certificate, SecurityAlgorithms.RsaSsaPssSha256); + } + + /// + public async Task RevokeAdrArrangement(Client client, string arrangementId, TimeSpan revocationTimeout, CancellationToken cancellationToken = default) + { + Exception? exception = null; + HttpResponseMessage? response = null; + var request = await PopulateRequestMessageForRevocationCall(client, arrangementId); + + using (var requestTimeout = new CancellationTokenSource(revocationTimeout)) + { + // Combine the cancellation token with the incoming one so that if that is cancelled before the timeout it's also handled. + using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource(requestTimeout.Token, cancellationToken); + + try + { + response = await httpClient.SendAsync(request, linkedCancellation.Token).ConfigureAwait(false); + } + catch (Exception ex) + { + exception = ex; + } + } + + return new OutboundCallDetails(request, response, exception); + } + + /// + /// Creates the request message for revoking the arrangement. + /// + /// The client details. + /// The arrangement to revoke. + /// The request message populated with the appropriate payload and bearer token. + private async Task PopulateRequestMessageForRevocationCall(Client client, string cdrArrangementId) + { + // Build the parameters for the call to the DR's arrangement revocation endpoint. + var revocationUri = new Uri($"{client.RecipientBaseUri}/arrangements/revoke"); + var brandId = _configurationOptions.BrandId; + + var signedBearerTokenJwt = await GenerateBearerToken(brandId, revocationUri.ToString()); + logger.LogDebug("Bearer {Token}", signedBearerTokenJwt); + + var formValues = await GetRequestPayload(cdrArrangementId, brandId, revocationUri.ToString()); + var urlEncodedContent = new FormUrlEncodedContent(formValues); + + HttpRequestMessage revokeRequest = new(HttpMethod.Post, revocationUri) { Content = urlEncodedContent }; + revokeRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", signedBearerTokenJwt); + + return revokeRequest; + } + + /// + /// Builds the form values with the arrangement JWT that needs to be revoked. + /// + /// The identifier of the arrangement to be revoked. + /// The brand. + /// The audience. + /// The request payload with arrangement JWT. + private async Task> GetRequestPayload( + string cdrArrangementId, + string brandId, + string audience) + { + var arrangementJwt = await GenerateCdrArrangementJwt(cdrArrangementId, brandId, audience); + logger.LogDebug("Arrangement {ArrangementJWT}", arrangementJwt); + + var formValues = new Dictionary + { + { "cdr_arrangement_jwt", arrangementJwt }, + }; + + return formValues; + } + + /// + /// Generates a JWT representing the CDR arrangement that is to be revoked. + /// + /// The identifier of the arrangement to be revoked. + /// The brand. + /// The audience. + /// The CDR arrangement JWT. + private async Task GenerateCdrArrangementJwt(string cdrArrangementId, string brandId, string audience) + { + var handler = new JsonWebTokenHandler(); + var signingCredentials = await signingCredentialsTask; + var descriptor = new SecurityTokenDescriptor + { + Claims = new Dictionary + { + { ClaimNames.CdrArrangementId, cdrArrangementId }, + { JwtRegisteredClaimNames.Sub, brandId }, + { JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString() }, + }, + Issuer = brandId, + Audience = audience, + Expires = DateTime.UtcNow.AddMinutes(5), + SigningCredentials = signingCredentials, + }; + + return handler.CreateToken(descriptor); + } + + /// + /// Generates a bearer token for the revocation request. + /// + /// The brand. + /// The audience. + /// The bearer token for the revocation request. + private async Task GenerateBearerToken(string brandId, string audience) + { + var handler = new JsonWebTokenHandler(); + var signingCredentials = await signingCredentialsTask; + var descriptor = new SecurityTokenDescriptor + { + Claims = new Dictionary + { + { JwtRegisteredClaimNames.Sub, brandId }, + { JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString() }, + }, + Issuer = brandId, + Audience = audience, + Expires = DateTime.UtcNow.AddMinutes(5), + SigningCredentials = signingCredentials, + }; + + return handler.CreateToken(descriptor); + } + } +} diff --git a/Source/CdrAuthServer/Services/CustomerService.cs b/Source/CdrAuthServer/Services/CustomerService.cs index 2c576e0..00ad607 100644 --- a/Source/CdrAuthServer/Services/CustomerService.cs +++ b/Source/CdrAuthServer/Services/CustomerService.cs @@ -5,17 +5,18 @@ namespace CdrAuthServer.Services { public class CustomerService : ICustomerService - { + { private readonly IConfiguration _config; - private readonly ILogger _logger; - private string SeedDataFilePath => _config[Keys.SeedDataFilePath]; + private readonly ILogger _logger; + + private string SeedDataFilePath => _config[Keys.SeedDataFilePath] ?? string.Empty; public CustomerService( IConfiguration config, ILogger logger) { _config = config; - _logger = logger; + _logger = logger; } public async Task Get(string subjectId) @@ -34,6 +35,7 @@ public async Task Get(string subjectId) _logger.LogInformation("Seed data file '{SeedDataFilePath}' not found.", SeedDataFilePath); return userInfo; } + customerDataJson = await File.ReadAllTextAsync(SeedDataFilePath); } @@ -42,12 +44,12 @@ public async Task Get(string subjectId) _logger.LogInformation("Seed data is unavailable."); return userInfo; } - + var dataHolderCustomer = JsonConvert.DeserializeObject(customerDataJson); var dataHolderCustomerList = dataHolderCustomer?.Customers; if (dataHolderCustomerList != null && dataHolderCustomerList.Any()) { - var customer = dataHolderCustomerList.FirstOrDefault(x => x.LoginId == subjectId); + var customer = dataHolderCustomerList.Find(x => x.LoginId == subjectId); if (customer == null) { @@ -55,13 +57,13 @@ public async Task Get(string subjectId) return userInfo; } - userInfo.GivenName = customer.Person.FirstName; - userInfo.FamilyName = customer.Person.LastName; - userInfo.Name = $"{customer.Person.FirstName} {customer.Person.LastName}"; + userInfo.GivenName = customer.Person?.FirstName ?? string.Empty; + userInfo.FamilyName = customer.Person?.LastName ?? string.Empty; + userInfo.Name = $"{customer.Person?.FirstName} {customer.Person?.LastName}"; return userInfo; - } - + } + return userInfo; } @@ -74,4 +76,4 @@ private async Task DownloadSeedDataFile(string url) } } } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer/Services/GrantService.cs b/Source/CdrAuthServer/Services/GrantService.cs index 547d0dc..0746974 100644 --- a/Source/CdrAuthServer/Services/GrantService.cs +++ b/Source/CdrAuthServer/Services/GrantService.cs @@ -35,7 +35,7 @@ public async Task> ListForClient(string clientId, string grantType) if (grant == null && grantType.Equals(GrantTypes.RefreshToken, StringComparison.Ordinal)) { - // For backwards compatibility ; + // For backwards compatibility var hashedKey = GetHashKey(key); grant = await grantRepository.Get(hashedKey); } @@ -45,11 +45,13 @@ public async Task> ListForClient(string clientId, string grantType) logger.LogError("Grant not found for key:{Key}", key); return null; } + if (grant.GrantType != grantType) { logger.LogError("Grant not found doesn't match for:{Key}", key); return null; } + if (!string.IsNullOrEmpty(clientId) && grant.ClientId != clientId) { logger.LogError("Grant not found doesn't match with clientId:{Key}", clientId); @@ -66,7 +68,7 @@ public async Task> ListForClient(string clientId, string grantType) GrantTypes.RequestUri => mapper.Map(grant), - _ => mapper.Map(grant) + _ => mapper.Map(grant), }; } @@ -107,7 +109,7 @@ public async Task Delete(string? clientId, string grantType, string key) if (grant != null) { await grantRepository.Delete(key); - logger.LogInformation("Grant deleted with, key:{Key}", grant?.Key); + logger.LogInformation("Grant deleted with, key:{Key}", grant.Key); } } } diff --git a/Source/CdrAuthServer/Services/ICdrService.cs b/Source/CdrAuthServer/Services/ICdrService.cs index 6ab4510..6ef4583 100644 --- a/Source/CdrAuthServer/Services/ICdrService.cs +++ b/Source/CdrAuthServer/Services/ICdrService.cs @@ -5,7 +5,9 @@ namespace CdrAuthServer.Services public interface ICdrService { Task GetSoftwareProduct(string softwareProductId); + Task InsertDataRecipients(List softwareProducts); + Task PurgeDataRecipients(); } } diff --git a/Source/CdrAuthServer/Services/IClientService.cs b/Source/CdrAuthServer/Services/IClientService.cs index 7ee8888..d63d1d7 100644 --- a/Source/CdrAuthServer/Services/IClientService.cs +++ b/Source/CdrAuthServer/Services/IClientService.cs @@ -6,11 +6,17 @@ public interface IClientService { Task Get(string? clientId); + Task Create(Client client); + Task Update(Client client); + Task Delete(string clientId); - Task GetJwks(Client client); + + Task GetJwks(Client client); + Task> GetSigningKeys(Client client); + Task GetClientBySoftwareProductId(string softwareProductId); } } diff --git a/Source/CdrAuthServer/Services/IConsentRevocationService.cs b/Source/CdrAuthServer/Services/IConsentRevocationService.cs new file mode 100644 index 0000000..c002065 --- /dev/null +++ b/Source/CdrAuthServer/Services/IConsentRevocationService.cs @@ -0,0 +1,30 @@ +using CdrAuthServer.Models; + +namespace CdrAuthServer.Services +{ + /// + /// DH initiated consent revocation functionality. + /// + public interface IConsentRevocationService + { + /// + /// Represents the details of an outbound call. + /// + /// The request that was sent / attempted. + /// The response that was received (if no exception was thrown). + /// The exception if one was thrown. + public record OutboundCallDetails(HttpRequestMessage Request, HttpResponseMessage? Response, Exception? Exception); + + /// + /// Revokes ADR Arrangement. + /// + /// The client details. + /// The arrangement to revoke. + /// The timeout for the revocation. + /// The cancellation token. + /// + /// The request and response, as well as an exception if one was thrown. + /// + Task RevokeAdrArrangement(Client client, string arrangementId, TimeSpan revocationTimeout, CancellationToken cancellationToken = default); + } +} diff --git a/Source/CdrAuthServer/Services/ICustomerService.cs b/Source/CdrAuthServer/Services/ICustomerService.cs index 1900982..2681cba 100644 --- a/Source/CdrAuthServer/Services/ICustomerService.cs +++ b/Source/CdrAuthServer/Services/ICustomerService.cs @@ -6,4 +6,4 @@ public interface ICustomerService { Task Get(string subjectId); } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer/Services/IGrantService.cs b/Source/CdrAuthServer/Services/IGrantService.cs index 7c2d9de..3da1785 100644 --- a/Source/CdrAuthServer/Services/IGrantService.cs +++ b/Source/CdrAuthServer/Services/IGrantService.cs @@ -5,9 +5,13 @@ namespace CdrAuthServer.Services public interface IGrantService { Task> ListForClient(string clientId, string grantType); + Task Get(string grantType, string key, string? clientId = null); + Task Create(Grant grant); + Task Update(Grant grant); + Task Delete(string? clientId, string grantType, string key); } } diff --git a/Source/CdrAuthServer/Services/IJwksService.cs b/Source/CdrAuthServer/Services/IJwksService.cs index 078c241..03daa21 100644 --- a/Source/CdrAuthServer/Services/IJwksService.cs +++ b/Source/CdrAuthServer/Services/IJwksService.cs @@ -1,11 +1,9 @@ -using CdrAuthServer.Models; -using Microsoft.IdentityModel.Tokens; - -namespace CdrAuthServer.Services +namespace CdrAuthServer.Services { public interface IJwksService { - Task GetJwks(Uri jwksUri); - Task GetJwks(Uri jwksUri, string kid); + Task GetJwks(Uri jwksUri); + + Task GetJwks(Uri jwksUri, string kid); } } diff --git a/Source/CdrAuthServer/Services/IRegisterClientService.cs b/Source/CdrAuthServer/Services/IRegisterClientService.cs new file mode 100644 index 0000000..27e4aca --- /dev/null +++ b/Source/CdrAuthServer/Services/IRegisterClientService.cs @@ -0,0 +1,18 @@ +using CdrAuthServer.Models.Json; +using CdrAuthServer.Models.Register; + +namespace CdrAuthServer.Services +{ + /// + /// Provides functionality for interacting with CDR register. + /// + public interface IRegisterClientService + { + /// + /// Retrieve Data Recipients from the register. + /// + /// The cancellation token. + /// That data recipients or null if the request was unsuccessful. + Task?> GetDataRecipients(CancellationToken cancellationToken = default); + } +} diff --git a/Source/CdrAuthServer/Services/ITokenService.cs b/Source/CdrAuthServer/Services/ITokenService.cs index 02b572d..010f22c 100644 --- a/Source/CdrAuthServer/Services/ITokenService.cs +++ b/Source/CdrAuthServer/Services/ITokenService.cs @@ -8,15 +8,14 @@ namespace CdrAuthServer.Services public interface ITokenService { Task IssueTokens( - TokenRequest tokenRequest, - string cnf, + TokenRequest tokenRequest, + string cnf, ConfigurationOptions configOptions); Task IssueIdToken( string clientId, string subjectId, ConfigurationOptions configOptions, - bool encrypt, string? state = null, string? nonce = null, string? authCode = null, @@ -29,7 +28,7 @@ Task CreateToken( string tokenType, int expirySeconds, ConfigurationOptions configOptions, - string? signingAlg = Constants.Algorithms.Signing.PS256, + string signingAlg = Constants.Algorithms.Signing.PS256, string? encryptedResponseAlg = null, string? encryptedResponseEnc = null, Microsoft.IdentityModel.Tokens.JsonWebKey? clientJwk = null, diff --git a/Source/CdrAuthServer/Services/JwksService.cs b/Source/CdrAuthServer/Services/JwksService.cs index d3ba28c..c38ec19 100644 --- a/Source/CdrAuthServer/Services/JwksService.cs +++ b/Source/CdrAuthServer/Services/JwksService.cs @@ -24,7 +24,7 @@ public JwksService( _cache = cache; } - public async Task GetJwks(Uri jwksUri) + public async Task GetJwks(Uri jwksUri) { var cachedJwks = RetrieveFromCache(jwksUri); if (cachedJwks != null) @@ -36,13 +36,13 @@ public JwksService( return await RefreshJwks(jwksUri); } - public async Task GetJwks(Uri jwksUri, string kid) + public async Task GetJwks(Uri jwksUri, string kid) { // Retrieve the jwks, which could be from cache. - var jwks = await GetJwks(jwksUri); + var jwks = RetrieveFromCache(jwksUri); // Check the jwks to see if the given kid is included in the set. - if (jwks.Keys.Any(k => k.Kid == kid)) + if (jwks != null && jwks.Keys.Any(k => k.Kid == kid)) { _logger.LogInformation("Matching kid ({Kid}) was found in jwks", kid); return jwks; @@ -53,7 +53,7 @@ public JwksService( return await RefreshJwks(jwksUri); } - public async Task RefreshJwks(Uri jwksUri) + private async Task RefreshJwks(Uri jwksUri) { HttpResponseMessage httpResponse; try @@ -77,7 +77,7 @@ public JwksService( var statusCode = httpResponse.StatusCode; var responseContent = await httpResponse.Content.ReadAsStringAsync(); _logger.LogError( - "{jwksUri} returned {statusCode} Content:\r\n{responseContent}", + "{JwksUri} returned {StatusCode} Content:\r\n{ResponseContent}", jwksUri, statusCode, responseContent); @@ -85,7 +85,12 @@ public JwksService( } var jwks = await GetJwksFromResponse(jwksUri, httpResponse); - AddToCache(jwksUri, jwks); + + if (jwks != null) + { + AddToCache(jwksUri, jwks); + } + return jwks; } @@ -95,9 +100,9 @@ public JwksService( { return await httpResponse.Content.ReadAsJson(); } - catch + catch (Exception ex) { - _logger.LogError("No valid JWKS found from {jwksUri}", jwksUri); + _logger.LogError(ex, "No valid JWKS found from {JwksUri}", jwksUri); throw new JwksException($"No valid JWKS found from {jwksUri}"); } } diff --git a/Source/CdrAuthServer/Services/RegisterClientService.cs b/Source/CdrAuthServer/Services/RegisterClientService.cs new file mode 100644 index 0000000..945df89 --- /dev/null +++ b/Source/CdrAuthServer/Services/RegisterClientService.cs @@ -0,0 +1,31 @@ +using CdrAuthServer.Configuration; +using CdrAuthServer.Models.Json; +using CdrAuthServer.Models.Register; +using Microsoft.Extensions.Options; + +namespace CdrAuthServer.Services +{ + /// + /// Provides functionality for interacting with CDR register. + /// + public class RegisterClientService(HttpClient httpClient, IOptions cdrRegisterOptions) : IRegisterClientService + { + private readonly CdrRegisterConfiguration _cdrRegisterOptions = cdrRegisterOptions.Value; + + /// + public async Task?> GetDataRecipients(CancellationToken cancellationToken = default) + { + var request = new HttpRequestMessage(HttpMethod.Get, _cdrRegisterOptions.GetDataRecipientsEndpoint); + request.Headers.Add("x-v", _cdrRegisterOptions.Version.ToString()); + + var response = await httpClient.SendAsync(request, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + return null; + } + + return await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken); + } + } +} diff --git a/Source/CdrAuthServer/Services/TokenService.cs b/Source/CdrAuthServer/Services/TokenService.cs index 03e22b8..9dbb515 100644 --- a/Source/CdrAuthServer/Services/TokenService.cs +++ b/Source/CdrAuthServer/Services/TokenService.cs @@ -60,8 +60,13 @@ private async Task IssueAccessToken( claims.Add(new Claim(ClaimNames.Issuer, issuer)); claims.Add(new Claim(ClaimNames.ClientId, clientId)); - claims.Add(new Claim(ClaimNames.Subject, EncryptSub(subjectId, client))); - claims.Add(new Claim(ClaimNames.SoftwareId, client.SoftwareId)); + + if (client != null) + { + claims.Add(new Claim(ClaimNames.Subject, EncryptSub(subjectId, client))); + } + + claims.Add(new Claim(ClaimNames.SoftwareId, client?.SoftwareId ?? string.Empty)); claims.Add(new Claim(ClaimNames.JwtId, Guid.NewGuid().ToString())); claims.Add(new Claim(ClaimNames.AuthTime, DateTime.UtcNow.ToEpoch().ToString(), ClaimValueTypes.Integer64)); @@ -85,9 +90,9 @@ private async Task IssueAccessToken( claims.Add(new Claim(ClaimNames.CdrArrangementVersion, cdrArrangementVersion.Value.ToString(), ClaimValueTypes.Integer32)); } - if (!string.IsNullOrEmpty(client.SectorIdentifierUri)) + if (!string.IsNullOrEmpty(client?.SectorIdentifierUri)) { - claims.Add(new Claim(ClaimNames.SectorIdentifierUri, client.SectorIdentifierUri)); + claims.Add(new Claim(ClaimNames.SectorIdentifierUri, client?.SectorIdentifierUri ?? string.Empty)); } // If there are selected account Ids, add them to the access token. @@ -96,8 +101,8 @@ private async Task IssueAccessToken( var idPermananceManager = new IdPermanenceManager(_configuration); var idParameters = new IdPermanenceParameters { - SoftwareProductId = client.SoftwareId, - CustomerId = subjectId + SoftwareProductId = client?.SoftwareId ?? string.Empty, + CustomerId = subjectId, }; claims.AddRange(accountIds.Select(a => new Claim(ClaimNames.AccountId, idPermananceManager.EncryptId(a, idParameters)))); @@ -111,12 +116,11 @@ private async Task IssueAccessToken( configOptions, cnf: cnf); } - + public async Task IssueIdToken( string clientId, string subjectId, ConfigurationOptions configOptions, - bool encrypt, string? state = null, string? nonce = null, string? authCode = null, @@ -128,7 +132,6 @@ public async Task IssueIdToken( AddOptionalClaims(claims, nonce, authCode, accessToken, state); await AddMandatoryClaims(claims, subjectId, clientId, authTime); - // Add user name claims. These should only be added during a token request, not the authorisation request. if (!string.IsNullOrEmpty(accessToken)) { @@ -138,13 +141,6 @@ public async Task IssueIdToken( // Set the acr claim to a LoA of 2 - urn:cds.au:cdr:2 claims.Add(new Claim(ClaimNames.Acr, configOptions.DefaultAcrValue)); - var (encryptedResponseAlg, encryptedResponseEnc, encryptedJwk) = await GetEncryptionParameters(clientId, encrypt); - - _logger.LogDebug("Encrypting id_token: {Encrypt}", encrypt); - _logger.LogDebug("encryptedResponseAlg: {EncryptedResponseAlg}", encryptedResponseAlg); - _logger.LogDebug("encryptedResponseEnc: {EncryptedResponseEnc}", encryptedResponseEnc); - _logger.LogDebug("encryptedJwk: {EncryptedJwk}", encryptedJwk); - var client = await _clientService.Get(clientId); return await CreateToken( @@ -153,10 +149,7 @@ public async Task IssueIdToken( TokenTypes.IdToken, configOptions.IdTokenExpirySeconds, configOptions, - signingAlg: client!.IdTokenSignedResponseAlg ?? Constants.Algorithms.Signing.PS256, - encryptedResponseAlg: encryptedResponseAlg, - encryptedResponseEnc: encryptedResponseEnc, - clientJwk: encryptedJwk); + signingAlg: client!.IdTokenSignedResponseAlg ?? Constants.Algorithms.Signing.PS256); } private static void AddOptionalClaims(List claims, string? nonce, string? authCode, string? accessToken, string? state) @@ -166,16 +159,19 @@ private static void AddOptionalClaims(List claims, string? nonce, string? { claims.Add(new Claim(ClaimNames.Nonce, nonce)); } + // add at_hash claim if (accessToken != null && accessToken.HasValue()) { claims.Add(new Claim(ClaimNames.AccessTokenHash, CreateHashClaimValue(accessToken))); } + // add c_hash claim if (authCode != null && authCode.HasValue()) { claims.Add(new Claim(ClaimNames.AuthorizationCodeHash, CreateHashClaimValue(authCode))); } + // add s_hash claim if (state != null && state.HasValue()) { @@ -190,13 +186,16 @@ private async Task AddMandatoryClaims(List claims, string subjectId, stri // Run the sub claim through the ID Permanence. var client = await _clientService.Get(clientId); - claims.Add(new Claim(ClaimNames.Subject, EncryptSub(subjectId, client))); + if (client != null) + { + claims.Add(new Claim(ClaimNames.Subject, EncryptSub(subjectId, client))); - // add auth_time claim - claims.Add(new Claim(ClaimNames.AuthTime, authTime ?? DateTime.UtcNow.ToEpoch().ToString(), ClaimValueTypes.Integer64)); + // add auth_time claim + claims.Add(new Claim(ClaimNames.AuthTime, authTime ?? DateTime.UtcNow.ToEpoch().ToString(), ClaimValueTypes.Integer64)); - // add updated_at claim - claims.Add(new Claim(ClaimNames.UpdatedAt, DateTime.UtcNow.ToEpoch().ToString(), ClaimValueTypes.Integer64)); + // add updated_at claim + claims.Add(new Claim(ClaimNames.UpdatedAt, DateTime.UtcNow.ToEpoch().ToString(), ClaimValueTypes.Integer64)); + } } private async Task AddUserClaims(List claims, string subjectId, ConfigurationOptions configOptions) @@ -217,37 +216,13 @@ private async Task AddUserClaims(List claims, string subjectId, Configura } } - private async Task<(string? encryptedResponseAlg, string? encryptedResponseEnc, Microsoft.IdentityModel.Tokens.JsonWebKey? encryptedJwk)> GetEncryptionParameters(string clientId, bool encrypt) - { - string? encryptedResponseAlg = null; - string? encryptedResponseEnc = null; - Microsoft.IdentityModel.Tokens.JsonWebKey? encryptedJwk = null; - - if (encrypt) - { - var client = await _clientService.Get(clientId); - var jwks = await _clientService.GetJwks(client!); - encryptedJwk = jwks.Keys.FirstOrDefault(jwk => jwk.Alg == client!.IdTokenEncryptedResponseAlg); - - if (encryptedJwk == null) - { - throw new InvalidOperationException("Unable to get encryption key required for id_token encryption from client JWKS"); - } - - encryptedResponseAlg = client!.IdTokenEncryptedResponseAlg ?? Constants.Algorithms.Jwe.Alg.RSAOAEP256; - encryptedResponseEnc = client.IdTokenEncryptedResponseEnc ?? Constants.Algorithms.Jwe.Enc.A256GCM; - } - - return (encryptedResponseAlg, encryptedResponseEnc, encryptedJwk); - } - private string EncryptSub(string subjectId, Client client) { var idPermanenceManager = new IdPermanenceManager(_configuration); var param = new SubPermanenceParameters() { SoftwareProductId = client.SoftwareId, - SectorIdentifierUri = client.SectorIdentifierUri ?? "" + SectorIdentifierUri = client.SectorIdentifierUri ?? string.Empty, }; return idPermanenceManager.EncryptSub(subjectId, param); @@ -262,41 +237,42 @@ private static string CreateHashClaimValue(string value) public async Task IssueTokens(TokenRequest tokenRequest, string cnf, ConfigurationOptions configOptions) { - if (tokenRequest.grant_type == GrantTypes.ClientCredentials) + if (tokenRequest.Grant_type == GrantTypes.ClientCredentials) { return await GetClientCredentialsTokenResponse(tokenRequest, cnf, configOptions); } - if (tokenRequest.grant_type == GrantTypes.AuthCode) + if (tokenRequest.Grant_type == GrantTypes.AuthCode) { return await GetAuthCodeTokenResponse(tokenRequest, cnf, configOptions); } - if (tokenRequest.grant_type == GrantTypes.RefreshToken) + if (tokenRequest.Grant_type == GrantTypes.RefreshToken) { return await GetRefreshTokenResponse(tokenRequest, cnf, configOptions); } - return new TokenResponse() { Error = new Error(ErrorCodes.Generic.UnsupportedGrantType, $"Invalid grant_type: {tokenRequest.grant_type}") }; + return new TokenResponse() { Error = new Error(ErrorCodes.Generic.UnsupportedGrantType, $"Invalid grant_type: {tokenRequest.Grant_type}") }; } private async Task GetRefreshTokenResponse(TokenRequest tokenRequest, string cnf, ConfigurationOptions configOptions) { // Get the refresh token and cdr arrangement grants. - if (await _grantService.Get(GrantTypes.RefreshToken, tokenRequest.refresh_token, tokenRequest.client_id) is not RefreshTokenGrant refreshTokenGrant) + if (await _grantService.Get(GrantTypes.RefreshToken, tokenRequest.Refresh_token, tokenRequest.Client_id) is not RefreshTokenGrant refreshTokenGrant) { throw new InvalidOperationException($"Value is null or empty {nameof(refreshTokenGrant)}"); } - if (await _grantService.Get(GrantTypes.CdrArrangement, refreshTokenGrant.CdrArrangementId ?? "", tokenRequest.client_id) is not CdrArrangementGrant cdrArrangementGrant) + + if (await _grantService.Get(GrantTypes.CdrArrangement, refreshTokenGrant.CdrArrangementId ?? string.Empty, tokenRequest.Client_id) is not CdrArrangementGrant cdrArrangementGrant) { throw new InvalidOperationException($"Value is null or empty {nameof(cdrArrangementGrant)}"); } var scope = refreshTokenGrant.Scope; - if (tokenRequest.scope.HasValue()) + if (tokenRequest.Scope.HasValue()) { var currentScopes = refreshTokenGrant.Scope.Split(' '); - var newScopes = tokenRequest.scope.Split(' '); + var newScopes = tokenRequest.Scope.Split(' '); // Verify that the client has not requested additional scopes that exceed the original request. foreach (var newScope in newScopes) @@ -305,15 +281,15 @@ private async Task GetRefreshTokenResponse(TokenRequest tokenRequ { return new TokenResponse() { - Error = new Error(ErrorCodes.Generic.InvalidScope, "Additional scopes were requested in the refresh_token request") + Error = new Error(ErrorCodes.Generic.InvalidScope, "Additional scopes were requested in the refresh_token request"), }; } } // Additional scopes were not requested, so return the same or subset of scopes. - scope = tokenRequest.scope; + scope = tokenRequest.Scope; } - + List accountIds = GetAccountIdsForRefreshToken(cdrArrangementGrant.Data); string? idToken; @@ -336,7 +312,6 @@ private async Task GetRefreshTokenResponse(TokenRequest tokenRequ refreshTokenGrant.ClientId, refreshTokenGrant.SubjectId, configOptions, - configOptions.AlwaysEncryptIdTokens || (refreshTokenGrant.ResponseType?.IsHybridFlow() ?? false), accessToken: accessToken, authTime: refreshTokenGrant.CreatedAt.ToEpoch().ToString()); } @@ -344,7 +319,7 @@ private async Task GetRefreshTokenResponse(TokenRequest tokenRequ { return new TokenResponse() { - Error = new Error(ErrorCodes.Cds.UnexpectedError, ex.Message) + Error = new Error(ErrorCodes.Cds.UnexpectedError, ex.Message), }; } @@ -352,42 +327,41 @@ private async Task GetRefreshTokenResponse(TokenRequest tokenRequ { IdToken = idToken, AccessToken = accessToken, - RefreshToken = tokenRequest.refresh_token, + RefreshToken = tokenRequest.Refresh_token, ExpiresIn = configOptions.AccessTokenExpirySeconds, TokenType = "Bearer", CdrArrangementId = refreshTokenGrant.CdrArrangementId, - Scope = scope + Scope = scope, }; } private static List GetAccountIdsForRefreshToken(IDictionary data) - { - if (data.TryGetValue(ClaimNames.AccountId, out var item)) + { + if (data.TryGetValue(ClaimNames.AccountId, out var item) && item != null) { - if (item != null) + var accountIds = (System.Collections.IEnumerable)item; + List accountList = []; + + foreach (var accountId in accountIds) { - var accountIds = ((System.Collections.IEnumerable)item); - List accountList = []; - - foreach (var accountId in accountIds) - { - accountList.Add(accountId.ToString() ?? ""); - } - return accountList; + accountList.Add(accountId.ToString() ?? string.Empty); } + + return accountList; } + return []; } private async Task GetAuthCodeTokenResponse(TokenRequest tokenRequest, string cnf, ConfigurationOptions configOptions) { // Get the auth code grant. - if (await _grantService.Get(GrantTypes.AuthCode, tokenRequest.code, tokenRequest.client_id) is not AuthorizationCodeGrant authCodeGrant) + if (await _grantService.Get(GrantTypes.AuthCode, tokenRequest.Code, tokenRequest.Client_id) is not AuthorizationCodeGrant authCodeGrant) { throw new InvalidOperationException($"Value is null or empty {nameof(authCodeGrant)}"); } - var authRequestObject = JsonConvert.DeserializeObject(authCodeGrant.Request ?? ""); + var authRequestObject = JsonConvert.DeserializeObject(authCodeGrant.Request ?? string.Empty); var sharingDuration = authRequestObject?.Claims.SharingDuration ?? 0; var existingCdrArrangementId = authRequestObject?.Claims.CdrArrangementId; string? refreshToken = null; @@ -402,6 +376,7 @@ private async Task GetAuthCodeTokenResponse(TokenRequest tokenReq string? cdrArrangementId; int? cdrArrangementVersion; + // Amending consent. if (!string.IsNullOrEmpty(existingCdrArrangementId)) { @@ -421,7 +396,7 @@ private async Task GetAuthCodeTokenResponse(TokenRequest tokenReq cdrArrangementGrant.Version = cdrArrangementVersion.Value; // As the cdr arrangement has been amended the existing refresh token needs to be removed and a new refresh token needs to be issued. - await _grantService.Delete(authCodeGrant.ClientId, GrantTypes.RefreshToken, cdrArrangementGrant.RefreshToken ?? ""); + await _grantService.Delete(authCodeGrant.ClientId, GrantTypes.RefreshToken, cdrArrangementGrant.RefreshToken ?? string.Empty); refreshToken = Guid.NewGuid().ToString(); var refreshTokenGrant = new RefreshTokenGrant() @@ -435,7 +410,7 @@ private async Task GetAuthCodeTokenResponse(TokenRequest tokenReq SubjectId = authCodeGrant.SubjectId, CdrArrangementId = cdrArrangementId, ResponseType = authRequestObject!.ResponseType, - AuthorizationCode = tokenRequest.code // Keep track of the original auth code that initiated the request + AuthorizationCode = tokenRequest.Code, // Keep track of the original auth code that initiated the request }; await _grantService.Create(refreshTokenGrant); @@ -464,7 +439,7 @@ private async Task GetAuthCodeTokenResponse(TokenRequest tokenReq SubjectId = authCodeGrant.SubjectId, CdrArrangementId = cdrArrangementId, ResponseType = authRequestObject!.ResponseType, - AuthorizationCode = tokenRequest.code // Keep track of the original auth code that initiated the request + AuthorizationCode = tokenRequest.Code, // Keep track of the original auth code that initiated the request }; await _grantService.Create(refreshTokenGrant); } @@ -476,12 +451,12 @@ private async Task GetAuthCodeTokenResponse(TokenRequest tokenReq ClientId = authCodeGrant.ClientId, GrantType = GrantTypes.CdrArrangement, CreatedAt = authTime, - ExpiresAt = (expiresAt == null ? DateTime.UtcNow.AddMinutes(configOptions.AccessTokenExpirySeconds) : expiresAt.Value), + ExpiresAt = expiresAt == null ? DateTime.UtcNow.AddMinutes(configOptions.AccessTokenExpirySeconds) : expiresAt.Value, Scope = authCodeGrant.Scope, // Filtered scopes. SubjectId = authCodeGrant.SubjectId, AccountIds = authCodeGrant.GetAccountIds(), RefreshToken = refreshToken, - Version = cdrArrangementVersion.Value + Version = cdrArrangementVersion.Value, }; await _grantService.Create(cdrArrangementGrant); } @@ -489,7 +464,7 @@ private async Task GetAuthCodeTokenResponse(TokenRequest tokenReq // Delete the auth code grant as it has been used. await _grantService.Delete(authCodeGrant.ClientId, GrantTypes.AuthCode, authCodeGrant.Key); - var accessTokenScopes = GetTokenResponseScopes(tokenRequest.scope, authCodeGrant.Scope); + var accessTokenScopes = GetTokenResponseScopes(tokenRequest.Scope, authCodeGrant.Scope); if (authRequestObject == null) { @@ -504,18 +479,17 @@ private async Task GetAuthCodeTokenResponse(TokenRequest tokenReq authCodeGrant.ClientId, authCodeGrant.SubjectId, authCodeGrant.GetAccountIds(), - accessTokenScopes ?? "", + accessTokenScopes ?? string.Empty, cnf, configOptions, cdrArrangementId, cdrArrangementVersion, - tokenRequest.code); + tokenRequest.Code); idToken = await IssueIdToken( authCodeGrant.ClientId, authCodeGrant.SubjectId, configOptions, - encrypt: configOptions.AlwaysEncryptIdTokens || authRequestObject.IsHybridFlow(), authCode: authCodeGrant.Key, nonce: authRequestObject!.Nonce, accessToken: accessToken, @@ -525,7 +499,7 @@ private async Task GetAuthCodeTokenResponse(TokenRequest tokenReq { return new TokenResponse() { - Error = new Error(ErrorCodes.Cds.UnexpectedError, ex.Message) + Error = new Error(ErrorCodes.Cds.UnexpectedError, ex.Message), }; } @@ -537,7 +511,7 @@ private async Task GetAuthCodeTokenResponse(TokenRequest tokenReq ExpiresIn = configOptions.AccessTokenExpirySeconds, TokenType = "Bearer", CdrArrangementId = cdrArrangementId, - Scope = accessTokenScopes + Scope = accessTokenScopes, }; } @@ -576,12 +550,12 @@ private async Task IssueAccessTokenForClient(TokenRequest tokenRequest, { var claims = new List { - new(ClaimNames.ClientId, tokenRequest.client_id), - new(ClaimNames.JwtId, Guid.NewGuid().ToString()) + new(ClaimNames.ClientId, tokenRequest.Client_id), + new(ClaimNames.JwtId, Guid.NewGuid().ToString()), }; // Add the scopes as an array. - var scopes = SetAccessTokenScopes(tokenRequest.scope, GrantTypes.ClientCredentials, configOptions).Split(' '); + var scopes = SetAccessTokenScopes(tokenRequest.Scope, GrantTypes.ClientCredentials, configOptions).Split(' '); claims.AddRange(scopes.Select(s => new Claim(ClaimNames.Scope, s))); return await CreateToken( @@ -614,7 +588,7 @@ public async Task CreateToken( "cnf", JsonConvert.SerializeObject(new Dictionary { - { "x5t#S256", cnf } + { "x5t#S256", cnf }, }), System.IdentityModel.Tokens.Jwt.JsonClaimValueTypes.Json)); } @@ -646,11 +620,10 @@ public async Task CreateToken( return token; } - // Create a signed, encrypted JWT. var rsaEncryption = GetEncryptionKey(clientJwk); try - { + { _logger.LogDebug("Encrypting Id Token with Alg {Alg}, Enc {Enc}", encryptedResponseAlg, encryptedResponseEnc); // Encode the token and add the kid @@ -659,8 +632,9 @@ public async Task CreateToken( rsaEncryption, GetJweAlgorithm(encryptedResponseAlg), GetJweEncryption(encryptedResponseEnc), - extraHeaders: new Dictionary() { - { "kid", clientJwk!.Kid } + extraHeaders: new Dictionary() + { + { "kid", clientJwk!.Kid }, }); } catch (Exception ex) @@ -685,7 +659,7 @@ private async Task GetSigningCredentials( var securityKey = new ECDsaSecurityKey(ecdsa) { KeyId = es256Cert?.Thumbprint }; return new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256) { - CryptoProviderFactory = new CryptoProviderFactory() + CryptoProviderFactory = new CryptoProviderFactory(), }; } diff --git a/Source/CdrAuthServer/SwaggerFilters/AuthorizationOperationFilter.cs b/Source/CdrAuthServer/SwaggerFilters/AuthorizationOperationFilter.cs index 8f76354..a607b2f 100644 --- a/Source/CdrAuthServer/SwaggerFilters/AuthorizationOperationFilter.cs +++ b/Source/CdrAuthServer/SwaggerFilters/AuthorizationOperationFilter.cs @@ -16,7 +16,7 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) { var typeList = new List() { typeof(PolicyAuthorizeAttribute), typeof(ValidateMtlsAttribute), typeof(ValidateClientAssertionAttribute), typeof(ServiceFilterAttribute) }; var relAtts = AttributeExtensions.GetAttributes(typeList, context.MethodInfo, true); - + if (relAtts.Any()) { var authAtt = (PolicyAuthorizeAttribute?)relAtts.FirstOrDefault(attr => attr.GetType() == typeof(PolicyAuthorizeAttribute)); @@ -27,8 +27,8 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) if (authAtt != null) { - //Get the details of the policy - var authPolicy = authAtt.policy.GetPolicy(); + // Get the details of the policy + var authPolicy = authAtt.PolicyName.GetPolicy(); if (authPolicy != null) { @@ -36,14 +36,17 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) { openApiObj["hasHolderOfKeyRequirement"] = new OpenApiBoolean(true); } + if (authPolicy.HasAccessTokenRequirement) { openApiObj["hasAccessTokenRequirement"] = new OpenApiBoolean(true); } + if (authPolicy.HasMtlsRequirement) { openApiObj["hasMtlsRequirement"] = new OpenApiBoolean(true); } + if (!authPolicy.ScopeRequirement.IsNullOrEmpty()) { openApiObj["scopeRequirement"] = new OpenApiString(authPolicy.ScopeRequirement); @@ -51,7 +54,8 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) } } - if (mtlsFilterAttr != null) //Make MTLS requirement checks the same + // Make MTLS requirement checks the same + if (mtlsFilterAttr != null) { openApiObj["hasValidateMtlsAttribute"] = new OpenApiBoolean(true); } diff --git a/Source/CdrAuthServer/SwaggerFilters/CustomDocumentFilter.cs b/Source/CdrAuthServer/SwaggerFilters/CustomDocumentFilter.cs index c9bae38..14fecee 100644 --- a/Source/CdrAuthServer/SwaggerFilters/CustomDocumentFilter.cs +++ b/Source/CdrAuthServer/SwaggerFilters/CustomDocumentFilter.cs @@ -20,7 +20,7 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) foreach (var op in path.Operations.Select(op => op.Value)) { - //api-version included as it is currently used in some places in PT instead of x-v + // api-version included as it is currently used in some places in PT instead of x-v if (op.Parameters.Any(p => p.Name.Equals("x-v", StringComparison.OrdinalIgnoreCase)) || op.Parameters.Any(p => p.Name.Equals("api-version", StringComparison.OrdinalIgnoreCase))) { op.Extensions.TryGetValue("x-version", out var value); diff --git a/Source/CdrAuthServer/Validation/AuthorizeRequestValidationResult.cs b/Source/CdrAuthServer/Validation/AuthorizeRequestValidationResult.cs index 4fa031c..825ffab 100644 --- a/Source/CdrAuthServer/Validation/AuthorizeRequestValidationResult.cs +++ b/Source/CdrAuthServer/Validation/AuthorizeRequestValidationResult.cs @@ -6,7 +6,8 @@ public class AuthorizeRequestValidationResult : ValidationResult { public AuthorizationRequestObject ValidatedAuthorizationRequestObject { get; set; } - public AuthorizeRequestValidationResult(bool isValid) : base(isValid) + public AuthorizeRequestValidationResult(bool isValid) + : base(isValid) { ValidatedAuthorizationRequestObject = new AuthorizationRequestObject(); } diff --git a/Source/CdrAuthServer/Validation/AuthorizeRequestValidator.cs b/Source/CdrAuthServer/Validation/AuthorizeRequestValidator.cs index aca0b5b..c1aed8e 100644 --- a/Source/CdrAuthServer/Validation/AuthorizeRequestValidator.cs +++ b/Source/CdrAuthServer/Validation/AuthorizeRequestValidator.cs @@ -32,9 +32,9 @@ public AuthorizeRequestValidator( /// Validates an auth request using the requestObject (from PAR endpoint) and /// the parameters passed directly to the auth endpoint (authRequest). /// - /// Parameters received on authorization endpoint - /// configuration - /// checkGrantExpiredOrUsed + /// Parameters received on authorization endpoint. + /// configuration. + /// checkGrantExpiredOrUsed. /// /// AuthorizeRequestValidationResult containing a validated auth request object. /// @@ -50,28 +50,53 @@ public async Task Validate( var result = new AuthorizeRequestValidationResult(false); // Client validation. - if (!await ValidateClient(authRequest, result)) return result; - var client = await _clientService.Get(authRequest.client_id); - - if (client == null) return ErrorResult(result, ErrorCatalogue.CLIENT_NOT_FOUND); + if (!await ValidateClient(authRequest, result)) + { + return result; + } + + var client = await _clientService.Get(authRequest.Client_id); + + if (client == null) + { + return ErrorResult(result, ErrorCatalogue.CLIENT_NOT_FOUND); + } // Request URI validation. - if (!await ValidateRequestUri(authRequest, result, checkGrantExpiredOrUsed)) return result; + if (!await ValidateRequestUri(authRequest, result, checkGrantExpiredOrUsed)) + { + return result; + } // Validate and set the Redirect URI based on parameters, request object takes precedence. - if (!ValidateRedirectUri(authRequest, result, client)) return result; + if (!ValidateRedirectUri(authRequest, result, client)) + { + return result; + } // Response type validation. - if (!ValidateResponseType(authRequest, result, configOptions)) return result; + if (!ValidateResponseType(authRequest, result, configOptions)) + { + return result; + } // Scope validation. - if (!ValidateScope(authRequest, result)) return result; + if (!ValidateScope(authRequest, result)) + { + return result; + } // Response mode validation. - if (!ValidateResponseMode(authRequest, result)) return result; + if (!ValidateResponseMode(authRequest, result)) + { + return result; + } // Check software product status (if configured). - if (!await ValidateSoftwareProduct(authRequest, result, client, configOptions)) return result; + if (!await ValidateSoftwareProduct(authRequest, result, client, configOptions)) + { + return result; + } SetOptionalAndDefaultValues(authRequest, result); @@ -81,16 +106,16 @@ public async Task Validate( private async Task ValidateClient(AuthorizeRequest authRequest, AuthorizeRequestValidationResult result) { - if (string.IsNullOrEmpty(authRequest.client_id)) + if (string.IsNullOrEmpty(authRequest.Client_id)) { _logger.LogError("AuthorizeRequestValidationResult - client_id is missing"); return SetErrorResult(result, ErrorCatalogue.CLIENT_ID_MISSING); } - var client = await _clientService.Get(authRequest.client_id); + var client = await _clientService.Get(authRequest.Client_id); if (client == null) { - _logger.LogError("AuthorizeRequestValidationResult - no client found: {Client_id}", authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - no client found: {Client_id}", authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.INVALID_CLIENT_ID); } @@ -99,39 +124,39 @@ private async Task ValidateClient(AuthorizeRequest authRequest, AuthorizeR private async Task ValidateRequestUri(AuthorizeRequest authRequest, AuthorizeRequestValidationResult result, bool checkGrantExpiredOrUsed) { - if (string.IsNullOrEmpty(authRequest.request_uri)) + if (string.IsNullOrEmpty(authRequest.Request_uri)) { _logger.LogError("AuthorizeRequestValidationResult - request_uri is missing"); return SetErrorResult(result, ErrorCatalogue.REQUEST_URI_MISSING); } - var requestUriGrant = await _grantService.Get(GrantTypes.RequestUri, authRequest.request_uri, authRequest.client_id) as RequestUriGrant; + var requestUriGrant = await _grantService.Get(GrantTypes.RequestUri, authRequest.Request_uri, authRequest.Client_id) as RequestUriGrant; if (requestUriGrant == null) { - _logger.LogError("AuthorizeRequestValidationResult - no requestUriGrant found for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - no requestUriGrant found for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.INVALID_REQUEST_URI); } result.ValidatedAuthorizationRequestObject = JsonConvert.DeserializeObject(requestUriGrant.Request ?? string.Empty) ?? new AuthorizationRequestObject(); // Make sure client_id matches client_id in request object. - if (!string.IsNullOrEmpty(result.ValidatedAuthorizationRequestObject.ClientId) && authRequest.client_id != result.ValidatedAuthorizationRequestObject.ClientId) + if (!string.IsNullOrEmpty(result.ValidatedAuthorizationRequestObject.ClientId) && authRequest.Client_id != result.ValidatedAuthorizationRequestObject.ClientId) { - _logger.LogError("AuthorizeRequestValidationResult - client_id does not match request_uri client_id for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - client_id does not match request_uri client_id for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.REQUEST_URI_CLIENT_ID_MISMATCH); } // Check if the request uri has expired. if (checkGrantExpiredOrUsed && requestUriGrant.IsExpired) { - _logger.LogError("AuthorizeRequestValidationResult - request_uri has expired for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - request_uri has expired for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.REQUEST_URI_EXPIRED); } // Check if the request uri has already been used. if (checkGrantExpiredOrUsed && requestUriGrant.UsedAt != null) { - _logger.LogError("AuthorizeRequestValidationResult - request_uri has already been used for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - request_uri has already been used for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.REQUEST_URI_ALREADY_USED); } @@ -140,7 +165,7 @@ private async Task ValidateRequestUri(AuthorizeRequest authRequest, Author private bool ValidateRedirectUri(AuthorizeRequest authRequest, AuthorizeRequestValidationResult result, Client client) { - var redirectUri = result.ValidatedAuthorizationRequestObject.RedirectUri ?? authRequest.redirect_uri; + var redirectUri = result.ValidatedAuthorizationRequestObject.RedirectUri ?? authRequest.Redirect_uri; if (string.IsNullOrEmpty(redirectUri)) { @@ -157,7 +182,7 @@ private bool ValidateRedirectUri(AuthorizeRequest authRequest, AuthorizeRequestV // Redirect URI validation. if (!client.RedirectUris.Contains(testRedirectUri, StringComparer.OrdinalIgnoreCase)) { - _logger.LogError("AuthorizeRequestValidationResult - Invalid redirect_uri for client for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - Invalid redirect_uri for client for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.INVALID_REDIRECT_URI); } } @@ -169,26 +194,26 @@ private bool ValidateRedirectUri(AuthorizeRequest authRequest, AuthorizeRequestV private bool ValidateResponseType(AuthorizeRequest authRequest, AuthorizeRequestValidationResult result, ConfigurationOptions configOptions) { - result.ValidatedAuthorizationRequestObject.ResponseType = result.ValidatedAuthorizationRequestObject.ResponseType ?? authRequest.response_type; + result.ValidatedAuthorizationRequestObject.ResponseType = result.ValidatedAuthorizationRequestObject.ResponseType ?? authRequest.Response_type; _logger.LogDebug("AuthorizeRequestValidator: response_type = {ResponseType}", result.ValidatedAuthorizationRequestObject.ResponseType); if (string.IsNullOrEmpty(result.ValidatedAuthorizationRequestObject.ResponseType)) { - _logger.LogError("AuthorizeRequestValidationResult - response_type is missing for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - response_type is missing for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.RESPONSE_TYPE_MISSING); } if (configOptions.ResponseTypesSupported?.Contains(result.ValidatedAuthorizationRequestObject.ResponseType) is not true) { - _logger.LogError("AuthorizeRequestValidationResult - response_type is not supported for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - response_type is not supported for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.RESPONSE_TYPE_NOT_SUPPORTED); } - if (!string.IsNullOrEmpty(authRequest.response_type) + if (!string.IsNullOrEmpty(authRequest.Response_type) && !string.IsNullOrEmpty(result.ValidatedAuthorizationRequestObject.ResponseType) - && authRequest.response_type != result.ValidatedAuthorizationRequestObject.ResponseType) + && authRequest.Response_type != result.ValidatedAuthorizationRequestObject.ResponseType) { - _logger.LogError("AuthorizeRequestValidationResult - response_type does not match request_uri response_type for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - response_type does not match request_uri response_type for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.RESPONSE_TYPE_MISMATCH_REQUEST_URI_RESPONSE_TYPE); } @@ -197,19 +222,19 @@ private bool ValidateResponseType(AuthorizeRequest authRequest, AuthorizeRequest private bool ValidateScope(AuthorizeRequest authRequest, AuthorizeRequestValidationResult result) { - result.ValidatedAuthorizationRequestObject.Scope = result.ValidatedAuthorizationRequestObject.Scope ?? authRequest.scope; + result.ValidatedAuthorizationRequestObject.Scope = result.ValidatedAuthorizationRequestObject.Scope ?? authRequest.Scope; _logger.LogDebug("AuthorizeRequestValidator: scope = {Scope}", result.ValidatedAuthorizationRequestObject.Scope); if (string.IsNullOrEmpty(result.ValidatedAuthorizationRequestObject.Scope)) { - _logger.LogError("AuthorizeRequestValidationResult - scope is missing for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - scope is missing for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.SCOPE_MISSING); } var scopes = result.ValidatedAuthorizationRequestObject.Scope.Split(' '); if (!scopes.Contains(Scopes.OpenId)) { - _logger.LogError("AuthorizeRequestValidationResult - openid scope is missing for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - openid scope is missing for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.OPEN_ID_SCOPE_MISSING); } @@ -218,16 +243,15 @@ private bool ValidateScope(AuthorizeRequest authRequest, AuthorizeRequestValidat private bool ValidateResponseMode(AuthorizeRequest authRequest, AuthorizeRequestValidationResult result) { - var responseMode = result.ValidatedAuthorizationRequestObject.ResponseMode ?? authRequest.response_mode; + var responseMode = result.ValidatedAuthorizationRequestObject.ResponseMode ?? authRequest.Response_mode; - if (!string.IsNullOrEmpty(responseMode)) + // check if the response_modes supported for this response type contains the response_mode passed in the request. + if (!string.IsNullOrEmpty(responseMode) && + SupportedResponseModesForResponseType.TryGetValue(result.ValidatedAuthorizationRequestObject.ResponseType, out string[]? validResponseModes) && + validResponseModes?.Contains(responseMode) == false) { - var validResponseModes = Constants.SupportedResponseModesForResponseType[result.ValidatedAuthorizationRequestObject.ResponseType]; - if (!validResponseModes.Contains(responseMode)) - { - _logger.LogError("AuthorizeRequestValidationResult - response_mode is not valid for the response_type for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); - return SetErrorResult(result, ErrorCatalogue.INVALID_RESPONSE_MODE); - } + _logger.LogError("AuthorizeRequestValidationResult - response_mode is not valid for the response_type for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); + return SetErrorResult(result, ErrorCatalogue.INVALID_RESPONSE_MODE); } result.ValidatedAuthorizationRequestObject.ResponseMode = EnsureResponseMode(responseMode, result.ValidatedAuthorizationRequestObject.ResponseType); @@ -241,12 +265,13 @@ private async Task ValidateSoftwareProduct(AuthorizeRequest authRequest, A var softwareProduct = await _cdrService.GetSoftwareProduct(client.SoftwareId); if (softwareProduct == null) { - _logger.LogError("AuthorizeRequestValidationResult - Software product not found for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - Software product not found for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.SOFTWARE_PRODUCT_NOT_FOUND); } + if (!softwareProduct.IsActive()) { - _logger.LogError("AuthorizeRequestValidationResult - Software product status is not active for uri:{Request_uri} clientid:{Client_id}", authRequest.request_uri, authRequest.client_id); + _logger.LogError("AuthorizeRequestValidationResult - Software product status is not active for uri:{Request_uri} clientid:{Client_id}", authRequest.Request_uri, authRequest.Client_id); return SetErrorResult(result, ErrorCatalogue.SOFTWARE_PRODUCT_STATUS_INACTIVE, softwareProduct.GetStatusDescription()); } } @@ -256,8 +281,8 @@ private async Task ValidateSoftwareProduct(AuthorizeRequest authRequest, A private static void SetOptionalAndDefaultValues(AuthorizeRequest authRequest, AuthorizeRequestValidationResult result) { - result.ValidatedAuthorizationRequestObject.Nonce = result.ValidatedAuthorizationRequestObject.Nonce ?? authRequest.nonce; - result.ValidatedAuthorizationRequestObject.Scope = result.ValidatedAuthorizationRequestObject.Scope ?? authRequest.scope; + result.ValidatedAuthorizationRequestObject.Nonce = result.ValidatedAuthorizationRequestObject.Nonce ?? authRequest.Nonce; + result.ValidatedAuthorizationRequestObject.Scope = result.ValidatedAuthorizationRequestObject.Scope ?? authRequest.Scope; } private static bool SetErrorResult(AuthorizeRequestValidationResult result, string errorCode, string? context = null) @@ -269,7 +294,6 @@ private static bool SetErrorResult(AuthorizeRequestValidationResult result, stri return false; } - public AuthorizeRequestValidationResult ValidateCallback(AuthorizeRequestValidationResult currentResult, AuthorizeCallbackRequest authCallbackRequest) { // If there are existing errors, we don't want to add more errors. @@ -278,9 +302,9 @@ public AuthorizeRequestValidationResult ValidateCallback(AuthorizeRequestValidat return currentResult; } - if (!string.IsNullOrEmpty(authCallbackRequest.error_code)) + if (!string.IsNullOrEmpty(authCallbackRequest.Error_code)) { - return ErrorResult(currentResult, authCallbackRequest.error_code); + return ErrorResult(currentResult, authCallbackRequest.Error_code); } return currentResult; @@ -300,10 +324,9 @@ private static AuthorizeRequestValidationResult ErrorResult( private static string EnsureResponseMode(string responseMode, string responseType) { - if (string.IsNullOrEmpty(responseMode)) + if (string.IsNullOrEmpty(responseMode) && SupportedResponseModesForResponseType.TryGetValue(responseType, out string[]? responseModeForResponseType) && responseModeForResponseType != null) { - // Set default based on the response type. - return SupportedResponseModesForResponseType[responseType][0]; + return responseModeForResponseType[0]; } // Default for "jwt" is "query.jwt". diff --git a/Source/CdrAuthServer/Validation/ClientAssertionValidator.cs b/Source/CdrAuthServer/Validation/ClientAssertionValidator.cs index 670882f..226b123 100644 --- a/Source/CdrAuthServer/Validation/ClientAssertionValidator.cs +++ b/Source/CdrAuthServer/Validation/ClientAssertionValidator.cs @@ -26,21 +26,17 @@ public ClientAssertionValidator( _jwtValidator = jwtValidator; } - public async Task<(ValidationResult, string? clientId)> ValidateClientAssertionRequest( + public async Task<(ValidationResult Result, string? ClientId)> ValidateClientAssertionRequest( ClientAssertionRequest clientAssertionRequest, ConfigurationOptions configOptions, bool isTokenEndpoint) { // Basic validation. - // TODO: this is in the consumer data standards but is not valid as per the normative standards. - // FAPI conformance testing will not pass the client_id in the request, thus we will not pass FAPI testing + // this is in the consumer data standards but is not valid as per the normative standards. + // FAPI conformance testing will not pass the client_id in the request, thus we will not pass FAPI testing // if this validation is in place. - //if (string.IsNullOrEmpty(clientAssertion.client_id)) - //{ - // return (false, "invalid_client", "client_id not provided", null); - //} - + // if (string.IsNullOrEmpty(clientAssertion.client_id)) return (false, "invalid_client", "client_id not provided", null) if (string.IsNullOrEmpty(clientAssertionRequest.ClientAssertion)) { _logger.LogError("client_assertion not provided"); @@ -103,7 +99,7 @@ public ClientAssertionValidator( return (ValidationResult.Pass(), client?.ClientId); } - public async Task<(ValidationResult, Client?)> ValidateClientAssertion( + public async Task<(ValidationResult Result, Client? Client)> ValidateClientAssertion( string clientAssertion, ConfigurationOptions configOptions, IList? validAudiences = null) @@ -136,14 +132,14 @@ public ClientAssertionValidator( var client = await _clientService.Get(clientId); if (client == null) { - _logger.LogError("Client not found - {clientId}", clientId); + _logger.LogError("Client not found - {ClientId}", clientId); return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.CLIENT_NOT_FOUND), null); } var (validationResult, jwtToken) = await _jwtValidator.Validate( clientAssertion, client, - JwtValidationContext.client_assertion, + JwtValidationContext.Client_assertion, configOptions, validAudiences: validAudiences); diff --git a/Source/CdrAuthServer/Validation/ClientRegistrationValidator.cs b/Source/CdrAuthServer/Validation/ClientRegistrationValidator.cs index e9d35cb..86f98d4 100644 --- a/Source/CdrAuthServer/Validation/ClientRegistrationValidator.cs +++ b/Source/CdrAuthServer/Validation/ClientRegistrationValidator.cs @@ -63,14 +63,13 @@ public async Task Validate(ClientRegistrationRequest clientReg } // 3. Validate the sector identifier uri - var sectorIdentifierResult = await ValidateSectorIdentifierUri(clientRegistrationRequest.SoftwareStatement.SectorIdentifierUri); + var sectorIdentifierResult = await ValidateSectorIdentifierUri(clientRegistrationRequest.SoftwareStatement?.SectorIdentifierUri); if (!sectorIdentifierResult.IsValid) { _logger.LogError("Sector Identifier validation failed: {Error} {ErrorDescription}", sectorIdentifierResult.Error, sectorIdentifierResult.ErrorDescription); return sectorIdentifierResult; } - // // Signature validation has been completed successfully. // @@ -80,10 +79,10 @@ public async Task Validate(ClientRegistrationRequest clientReg CheckMandatory(clientRegistrationRequest.Exp, nameof(clientRegistrationRequest.Exp)); CheckMandatory(clientRegistrationRequest.Jti, nameof(clientRegistrationRequest.Jti)); MustEqual(clientRegistrationRequest.Aud, nameof(clientRegistrationRequest.Aud), configOptions.Issuer); - MustEqual(clientRegistrationRequest.Iss, nameof(clientRegistrationRequest.Iss), clientRegistrationRequest.SoftwareStatement.SoftwareId!); - MustBeOne(clientRegistrationRequest.TokenEndpointAuthSigningAlg, nameof(clientRegistrationRequest.TokenEndpointAuthSigningAlg), configOptions.TokenEndpointAuthSigningAlgValuesSupported!); - MustBeOne(clientRegistrationRequest.TokenEndpointAuthMethod, nameof(clientRegistrationRequest.TokenEndpointAuthMethod), configOptions.TokenEndpointAuthMethodsSupported!); - MustBeOne(clientRegistrationRequest.IdTokenSignedResponseAlg, nameof(clientRegistrationRequest.IdTokenSignedResponseAlg), configOptions.IdTokenSigningAlgValuesSupported!); + MustEqual(clientRegistrationRequest.Iss, nameof(clientRegistrationRequest.Iss), clientRegistrationRequest.SoftwareStatement?.SoftwareId!); + _ = MustBeOne(clientRegistrationRequest.TokenEndpointAuthSigningAlg, nameof(clientRegistrationRequest.TokenEndpointAuthSigningAlg), configOptions.TokenEndpointAuthSigningAlgValuesSupported!); + _ = MustBeOne(clientRegistrationRequest.TokenEndpointAuthMethod, nameof(clientRegistrationRequest.TokenEndpointAuthMethod), configOptions.TokenEndpointAuthMethodsSupported!); + _ = MustBeOne(clientRegistrationRequest.IdTokenSignedResponseAlg, nameof(clientRegistrationRequest.IdTokenSignedResponseAlg), configOptions.IdTokenSigningAlgValuesSupported!); MustContain(clientRegistrationRequest.GrantTypes, nameof(clientRegistrationRequest.GrantTypes), "authorization_code"); if (!string.IsNullOrEmpty(clientRegistrationRequest.RequestObjectSigningAlg)) @@ -107,12 +106,6 @@ public async Task Validate(ClientRegistrationRequest clientReg { MustBeOne(clientRegistrationRequest.AuthorizationSignedResponseAlg, nameof(clientRegistrationRequest.AuthorizationSignedResponseAlg), configOptions.AuthorizationSigningAlgValuesSupported!); } - - if (clientRegistrationRequest.ResponseTypes.Contains(ResponseTypes.Hybrid)) - { - MustBeOne(clientRegistrationRequest.IdTokenEncryptedResponseAlg, nameof(clientRegistrationRequest.IdTokenEncryptedResponseAlg), configOptions.IdTokenEncryptionAlgValuesSupported!); - MustBeOne(clientRegistrationRequest.IdTokenEncryptedResponseEnc, nameof(clientRegistrationRequest.IdTokenEncryptedResponseEnc), configOptions.IdTokenEncryptionEncValuesSupported!); - } } if (!string.IsNullOrEmpty(clientRegistrationRequest.AuthorizationEncryptedResponseEnc)) @@ -134,13 +127,13 @@ public async Task Validate(ClientRegistrationRequest clientReg // redirect_uri validation. foreach (var redirectUri in clientRegistrationRequest.RedirectUris) { - if (!clientRegistrationRequest.SoftwareStatement.RedirectUris.Contains(redirectUri, StringComparer.OrdinalIgnoreCase)) + if (clientRegistrationRequest.SoftwareStatement != null && !clientRegistrationRequest.SoftwareStatement.RedirectUris.Contains(redirectUri, StringComparer.OrdinalIgnoreCase)) { _logger.LogError("redirect_uri: {RedirectUri} is not present in SoftwareStatement", redirectUri); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.REGISTRATION_REQUEST_INVALID_REDIRECT_URI, redirectUri); } - if (!Uri.TryCreate(redirectUri, UriKind.Absolute, out var _)) + if (!Uri.TryCreate(redirectUri, UriKind.Absolute, out _)) { _logger.LogError("malformed redirect_uri: {RedirectUri}", redirectUri); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_REDIRECT_URI); @@ -158,7 +151,7 @@ public async Task Validate(ClientRegistrationRequest clientReg private bool CheckMandatory(object? propValue, string propName) { - if (propValue == null || (propValue as string) == "") + if (propValue == null || (propValue as string) == string.Empty) { _validationResults.Add(new System.ComponentModel.DataAnnotations.ValidationResult(string.Format(Constants.ValidationErrorMessages.MissingClaim, GetDisplayName(propName)), [propName])); return false; @@ -178,7 +171,7 @@ private bool MustBeOne(object? propValue, string propName, IEnumerable e { if (string.IsNullOrEmpty(customErrorMessage)) { - _validationResults.Add(new System.ComponentModel.DataAnnotations.ValidationResult(string.Format(Constants.ValidationErrorMessages.MustBeOne, GetDisplayName(propName), String.Join(",", expectedValues)), [propName])); + _validationResults.Add(new System.ComponentModel.DataAnnotations.ValidationResult(string.Format(Constants.ValidationErrorMessages.MustBeOne, GetDisplayName(propName), string.Join(",", expectedValues)), [propName])); } else { @@ -215,7 +208,7 @@ private void MustContain(IEnumerable propValue, string propName, string private async Task ValidateRequestSignature(ClientRegistrationRequest request, ConfigurationOptions configOptions) { // Check the Data Recipient's JWKS from the Software Statement. - if (string.IsNullOrEmpty(request.SoftwareStatement.JwksUri) || !Uri.IsWellFormedUriString(request.SoftwareStatement.JwksUri, UriKind.Absolute)) + if (string.IsNullOrEmpty(request.SoftwareStatement?.JwksUri) || !Uri.IsWellFormedUriString(request.SoftwareStatement.JwksUri, UriKind.Absolute)) { _logger.LogError("Invalid jwks_uri in SSA"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_JWKS_URI); @@ -338,22 +331,21 @@ private async Task ValidateSSA(ClientRegistrationRequest clien return ValidationResult.Pass(); } - private async Task GetSsaJwks(ClientRegistrationRequest clientRegistrationRequest) + private async Task GetSsaJwks(ClientRegistrationRequest clientRegistrationRequest) { // Remove this logic once CTS has removed the use of the x-cts-ssa-publickey header for passing SSA JWKS public key. // Check if the SSA JWKS public key was passed as a http header (CTS). - var ssaJwksHeaderName = _configuration.GetValue("CdrAuthServer:RegisterSsaPublicKeyHttpHeaderName", ""); + var ssaJwksHeaderName = _configuration.GetValue("CdrAuthServer:RegisterSsaPublicKeyHttpHeaderName", string.Empty); if (!string.IsNullOrEmpty(ssaJwksHeaderName) && _httpContextAccessor != null && _httpContextAccessor.HttpContext != null && _httpContextAccessor.HttpContext.Request.Headers.TryGetValue(ssaJwksHeaderName, out StringValues publicKey) && !string.IsNullOrEmpty(publicKey)) { - _logger.LogInformation("SSA JWKS found in http header {Header}: {Value}", ssaJwksHeaderName, publicKey); var jwk = new Microsoft.IdentityModel.Tokens.JsonWebKey(publicKey); - if (jwk.Kid == clientRegistrationRequest.SoftwareStatement.Header.Kid) + if (jwk.Kid == clientRegistrationRequest.SoftwareStatement?.Header.Kid) { var jwks = new Microsoft.IdentityModel.Tokens.JsonWebKeySet(); jwks.Keys.Add(jwk); @@ -364,7 +356,7 @@ private async Task ValidateSSA(ClientRegistrationRequest clien // Retrieve the JWKS from the Register's SSA JWKS endpoint. var configOptions = _configuration.GetConfigurationOptions(); _logger.LogInformation("Retrieving SSA JWKS from Register {Uri}...", configOptions?.CdrRegister?.SsaJwksUri); - return await _jwksService.GetJwks(new Uri(configOptions?.CdrRegister?.SsaJwksUri ?? "about:blank"), clientRegistrationRequest.SoftwareStatement.Header.Kid); + return await _jwksService.GetJwks(new Uri(configOptions?.CdrRegister?.SsaJwksUri ?? "about:blank"), clientRegistrationRequest.SoftwareStatement?.Header.Kid ?? string.Empty); } private static string GetDisplayName(string propName) @@ -380,7 +372,7 @@ private static string GetDisplayName(string propName) if (displayAttr != null) { - return displayAttr.Name ?? String.Empty; + return displayAttr.Name ?? string.Empty; } return propName; diff --git a/Source/CdrAuthServer/Validation/IAuthorizeRequestValidator.cs b/Source/CdrAuthServer/Validation/IAuthorizeRequestValidator.cs index 0eeaf8f..5a52c23 100644 --- a/Source/CdrAuthServer/Validation/IAuthorizeRequestValidator.cs +++ b/Source/CdrAuthServer/Validation/IAuthorizeRequestValidator.cs @@ -6,6 +6,7 @@ namespace CdrAuthServer.Validation public interface IAuthorizeRequestValidator { Task Validate(AuthorizeRequest authRequest, ConfigurationOptions configOptions, bool checkGrantExpiredOrUsed = true); + AuthorizeRequestValidationResult ValidateCallback(AuthorizeRequestValidationResult currentResult, AuthorizeCallbackRequest authCallbackRequest); } } diff --git a/Source/CdrAuthServer/Validation/IClientAssertionValidator.cs b/Source/CdrAuthServer/Validation/IClientAssertionValidator.cs index 0963635..23e68c2 100644 --- a/Source/CdrAuthServer/Validation/IClientAssertionValidator.cs +++ b/Source/CdrAuthServer/Validation/IClientAssertionValidator.cs @@ -5,12 +5,12 @@ namespace CdrAuthServer.Validation { public interface IClientAssertionValidator { - Task<(ValidationResult, string? clientId)> ValidateClientAssertionRequest( + Task<(ValidationResult Result, string? ClientId)> ValidateClientAssertionRequest( ClientAssertionRequest clientAssertionRequest, ConfigurationOptions configOptions, bool isTokenEndpoint); - Task<(ValidationResult, Client?)> ValidateClientAssertion( + Task<(ValidationResult Result, Client? Client)> ValidateClientAssertion( string clientAssertion, ConfigurationOptions configOptions, IList? validAudiences = null); diff --git a/Source/CdrAuthServer/Validation/IClientRegistrationValidator.cs b/Source/CdrAuthServer/Validation/IClientRegistrationValidator.cs index cd6e9be..7243c94 100644 --- a/Source/CdrAuthServer/Validation/IClientRegistrationValidator.cs +++ b/Source/CdrAuthServer/Validation/IClientRegistrationValidator.cs @@ -7,4 +7,4 @@ public interface IClientRegistrationValidator { Task Validate(ClientRegistrationRequest clientRegistrationRequest, ConfigurationOptions configOptions); } -} \ No newline at end of file +} diff --git a/Source/CdrAuthServer/Validation/IJwtValidator.cs b/Source/CdrAuthServer/Validation/IJwtValidator.cs index 6dea39f..12a5e14 100644 --- a/Source/CdrAuthServer/Validation/IJwtValidator.cs +++ b/Source/CdrAuthServer/Validation/IJwtValidator.cs @@ -7,9 +7,12 @@ namespace CdrAuthServer.Validation { public enum JwtValidationContext { - client_assertion, - access_token, - request + [Display(Name = "client_assertion")] + Client_assertion, + [Display(Name = "access_token")] + Access_token, + [Display(Name = "request")] + Request, } public interface IJwtValidator @@ -17,13 +20,13 @@ public interface IJwtValidator /// /// Validates a JWT against the JWKS endpoint of the client. /// - /// JWT to validate - /// Client - /// client_assertion, request or access_token - /// ValidationResult and JwtSecurityToken - Task<(ValidationResult, JwtSecurityToken?)> Validate( - string jwt, - Client client, + /// JWT to validate. + /// Client. + /// client_assertion, request or access_token. + /// ValidationResult and JwtSecurityToken. + Task<(ValidationResult ValidationResult, JwtSecurityToken? JwtSecurityToken)> Validate( + string jwt, + Client client, JwtValidationContext context, ConfigurationOptions configOptions, IList? validAudiences = null, diff --git a/Source/CdrAuthServer/Validation/IParValidator.cs b/Source/CdrAuthServer/Validation/IParValidator.cs index 331685d..e611517 100644 --- a/Source/CdrAuthServer/Validation/IParValidator.cs +++ b/Source/CdrAuthServer/Validation/IParValidator.cs @@ -5,6 +5,6 @@ namespace CdrAuthServer.Validation { public interface IParValidator { - Task<(ValidationResult, AuthorizationRequestObject?)> Validate(string clientId, string requestObject, ConfigurationOptions configOptions); + Task<(ValidationResult ValidationResult, AuthorizationRequestObject? AuthorizationRequestObject)> Validate(string clientId, string requestObject, ConfigurationOptions configOptions); } } diff --git a/Source/CdrAuthServer/Validation/IRequestObjectValidator.cs b/Source/CdrAuthServer/Validation/IRequestObjectValidator.cs index ae368f9..7bde15d 100644 --- a/Source/CdrAuthServer/Validation/IRequestObjectValidator.cs +++ b/Source/CdrAuthServer/Validation/IRequestObjectValidator.cs @@ -6,6 +6,6 @@ namespace CdrAuthServer.Validation { public interface IRequestObjectValidator { - Task<(ValidationResult, AuthorizationRequestObject)> Validate(string clientId, JwtSecurityToken requestObject, ConfigurationOptions configOptions); + Task<(ValidationResult ValidationResult, AuthorizationRequestObject? AuthorizationRequestObject)> Validate(string clientId, JwtSecurityToken requestObject, ConfigurationOptions configOptions); } } diff --git a/Source/CdrAuthServer/Validation/JwtValidator.cs b/Source/CdrAuthServer/Validation/JwtValidator.cs index ed24e41..58de4ba 100644 --- a/Source/CdrAuthServer/Validation/JwtValidator.cs +++ b/Source/CdrAuthServer/Validation/JwtValidator.cs @@ -1,9 +1,10 @@ -using CdrAuthServer.Configuration; +using System.IdentityModel.Tokens.Jwt; +using CdrAuthServer.Configuration; using CdrAuthServer.Exceptions; +using CdrAuthServer.Extensions; using CdrAuthServer.Models; using CdrAuthServer.Services; using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Validation @@ -22,7 +23,7 @@ public JwtValidator( _clientService = clientService; } - public async Task<(ValidationResult, JwtSecurityToken?)> Validate( + public async Task<(ValidationResult ValidationResult, JwtSecurityToken? JwtSecurityToken)> Validate( string jwt, Client client, JwtValidationContext context, @@ -36,13 +37,14 @@ public JwtValidator( var signingKeys = await _clientService.GetSigningKeys(client); // Validate the jwt. - var coreValidAudiences = new List { + var coreValidAudiences = new List + { configOptions.Issuer, configOptions.TokenEndpoint, configOptions.PushedAuthorizationEndpoint, configOptions.IntrospectionEndpoint, configOptions.RevocationEndpoint, - configOptions.ArrangementRevocationEndpoint + configOptions.ArrangementRevocationEndpoint, }; if (validAudiences != null && validAudiences.Any()) @@ -67,7 +69,7 @@ public JwtValidator( ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(configOptions.ClockSkewSeconds), - ValidAlgorithms = validAlgorithms ?? new List() { Algorithms.Signing.PS256, Algorithms.Signing.ES256 } + ValidAlgorithms = validAlgorithms ?? new List() { Algorithms.Signing.PS256, Algorithms.Signing.ES256 }, }; var handler = new JwtSecurityTokenHandler(); @@ -78,22 +80,22 @@ public JwtValidator( catch (SecurityTokenInvalidAudienceException audException) { _logger.LogError(audException, "Invalid audience"); - return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.JWT_INVALID_AUDIENCE, context.ToString()), null); + return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.JWT_INVALID_AUDIENCE, context.GetDisplayName() ?? context.ToString()), null); } catch (SecurityTokenExpiredException expException) { _logger.LogError(expException, "JWT has expired"); - return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.JWT_EXPIRED, context.ToString()), null); + return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.JWT_EXPIRED, context.GetDisplayName() ?? context.ToString()), null); } catch (JwksException jwksException) { _logger.LogError(jwksException, "Invalid {Context} - jwks error", context); - return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.JWKS_ERROR, context.ToString()), null); + return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.JWKS_ERROR, context.GetDisplayName() ?? context.ToString()), null); } catch (Exception ex) { _logger.LogError(ex, "Invalid {Context} - token validation error", context); - return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.JWT_VALIDATION_ERROR, context.ToString()), null); + return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.JWT_VALIDATION_ERROR, context.GetDisplayName() ?? context.ToString()), null); } } } diff --git a/Source/CdrAuthServer/Validation/ParValidator.cs b/Source/CdrAuthServer/Validation/ParValidator.cs index d36b2c9..b642b00 100644 --- a/Source/CdrAuthServer/Validation/ParValidator.cs +++ b/Source/CdrAuthServer/Validation/ParValidator.cs @@ -1,8 +1,7 @@ -using CdrAuthServer.Configuration; -using CdrAuthServer.Extensions; +using System.IdentityModel.Tokens.Jwt; +using CdrAuthServer.Configuration; using CdrAuthServer.Models; using CdrAuthServer.Services; -using System.IdentityModel.Tokens.Jwt; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Validation @@ -26,9 +25,9 @@ public ParValidator( _clientService = clientService; } - public async Task<(ValidationResult, AuthorizationRequestObject?)> Validate( - string clientId, - string requestObject, + public async Task<(ValidationResult ValidationResult, AuthorizationRequestObject? AuthorizationRequestObject)> Validate( + string clientId, + string requestObject, ConfigurationOptions configOptions) { var tokenHandler = new JwtSecurityTokenHandler(); @@ -39,15 +38,20 @@ public ParValidator( } var client = await _clientService.Get(clientId); + if (client == null) + { + return (ValidationResult.Fail(ErrorCodes.Generic.InvalidClient, $"No client found for {clientId}"), null); + } + var (validationResult, requestJwt) = await _jwtValidator.Validate( - requestObject, - client, - JwtValidationContext.request, + requestObject, + client, + JwtValidationContext.Request, configOptions, - validAudiences: new List() { configOptions.Issuer, configOptions.PushedAuthorizationEndpoint }, + validAudiences: [configOptions.Issuer, configOptions.PushedAuthorizationEndpoint], validAlgorithms: configOptions.RequestObjectSigningAlgValuesSupported); - if (validationResult == null || !validationResult.IsValid) + if (validationResult == null || !validationResult.IsValid || requestJwt == null) { _logger.LogError("request validation failed with error {@ErrorDescription}", validationResult); return (ValidationResult.Fail(ErrorCodes.Generic.InvalidRequestObject, $"{validationResult?.ErrorDescription}"), null); @@ -57,8 +61,8 @@ public ParValidator( var (requestValidationResult, validatedRequestObject) = await _requestObjectValidator.Validate(clientId, requestJwt, configOptions); if (requestValidationResult != null && !requestValidationResult.IsValid) { - _logger.LogError("additional request validation failed with error {@requestValidationResult.ErrorDescription}", requestValidationResult); - return (ValidationResult.Fail(requestValidationResult.Error, requestValidationResult.ErrorDescription, requestValidationResult.StatusCode.HasValue ? requestValidationResult.StatusCode.Value : 400), null); + _logger.LogError("additional request validation failed with error {ErrorDescription}", requestValidationResult.ErrorDescription); + return (ValidationResult.Fail(requestValidationResult.Error ?? string.Empty, requestValidationResult.ErrorDescription, requestValidationResult.StatusCode.HasValue ? requestValidationResult.StatusCode.Value : 400), null); } return (ValidationResult.Pass(), validatedRequestObject); diff --git a/Source/CdrAuthServer/Validation/RequestObjectValidator.cs b/Source/CdrAuthServer/Validation/RequestObjectValidator.cs index ee23114..0a1d2b8 100644 --- a/Source/CdrAuthServer/Validation/RequestObjectValidator.cs +++ b/Source/CdrAuthServer/Validation/RequestObjectValidator.cs @@ -29,7 +29,7 @@ public RequestObjectValidator( _validatedAuthorizationRequestObject = new AuthorizationRequestObject(); } - public async Task<(ValidationResult, AuthorizationRequestObject?)> Validate(string clientId, JwtSecurityToken requestObject, ConfigurationOptions configOptions) + public async Task<(ValidationResult ValidationResult, AuthorizationRequestObject? AuthorizationRequestObject)> Validate(string clientId, JwtSecurityToken requestObject, ConfigurationOptions configOptions) { // Extract the client_id from the request object. if (!requestObject.Payload.TryGetValue(ClaimNames.ClientId, out var payloadClientId)) @@ -55,7 +55,7 @@ public RequestObjectValidator( if (!Uri.TryCreate(redirectUri, UriKind.Absolute, out var _)) { - _logger.LogError("malformed redirect_uri: {redirectUri}", redirectUri); + _logger.LogError("malformed redirect_uri: {RedirectUri}", redirectUri); return (ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_REDIRECT_URI), null); } @@ -129,8 +129,8 @@ public RequestObjectValidator( private ValidationResult ValidateLifetime(JwtSecurityToken requestObject) { - var exp = requestObject.Payload.Exp.FromEpoch(); - var nbf = requestObject.Payload.Nbf.FromEpoch(); + var exp = requestObject.Payload.Expiration.FromEpoch(); + var nbf = requestObject.Payload.NotBefore.FromEpoch(); // exp claim is required. if (exp == null) @@ -141,15 +141,15 @@ private ValidationResult ValidateLifetime(JwtSecurityToken requestObject) // nbf claim is required. if (nbf == null) - { + { _logger.LogError("Invalid request - nbf is missing"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.NBF_MISSING); } // nbf cannot be after expiry - // expiry cannot be longer than 60 min after not before + // expiry cannot be longer than 60 min after not before if (nbf.Value > exp.Value || (exp.Value.Subtract(nbf.Value) > TimeSpan.FromMinutes(60))) - { + { _logger.LogError("Invalid request - exp claim cannot be longer than 60 minutes after the nbf claim"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.EXPIRY_GREATER_THAN_60_AFTER_NBF); } @@ -170,7 +170,7 @@ private ValidationResult ValidateCoreParameters(JwtSecurityToken requestObject, ////////////////////////////////////////////////////////// var responseType = requestObject.GetClaimValue(ClaimNames.ResponseType); if (string.IsNullOrEmpty(responseType)) - { + { _logger.LogError("response_type missing from request object JWT"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.RESPONSE_TYPE_MISSING); } @@ -189,13 +189,13 @@ private ValidationResult ValidateCoreParameters(JwtSecurityToken requestObject, .FirstOrDefault(x => comparer.Equals(x, responseType.ToString())); if (string.IsNullOrEmpty(supportedResponseType)) - { + { _logger.LogError("response_type is not supported"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.RESPONSE_TYPE_NOT_SUPPORTED); } _validatedAuthorizationRequestObject.ResponseType = supportedResponseType; - _validatedAuthorizationRequestObject.GrantType = (supportedResponseType == ResponseTypes.AuthCode) ? GrantTypes.AuthCode : GrantTypes.Hybrid; + _validatedAuthorizationRequestObject.GrantType = GrantTypes.AuthCode; ///////////////////////////////////////////////////////////////////////////// // validate code_challenge and code_challenge_method @@ -211,17 +211,19 @@ private ValidationResult ValidateCoreParameters(JwtSecurityToken requestObject, ////////////////////////////////////////////////////////// // check if response_mode parameter is present and valid - var responseMode = requestObject.GetClaimValue(ClaimNames.ResponseMode) ?? GetDefaultResponseMode(_validatedAuthorizationRequestObject.ResponseType, configOptions); - if (responseMode.HasValue()) + var responseMode = requestObject.GetClaimValue(ClaimNames.ResponseMode); + if (responseMode != null && responseMode.HasValue()) { if (configOptions.ResponseModesSupported?.Contains(responseMode) is not true) - { + { _logger.LogError("Invalid response_mode"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_RESPONSE_MODE); } - if (!SupportedResponseModesForResponseType[_validatedAuthorizationRequestObject.ResponseType].Contains(responseMode)) - { + if (SupportedResponseModesForResponseType.TryGetValue(_validatedAuthorizationRequestObject.ResponseType, out var validResponseType) && + validResponseType?.Length > 0 && + !validResponseType.Contains(responseMode)) + { _logger.LogError("Invalid response_mode for response_type"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_RESPONSE_MODE_FOR_RESPONSE_TYPE); } @@ -242,8 +244,7 @@ private ValidationResult ValidateCoreParameters(JwtSecurityToken requestObject, // https://openid.net/specs/openid-financial-api-part-2-1_0.html#advanced-security-provisions // // 2. shall require - // 1. the response_type value code id_token, or - // 2. the response_type value code in conjunction with the response_mode value jwt; + // a. the response_type value code in conjunction with the response_mode value jwt //////////////////////////////////////////////////////////////////////////////////////////////// if (_validatedAuthorizationRequestObject.ResponseType == ResponseTypes.AuthCode) { @@ -256,28 +257,33 @@ private ValidationResult ValidateCoreParameters(JwtSecurityToken requestObject, return ValidationResult.Pass(); } - private string GetDefaultResponseMode(string responseType, ConfigurationOptions configOptions) + private static string GetDefaultResponseMode(string responseType, ConfigurationOptions configOptions) { if (string.IsNullOrEmpty(responseType)) { - return configOptions.ResponseModesSupported.First(); + return configOptions.ResponseModesSupported == null ? string.Empty : configOptions.ResponseModesSupported[0]; + } + + if (SupportedResponseModesForResponseType.TryGetValue(responseType, out string[]? responseModeForResponseType) && responseModeForResponseType != null) + { + return responseModeForResponseType[0]; } - return SupportedResponseModesForResponseType[responseType].First(); + return configOptions.ResponseModesSupported?.Count > 0 ? configOptions.ResponseModesSupported[0] : string.Empty; } private ValidationResult ValidatePkceParameters(JwtSecurityToken requestObject, ConfigurationOptions configOptions) { var codeChallenge = requestObject.GetClaimValue(ClaimNames.CodeChallenge); if (string.IsNullOrEmpty(codeChallenge)) - { + { _logger.LogError("code_challenge is missing"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.CODE_CHALLENGE_MISSING); } if (codeChallenge.Length < ValidationRestrictions.InputLengthRestrictions.CodeChallengeMinLength || codeChallenge.Length > ValidationRestrictions.InputLengthRestrictions.CodeChallengeMaxLength) - { + { _logger.LogError("Invalid code_challenge - invalid length"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.CODE_CHALLENGE_INVALID_LENGTH); } @@ -290,7 +296,7 @@ private ValidationResult ValidatePkceParameters(JwtSecurityToken requestObject, } if (configOptions.CodeChallengeMethodsSupported?.Contains(codeChallengeMethod) is not true) - { + { _logger.LogError("Unsupported code_challenge_method"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.UNSUPPORTED_CHALLENGE_METHOD); } @@ -308,21 +314,21 @@ private ValidationResult ValidateScope(JwtSecurityToken requestObject) ////////////////////////////////////////////////////////// var scope = requestObject.GetClaimValue(ClaimNames.Scope); if (scope.IsNullOrEmpty()) - { + { _logger.LogError("Invalid scope - scope is missing"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.SCOPE_MISSING); } - if (scope.Length > ValidationRestrictions.InputLengthRestrictions.ScopeMaxLength) - { + if (scope?.Length > ValidationRestrictions.InputLengthRestrictions.ScopeMaxLength) + { _logger.LogError("Invalid scope - scope is too long"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.SCOPE_TOO_LONG); } - _validatedAuthorizationRequestObject.Scope = scope; + _validatedAuthorizationRequestObject.Scope = scope ?? string.Empty; if (!_validatedAuthorizationRequestObject.Scopes.Contains(Scopes.OpenId)) - { + { _logger.LogError("Missing openid scope"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.OPEN_ID_SCOPE_MISSING); } @@ -336,11 +342,11 @@ private async Task ValidateCdrArrangementId(string clientId, J try { - _validatedAuthorizationRequestObject.Claims = JsonConvert.DeserializeObject(claims) ?? new AuthorizeClaims(); + _validatedAuthorizationRequestObject.Claims = JsonConvert.DeserializeObject(claims ?? string.Empty) ?? new AuthorizeClaims(); } catch (Exception ex) - { - _logger.LogError(ex, "Invalid claims in request object - {message}", ex.Message); + { + _logger.LogError(ex, "Invalid claims in request object - {Message}", ex.Message); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_CLAIMS); } @@ -366,75 +372,12 @@ private ValidationResult ValidateOptionalParameters(JwtSecurityToken requestObje ////////////////////////////////////////////////////////// var nonce = requestObject.GetClaimValue(ClaimNames.Nonce); if (nonce.IsNullOrEmpty()) - { + { _logger.LogError("Invalid nonce"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_NONCE); } - _validatedAuthorizationRequestObject.Nonce = nonce; - - ////////////////////////////////////////////////////////// - // check prompt - ////////////////////////////////////////////////////////// - //var prompt = request.Raw.Get(OidcConstants.AuthorizeRequest.Prompt); - //if (prompt.IsPresent()) - //{ - // if (SupportedPromptModes.Contains(prompt)) - // { - // request.PromptModes = new List(new string[] { prompt }); - // } - // else - // { - // _logger.LogDebug("Unsupported prompt mode - ignored: {prompt}", prompt); - // } - //} - - ////////////////////////////////////////////////////////// - // check ui locales - ////////////////////////////////////////////////////////// - //var uilocalesParameter = GetParameter(request, OidcConstants.AuthorizeRequest.UiLocales, _options.InputLengthRestrictions.UiLocale); - //if (uilocalesParameter.Error != null) - //{ - // return uilocalesParameter.Error; - //} - //request.UiLocales = uilocalesParameter.Value; - - - ////////////////////////////////////////////////////////// - // check max_age - ////////////////////////////////////////////////////////// - //var maxAge = request.Raw.Get(OidcConstants.AuthorizeRequest.MaxAge); - //if (maxAge.IsPresent()) - //{ - // if (!int.TryParse(maxAge, out var seconds) || seconds < 0) - // { - // LogError("Invalid max_age.", request); - // return Invalid(request, description: "Invalid max_age"); - // } - - // request.MaxAge = seconds; - //} - - ////////////////////////////////////////////////////////// - // check login_hint - ////////////////////////////////////////////////////////// - //var loginHintParameter = GetParameter(request, OidcConstants.AuthorizeRequest.LoginHint, _options.InputLengthRestrictions.LoginHint); - //if (loginHintParameter.Error != null) - //{ - // return loginHintParameter.Error; - //} - //request.LoginHint = loginHintParameter.Value; - - ////////////////////////////////////////////////////////// - // check acr_values - ////////////////////////////////////////////////////////// - //var acrValuesParameter = GetParameter(request, OidcConstants.AuthorizeRequest.AcrValues, _options.InputLengthRestrictions.AcrValues); - //if (acrValuesParameter.Error != null) - //{ - // return acrValuesParameter.Error; - //} - //request.AuthenticationContextReferenceClasses = acrValuesParameter.Value.FromSpaceSeparatedString().Distinct().ToList(); - + _validatedAuthorizationRequestObject.Nonce = nonce ?? string.Empty; return ValidationResult.Pass(); } } diff --git a/Source/CdrAuthServer/Validation/TokenRequestValidator.cs b/Source/CdrAuthServer/Validation/TokenRequestValidator.cs index aa5a62e..1bc9f5e 100644 --- a/Source/CdrAuthServer/Validation/TokenRequestValidator.cs +++ b/Source/CdrAuthServer/Validation/TokenRequestValidator.cs @@ -41,18 +41,18 @@ public async Task Validate(string? clientId, TokenRequest toke return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_TOKEN_REQUEST); } - if (string.IsNullOrEmpty(tokenRequest.grant_type)) + if (string.IsNullOrEmpty(tokenRequest.Grant_type)) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.GRANT_TYPE_MISSING); } - if (configOptions.GrantTypesSupported?.Contains(tokenRequest.grant_type) is not true) + if (configOptions.GrantTypesSupported?.Contains(tokenRequest.Grant_type) is not true) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.UNSUPPORTED_GRANT_TYPE); } // If the client_id was passed, then it should match the client making the request. - if (!string.IsNullOrEmpty(tokenRequest.client_id) && !clientId.Equals(tokenRequest.client_id, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(tokenRequest.Client_id) && !clientId.Equals(tokenRequest.Client_id, StringComparison.OrdinalIgnoreCase)) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.CLIENT_ID_MISMATCH); } @@ -65,13 +65,14 @@ public async Task Validate(string? clientId, TokenRequest toke } // Check software product status (if configured). - if (configOptions.CdrRegister!= null && configOptions.CdrRegister.CheckSoftwareProductStatus) + if (configOptions.CdrRegister != null && configOptions.CdrRegister.CheckSoftwareProductStatus) { var softwareProduct = await _cdrService.GetSoftwareProduct(client.SoftwareId); if (softwareProduct == null) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.SOFTWARE_PRODUCT_NOT_FOUND); } + if (!softwareProduct.IsActive()) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.SOFTWARE_PRODUCT_STATUS_INACTIVE, softwareProduct.GetStatusDescription()); @@ -79,32 +80,31 @@ public async Task Validate(string? clientId, TokenRequest toke } // Auth code request validation. - if (tokenRequest.grant_type == GrantTypes.AuthCode) + if (tokenRequest.Grant_type == GrantTypes.AuthCode) { - if (string.IsNullOrEmpty(tokenRequest.code)) + if (string.IsNullOrEmpty(tokenRequest.Code)) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.CODE_IS_MISSING); - } // Redirect URI validation. - if (string.IsNullOrEmpty(tokenRequest.redirect_uri)) + if (string.IsNullOrEmpty(tokenRequest.Redirect_uri)) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.REDIRECT_URI_IS_MISSING); } // Code verifier validation. - if (string.IsNullOrEmpty(tokenRequest.code_verifier)) + if (string.IsNullOrEmpty(tokenRequest.Code_verifier)) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.CODE_VERIFIER_IS_MISSING); } // Find the matching auth code grant. - var authCodeGrant = await _grantService.Get(GrantTypes.AuthCode, tokenRequest.code, clientId) as AuthorizationCodeGrant; + var authCodeGrant = await _grantService.Get(GrantTypes.AuthCode, tokenRequest.Code, clientId) as AuthorizationCodeGrant; if (authCodeGrant == null) { // Blacklist the auth code as it may have been a re-use attempt. - await _tokenService.AddToBlacklist($"{clientId}::{tokenRequest.code}"); + await _tokenService.AddToBlacklist($"{clientId}::{tokenRequest.Code}"); return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_AUTHORIZATION_CODE); } @@ -114,17 +114,21 @@ public async Task Validate(string? clientId, TokenRequest toke } // Get the original auth request object. - var authRequestObject = JsonConvert.DeserializeObject(authCodeGrant.Request); + AuthorizationRequestObject? authRequestObject = null; + if (authCodeGrant.Request != null) + { + authRequestObject = JsonConvert.DeserializeObject(authCodeGrant.Request); + } // Verify the redirect_uri. - if (authRequestObject?.RedirectUri != tokenRequest.redirect_uri) + if (authRequestObject?.RedirectUri != tokenRequest.Redirect_uri) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.REDIRECT_URI_AUTHORIZATION_REQUEST_MISMATCH); } // Verify the code_verifier. var expectedCodeChallenge = authRequestObject.CodeChallenge; - var codeChallenge = CreatePkceChallenge(tokenRequest.code_verifier); + var codeChallenge = CreatePkceChallenge(tokenRequest.Code_verifier); if (expectedCodeChallenge != codeChallenge) { @@ -133,15 +137,15 @@ public async Task Validate(string? clientId, TokenRequest toke } // Refresh token request validation. - if (tokenRequest.grant_type == GrantTypes.RefreshToken) + if (tokenRequest.Grant_type == GrantTypes.RefreshToken) { - if (string.IsNullOrEmpty(tokenRequest.refresh_token)) + if (string.IsNullOrEmpty(tokenRequest.Refresh_token)) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.REFRESH_TOKEN_MISSING); } // Find the matching refresh token grant. - var refreshTokenGrant = await _grantService.Get(GrantTypes.RefreshToken, tokenRequest.refresh_token, clientId); + var refreshTokenGrant = await _grantService.Get(GrantTypes.RefreshToken, tokenRequest.Refresh_token, clientId); if (refreshTokenGrant == null) { return ErrorCatalogue.Catalogue().GetValidationResult(ErrorCatalogue.INVALID_REFRESH_TOKEN); diff --git a/Source/CdrAuthServer/Validation/ValidateClientAssertionAttribute.cs b/Source/CdrAuthServer/Validation/ValidateClientAssertionAttribute.cs index 8e7ad26..0fa3721 100644 --- a/Source/CdrAuthServer/Validation/ValidateClientAssertionAttribute.cs +++ b/Source/CdrAuthServer/Validation/ValidateClientAssertionAttribute.cs @@ -1,10 +1,8 @@ -using CdrAuthServer.Authorisation; +using System.Security.Claims; using CdrAuthServer.Extensions; -using CdrAuthServer.Infrastructure; using CdrAuthServer.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using System.Security.Claims; using static CdrAuthServer.Domain.Constants; namespace CdrAuthServer.Validation @@ -26,7 +24,7 @@ public override void OnActionExecuting(ActionExecutingContext context) var logger = context.HttpContext.RequestServices.GetRequiredService>(); // Basic validation. - if (!context.HttpContext.Request.Method.Equals("post", StringComparison.OrdinalIgnoreCase) + if (!context.HttpContext.Request.Method.Equals("post", StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(context.HttpContext.Request.ContentType) || !context.HttpContext.Request.ContentType.Contains("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { @@ -49,15 +47,15 @@ public override void OnActionExecuting(ActionExecutingContext context) var (result, clientId) = clientAssertionValidator.ValidateClientAssertionRequest(clientAssertionRequest, configOptions, _isTokenEndpoint).GetAwaiter().GetResult(); if (!result.IsValid) { - logger.LogError("ValidateClientAssertion: failed. {@error}", result.Error); - context.Result = new BadRequestObjectResult(new Error(result.Error, result.ErrorDescription)); + logger.LogError("ValidateClientAssertion: failed. {@Error}", result.Error); + context.Result = new BadRequestObjectResult(new Error(result.Error ?? string.Empty, result.ErrorDescription)); return; } // Set the claims principal. var claims = new List() { - new Claim(ClaimNames.ClientId, clientId), + new(ClaimNames.ClientId, clientId ?? string.Empty), }; context.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(claims, "client_assertion", ClaimNames.ClientId, ClaimNames.Scope)); diff --git a/Source/CdrAuthServer/Validation/ValidateMtlsAttribute.cs b/Source/CdrAuthServer/Validation/ValidateMtlsAttribute.cs index 66eba4f..c5ea04a 100644 --- a/Source/CdrAuthServer/Validation/ValidateMtlsAttribute.cs +++ b/Source/CdrAuthServer/Validation/ValidateMtlsAttribute.cs @@ -26,7 +26,6 @@ public ValidateMtlsAttribute(ILogger logger, IConfigurati _httpClient = httpClient; } - public override void OnActionExecuting(ActionExecutingContext context) { var configOptions = _configuration.GetConfigurationOptions(); @@ -79,7 +78,7 @@ private void VerifyCertificateRevocation(ActionExecutingContext context, Configu // Build the OCSP request URL from the client cert var ocspResponderUrl = cert.GetOCSPUrlFromCertificate(); - //Read the CA PEM from configuration. + // Read the CA PEM from configuration. var clientCertCAPem = _configuration.GetValue("Certificates:Ocsp:MtlsOcspResponderPem"); if (string.IsNullOrEmpty(clientCertCAPem)) @@ -88,7 +87,7 @@ private void VerifyCertificateRevocation(ActionExecutingContext context, Configu throw new ConfigurationErrorsException("Certificates:Ocsp:MtlsOcspResponderPem value is either null or empty"); } - //create request object for ocsp. + // create request object for ocsp. var ocspRequester = new OcspRequester(ocspResponderUrl, clientCertCAPem, _logger, _httpClient); _logger.LogInformation("mTLS certificate check - calling OCSP Responder at {OcspResponderUrl}", ocspResponderUrl); diff --git a/Source/CdrAuthServer/Validation/ValidationResult.cs b/Source/CdrAuthServer/Validation/ValidationResult.cs index 25d9a09..822615a 100644 --- a/Source/CdrAuthServer/Validation/ValidationResult.cs +++ b/Source/CdrAuthServer/Validation/ValidationResult.cs @@ -1,5 +1,4 @@ - -namespace CdrAuthServer.Validation +namespace CdrAuthServer.Validation { public class ValidationResult { diff --git a/Source/CdrAuthServer/appsettings.Development.json b/Source/CdrAuthServer/appsettings.Development.json index 53b1636..19e266c 100644 --- a/Source/CdrAuthServer/appsettings.Development.json +++ b/Source/CdrAuthServer/appsettings.Development.json @@ -1,226 +1,169 @@ { - "DetailedErrors": true, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "ConnectionStrings": { - "CDR_Auth_Server_RW": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-auth-server;Integrated Security=true;TrustServerCertificate=True;Encrypt=False", - "CDR_Auth_Server_Migrations_DBO": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-auth-server;Integrated Security=true;TrustServerCertificate=True;Encrypt=False", - "CDR_Auth_Server_Logging_DBO": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-auth-server;Integrated Security=true;TrustServerCertificate=True;Encrypt=False", - "CDR_Auth_Server_RequestResponse_Logging_DBO": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-auth-server;Integrated Security=true;TrustServerCertificate=True;Encrypt=False" - }, - "Serilog": { - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.MSSqlServer" ], - "MinimumLevel": { - "Default": "Information" - }, - "WriteTo": [ - { - "Name": "Console", - "Args": { - "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {Level:u3}] {Username} {Message:lj}{NewLine}{Exception}" - } - }, - { - "Name": "MSSqlServer", - "Args": { - "connectionString": "CDR_Auth_Server_Logging_DBO", - "sinkOptionsSection": { - "tableName": "LogEvents-AuthServer", - "autoCreateSqlTable": true - }, - "restrictedToMinimumLevel": "Verbose", - "batchPostingLimit": 1000, - "period": "0.00:00:10", - "columnOptionsSection": { - "disableTriggers": true, - "clusteredColumnstoreIndex": false, - "primaryKeyColumnName": "Id", - "removeStandardColumns": [ "MessageTemplate", "Properties" ], - "additionalColumns": [ - { - "ColumnName": "Environment", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ThreadId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "MethodName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "SourceContext", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 100 - } - ] - } + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" } - } - ] - }, - "SerilogRequestResponseLogger": { - "Using": [ "Serilog.Sinks.MSSqlServer" ], - "MinimumLevel": "Debug", - "IPAddressHeaderKey": "X-Forwarded-For", - "HostNameHeaderKey": "X-Forwarded-Host", - "WriteTo": [ - { - "Name": "MSSqlServer", - "Args": { - "connectionString": "CDR_Auth_Server_RequestResponse_Logging_DBO", - "sinkOptionsSection": { - "tableName": "LogEvents-RequestResponse", - "autoCreateSqlTable": true - }, - "restrictedToMinimumLevel": "Information", - "batchPostingLimit": 1000, - "period": "0.00:00:10", - "columnOptionsSection": { - "disableTriggers": true, - "clusteredColumnstoreIndex": false, - "primaryKeyColumnName": "Id", - "removeStandardColumns": [ "MessageTemplate", "Properties" ], - "additionalColumns": [ - { - "ColumnName": "SourceContext", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 100 - }, - { - "ColumnName": "ClientId", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "SoftwareId", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "FapiInteractionId", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "RequestMethod", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 20 - }, - { - "ColumnName": "RequestBody", - "DataType": "varchar", - "AllowNull": true, - "DataLength": -1 - }, - { - "ColumnName": "RequestHeaders", - "DataType": "varchar", - "AllowNull": true, - "DataLength": -1 - }, - { - "ColumnName": "RequestPath", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 2000 - }, - { - "ColumnName": "RequestQueryString", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 4000 - }, - { - "ColumnName": "StatusCode", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 20 - }, - { - "ColumnName": "ElapsedTime", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 20 - }, - { - "ColumnName": "RequestHost", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 4000 - }, - { - "ColumnName": "RequestIpAddress", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ResponseHeaders", - "DataType": "varchar", - "AllowNull": true, - "DataLength": 4000 - }, - { - "ColumnName": "ResponseBody", - "DataType": "varchar", - "AllowNull": true, - "DataLength": -1 - } + }, + "ConnectionStrings": { + "CDR_Auth_Server_RW": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-auth-server;Integrated Security=true;TrustServerCertificate=True;Encrypt=False", + "CDR_Auth_Server_Migrations_DBO": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-auth-server;Integrated Security=true;TrustServerCertificate=True;Encrypt=False", + "CDR_Auth_Server_Logging_DBO": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-auth-server;Integrated Security=true;TrustServerCertificate=True;Encrypt=False", + "CDR_Auth_Server_RequestResponse_Logging_DBO": "Server=(localdb)\\MSSQLLocalDB;Database=cdr-auth-server;Integrated Security=true;TrustServerCertificate=True;Encrypt=False" + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Information" + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {Level:u3}] {Username} {Message:lj}{NewLine}{Exception}" + } + } + ] + }, + "SerilogRequestResponseLogger": { + "Using": [ "Serilog.Sinks.MSSqlServer" ], + "MinimumLevel": "Information", + "IPAddressHeaderKey": "X-Forwarded-For", + "HostNameHeaderKey": "X-Forwarded-Host", + "WriteTo": [ + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "CDR_Auth_Server_RequestResponse_Logging_DBO", + "sinkOptionsSection": { + "tableName": "LogEvents-RequestResponse", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Information", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "SourceContext", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 100 + }, + { + "ColumnName": "ClientId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "SoftwareId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "FapiInteractionId", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "RequestMethod", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + }, + { + "ColumnName": "RequestPath", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 2000 + }, + { + "ColumnName": "RequestQueryString", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "StatusCode", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "ElapsedTime", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 20 + }, + { + "ColumnName": "RequestHost", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "RequestIpAddress", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ResponseHeaders", + "DataType": "varchar", + "AllowNull": true, + "DataLength": 4000 + }, + { + "ColumnName": "ResponseBody", + "DataType": "varchar", + "AllowNull": true, + "DataLength": -1 + } - ] - } - } - } - ] - }, - "CdrAuthServer": { - "HttpsPort": 8001, - "HttpPort": 8080, - "BaseUri": "https://localhost:8081", - "SecureBaseUri": "https://localhost:8082", - "BasePath": "", - "AccessTokenExpirySeconds": 3600, - "CdrRegister": { - "CheckSoftwareProductStatus": true, - "SsaJwksUri": "https://localhost:7000/cdr-register/v1/jwks", - "GetDataRecipientsEndpoint": "https://localhost:7000/cdr-register/v1/all/data-recipients", - "Version": 3 + ] + } + } + } + ] }, - "SeedData": { - "FilePath": "Data\\customer-seed-data.json" + "CdrAuthServer": { + "HttpsPort": 8001, + "HttpPort": 8080, + "BaseUri": "https://localhost:8081", + "SecureBaseUri": "https://localhost:8082", + "BasePath": "", + "AccessTokenExpirySeconds": 3600, + "CdrRegister": { + "CheckSoftwareProductStatus": true, + "SsaJwksUri": "https://localhost:7000/cdr-register/v1/jwks", + "GetDataRecipientsEndpoint": "https://localhost:7000/cdr-register/v1/all/data-recipients", + "Version": 3 + }, + "SeedData": { + "FilePath": "Data\\customer-seed-data.json" + }, + "AuthUiBaseUri": "http://localhost:3000" }, - "AuthUiBaseUri": "http://localhost:3000" - }, - // Used when testing the resource api endpoint embedded in the auth server. - "AccessTokenIntrospectionEndpoint": "https://localhost:8081/connect/introspect-internal" + // Used when testing the resource api endpoint embedded in the auth server. + "AccessTokenIntrospectionEndpoint": "https://localhost:8081/connect/introspect-internal" } diff --git a/Source/CdrAuthServer/appsettings.Release.Standalone.json b/Source/CdrAuthServer/appsettings.Release.Standalone.json index e504989..7985fa3 100644 --- a/Source/CdrAuthServer/appsettings.Release.Standalone.json +++ b/Source/CdrAuthServer/appsettings.Release.Standalone.json @@ -15,7 +15,7 @@ "Serilog": { "Using": [ "Serilog.Sinks.Console", - "Serilog.Sinks.MSSqlServer" + "Serilog.Sinks.File" ], "MinimumLevel": { "Default": "Information" @@ -33,63 +33,6 @@ "path": "/tmp/CdrAuthServer.log", "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" } - }, - { - "Name": "MSSqlServer", - "Args": { - "connectionString": "CDR_Auth_Server_Logging_DBO", - "sinkOptionsSection": { - "tableName": "LogEvents-AuthServer", - "autoCreateSqlTable": true - }, - "restrictedToMinimumLevel": "Verbose", - "batchPostingLimit": 1000, - "period": "0.00:00:10", - "columnOptionsSection": { - "disableTriggers": true, - "clusteredColumnstoreIndex": false, - "primaryKeyColumnName": "Id", - "removeStandardColumns": [ "MessageTemplate", "Properties" ], - "additionalColumns": [ - { - "ColumnName": "Environment", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ThreadId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "MethodName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "SourceContext", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 100 - } - ] - } - } } ] }, diff --git a/Source/CdrAuthServer/appsettings.Release.json b/Source/CdrAuthServer/appsettings.Release.json index f52f151..4ad48df 100644 --- a/Source/CdrAuthServer/appsettings.Release.json +++ b/Source/CdrAuthServer/appsettings.Release.json @@ -15,7 +15,7 @@ "Serilog": { "Using": [ "Serilog.Sinks.Console", - "Serilog.Sinks.MSSqlServer" + "Serilog.Sinks.File" ], "MinimumLevel": { "Default": "Information" @@ -33,63 +33,6 @@ "path": "/tmp/CdrAuthServer.log", "outputTemplate": "{Timestamp:dd/MM/yyyy HH:mm:ss.fff zzz} {Level} [{SourceContext}] {Message}{NewLine}{Exception}" } - }, - { - "Name": "MSSqlServer", - "Args": { - "connectionString": "CDR_Auth_Server_Logging_DBO", - "sinkOptionsSection": { - "tableName": "LogEvents-AuthServer", - "autoCreateSqlTable": true - }, - "restrictedToMinimumLevel": "Verbose", - "batchPostingLimit": 1000, - "period": "0.00:00:10", - "columnOptionsSection": { - "disableTriggers": true, - "clusteredColumnstoreIndex": false, - "primaryKeyColumnName": "Id", - "removeStandardColumns": [ "MessageTemplate", "Properties" ], - "additionalColumns": [ - { - "ColumnName": "Environment", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ProcessName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "ThreadId", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "MethodName", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 50 - }, - { - "ColumnName": "SourceContext", - "DataType": "nvarchar", - "AllowNull": true, - "DataLength": 100 - } - ] - } - } } ] }, diff --git a/Source/CdrAuthServer/appsettings.json b/Source/CdrAuthServer/appsettings.json index a6a6d9e..70308d1 100644 --- a/Source/CdrAuthServer/appsettings.json +++ b/Source/CdrAuthServer/appsettings.json @@ -1,90 +1,153 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "EnableSwagger": "true", - "CdrAuthServer": { - "IdPermanence": { - "PrivateKey": "90733A75F19347118B3BE0030AB590A8" - }, - "SeedData": { - "FilePath": "Data/customer-seed-data.json" - }, - "HeadlessMode": true, - "AllowDuplicateRegistrations": false, - "SupportJarmEncryption": false, - "UseMtlsEndpointAliases": false, - "ClockSkewSeconds": 0, - "ScopesProfile": "all", // "banking", "energy", "all" - "ScopesSupported": [ - "profile", - "openid", - "common:customer.basic:read", - "common:customer.detail:read" - ], - "BankingScopesSupported": [ - "bank:accounts.basic:read", - "bank:accounts.detail:read", - "bank:transactions:read", - "bank:payees:read", - "bank:regular_payments:read" - ], - "EnergyScopesSupported": [ - "energy:electricity.servicepoints.basic:read", - "energy:electricity.servicepoints.detail:read", - "energy:electricity.usage:read", - "energy:electricity.der:read", - "energy:accounts.basic:read", - "energy:accounts.detail:read", - "energy:accounts.paymentschedule:read", - "energy:accounts.concessions:read", - "energy:billing:read" - ], - "ClientCredentialScopesSupported": [ - "admin:metrics.basic:read", - "admin:metadata:update", - "cdr:registration" - ], - "ResponseModesSupported": [ - "fragment", - "form_post", - "jwt" - ], - "PS256SigningCertificate": { - "Source": "File", - "Location": "Certificates/ps256-private.pfx", - "Password": "#M0ckDataHolder#" + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } }, - "ES256SigningCertificate": { - "Source": "File", - "Location": "Certificates/es256-private.pfx", - "Password": "#M0ckDataHolder#" + "AllowedHosts": "*", + "EnableSwagger": "true", + "CdrAuthServer": { + "IdPermanence": { + "PrivateKey": "90733A75F19347118B3BE0030AB590A8" + }, + "SeedData": { + "FilePath": "Data/customer-seed-data.json" + }, + "HeadlessMode": true, + "AllowDuplicateRegistrations": false, + "SupportJarmEncryption": false, + "UseMtlsEndpointAliases": false, + "ClockSkewSeconds": 0, + "ScopesProfile": "all", // "banking", "energy", "all" + "ScopesSupported": [ + "profile", + "openid", + "common:customer.basic:read", + "common:customer.detail:read" + ], + "BankingScopesSupported": [ + "bank:accounts.basic:read", + "bank:accounts.detail:read", + "bank:transactions:read", + "bank:payees:read", + "bank:regular_payments:read" + ], + "EnergyScopesSupported": [ + "energy:electricity.servicepoints.basic:read", + "energy:electricity.servicepoints.detail:read", + "energy:electricity.usage:read", + "energy:electricity.der:read", + "energy:accounts.basic:read", + "energy:accounts.detail:read", + "energy:accounts.paymentschedule:read", + "energy:accounts.concessions:read", + "energy:billing:read" + ], + "ClientCredentialScopesSupported": [ + "admin:metrics.basic:read", + "admin:metadata:update", + "cdr:registration" + ], + "ResponseModesSupported": [ + "jwt" + ], + "PS256SigningCertificate": { + "Source": "File", + "Location": "Certificates/ps256-private.pfx", + "Password": "#M0ckDataHolder#" + }, + "ES256SigningCertificate": { + "Source": "File", + "Location": "Certificates/es256-private.pfx", + "Password": "#M0ckDataHolder#" + }, + "RequestUriExpirySeconds": 90, + "ClientCertificateThumbprintHttpHeaderName": "X-TlsClientCertThumbprint", + "ClientCertificateCommonNameHttpHeaderName": "X-TlsClientCertCN", + "AutoFillCustomerId": "ksmith", + "AutoFillOtp": "000789", + "BrandName": "Mock Data Holder Banking", + "BrandAbn": "48 XXX XXX", + "CdrRegister": { + "SsaJwksUri": "https://mock-register:7000/cdr-register/v1/jwks" + }, + "EnableServerCertificateValidation": false }, - "RequestUriExpirySeconds": 90, - "ClientCertificateThumbprintHttpHeaderName": "X-TlsClientCertThumbprint", - "ClientCertificateCommonNameHttpHeaderName": "X-TlsClientCertCN", - "AutoFillCustomerId": "ksmith", - "AutoFillOtp": "000789", - "BrandName": "Mock Data Holder Banking", - "BrandAbn": "48 XXX XXX", - "CdrRegister": { - "SsaJwksUri": "https://mock-register:7000/cdr-register/v1/jwks" + "Certificates": { + "TlsInternalCertificate": { + "Source": "File", + "Location": "Certificates/tls-server.pfx", + "Password": "#M0ckDataHolder#" + }, + "Ocsp": { + "Enabled": false, + "MtlsOcspResponderPem": "" + } }, - "EnableServerCertificateValidation": false - }, - "Certificates": { - "TlsInternalCertificate": { - "Source": "File", - "Location": "Certificates/tls-server.pfx", - "Password": "#M0ckDataHolder#" + "SerilogMSSqlServerWriteTo": { + "Using": [ "Serilog.Sinks.MSSqlServer" ], + "WriteTo": [ + { + "Name": "MSSqlServer", + "Args": { + "connectionString": "CDR_Auth_Server_Logging_DBO", + "sinkOptionsSection": { + "tableName": "LogEvents-AuthServer", + "autoCreateSqlTable": true + }, + "restrictedToMinimumLevel": "Verbose", + "batchPostingLimit": 1000, + "period": "0.00:00:10", + "columnOptionsSection": { + "disableTriggers": true, + "clusteredColumnstoreIndex": false, + "primaryKeyColumnName": "Id", + "removeStandardColumns": [ "MessageTemplate", "Properties" ], + "additionalColumns": [ + { + "ColumnName": "Environment", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ProcessName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "ThreadId", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "MethodName", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 50 + }, + { + "ColumnName": "SourceContext", + "DataType": "nvarchar", + "AllowNull": true, + "DataLength": 100 + } + ] + } + } + } + ] }, - "Ocsp": { - "Enabled": false, - "MtlsOcspResponderPem": "" + "SerilogRequestResponseLogger": { + "IsDisabled": false } - } } diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index db8b161..c75d430 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -1,7 +1,10 @@ net8.0 - 2.0.0 + 3.0.0 + true + true + true $(NoWarn);1591 diff --git a/Source/Dockerfile b/Source/Dockerfile index 801dac8..6030b96 100644 --- a/Source/Dockerfile +++ b/Source/Dockerfile @@ -20,6 +20,7 @@ COPY ./ . FROM build AS publish COPY ./Directory.Build.props /app/Directory.Build.props +COPY ./.editorconfig /app/.editorconfig COPY ./CdrAuthServer.Domain/. /app/CdrAuthServer.Domain COPY ./CdrAuthServer.Repository/. /app/CdrAuthServer.Repository COPY ./CdrAuthServer/. /app/CdrAuthServer diff --git a/Source/Dockerfile.get-data-recipients b/Source/Dockerfile.get-data-recipients index 66ae1da..846e051 100644 --- a/Source/Dockerfile.get-data-recipients +++ b/Source/Dockerfile.get-data-recipients @@ -11,6 +11,8 @@ RUN echo $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS # Copy Source and build for integration tests COPY . /src +COPY ./Directory.Build.props /src/Directory.Build.props +COPY ./.editorconfig /src/.editorconfig WORKDIR /src/CdrAuthServer.GetDataRecipients ARG BUILD_CONFIGURATION=Release RUN dotnet restore diff --git a/Source/Dockerfile.standalone b/Source/Dockerfile.standalone index b22a107..486c58b 100644 --- a/Source/Dockerfile.standalone +++ b/Source/Dockerfile.standalone @@ -25,7 +25,7 @@ FROM node:20-alpine AS ui-build WORKDIR /app ARG target_environment=production -ENV PATH /app/node_modules/.bin:$PATH +ENV PATH=/app/node_modules/.bin:$PATH RUN npm install react-scripts@latest -g COPY CdrAuthServer.UI/package.json package.json @@ -46,6 +46,7 @@ COPY ./ . FROM build AS publish COPY ./Directory.Build.props /app/Directory.Build.props +COPY ./.editorconfig /app/.editorconfig COPY ./CdrAuthServer.Domain/. /app/CdrAuthServer.Domain COPY ./CdrAuthServer.Repository/. /app/CdrAuthServer.Repository COPY ./CdrAuthServer/. /app/CdrAuthServer diff --git a/Source/docker-compose.E2ETests.Standalone.yml b/Source/docker-compose.E2ETests.Standalone.yml index 867cced..b8e8ecd 100644 --- a/Source/docker-compose.E2ETests.Standalone.yml +++ b/Source/docker-compose.E2ETests.Standalone.yml @@ -1,8 +1,10 @@ # Docker compose e2e tests +name: cdr-auth-server-e2e-tests + services: mock-register: - container_name: mock-register + container_name: mock-register-cdr-auth-server-e2e-tests image: mock-register hostname: mock-register ports: @@ -26,7 +28,7 @@ services: condition: service_healthy cdr-auth-server: - container_name: cdr-auth-server-standalone + container_name: cdr-auth-server-standalone-e2e-tests image: cdr-auth-server-standalone build: context: . @@ -65,7 +67,7 @@ services: condition: service_healthy cdr-auth-server-e2e-tests: - container_name: cdr-auth-server-e2e-tests + container_name: cdr-auth-server-e2e-tests-testing image: cdr-auth-server-e2e-tests ports: - "9999:9999" @@ -91,7 +93,7 @@ services: condition: service_healthy mssql: - container_name: sql1 + container_name: sql-cdr-auth-server-e2e-tests image: 'mcr.microsoft.com/mssql/server:2022-latest' ports: - "1433:1433" diff --git a/Source/docker-compose.IntegrationTests.Standalone.yml b/Source/docker-compose.IntegrationTests.Standalone.yml index f6f3416..bd16ef1 100644 --- a/Source/docker-compose.IntegrationTests.Standalone.yml +++ b/Source/docker-compose.IntegrationTests.Standalone.yml @@ -1,8 +1,8 @@ # Docker compose integration tests - +name: cdr-auth-server-integration-tests services: mock-register: - container_name: mock-register + container_name: mock-register-cdr-auth-server-integration-tests image: mock-register hostname: mock-register ports: @@ -26,7 +26,7 @@ services: condition: service_healthy cdr-auth-server: - container_name: cdr-auth-server-standalone + container_name: cdr-auth-server-standalone-integration-tests image: cdr-auth-server-standalone build: context: . @@ -87,7 +87,7 @@ services: # condition: service_healthy cdr-auth-server-integration-tests: - container_name: cdr-auth-server-integration-tests + container_name: cdr-auth-server-integration-tests-testing image: cdr-auth-server-integration-tests hostname: cdr-auth-server-integration-tests ports: @@ -116,7 +116,7 @@ services: condition: service_healthy mssql: - container_name: sql1 + container_name: sql-cdr-auth-server-integration-tests image: 'mcr.microsoft.com/mssql/server:2022-latest' ports: - "1433:1433" diff --git a/Source/run-e2e-tests-standalone.ps1 b/Source/run-e2e-tests-standalone.ps1 index 924754b..6bd2de9 100644 --- a/Source/run-e2e-tests-standalone.ps1 +++ b/Source/run-e2e-tests-standalone.ps1 @@ -14,7 +14,7 @@ docker-compose -f docker-compose.E2ETests.Standalone.yml up --build --abort-on-c $_lastExitCode = $LASTEXITCODE # Stop containers -docker-compose -f docker-compose.E2ETests.yml down +docker-compose -f docker-compose.E2ETests.Standalone.yml down if ($_lastExitCode -eq 0) { Write-Output "***********************************************************"