Skip to content
This repository has been archived by the owner on Jan 15, 2024. It is now read-only.

Commit

Permalink
Feature outline
Browse files Browse the repository at this point in the history
  • Loading branch information
kleijnweb committed May 1, 2016
1 parent 9a22e85 commit 3b52e0d
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 56 deletions.
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,59 @@ jwt:

To use *asymmetric keys*, `type` MUST be set to `RS256` or `RS512`. The secret in this case is the public key of the issuer.

### Loading Secrets From An External Resource

Instead of configuring secrets statically, they can also be loaded dynamically, using any data available in the JWT token. Example configuration:

```yml
jwt:
keys:
dynamicKeyName: # Must match 'kid'
issuer: http://api.server1.com/oauth2/token
loader: 'my.loader.di.key'
# type: Defaults to HS256 (HMACSHA256). All options: HS256, HS512, RS256 and RS512

```

The loader must implement `KleijnWeb\JwtBundle\Authenticator\SecretLoader`. A simple example that loads the secret from an ambiguous data store:

```php
use KleijnWeb\JwtBundle\Authenticator\JwtToken;
use KleijnWeb\JwtBundle\Authenticator\SecretLoader;

class SimpleSecretLoader implements SecretLoader
{
/**
* @var DataStore
*/
private $store;

/**
* @param DataStore $store
*/
public function __construct(DataStore $store)
{
$this->store = $store;
}

/**
* @param JwtToken $token
*
* @return string
*/
public function load(JwtToken $token)
{
return $this->store->loadSecretByUsername($token->getPrn());
}
}
```

You could use any information available in the token, such as the `kid`, `alg` or any custom claims. You cannot configure both `secret` and `loader`.

### Integration Into Symfony Security

When enabled, `Authenticator` will be used for any operations referencing a `SecurityDefinition` of type `apiKey` or `oath2`. You will need a *user provider*, which will be passed the
'prn' value when invoking `loadUserByUsername`. Trivial example using 'in memory':
`prn` value when invoking `loadUserByUsername`. Trivial example using 'in memory':

