diff --git a/.github/workflows/_build-and-push.yml b/.github/workflows/_build-and-push.yml index ba246aaa62..18950f7598 100644 --- a/.github/workflows/_build-and-push.yml +++ b/.github/workflows/_build-and-push.yml @@ -146,9 +146,19 @@ jobs: matrix.run_unit_tests == 'true' && (inputs.specific_path == 'all' || inputs.specific_path == matrix.svc_prefix) + - name: ecr login + id: login_ecr + uses: aws-actions/amazon-ecr-login@b5f9031d829ac39b7fd26092019aeca3ee6b3ebd # pin@v1.5.1 + with: + registries: 311462405659 + if: | + inputs.push_to_ecr == 'true' && + (inputs.specific_path == 'all' || inputs.specific_path == matrix.svc_prefix) + - name: docker integration and acceptance tests env: SVC_PREFIX: ${{ matrix.svc_prefix }} + ECR_REGISTRY: ${{ steps.login_ecr.outputs.registry }} run: | if [[ "${SVC_PREFIX}" = "api" ]]; then docker run -d --rm --name lpa-codes-pact-mock pactfoundation/pact-cli:latest mock-service -p "80" --host "0.0.0.0" \ @@ -157,10 +167,15 @@ jobs: --pact-dir /tmp/pacts --consumer use_a_lasting_power_of_attorney --provider api-gateway docker run -d --rm --name iap-images-mock pactfoundation/pact-cli:latest mock-service -p "80" --host "0.0.0.0" \ --pact-dir /tmp/pacts --consumer use_a_lasting_power_of_attorney --provider iap-api-gateway + docker run -d --rm --name mock-one-login $ECR_REGISTRY/use_an_lpa/mock_onelogin_app export DOCKER_REMOTE_LPA_PACT_IP="$(docker inspect --format='{{.NetworkSettings.IPAddress}}' lpa-codes-pact-mock)" export DOCKER_REMOTE_API_PACT_IP="$(docker inspect --format='{{.NetworkSettings.IPAddress}}' api-gateway-pact-mock)" export DOCKER_REMOTE_IAP_PACT_IP="$(docker inspect --format='{{.NetworkSettings.IPAddress}}' iap-images-mock)" + export DOCKER_REMOTE_MOCK_ONELOGIN_IP="$(docker inspect --format='{{.NetworkSettings.IPAddress}}' mock-one-login)" + + curl -XGET $DOCKER_REMOTE_MOCK_ONELOGIN_IP:8080/token + docker logs mock-one-login docker run -d --name behattests \ --add-host lpa-codes-pact-mock:$DOCKER_REMOTE_LPA_PACT_IP \ @@ -174,6 +189,7 @@ jobs: docker stop lpa-codes-pact-mock docker stop api-gateway-pact-mock docker stop iap-images-mock + docker stop mock-one-login elif [[ "${SVC_PREFIX}" = "front" ]]; then docker run -d --name behattests front-app:latest @@ -256,15 +272,6 @@ jobs: with: sarif_file: 'trivy-results.sarif' - - name: ecr login - id: login_ecr - uses: aws-actions/amazon-ecr-login@b5f9031d829ac39b7fd26092019aeca3ee6b3ebd # pin@v1.5.1 - with: - registries: 311462405659 - if: | - inputs.push_to_ecr == 'true' && - (inputs.specific_path == 'all' || inputs.specific_path == matrix.svc_prefix) - - name: tag and push container env: ECR_REGISTRY: ${{ steps.login_ecr.outputs.registry }} diff --git a/service-api/app/config/routes.php b/service-api/app/config/routes.php index ce4e6a02fd..1fca8d5252 100644 --- a/service-api/app/config/routes.php +++ b/service-api/app/config/routes.php @@ -2,9 +2,34 @@ declare(strict_types=1); +use App\Handler\AccessForAllLpaConfirmationHandler; +use App\Handler\AccessForAllLpaValidationHandler; +use App\Handler\AddLpaConfirmationHandler; +use App\Handler\AddLpaValidationHandler; +use App\Handler\AuthHandler; +use App\Handler\CanPasswordResetHandler; +use App\Handler\CanResetEmailHandler; +use App\Handler\ChangePasswordHandler; +use App\Handler\CompleteChangeEmailHandler; +use App\Handler\CompleteDeleteAccountHandler; +use App\Handler\CompletePasswordResetHandler; +use App\Handler\HealthcheckHandler; +use App\Handler\LpasCollectionHandler; +use App\Handler\LpasResourceCodesCollectionHandler; +use App\Handler\LpasResourceHandler; +use App\Handler\LpasResourceImagesCollectionHandler; +use App\Handler\NotifyHandler; +use App\Handler\RequestChangeEmailHandler; +use App\Handler\RequestCleanseHandler; +use App\Handler\RequestPasswordResetHandler; +use App\Handler\UserActivateHandler; +use App\Handler\UserHandler; +use App\Handler\ViewerCodeFullHandler; +use App\Handler\ViewerCodeSummaryHandler; use Mezzio\Application; use Mezzio\MiddlewareFactory; use Psr\Container\ContainerInterface; +use App\Handler\OneLoginAuthorisationRequestHandler; /** * Setup routes with a single request method: @@ -33,82 +58,84 @@ * ); */ return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { - $app->get('/healthcheck', App\Handler\HealthcheckHandler::class, 'healthcheck'); + $app->get('/healthcheck', HealthcheckHandler::class, 'healthcheck'); - $app->get('/v1/lpas', App\Handler\LpasCollectionHandler::class, 'lpa.collection'); + $app->get('/v1/lpas', LpasCollectionHandler::class, 'lpa.collection'); $app->post( '/v1/older-lpa/validate', - App\Handler\AccessForAllLpaValidationHandler::class, + AccessForAllLpaValidationHandler::class, 'lpa.older.validate' ); $app->patch( '/v1/older-lpa/confirm', - App\Handler\AccessForAllLpaConfirmationHandler::class, + AccessForAllLpaConfirmationHandler::class, 'lpa.older.confirm' ); $app->post( '/v1/older-lpa/cleanse', - App\Handler\RequestCleanseHandler::class, + RequestCleanseHandler::class, 'lpa.older.cleanse' ); - $app->get('/v1/lpas/{user-lpa-actor-token:[0-9a-f\-]+}', App\Handler\LpasResourceHandler::class, 'lpa.resource'); - $app->delete('/v1/lpas/{user-lpa-actor-token:[0-9a-f\-]+}', App\Handler\LpasResourceHandler::class, 'lpa.remove'); + $app->get('/v1/lpas/{user-lpa-actor-token:[0-9a-f\-]+}', LpasResourceHandler::class, 'lpa.resource'); + $app->delete('/v1/lpas/{user-lpa-actor-token:[0-9a-f\-]+}', LpasResourceHandler::class, 'lpa.remove'); $app->post( '/v1/lpas/{user-lpa-actor-token:[0-9a-f\-]+}/codes', - App\Handler\LpasResourceCodesCollectionHandler::class, + LpasResourceCodesCollectionHandler::class, 'lpa.create.code' ); $app->get( '/v1/lpas/{user-lpa-actor-token:[0-9a-f\-]+}/codes', - App\Handler\LpasResourceCodesCollectionHandler::class, + LpasResourceCodesCollectionHandler::class, 'lpa.get.codes' ); $app->put( '/v1/lpas/{user-lpa-actor-token:[0-9a-f\-]+}/codes', - App\Handler\LpasResourceCodesCollectionHandler::class, + LpasResourceCodesCollectionHandler::class, 'lpa.cancel.code' ); $app->get( '/v1/lpas/{user-lpa-actor-token:[0-9a-f\-]+}/images', - App\Handler\LpasResourceImagesCollectionHandler::class, + LpasResourceImagesCollectionHandler::class, 'lpa.get.images' ); - $app->post('/v1/add-lpa/validate', App\Handler\AddLpaValidationHandler::class, 'lpa.add.validate'); - $app->post('/v1/add-lpa/confirm', App\Handler\AddLpaConfirmationHandler::class, 'lpa.add.confirm'); + $app->post('/v1/add-lpa/validate', AddLpaValidationHandler::class, 'lpa.add.validate'); + $app->post('/v1/add-lpa/confirm', AddLpaConfirmationHandler::class, 'lpa.add.confirm'); - $app->post('/v1/viewer-codes/summary', App\Handler\ViewerCodeSummaryHandler::class, 'lpa.viewer-code.summary'); - $app->post('/v1/viewer-codes/full', App\Handler\ViewerCodeFullHandler::class, 'lpa.viewer-code.full'); + $app->post('/v1/viewer-codes/summary', ViewerCodeSummaryHandler::class, 'lpa.viewer-code.summary'); + $app->post('/v1/viewer-codes/full', ViewerCodeFullHandler::class, 'lpa.viewer-code.full'); - $app->get('/v1/user', App\Handler\UserHandler::class, 'user.get'); - $app->post('/v1/user', App\Handler\UserHandler::class, 'user.create'); - $app->patch('/v1/user-activation', App\Handler\UserActivateHandler::class, 'user.activate'); + $app->get('/v1/user', UserHandler::class, 'user.get'); + $app->post('/v1/user', UserHandler::class, 'user.create'); + $app->patch('/v1/user-activation', UserActivateHandler::class, 'user.activate'); - $app->patch('/v1/request-password-reset', App\Handler\RequestPasswordResetHandler::class, 'user.password-reset'); - $app->get('/v1/can-password-reset', App\Handler\CanPasswordResetHandler::class, 'user.can-password-reset'); + $app->patch('/v1/request-password-reset', RequestPasswordResetHandler::class, 'user.password-reset'); + $app->get('/v1/can-password-reset', CanPasswordResetHandler::class, 'user.can-password-reset'); $app->patch( '/v1/complete-password-reset', - App\Handler\CompletePasswordResetHandler::class, + CompletePasswordResetHandler::class, 'user.complete-password-reset' ); - $app->patch('/v1/request-change-email', App\Handler\RequestChangeEmailHandler::class, 'user.request-change-email'); - $app->get('/v1/can-reset-email', App\Handler\CanResetEmailHandler::class, 'user.can-reset-email'); + $app->patch('/v1/request-change-email', RequestChangeEmailHandler::class, 'user.request-change-email'); + $app->get('/v1/can-reset-email', CanResetEmailHandler::class, 'user.can-reset-email'); $app->patch( '/v1/complete-change-email', - App\Handler\CompleteChangeEmailHandler::class, + CompleteChangeEmailHandler::class, 'user.complete-change-email' ); - $app->patch('/v1/change-password', App\Handler\ChangePasswordHandler::class, 'user.change-password'); + $app->patch('/v1/change-password', ChangePasswordHandler::class, 'user.change-password'); $app->delete( '/v1/delete-account/{account-id:[0-9a-f\-]+}', - App\Handler\CompleteDeleteAccountHandler::class, + CompleteDeleteAccountHandler::class, 'user.delete-account' ); - $app->patch('/v1/auth', App\Handler\AuthHandler::class, 'user.auth'); + $app->patch('/v1/auth', AuthHandler::class, 'user.auth'); - $app->post('/v1/email-user/{emailTemplate}', App\Handler\NotifyHandler::class, 'lpa.user.notify'); + $app->get('/v1/auth-one-login', OneLoginAuthorisationRequestHandler::class, 'user.auth-one-login'); + + $app->post('/v1/email-user/{emailTemplate}', NotifyHandler::class, 'lpa.user.notify'); }; diff --git a/service-api/app/src/App/src/Handler/AuthHandler.php b/service-api/app/src/App/src/Handler/AuthHandler.php index 51d77dfaa1..c96973f486 100644 --- a/service-api/app/src/App/src/Handler/AuthHandler.php +++ b/service-api/app/src/App/src/Handler/AuthHandler.php @@ -14,9 +14,6 @@ use Psr\Http\Server\RequestHandlerInterface; /** - * Class AuthHandler - * - * @package App\Handler * @codeCoverageIgnore */ class AuthHandler implements RequestHandlerInterface @@ -32,7 +29,6 @@ public function __construct( * May call other collaborating code to generate the response. * * @param ServerRequestInterface $request - * * @return ResponseInterface * @throws Exception */ diff --git a/service-api/app/src/App/src/Handler/OneLoginAuthorisationRequestHandler.php b/service-api/app/src/App/src/Handler/OneLoginAuthorisationRequestHandler.php new file mode 100644 index 0000000000..f87d3c19c7 --- /dev/null +++ b/service-api/app/src/App/src/Handler/OneLoginAuthorisationRequestHandler.php @@ -0,0 +1,47 @@ +getParsedBody(); + + if (empty($params['ui_locale'])) { + throw new BadRequestException('Ui locale must be provided'); + } + + $ui_locale = strtolower($params['ui_locale']); + if ($ui_locale !== 'en' and $ui_locale !== 'cy') { + throw new BadRequestException('ui_locale is not set to en or cy'); + } + + $authorisationUri = $this->authorisationRequestService->createAuthorisationRequest($params['ui_locale']); + + return new JsonResponse($authorisationUri); + } +} diff --git a/service-api/app/src/App/src/Service/Authentication/OneLoginAuthorisationRequestService.php b/service-api/app/src/App/src/Service/Authentication/OneLoginAuthorisationRequestService.php new file mode 100644 index 0000000000..a470462491 --- /dev/null +++ b/service-api/app/src/App/src/Service/Authentication/OneLoginAuthorisationRequestService.php @@ -0,0 +1,61 @@ +issuerBuilder + ->build('http://mock-one-login:8080/.well-known/openid-configuration'); + + + $clientMetadata = ClientMetadata::fromArray([ + 'client_id' => 'client-id', + 'client_secret' => 'my-client-secret', + 'token_endpoint_auth_method' => 'private_key_jwt', + 'jwks' => [ + 'keys' => [ + ($this->JWKFactory)(), + ], + ], + ]); + + $client = (new ClientBuilder()) + ->setIssuer($issuer) + ->setClientMetadata($clientMetadata) + ->build(); + + $authorisationService = (new AuthorizationServiceBuilder())->build(); + + return $authorisationService->getAuthorizationUri( + $client, + [ + 'scope' => 'openid email', + 'state' => base64url_encode(random_bytes(12)), + 'redirect_uri' => '/lpa/dashboard', + 'nonce' => openssl_digest(base64url_encode(random_bytes(12)), 'sha256'), + 'vtr' => '["Cl.Cm.P2"]', + 'ui_locales' => $uiLocale, + 'claims' => '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT": null}}', + ] + ); + } +} diff --git a/service-api/app/test/AppTest/Service/Authentication/OneLoginAuthorisationRequestServiceTest.php b/service-api/app/test/AppTest/Service/Authentication/OneLoginAuthorisationRequestServiceTest.php new file mode 100644 index 0000000000..6ff1501f7d --- /dev/null +++ b/service-api/app/test/AppTest/Service/Authentication/OneLoginAuthorisationRequestServiceTest.php @@ -0,0 +1,55 @@ +prophesize(JWK::class); + $this->JWKFactory = $this->prophesize(JWKFactory::class); + $this->issuerBuilder = $this->prophesize(IssuerBuilderInterface::class); + $issuer = $this->prophesize(IssuerInterface::class); + $issuerMetaData = $this->prophesize(IssuerMetadataInterface::class); + + $this->JWKFactory->__invoke()->willReturn($jwk); + $issuer->getMetadata()->willReturn($issuerMetaData); + $issuerMetaData->getAuthorizationEndpoint()->willReturn('fake endpoint'); + $this->issuerBuilder->build('http://mock-one-login:8080/.well-known/openid-configuration')->willReturn($issuer); + } + + /** + * @test + */ + public function create_authorisation_request(): void + { + $authorisationRequestService = new OneLoginAuthorisationRequestService( + $this->JWKFactory->reveal(), + $this->issuerBuilder->reveal() + ); + $authorisationRequest = $authorisationRequestService->createAuthorisationRequest('en'); + $this->assertStringContainsString('client_id=client-id', $authorisationRequest); + $this->assertStringContainsString('scope=openid+email', $authorisationRequest); + $this->assertStringContainsString('vtr=%5B%22Cl.Cm.P2%22%5D', $authorisationRequest); + $this->assertStringContainsString('ui_locales=en', $authorisationRequest); + $this->assertStringContainsString('redirect_uri=%2Flpa%2Fdashboard', $authorisationRequest); + } +}