Silex framework : Create an UserProvider/PasswordEncoder/User - php

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);
}

Related

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

Symfony 3 FosUserBundle non unique username login

Things we want to achieve in our application are:
Non-unique usernames [Done]
Unique username and email combination
FosUserBundle will fetch all users (on user login) with given username and checks if any of the users has the given password (hashed with bcrypt). When a user is found it logs the user in.
Making username non unique was quite simple by just overriding the username field in the user ORM. But we're kinda stuck with how to proceed in achieving the last two points. We've started creating a custom User Provider but it seems Symfony Security can only handle one user(name).
Is there anyone with experience that might be able to help us? If you need more information or code snippets, please ask. Thank you in advance!
So after looking through alot of the documentation for the Symfony Security module we figured it out.
We added an extra field (displayname) to the User model because Symfony is completely build around the fact that usernames are Unique. It always fetches the first user with the given username, this is not what we wanted.
So we started with writing our own Guard Authentication System, this was pretty straight forward although we had to make some adjustments.
This was all working well, but we ran into a problem with the built-in UsernamePasswordFormAuthenticationListener, this listener was still picking up the displayname from the login form. We actually want the unique username so that Symfony knows which user to use.
We created a custom listener that extended the standard listener and made sure the username was not fetched from the login form but from the user token.
So our flow is now like this: The user fills in his username (actually his displayname) and password, the system fetches all users with that displayname. Then we loop these users and check if someone has that password. If so, authenticate the user.
On user create the admin fills in the displayname and the system will autoincrement this as a username. (admin_1, admin_2, ...).
We have to monitor if what #kero said is true, but with Bcrypt it seems that even with simple passwords like "123", it results in a different hash for each user.
The only thing that is left is to have a UniqueConstraint on the unique combination of the displayname and email. If anyone knows how this can be achieved in our orm.xml and form, thank you.
http://symfony.com/doc/current/security/guard_authentication.html
Custom Guard Authenticator
class Authenticator extends AbstractGuardAuthenticator
{
private $encoderFactory;
private $userRepository;
private $tokenStorage;
private $router;
public function __construct(EncoderFactoryInterface $encoderFactory, UserRepositoryInterface $userRepository, TokenStorageInterface $tokenStorage, Router $router)
{
$this->encoderFactory = $encoderFactory;
$this->userRepository = $userRepository;
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}
/**
* Called on every request. Return whatever credentials you want,
* or null to stop authentication.
*/
public function getCredentials(Request $request)
{
$encoder = $this->encoderFactory->getEncoder(new User());
$displayname = $request->request->get('_username');
$password = $request->request->get('_password');
$users = $this->userRepository->findByDisplayname($displayname);
if ($users !== []) {
foreach ($users as $user) {
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}
} else {
if ($this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();
return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}
return null;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
if ($credentials !== null) {
return $userProvider->loadUserByUsername($credentials["username"]);
}
return null;
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($user !== null) {
return true;
} else {
return false;
}
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$exclusions = ['/login'];
if (!in_array($request->getPathInfo(), $exclusions)) {
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
throw $exception;
}
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
// you might translate this message
'message' => 'Authentication Required'
);
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
Custom listener
class CustomAuthListener extends UsernamePasswordFormAuthenticationListener
{
private $csrfTokenManager;
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenManagerInterface $csrfTokenManager = null)
{
parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'csrf_token_id' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfTokenManager = $csrfTokenManager;
$this->tokenStorage = $tokenStorage;
}
/**
* {#inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if ($user = $this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();
$username = $user->getUsername();
if ($this->options['post_only']) {
$password = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']);
} else {
$password = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']);
}
if (strlen($username) > Security::MAX_USERNAME_LENGTH) {
throw new BadCredentialsException('Invalid username.');
}
$request->getSession()->set(Security::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
} else {
return null;
}
}
}
Listener service
<service id="security.authentication.listener.form" class="Your\Path\To\CustomAuthListener" parent="security.authentication.listener.abstract" abstract="true" />

Laravel 5.2 Custom Auth fail after next request

I write a custom user provider to validate my user in a external API on external server.
CustomUserProvider:
<?php
namespace App\Auth;
use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable as Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Psr\Http\Message\ResponseInterface;
use Illuminate\Http\Request;
use App\XXX\Facades\XXXX;
use Log;
class CustomUserProvider implements UserProvider {
public $errors = [];
public function getErrorMessages ( ) {
return $this->errors;
}
public function retrieveById($identifier)
{
// TODO: Implement retrieveById() method.
}
public function retrieveByToken($identifier, $token)
{
// TODO: Implement retrieveByToken() method.
return null;
}
public function updateRememberToken(Authenticatable $user, $token)
{
// TODO: Implement updateRememberToken() method.
}
public function retrieveByCredentials(array $credentials)
{
$response =APIService::get( 'GET', $credentials, 'tokens');
if ( array_key_exists('code', $response) ) {
/* Exception o Credentials Error */
$this->errors = $response;
return null;
} else {
$apiCredentials = json_decode( $response->getBody(), true );
if( $apiCredentials ) {
$attributes = array (
'id' => 1,
'name' => 'Name',
'lastname' => 'LastName',
'email' => 'email#email.com'
);
$user = new GenericUser( $attributes );
return $user;
}
}
return null;
}
public function validateCredentials(Authenticatable $user, array $credentials)
{
if( $user->email == $credentials['email'] ) {
return true;
}
return false;
}
}
The problem is that the user if logged fine, but in the next request Auth::user() returns false. Debugging in the middleware Auth:user() is returning false too.

