Skip to content

Commit

Permalink
Authentication handlers (#51)
Browse files Browse the repository at this point in the history
Handlers and request options storage added
  • Loading branch information
Spomky authored Apr 17, 2019
1 parent 392eb72 commit 0241f62
Show file tree
Hide file tree
Showing 18 changed files with 503 additions and 61 deletions.
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ parameters:
- '#Fetching class constant class of deprecated class Webauthn\\CredentialRepository\.#'
- '#Interface Webauthn\\PublicKeyCredentialSourceRepository extends deprecated interface Webauthn\\CredentialRepository\.#'
- '#Parameter (.*) of class Webauthn\\PublicKeyCredentialSource constructor expects string, string|null given\.#'
- '#Instanceof between Symfony\\Component\\HttpFoundation\\Response and Symfony\\Component\\HttpFoundation\\Response will always evaluate to true\.#'
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
Expand Down
31 changes: 29 additions & 2 deletions src/symfony-json-security/src/Resources/config/security.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
use Webauthn\JsonSecurityBundle\Security\Authentication\Provider\WebauthnProvider;
use Webauthn\JsonSecurityBundle\Security\EntryPoint\WebauthnEntryPoint;
use Webauthn\JsonSecurityBundle\Security\Firewall\WebauthnListener;
use Webauthn\JsonSecurityBundle\Security\Handler\DefaultFailureHandler;
use Webauthn\JsonSecurityBundle\Security\Handler\DefaultRequestOptionsHandler;
use Webauthn\JsonSecurityBundle\Security\Handler\DefaultSuccessHandler;
use Webauthn\JsonSecurityBundle\Security\Storage\SessionStorage;
use Webauthn\JsonSecurityBundle\Security\Voter\IsUserPresentVoter;
use Webauthn\JsonSecurityBundle\Security\Voter\IsUserVerifiedVoter;
use Webauthn\PublicKeyCredentialLoader;
Expand Down Expand Up @@ -57,8 +61,12 @@
ref(SessionAuthenticationStrategyInterface::class),
ref(HttpUtils::class),
ref('webauthn_json_security.fake_user_entity_provider')->nullOnInvalid(),
'',
[],
'', // Provider key
[], // Options
null, // Authentication success handler
null, // Authentication failure handler
null, // Request Options handler
null, // Request Options Storage
ref(LoggerInterface::class)->nullOnInvalid(),
ref(EventDispatcherInterface::class)->nullOnInvalid(),
])
Expand All @@ -68,6 +76,9 @@
$container->services()->set(WebauthnEntryPoint::class)
->abstract()
->private()
->args([
null, // Authentication failure handler
])
;

$container->services()->set(IsUserPresentVoter::class)
Expand All @@ -79,4 +90,20 @@
->private()
->tag('security.voter')
;

$container->services()->set(DefaultSuccessHandler::class)
->private()
;

$container->services()->set(DefaultFailureHandler::class)
->private()
;

$container->services()->set(SessionStorage::class)
->private()
;

$container->services()->set(DefaultRequestOptionsHandler::class)
->private()
;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,31 @@

namespace Webauthn\JsonSecurityBundle\Security\EntryPoint;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;

