I'm have an authenticated section on my app, but the authentication is done via oauth to a 3rd party service. I get the 200 callback from the service, now I create/find my user and set him as logged in.
So my provider is:
providers:
users:
entity: { class: MainBundle:User, property: id }
My user implements the security's UserInterface, although I don't have the username, password, etc properties on my user entity. I assume the id is then the only identifier I can use then.
My token is set as follows:
$token = new UsernamePasswordToken($user, null, 'secured_area', $user->getRoles());
$this->securityContext->setToken($token);
I wish to do this without using JMS bundle for now; my serialization looks like this:
/**
* Serialize
* #return string|void
*/
public function serialize()
{
return serialize(array(
'id' => $this->getId(),
'display_name' => $this->getDisplayName(),
'email' => $this->getEmail(),
));
}
/**
* Unserialize
* #return mixed|void
*/
public function unserialize($data)
{
$data = unserialize($data);
$this->setId($data['id']);
$this->setDisplayName($data['display_name']);
$this->setEmail($data['email']);
return $this;
}
Using the above I get an infinite loop redirect.
At first I only serialized the id, but then all the other properties of the user isn't available.
Then I tried serializing the whole object ($this), but that gives me a xdebug nesting level error of 1000.
I'm a bit lost of how to make this authentication work with serialization
security.yml
providers:
users:
entity: { class: MainBundle:User, property: id }
login logic
$token = new UsernamePasswordToken($user, null, 'secured_area', $user->getRoles());
$this->securityContext->setToken($token);
I took out the serialization out of the user entity, also not implementing equatable interface. Also still do not have the interface properties, although this is needed:
public function getUsername()
{
return $this->getId();
}
Related
Suppose I have two authentication mechanisms. How do I allow/deny a user's access to content depending on which authentication mechanism they used to authenticate themselves.
For example, let's say I have:
two type of users: admin and user such that all admins are users but users are not admins
two authentication mechanisms: admin_login and user_login
I would like to make it so that an admin has to authenticate through admin_login in order to have admin accesses. This means he would be considered a regular user if he was authenticated through user_login.
I first thought about using firewall context, but quickly realized it wasn't gonna work as I would need a firewall to support multiple contexts :
# config/packages/security.yaml
security:
# ...
firewalls:
admin:
# ...
context: // not currently supported
- admin
- user
user:
# ...
context: user
The other idea I came up with was creating a property called is_allow_admin in the User class and using it to change the way the roles are retrieved by setting it in the admin_login authenticator :
// src/Entity/User.php
// ...
class User implements UserInterface
{
// ...
private $is_allow_admin = false;
// ...
public function setIsAllowAdmin(bool $is_allow_admin)
{
$this->is_allow_admin = $is_allow_admin ;
}
public function getRoles(): array
{
if (!$this->is_allow_admin) {
return ['ROLE_USER'];
}
//...
return $roles;
}
// ...
}
// src/Security/AdminAuthenticator.php
class AdminAuthenticator extends AbstractFormLoginAuthenticator
{
//...
public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
{
$user = $userProvider->loadUserByUsername($credentials['username']);
if (!$user) {
throw new CustomUserMessageAuthenticationException('username could not be found.');
}
$user->setIsAllowAdmin(true); // <-
return $user;
}
//...
}
This unfortunately doesn't work. Everything goes smoothly with the authenticator, onAuthenticationSuccess is triggered. But somehow in the end, no user is authenticated.
I know it has to do with $this->setIsAllowAdmin(true); since it works correctly when I remove the line.
Is there another way to tackle this problem?
Thank you in advance.
Currently the logic behind Resetting Password is that user must provide valid/registered e-mail to receive password recovery e-mail.
In my case I don't want to validate if the e-mail is registered or not due to security concerns and I want to just do the check in back-end and tell user that "If he has provided registered e-mail, he should get recovery e-mail shortly".
What I've done to achieve this is edited in vendor\laravel\framework\src\Illuminate\Auth\Passwords\PasswordBroker.php sendResetLink() method from this:
/**
* Send a password reset link to a user.
*
* #param array $credentials
* #return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
if (is_null($user)) {
return static::INVALID_USER;
}
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
return static::RESET_LINK_SENT;
}
to this:
/**
* Send a password reset link to a user.
*
* #param array $credentials
* #return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
// if (is_null($user)) {
// return static::INVALID_USER;
// }
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
if(!is_null($user)) {
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
}
return static::RESET_LINK_SENT;
}
This hard-coded option is not the best solution because it will disappear after update.. so I would like to know how can I extend or implement this change within the project scope within App folder to preserve this change at all times?
P.S. I've tried solution mentioned here: Laravel 5.3 Password Broker Customization but it didn't work.. also directory tree differs and I couldn't understand where to put new PasswordBroker.php file.
Thanks in advance!
Here are the steps you need to follow.
Create a new custom PasswordResetsServiceProvider. I have a folder (namespace) called Extensions where I'll place this file:
<?php
namespace App\Extensions\Passwords;
use Illuminate\Auth\Passwords\PasswordResetServiceProvider as BasePasswordResetServiceProvider;
class PasswordResetServiceProvider extends BasePasswordResetServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* #var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerPasswordBroker();
}
/**
* Register the password broker instance.
*
* #return void
*/
protected function registerPasswordBroker()
{
$this->app->singleton('auth.password', function ($app) {
return new PasswordBrokerManager($app);
});
$this->app->bind('auth.password.broker', function ($app) {
return $app->make('auth.password')->broker();
});
}
}
As you can see this provider extends the base password reset provider. The only thing that changes is that we are returning a custom PasswordBrokerManager from the registerPasswordBroker method. Let's create a custom Broker manager in the same namespace:
<?php
namespace App\Extensions\Passwords;
use Illuminate\Auth\Passwords\PasswordBrokerManager as BasePasswordBrokerManager;
class PasswordBrokerManager extends BasePasswordBrokerManager
{
/**
* Resolve the given broker.
*
* #param string $name
* #return \Illuminate\Contracts\Auth\PasswordBroker
*
* #throws \InvalidArgumentException
*/
protected function resolve($name)
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException(
"Password resetter [{$name}] is not defined."
);
}
// The password broker uses a token repository to validate tokens and send user
// password e-mails, as well as validating that password reset process as an
// aggregate service of sorts providing a convenient interface for resets.
return new PasswordBroker(
$this->createTokenRepository($config),
$this->app['auth']->createUserProvider($config['provider'] ?? null)
);
}
}
Again, this PasswordBrokerManager extends the base manager from laravel. The only difference here is the new resolve method which returns a new and custom PasswordBroker from the same namespace. So the last file we'll create a custom PasswordBroker in the same namespace:
<?php
namespace App\Extensions\Passwords;
use Illuminate\Auth\Passwords\PasswordBroker as BasePasswordBroker;
class PasswordBroker extends BasePasswordBroker
{
/**
* Send a password reset link to a user.
*
* #param array $credentials
* #return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
// if (is_null($user)) {
// return static::INVALID_USER;
// }
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
if(!is_null($user)) {
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
}
return static::RESET_LINK_SENT;
}
}
As you can see we extend the default PasswordBroker class from Laravel and only override the method we need to override.
The final step is to simply replace the Laravel Default PasswordReset broker with ours. In the config/app.php file, change the line that registers the provider as such:
'providers' => [
...
// Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
App\Extensions\Passwords\PasswordResetServiceProvider::class,
...
]
That's all you need to register a custom password broker. Hope that helps.
The easiest solution here would be to place your customised code in app\Http\Controllers\Auth\ForgotPasswordController - this is the controller that pulls in the SendsPasswordResetEmails trait.
Your method overrides the one provided by that trait, so it will be called instead of the one in the trait. You could override the whole sendResetLinkEmail method with your code to always return the same response regardless of success.
public function sendResetLinkEmail(Request $request)
{
$this->validateEmail($request);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$request->only('email')
);
return back()->with('status', "If you've provided registered e-mail, you should get recovery e-mail shortly.");
}
You can just override the sendResetLinkFailedResponse method in your ForgetPasswordController class.
protected function sendResetLinkFailedResponse(Request $request, $response)
{
return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT);
}
We'll just send the successful response even if the validation failed.
Please, I am new to symfony, I can't seem to make this work. I created a simple service that will check if the user status is active or deleted, and if they are, log them out from application. I have created the code that works, and then I wanted to be a badass and create a service that will do this. But nooo. So, I created a service and it prints out the result, so it is registered and it is working. The problem is I guess that I cannot approach the same variables as I did in Controllers to my service class. Here is the code:
<?php
namespace WebBundle\Roles;
class Roles
{
public function getApplicationId($loggedUser, $request)
{
// Get the current user role
/** #var $userRole array */
$userRole = $loggedUser->getRoles();
// Check if the user role is super admin or admin - client
/** #var $superAdmin bool */
$superAdmin = in_array('ROLE_SUPER_ADMIN', $userRole) ? true : false;
$admin = in_array('ROLE_ADMIN', $userRole) ? true : false;
if ($superAdmin) {
/** #var $application int */
$application = $request->get('application');
} elseif ($admin) {
/** #var $application int */
$application = $loggedUser->getAppClient()->getId();
}
return $application;
}
public function logoutInactiveAndDeletedUsers($loggedUser)
{
// Log out inactive or deleted users.
if ($loggedUser->getStatus()->getStatus() == 'inactive' || $loggedUser->getStatus()->getStatus() == 'deleted' ) {
//$this->get('security.context')->setToken(null);
//var_dump('test');exit;
return $this->container->render('WebBundle:Default:login.html.twig', array('last_username' => null, 'error' => null,));
}
}
}
So, this first service getApplicationId is working fine. But the other one is causing me real trouble. So the serivce is breaking when I call both:
$this->get('security.context')->setToken(null);
and
return $this->container->render('WebBundle:Default:login.html.twig', array('last_username' => null, 'error' => null,));
But If I put this code in Controller, it works perfectly. It destroys the token and redirect the user to the login page. Please, help me understand how to make it work as a service to.
Ok, I figure out how to call a view:
In services.yaml add templating as an argument
arguments: [#templating]
Then create a property and assign it to a constructor:
private $templating;
public function __construct($templating)
{
$this->templating = $templating;
}
And call it like $this:
$this->templating->render('WebBundle:Default:login.html.twig', array('last_username' => null, 'error' => null,));
Now I need to find a solution how to disable token for the user. If anybody know help me out dude (I am not stoner anymore).
Heyo!
I know it's a common problem people having problems with custom providers and web service authentication. I'm spending hours trying to figure out how to do that but I'm almost freaking out.
So, the thing is: I'm using the Symfony Firewalls with a custom UserProvider and a AbstractGuardAuthenticator as well. The problem is in the loadUserByUsername($username) function inside the UserProvider implementation.
For security reasons I can't retrieve the user password from Parse (my web service), and the loadUserByUsername($username) function asks for that. Even in the documentation about how to create a custom user provider using web services they are retrieving the user password from the database.
So what's the solution in that case? What can I do when I don't have access to the user password?
My current code is something like that:
$app['app.authenticator'] = function () {
return new Authenticator($app);
};
$app['security.firewalls'] = array(
'login' => array(
'pattern' => '^/login/$',
),
'secured' => array(
'pattern' => '^.*$',
'form' => array('login_path' => '/login/', 'check_path' => '/login/auth/'),
'logout' => array('logout_path' => '/logout/', 'invalidate_session' => true),
'guard' => array(
'authenticators' => array(
'app.authenticator'
),
),
'users' => function () use ($app) {
return new UserProvider($app);
},
)
);
The Authenticator.php is quite big code because extends the AbstractGuardAuthenticator class. But I'm basically using this one from Symfony docs. The only thing Is that I'm sending to the UserProvider class the username AND the password as well, because that way I can check if the user and password are right. Like this:
public function getUser($credentials, UserProviderInterface $userProvider) {
return $userProvider->loadUserByUsername($credentials);
}
And my UserProvider class is the default one, I'm just checking inside the loadUserByUsername function if the credentials comming from my Authenticator are right. Something like this:
public function loadUserByUsername($credentials) {
$encoder = new BCryptPasswordEncoder(13);
try {
$user = ParseUser::logIn($credentials['username'], $credentials['password']);
} catch (ParseException $error) {
throw new UsernameNotFoundException(sprintf('Invalid Credentials.'));
}
return new User($credentials['username'], $encoder->encodePassword($credentials['password'], ''), explode(',', 'ROLE_USER'), true, true, true, true);
}
The problem is: after the login (everything with the login is working fine), Silex calls the loadUserByUsername function in every page which needs to be secured, but just sending the username parameter.
So basically, I don't know what to do guys. I'm really trying to figure out how to get this thing working.
Thanks for your help!
I have a similar implementation and this issue is well known. In my user provider I have the methods loadUserByUsername($username) and refreshUser(UserInterface $user). Since I have the same issue like you, I don't check the user in loadUserByUsername but simple return a new Object with only the username in it to not disturb the flow. loadUserByUsername doesn't make sense for external APIs, so I simply jump over it. The method refreshUser is either beeing called on every request, this is usefull.
In your AbstractGuardAuthenticator you have the method createAuthenticatedToken, which returns an token. There you should have the full authentificated user:
abstract class AbstractGuardAuthenticator implements GuardAuthenticatorInterface
{
/**
* Shortcut to create a PostAuthenticationGuardToken for you, if you don't really
* care about which authenticated token you're using.
*
* #param UserInterface $user
* #param string $providerKey
*
* #return PostAuthenticationGuardToken
*/
public function createAuthenticatedToken(UserInterface $user, $providerKey)
{
//do login stuff
//save password in user
return new PostAuthenticationGuardToken(
$user,
$providerKey,
$user->getRoles()
);
}
}
Then, I would't use loadUserByUsername but refreshUser instead. Both are called on every request:
/**
* Don't use
* #codeCoverageIgnore
* #param string $username
* #return User
*/
public function loadUserByUsername($username)
{
return new User($username, null, '', ['ROLE_USER'], '');
}
/**
* Refresh user on every subrequest after login
* #param UserInterface $user
* #return User
*/
public function refreshUser(UserInterface $user)
{
$password = $user->getPassword();
$username = $user->getUsername();
//login check
}
the problem that I am experiencing is that I am trying to autologin users towards the profile section of my website but since it is protected by symfony's firewall the users get redirected to the login page before the autologin system kicks in.
So I was wondering if there is an event that gets triggered when the user tries to access a page under firewall which I could listen to or eventually another mechanism I could put in play to solve this scenario.
At the moment the fix I went for was to create a custom redirect controller which I send my users to with the autologin hash and a path parameter containing the final location to send the user to, so after authenticating them I redirect them to the final destination.
What I am trying to achieve is to be able to autologin users directly to those pages behind firewall without having a custom controller which autologin users and then redirect them to the desired page.
Thank you.
D.
Basically, you have three options:
1. Create a custom authentication provider
If you want to really listen to the firewall, you need to create a custom authentication provider, which is rather a challenging task.
You can follow this guide in the documentation: http://symfony.com/doc/current/security/custom_authentication_provider.html
or http://sirprize.me/scribble/under-the-hood-of-symfony-security/
The point here is to create your own firewall listener, which decides whether or not to authenticate the user. A firewall can have multiple listeners.
2. Listen to every request
Alternatively, you can listen to the kernel.request event, possibly manually checking if you are on the secured path, and if so, authenticate the user manually by a helper method. Example:
namespace AppBundle\Subscriber;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
class RequestSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
private $eventDispatcher;
public function __construct(TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher)
{
$this->tokenStorage = $tokenStorage;
$this->eventDispatcher = $eventDispatcher;
}
public static function getSubscribedEvents()
{
return [ KernelEvents::REQUEST => 'onRequest' ];
}
public function onRequest(GetResponseEvent $event)
{
// only master requests and not authenticated users
if (!$event->isMasterRequest() || $this->isUserLoggedIn()) {
return;
}
// add your own logic for creating or loading a User object
// and filtering secured routes
/* ... */
// wohoo, user is going to be logged in manually!
$this->authenticate($user, $event->getRequest(), 'secured_area');
}
protected function authenticate(UserInterface $user, Request $request, String $provider) : Bool
{
// password doesn't matter in this case
$token = new UsernamePasswordToken($user, null, $provider, $user->getRoles());
// actual authenticating
$this->tokenStorage->setToken($token);
// dispatch the authentication event, so event listeners can do stuff
$loggedUser = $token->getUser();
if ($loggedUser instanceof UserInterface && $loggedUser->isAccountNonLocked() &&
$loggedUser->isEnabled()) {
$this->eventDispatcher
->dispatch(
SecurityEvents::INTERACTIVE_LOGIN,
new InteractiveLoginEvent($request, $token)
);
return true;
}
return false;
}
protected function isUserLoggedIn()
{
$token = $this->tokenStorage->getToken();
if (!$token) {
return false;
}
return ($token->getUser() instanceof UserInterface);
}
}
And don't forget to add it to the app/config/services.yml:
app.request_subscriber:
class: AppBundle\Subscriber\RequestSubscriber
tags:
- { name: kernel.event_subscriber }
arguments: ['#security.token_storage', '#event_dispatcher']
3. Turn off the authorization
(Probably not a solution for you, since you want to authenticate, not authorize users, but I still mention it for others.)
In app/config/security.yml, change the access control settings so that everyone can access the secured area:
# ...
access_control:
- { path: ^/secured-area$, role: IS_AUTHENTICATED_ANONYMOUSLY }