```yml
security:
Expand Down
12 changes: 6 additions & 6 deletions src/Authenticator/Authenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ class Authenticator implements SimplePreAuthenticatorInterface
private $keys = [];

/**
* @param array $keyOptions
* @param JwtKey[] $keys
*/
public function __construct(array $keyOptions)
public function __construct(\ArrayObject $keys)
{
foreach ($keyOptions as $name => $options) {
$this->keys[$name] = new JwtKey($options);
foreach ($keys as $key) {
$this->keys[$key->getId()] = $key;
}
}

Expand Down Expand Up @@ -92,7 +92,7 @@ public function createToken(Request $request, $providerKey)

try {
$token = new JwtToken($tokenString);
$key = $this->getKeyById($token->getKeyId());
$key = $this->getKeyById($token->getKeyId());
$key->validateToken($token);
} catch (\Exception $e) {
throw new AuthenticationException('Invalid key', 0, $e);
Expand All @@ -111,7 +111,7 @@ public function createToken(Request $request, $providerKey)
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$claims = $token->getCredentials();
$user = $userProvider->loadUserByUsername($this->getUsername($claims));
$user = $userProvider->loadUserByUsername($this->getUsername($claims));

return new PreAuthenticatedToken($user, $claims, $providerKey, $user->getRoles());
}
Expand Down
45 changes: 34 additions & 11 deletions src/Authenticator/JwtKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,21 @@ class JwtKey
*/
private $secret;

/**
* @var SecretLoader
*/
private $secretLoader;

/**
* @param array $options
*/
public function __construct(array $options)
{
if (!isset($options['secret'])) {
throw new \InvalidArgumentException("Need a secret to verify tokens");
if (!isset($options['secret']) && !isset($options['loader'])) {
throw new \InvalidArgumentException("Need a secret or a loader to verify tokens");
}
if (isset($options['secret']) && isset($options['loader'])) {
throw new \InvalidArgumentException("Cannot configure both secret and loader");
}
$defaults = [
'kid' => null,
Expand All @@ -77,15 +85,25 @@ public function __construct(array $options)
'type' => $this->type,
'require' => $this->requiredClaims,
];
$options = array_merge($defaults, $options);
$this->issuer = $options['issuer'];
$this->audience = $options['audience'];
$this->type = $options['type'];
$this->minIssueTime = $options['minIssueTime'];
$this->requiredClaims = $options['require'];

$options = array_merge($defaults, $options);
$this->issuer = $options['issuer'];
$this->audience = $options['audience'];
$this->type = $options['type'];
$this->minIssueTime = $options['minIssueTime'];
$this->requiredClaims = $options['require'];
$this->issuerTimeLeeway = $options['leeway'];
$this->id = $options['kid'];
$this->secret = $options['secret'];
$this->id = $options['kid'];
$this->secret = isset($options['secret']) ? $options['secret'] : null;
$this->secretLoader = isset($options['loader']) ? $options['loader'] : null;
}

/**
* @return string
*/
public function getId()
{
return $this->id;
}

/**
Expand All @@ -96,8 +114,13 @@ public function __construct(array $options)
public function validateToken(JwtToken $token)
{
$this->validateHeader($token->getHeader());
$token->validateSignature($this->secret, $this->getSignatureValidator());
$this->validateClaims($token->getClaims());

if (!$this->secretLoader) {
$token->validateSignature($this->secret, $this->getSignatureValidator());
return;
}
$token->validateSignature($this->secretLoader->load($token), $this->getSignatureValidator());
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/Authenticator/JwtToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ public function getKeyId()
return isset($this->header['kid']) ? $this->header['kid'] : null;
}

/**
* @return string|null
*/
public function getPrn()
{
return isset($this->header['prn']) ? $this->header['prn'] : null;
}

/**
* @param string $secret
* @param SignatureValidator $validator
Expand Down
22 changes: 22 additions & 0 deletions src/Authenticator/SecretLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the KleijnWeb\JwtBundle package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace KleijnWeb\JwtBundle\Authenticator;

/**
* @author John Kleijn <john@kleijnweb.nl>
*/
interface SecretLoader
{
/**
* @param JwtToken $token
*
* @return string
*/
public function load(JwtToken $token);
}
20 changes: 19 additions & 1 deletion src/DependencyInjection/KleijnWebJwtExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Reference;

/**
* @author John Kleijn <john@kleijnweb.nl>
Expand All @@ -27,7 +29,23 @@ public function load(array $configs, ContainerBuilder $container)
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');

$container->setParameter('jwt.keys', $config['keys']);
$keysDefinition = new Definition('jwt.keys');
$keysDefinition->setClass('ArrayObject');

foreach ($config['keys'] as $keyId => $keyConfig) {

$keyConfig['kid'] = $keyId;
$keyDefinition = new Definition('jwt.keys.' . $keyId);
$keyDefinition->setClass('KleijnWeb\JwtBundle\Authenticator\JwtKey');

if (isset($keyConfig['loader'])) {
$keyConfig['loader'] = $container->getDefinition($keyConfig['loader']);;
}
$keyDefinition->addArgument($keyConfig);
$keysDefinition->addMethodCall('append', [$keyDefinition]);
}

$container->getDefinition('jwt.authenticator')->addArgument($keysDefinition);

}

Expand Down
1 change: 0 additions & 1 deletion src/Resources/config/services.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
services:
jwt.authenticator:
class: KleijnWeb\JwtBundle\Authenticator\Authenticator
arguments: [%jwt.keys%]
public: false
49 changes: 32 additions & 17 deletions src/Tests/Authenticator/AuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace KleijnWeb\JwtBundle\Tests\Authenticator;

use KleijnWeb\JwtBundle\Authenticator\Authenticator;
use KleijnWeb\JwtBundle\Authenticator\JwtKey;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\User\User;
Expand All @@ -25,7 +26,7 @@ class AuthenticatorTest extends \PHPUnit_Framework_TestCase
const TEST_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleU9uZSJ9.eyJwcm4iOiJqb2huIiwiaXNzIjoiaHR0cDovL2FwaS5zZXJ2ZXIxLmNvbS9vYXV0aDIvdG9rZW4ifQ._jXjAWMzwwG1v5N3ZOEUoLGSINtmwLsvQdfYkYAcWiY';

const JKEY_CLASS = 'KleijnWeb\JwtBundle\Authenticator\JwtKey';

/**
* @var array
*/
Expand All @@ -46,12 +47,26 @@ class AuthenticatorTest extends \PHPUnit_Framework_TestCase

// @codingStandardsIgnoreEnd

/**
* @var JwtKey[]
*/
private $keys;

protected function setUp()
{
$this->keys = new \ArrayObject;
foreach (self::$keyConfig as $keyId => $config) {
$config['kid'] = $keyId;
$this->keys[$keyId] = new JwtKey($config);
}
}

/**
* @test
*/
public function getGetKeysUsingIndexesInConfig()
{
$authenticator = new Authenticator(self::$keyConfig);
$authenticator = new Authenticator($this->keys);

$this->assertInstanceOf(self::JKEY_CLASS, $authenticator->getKeyById('keyOne'));
$this->assertInstanceOf(self::JKEY_CLASS, $authenticator->getKeyById('keyTwo'));
Expand All @@ -62,7 +77,7 @@ public function getGetKeysUsingIndexesInConfig()
*/
public function willGetSingleKeyWhenKeyIdIsNull()
{
$config = self::$keyConfig;
$config = $this->keys;
unset($config['keyTwo']);

$authenticator = new Authenticator($config);
Expand All @@ -76,7 +91,7 @@ public function willGetSingleKeyWhenKeyIdIsNull()
*/
public function willFailWhenTryingToGetKeyWithoutIdWhenThereAreMoreThanOne()
{
$authenticator = new Authenticator(self::$keyConfig);
$authenticator = new Authenticator($this->keys);

$this->assertInstanceOf(self::JKEY_CLASS, $authenticator->getKeyById(null));
}
Expand All @@ -87,7 +102,7 @@ public function willFailWhenTryingToGetKeyWithoutIdWhenThereAreMoreThanOne()
*/
public function willFailWhenTryingToGetUnknownKey()
{
$authenticator = new Authenticator(self::$keyConfig);
$authenticator = new Authenticator($this->keys);

$this->assertInstanceOf(self::JKEY_CLASS, $authenticator->getKeyById('blah'));
}
Expand All @@ -98,7 +113,7 @@ public function willFailWhenTryingToGetUnknownKey()
*/
public function willFailWhenTryingToGetUserNameFromClaimsWithoutPrn()
{
$authenticator = new Authenticator(self::$keyConfig);
$authenticator = new Authenticator($this->keys);

$authenticator->getUsername([]);
}
Expand All @@ -108,7 +123,7 @@ public function willFailWhenTryingToGetUserNameFromClaimsWithoutPrn()
*/
public function canGetUserNameFromClaims()
{
$authenticator = new Authenticator(self::$keyConfig);
$authenticator = new Authenticator($this->keys);

$authenticator->getUsername(['prn' => 'johndoe']);
}
Expand All @@ -118,10 +133,10 @@ public function canGetUserNameFromClaims()
*/
public function authenticateTokenWillSetUserFetchedFromUserProviderOnToken()
{
$claims = ['prn' => 'john'];
$authenticator = new Authenticator(self::$keyConfig);
$anonToken = new PreAuthenticatedToken('foo', $claims, 'myprovider');
$userProvider = $this->getMockBuilder(
$claims = ['prn' => 'john'];
$authenticator = new Authenticator($this->keys);
$anonToken = new PreAuthenticatedToken('foo', $claims, 'myprovider');
$userProvider = $this->getMockBuilder(
'Symfony\Component\Security\Core\User\UserProviderInterface'
)->getMockForAbstractClass();

Expand All @@ -137,10 +152,10 @@ public function authenticateTokenWillSetUserFetchedFromUserProviderOnToken()
*/
public function supportsPreAuthToken()
{
$authenticator = new Authenticator(self::$keyConfig);
$authenticator = new Authenticator($this->keys);

$securityToken = new PreAuthenticatedToken('foo', 'bar', 'myprovider');
$actual = $authenticator->supportsToken($securityToken, 'myprovider');
$actual = $authenticator->supportsToken($securityToken, 'myprovider');
$this->assertTrue($actual);
}

Expand All @@ -150,8 +165,8 @@ public function supportsPreAuthToken()
*/
public function willFailWhenApiKeyNotFoundInHeader()
{
$authenticator = new Authenticator(self::$keyConfig);
$request = new Request();
$authenticator = new Authenticator($this->keys);
$request = new Request();
$authenticator->createToken($request, 'myprovider');
}

Expand All @@ -160,8 +175,8 @@ public function willFailWhenApiKeyNotFoundInHeader()
*/
public function canGetAnonTokenWithClaims()
{
$authenticator = new Authenticator(self::$keyConfig);
$request = new Request();
$authenticator = new Authenticator($this->keys);
$request = new Request();
$request->headers->set('Authorization', 'Bearer ' . self::TEST_TOKEN);
$token = $authenticator->createToken($request, 'myprovider');

Expand Down
Loading

0 comments on commit 3b52e0d

Please sign in to comment.