Symfony2 FOSUserBundle – Validate against "user active" flag on login - forms

I have a flag on my users for 'active' and if set to zero or null, I will not allow login.
I have tried a couple of approaches and come up short.
If I do the logout route the flash message is not preserved, so the user sees nothing.
I looked into adding a validation on the login form so that it would throw a normal form error if the flag was not set to true, but in that folder (vendor/Bundles/FOS/UserBundle/Form/Type) I find nothing for login form, only registration and such, so I wouldn't know where to put it or where to inherit from in order to override.
I also tried as suggested here to manually log out, but that left me with a white screen of death...
Any suggestions how to easily accomplish this?
************** UPDATE ************
I realized that I probably want to go about it adding a validator on the login form. I currently have it coded into the controller of the first route a user gets sent to, but that won't provide much security if a user types a route before logging in, because on a successful login attempt, my default "landing page" after login will not be the route that the user is taken to, but he will be landing on the route of his choice...
***UPDATE AGAIN ****
So the service config file has this...
<service id="security.user_checker" class="%security.user_checker.class%" public="false" />
And that parameter is defined here...
<parameter key="security.user_checker.class">Symfony\Component\Security\Core\User\UserChecker</parameter>
So in order to modify the login logics I need to override
Symfony\Component\Security\Core\User\UserChecker
Now I have done that by overriding that parameter above in my own parameters.ini in the symfony app/config like this
security.user_checker.class = BizTV\UserBundle\Controller\UserChecker
.. and added this check to my userChecker overrider...
//Test for companylock...
if ( !$user->getCompany()->getActive() ) {
throw new LockedException('The company of this user is locked.', $user);
}
Here's the entire file:
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien#symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
//Override by Mattias
namespace BizTV\UserBundle\Controller;
//namespace Symfony\Component\Security\Core\User;
use Symfony\Component\Security\Core\Exception\CredentialsExpiredException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\Exception\AccountExpiredException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserChecker as OriginalUserChecker;
/**
* UserChecker checks the user account flags.
*
* #author Fabien Potencier <fabien#symfony.com>
*/
class UserChecker extends OriginalUserChecker
{
/**
* {#inheritdoc}
*/
public function checkPreAuth(UserInterface $user)
{
//Test for companylock...
if ( !$user->getCompany()->getActive() ) {
throw new LockedException('The company of this user is locked.', $user);
}
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isCredentialsNonExpired()) {
throw new CredentialsExpiredException('User credentials have expired.', $user);
}
}
/**
* {#inheritdoc}
*/
public function checkPostAuth(UserInterface $user)
{
//Test for companylock...
if ( !$user->getCompany()->getActive() ) {
throw new LockedException('The company of this user is locked.', $user);
}
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isAccountNonLocked()) {
throw new LockedException('User account is locked.', $user);
}
if (!$user->isEnabled()) {
throw new DisabledException('User account is disabled.', $user);
}
if (!$user->isAccountNonExpired()) {
throw new AccountExpiredException('User account has expired.', $user);
}
}
}
* Update nb 3 ********
Now I only have left to make it actually check for the standard user lock which surprisingly it doesn't do out of the box. (Thanks nifr for getting me this far!)
My user entity starts off like this, and like Nifr said, I need to implement the AdvancedUserInterface, but this is probably not the way to do it since it still doesn't check for this lock... but it throws me no error message either (if I change them up and put implememts AdvancedUserInterface and then EXTENDs baseUser it throws an error so...)
<?php
// src/BizTV/UserBundle/Entity/User.php
namespace BizTV\UserBundle\Entity;
use BizTV\UserBundle\Validator\Constraints as BizTVAssert;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use BizTV\BackendBundle\Entity\company as company;
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser implements AdvancedUserInterface
{
Not sure if that's how you do it when you both extend base user and try and implement AdvancedUserInterface, when done as above I still can't use the features it's supposed to add (but it throws me no error message either), but If I switch places of the EXTENDS and IMPLEMENTS like this (line 18)...
class User implements AdvancedUserInterface extends BaseUser
...I get this error:
Parse error: syntax error, unexpected T_EXTENDS, expecting '{' in /var/www/cloudsign/src/BizTV/UserBundle/Entity/User.php on line 18

FOSUserBundle / Symfony already has some kind of "active" flag integrated.
FOS\UserBundle\Model\User already provides the properties "locked" and "enabled" which are intended basically for this purpose. The difference between those two properties is the following ( quoting #stof's comment here)
From the Security component point of view, there is no real
difference: both are forbidden to log in. The difference is a
semantic one: disabled users are generally users that need to
activate their account (for instance, when you activate the need to
confirm the email in FOSUserBundle, the user is disabled on creation
and enabled on confirmation). On the other hand, locking a user is
generally an action done by the admin of the site to ban a user.
Using the same field in the database does not make sense as it would
allow banned user to have access again by simply going through the
confirmation process.
The check for locked/disabled users is being performed by a UserChecker ( symfony provides this one as #security.user_checker ) in FOSUserBundle's AuthenticationListener which implements Symfony\Component\Security\Core\User\UserCheckerInterface.
Now in order to redirect inactive user's to a different route you would:
Catch the Symfony\Component\Security\Core\Exception\DisabledException in the try/catch block in an extended AuthenticationListener
Redirect the user to a certain route if the caught exception is of type InactiveUserException
Optionally move the redirect to a newly created EventListener/-Subscriber which is being dispatched in the extended AuthenticationListener. This way you could later create additional Listeners i.e. for logging purposes and just subscribe them to the inactive-user login-attempt event.

Related

Validation in custom operation controller

I'm trying to do a validation only on a custom operation on api platform but it doesn't work.
I would like to change the password only if there is no error in the validation but in any case the password is changed.
If I remove the validation groups from the validation annotations it works.
for example if i replace #Assert\NotBlank(groups={"put-reset-password"}) with #Assert\NotBlank the validation pass.
This is the code of the entity :
"CHANGE-PASSWORD"={
"method"="PUT",
"path"="/users/change-password/{id}",
controller"="App\Controller\ChangePasswordController",
"access_control"="is_granted('EDIT-PASSWORD', previous_object) and object == user",
"openapi_context"={
"summary"="Change Password"
},
"denormalization_context"={
"groups"={"put-reset-password"}
},
"validation_groups"={"put-reset-password"},
},
and here is my controller
public function __invoke(User $data)
{
// Validate data and handle validation errors
$this->validator->validate($data);
$data->setPassword($this->userPasswordEncoder->encodePassword($data, $data->getNewPassword()));
$this->entityManager->flush();
$token = $this->tokenManager->create($data);
return $data;
}
and here is one of my attributes in which i use validation group.
/**
* #Groups({"put-reset-password"})
* #Assert\NotBlank(groups={"put-reset-password"})
* #UserPassword(groups={"put-reset-password"})
*/
private $oldPassword;
Any issue please ?
This is strange behavior.
You get null probably because you use interface
use ApiPlatform\Core\Validator\ValidatorInterface;
If use inface use Symfony\Component\Validator\Validator\ValidatorInterface you must be get object ConstraintViolationListInterface.
I have a similar problem. The following solution helped me:
/**
* #Groups({"put-reset-password"})
* #Assert\NotBlank(groups={"Default", "put-reset-password"})
* #UserPassword(groups={"put-reset-password"})
*/
private $oldPassword;
It's strange, but after added "Default" my validation is worked in my custom operation.
Useful links:
https://symfony.com/doc/current/validation/groups.html
https://symfony.com/doc/current/validation/sequence_provider.html
P.S. Sorry for my bad english.
I have the exact same case and I had just kind of made things to work with the Default group, but the actual solution is to give the validator the group you want to validate:
public function __invoke(User $data)
{
$this->validator->validate($data, null, 'put-reset-password');
...
}

Symfony redirect after login with condition on user

My symfony3 login page redirects to home by default, as stated inside my security.yml file.
However, I want it to redirect to my "Edit profile" page if the user didn't fulfil it yet. In any other form I would make this in the controller, but since there is no $form->handleRequest($user) in the login form, I don't have a $user variable to test on.
There is a lot of SO topics about how to redirect user based on roles, and the documentation tells about redirecting from the action field of the form or within security.yml, but not any is what I'm looking for.
How can I redirect based on a condition ?
NB : for some reasons, I cannot use FOSUserBundle yet :-(
I assume you are using the Guard Authentication system. ( http://symfony.com/doc/current/security/guard_authentication.html )
Then you should have a class extending the AbstractGuardAuthenticator class.
In that class, there is a method called onAuthenticationSuccess, in here you can put some logic for redirecting the request.
If you return null here, it will just continue, and use the route configured in your security.yml.
You will need to pass the #router service to the Authenticator class through dependencyInjection.
Assuming you passed the router service, your method will look something like this:
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
/** #var User $user */
$user = $token->getUser();
if ($user->hasCompleteProfile() == false) {
$url = $this->router->generate('edit_profile');
return new RedirectResponse($url);
}
// Continue with default behaviour
return null;
}
If all else fails (or becomes an excessive pain to deal with) you can always simply introduce an intermediary route and do your logic in there.
That is, create an action who's sole purpose is to redirect users based on whatever logic is needed, and then put that as the target path in the security.yml firewall.
security:
firewalls:
main:
pattern: ^/
anonymous: ~
form_login:
login_path: login
check_path: login
default_target_path: login_success
always_use_default_target_path: true
logout:
path: logout
Where the login would be something like:
class AuthenticationController extends Controller
{
/**
* #Route("/login", name="login")
* #Route("/logout", name="logout")
*/
public function loginAction(Request $request)
{
// Standard login stuff here
}
/**
* #Route("/login_success", name="login_success")
*/
public function postLoginRedirectAction()
{
if (/* user needs to see location A */) {
return $this->redirectToRoute("location_a");
} else if (/* user needs to see location B */) {
return $this->redirectToRoute("location_b");
} else {
return $this->redirectToRoute("index");
}
}
}

Is splitting an index action into multiple ones a restful-friendly approach?

I need to display two different index pages to two different user groups. For example, a regular user should see one page, and a privileged user - another one. I see two ways of approaching this issue:
One index action with conditionals:
public function index()
{
// view for privileged users
if(request()->user()->hasRole('privileged')){
return view('index_privileged');
}
// view for regular users
if(request()->user()->hasRole('regular')){
return view('index_regular');
}
return redirect('/');
}
Multiple actions:
public function index_privileged()
{
return view('index_privileged');
}
public function index_regular()
{
return view('index_regular');
}
Which approach is more "restful-friendly" and generally better?
I'm a big fan of light controllers. This might be a little overboard for a simple problem but if something like this pops up again, you'd already have everything all setup.
With that said, it might be best to create a PrivilegedUser class and a RegularUser class and give them both an index method which returns their respective views. Code them both to an interface UserInterface and make sure they both implement that.
Here is what those looked like in my test.
class RegularUser implements UserInterface
{
public function index()
{
return view('index_regular');
}
}
class PrivilegedUser implements UserInterface
{
public function index()
{
return view('index_privileged');
}
}
interface UserInterface
{
public function index();
}
Then you can add a listener which should run for the event Illuminate\Auth\Events\Login. Laravel will fire this event for you automatically when someone logs in. This goes into the file EventServiceProvider.php.
protected $listen = [
'Illuminate\Auth\Events\Login' => [
'App\Listeners\AuthLoginListener',
],
];
Now you can run php artisan event:generate to generate the new listener. Here is what my listener looks like, it should work for you.
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Foundation\Application;
class AuthLoginListener
{
/**
* Create the event listener.
*
* #param Application $app
*/
public function __construct(Application $app)
{
$this->app = $app;
}
/**
* Handle the event.
*
* #param Login $event
* #return void
*/
public function handle(Login $event)
{
if ($event->user->hasRole('privileged')) {
$this->app->bind('App\Repositories\UserInterface', 'App\Repositories\PrivilegedUser');
} else if ($event->user->hasRole('regular')) {
$this->app->bind('App\Repositories\UserInterface', 'App\Repositories\RegularUser');
}
}
}
Essentially what this is doing is telling Laravel to load up a certain class based on the type of user that just logged in. The User instance is available through the Login object which was automatically passed in by Laravel.
Now that everything is setup, we barely have to do anything in our controller and if you need to do more things that are different depending on the user, just add them to the RegularUser or PrivilegedUser class. If you get more types of users, simply write a new class for them that implements the interface, add an additional else if to your AuthLoginListener and you should be good to go.
To use this, in your controller, you'd do something like the following...
// Have Laravel make our user class
$userRepository = App::make('App\Repositories\UserInterface');
return $userRepository->index()->with('someData', $data);
Or even better, inject it as a dependency.
use App\Repositories\UserInterface;
class YourController extends Controller
{
public function index(UserInterface $user)
{
return $user->index();
}
}
Edit:
I just realized I forgot the part where you wanted to return redirect('/'); if no condition was met. You could create a new class GuestUser (I know this sounds like an oxymoron) which implements UserInterface but instead of using the AuthLoginListener, I'd bind it in a service provider when Laravel boots. This way Laravel will always have something to return when it needs an implementation of UserInterface in the event it needs this class if no one is logged in.
Well, its more like a refactoring "issue" than a rest-friendly issue. Check this guideline and you can see that most of the things that makes an api friendly is concerned to the url.
But, lets answer what you are asking. The thing you wanna do is a refactoring method but it is not only the move method but something like the extract variable.
The second option would make the code more readable, either ways are right but the second is more developer friendly. It enhances the code readability from any developer. I would recommend using the second option.
Refactoring is never enough, but read something like this, it will help you a lot writing more readable codes.

Laravel password reset "No Sender"

When I do a password reset in Laravel 5.1, I get the email, but it says "No Sender" in the title.
Is there a way to specify the sender from somewhere for password reset emails? Apart from the sender, I assume Laravel should automatically uses the email settings as specified in the config files? It's strange because when I set the Laravel mail config to use the 'mail' driver, I get bounced emails saying I can't send from a dynamic address (which is to be expected on dev), but still the password reset emails go through. Shouldn't email reset use the same config settings?
Jack, you can set in the From attributes for email id and name in config/mail.php. I too had the same problem and just got it sorted, as i mentioned above.
#SeriousJelly Answer Update for Laravel 5.2
in your Auth PasswordController Override resetEmailBuilder Method
class PasswordController extends Controller
{
protected function resetEmailBuilder()
{
return function (Message $message) {
$message->subject($this->getEmailSubject());
$message->from('you#email.com', 'you');
};
}
}
This might help someone
So, Alexey Mezenin's answer is almost there, however, one big no no is overwriting the core files as any future updates can break functionality.
As your PasswordController should be using the ResetsPassword trait you should be able to just overwrite any methods from ResetsPassword trait in your PasswordController.
For example, adding your own from and subject line to emails is a simple case of finding the relevant function in your trait, copy and pasting into your PasswordController and amending it.
Here is an example PasswordController with a function that over writes the sendResetLinkEmail() function.
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\Password;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
class PasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Create a new password controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Send a reset link to the given user.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function sendResetLinkEmail(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
$broker = $this->getBroker();
$response = Password::broker($broker)->sendResetLink($request->only('email'), function (Message $message) {
$message->subject($this->getEmailSubject());
$message->from(env('MAIL_FROM'), env('APP_NAME'));
});
switch ($response) {
case Password::RESET_LINK_SENT:
return $this->getSendResetLinkEmailSuccessResponse($response);
case Password::INVALID_USER:
default:
return $this->getSendResetLinkEmailFailureResponse($response);
}
}
Maybe there is a better solution, but you could manually add code into \vendor\laravel\framework\src\Illuminate\Foundation\Auth\ResetPasswords.php, after this line:
$message->subject($this->getEmailSubject()); // this is line 66
Add something like this:
$message->from('my#email.com', 'My Site');
https://laravel.com/docs/5.1/mail#sending-mail

ManyToOne with FOSUSerBundle ignoring exclusion policy

Building a JSON response for an API type thing, to retrieve a specific set of data that includes a ManyToOne relationship in the entity for my entity that extends FOSUSerBundle's User entity (called Account in my case).
The problem is, the Account entity thats included as a field in the response, is wanted, but I dont want to include all of the password and role type stuff.
I've been browing the internet for a couple hours now, and I've followed many guides on this, and I've cleared my cache every single time, and to no avail; So here's where I ended up:
// app/config/config.yml
jms_serializer:
metadata:
auto_detection: true
directories:
FOSUserBundle:
namespace_prefix: "FOS\\UserBundle"
path: "%kernel.root_dir%/Resources/serializer/FOS"
I've for below I've tried User.Model.yml and Model.User.yml and User.Entity.yml as well in a vain thought that the file name actually matters
// app/Resources/serializer/FOS/Entity.User.yml
FOS\UserBundle\Model\User:
exclusion_policy: ALL
properties:
id:
expose: true
and what I get still looks like this:
{
"status":"ok",
"api_version":"1.0",
"code":200,
"data":{
"video":{
"id":1,
"published":true,
"visibility":true,
"title":"Megaman 2",
"slug":"megaman-2",
"summary":"A rap song about Megaman",
"description":"A rap song\r\nAbout megaman",
"youtube_id":"R6L9bUouDr8",
"date_published":"2014-07-02T14:09:26-0700",
"date_created":"2014-07-02T14:09:26-0700",
"date_updated":"2014-07-02T14:09:26-0700",
"author_id":3,
"author":{
"id":3,
"username":"kharrison",
"username_canonical":"kharrison",
"email":"(sorry private)",
"email_canonical":"(sorry, private)",
"enabled":true,
"salt":"(sorry, private)",
"password":"(sorry, private)",
"last_login":"2014-07-04T15:17:34-0700",
"locked":false,
"expired":false,
"roles":[
"ROLE_SUPER_ADMIN"
],
"credentials_expired":false,
"display_name":"Kyle Harrison",
"slug":"kyle-harrison",
"bio":"Test"
}
}
}
}
The "author" field, is my Account entity thats being run through the JMSSerializer
I want to exclude ALL of that, except the user ID, Display name, and slug.
And finally this is how the API works:
// My/Bundle/Controller/BaseAPIController.php
//......... other code
/**
* #param string $status
* #param integer $code
* #return Response
*/
public function render_api($status, $code)
{
$this->apiResponse->setStatus($status);
$this->apiResponse->setCode($code);
return new Response($this->apiResponse->serialize($this->get('jms_serializer')), $this->apiResponse->getCode(), ["Content-type"=>"application/json"]);
}
//............. other code
and finally, that calls this:
// My/Bundle/Models
class APIResponse {
protected $status;
protected $apiVersion;
protected $code;
protected $data;
public function __construct($apiVersion, $status = "OK", $code = 500)
{
$this->status = $status;
$this->code = $code;
$this->apiVersion = $apiVersion;
$this->data = [];
}
// ... getters and setters
/**
* #return mixed
*/
public function serialize($serializer) {
return $serializer->serialize($this, "json");
}
}
I've for below I've tried User.Model.yml and Model.User.yml and
User.Entity.yml as well in a vain thought that the file name actually
matters.
It does matter, actually. It's a concatenation of the namespace and class name. In this case, you're trying to configure the FOS\UserBundle\Model\User class, so the file name should be Model.User.yml. (FOS\UserBundle\ should be excluded from the file name, since you configured it as namespace_prefix in your config.yml)
Also make sure that your Account class doesn't re-declare (overwrite) the properties, as the serializer config only works if you configure it for the class that actually declares the properties.
Ok So, the actual answer, couldn't have been arrived to via the information I provided. But Nic's Answer did lead me towards the solution. The description of how the the serializer looks at and deciphers the config file lead me to the real problem at hand.
This is what I failed to show:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends BaseUser
{
The problem lays with the Alias I provided the FOS\UserBundle\Model\User namespace. I no longer remember why I wrote that that way. However, the moment I remove the Alias and rewrote the extends to resemble this instead:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends User
{
combined with the new correct filename from Nic's answer, the config based Exclusion policy for JMSSerializerBundle totally kicks in, and every instance of FOSUserBundle's items are now completely hidden, except for the fields I've now explicitly told it to expose.
This is exactly what I wanted :)
Thanks everyone for your help! Cheers
~k
I'm not sure it's the exact way you want it, more a way around:
way around 1: Select only the properties you want (via the entity manager) and then serialize the array obtained.
It's what I do with what I call my API (which is not a class as you but controllers)