Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a JWTUserProvider allowing to trust in the JWT #278

Merged
merged 1 commit into from
Dec 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions DependencyInjection/Security/Factory/JWTUserFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory;

use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUser;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;

/**
* Creates the `lexik_jwt` user provider.
*
* @internal
*
* @author Robin Chalas <[email protected]>
*/
final class JWTUserFactory implements UserProviderFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config)
{
$definition = $container->setDefinition($id, new DefinitionDecorator('lexik_jwt_authentication.security.jwt_user_provider'));
$definition->replaceArgument(0, $config['class']);
}

public function getKey()
{
return 'lexik_jwt';
}

public function addConfiguration(NodeDefinition $node)
{
$node
->children()
->scalarNode('class')
->cannotBeEmpty()
->defaultValue(JWTUser::class)
->validate()
->ifTrue(function ($class) {
return !(new \ReflectionClass($class))->implementsInterface(JWTUserInterface::class);
})
->thenInvalid('The %s class must implement '.JWTUserInterface::class.' for using the "lexik_jwt" user provider.')
->end()
->end()
->end()
;
}
}
5 changes: 3 additions & 2 deletions LexikJWTAuthenticationBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Lexik\Bundle\JWTAuthenticationBundle;

use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTFactory;
use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Security\Factory\JWTUserFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
Expand All @@ -24,7 +25,7 @@ public function build(ContainerBuilder $container)
/** @var SecurityExtension $extension */
$extension = $container->getExtension('security');

