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
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.
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.
I am writing this because previous answers to Using multiple firewalls cause ERR_TOO_MANY_REDIRECTS in Symfony 2 have not been helpful. "Main" firewall seem to work just fine, "Admin" is the one causing problems. Every time I try to enter the path "http://localhost:8000/admin" it redirects to"http://localhost:8000/admin_login" as it should, but goes into redirect loop and crashes with the error named above.
security.yaml
security:
encoders:
App\Entity\User:
algorithm: bcrypt
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
chain_provider:
chain:
providers: [in_memory, db_provider]
in_memory:
memory:
users:
theadmin:
password: iamadmin
roles: 'ROLE_SUPER_ADMIN'
db_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
admin:
pattern: /admin
anonymous: ~
form_login:
username_parameter: _username
login_path: /admin_login
check_path: /admin_login
provider: in_memory
default_target_path: admin
logout:
path: /admin_logout
target: /
main:
pattern: /
anonymous: ~
form_login:
username_parameter: _email
login_path: /login
check_path: /login
provider: db_provider
default_target_path: welcome
logout:
path: /logout
target: /
access_control:
- { path: ^/welcome, roles: ROLE_USER }
- { path: ^/admin, roles: ROLE_SUPER_ADMIN }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin_login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
AdminSecurityController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class AdminSecurityController extends AbstractController
{
/**
* #Route("/admin_login", name="admin_login")
*/
public function admin_login(Request $request, AuthenticationUtils $utils)
{
$error = $utils->getLastAuthenticationError();
$auth_checker = $this->get('security.authorization_checker');
if ($auth_checker->isGranted('ROLE_SUPER_ADMIN')) {
return $this->render('admin/dashboard.html.twig', [
'controller_name' => 'AdminController',
]);
} else{
return $this->render('admin_security/admin_login.html.twig', [
'error' => $error
]);
}
}
/**
* #Route("/admin_logout", name="admin_logout")
*/
public function admin_logout()
{
}
}
Access control entries are analyzed from the top to the bottom. So, you need to place the ^/admin_login entry before the ^/admin.
Imagine how the security component is currently set:
You visit the login form then press submit
You (guest) are redirected to the /admin_login path
Security component goes through the entries and matches the /admin_login to the ^/admin entry
Because it requires ROLE_SUPER_ADMIN, you thus end up with a loop
Remember to clear the cache afterwards.
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');
I have seen some posts in this regard, specifically Auto-Login via URL with symfony2 (which I can't use because I do not know what needs to go into $request) and Automatic post-registration user authentication. I tried the latter, but it is not logging in.
Code from security.yml
firewalls:
admin_login_firewall:
pattern: ^/admin/login$
anonymous: ~
admin:
pattern: ^/admin
form_login:
login_path: admin_login
check_path: admin_login_check
default_target_path: admin_dashboard
logout:
path: admin_logout
target: admin_login
http_basic:
realm: "Licensing Admin Portal"
member_login_firewall:
pattern: ^/members/login$
anonymous: ~
members:
pattern: ^/members
form_login:
login_path: member_login
check_path: member_login_check
default_target_path: member_dashboard
logout:
path: member_logout
target: home
http_basic:
realm: "Licensing Member Portal"
encoders:
Pmb\LicensingBundle\Entity\User: plaintext
access_control:
- { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/members/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/members/, roles: ROLE_USER }
Code snippet from Controller function saveUserAction():
... // Code preceding this point creates user entity and sets all fields
$em = $this->getDoctrine()->getManager();
if (empty($data['user_id'])) $em->persist($user);
$em->flush();
if (!empty($organization)) $this->linkOrganizationUserAction($organization,$user, true);
if (isset($data['registering']))
{
$token = new UsernamePasswordToken($user, null, 'members', $user->getRoles());
$this->get('security.context')->setToken($token);
$this->get('session')->set('_security_main',serialize($token));
}
return $this->createJsonResponse($user);
I am trying to log in to the members firewall. I do not know enough about this to troubleshoot. Most of this is just copy/paste/edit. Any help / explanation would be greatly appreciated. I also read the article under Symfony2 auto-login after registration, but I do not see the significance of this, as I do not need have users logged in accross different firewalls, and just need the user logged in under the members firewall.
One thing that I DID notice, is that the user entity is having its salt field populated when persiting to the database, even though I did not set a salt and I cannot see anything auto-setting the salt. I am not yet using the salt as I am not yet encrypting my passwords (just trying to get it working with plain text passwords first), and when trying to log in with a created user (which does get created, just not getting logged in) I cannot log in unless I clear the salt on the user. I don't know if this has anything to do with the fact that the auto-login is not working.
The issue here was with the line $this->get('session')->set('_security_main',serialize($token));. The "_main" of "security_main" is also the firewall that you are authenticating against. So it should have been "_security_members". After changing it the code worked as is.