expiration of a role Symfony - php

You can subscribe on my website. I use FOSUserBundle.
When the user subscribes, he won the role ROLE_SUBSCRIBER giving it access to new page.
I would like this role expires after a period that I recorded in the User entity.
class User extends BaseUser
{
// ...
* #ORM\Column(type="datetime")
protected $subscribeExpiration;
public function setSubscribeExpiration(\DateTime $subscribeExpiration) {
$this->subscribeExpiration = clone $subscribeExpiration;
return $this;
}
public function getSubscribeExpiration() {
return $this->subscribeExpiration;
}
// ...
}

Don't use a ROLE, but a Voter.
Then, in your voter check for the expireDate to decide if the user is a subsciber or not :
// src/AppBundle/Security/PostVoter.php
namespace AppBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use AppBundle\Entity\User;
class SubscriberVoter extends Voter
{
const IS_SUBSCRIBER = 'is_subscriber';
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::IS_SUBSCRIBER))) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
// you know $subject is a Post object, thanks to supports
/** #var Post $post */
$post = $subject;
switch ($attribute) {
case self::IS_SUBSCRIBER:
$expireDate = $user->getSubscriberExpireDate();
$currendDate = new \DateTime();
return (null !== $expireDate && $expireDate > $currendDate);
}
throw new \LogicException('This code should not be reached!');
}
}
To check this 'role' :
$this->isGranted('is_subscriber');

Related

Dynamically set roles on authentication

In Symfony, during the authentication, I want to attribute specific role to my user.
If I specify ->setRoles() in my authenticator, or my "getRoles" function, I come back to the login page, anonymously
Following code in Authenticator doesn't work
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['customId' => $credentials['customId']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('CustomId could not be found.');
}
if($user->getId() == 2) {
$user->setRoles(['ROLE_SUPER_ADMIN']);
}
return $user;
}
This code in my Entity doesn't work
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
if($this->getId() == 2) {
$this->setRoles(['ROLE_SUPER_ADMIN']);
}
return array_unique($roles);
}
If you change the user object it will not match the one in the database. Symfony will recognize this as someone messing with the stored data and log you out for safety.
You can change how the comparison of the user is done by implementing the EquatableInterface:
class User implements EquatableInterface
{
public function isEqual(UserInterface $user): bool
{
// Example for what your comparison could look like
return $user->getUsername() === $this->getUsername() && $user->getId() === $this->getId();
}
}
You can find this (in a rather small section) in the docs: https://symfony.com/doc/current/security/user_provider.html#comparing-users-manually-with-equatableinterface

Can I create something similar to isGranted in symfony?

