Reputation: 301
Is there any way to authenticate and authorize user in symfony from another symfony apps on different domain?
So here is my scenario, I have a symfony apps mainly used as user related service (user, roles, student name, etc) on domain example1.dev, this site provide api endpoint to login with JWTLexicBundle (on example1.dev/api/authentication), and provide endpoint to get student data (roles, etc) on eample1.dev/api/whoami by sending that endpoint a valid JWT Token Header. The JWT token from example1.dev is being used as an authentication token on various front end site (example3.dev, example4.dev which built on top react)
Then, I have the second symfony apps used for class with no user/authentication method yet on example2.dev.
How can I use example1.dev user data and roles to be implemented at example2.dev? I mean the authentication and authorization on example2.dev using example1.dev api service, like checking whether a request provide a JWT Token, if so, check to example1.dev whether the token is valid, if it's valid, get the user data from example1.dev. Is that possible?
Upvotes: 2
Views: 2970
Reputation: 2454
Sharing jwt tokens across different apps:
Set up the lexikjwt bundle in your auth app using the default documentation (I use the standard doctrine user entity config)
Install the lexikjwt bundle in your app that accepts the token
Ensure that the public key is the same as the one in your auth app
In security.yaml add the provider
providers:
users:
lexik_jwt: ~
The jwt bundle will simply decode the token using the pub key and all the claims like (username and roles) are there
If you need custom claims like IP address add them as follows:
Upvotes: 0
Reputation: 4363
It's definitely possible. Let's take a look at a possible implementation, hope it would be a good starting point to design a solution, tailored for your requirements. In the first Symfony app (let's call it User Service), we would have login functionality to exchange credentials to JWT token, refresh JWT token, etc. After obtaining a JWT token, a user is able to call other services and sign requests with a JWT token. On other services, we need to decode the JWT token (it would check if it's valid and not expired). In order to do so, we should have LexikJWTAuthenticationBundle dependency in all services, but with a different configuration. For User Service, we would have both public and secret keys in order to generate a JWT token and validate it, while other services require only a public key in order to validate a JWT token and decode it to read a payload.
User Service config.yml configuration.
# JWT Configuration
lexik_jwt_authentication:
secret_key: '%jwt_private_key%'
public_key: '%jwt_public_key%'
pass_phrase: '%jwt_key_pass_phrase%'
token_ttl: '%jwt_token_ttl%'
user_identity_field: email
Other service config.yml configuration.
# JWT Configuration
lexik_jwt_authentication:
public_key: '%jwt_public_key%'
token_ttl: '%jwt_token_ttl%'
user_identity_field: email
After that, we may want to create a small shared library to share possible roles. Or just duplicate roles for all services. Roles are just strings, so whatever approach would work. We also may want to have a shared user provider and UserInterface implementation, but it's completely optional. Inside of JWT token payload, we may pass available roles of a user, which would be filled by User Service when a user would be authenticated and a JWT token would be generated. This approach enables other services to read a JWT token payload and get user roles to check user authorization against requested resources.
Sample security.yml User Service configuration.
security:
encoders:
SharedAuthLibrary\Security\User:
algorithm: bcrypt
App\Entity\User:
algorithm: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
providers:
service:
id: shared_auth_library_jwt_user_provider
login:
id: app.user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
refresh:
pattern: ^/api/token/refresh
stateless: true
anonymous: true
login:
pattern: ^/api/login$
stateless: true
anonymous: true
provider: login
json_login:
check_path: /api/login
username_path: email
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
anonymous: true
provider: service
guard:
entry_point: lexik_jwt_authentication.jwt_token_authenticator
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
access_control:
- { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/token/refresh, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
Sample security.yml oconfiguration on other services.
security:
encoders:
SharedAuthLibrary\Security\User:
algorithm: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
providers:
service:
id: shared_auth_library_jwt_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api
stateless: true
anonymous: true
provider: service
guard:
entry_point: lexik_jwt_authentication.jwt_token_authenticator
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
access_control:
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
In order to have required data for authorisation on other services, we need to enrich JWT payload with id, email, and roles. Let's create a JWT created event listener in our User Service.
<?php
declare(strict_types=1);
namespace App\EventListener;
use SharedAuthLibrary\Security\User;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SecurityEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
Events::JWT_CREATED => 'onJwtCreated',
];
}
public function onJwtCreated(JWTCreatedEvent $event): void
{
/** @var User $user */
$user = $event->getUser();
$payload = $event->getData();
$payload['id'] = $user->getId();
$payload['roles'] = $user->getRoles();
$payload['email'] = $user->getUsername();
$payload['exp'] = (new \DateTimeImmutable())->getTimestamp() + 86400;
$event->setData($payload);
}
}
Let's take a look at shared library. We need a payload container to pass payload to our user provider in order to create a authenticated User with all fields from payload we need to check authorization to resources and things like that.
<?php
declare(strict_types=1);
namespace SharedAuthLibrary\Security;
class JwtPayloadContainer
{
private array $payload = [];
public function setPayload(array $payload): void
{
if (empty($this->payload)) {
$this->payload = $payload;
}
}
public function getPayload(): array
{
return $this->payload;
}
}
And a listener to actually use the payload container.
<?php
declare(strict_types=1);
namespace SharedAuthLibrary\Listener;
use SharedAuthLibrary\Security\JwtPayloadContainer;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
class JwtPayloadListener
{
private JwtPayloadContainer $jwtPayloadContainer;
public function __construct(JwtPayloadContainer $jwtPayloadContainer)
{
$this->jwtPayloadContainer = $jwtPayloadContainer;
}
public function onJWTDecoded(JWTDecodedEvent $event): void
{
$payload = $event->getPayload();
$this->jwtPayloadContainer->setPayload($payload);
}
}
Our user provider may look something like this.
<?php
declare(strict_types=1);
namespace SharedAuthLibrary\Security;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class JwtUserProvider implements UserProviderInterface
{
private JwtPayloadContainer $jwtPayloadContainer;
public function __construct(JwtPayloadContainer $jwtPayloadContainer)
{
$this->jwtPayloadContainer = $jwtPayloadContainer;
}
public function loadUserByUsername($username): User
{
$payload = $this->jwtPayloadContainer->getPayload();
return new User($payload['id'], $payload['email'], $payload['roles']);
}
public function refreshUser(UserInterface $user): User
{
if (!$user instanceof User) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', \get_class($user))
);
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class): bool
{
return User::class === $class;
}
}
Sample shared User model.
<?php
declare(strict_types=1);
namespace SharedAuthLibrary\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
private string $id;
private string $username;
private array $roles;
private string $password;
private string $salt;
public function __construct(
string $id,
string $email,
array $roles,
string $password = '',
string $salt = '',
) {
$this->id = $id;
$this->roles = $roles;
$this->username = $email;
$this->password = $password;
$this->salt = $salt;
}
public function getId(): string
{
return $this->id;
}
public function getUsername(): string
{
return $this->username;
}
public function getRoles(): array
{
return $this->roles;
}
public function getPassword(): string
{
return $this->password;
}
public function getSalt(): string
{
return $this->salt;
}
public function eraseCredentials()
{
// TODO: Implement eraseCredentials() method.
}
}
Finally, register the subsriber and listener to service.yml:
shared_auth_library_jwt_user_provider:
class: App\SharedAuthLibrary\Security\JwtUserProvider
App\EventListener\SecurityEventSubscriber:
tags:
- { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated}
App\SharedAuthLibrary\Listener\JwtPayloadListener:
tags:
- { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_decoded, method: onJWTDecoded}
Upvotes: 4