Skip to content

Commit

Permalink
Uml 3013 kick off one login process (#2369)
Browse files Browse the repository at this point in the history
* Initial landing page and route, structure behat test

* Add landing page with button and add test step

* One login page behind feature flag

* Add one login form, submit button produces logger output

* Fix linting errors

* Button returns 200 status - Link factory classes in configProvider, api returns array

* AuthoriseOneLoginHandler sends request with en or cy

* Add unit test coverage

* Clicking button sends us to redirect pag, change login flag

* Add unit test for OneLoginForm and fix failing api-app test

* Add SecretsManagerFactoryTest

---------

Co-authored-by: Sam Ainsworth <[email protected]>
  • Loading branch information
MishNajam and Sam Ainsworth authored Oct 30, 2023
1 parent c434ccb commit 5ae1593
Show file tree
Hide file tree
Showing 19 changed files with 383 additions and 39 deletions.
2 changes: 1 addition & 1 deletion docker-compose.dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,4 @@ services:
environment:
PUBLIC_URL: http://localhost:4013
INTERNAL_URL: http://mock-one-login:8080
REDIRECT_URL: http://localhost:9002/
REDIRECT_URL: http://localhost:9002/auth/redirect
4 changes: 2 additions & 2 deletions service-api/app/config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
use Mezzio\Application;
use Mezzio\MiddlewareFactory;
use Psr\Container\ContainerInterface;
use App\Handler\OneLoginAuthorisationRequestHandler;
use App\Handler\OneLoginAuthenticationRequestHandler;

/**
* Setup routes with a single request method:
Expand Down Expand Up @@ -135,7 +135,7 @@

$app->patch('/v1/auth', AuthHandler::class, 'user.auth');

$app->get('/v1/auth-one-login', OneLoginAuthorisationRequestHandler::class, 'user.auth-one-login');
$app->get('/v1/auth-one-login', OneLoginAuthenticationRequestHandler::class, 'user.auth-one-login');

$app->post('/v1/email-user/{emailTemplate}', NotifyHandler::class, 'lpa.user.notify');
};
9 changes: 8 additions & 1 deletion service-api/app/src/App/src/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace App;

use App\DataAccess\ApiGateway\InstructionsAndPreferencesImagesFactory;
use Aws;
use Http;
use Facile;
use Laminas;
use Psr;

Expand Down Expand Up @@ -56,12 +56,16 @@ public function getDependencies(): array
DataAccess\Repository\LpasInterface::class => DataAccess\ApiGateway\Lpas::class,
DataAccess\Repository\InstructionsAndPreferencesImagesInterface::class
=> DataAccess\ApiGateway\InstructionsAndPreferencesImages::class,

//One Login
Facile\OpenIDClient\Issuer\IssuerBuilderInterface::class => Facile\OpenIDClient\Issuer\IssuerBuilder::class
],

'factories' => [
// Services
Aws\Sdk::class => Service\Aws\SdkFactory::class,
Aws\DynamoDb\DynamoDbClient::class => Service\Aws\DynamoDbClientFactory::class,
Aws\SecretsManager\SecretsManagerClient::class => Service\Aws\SecretsManagerFactory::class,
Service\ApiClient\Client::class => Service\ApiClient\ClientFactory::class,
Service\Email\EmailClient::class => Service\Email\EmailClientFactory::class,

Expand Down Expand Up @@ -92,6 +96,9 @@ public function getDependencies(): array
Laminas\Stratigility\Middleware\ErrorHandler::class => [
Service\Log\LogStderrListenerDelegatorFactory::class,
],
Laminas\Cache\Storage\AdapterPluginManager::class => [
Laminas\Cache\Storage\Adapter\Apcu\AdapterPluginManagerDelegatorFactory::class,
],
],
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace App\Handler;

use App\Exception\BadRequestException;
use App\Service\Authentication\OneLoginAuthorisationRequestService;
use App\Service\Authentication\OneLoginAuthenticationRequestService;
use Exception;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -15,10 +15,10 @@
/**
* @codeCoverageIgnore
*/
class OneLoginAuthorisationRequestHandler implements RequestHandlerInterface
class OneLoginAuthenticationRequestHandler implements RequestHandlerInterface
{
public function __construct(
private OneLoginAuthorisationRequestService $authorisationRequestService,
private OneLoginAuthenticationRequestService $authenticationRequestService,
) {
}

Expand All @@ -29,19 +29,17 @@ public function __construct(
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$params = $request->getParsedBody();
$requestData = $request->getQueryParams();

if (empty($params['ui_locale'])) {
if (empty($requestData['ui_locale'])) {
throw new BadRequestException('Ui locale must be provided');
}

$ui_locale = strtolower($params['ui_locale']);
$ui_locale = strtolower($requestData['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);
return new JsonResponse($this->authenticationRequestService->createAuthenticationRequest($ui_locale));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@

use function Facile\OpenIDClient\base64url_encode;

class OneLoginAuthorisationRequestService
class OneLoginAuthenticationRequestService
{
public function __construct(
private JWKFactory $jwkFactory,
private IssuerBuilderInterface $issuerBuilder,
private CacheFactory $cacheFactory
private CacheFactory $cacheFactory,
) {
}

public function createAuthorisationRequest(string $uiLocale): string
public function createAuthenticationRequest(string $uiLocale): array
{

$cachedBuilder = new MetadataProviderBuilder();
$cachedBuilder->setCache(($this->cacheFactory)('cache'))
$cachedBuilder->setCache(($this->cacheFactory)('one-login'))
->setCacheTtl(3600);

$issuer = $this->issuerBuilder
Expand All @@ -52,17 +52,25 @@ public function createAuthorisationRequest(string $uiLocale): string

$authorisationService = (new AuthorizationServiceBuilder())->build();

return $authorisationService->getAuthorizationUri(
$state = base64url_encode(random_bytes(12));
$nonce = openssl_digest(random_bytes(24), 'sha256');
$authorisationRequestUrl = $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'),
'state' => $state,
'redirect_uri' => 'http://localhost:9002/auth/redirect', //TODO: use dynamic domain UML-3121
'nonce' => $nonce,
'vtr' => '["Cl.Cm.P2"]',
'ui_locales' => $uiLocale,
'claims' => '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT": null}}',
]
);

return [
'state' => $state,
'nonce' => $nonce,
'url' => $authorisationRequestUrl,
];
}
}
19 changes: 19 additions & 0 deletions service-api/app/src/App/src/Service/Aws/SecretsManagerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace App\Service\Aws;

use Aws\Sdk;
use Psr\Container\ContainerInterface;

/**
* Builds a configured instance of the AWS Secrets Manager Client
*/
class SecretsManagerFactory
{
public function __invoke(ContainerInterface $container)
{
return $container->get(Sdk::class)->createSecretsManager();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,20 @@

namespace AppTest\Service\Authentication;

use App\Service\Authentication\OneLoginAuthorisationRequestService;
use App\Service\Authentication\OneLoginAuthenticationRequestService;
use App\Service\Authentication\JWKFactory;
use App\Service\Cache\CacheFactory;
use App\Service\Authentication\IssuerBuilder;
use Facile\OpenIDClient\Issuer\IssuerBuilderInterface;
use Facile\OpenIDClient\Issuer\IssuerInterface;
use Facile\OpenIDClient\Issuer\Metadata\IssuerMetadataInterface;
use Facile\OpenIDClient\Issuer\Metadata\Provider\MetadataProviderBuilder;
use Interop\Container\Containerinterface;
use Jose\Component\Core\JWK;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\SimpleCache\CacheInterface;

class OneLoginAuthorisationRequestServiceTest extends TestCase
class OneLoginAuthenticationRequestServiceTest extends TestCase
{
use ProphecyTrait;

Expand All @@ -43,24 +40,28 @@ public function setup(): void
$issuerMetaData->getAuthorizationEndpoint()->willReturn('fake endpoint');
$this->issuerBuilder->setMetadataProviderBuilder(Argument::any())->willReturn($this->issuerBuilder);
$this->issuerBuilder->build('http://mock-one-login:8080/.well-known/openid-configuration')->willReturn($issuer);
$this->cacheFactory->__invoke('cache')->willReturn($cacheInterface);
$this->cacheFactory->__invoke('one-login')->willReturn($cacheInterface);
}

/**
* @test
*/
public function create_authorisation_request(): void
public function create_authentication_request(): void
{
$authorisationRequestService = new OneLoginAuthorisationRequestService(
$authorisationRequestService = new OneLoginAuthenticationRequestService(
$this->jwkFactory->reveal(),
$this->issuerBuilder->reveal(),
$this->cacheFactory->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);
$authorisationRequest = $authorisationRequestService->createAuthenticationRequest('en');
$authorisationRequestUrl = $authorisationRequest['url'];
$this->assertStringContainsString('client_id=client-id', $authorisationRequestUrl);
$this->assertStringContainsString('scope=openid+email', $authorisationRequestUrl);
$this->assertStringContainsString('vtr=["Cl.Cm.P2"]', urldecode($authorisationRequestUrl));
$this->assertStringContainsString('ui_locales=en', $authorisationRequestUrl);
$this->assertStringContainsString(
'redirect_uri=http://localhost:9002/auth/redirect',
urldecode($authorisationRequestUrl)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace AppTest\Service\Aws;

use App\Service\Aws\SecretsManagerFactory;
use Aws\Sdk;
use Aws\SecretsManager\SecretsManagerClient;
use Monolog\Test\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Container\ContainerInterface;

class SecretsManagerFactoryTest extends TestCase
{
use ProphecyTrait;

/** @test */
public function it_creates_a_SecretsManager(): void
{
$containerProphecy = $this->prophesize(ContainerInterface::class);

$containerProphecy->get(Sdk::class)
->willReturn(new Sdk([
'region' => 'eu-west-1',
'version' => 'latest',
]));

$factory = new SecretsManagerFactory();
$client = $factory($containerProphecy->reveal());

$this->assertInstanceOf(SecretsManagerClient::class, $client);
}
}
10 changes: 9 additions & 1 deletion service-front/app/config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,18 @@
$actorRoutes = function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void {
$USE_OLDER_LPA_JOURNEY = 'use_older_lpa_journey';
$DELETE_LPA_FEATURE = 'delete_lpa_feature';
$ALLOW_GOV_ONE_LOGIN = 'allow_gov_one_login';

$defaultNotFoundPage = Actor\Handler\LpaDashboardHandler::class;

$app->route('/home', Actor\Handler\ActorTriagePageHandler::class, ['GET', 'POST'], 'home');
$app->route('/home', [
new ConditionalRoutingMiddleware(
$container,
$ALLOW_GOV_ONE_LOGIN,
Actor\Handler\AuthenticateOneLoginHandler::class,
Actor\Handler\ActorTriagePageHandler::class
)
], ['GET', 'POST'], 'home');
$app->route('/', Actor\Handler\ActorTriagePageHandler::class, ['GET', 'POST'], 'home-trial');
$app->get('/healthcheck', Common\Handler\HealthcheckHandler::class, 'healthcheck');
$app->get('/stats', Actor\Handler\StatsPageHandler::class, 'actor-stats');
Expand Down
2 changes: 1 addition & 1 deletion service-front/app/features/bootstrap/behat.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
'delete_lpa_feature' => true,
'dont_send_lpas_registered_after_sep_2019_to_cleansing_team' => true,
'instructions_and_preferences' => true,
'allow_gov_one_login' => true,
'allow_gov_one_login' => false,
],
'notify' => [
'api' => [
Expand Down
50 changes: 50 additions & 0 deletions service-front/app/features/context/UI/AccountContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
use PHPUnit\Framework\Assert;
use Psr\Http\Message\RequestInterface;

use function PHPUnit\Framework\assertArrayHasKey;

/**
* @property string userEmail
* @property string userPassword
Expand Down Expand Up @@ -2029,4 +2031,52 @@ public function iReceiveAnEmailAndShownUniqueInstructionsOnHowToResetMyPassword(
$this->ui->assertPageAddress('/reset-password');
$this->ui->assertPageContainsText('We\'ve emailed a link to ');
}

/**
* @Given /^I am on the temporary one login page$/
*/
public function iAmOnTheTemporaryOneLoginPage(): void
{
$this->ui->visit('/home');
$this->ui->assertPageAddress('/home');
$this->ui->assertElementContainsText('button[name=sign-in-one-login]', 'Sign in via One Login');
}

/**
* @When /^I click the one login button$/
*/
public function iClickTheOneLoginButton(): void
{
$this->apiFixtures->append(
ContextUtilities::newResponse(
StatusCodeInterface::STATUS_OK,
json_encode(
[
'state' => 'fakestate',
'nonce' => 'fakenonce',
'url' => 'http://fake.url/authorize',
]
),
self::USER_SERVICE_REQUEST_PASSWORD_RESET
)
);

$this->iDoNotFollowRedirects();
$this->ui->pressButton('Sign in via One Login');
$this->iDoFollowRedirects();

$request = $this->apiFixtures->getLastRequest();
$params = $request->getUri()->getQuery();
Assert::assertStringContainsString('ui_locale=en', $params);
}

/**
* @Then /^I am redirected to the redirect page$/
*/
public function iAmRedirectedToTheRedirectPage(): void
{
$locationHeader = $this->ui->getSession()->getResponseHeader('Location');
assert::assertTrue(isset($locationHeader));
assert::assertEquals($locationHeader, 'http://fake.url/authorize');
}
}
8 changes: 8 additions & 0 deletions service-front/app/features/one-login.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@onelogin
Feature: Authenticate One Login

@ui @actor @ff:allow_gov_one_login:true
Scenario: I initiate authentication via one login
Given I am on the temporary one login page
When I click the one login button
Then I am redirected to the redirect page
Loading

0 comments on commit 5ae1593

Please sign in to comment.