So basically I want to create something like #IsGranted.
I used #IsGranted on my application to access control to prevent a simple user from accessing an admin page for example.
On my entity, I have a boolean field called is_Active
if it's true (1) then the user can use his account
if it's false (0) then he gets redirected to an error page!
In this case, I am not going to test on the Rolesfield of the user but I am gonna test on the is_Active field that's why I can't use the #IsGranted.
I created an error twig page active.html.twig
and I place it on templates folder, and I found myself FORCED to add those 2 lines on every controller function.
if ($this->getUser()->getIsActive()==false) {
return $this->render('active.html.twig');}
Here is an example:
/**
* #IsGranted("ROLE_ADMIN")
* #Route("/", name="user_index", methods={"GET"})
*/
public function index(UserRepository $userRepository): Response
{
if ($this->getUser()->getIsActive()==false) {
return $this->render('active.html.twig');}
return $this->render('user/index.html.twig', [
'users' => $userRepository->findAll(),
]);
}
This is very heavy and bad to add this if statement on every function (I have +30 functions on the app)
Maybe I can create something similar to #IsGranted and use it on the annotation of each function instead?
You can keep using #IsGranted with a custom voter. https://symfony.com/doc/current/security/voters.html#creating-the-custom-voter
Create new voter like in the documentation
public const ACTIVE = 'active';
protected function supports(string $attribute, $subject)
{
return $attribute === self::ACTIVE;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if ($user instanceof User && !$user->isActive()) {
throw new InactiveUserException();
}
return true;
}
Then you can create a listener for InactiveUserException and show what ever you want to the client.
In your controller you'll need to put #IsGranted("active") or #Security(expression="is_granted('active')") before the route method or controller
I would use the authentication for this then you don't have to touch your controllers. You can check if they are logged in and active then they can view the content or if they fail auth then you can direct them to another route with your active.html.twig.
You can also just have this set on certain routes or all of them.
https://symfony.com/doc/current/security/guard_authentication.html
Sample Authenticator and set this just for your admin routes then you can have a normal authenticator without checking for an active user on the checkCredentials for all other routes.
<?php
namespace App\Security;
use App\Entity\User;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Twig\Environment;
class AdminAuthenticator extends AbstractGuardAuthenticator
{
/** #var Environment */
private $twig;
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
public function supports(Request $request): bool
{
$email = $request->request->get('email');
$password = $request->request->get('password');
return $email && $password;
}
public function getCredentials(Request $request)
{
$email = $request->request->get('email');
$password = $request->request->get('password');
return [
'email' => $email,
'password' => $password
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$email = $credentials['email'];
return $userProvider->loadUserByUsername($email);
}
public function checkCredentials($credentials, UserInterface $user)
{
$password = $credentials['password'];
if (!$this->passwordEncoder->isPasswordValid($user, $password)) {
throw new CustomUserMessageAuthenticationException(
'Sorry, you\'ve entered an invalid username or password.'
);
}
if (!$user->isActive()) {
throw new NotActiveUserException(
'This account is not active'
);
}
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
if ($exception instanceof NotActiveUserException) {
// You should redirect here but you get the idea!
$this->twig->render('active.html.twig');
}
// Do something else for any other failed auth
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new JsonResponse('success', Response::HTTP_OK);
}
public function start(Request $request, AuthenticationException $authException = null)
{
return new JsonResponse('Not Authorized', Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
Then in your security.yaml
firewalls:
admin:
pattern: ^/admin
provider: user
guard:
authenticators:
- App\Security\AdminAuthenticator

How does one load gate defines when testing a Laravel application?

I'm writing tests for a Laravel application. In my AuthServiceProvider->boot(), I define a number of user abilities with $gate->define() based on a permissions table in my database.
Basically this:
foreach ($this->getPermissions() as $permission) {
$gate->define($permission->name, function ($user) use ($permission) {
return $user->hasPermission($permission->name);
});
}
In my tests I'm creating permissions on the fly, but the AuthServiceProvider has already booted up, which means I can't verify user permissions with #can, Gate, etc.
Is there a proper way to deal with this issue?
I know I'm a bit late for the party on this one, but still - I just had the same problem myself and hence this question doesn't have a comprehensive answer, here is my solution for the same issue (in Laravel 5.3):
I've got this in my app\Providers\AuthServiceProvider:
/**
* Register any authentication / authorization services.
*
* #param Gate $gate
*/
public function boot(Gate $gate)
{
$this->registerPolicies();
if (!app()->runningInConsole()) {
$this->definePermissions($gate);
}
}
/**
* #param Gate $gate
*/
private function definePermissions(Gate $gate)
{
$permissions = Permission::with('roles')->get();
foreach($permissions as $permission) {
$gate->define($permission->key, function($user) use ($permission) {
return $user->hasRole($permission->roles);
});
}
}
This takes care of the normal application flow when not testing and disables the premature policy registration when testing.
In my tests/TestCase.php file I have the following methods defined (note that Gate points to Illuminate\Contracts\Auth\Access\Gate):
/**
* Logs a user in with specified permission(s).
*
* #param $permissions
* #return mixed|null
*/
public function loginWithPermission($permissions)
{
$user = $this->userWithPermissions($permissions);
$this->definePermissions();
$this->actingAs($user);
return $user;
}
/**
* Create user with permissions.
*
* #param $permissions
* #param null $user
* #return mixed|null
*/
private function userWithPermissions($permissions, $user = null)
{
if(is_string($permissions)) {
$permission = factory(Permission::class)->create(['key'=>$permissions, 'label'=>ucwords(str_replace('_', ' ', $permissions))]);
if (!$user) {
$role = factory(Role::class)->create(['key'=>'role', 'label'=>'Site Role']);
$user = factory(User::class)->create();
$user->assignRole($role);
} else {
$role = $user->roles->first();
}
$role->givePermissionTo($permission);
} else {
foreach($permissions as $permission) {
$user = $this->userWithPermissions($permission, $user);
}
}
return $user;
}
/**
* Registers defined permissions.
*/
private function definePermissions()
{
$gate = $this->app->make(Gate::class);
$permissions = Permission::with('roles')->get();
foreach($permissions as $permission) {
$gate->define($permission->key, function($user) use ($permission) {
return $user->hasRole($permission->roles);
});
}
}
This enables me to use this in tests in multiple ways. Consider the use cases in my tests/integration/PermissionsTest.php file:
/** #test */
public function resource_is_only_visible_for_those_with_view_permission()
{
$this->loginWithPermission('view_users');
$this->visit(route('dashboard'))->seeLink('Users', route('users.index'));
$this->visit(route('users.index'))->assertResponseOk();
$this->actingAs(factory(User::class)->create());
$this->visit(route('dashboard'))->dontSeeLink('Users', route('users.index'));
$this->get(route('users.index'))->assertResponseStatus(403);
}
/** #test */
public function resource_action_is_only_visible_for_those_with_relevant_permissions()
{
$this->loginWithPermission(['view_users', 'edit_users']);
$this->visit(route('users.index'))->seeLink('Edit', route('users.edit', User::first()->id));
$this->loginWithPermission('view_users');
$this->visit(route('users.index'))->dontSeeLink('Edit', route('users.edit', User::first()->id));
}
This works just fine in all my tests. I hope it helps.
public function boot(GateContract $gate)
{
parent::registerPolicies($gate);
$gate->before(function($user, $ability) use ($gate){
return $user->hasPermission($ability);
});
}
I haven't extensively tested this, but it seems to work from my quick tests.
I'm not sure what the "proper" way (if there is one) to define a gate for testing. I couldn't find an answer for this after looking at the documentation and searching, but this seems to work in a pinch in Laravel 5.7:
Defining a gate in a model factory state:
$factory->state(App\User::class, 'employee', function () {
Gate::define('employee', function ($user) {
return true;
});
return [];
});
This test function will have both the 'employee' and the 'admin' gate applied since we are using the 'employee' state when creating the user:
/** #test */
public function an_admin_user_can_view_the_admin_page()
{
$user = factory('App\User')->state('employee')->make();
$this->actingAs($user);
Gate::define('admin', function ($user) {
return true;
});
$this->get('/admin')
->assertOk();
}
I know this is a really old question, but it was the top result in a search and hopefully can help someone.
Don't forget to use the Gate facade:
use Illuminate\Support\Facades\Gate;
You could do something like this inside AuthServiceProvider
First import the necessary packages
use Illuminate\Auth\Access\Gate;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
and then add this boot() method
public function boot(GateContract $gate)
{
parent::registerPolicies($gate);
$gate->define('update-post', function ($user, $post, $isModerator) {
// check if user id equals post user id or whatever
if ($user->id === $post->user->id) {
return true;
}
// you can define multiple ifs
if ($user->id === $category->user_id) {
return true;
}
if ($isModerator) {
return true;
}
return false;
});
// you can also define multiple gates
$gate->define('update-sub', function($user, $subreddit) {
if($user->id === $subreddit->user->id) {
return true;
}
return false;
});
And then in your controller you could do something like this
if (Gate::denies('update-post', [$post, $isModerator])) {
// do something
}

Silex framework: using both OAuth and Basic authentication

in my current Silex project I would like to realize a login via Oauth and basic login both. For OAuth, I am using a Silex extension (https://github.com/gigablah/silex-oauth). Unfortunately, now I have problems with integrating the basic login.
My thoughts are that I have to create a custom user provider which provides both OAuth and password via DB, but I don't know how to realize it really.
At this point, I have a really ugly mixture of two User providers. For me, it is logic, but it will not work. I think I am off the track, so it would be really nice, if you can give me some tips - I am trying it for a few days now...
My user provider:
<?php
namespace Core;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\DBAL\Connection;
class UserProvider implements UserProviderInterface
{
private $conn;
private $oauth;
public function __construct($oauth = null)
{
global $app;
if($oauth) { $this->oauth = $oauth; }
$this->conn = $app['db'];
}
/*public function loadAllUsers()
{
$users = $this->conn->executeQuery('SELECT * FROM users')->fetchAll();
$users_object = array();
foreach ($users as $user) {
if (!empty($user['username'])) {
$users_object[$user['username']] = array($user['password'], $user['firstname'], $user['lastname'], explode(',', $user['roles']));
}
}
$oauth = (array)$this->oauth;
print_r($oauth->users);
if (count($oauth['users']) > 0 ) {
print_r($this->oauth);
}
if ($this->oauth) {
if (count($oauth['users'])) {
return $this->oauth;
} else {
} return $users_object;
} else {
return $users_object;
}
}*/
public function loadUserByOAuthCredentials($token)
{
return $this->oauth->loadUserByOAuthCredentials($token);
}
public function loadUserByUsername($username)
{
if ($this->oauth->loadUserByUsername($username)) {
return $this->oauth->loadUserByUsername($username);
} else {
$stmt = $this->conn->executeQuery('SELECT * FROM users WHERE username = ?', array(strtolower($username)));
if (!$user = $stmt->fetch()) {
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
return new User($user['username'], $user['password'], explode(',', $user['roles']), true, true, true, true);
}
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'Symfony\Component\Security\Core\User\User';
}
}
Thank you in advance, if you need more info, please tell me.
thoras

Silex framework : Create an UserProvider/PasswordEncoder/User

I use the framework Silex, especially SecurityServiceProvider.
I have to create my own User class (because my salt is the username => with the default class the salt is null) :
<?php
namespace Adh\Security;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
class User implements AdvancedUserInterface {
private $username;
private $password;
public function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
}
public function getRoles()
{
return array();
}
public function getPassword()
{
return $this->password;
}
public function getSalt()
{
return $this->username;
}
...
}
Until this, no problem. Now, I have to create a custom UserProvider to retrieve my user from MySQL :
<?php
namespace Adh\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Doctrine\DBAL\Connection;
class UserProvider implements UserProviderInterface
{
private $conn;
public function __construct(Connection $conn)
{
$this->conn = $conn;
}
public function loadUserByUsername($username)
{
$stmt = $this->conn->executeQuery('SELECT * FROM account WHERE username like ?', array($username));
if (!$user = $stmt->fetch()) {
throw new UsernameNotFoundException(sprintf('Le nom d\'utilisateur "%s" n\'existe pas', $username));
}
return new \Adh\Security\User($user['username'], $user['sha_pass_hash']);
}
...
}
And to register the security provider :
$app->register(new Silex\Provider\SecurityServiceProvider(), array(
'security.firewalls' => array(
'user' => array(
'pattern' => '^/user',
'form' => array('login_path' => '/connexion', 'check_path' => '/user'),
'users' => $app->share(function () use ($app) {
return new Adh\Security\UserProvider($app['db']);
})
)
)
));
$app['security.encoder_factory'] = $app->share(function ($app) {
return new EncoderFactory(
array('Adh\Security\User' => new Adh\Security\PasswordEncoder())
);
});
It works, except when the authentification is positive (the username and password match) I've this exception :
RuntimeException: There is no user provider for user
"Adh\Security\User".
How to set my UserProvider for my User class ?
Thank's
I found the solution. To create my provider I followed this example : http://silex.sensiolabs.org/doc/providers/security.html#defining-a-custom-user-provider
In the refreshUser method:
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
This is correct for the default User class: I have my own User class so the exception is raised.
The condition becomes :
if (!$user instanceof \Adh\Security\User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
Your function
loadUserByUsername()
does not return any role. By default a Symfony\Component\Security\Core\User\User record is returned with the roles of the user as third parameter. At least any user must have one role.
Sample:
use Symfony\Component\Security\Core\User\User;
public function loadUserByUsername($username)
{
$frameworkUser = new FrameworkUser($this->app);
if (false === ($user = $frameworkUser->selectUser($username))) {
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
return new User($user['username'], $user['password'], $user['roles'], true, true, true, true);
}

Categories