I have multiple user entities (multiple tables):
App\Entity\Customer
App\Entity\Dealer
How to configured multiple user entity with JWT token?
encoders:
App\Entity\Dealer:
algorithm: bcrypt
App\Entity\Customer:
algorithm: bcrypt
providers:
dealer:
entity:
class: App\Entity\Dealer
property: username
customer:
entity:
class: App\Entity\Customer
property: username
There is nothing JWT specific for having multiple user providers.
If both types of user need to log in to the same firewall (e.g. the same URL pattern), what you need to do is create a chain user provider so the system attempts to fetch a user from each of of the user providers:
providers:
## ... your other providers up here.
all_users:
chain:
providers: ['customer', 'dealer']
You will need to use this provider in the firewall you want to protect:
firewall:
## ... other firewall entries ...
api:
pattern: ^/api
stateless: true
anonymous: true
provider: all_users
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
You should also have separate login paths for each type of users, each with its own specific user provider:
firewall:
###
customer_login:
pattern: ^/auth/login/customer
stateless: true
anonymous: true
provider: customer
json_login:
check_path: /auth/login/customer
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
dealer_login:
pattern: ^/auth/login/dealer
stateless: true
anonymous: true
provider: dealer
json_login:
check_path: /auth/login/dealer
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
Now your "dealers" get their token at /auth/login/dealer, and your "customers" get their token at /auth/login/customer.
Since both dealer's and customer's providers are going to be checked in sequence, if you have users in both tables with the same username, it can be problematic (since the second provider will only be checked if the user is not found in the first one), so you should plan accordingly.
Related
I'm trying to split the authentication of a user from the authentication of an admin.
So I created 2 firewalls and 2 different access controls.
My security.yaml looks like that:
enable_authenticator_manager: true
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
owner_authentication:
entity:
class: App\Entity\Merchant\Owner
property: emailAddress
user_authentication:
entity:
class: App\Entity\User\User
property: emailAddress
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
user:
lazy: true
pattern: ^/admin/login/
provider: user_authentication
user_checker: App\Security\AuthentificableModelChecker
form_login:
provider: user_authentication
default_target_path: app.dashboard.index
use_referer: true
use_forward: false
login_path: app.authorization.admin_login
check_path: app.authorization.admin_login
username_parameter: login[emailAddress]
password_parameter: login[password]
logout:
path: app.authorization.logout
target: app.authorization.admin_login
main:
lazy: true
pattern: ^/
provider: owner_authentication
user_checker: App\Security\AuthentificableModelChecker
form_login:
provider: owner_authentication
default_target_path: app.dashboard.index
use_referer: true
use_forward: false
login_path: app.authorization.login
check_path: app.authorization.login
username_parameter: login[emailAddress]
password_parameter: login[password]
logout:
path: app.authorization.logout
target: app.authorization.login
access_control:
- { path: ^/admin/login, roles: PUBLIC_ACCESS}
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER }
Everything works fine on the main firewall, but when I submit the button using user (admin) firewall, the login page refreshes itself and nothing happens. I don't have any error.
** If I add user(admin) login on the main firewall, then /admin/login will work fine and the other one won't work anymore.
When I call $authenticationUtils->getLastAuthenticationError() I don't get any error. But the validations don't work either.
This is how my Controller looks like:
public function adminLogin(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('app.dashboard.index');
}
$loginForm = $this->createForm(LoginType::class, ['emailAddress' => $authenticationUtils->getLastUsername()]);
return $this->renderForm('app/pages/authorization/admin_login.html.twig', [
'title' => 'Log in',
'login_form' => $loginForm,
'error' => $authenticationUtils->getLastAuthenticationError()?->getMessageKey(),
]);
}
It's the same problem this guy had: https://grafikart.fr/forum/35234 but I can't find any solution for this.
Finally, I found the answer so I'll post it here: https://stackoverflow.com/a/42352112/8003007
What I had to do was to add a context: my_context in both firewalls.
It was a difficult option to identify because it doesn't appear in the official and current Symfony documentation but only in the previous ones, like Symfony 3.4.
As the question states is it possible to create a separate login route for lexik JWT bundle?
I am trying to separate admin and normal user types into separate tables (separate user providers).
Tried to add in routes.yaml:
api_login_check:
path: /api/login
api_login_check_admin:
path: /api/login-admin
controller:
security.yaml:
firewalls:
login:
pattern: ^/api/login
stateless: true
user_checker: App\Security\UserChecker
json_login:
check_path: /api/login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
username_path: email
password_path: password
login_admin:
pattern: ^/api/login-admin
stateless: true
user_checker: App\Security\UserChecker
json_login:
check_path: /api/login-admin
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
username_path: email
password_path: password
I am not sure how or is it even possible for this to be configured.
Regards
I 2 part of applications - first for admins (admin panel) and second API.
For API I want to use another model to check credentials and that retrieve a token.
I thought that it could be achieved by specified check_path route where I can verify the provided data and then return manually token.
But It seems that the application doesn't event go to this endpoint because I haven`t seen any debug message from the response - only 401 error code.
Here is my security.yml config:
security:
encoders:
App\Entity\Security\AdminUser:
algorithm: bcrypt
Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUser:
algorithm: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
jwt:
lexik_jwt: ~
firewalls:
api:
provider: jwt
pattern: ^/api/
stateless: true
anonymous: true
guard:
authenticators:
- 'jwt.token.authenticator'
json_login:
check_path: api.v1.0.token.get
username_path: passwordName
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
context: 'main'
pattern: ^/
form_login:
provider: fos_userbundle
default_target_path: easyadmin
csrf_token_generator: security.csrf.token_manager
logout: true
anonymous: true
access_control:
- { path: ^/api/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/v1.0/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
And here is my action where I tried to debug:
class TokenController extends AbstractController
{
/**
* #Route("/login", name="api.v1.0.token.get", methods={"POST"})
* #param Request $request
*/
public function obtainToken(Request $request, JWTEncoderInterface $encoder, SiteRepository $siteRepository)
{
dd(123); // I don`t see this message - only 401 error
}
}
First, I'm not sure what you're trying to do with your obtainToken function but if you need to either create a token programatically or manipulate / customize its content before returning it, I highly suggest that you have a look at their documentation first as you'll have all the tools to achieve what you want to do:
here for the customization through events
here for manual creation
Otherwise, the bundle will handle that for you.
Now, assuming that you simply want to protect your api with JWT, you'll need to separate your api firewall into two different ones:
First one to login, like so:
login:
pattern: ^/api/login
stateless: true
anonymous: true
json_login:
check_path: /api/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
And don't forget to update the access control to make sure your users can access it anonymously:
- { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
This will allow you to hit /api/login_check with the credentials to authenticate and obtain your token.
Then, protect the rest of your public api by defining the JWT guard authenticator, like so:
api:
pattern: ^/api
stateless: true
# /!\ shouldn't be anonymous: true here
provider: jwt
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
And the access control too:
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
If you need to create an account, you can define your own route and create the token programmatically upon success.
Some other things to mention:
check_path: api.v1.0.token.get : I actually never tried but I don't think you can define a path by its route name like so, you better specify the path directly.
username_path: passwordName : here you're telling the bundle to use 'passwordName' as the username, which sounds weird.
If you want to specify custom identifiers for both username and password, you better use something like this:
username_path: email # (or whatever field you use for the authentication)
password_path: password
How can we configure Symfony 4 Remember me functionality to use email instead of username (as set by default) when creating the cookie and storing it in the browser?
My issue is that by using email to authenticate in S4, the cookie is created with the username instead of the email in its hash, stored in the browser but when S4 check my cookie to see if IS_AUTHENTICATED_REMEMBERED is true, it checks it against the username stored in the DB which doesn’t make sens. It should check it against email. So my remember me functionality doesn’t work.
If I use the username to login, then it works, but that’s not what I want, I’d like my users to log in with their email address.
I’ve configurered the login to work with email instead of the default username behavior, but I can’t have remember me working that way.
I tried the following in my security.yaml
security:
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
user_provider:
entity:
class: App\Entity\User
property: email
in_memory: { memory: ~ }
our_db_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
http_basic: ~
provider: our_db_provider
anonymous: ~
form_login:
login_path: login
check_path: login
default_target_path: dashboard
username_parameter: email
password_parameter: password
remember_me: true
remember_me:
secret: '%kernel.secret%'
lifetime: 31536000 # 1 week in seconds
path: /
domain: ~
secure: true
name: REMEMBERME
remember_me_parameter: remember_me
always_remember_me: true
logout:
path: /logout
target: /
but this doesn’t let you parameter what field remember is using to generate the hash stored in the cookie.
If you’ve managed to set up your login / authentication & remember me working with a field different than username, please share :)
UPDATE: I tried Ahmed answer with the following lines on services but it’s not working:
App\Security\TokenBasedRememberMeServices:
decorates: Symfony\Component\Security\Http\RememberMe\TokenBasedRememberMeServices
it says You have requested a non-existent service "Symfony\Component\Security\Http\RememberMe\TokenBasedRememberMeServices”.
The problem that security component use getUsername() getter to build the token https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php#L74
So we need to overide the service responosible for creating the remember-me cookie, wish is security.authentication.rememberme.services.simplehash.class.
Firstable: Update the onLoginSuccess method L74 so it uses the email instead of the username.
namespace App\Security;
...
class TokenBasedRememberMeServices extends AbstractRememberMeServices
{
....
protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token)
{
...
$value = $this->generateCookieValue(\get_class($user), $user->getEmail(), $expires, $user->getPassword());
...
}
...
Second: Register your class as a service.
App\Security\TokenBasedRememberMeServices:
decorates: 'security.authentication.rememberme.services.simplehash.class'
Or you can flow the contract under UserInterface It define and returns the username used to authenticate the user. In our case it's the email property.
public function getUsername()
{
return $this->email;
}
I don't have enough SO reputation to add a comment for answer upon, so I write it here.
Instead of decorating service:
security.authentication.rememberme.services.simplehash.class
Decorate:
security.authentication.rememberme.services.simplehash
It works on Symfony 4.4
I have two firewalls for auth and api. But i want to combine them and be able to check if there is some user in controller.
firewalls:
user:
pattern: ^/api/auth
stateless: true
anonymous: true
json_login:
check_path: /api/auth/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
access_control:
- { path: ^/api/auth, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
What I want to do is to combine firewalls to one and allow any user everythere, but still be available to identify user if JWT passwed.
Is it possible?
In my opinion,to achieve this you will need only the firewall matching ^/api pattern and continue using guard authenticator. Then, inside your authenticator, check for the exact route requested and chose whether to continue with normal guard authenticator flow or use a custom logic to implement json login. But it is a little bit dirty even if guard permits you to implement your own logic. Keeping separated firewalls sounds better.
I think it is better to keep separate firewalls, as mentioned already, but in case you absolutely need to keep your application open to everybody and check if the user is authenticated in your controller, you can refer to symfony's documentation and use something like:
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');