final class WebauthnEntryPoint implements AuthenticationEntryPointInterface
{
/**
* @var AuthenticationFailureHandlerInterface
*/
private $failureHandler;

public function __construct(AuthenticationFailureHandlerInterface $failureHandler)
{
$this->failureHandler = $failureHandler;
}

/**
* {@inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
public function start(Request $request, AuthenticationException $authException = null): Response
{
$data = [
'status' => 'error',
'errorMessage' => 'Authentication Required',
];
$exception = $authException ?? new AuthenticationException('Authentication Required');

return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
return $this->failureHandler->onAuthenticationFailure($request, $exception);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
use Symfony\Component\DependencyInjection\Reference;
use Webauthn\JsonSecurityBundle\Security\Authentication\Provider\WebauthnProvider;
use Webauthn\JsonSecurityBundle\Security\EntryPoint\WebauthnEntryPoint;
use Webauthn\JsonSecurityBundle\Security\Handler\DefaultFailureHandler;
use Webauthn\JsonSecurityBundle\Security\Handler\DefaultRequestOptionsHandler;
use Webauthn\JsonSecurityBundle\Security\Handler\DefaultSuccessHandler;
use Webauthn\JsonSecurityBundle\Security\Storage\SessionStorage;

class WebauthnSecurityFactory implements SecurityFactoryInterface
{
Expand All @@ -36,7 +40,7 @@ class WebauthnSecurityFactory implements SecurityFactoryInterface
public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId): array
{
$authProviderId = $this->createAuthProvider($container, $id, $config, $userProviderId);
$entryPointId = $this->createEntryPoint($container, $id);
$entryPointId = $this->createEntryPoint($container, $id, $config);
$listenerId = $this->createListener($container, $id, $config);

return [$authProviderId, $listenerId, $entryPointId];
Expand Down Expand Up @@ -70,8 +74,11 @@ public function addConfiguration(NodeDefinition $node): void
->scalarNode('profile')->isRequired()->end()
->scalarNode('options_path')->defaultValue('/login/options')->end()
->scalarNode('login_path')->defaultValue('/login')->end()
->scalarNode('session_parameter')->defaultValue('WEBAUTHN_PUBLIC_KEY_REQUEST_OPTIONS')->end()
->scalarNode('user_provider')->defaultNull()->end()
->scalarNode('request_options_storage')->defaultValue(SessionStorage::class)->end()
->scalarNode('request_options_handler')->defaultValue(DefaultRequestOptionsHandler::class)->end()
->scalarNode('success_handler')->defaultValue(DefaultSuccessHandler::class)->end()
->scalarNode('failure_handler')->defaultValue(DefaultFailureHandler::class)->end()
->end()
;
}
Expand All @@ -93,17 +100,24 @@ private function createListener(ContainerBuilder $container, string $id, array $
$listener = new ChildDefinition($listenerId);
$listener->replaceArgument(13, $id);
$listener->replaceArgument(14, $config);
$listener->replaceArgument(15, new Reference($config['success_handler']));
$listener->replaceArgument(16, new Reference($config['failure_handler']));
$listener->replaceArgument(17, new Reference($config['request_options_handler']));
$listener->replaceArgument(18, new Reference($config['request_options_storage']));

$listenerId .= '.'.$id;
$container->setDefinition($listenerId, $listener);

return $listenerId;
}

private function createEntryPoint(ContainerBuilder $container, string $id): string
private function createEntryPoint(ContainerBuilder $container, string $id, array $config): string
{
$entryPointId = 'webauthn.security.json.authentication.entrypoint.'.$id;
$container->setDefinition($entryPointId, new ChildDefinition(WebauthnEntryPoint::class));
$entryPoint = new ChildDefinition(WebauthnEntryPoint::class);
$entryPoint->replaceArgument(0, new Reference($config['failure_handler']));

$container->setDefinition($entryPointId, $entryPoint);

return $entryPointId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
use function Safe\sprintf;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
Expand All @@ -30,6 +29,8 @@
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Http\HttpUtils;
Expand All @@ -45,9 +46,11 @@
use Webauthn\JsonSecurityBundle\Model\PublicKeyCredentialFakeUserEntity;
use Webauthn\JsonSecurityBundle\Provider\FakePublicKeyCredentialUserEntityProvider;
use Webauthn\JsonSecurityBundle\Security\Authentication\Token\WebauthnToken;
use Webauthn\JsonSecurityBundle\Security\Handler\RequestOptionsHandler;
use Webauthn\JsonSecurityBundle\Security\Storage\RequestOptionsStorage;
use Webauthn\JsonSecurityBundle\Security\Storage\StoredData;
use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialSourceRepository;
use Webauthn\PublicKeyCredentialUserEntity;
Expand Down Expand Up @@ -135,7 +138,26 @@ class WebauthnListener implements ListenerInterface
*/
private $fakePublicKeyCredentialUserEntityProvider;

public function __construct(HttpMessageFactoryInterface $httpMessageFactory, SerializerInterface $serializer, ValidatorInterface $validator, PublicKeyCredentialRequestOptionsFactory $publicKeyCredentialRequestOptionsFactory, PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, PublicKeyCredentialUserEntityRepository $userEntityRepository, PublicKeyCredentialLoader $publicKeyCredentialLoader, AuthenticatorAssertionResponseValidator $authenticatorAssertionResponseValidator, TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, ?FakePublicKeyCredentialUserEntityProvider $fakePublicKeyCredentialSourceRepository, string $providerKey, array $options = [], LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null)
/**
* @var AuthenticationSuccessHandlerInterface
*/
private $authenticationSuccessHandler;

/**
* @var AuthenticationFailureHandlerInterface
*/
private $authenticationFailureHandler;

/**
* @var RequestOptionsStorage
*/
private $requestOptionsStorage;
/**
* @var RequestOptionsHandler
*/
private $requestOptionsHandler;

public function __construct(HttpMessageFactoryInterface $httpMessageFactory, SerializerInterface $serializer, ValidatorInterface $validator, PublicKeyCredentialRequestOptionsFactory $publicKeyCredentialRequestOptionsFactory, PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, PublicKeyCredentialUserEntityRepository $userEntityRepository, PublicKeyCredentialLoader $publicKeyCredentialLoader, AuthenticatorAssertionResponseValidator $authenticatorAssertionResponseValidator, TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, ?FakePublicKeyCredentialUserEntityProvider $fakePublicKeyCredentialSourceRepository, string $providerKey, array $options, AuthenticationSuccessHandlerInterface $authenticationSuccessHandler, AuthenticationFailureHandlerInterface $authenticationFailureHandler, RequestOptionsHandler $requestOptionsHandler, RequestOptionsStorage $requestOptionsStorage, LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null)
{
Assertion::notEmpty($providerKey, '$providerKey must not be empty.');

Expand All @@ -157,6 +179,10 @@ public function __construct(HttpMessageFactoryInterface $httpMessageFactory, Ser
$this->publicKeyCredentialRequestOptionsFactory = $publicKeyCredentialRequestOptionsFactory;
$this->validator = $validator;
$this->fakePublicKeyCredentialUserEntityProvider = $fakePublicKeyCredentialSourceRepository;
$this->authenticationSuccessHandler = $authenticationSuccessHandler;
$this->authenticationFailureHandler = $authenticationFailureHandler;
$this->requestOptionsStorage = $requestOptionsStorage;
$this->requestOptionsHandler = $requestOptionsHandler;
}

/**
Expand Down Expand Up @@ -189,8 +215,8 @@ public function handle(GetResponseEvent $event): void

private function onOptionsPath(GetResponseEvent $event): void
{
$request = $event->getRequest();
try {
$request = $event->getRequest();
$content = $request->getContent();
Assertion::string($content, 'Invalid data');
$creationOptionsRequest = $this->getServerPublicKeyCredentialRequestOptionsRequest($content);
Expand All @@ -208,10 +234,10 @@ private function onOptionsPath(GetResponseEvent $event): void
$this->options['profile'],
$allowedCredentials
);
$request->getSession()->set($this->options['session_parameter'], ['options' => $publicKeyCredentialRequestOptions, 'userEntity' => $userEntity]);
$response = new JsonResponse($publicKeyCredentialRequestOptions->jsonSerialize());
$this->requestOptionsStorage->store($request, new StoredData($publicKeyCredentialRequestOptions, $userEntity));
$response = $this->requestOptionsHandler->onRequestOptions($publicKeyCredentialRequestOptions, $userEntity);
} catch (\Exception $e) {
$response = $this->onAssertionFailure(new AuthenticationException($e->getMessage(), 0, $e));
$response = $this->onAssertionFailure($request, new AuthenticationException($e->getMessage(), 0, $e));
}

$event->setResponse($response);
Expand Down Expand Up @@ -245,15 +271,15 @@ private function onLoginPath(GetResponseEvent $event): void
$this->sessionStrategy->onAuthentication($request, $authenticatedToken);
$response = $this->onAssertionSuccess($request, $authenticatedToken);
} catch (AuthenticationException $e) {
$response = $this->onAssertionFailure($e);
$response = $this->onAssertionFailure($request, $e);
} catch (\Exception $e) {
$response = $this->onAssertionFailure(new AuthenticationException($e->getMessage(), 0, $e));
$response = $this->onAssertionFailure($request, new AuthenticationException($e->getMessage(), 0, $e));
}

$event->setResponse($response);
}

private function onAssertionFailure(AuthenticationException $failed): Response
private function onAssertionFailure(Request $request, AuthenticationException $failed): Response
{
if (null !== $this->logger) {
$this->logger->info('Webauthn authentication request failed.', ['exception' => $failed]);
Expand All @@ -264,12 +290,13 @@ private function onAssertionFailure(AuthenticationException $failed): Response
$this->tokenStorage->setToken(null);
}

$data = [
'status' => 'error',
'errorMessage' => $failed->getMessage(),
];
$response = $this->authenticationFailureHandler->onAuthenticationFailure($request, $failed);

return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
if (!$response instanceof Response) {
throw new RuntimeException('Authentication Failure Handler did not return a Response.');
}

return $response;
}

/**
Expand All @@ -291,31 +318,20 @@ private function onAssertionSuccess(Request $request, TokenInterface $token): Re
$this->dispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent);
}

$data = [
'status' => 'ok',
'errorMessage' => '',
];
$response = $this->authenticationSuccessHandler->onAuthenticationSuccess($request, $token);

if (!$response instanceof Response) {
throw new RuntimeException('Authentication Success Handler did not return a Response.');
}

return new JsonResponse($data, Response::HTTP_OK);
return $response;
}

private function processWithAssertion(Request $request): WebauthnToken
{
$sessionValue = $request->getSession()->remove($this->options['session_parameter']);
if (!\is_array($sessionValue) || !\array_key_exists('options', $sessionValue) || !\array_key_exists('userEntity', $sessionValue)) {
throw new BadRequestHttpException('No public key credential request options available for this session.');
}

$publicKeyCredentialRequestOptions = $sessionValue['options'];
$userEntity = $sessionValue['userEntity'];
$storedData = $this->requestOptionsStorage->get($request);

if (!$publicKeyCredentialRequestOptions instanceof PublicKeyCredentialRequestOptions) {
throw new BadRequestHttpException('No public key credential request options available for this session.');
}
if (!$userEntity instanceof PublicKeyCredentialUserEntity) {
throw new BadRequestHttpException('No user entity available for this session.');
}
if ($userEntity instanceof PublicKeyCredentialFakeUserEntity) {
if ($storedData->getPublicKeyCredentialUserEntity() instanceof PublicKeyCredentialFakeUserEntity) {
throw new BadRequestHttpException('Invalid assertion');
}

Expand All @@ -337,16 +353,16 @@ private function processWithAssertion(Request $request): WebauthnToken
$this->authenticatorAssertionResponseValidator->check(
$publicKeyCredential->getRawId(),
$response,
$publicKeyCredentialRequestOptions,
$storedData->getPublicKeyCredentialRequestOptions(),
$psr7Request,
$userEntity->getId()
$storedData->getPublicKeyCredentialUserEntity()->getId()
);
} catch (\Throwable $throwable) {
if (null !== $this->logger) {
$this->logger->error(sprintf(
'Invalid assertion: %s. Request was: %s. Reason is: %s (%s:%d)',
$assertion,
json_encode($publicKeyCredentialRequestOptions),
json_encode($storedData->getPublicKeyCredentialRequestOptions()),
$throwable->getMessage(),
$throwable->getFile(),
$throwable->getLine()
Expand All @@ -356,8 +372,8 @@ private function processWithAssertion(Request $request): WebauthnToken
}

$token = new WebauthnToken(
$userEntity->getName(),
$publicKeyCredentialRequestOptions,
$storedData->getPublicKeyCredentialUserEntity()->getName(),
$storedData->getPublicKeyCredentialRequestOptions(),
$publicKeyCredentialSource->getPublicKeyCredentialDescriptor(),
$response->getAuthenticatorData()->isUserPresent(),
$response->getAuthenticatorData()->isUserVerified(),
Expand Down
Loading

0 comments on commit 0241f62

Please sign in to comment.