doctrine 2 - The user provider must be an instance of WebserviceUserRepository

i wanted to create an Custom EntityRepository to load a User from the Database, so i followed http://symfony.com/doc/current/cookbook/security/entity_provider.html this tutorial.
Now i get a exception and i do not know why:
{"error":{"code":500,"message":"Internal Server Error","exception":[{"message":"The user provider must be an instance of WebserviceUserRepository (Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider was given)."
If I remove extends EntityRepository from my WebserviceUser repo and also change security.ml and services.yml than the authentication works and the class is an instanceof WebserviceUserRepository.
I am using Symfony 2.7
I hope somebody can help me with that i am starting to pull my hair out ;-)
My Api Key Authenticator looks like that:
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface,
AuthenticationFailureHandlerInterface {
public function createToken(Request $request, $providerKey)
{
// look for an apikey query parameter
$apiKey = $request->query->get('apikey');
// or if you want to use an "apikey" header, then do something like this:
// $apiKey = $request->headers->get('apikey');
if (!$apiKey) {
throw new BadCredentialsException('No API key found');
// or to just skip api key authentication
//return null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
**if (!$userProvider instanceof WebserviceUserRepository) {**
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of WebserviceUserRepository (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
throw new AuthenticationException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response("Authentication Failed.".$exception->getMessage(), 403);
}
this is my user repo:
class WebserviceUserRepository extends EntityRepository implements UserProviderInterface {
public function getUsernameForApiKey($apiKey)
{
// Look up the username based on the token in the database, via
// an API call, or do something entirely different
$username = 'username';
return $username;
}
public function loadUserByUsername($username)
{
// make a call to your webservice here
$userData = "";
// pretend it returns an array on success, false if there is no user
if (false) {
$password = 'password';
$salt = '';
$roles = array('roles' => 'ROLE_ADMIN');
return new WebserviceUser($username, $password, $salt, $roles);
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
public function refreshUser(UserInterface $user)
{
$class = get_class($user);
if (!$this->supportsClass($class)) {
throw new UnsupportedUserException(
sprintf(
'Instances of "%s" are not supported.',
$class
)
);
}
return $this->find($user->getUsername());
}
public function supportsClass($class)
{
return $this->getEntityName() === $class
|| is_subclass_of($class, $this->getEntityName());
}
}
i also added the repository class to my entity object:
* #ORM\Table()
* #ORM\Entity(repositoryClass="Moera\RestBundle\Entity\WebserviceUserRepository")
*/
class WebserviceUser implements UserInterface, \Serializable, EquatableInterface
and i also registered bot classes in security.yml and services.yml:
services:
webservice_user_provider:
class: Moera\RestBundle\Entity\WebserviceUserRepository
apikey_authenticator:
class: Moera\RestBundle\Security\ApiKeyAuthenticator
public: false
security:
encoders:
Moera\RestBundle\Entity\WebserviceUser:
algorithm: bcrypt
providers:
webservice_user_provider:
entity:
class: MoeraRestBundle:WebserviceUser
firewalls:
secured_area:
pattern: ^/
stateless: true
simple_preauth:
authenticator: apikey_authenticator
provider: webservice_user_provider
Thank You very much!
Kind Regards,
Johannes

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

Categories