Skip to content

Commit

Permalink
refactor: Improve failure detection by providing custom exceptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
drupol committed Feb 9, 2021
1 parent b28193a commit ca1f6f7
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 35 deletions.
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The features it provides are:

- Provides default configuration to work with API Gateway,
- Has a failsafe mechanism for public key retrieval and embed the public keys of the default API Gateway in case of failure,
- Provides a default `UserProvider` service and `User` entity,
- Provides a default ``UserProvider`` service and ``User`` entity,

API Gateway
~~~~~~~~~~~
Expand Down
22 changes: 21 additions & 1 deletion docs/pages/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,29 @@ Hereunder an example of configuration for this bundle.
api_gw_authentication:
defaults:
env: acceptance # Available values are: acceptance, intra, production
env: acceptance # Available values are: acceptance, intra, production, user
security:
providers:
api_gw_authentication:
id: api_gw_authentication.user_provider
You may customize a specific configuration by doing:

.. code:: yaml
api_gw_authentication:
defaults:
env: user # Available values are: acceptance, intra, production, user
envs:
user:
public: <path-to-public-key-in-pem>
private: <path-to-private-key-in-pem>
security:
providers:
api_gw_authentication:
id: api_gw_authentication.user_provider
However, it is impossible to override existing API Gateway environments (
acceptance, intra and production).
2 changes: 1 addition & 1 deletion docs/pages/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Edit the bundle configuration by editing the file `config/packages/dev/api_gw_au
api_gw_authentication:
defaults:
env: acceptance # Available values are: acceptance, intra, production
env: acceptance # Available values are: acceptance, intra, production, user
security:
providers:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,12 @@ private function prepareDeps(KeyLoaderInterface $keyLoader, HttpClientInterface

$response
->toArray()
->willReturn(['keys' => []]);
->willReturn(['keys' => [
'jwks array structure',
]]);

$keyConverter
->fromJWKStoPEMS([])
->fromJWKStoPEMS(['jwks array structure'])
->willReturn(['foo']);

$httpClient
Expand Down
2 changes: 1 addition & 1 deletion src/ApiGwAuthenticationBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@

use Symfony\Component\HttpKernel\Bundle\Bundle;

class ApiGwAuthenticationBundle extends Bundle
final class ApiGwAuthenticationBundle extends Bundle
{
}
2 changes: 1 addition & 1 deletion src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->arrayNode('defaults')
->children()
->enumNode('env')
->values(['production', 'acceptance', 'intra', 'custom'])
->values(['production', 'acceptance', 'intra', 'user'])
->defaultValue('production')
->isRequired()
->cannotBeEmpty()
Expand Down
11 changes: 11 additions & 0 deletions src/Exception/ApiGwAuthenticationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace EcPhp\ApiGwAuthenticationBundle\Exception;

use Exception;

final class ApiGwAuthenticationException extends Exception
{
}
42 changes: 20 additions & 22 deletions src/Service/KeyLoader/ApiGwKeyLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use EcPhp\ApiGwAuthenticationBundle\Service\KeyConverter\KeyConverterInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\RawKeyLoader;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;

Expand Down Expand Up @@ -83,26 +82,32 @@ public function getSigningKey(): string

public function loadKey($type)
{
$key = $this->getKey($type);
$publicKey = $this->getPublicKey();
$signingKey = $this->getSigningKey();
$passPhrase = $this->getPassphrase();

if (true === file_exists($this->projectDir . $key)) {
return (new RawKeyLoader($this->projectDir . $this->getSigningKey(), $this->projectDir . $this->getPublicKey(), $this->getPassphrase()))->loadKey($type);
}
$key = KeyLoaderInterface::TYPE_PUBLIC === $type ? $publicKey : $signingKey;

if (true === file_exists(__DIR__ . $key)) {
return (new RawKeyLoader(__DIR__ . $this->getSigningKey(), __DIR__ . $this->getPublicKey(), $this->getPassphrase()))->loadKey($type);
}
if ('user' === $this->environment['env']) {
$keyPathCandidates = [
[$this->projectDir, $key], // Look in the App dir,
[__DIR__, $key], // Look in this bundle dir,
['', $key], // Look whereever you want.
];

if (true === file_exists($key)) {
return (new RawKeyLoader($this->getSigningKey(), $this->getPublicKey(), $this->getPassphrase()))->loadKey($type);
}
foreach ($keyPathCandidates as $keyPathCandidateParts) {
if (true === file_exists(implode('', $keyPathCandidateParts))) {
$prefix = current($keyPathCandidateParts);

$keyLoader = new JWKSKeyLoader($this, $this->httpClient, $this->keyConverter);
return (new RawKeyLoader($prefix . $signingKey, $prefix . $publicKey, $passPhrase))
->loadKey($type);
}
}
}

try {
$key = $keyLoader->loadKey($type);
} catch (TransportExceptionInterface $e) {
$key = $this->loadFailsafeKey($type);
$key = (new JWKSKeyLoader($this, $this->httpClient, $this->keyConverter))
->loadKey($type);
} catch (Throwable $e) {
$key = $this->loadFailsafeKey($type);
}
Expand Down Expand Up @@ -143,13 +148,6 @@ private function getFailsafePublicKey(): string
return $this->environment['failsafe'][KeyLoaderInterface::TYPE_PUBLIC];
}

private function getKey(string $type): string
{
return KeyLoaderInterface::TYPE_PUBLIC === $type ?
$this->getPublicKey() :
$this->getSigningKey();
}

private function loadFailsafeKey(string $type): string
{
$key = KeyLoaderInterface::TYPE_PUBLIC === $type ?
Expand Down
31 changes: 25 additions & 6 deletions src/Service/KeyLoader/JWKSKeyLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

namespace EcPhp\ApiGwAuthenticationBundle\Service\KeyLoader;

use EcPhp\ApiGwAuthenticationBundle\Exception\ApiGwAuthenticationException;
use EcPhp\ApiGwAuthenticationBundle\Service\KeyConverter\KeyConverterInterface;
use Exception;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

use function array_key_exists;

final class JWKSKeyLoader implements KeyLoaderInterface
{
private HttpClientInterface $httpClient;
Expand Down Expand Up @@ -44,18 +46,35 @@ public function getSigningKey(): string

public function loadKey($type)
{
// @Todo: Implements for PRIVATE key as well.
$key = $this->keyLoader->getPublicKey();

try {
$jwks = $this->httpClient->request('GET', $this->keyLoader->getPublicKey());
$jwks = $this->httpClient->request('GET', $key);
} catch (TransportExceptionInterface $e) {
throw $e;
}

if ($jwks->getStatusCode() !== 200) {
throw new Exception('Foo');
if (200 !== $statusCode = $jwks->getStatusCode()) {
throw new ApiGwAuthenticationException(
sprintf('Invalid code(%s) thrown while fetching the %s key at %s.', $statusCode, $type, $key)
);
}

$jwksArray = $jwks->toArray();

if (false === array_key_exists('keys', $jwksArray)) {
throw new ApiGwAuthenticationException(
sprintf('Invalid JWKS format of %s key at %s.', $type, $key)
);
}

$keys = $this->keyConverter->fromJWKStoPEMS($jwks->toArray()['keys']);
if ([] === $jwksArray['keys']) {
throw new ApiGwAuthenticationException(
sprintf('Invalid JWKS format of %s key at %s, keys array is empty.', $type, $key)
);
}

return current($keys);
return current($this->keyConverter->fromJWKStoPEMS($jwksArray['keys']));
}
}

0 comments on commit ca1f6f7

Please sign in to comment.