Skip to content

Commit

Permalink
UML-3056 initial code and working test (#2360)
Browse files Browse the repository at this point in the history
* UML-3056 initial code and working test

* UML-3056 fix php linting

* get mock onelogin from ecr and spin that up locally and in CI

* tell docker where image is

* move ecr login earlier as its needed to pull down mock login container

* use variable for registry

* add docker logs

* force a rebuild

* docker inspect to get ip addr of mock

* ecr login

* add jwks to authService and test, and check ui_locale set to en or cy

* Turn AuthenticationServiceTest into unit test

* Remove try catch in createAuthorisationRequest

---------

Co-authored-by: Nick Davis <[email protected]>
Co-authored-by: Lbagg1 <[email protected]>
Co-authored-by: Nick Davis <[email protected]>
  • Loading branch information
4 people authored Oct 11, 2023
1 parent d7694e9 commit b87582a
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 41 deletions.
25 changes: 16 additions & 9 deletions .github/workflows/_build-and-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 # [email protected]
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" \
Expand All @@ -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 \
Expand All @@ -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
Expand Down Expand Up @@ -256,15 +272,6 @@ jobs:
with:
sarif_file: 'trivy-results.sarif'

- name: ecr login
id: login_ecr
uses: aws-actions/amazon-ecr-login@b5f9031d829ac39b7fd26092019aeca3ee6b3ebd # [email protected]
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 }}
Expand Down
83 changes: 55 additions & 28 deletions service-api/app/config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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');
};
4 changes: 0 additions & 4 deletions service-api/app/src/App/src/Handler/AuthHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
use Psr\Http\Server\RequestHandlerInterface;

/**
* Class AuthHandler
*
* @package App\Handler
* @codeCoverageIgnore
*/
class AuthHandler implements RequestHandlerInterface
Expand All @@ -32,7 +29,6 @@ public function __construct(
* May call other collaborating code to generate the response.
*
* @param ServerRequestInterface $request
*
* @return ResponseInterface
* @throws Exception
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace App\Handler;

use App\Exception\BadRequestException;
use App\Service\Authentication\OneLoginAuthorisationRequestService;
use Exception;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* @codeCoverageIgnore
*/
class OneLoginAuthorisationRequestHandler implements RequestHandlerInterface
{
public function __construct(
private OneLoginAuthorisationRequestService $authorisationRequestService,
) {
}

/**
* @param ServerRequestInterface $request
* @return ResponseInterface
* @throws Exception
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$params = $request->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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace App\Service\Authentication;

use Facile\OpenIDClient\Client\ClientBuilder;
use Facile\OpenIDClient\Client\Metadata\ClientMetadata;
use Facile\OpenIDClient\Issuer\IssuerBuilderInterface;
use Facile\OpenIDClient\Service\Builder\AuthorizationServiceBuilder;

use function Facile\OpenIDClient\base64url_encode;

class OneLoginAuthorisationRequestService
{
public function __construct(
private JWKFactory $JWKFactory,
private IssuerBuilderInterface $issuerBuilder,
) {
}

public function createAuthorisationRequest(string $uiLocale): string
{
//TODO UML-3080 Configure cache

$issuer = $this->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}}',
]
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace AppTest\Service\Authentication;

use App\Service\Authentication\OneLoginAuthorisationRequestService;
use App\Service\Authentication\JWKFactory;
use Facile\OpenIDClient\Issuer\IssuerBuilder;
use Facile\OpenIDClient\Issuer\IssuerBuilderInterface;
use Facile\OpenIDClient\Issuer\IssuerInterface;
use Facile\OpenIDClient\Issuer\Metadata\IssuerMetadataInterface;
use Jose\Component\Core\JWK;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;

class OneLoginAuthorisationRequestServiceTest extends TestCase
{
use ProphecyTrait;

private ObjectProphecy|JWKFactory $JWKFactory;
private ObjectProphecy|IssuerBuilder $issuerBuilder;

public function setup(): void
{
$jwk = $this->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);
}
}

0 comments on commit b87582a

Please sign in to comment.