I'm working on adding Oauth login to a Symfony2 site. I have the bundle working and configured with paypal and Facebook. I have remember me working. My issue is when a user comes back via remember me and tries to reauthenticate via oauth it tells me the accounts were connected fully but doesn't authenticate me at all. Re logging in with a user name and password works fine.
Config.yml
hwi_oauth:
# name of the firewall in which this bundle is active, this setting MUST be set
firewall_name: main
connect:
account_connector: app.provider.oauth
confirmation: true
resource_owners:
facebook:
type: facebook
client_id: %facebook_client_id%
client_secret: %facebook_client_secret%
scope: "email, public_profile"
infos_url: "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email,picture.type(large)"
paths:
email: email
options:
csrf: true
paypal:
type: paypal
client_id: %paypal_client_id%
client_secret: %paypal_client_secret%
scope: 'openid profile email'
options:
csrf: true
Security.yml
firewalls:
main:
pattern: ^/
anonymous: ~
oauth:
failure_path: /login
login_path: /login
check_path: /login
provider: fos_userbundle
remember_me: true
always_use_default_target_path: false
default_target_path: /login
resource_owners:
facebook: "/external-login/check-facebook"
paypal: "/external-login/check-paypal"
amazon: "/external-login/check-amazon"
oauth_user_provider:
service: app.provider.oauth
remember_me:
key: %secret%
lifetime: 31536000
path: /
domain: ~
always_remember_me: true
form_login:
login_path: /login
check_path: /login_check
success_handler: authentication_handler
failure_handler: authentication_handler
csrf_provider: form.csrf_provider
remember_me: true
logout: true
anonymous: true
switch_user: { role: ROLE_ALLOWED_TO_SWITCH, parameter: _new_user }
remember_me:
key: %secret%
lifetime: 31536000
path: /
domain: ~
always_remember_me: true
Routing.yml
hwi_oauth_redirect:
resource: "#HWIOAuthBundle/Resources/config/routing/redirect.xml"
prefix: /connect
hwi_oauth_login:
resource: "#HWIOAuthBundle/Resources/config/routing/login.xml"
prefix: /external-login/
hwi_oauth_connect:
resource: "#HWIOAuthBundle/Resources/config/routing/connect.xml"
prefix: /external-login/
facebook_login:
pattern: /external-login/check-facebook
paypal_login:
pattern: /external-login/check-paypal
Thanks!
My Own Answer (Solved)
so, I finally found the solution to my problem, maybe it will help others to solve it.
This is why the token failed to be authenticated.
For a returning user, on my home page, I called the is_granted('ROLE_ADMIN'), so the token was authenticated by the method authenticate of the AuthenticationManager, which was called in the AuthorizationChecker, as you can see below
final public function isGranted($attributes, $object = null)
{
if (null === ($token = $this->tokenStorage->getToken())) {
throw new AuthenticationCredentialsNotFoundException('The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.');
}
if ($this->alwaysAuthenticate || !$token->isAuthenticated()) {
$this->tokenStorage->setToken($token = $this->authenticationManager->authenticate($token));
}
if (!\is_array($attributes)) {
$attributes = array($attributes);
}
return $this->accessDecisionManager->decide($token, $attributes, $object);
}
The AuthenticationManager then uses the method authenticate of the OAuthProvider, which is one of its dependencies.This method looks like this:
* {#inheritdoc}
*/
public function authenticate(TokenInterface $token)
{
if (!$this->supports($token)) {
return;
}
// fix connect to external social very time
if ($token->isAuthenticated()) {
return $token;
}
/* #var OAuthToken $token */
$resourceOwner = $this->resourceOwnerMap->getResourceOwnerByName($token->getResourceOwnerName());
$oldToken = $token->isExpired() ? $this->refreshToken($token, $resourceOwner) : $token;
$userResponse = $resourceOwner->getUserInformation($oldToken->getRawToken());
try {
$user = $this->userProvider->loadUserByOAuthUserResponse($userResponse);
} catch (OAuthAwareExceptionInterface $e) {
$e->setToken($oldToken);
$e->setResourceOwnerName($oldToken->getResourceOwnerName());
throw $e;
}
if (!$user instanceof UserInterface) {
throw new AuthenticationServiceException('loadUserByOAuthUserResponse() must return a UserInterface.');
}
$this->userChecker->checkPreAuth($user);
$this->userChecker->checkPostAuth($user);
$token = new OAuthToken($oldToken->getRawToken(), $user->getRoles());
$token->setResourceOwnerName($resourceOwner->getName());
$token->setUser($user);
$token->setAuthenticated(true);
$token->setRefreshToken($oldToken->getRefreshToken());
$token->setCreatedAt($oldToken->getCreatedAt());
return $token;
}
As you can see, for a returning user, the OAuthToken is expired, so the OAuthProvider will try to refresh it
$oldToken = $token->isExpired() ? $this->refreshToken($token, $resourceOwner) : $token;
My issue was that my previous OAuthToken had a null refresh_token option. Although I had set the access_type to offline on my config.yml for the HWIOAuthBundle, it would not work, the refresh_token would still be null
hwi_oauth
resource_owners:
google:
type: google
client_id: "%google_app_id%"
client_secret: "%google_app_secret%"
scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/user.birthday.read"
options:
access_type: offline
prompt: consent
So why was my refresh_token always null? I fell on this other question on stack over flow
Not receiving Google OAuth refresh token
The answers tell you exactly why google does not send you a refresh_token.
Once you used the OAuth authentication without the refresh_token, google, will never send you a refresh token, unless you prompt the APi for consent.
This is why I added the prompt: consent to my configuration file.
So why did all of this generate a bug?
If I don't get the refresh_token from the google API, the userResponse of the OAuthProvider, which is based on the google API response, would have a null username attribute.
$userResponse = $resourceOwner->getUserInformation($oldToken->getRawToken());
Therefore, when the OAuthProvider tries to load a user thanks to the OAuthUserProvider, the username would be null so the OAuthUserProvider would not find the user.
$user = $this->userProvider->loadUserByOAuthUserResponse($userResponse);
Therefore, I needed to get the refresh_token back from google in order to be able to have a correct userResponse with a non-empty username attribute.
Changing my configuration for hwi_oauth and adding these two options to google ressource_owners solved my problem
options:
access_type: offline
prompt: consent
Related
How to refresh logged in user role e.g. when it has been changed by admin user? I've found the always_authenticate_before_granting security option (it's not included in Symfony 4 documentation) and set it to true.
security.yaml:
security:
always_authenticate_before_granting: true
encoders:
App\Entity\Main\User:
algorithm: bcrypt
providers:
app_user_provider:
entity:
class: App\Entity\Main\User
property: email
role_hierarchy:
ROLE_ADMIN: ROLE_USER
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
guard:
authenticators:
- App\Security\LoginFormAuthenticator
form_login:
login_path: login
check_path: login
logout:
path: logout
target: homepage
remember_me:
secret: '%kernel.secret%'
path: /
access_control:
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/.*, roles: ROLE_USER }
but it doesn't take effect.
UPDATE
I've created onRequest subscriber:
class RequestSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onRequest'
];
}
public function onRequest(GetResponseEvent $event): void
{
if (!$event->isMasterRequest()) {
return;
}
if(!$token = $this->tokenStorage->getToken()) return;
$sessionUser = $token->getUser();
if ($sessionUser instanceof User) {
$this->tokenStorage->setToken(new PostAuthenticationGuardToken($sessionUser, 'main', $sessionUser->getRoles()));
}
}
}
and now I can refresh the updated roles on every request, but comparing sessionUser to databaseUser is pointless, because the sessionUser always contains newly updated roles, though in Symfony Profiler > Security Token are listed the old ones (in case when I don't set the new token, of course).
Tl;dr I'm afraid you will have to introduce a mechanism of your own in order to make this work.
The session token is stored inside the user's session. This will have quite an impact on your application's performance, because each time a call to the database will be required in order to check if the role had changed.
So you will need a request listener which will compare database role with current user role, and if it is not same, replace the token in the session, this time with new role list, eg. (pseudo code):
sessionUser = tokenStorage.token.user
databaseUser = userRepository.find(sessionUser.id)
if sessionUser.roles !== databaseUser.roles
tokenStorage.setToken(new PostAuthenticationGuardToken(…))
or use a cache as a flag carrier to notify the user about the change. This method is going to be much quicker for sure.
sessionUser = tokenStorage.token.user
// of course the flag has to be set when the user's role gets changed
cacheToken = 'users.role_reload.' . sessionUser.id
if cache.has(cacheToken)
databaseUser = userRepository.find(sessionUser.id)
tokenStorage.setToken(new PostAuthenticationGuardToken(…))
cache.remove(cacheToken)
Either way the user has to ask the application has there been role change, on each request.
In my api I'm trying to authenticate a user using LdapToolsBundle, FOSUserBundle and LexikJWTAuthenticationBundle. Doing things step by step and following the integration docs for fosuser and ldaptools and later the jwt docs I manage to acomplish the following:
FosUserBundle + LdapToolsBundle was successfull
Api Integration + FosUserBundle + LdapToolsBundle was successfull
Jwt + FosUserBundle + LdapToolsBundle on Api failed.
the problem is that I just can only log in against my database but not ldap.
in my database I have one user record which I created with fos command and any password(making shure the authentication is on ldap and not fosuser). So far so good. but once instroduced JWT the authentication is made by fosuser instead of ldap authentication guard. When I change the password with fos command I can get the token with out problems.
this is my config:
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
ldap:
id: ldap_tools.security.user.ldap_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api_login:
pattern: ^/login
stateless: true
provider: fos_userbundle
anonymous: true
form_login:
check_path: /login
require_previous_session: false
username_parameter: username
password_parameter: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
guard:
authenticators:
- ldap_tools.security.ldap_guard_authenticator
logout: true
api:
pattern: ^/
stateless: true
lexik_jwt: ~
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: IS_AUTHENTICATED_FULLY }
My question is how can I get to work the ldap authenticator? I'll show the wanted workflow
Auth request is received (username and password)
The fosuser provider finds the user or user not found(bad credentials-->end)
The ldap authenticator guard authenticate the user against the domain server or bad credentials -->end
The user is logged in successfully and token is received
but I still getting bad credentials with a registered user in database and domain server (the user credential works)
Thanks in advance!
Diggin into the ldap_tools.security.ldap_guard_authenticator authenticator(namespace LdapTools\Bundle\LdapToolsBundle\Security;) I found this
public function getUser($credentials, UserProviderInterface $userProvider)
{
$domain = $this->ldap->getDomainContext();
try {
$credDomain = isset($credentials['ldap_domain']) ? $credentials['ldap_domain'] : '';
$this->switchDomainIfNeeded($credDomain);
$this->setLdapCredentialsIfNeeded($credentials['username'], $credentials['password'], $userProvider);
$user = $userProvider->loadUserByUsername($credentials['username']);
$this->userChecker->checkPreAuth($user);
return $user;
} catch (UsernameNotFoundException $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} catch (BadCredentialsException $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} catch (LdapConnectionException $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} catch (\Exception $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} finally {
$this->switchDomainBackIfNeeded($domain);
}
}
This method loads an user given an userprovider and some credentials.
So, the getUser relies in the userprovider in order to load any user, but since you are using fos_userbundle as userprovider for your api_login firewall you are, in fact, authenticating against your local database. Try using the ldap userprovider in your config instead.
Of course, doing this you will authenticate against the ldap server and not the flow you descibed above. To do so consider handling authentication by yourself, so you can handle the flow as you want.
Also you can do like this
customauthenticator and in your public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
you can check credentials against your ldap server with the service "#ldap_tools.ldap_manager"->authenticate($user, $password, &$errorMessage = false, &$errorNumber = false).
Here is my answer: Just force the auth after checking the ldap server.
public function onAuthenticationFailure(AuthenticationFailureEvent $event)//when auth fails on DB(Allways!!!)
{
$userToken = $event->getAuthenticationToken();
$username = $userToken->getUsername();
$password = $this->requestStack->getCurrentRequest()->get('password');
if ($this->ldapManager->authenticate($username, $password)) {//good credentials
$token = new UsernamePasswordToken($userToken, 'yes', "public", $userToken->getRoles());
$this->container->get('security.token_storage')->setToken($token);//set a token
$event = new InteractiveLoginEvent($this->requestStack->getCurrentRequest(), $token); //dispatch the auth event
$event->stopPropagation();
$this->container->get('event_dispatcher')->dispatch(SecurityEvents::INTERACTIVE_LOGIN,$event);
}
//symfony takes care of the response
}
So far this is the best answer I've found
I'm trying to use auto login feature in symfony something like firing the login event and setting the user object. The user object is available in that controller but when I try to use other controller the user object says annon instead of showing the logged in user info
Controller A
private function autoLogin($request, $username)
{
$em = $this->getDoctrine()->getManager();
if (!$usr = $em->getRepository('AppBundle:User')->findOneBy(['username' => $username])) {
throw $this->createNotFoundException('User does not exist');
}
$token = new UsernamePasswordToken($usr, $usr->getPassword(), "secured_area", $usr->getRoles());
$this->get('security.token_storage')->setToken($token);
$loginEvent = new InteractiveLoginEvent($request, $token);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $loginEvent);
$user = $this->get('security.token_storage')->getToken()->getUser();
dump($user); // can see user object without any issue
if (!$this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
return $usr;
}
Controller B
public function editAction(Request $request)
{
$user = $this->get('security.token_storage')->getToken()->getUser();
print_r($user); // result is annon.
}
security.yml
security:
encoders:
AppBundle\Entity\User:
algorithm: bcrypt
providers:
doctrine_provider:
entity:
class: AppBundle:User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured_area:
anonymous: ~
provider: doctrine_provider
pattern: ^/
form_login:
login_path: security_login
check_path: security_login
csrf_token_generator: security.csrf.token_manager
logout:
path: /logout
target: /login
access_control:
- { path: ^/.*, roles: IS_AUTHENTICATED_ANONYMOUSLY }
I'd assume that you're not using any security setup and the second controller is called after user refreshes the page.
The most likely problem is that your user is not persisted into the session. Symfony\Component\Security\Http\Firewall\ContextListener is responsible for that. If you have a look at onKernelResponse() method you can find out how it does it. Basically it gets token from token storage, serialize it and stores to the session. On the request it does opposite: gets token from session and puts it to token storage.
I'd suggest that you play with the configuration of firewall and set up something like this:
firewalls:
autologin:
pattern: /autologinUrl/
context: autologing
In this case context listener will be called doing session-related stuff and your code should work.
I'm using this LexikJWTAuthenticationBundle with FosUserBundle.
I have this in security.yml :
firewalls:
app:
pattern: ^/api
stateless: true
anonymous: true
lexik_jwt: ~
with the following access_control :
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/api/user/action2, roles: IS_AUTHENTICATED_ANONYMOUSLY }
The behaviour I was expecting for /api/user/action2 is having access no matter what is inside the request header. However I'm getting a 401 in the case where the Authorization Bearer is set but not valid (it is ok with valid token or no Authorization Bearer at all).
My use case is I need to check in my controller if the user is logged in but if not, I still want to let that anonymous user access the route.
You have to create a specific firewall for the route/pattern you want allow for anonymous users :
action2:
pattern: ^/api/user/action2
anonymous: true
lexik_jwt: ~
Then, just move your less-protected access_control just before the fully-protected :
- { path: ^/api/user/action2, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
In this way, you are application doesn't care about an Authorization header, and all users can access the resource without JWT.
Update
Change the anonymous route's firewall to :
action2:
pattern: ^/api/user/action2
anonymous: true
lexik_jwt: ~
And make the access_control accepting anonymous And fully authenticated users :
- { path: ^/api/user/action2, roles: [IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_FULLY] }
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
Please use the same order and clear your cache correctly.
It's working well in my JWT/FOSUB application, if it doesn't work for you I'll give you a working ready-to-use example.
And the controller :
$currentToken = $this->get('security.token_storage')->getToken();
if (is_object($currentToken->getUser())) {
// Do your logic with the current user
return new JsonResponse(['user' => $currentToken->getUser()->getUsername()]);
} else {
return new JsonResponse(['user' => 'Anonymous']);
}
Hope it works for you.
I resolved your problem in this way:
api_public:
pattern: ^/api/v1/public
anonymous: true
lexik_jwt:
authorization_header:
enabled: false
prefix: Bearer
query_parameter:
enabled: false
name: bearer
api:
pattern: ^/api
stateless: true
anonymous: true
lexik_jwt:
authorization_header:
enabled: true
prefix: Bearer
query_parameter:
enabled: true
name: bearer
*** For those landing here in 2022 ***
To allow anonymous access with JWT
You must write your own JWTAuthenticator class -
(Code Source)
// src/Security/JWTAuthenticator.php
namespace App\Security;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
// use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; // For Symfony 4.4 and above
final class JWTAuthenticator extends JWTTokenAuthenticator
{
private $firewallMap;
public function __construct(
JWTTokenManagerInterface $jwtManager,
EventDispatcherInterface $dispatcher,
TokenExtractorInterface $tokenExtractor,
// TokenStorage $tokenStorage, // For Symfony 4.4 and above
FirewallMap $firewallMap
) {
parent::__construct($jwtManager, $dispatcher, $tokenExtractor);
// For Symfony 4.4 and above, use the next line instead of the above one
// parent::__construct($jwtManager, $dispatcher, $tokenExtractor, $tokenStorage);
$this->firewallMap = $firewallMap;
}
/* For Symfony 3.x and below */
public function getCredentials(Request $request)
{
try {
return parent::getCredentials($request);
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
// if anonymous is allowed, do not throw error
if ($firewall->allowsAnonymous()) {
return;
}
throw $e;
}
}
/* For Symfony 4.x and above */
public function supports(Request $request) {
try {
return parent::supports($request) && parent::getCredentials($request);
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
// if anonymous is allowed, skip authenticator
if ($firewall->allowsAnonymous()) {
return false;
}
throw $e;
}
}
}
Register this class as a service by adding the following to your services.yaml file
app.jwt_authenticator:
#autowire: false # uncomment if you had autowire enabled.
autoconfigure: false
public: false
parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator
class: App\Security\JWTAuthenticator
arguments: ['#security.firewall.map']
Then update the firewall in security.yaml to use the newly registered service
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- app.jwt_authenticator
Lastly, here's a complete tutorial to setup Lexik JWT bundle with Symfony.
I had to add token extractor to config. I was wrong thinking this is enabled by default.
# lexic_jwt_authentication.yaml
token_extractors:
authorization_header:
enabled: true
prefix: Bearer
name: Authorization
I'm developing an api, and I can't get the current user when I try the BasicAuth :
FatalErrorException: Error: Call to a member function getUser() on a non-object
Below you'll find the security.yml part that cause the error (I think it does) :
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: true
stateless: true
http_basic:
realm: "REST Service Realm"
provider: fos_userbundle
access_control:
- { path: ^/users/me, role: IS_AUTHENTICATED_FULLY }
I just put the path in access_control I'm testing. I've several more.
I'm using FOSRestBundle as well as FOSUserBundle (as you can see) and I didn't want to put a prefix, because it would be redundant :
// Routing.yml
rest :
type : rest
resource : "routing_rest.yml"
Now the part where the error occur is in the UserController (extending the FOSUser one) :
private function response($data, $status, $groups = array())
{
$currentUser = $this->container->get('security.context')->getToken()->getUser();
if (!$currentUser)
$groups = array("anon");
else
{
if ($currentUser->hasRole("ROLE_SUPER_ADMIN"))
array_push($groups, "admin");
else if ($currentUser->hasRole("ROLE_ADMIN"))
array_push($groups, "admin");
else if ($currentUser->hasRole("ROLE_USER"))
array_push($groups, "user");
}
return $this->view($data, $status)->setSerializationContext(SerializationContext::create()->setGroups($groups));
}
Any idea what's wrong ?
The problem is that your token is null. You have an unknown user if your token does not exist.
$token = $this->container->get('security.context')->getToken();
if (!$token) {
return ('anon.');
}
$user = $token->getUser();