// BC 1.x, to be removed in 3.0
$extension->addSecurityListenerFactory(new JWTFactory());
$extension->addUserProviderFactory(new JWTUserFactory());
$extension->addSecurityListenerFactory(new JWTFactory()); // BC 1.x, to be removed in 3.0
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ The bulk of the documentation is stored in the [`Resources/doc`](Resources/doc/i
* [JWT encoder service customization](Resources/doc/5-encoder-service.md)
* [Extending JWTTokenAuthenticator](Resources/doc/6-extending-jwt-authenticator.md)
* [Creating JWT tokens programmatically](Resources/doc/7-manual-token-creation.md)
* [A database-less user provider](Resources/doc/8-jwt-user-provider.md)

Support
-------
Expand Down
4 changes: 4 additions & 0 deletions Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@
<service id="lexik_jwt_authentication.extractor.cookie_extractor" class="Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\CookieTokenExtractor">
<argument /> <!-- Name -->
</service>
<!-- JWT User Provider -->
<service public="false" id="lexik_jwt_authentication.security.jwt_user_provider" class="Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserProvider">
<argument />
</service>
</services>

</container>
15 changes: 13 additions & 2 deletions Resources/doc/1-configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,25 @@ Security configuration
# app/config/security.yml
security:
# ...
providers:
# ...
jwt: # optional
lexik_jwt:
class: AppBundle\Security\JWTUser
firewalls:
# ...
api:
# ...
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
provider: jwt # optional
```

For more details about the `lexik_jwt_authentication.jwt_token_authenticator` service and how to
customize it, see ["Extending the Guard JWTTokenAuthenticator"](6-extending-jwt-authenticator.md)
##### authenticator

For more details about the `lexik_jwt_authentication.jwt_token_authenticator` service and how to customize it, see ["Extending the Guard JWTTokenAuthenticator"](6-extending-jwt-authenticator.md)

##### database-less user provider

For a database-less authentication (i.e. trusting into the JWT data instead of reloading the user from the database), see ["A database less user provider"](8-jwt-user-provider).
82 changes: 82 additions & 0 deletions Resources/doc/8-jwt-user-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
A database-less user provider
=============================

From [jwt.io](https://jwt.io/introduction):

> Self-contained: The payload contains all the required information about the user, avoiding the need to query the database more than once.
> https://jwt.io/introduction

A JWT is _self-contained_, meaning that we can trust into its payload for processing the authentication.
In a nutshell, there should be no need for loading the user from the database when authenticating a JWT Token,
the database should be hit only once for delivering the token.

That's why we decided to provide an user provider which is able to create User instances from the JWT payload.

Configuring the user provider
-----------------------------

To work, the provider just needs a few lines of configuration:

```yaml
# app/config/security.yml
security:
providers:
jwt:
lexik_jwt: ~
```

What does it change?
--------------------

Now that the provider is configured, it will automatically be used by the `JWTGuardAuthenticator` at the time to authenticate a token.
Instead of loading the user from a "datastore" (i.e. memory or any database engine), a `JWTUserInterface` instance will be created from the JWT payload, will be cached for a request and be authenticated.
We provide a simple `JWTUser` class implementing this interface, which is used by default when configuring the provider.

Can I use my own user class?
----------------------------

Of course, you can. You just need to make your user class implement the `JWTUserInterface` interface.
This interface contains only a `createFromPayload()` _named constructor_ which takes the user's username and
the JWT token payload as arguments and returns an instance of the class.

##### Example of implementation

```php
namespace AppBundle\Security;

final class User implements JWTUserInterface
{
// Your own logic

public function __construct($username, array $roles, $email)
{
$this->username = $username;
$this->roles = $roles;
$this->email = $email;
}

public static function createFromPayload($username, array $payload)
{
return new self(
$username,
$payload['roles'] // Added by default
$payload['email'] // Custom
);
}
}
```

_Note_: You can extend the default `JWTUser` class if that fits your needs.

##### Configuration

```yaml
# app/config/security.yml
providers:
# ...
jwt:
lexik_jwt:
class: AppBundle\Security\User
```

And voilà!
1 change: 1 addition & 0 deletions Resources/doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,4 @@ The following documents are available:
- [JWT encoder service customization](5-encoder-service.md)
- [Extending JWTTokenAuthenticator](6-extending-jwt-authenticator.md)
- [Creating JWT tokens programmatically](7-manual-token-creation.md)
- [A database-less user provider](8-jwt-user-provider.md)
23 changes: 21 additions & 2 deletions Security/Guard/JWTTokenAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\PreAuthenticationJWTUserToken;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserProvider;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -145,7 +146,7 @@ public function getUser($preAuthToken, UserProviderInterface $userProvider)
$identity = $payload[$identityField];

try {
$user = $userProvider->loadUserByUsername($identity);
$user = $this->loadUser($userProvider, $payload, $identity);
} catch (UsernameNotFoundException $e) {
throw new UserNotFoundException($identityField, $identity);
}
Expand Down Expand Up @@ -214,7 +215,7 @@ public function createAuthenticatedToken(UserInterface $user, $providerKey)
$preAuthToken = $this->preAuthenticationTokenStorage->getToken();

if (null === $preAuthToken) {
throw new \RuntimeException('Unable to return an post authentication token since there is no pre authentication token in %s::$preAuthenticationTokenStorage');
throw new \RuntimeException('Unable to return an authenticated token since there is no pre authentication token.');
}

$authToken = new JWTUserToken($user->getRoles(), $user, $preAuthToken->getCredentials(), $providerKey);
Expand Down Expand Up @@ -246,4 +247,22 @@ protected function getTokenExtractor()
{
return $this->tokenExtractor;
}

/**
* Loads the user to authenticate.
*
* @param UserProviderInterface $userProvider An user provider
* @param array $payload The token payload
* @param string $identity The key from which to retrieve the user "username"
*
* @return UserInterface
*/
protected function loadUser(UserProviderInterface $userProvider, array $payload, $identity)
{
if ($userProvider instanceof JWTUserProvider) {
return $userProvider->loadUserByUsername($identity, $payload);
}

return $userProvider->loadUserByUsername($identity);
}
}
71 changes: 71 additions & 0 deletions Security/User/JWTUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Security\User;

/**
* User class for which to create instances from JWT tokens.
*
* Note: This is only useful when using the JWTUserProvider (database-less).
*
* @author Robin Chalas <[email protected]>
*/
class JWTUser implements JWTUserInterface
{
private $username;
private $roles;

public function __construct($username, array $roles = [])
{
$this->username = $username;
$this->roles = $roles;
}

/**
* {@inheritdoc}
*/
public static function createFromPayload($username, array $payload)
{
if (isset($payload['roles'])) {
return new self($username, (array) $payload['roles']);
}

return new self($username);
}

/**
* {@inheritdoc}
*/
public function getUsername()
{
return $this->username;
}

/**
* {@inheritdoc}
*/
public function getRoles()
{
return $this->roles;
}

/**
* {@inheritdoc}
*/
public function getPassword()
{
}

/**
* {@inheritdoc}
*/
public function getSalt()
{
}

/**
* {@inheritdoc}
*/
public function eraseCredentials()
{
}
}
18 changes: 18 additions & 0 deletions Security/User/JWTUserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;

interface JWTUserInterface extends UserInterface
{
/**
* Creates a new instance from a given JWT payload.
*
* @param string $username
* @param array $payload
*
* @return JWTUserInterface
*/
public static function createFromPayload($username, array $payload);
}
56 changes: 56 additions & 0 deletions Security/User/JWTUserProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Lexik\Bundle\JWTAuthenticationBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

/**
* JWT User provider.
*
* @author Robin Chalas <[email protected]>
*/
final class JWTUserProvider implements UserProviderInterface
{
private $class;
private $cache = [];

/**
* @param string $class The {@link JWTUserInterface} implementation FQCN for which to provide instances
*/
public function __construct($class)
{
$this->class = $class;
}

/**
* {@inheritdoc}
*
* @param array $payload The JWT payload from which to create an instance
*
* @return JWTUserInterface
*/
public function loadUserByUsername($username, array $payload = [])
{
$class = $this->class;

if (isset($this->cache[$username])) {
return $this->cache[$username];
}

return $this->cache[$username] = $class::createFromPayload($username, $payload);
}

/**
* {@inheritdoc}
*/
public function supportsClass($class)
{
return $class === $this->class || (new \ReflectionClass($class))->implementsInterface(JWTUserInterface::class);
}

public function refreshUser(UserInterface $user)
{
// noop
}
}
Loading