I'm building a multi-tenant application in Symfony2. For the secure "admin" area I have a custom entity provider (see: http://symfony.com/doc/current/cookbook/security/entity_provider.html)
However, it seems that Symfony2 only supports checking the entity on a single property.
my_entity_provider:
entity:
class: SecurityBundle:User
property: email
However, in my app a single User can have multiple accounts with the same email address. What I need is to also check for a tenant ID property when logging in.
my_entity_provider:
entity:
class: SecurityBundle:User
property: email, tenantID
I'm not sure how to accomplish this in Symfony2. I've been able to override the loadUsername method when creating a new User, but this isn't used by the login_check in Symfony2 security (and it is really ugly).
public function loadUserByUsername($username)
{
/* we create a concatenated string in the User entity to pass both
the email and tenantId values as the "username" */
$user_parts = explode("|", $username);
$q = $this->createQueryBuilder('u')
->where('u.tenantId = :tenantid AND u.email = :email')
->setParameter('tenantID', $user_parts[1])
->setParameter('email', $user_parts[0])
->getQuery();
try {
$user = $q->getSingleResult();
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf('Unable to find an active User object identified by "%s".', $username), null, 0, $e);
}
return $user;
}
Any guidance on implementing a custom security provider with multiple properties? Thanks!
You say you use a custom authentication provider, but I don't see where.
I think you use the default entity provider, which indeed, uses the associated entity Repositiory.
If you defined a property in configuration, it will use the repository ``findOneBy` method.
Otherwise, if you defined a custom repositoryClass, and this custom respository implements UserPrviderInterface, symfony will call loadUserByUsername
So, for you example to call you loadUserByUsername method, just remove property: email from your security.yml
Otherwise, there are many cleaner solutions that come to my mind, but the best to my mind is to
create your own user provider
It's a simple process:
create a class that implements UserProviderInterface
create a service for that class (f.e with id: app.security.user.provider)
configure security.yml to use this service
the class: (pseudo code, don't forget to add use statements, ...)
class UserProvider implements UserProviderInterface
{
public function __construct(ManagerRegistry $doctrine, $tenantIdProvider)
{
$this->doctrine = $doctrine;
$this->tenantIdProvider = $tenantIdProvider;
}
public function loadUserByUsername($username)
{
$this->doctrine->getRepository('App\Entity\User')->findOneBy([
'email' => $username,
'tenant' => $this->tenantIdProvider->getId(), // HERE $tenantIdProvider can be your listener for example.
]);
}
// more methods maybe?
}
the service (services.yml, app/config/config.yml, ... where you want)
services:
app.security.user.provider:
class: UserProvider
arguments:
- '#doctrine'
- '#app.tenant_id.provider' #maybe?
the security config
security:
providers:
user:
id: app.security.user.provider
I think overriding loadUserByUsername in your UserRepository, like you do, is the good thing to do in your case.
Maybe your problem is in the configuration :
Using the attribute "property", symfony doesn't use your own loadUserByUsername
So, simply remove the "property" line, and i hope all works :
my_entity_provider:
entity:
class: SecurityBundle:User
You've seen custom Authentication I assume? http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html#the-listener
Ignoring that this is for WSSE, this would allow you to create your own custom handle working with the tenant id and email address manually authenticating
Related
I have an Authenticator that needs to authenticate the user anonymously, but include a role. I do this by overriding createAuthenticatedToken in the Authenticator:
class ClientAuthenticator extends AbstractGuardAuthenticator
{
// supports(), getCredentials(), all working
public function getUser($credentials, UserProviderInterface $userProvider)
{
return new SessionUser;
}
// Return an anonymous user with the client role
public function createAuthenticatedToken(UserInterface $user, $providerKey)
{
return new AnonymousToken(
'Ynpir6i', // <-- here's the issue (the $secret param)
'anon.',
['ROLE_CLIENT_GUEST']
);
}
}
This works wonderfully -- when I hard-code the "secret" parameter of AnonymousToken.
I cannot figure out how to get this secret dynamically though. It is not the "secret" parameter provided in parameters.yml (aka %kernel.secret%).
I only got the secret I'm using now by dumping it out when it's set in AnonymousAuthenticationListener. I've looked at that service's configuration and I don't see it set at all.
What is this secret parameter, and how can I inject it into my Authenticator?
Or, is there a better way to add roles to an anonymous token that was authenticated a specific way?
That parameter may be set to a known value in security.yml:
security:
firewalls:
main:
anonymous:
secret: '%secret%'
In my current app, we've chosen not to use Doctrine or an ORM. I'm attempting to use Symfony 4.1's authentication system to log people in. What I'd like to do is use PDO directly to fetch users from the database.
I'm following this guide: http://symfony.com/doc/current/security/custom_provider.html. In security.yaml, I've created a new provider entry and added it to the firewall:
providers:
db_provider:
id: App\Utility\Security\UserAuthenticationProvider
encoders:
App\Utility\Security\UserAuthenticationProvider: bcrypt
firewalls:
main:
pattern: ^/
form_login:
login_path: login
check_path: check_login
default_target_path: user
anonymous: ~
provider: db_provider
And I've created UserAuthenticationProvider:
class UserAuthenticationProvider implements UserProviderInterface {
private $config;
private $userDAO;
public function __construct() {
$this->config = new Config();
$this->userDAO = new MySqlUserDAO($this->config);
}
public function loadUserByUsername($username) {
$user = $this->userDAO->getUserByUsername();
...
return $user;
}
public function refreshUser(UserInterface $user) {
...
}
public function supportsClass($class) {
...
}
My userDAO returns an object that implements UserInterface.
So when I got to my route /login, I get my login form. However, no users are loaded from the database. I can see that my UserProviderInterface gets created (by using dump in the constructor), but loadUserByUsername does not.
Do I need to implement something else that uses my UserAuthenticationProvider and calls loadUserByUsername?
Is there perhaps a better way to do authentication in Symfony without using Doctrine?
UPDATE
I found this guide which is older but has a bit more detail / context.
I've changed my classes / configs like so (edited for brevity):
#security.yaml
security:
providers:
db_provider:
id: database_user_provider
main:
pattern: ^/
form_login:
provider: db_provider
login_path: login
check_path: check_login
default_target_path: do_some_stuff
.
#services.yaml
services:
database_user_provider:
class: App\Utility\Security\DatabaseUserProvider
.
class DatabaseUser
implements
UserInterface,
EquatableInterface
{
protected $user;
public function getUser(): User {
return $this->user;
}
public function setUser(User $user): void {
$this->user = $user;
}
public function getRoles() {
return array("ROLE_USER");
}
public function getPassword() {
return $this->getUser()->getPassword();
}
public function getUsername() {
return $this->getUser()->getUsername();
}
}
.
class DatabaseUserProvider implements UserProviderInterface {
private $config;
private $userDAO;
public function __construct() {
$this->config = new Config();
$this->userDAO = new MySqlUserDAO($this->config);
}
public function loadUserByUsername($username): UserInterface {
$user = $this->userDAO->getUserByUsername($username);
$dbUser = new DatabaseUser();
$dbUser->setUser($user);
return $dbUser;
}
public function refreshUser(UserInterface $user) {
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class) {
return DatabaseUser::class === $class;
}
}
So what each file is doing (in a nutshell is):
services.yaml: naming my class DatabaseUserProvider to the service name database_user_provider for use in security.yaml.
security.yaml: setting database_user_provider to the alias db_provider and adding that as the provider on the main firewall.
DatabaseUser: my 'entity' class to represent my user from the database. I have a dumb User class that just has a few properties (username, password) and getters / setters for those. It is set as a property for the DatabaseUser object.
DatabaseUserProvider: Loads my DatabaseUser objects from the database using the DAO and returns them. (more specifically, the DAO returns a user, which is added to a new DatabaseUser object, which is returned).
The DAO simply runs a sql query to get a single result from the user table. it then takes this result and populates a User Value Object and returns it.
Results
When I was using Doctrine (and following this guide for loading users from the database and this one for a login form), the login route on the SecurityController would handle both rendering of the form and processing of the login request. Somehow, Symfony / Doctrine was smart enough to automatically (using a listener maybe???) load the appropriate user entity from the DB and authenticate them against the password they provided (and then set the user token and redirect them to the page they were trying to access.)
When I bypass doctrine (and use my own DAOs) and the classes above, authentication still does not occur. I can see that an instance of DatabaseUserProvider is being created (by dumping some vars in the constructor), and the Security tab of the profilier shows database_user_provider as the provider. But that seems to be as far as it gets.
Question
It seems to me that DatabaseUserProvider::loadUserByUsername should be the next thing that should happen. From where does this method get called from? Do I need to be passing the username into the constructor and kick it off from there? Should I be using this class as a service in my Controller and call the method manually from the controller (something I did not have to do when using doctrine - none of this logic was in the controller)?
I use FOSUB with Symfony 3.2
I wish to "normalize" the username when the user logs in.
By "normalize", I mean I have a "NormalizeService" to be called before checking in DB if user exist.
The process would be :
$normalizedUsername = $NormalizeService->normalize($providedUsername);
logUser($normalizedUsername, $providedPassword); // this is a simple login as usual
What do you think is the best way to do this ? Create a custom UserProvider ? I'm not sure it's what I need.
Regards
Like the documentation of FOSUserBundle says:
FOSUserBundle stores canonicalized versions of the username and the email which are used when querying and checking for uniqueness.
You just need to create a class that implements CanonicalizerInterface
namespace AppBundle\Util;
use FOS\UserBundle\Util\CanonicalizerInterface;
class MyCanonicalizer implements CanonicalizerInterface
{
private $normalizeService;
public function __construct(NormalizeService $normalizeService)
{
$this->normalizeService = $normalizeService;
}
public function canonicalize($string)
{
if (null === $string) {
return null;
}
return $this->normalizeService->normalize($string);
}
}
register it as a service:
# app/config/services.yml
services:
app.my_canonicalizer:
class: AppBundle\Util\MyCanonicalizer
arguments: ['#normalize_service'] # your NormalizeService
public: false
and then configure FOSUserBundle to use it:
# app/config/config.yml
fos_user:
service:
email_canonicalizer: app.my_canonicalizer
You could create a PRE_SUBMIT FormEvent in which you normalize the username.
See this for more information.
supposed having certain route string like "/path/index.html" protected by firewall, how to chek whether current user is able to access it?
Thanks in advance!
I am sorry, I should have been more explicit: I have an array of route names and I construct a menu. A lot of users with different roles can access a page with this menu. The purpose is to show only accessible liks in this menu for a particular user.
Something like:
'security_context'->'user'->isGranted('/path/index.html')
This answer is based on your comments:
You should get the roles needed to access that route.to that you need access to the security.access_map service which is private.so it has to be injected directly.e.g: you can create a path_roles service like such that you can get the roles for a certain path:
namespace Acme\FooBundle;
class PathRoles
{
protected $accessMap;
public function __construct($accessMap)
{
$this->accessMap = $accessMap;
}
public function getRoles($path)
{ //$path is the path you want to check access to
//build a request based on path to check access
$request = Symfony\Component\HttpFoundation\Request::create($path, 'GET');
list($roles, $channel) = $this->accessMap->getPatterns($request);//get access_control for this request
return $roles;
}
}
now declare it as a service:
services:
path_roles:
class: 'Acme\FooBundle\PathRoles'
arguments: ['#security.access_map']
now use that service in your controller to get the roles for the path and construct your menu based on those roles and isGranted.i.e:
//code from controller
public function showAction(){
//do stuff and get the link path for the menu,store it in $paths
$finalPaths=array();
foreach($paths as $path){
$roles = $this->get('path_roles')->getRoles($path);
foreach($roles as $role){
$role = $role->getRole();//not sure if this is needed
if($this->get('security.context')->isGranted($role)){
$finalPaths[] = $path;
break;
}
}
//now construct your menu based on $finalPaths
}
}
You could use security.access_control configuration option:
securty:
access_control:
- { path: "^/path/index.html$", roles: ROLE_SOME_ROLE}
Or simply check that manually from within your controller:
class SomeController extends Controller {
public function indexAction() {
if (!$this->get('security.context')->isGranted(...)) {
throw new AccessDeniedException(...);
}
...
}
}
This question about Symfony 2.1
How can I encode User password with:
$factory = $this->get('security.encoder_factory');
$user = new Acme\UserBundle\Entity\User();
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword('ryanpass', $user->getSalt());
$user->setPassword($password);
And base config:
# app/config/security.yml
security:
# ...
encoders:
Acme\UserBundle\Entity\User: sha512
Inside the setter models:
class User implements UserInterface, \Serializable
{
public function setPassword($password)
{
$this->password = $password;
}
}
I believe that the process of encryption password must deal by model.
How can I use standart encoder factory inside the model?
While I agree with #Vadim that you shouldn't be leaking business logic into your model, I would be careful with deferring the hashing of the plaintext password until the prePersist event, for example, unless you call persist and flush right after setPassword. A getPassword call in the meantime will return the plaintext string unless you have stored it in a separate field, which could have serious consequences. Ideally the plaintext password should exist for as short a time as possible within the application's lifecycle.
I recommend using a service layer wherein a "User Manager" provides an interface to common tasks, so that you don't need to pollute your password property even temporarily:
class UserManager
{
// ...
public function __construct(EncoderFactoryInterface $encoderFactory)
{
// $encoderFactory is injected by the DIC as requested by your service configuration
// ...
}
public function setUserPassword(UserInterface $user, $plaintextPassword)
{
$hash = $this->encoderFactory->getEncoder($user)->encodePassword($plaintextPassword, null);
$user->setPassword($hash);
}
// ...
}
In your controller for registration form submission, for instance:
public function userRegistrationAction()
{
// ...
if ($form->isValid()) {
$user = new User();
// ...
$this->get('my.bundle.user_manager')->setUserPassword($user, $form->get('password')->getData());
// ...
}
}
The entity contains data, not handles it. If you want to change data of an entity you can create the event listener and do stuff before persistence. Check How to Register Event Listeners and Subscribers from the official documentation.
You can also take a look at FosUserBundle and its user management.
FosUserBundle UserManager
So, the main idea is to pass plain password from a form to the user entity and encode it before persitence using event listener.