I am trying to create a hybrid api-token/form login security using Symfony's (2.8.24) guard mechanism.
My security.yml looks like this:
security:
acl:
connection: default
providers:
jwt:
id: app.jwt_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
guard:
entry_point: app.jwt_authenticator
authenticators:
- app.jwt_authenticator
- app.authenticator.form_login
provider: jwt
access_control:
- { path: ^/general/info, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: IS_AUTHENTICATED_FULLY }
So I have a main firewall with two configured authenticators. The entry point is the app.jwt_authenticator, with a fallback to the app.authenticator.form_login.
I want it so that when no token is present, that the app.jwt_authenticator is skipped.
The getCredentials() method of the app.jwt_authenticator looks like this basically:
public function getCredentials(Request $request): ?string
{
$tokenString = $request->headers->get('Authorization');
if (!$tokenString) {
$this->logger->warn('No authorization header');
return null;
}
return $tokenString;
}
So if there is no Authorization header it returns null.
And according to Symfony's documentation:
Get the authentication credentials from the request and return them as
any type (e.g. an associate array). If you return null, authentication
will be skipped.
So I would expect this authenticator to be skipped when there is no Authorization header, but unfortunately that is not the case.
Even though getCredentials() returns null (and I have verified that it does), the start() method gets called instead of skipping this authenticator and moving on to the next.
Is my configuration not correct? Am I misunderstanding the mechanism?
What do I need to do to implement this hybrid?
It turns out the trick was to change the entrypoint to app.authenticator.form_login
Related
My problem is very simple and I am on this since this morning..
When I go to /login, i am directly disconnected, i switch to anonymous status on symfony 5.
This is my login controller :
/**
* #Route("/login", name="app_login")
* #param AuthenticationUtils $authenticationUtils
* #return Response
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('index');
}
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
}
Before I come in my condition (if.. this->getUser()), I am already disconnected.
This is my log out controller :
/**
* #Route("/logout", name="app_logout")
*/
public function logout()
{
// controller can be blank: it will never be executed!
throw new \Exception('Don\'t forget to activate logout in security.yaml');
}
and the security.yaml :
security:
encoders:
App\Entity\User:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
users_in_memory: { memory: null }
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: lazy
provider: users_in_memory
guard:
authenticators:
- App\Security\SecureAuthentication
logout:
path: app_logout
# where to redirect after logout
target: index
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
thanks a lot and have a good day
In your main firewall configuration you have set
main:
provider: users_in_memory
So you are using a provider with no users, therefore you should not be able to login. Try changing it to app_user_provider.
Also, you are missing a pattern on your main firewall. I've never had firewall configurations without pattern so I'm not sure what happens in that case. It is possible it just skips it.
main:
pattern: ^/
You can try these 2 things, but be sure to do them one by one to know which one solved the problem (if this indeed turns out to be the solution) :)
I have a question regarding authentication with JWT and Facebook for a restful API app.
I am using Symfony 4 and for authentication "lexik/jwt-authentication-bundle": "^2.6", to generate jwt tokens based on username and password.
Here is my configuration security.yaml:
security:
encoders:
App\Entity\User: bcrypt
providers:
database:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/api/login
stateless: true
anonymous: true
json_login:
check_path: /api/login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
register:
pattern: ^/api/register
anonymous: true
methods: [POST]
docs:
pattern: ^/api/docs
anonymous: true
methods: [GET]
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
access_control:
- { path: ^/api/docs, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/profile/social-account, roles: ROLE_INFLUENCER }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
I am trying to add also login by facebook but I have no idea how to handle that with JWT, as I want to return after login a JWT token.
Is there any way to handle facebook login and JWT login?
You can have a good idea of how to implement it here : https://github.com/lexik/LexikJWTAuthenticationBundle/issues/295.
Basically after the user accept the Facebook login on your frontend :
send a POST request with the user's token to your custom endpoint (for example login/facebook.
Then you can make an extra request with the user's token to Facebook api to get extra information as firstname, lastname or user's email (for example with https://graph.facebook.com/me/?fields=first_name,last_name,email).
The token needs to have correct permissions to get access to those information (https://developers.facebook.com/docs/facebook-login/permissions/overview).
You can now create a user if the email is not in your database.
Finally just return the token with create method of JWTManager class.
$token = $this->jwtManager->create($user);
By using this logic, it is totally transparent for your frontend whether your user has already loged in with your app or not and it gets a token the same way a user would get with your /login endpoint.
I assume you have an frontend application (React/Angular/Vue...) that consumes your API.
I would handle the "login with Facebook" on the Frontend side. After the user accepts the authentication via Facebook, they will be send back to your frontend app. At that point you have access to all the information you need (e.g. e-mail, first name, last name, etc).
POST that information to a new endpoint (e.g. /api/facebook-login), which handles your registration and/or login process, and then return an JWT token with your newly created user. Lexik makes it possible to manually create tokens:
class ApiController extends Controller
{
public function getTokenUser(UserInterface $user, JWTTokenManagerInterface $JWTManager)
{
// ...
return new JsonResponse(['token' => $JWTManager->create($user)]);
}
}
I am using Gitlab authentication in my current symfony project. I am using following two packages.
omines/oauth2-gitlab
knpuniversity/oauth2-client-bundle
Inside my security.yaml file
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
app_user_provider:
id: App\Security\UserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
guard:
authenticators:
- App\Security\GitlabAuthenticator
access_control:
- { path: ^/connect, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
This is working fine and it is storing cookie PHPSESSID. I need to retrieve the username after successful login.
It is very very simple. Just use $this->getUser().
public function index()
{
/** #var \App\Entity\User $user */
$user = $this->getUser();
return new Response('Well hi there '.$user->getFirstName());
}
UserProvider (that's used in login process) returns a UserInterface, which represent a User. From it you can access the username.
Now, how to retrieve that user?
Depending on Symfony version, and depending where you want to access it, you have several possibilities:
Symfony <3.2
In controller -> you can use $this->getUser()->getUsername();
Symfony >=3.2
In controller -> Inject UserInterface directly as a dependency (read more)
For both cases, if you need the user not in controller, you can inject (based on your symfony version) the TokenStorage or the Security service (can't find which version supports which, for sure Security component is supported in sf5).
If you choose (or are forced) to go with TokenStorage (or its interface), you need to perform the following steps:
getToken
check if the token is not null
retrieve the username from the token (or optionally retrieve the whole user and then retrieve the username from there)
I'm using Symfony 4 and need users to be authenticated before accessing any page. These are my firewall-settings:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
access_denied_handler: App\Security\AccessDeniedHandler
guard:
authenticators:
- App\Security\UserAuthenticator
logout:
path: app_logout # logout-route
target: app_loggedout # where to go after successful logout
And this is the access_control (also in security.yaml):
access_control:
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/loggedout, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: ROLE_USER }
The class UserAuthenticator is called, users are authenticated automatically (because of the username that is provided in the $_SERVER-variable).
But now: I want the user to be redirected to the original URL after the authentication. E.g. if the user wants to go to SITE/test, this should happen automatically after they pass through the UserAuthenticator.
I have managed to achieve this by using the sf_redirect-cookie which Symfony seems to set automatically, it contains something like this:
{"token":"b2282c","route":"app_some_url","method":"GET","controller":{"class":"App\\Controller\\SecurityController","method":"some_method","file":"<PATH>\\src\\Controller\\SecurityController.php","line":23},"status_code":302,"status_text":"Found"}
In my UserAuthenticator I check if the cookie is there, is filled and has a route set and then return a RedirectRespone leading to the route in onAuthenticationSuccess().
The problem is: While this works fine on Apache (dev-system), the sf_redirect-cookie is missing completely on IIS (prod-system). It's not that IIS would not set cookies at all ($_COOKIE['PHPSESSID'] is there), it's just that sf_redirect is missing.
I've been experimenting with a custom subscriber to the redirect-to-login-event, but I'd rather not use a workaround, so the question is: How can I tell Symfony to set the sf_redirect-cookie on IIS, or how do I need to configure IIS to set it?
You can add TargetPathTrait to your authenticator to redirect the user after successful login.
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
....
return new RedirectResponse($this->getTargetPath($request->getSession(), $providerKey));
....
}
I'm trying to build a custom firewall for my Symfony3 website. I've been following the documentation, and was able to get it to work for the main firewall. My desired functionality is the ability for a user to login with their username and password, using Symfony's native classes. Here is my SecurityController:
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class SecurityController extends Controller
{
/**
* #Route("/m/login", name="model_login")
*/
public function loginAction(Request $request)
{
$authenticationUtils = $this->get("security.authentication_utils");
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('Model/login.html.twig', [
"error" => $error,
"lastUsername" => $lastUsername
]);
}
}
As you can see, it is exactly the same as the sample code, except with the routing settings changed. My login form renders fine. I get no errors when I submit the form, and I have my form POSTing to this exact controller. My view works perfectly as I am able to authenticate when my security settings are under "main." Here is my security.yml:
security:
providers:
in_memory:
memory: ~
doctrine_provider:
entity:
class: AppBundle:Model
property: username
encoders:
AppBundle\Entity\Model:
algorithm: bcrypt
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
model_area:
anonymous: ~
provider: doctrine_provider
pattern: ^/m/
form_login:
login_path: model_login
check_path: model_login
access_control:
- { path: ^/m/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/m/, roles: ROLE_MODEL }
My providers and encoders function properly, as exemplified by my success when authenticating under the "main" firewall. However, when I attempt to put my settings under "model_area," submitting my form just redirects me to the login form, with no authentication. I only added the pattern: ^/m/ and - { path: ^/m/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } so I do not block access to my login form. Everything else remained the same (even the route names!).
I have a route /m/model_dashboard, which throws an error, saying "full authentication is required to access this resource." So clearly my access control settings are working properly, however, it is not redirecting to my login form when I attempt to access a protected resource.
Is there something I'm missing? I'm extremely confused as to why the authentication would work under main but not under my custom firewall, which had the same exact settings.
The problem was my main firewall. I did not know that firewalls are determined similarly to routes, top to bottom. Every request was being filed under the "main" firewall (which has no form_login), which was why my login code wasn't working. I removed the main firewall and it is working beautifully. Here is what my updated security.yml looks like now:
security:
providers:
in_memory:
memory: ~
doctrine_provider:
entity:
class: AppBundle:Model
property: username
encoders:
AppBundle\Entity\Model:
algorithm: bcrypt
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
model_area:
anonymous: ~
provider: doctrine_provider
pattern: ^/m/
form_login:
login_path: model_login
check_path: model_login
access_control:
- { path: ^/m/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/m/, roles: ROLE_MODEL }