I have an OAuth API that requires an username and a password to get the user object (resource owner password credentials flow). I'm trying to get this end result :
User enters username/password
Symfony exchanges username/password for access and refresh tokens, then fetches the User object and populates a token with the fetched object
User is now authenticated on the website
The issue that I'm having is that I cannot seem to figure out how to do it the best way I can see : with an User provider. The UserProviderInterface asks to implement loadUserByUsername(), however I cannot do that, as I need the username AND the password to fetch the user object.
I tried to implement the SimplePreAuthenticatorInterface, but I still run into the same issue: after creating the PreAuthenticated token in createToken(), I need to authenticate it using authenticateToken(), and I still cannot fetch the user through the UserProvider, since I first have to use the username/password to get an access token that'd allow me to fetch the User object. I thought about adding a method to login in my UserProvider that'd login through the API using username/password and store the logged in tokens for any username in an array, and then fetch the tokens by username in that array, but that doesn't feel right.
Am I looking at it from the wrong angle ? Should I not be using PreAuthenticated tokens at all ?
A while ago i needed to implement a way to authenticate users through a webservice. This is what i end up doing based on this doc and the form login implementation of the symfony core.
First create a Token that represents the User authentication data present in the request:
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class WebserviceAuthToken extends AbstractToken
{
/**
* The password of the user.
*
* #var string
*/
private $password;
/**
* Authenticated Session ID.
*
* #var string
*/
private $authSessionID;
public function __construct($user, $password, array $roles = array())
{
parent::__construct($roles);
$this->setUser($user);
$this->password = $password;
parent::setAuthenticated(count($roles) > 0);
}
/**
* {#inheritDoc}
*/
public function getCredentials()
{
return '';
}
/**
* Returns the Authenticated Session ID.
*
* #return string
*/
public function getAuthSessionID()
{
return $this->authSessionID;
}
/**
* Sets the Authenticated Session ID.
*
* #param string $authSessionID
*/
public function setAuthSessionID($authSessionID)
{
$this->authSessionID = $authSessionID;
}
/**
* Returns the Password used to attempt login.
*
* #return string
*/
public function getPassword()
{
return $this->password;
}
/**
* {#inheritDoc}
*/
public function serialize()
{
return serialize(array(
$this->authSessionID,
parent::serialize()
));
}
/**
* {#inheritDoc}
*/
public function unserialize($serialized)
{
$data = unserialize($serialized);
list(
$this->authSessionID,
$parent,
) = $data;
parent::unserialize($parent);
}
}
The AuthSessionID that im storing is a token returned from the webservice that allows me to perform requests as an authenticated user.
Create a Webservice authentication listener which is responsible for fielding requests to the firewall and calling the authentication provider:
use RPanelBundle\Security\Authentication\Token\RPanelAuthToken;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class WebserviceAuthListener extends AbstractAuthenticationListener
{
private $csrfTokenManager;
/**
* {#inheritdoc}
*/
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, $csrfTokenManager = null)
{
if ($csrfTokenManager instanceof CsrfProviderInterface) {
$csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager);
} elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) {
throw new InvalidArgumentException('The CSRF token manager should be an instance of CsrfProviderInterface or CsrfTokenManagerInterface.');
}
parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'intention' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfTokenManager = $csrfTokenManager;
}
/**
* {#inheritdoc}
*/
protected function requiresAuthentication(Request $request)
{
if ($this->options['post_only'] && !$request->isMethod('POST')) {
return false;
}
return parent::requiresAuthentication($request);
}
/**
* {#inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if (null !== $this->csrfTokenManager) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['intention'], $csrfToken))) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}
if ($this->options['post_only']) {
$username = trim($request->request->get($this->options['username_parameter'], null, true));
$password = $request->request->get($this->options['password_parameter'], null, true);
} else {
$username = trim($request->get($this->options['username_parameter'], null, true));
$password = $request->get($this->options['password_parameter'], null, true);
}
$request->getSession()->set(Security::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new WebserviceAuthToken($username, $password));
}
}
Create a Webservice login factory where we wook into the Security Component, and tell which is the User Provider and the available options:
class WebserviceFormLoginFactory extends FormLoginFactory
{
/**
* {#inheritDoc}
*/
public function getKey()
{
return 'webservice-form-login';
}
/**
* {#inheritDoc}
*/
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
{
$provider = 'app.security.authentication.provider.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('app.security.authentication.provider'))
->replaceArgument(1, new Reference($userProviderId))
->replaceArgument(2, $id);
return $provider;
}
/**
* {#inheritDoc}
*/
protected function getListenerId()
{
return 'app.security.authentication.listener';
}
}
Create an Authentication provider that will verify the validaty of the WebserviceAuthToken
class WebserviceAuthProvider implements AuthenticationProviderInterface
{
/**
* Service to handle DMApi account related calls.
*
* #var AccountRequest
*/
private $apiAccountRequest;
/**
* User provider service.
*
* #var UserProviderInterface
*/
private $userProvider;
/**
* Security provider key.
*
* #var string
*/
private $providerKey;
public function __construct(AccountRequest $apiAccountRequest, UserProviderInterface $userProvider, $providerKey)
{
$this->apiAccountRequest = $apiAccountRequest;
$this->userProvider = $userProvider;
$this->providerKey = $providerKey;
}
/**
* {#inheritdoc}
*/
public function authenticate(TokenInterface $token)
{
// Check if both username and password exist
if (!$username = $token->getUsername()) {
throw new AuthenticationException('Username is required to authenticate.');
}
if (!$password = $token->getPassword()) {
throw new AuthenticationException('Password is required to authenticate.');
}
// Authenticate the User against the webservice
$loginResult = $this->apiAccountRequest->login($username, $password);
if (!$loginResult) {
throw new BadCredentialsException();
}
try {
$user = $this->userProvider->loadUserByWebserviceResponse($loginResult);
// We dont need to store the user password
$authenticatedToken = new WebserviceAuthToken($user->getUsername(), "", $user->getRoles());
$authenticatedToken->setUser($user);
$authenticatedToken->setAuthSessionID($loginResult->getAuthSid());
$authenticatedToken->setAuthenticated(true);
return $authenticatedToken;
} catch (\Exception $e) {
throw $e;
}
}
/**
* {#inheritdoc}
*/
public function supports(TokenInterface $token)
{
return $token instanceof WebserviceAuthToken;
}
}
And finally create a User provider. In my case after i receive the response from the webservice, i check if the user is stored on redis, and if not i create it. After that the user is always loaded from redis.
class WebserviceUserProvider implements UserProviderInterface
{
/**
* Wrapper to Access the Redis.
*
* #var RedisDao
*/
private $redisDao;
public function __construct(RedisDao $redisDao)
{
$this->redisDao = $redisDao;
}
/**
* {#inheritdoc}
*/
public function loadUserByUsername($username)
{
// Get the UserId based on the username
$userId = $this->redisDao->getUserIdByUsername($username);
if (!$userId) {
throw new UsernameNotFoundException("Unable to find an UserId identified by Username = $username");
}
if (!$user = $this->redisDao->getUser($userId)) {
throw new UsernameNotFoundException("Unable to find an User identified by ID = $userId");
}
if (!$user instanceof User) {
throw new UnsupportedUserException();
}
return $user;
}
/**
* Loads an User based on the webservice response.
*
* #param \AppBundle\Service\Api\Account\LoginResult $loginResult
* #return User
*/
public function loadUserByWebserviceResponse(LoginResult $loginResult)
{
$userId = $loginResult->getUserId();
$username = $loginResult->getUsername();
// Checks if this user already exists, otherwise we need to create it
if (!$user = $this->redisDao->getUser($userId)) {
$user = new User($userId, $username);
if (!$this->redisDao->setUser($user) || !$this->redisDao->mapUsernameToId($username, $userId)) {
throw new \Exception("Couldnt create a new User for username = $username");
}
}
if (!$user instanceof User) {
throw new UsernameNotFoundException();
}
if (!$this->redisDao->setUser($user)) {
throw new \Exception("Couldnt Update Data for for username = $username");
}
return $this->loadUserByUsername($username);
}
/**
* {#inheritdoc}
*/
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());
}
/**
* {#inheritdoc}
*/
public function supportsClass($class)
{
return $class === 'AppBundle\Entities\User';
}
}
Required services :
app.security.user.provider:
class: AppBundle\Security\User\WebserviceUserProvider
arguments: ["#app.dao.redis"]
app.security.authentication.provider:
class: AppBundle\Security\Authentication\Provider\WebserviceAuthProvider
arguments: ["#api_caller", "", ""]
app.security.authentication.listener:
class: AppBundle\Security\Firewall\WebserviceAuthListener
abstract: true
parent: security.authentication.listener.abstract
Configured security:
security:
providers:
app_user_provider:
id: app.security.user.provider
firewalls:
default:
pattern: ^/
anonymous: ~
provider: app_user_provider
webservice_form_login: # Configure just like form_login from the Symfony core
If you have any question please let me know.
Related
I'm trying to customize the login with a graphql query to load the User data and it shows me that error.
I use php bin / console make: auth and it works correctly when I query the $ user variable from MySQL but when I load the $ user variable from GRAPHQL it shows me that error.
This is the code:
public function getUser($credentials, UserProviderInterface $userProvider)
{
if ($credentials["csrf_token"] != "") {
$client = new GH();
$headers = ['Content-Type' => 'application/x-www-form-urlencoded'];
$result = $client->request('POST', 'https://graphql.clientecliente.com/get-token', [
'json' => [
'usuario' => $credentials["email"],
'clave' => $credentials["password"]
]
]);
$data = $result->getBody()->getContents();
$objetodata=(json_decode($data));
$token = $objetodata->token;
}
//Conexion servidor GraphQL
$servidor = $this->params->get('enlace_graphql');
//Codigo debe salir desde la base de datos
$codigoAuth = $token;
$options = [
'headers' => [
'Authorization' => 'Bearer ' . $codigoAuth,
],
];
$client = \Softonic\GraphQL\ClientBuilder::build($servidor, $options);
$gpl1 = <<<'QUERY'
query ($limit: Int!, $offset: Int!, $CODIGO_USUARIO: String) {
obt_usuarios(limit: $limit, offset: $offset, CODIGO_USUARIO: $CODIGO_USUARIO) {
totalCount
OBT_USUARIOS {
CODIGO_USUARIO
CLAVE_USUARIO
CODIGO_EMPRESA
CODIGO_CLIENTE
CODIGO_PASAJERO
ES_ADMINISTRADOR
}
}
}
QUERY;
$variables1 = [
'limit' => 10,
'offset' => 0,
'CODIGO_USUARIO' => $credentials["email"]
];
//$user = $this->entityManager->getRepository(Usuario::class)->findOneBy(['email' => $credentials['email']]);
$user = $client->query($gpl1,$variables1);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('El usuario no existe');
}
return $user;
}
Maybe, Anything idea?
Update.
Now create the custom user provider. I use this command: php bin/console make:user;
<?php
//src/Security/UserProvider.php
namespace App\Security;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
//Consulta API
use GuzzleHttp\Client as GH;
use GuzzleHttp\Pool;
//Consulta GraphQL
use Softonic\GraphQL;
use Softonic\GraphQL\Client;
use Softonic\GraphQL\ClientBuilder;
use function GuzzleHttp\json_decode;
class UserProvider implements UserProviderInterface
{
/**
* Symfony calls this method if you use features like switch_user
* or remember_me.
*
* If you're not using these features, you do not need to implement
* this method.
*
* #return UserInterface
*
* #throws UsernameNotFoundException if the user is not found
*/
public function loadUserByUsername($username)
{
$client = new GH();
$headers = ['Content-Type' => 'application/x-www-form-urlencoded'];
$result = $client->request('POST', 'https://graphql.cliente.com.ec/get-token', [
'json' => [
'usuario' => 'test',
'clave' => 'test1234',
]
]);
$data = $result->getBody()->getContents();
$objetodata=(json_decode($data));
//var_dump($objetodata->token);
//exit;
$token = $objetodata->token;
$servidor = "https://graphql.cliente.com/graphql";
//Codigo debe salir desde la base de datos
//$codigoAuth = $token;
$codigoAuth= "12345678912345678mlvIjoiT0JUX0NjIxOTI1ODN9.8dNiZI6iZsYS0plVU0fuqFlhkTDSOt9OFy5B-WZiRmk";
//var_dump($codigoAuth);
//exit;
//echo $codigoAuth; exit;
$options = [
'headers' => [
'Authorization' => 'Bearer ' . $codigoAuth,
],
];
$client = \Softonic\GraphQL\ClientBuilder::build($servidor, $options);
$gpl1 = <<<'QUERY'
query ($limit: Int!, $offset: Int!, $CODIGO_USUARIO: String) {
obt_usuarios(limit: $limit, offset: $offset, CODIGO_USUARIO: $CODIGO_USUARIO) {
totalCount
OBT_USUARIOS {
CODIGO_USUARIO
CLAVE_USUARIO
CODIGO_EMPRESA
CODIGO_CLIENTE
CODIGO_PASAJERO
ES_ADMINISTRADOR
}
}
}
QUERY;
$variables1 = [
'limit' => 10,
'offset' => 0,
'CODIGO_USUARIO' => 'test'
];
$user=$client->query($gpl1,$variables1);
//var_dump($user);exit;
//$username=$user;
return $user;
//throw new \Exception('TODO: fill in loadUserByUsername() inside '.__FILE__);
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*
* #return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
// Return a User object after making sure its data is "fresh".
// Or throw a UsernameNotFoundException if the user no longer exists.
throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
return $this->loadUserByUsername($user->getUsername());
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass($class)
{
return User::class === $class;
}
}
config/packages/security.yaml:
providers:
app_user_provider:
id: App\Security\UserProvider
firewalls:
secured_area:
anonymous: true
form_login:
login_path: loginanterior
check_path: login_check
default_target_path: index
provider: app_user_provider
remember_me: true
logout:
path: logout
target: index
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
src/Security/User.php
<?php
namespace App\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
private $email;
private $roles = [];
/**
* #var string The hashed password
*/
private $password;
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}
However, after this it does not show me any errors or anything. Just go back to the login again
You have to map your Softonic\GraphQL\Response to your User model. This should be done in your custom UserProvider so Authenticator doesn't know where the user actually comes from. You can also look at existing user providers code for inspiration.
Thanks to the help of Ion Bazan
I understood that I had to make a custom UserProvider.
Final steps encoder correction
config/packages/security.yaml
encoders:
App\Security\User:bcrypt
GraphQL result mapping on USER object by sending user, ROL and password (must be sent in the encryption correctly)
App/src/Security/UserProvider.php
$userData=$client->query($gpl1,$variables1)->getData();
//var_dump($userData);
$user = new User();
$user->setEmail($userData['obt_usuarios']["OBT_USUARIOS"][0]["CODIGO_USUARIO"]);
$user->setRoles(array("ROLE_USER"));
$user->setPassword('$2a$10$JSMRqSFX9Xm1gAbc/YzVcu5gETsPF8HJ3k5Zra/RZlx4IXfadwmW.');
return $user;
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*
* #return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
// Return a User object after making sure its data is "fresh".
// Or throw a UsernameNotFoundException if the user no longer exists.
//throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
return $this->loadUserByUsername($user);
}
With this finally it worked
I'm doing a authentication with guard feature. Problem is than I have to put a password to my User, he don't have to know this password so I choose to generate a random password. Problem is than I'm not in a controller so I can't use UserPasswordEncoderInterface ... So I'm looking for some help here.
I give you some code :
public function getUser($credentials, UserProviderInterface $userProvider)
{
/**
* #var FacebookUser $facebookUser
*/
$facebookUser = $this->getFacebookClient()
->fetchUserFromToken($credentials);
$email = $facebookUser->getEmail();
$user = $this->em->getRepository('App:User')
->findOneBy(['email' => $email]);
if (!$user) {
$user = new User();
$user->setEmail($facebookUser->getEmail());
$user->setName($facebookUser->getFirstName());
$user->setLastName($facebookUser->getLastName());
$user->setRoles(["ROLE_USER"]);
//TODO HASH PASSWORD
$user->setPassword(bin2hex(random_bytes(80)));
$this->em->persist($user);
$this->em->flush();
}
return $user;
}
and the method from controller
/**
* After going to Facebook, you're redirected back here
* because this is the "redirect_route" you configured
* in config/packages/knpu_oauth2_client.yaml
* #Route("/connect/facebook/check", name="connect_facebook_check")
*
* #return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function connectCheckAction() {
if (!$this->getUser()) {
return new JsonResponse(array('status' => false, 'message' => "User not found!"));
} else {
// $em = $this->getDoctrine()->getManager();
//
// $user = $this->getUser();
// $password = bin2hex(random_bytes(80));
// $hash = $encoder->encodePassword($user, $password);
// $user->setPassword($hash);
//
// $em->persist($user);
// $em->flush();
return $this->redirectToRoute('default');
}
}
You can inject EncoderFactoryInterface by constructor:
/**
* #var EncoderFactoryInterface
*/
private $securityEncoderFactory;
public function __construct(EncoderFactoryInterface $securityEncoderFactory)
{
$this->securityEncoderFactory = $securityEncoderFactory;
}
And then use:
$encoder = $this->securityEncoderFactory->getEncoder($user);
$encoder->encodePassword($user, $password);
You can just use the PHP's function password_hash to hash your randomly generated password. See the documentation here
I've an entity with a plainPassword and a password attribute. In form, I map on the plainPassword. After, when the user valid the form, I do password validation on the plainPassword.
To encode the password, I use an EventSubscriber that listen on prePersist and preUpdate. It works well for the register form, because it's a new entity, the user fill some persisted attributes, then doctrine persist it and flush.
But, when I just want to edit the password, it doesn't work, I think it's because the user just edit a non persisted attribute. Then Doctrine doesn't try to persist it. But I need it, to enter in the Subscriber.
Someone know how to do it ? (I've a similar problem in an other entity) For the moment, I do the operation in the controller...
Thanks a lot.
My UserSubscriber
class UserSubscriber implements EventSubscriber
{
private $passwordEncoder;
private $tokenGenerator;
public function __construct(UserPasswordEncoder $passwordEncoder, TokenGenerator $tokenGenerator)
{
$this->passwordEncoder = $passwordEncoder;
$this->tokenGenerator = $tokenGenerator;
}
public function getSubscribedEvents()
{
return array(
'prePersist',
'preUpdate',
);
}
public function prePersist(LifecycleEventArgs $args)
{
$object = $args->getObject();
if ($object instanceof User) {
$this->createActivationToken($object);
$this->encodePassword($object);
}
}
public function preUpdate(LifecycleEventArgs $args)
{
$object = $args->getObject();
if ($object instanceof User) {
$this->encodePassword($object);
}
}
private function createActivationToken(User $user)
{
// If it's not a new object, return
if (null !== $user->getId()) {
return;
}
$token = $this->tokenGenerator->generateToken();
$user->setConfirmationToken($token);
}
private function encodePassword(User $user)
{
if (null === $user->getPlainPassword()) {
return;
}
$encodedPassword = $this->passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($encodedPassword);
}
My user Entity:
class User implements AdvancedUserInterface, \Serializable
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="email", type="string", length=255, unique=true)
* #Assert\NotBlank()
* #Assert\Email()
*/
private $email;
/**
* #Assert\Length(max=4096)
*/
private $plainPassword;
/**
* #ORM\Column(name="password", type="string", length=64)
*/
private $password;
ProfileController:
class ProfileController extends Controller
{
/**
* #Route("/my-profile/password/edit", name="user_password_edit")
* #Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
public function editPasswordAction(Request $request)
{
$user = $this->getUser();
$form = $this->createForm(ChangePasswordType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Encode the password
// If I decomment it, it's work, but I want to do it autmaticlally, but in the form I just change the plainPassword, that is not persisted in database
//$password = $this->get('security.password_encoder')->encodePassword($user, $user->getPlainPassword());
//$user->setPassword($password);
$em = $this->getDoctrine()->getManager();
$em->flush();
$this->addFlash('success', 'Your password have been successfully changed.');
return $this->redirectToRoute('user_profile');
}
return $this->render('user/password/edit.html.twig', [
'form' => $form->createView(),
]);
}
}
You can force Doctrine to mark an object as dirty by manipulating the UnitOfWork directly.
$em->getUnitOfWork()->scheduleForUpdate($entity)
However, I do strongly discourage this approach as it violates carefully crafted abstraction layers.
I am trying to set up switch_user functionality on an application which authenticates using Apache's auth_kerb. REMOTE_USER is returned correctly and am able to log in. However when I try to masquerade as a different user I am unable to. The user I wish to switch to does exist within the application. The attempt to switch user occurs but pre authentication is called again and the initial REMOTE_USER is loaded.
Any ideas on how to get switch_user working using remote_user and custom user provider?
security.yml
security:
providers:
webservice_user_provider:
id: webservice_user_provider
...
firewalls:
secured_area:
switch_user: { role: ROLE_ALLOWED_TO_SWITCH, parameter: _masquerade }
pattern: ^/
remote_user:
provider: webservice_user_provider
...
services.yml
parameters:
account.security_listener.class: Acme\MyUserBundle\Listener\SecurityListener
services:
account.security_listener:
class: %account.security_listener.class%
arguments: ['#security.authorization_checker', '#session', '#doctrine.orm.entity_manager', '#router', '#event_dispatcher']
tags:
- { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure }
- { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
- { name: kernel.event_listener, event: security.switch_user, method: onSecuritySwitchUser }
webservice_user_provider:
class: Acme\MyUserBundle\Security\User\WebserviceUserProvider
calls:
- [setEntityManager , ['#logger', '#doctrine.orm.entity_manager']]
...
SecurityListener.php
namespace Acme\MyUserBundle\Listener;
use ...
/**
* Class SecurityListener
* #package Acme\MyUserBundle\Listener
*/
class SecurityListener {
protected $session;
protected $security;
protected $em;
protected $router;
protected $dispatcher;
public function __construct(
AuthorizationCheckerInterface $security,
Session $session,
EntityManager $em,
UrlGeneratorInterface $router,
EventDispatcherInterface $dispatcher
// TraceableEventDispatcher $dispatcher
// ContainerAwareEventDispatcher $dispatcher
) {
$this->security = $security;
$this->session = $session;
$this->em = $em;
$this->router = $router;
$this->dispatcher = $dispatcher;
}
/**
*
* #param AuthenticationFailureEvent $event
* #throws AuthenticationException
*/
public function onAuthenticationFailure(AuthenticationFailureEvent $event) {
throw new AuthenticationException($event->getAuthenticationException());
}
/**
* #param InteractiveLoginEvent $event
*/
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) {
// set some defaults...
}
/**
* #param SwitchUserEvent $event
*/
public function onSecuritySwitchUser(SwitchUserEvent $event) {
$this->dispatcher->addListener(KernelEvents::RESPONSE, array($this, 'onSwitchUserResponse'));
}
/**
* #param FilterResponseEvent $event
*/
public function onSwitchUserResponse(FilterResponseEvent $event) {
$response = new RedirectResponse($this->router->generate('acme_mybundle_default_index'));
$event->setResponse($response);
}
}
WebServiceProvider.php
namespace Acme\MyUserBundle\Security\User;
use ...
class WebserviceUserProvider implements UserProviderInterface {
protected $entityManager;
protected $logger;
/**
*
* #param LoggerInterface $logger
* #param EntityManager $em
*/
public function setEntityManager(LoggerInterface $logger, EntityManager $em) {
$this->logger = $logger;
$this->entityManager = $em;
}
/**
*
* #param string $username
* #return Person
* #throws UsernameNotFoundException
*/
public function loadUserByUsername($username) {
# Find the person
$person = $this->entityManager->getRepository('AcmeMyBundle:Person')
->find($username);
if ($person) {
$this->logger->debug("Logged in, finding person: " . $person->getUsername());
return $person;
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
/**
*
* #param \Symfony\Component\Security\Core\User\UserInterface $person
* #throws \Symfony\Component\Security\Core\Exception\UnsupportedUserException
* #internal param \Symfony\Component\Security\Core\User\UserInterface $user
* #return Person
*/
public function refreshUser(UserInterface $person) {
if (!$person instanceof Person) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($person))
);
}
return $this->loadUserByUsername($person->getUsername());
}
/**
*
* #param type $class
* #return type
*/
public function supportsClass($class) {
return $class === 'Acme\MyBundle\Entity\Person';
}
}
This fix involves adapting AbstractPreAuthenticatedListener to check for the existence of the standard token that matches the logged in user, and if not a customised token that has stored the logged in user, but is attached to the 'switched to' userid.
This is the important (non copied) part of the code:
if (null !== $token = $this->securityContext->getToken()) {
if ($token instanceof PreAuthenticatedToken && $this->providerKey == $token->getProviderKey() && $token->isAuthenticated() && $token->getUsername() === $user) {
return;
}
// Switch user token. Check the original token.
if ($token instanceof PreAuthenticatedSwitchUserToken && $this->providerKey == $token->getProviderKey() && $token->isAuthenticated() && $token->getOriginalUsername() === $user) {
return;
}
}
The token stores the logged in user and returns it with getOriginalUsername.
Store the existing authentication data (passed in $preAuthenticatedData)
/**
* Constructor.
*/
public function __construct($user, $credentials, $providerKey, array $roles = array(), $preAuthenticatedData)
{
parent::__construct($roles);
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}
$this->setUser($user);
$this->credentials = $credentials;
$this->providerKey = $providerKey;
if (!is_array($preAuthenticatedData) && count($preAuthenticatedData) > 0) {
throw new \InvalidArgumentException('No preauthenticated data. Must have the server login credentials.');
}
$this->original_username = $preAuthenticatedData[0];
if ($roles) {
$this->setAuthenticated(true);
}
}
Getter
public function getOriginalUsername() {
return $this->original_username;
}
Stash changes
/**
* {#inheritdoc}
*/
public function serialize()
{
return serialize(array($this->credentials, $this->providerKey, $this->original_username, parent::serialize()));
}
/**
* {#inheritdoc}
*/
public function unserialize($str)
{
list($this->credentials, $this->providerKey, $this->original_username, $parentStr) = unserialize($str);
parent::unserialize($parentStr);
}
These changes fit into the context of broader customisation of the Symfony security system. The source code for this is in github.
1 services.yml
Set account.security_listener, security.authentication.switchuser_listener and security.authentication.listener.remote_user_switch
This is in addition to the expected user provider.
2 security.yml
Use this security provider
secured_area:
switch_user: { role: ROLE_ALLOWED_TO_SWITCH, parameter: _masquerade }
pattern: ^/
remote_user_switch:
provider: webservice_user_provider
3 Check that the user provider loads the backing data for your user.
4 Install security files.
RemoteUserSwitchFactory.php: defines the listener to handle the
authentication events.
PreAuthenticatedWithSwitchUserListener.php:
our special authentication logic. SwitchUserListener.php: handles
the switch user event.
PreAuthenticatedSwitchUserToken.php: token to
store the logged in user as secondary data.
WebserviceUser.php: our
user data entity
WebserviceUserProvider.php: queries for user data.
I'm working on a creating a custom authentication provider. I've written my own Authentication Provider, Listener, Token and everything. It's based off a form login, and I've stepped through the code and everything seems to be configured properly. Everything is called in the right order, and my authentication provider is invoked perfectly. The authentication provider successfully authenticates the user, and returns the authenticated token. I extend AbstractAuthenticationListener which, in the handle method, will set the security context.
The user seems to be logged in, but in the debug toolbar, the token is not set and I see "You are not authenticated" and "No token".
Is there any configuration settings that I'm missing? Why would the user would be logging in, authentication provider returning successfully, with an authenticated token, being set in the security context but still be not authenticated? Any tips on how to debug this?
(I will post code as needed.)
EDIT: Token Definition:
This is very simple, just extending from AbstractToken:
class UserToken extends AbstractToken
{
private $username;
private $password;
private $domain;
private $providerKey;
public function __construct($username, $password, $domain, $provider_key, array $roles = array('ROLE_USER'))
{
parent::__construct($roles);
$this->username = $username;
$this->password = $password;
$this->domain = $domain;
$this->providerKey = $provider_key;
}
public function getCredentials()
{
return '';
}
function getUsername() {
return $this->username;
}
function getDomain() {
return $this->domain;
}
function getPassword() {
return $this->password;
}
function getProviderKey(){
return $this->providerKey;
}
}
Authentication Listener:
class Listener extends AbstractAuthenticationListener
{
protected $authenticationManager;
public function __construct(
SecurityContextInterface $securityContext,
AuthenticationManagerInterface $authenticationManager,
SessionAuthenticationStrategyInterface $sessionStrategy,
HttpUtils $httpUtils,
$providerKey,
AuthenticationSuccessHandlerInterface $successHandler,
AuthenticationFailureHandlerInterface $failureHandler,
array $options = array(),
LoggerInterface $logger = null,
EventDispatcherInterface $dispatcher = null
//CsrfProviderInterface $csrfProvider = null
) {
parent::__construct(
$securityContext,
$authenticationManager,
$sessionStrategy,
$httpUtils,
$providerKey,
$successHandler,
$failureHandler,
array_merge(
array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'domain_parameter' => '_domain',
'csrf_parameter' => '_csrf_token',
'intention' => 'authenticate',
'post_only' => true,
),
$options
),
$logger,
$dispatcher
);
}
/**
* Performs authentication.
*
* #param Request $request A Request instance
*
* #return TokenInterface|Response|null The authenticated token, null if full authentication is not possible, or a Response
*
* #throws AuthenticationException if the authentication fails
*/
protected function attemptAuthentication(Request $request)
{
// Create initial unauthenticated token and pass data to the authentication manager.
// TODO validate request data.
$username = trim($request->request->get($this->options['username_parameter'], null, true));
$password = $request->request->get($this->options['password_parameter'], null, true);
$domain = $request->request->get($this->options['domain_parameter'], null, true);
$token = $this->authenticationManager->authenticate(new UserToken($username, $password, $domain, $this->providerKey));
return $token;
}
}
The above code will invoke the the auth function on the provider via the AuthenticationManager:
//This is from the AuthenticationProvider
public function authenticate(TokenInterface $token) {
$loginHandler = new LoginAuthenticationHandler($token->getUsername(), $token->getPassword(), $token->getDomain());
//This code just calls our web service and authenticates. I removed some business logic here, but this shows the gist of it.
if(!$boAuthenticationToken = $loginHandler->authenticate())
{
throw new AuthenticationException('Bad credentials');
}
else{
$user = $this->userProvider->loadUserByUsername($token->getUsername());
//$user = $this->userProvider->getUser($token, $boAuthenticationToken);
// Set the user which will be invoked in the controllers.
$token->setUser($user);
$token->setAuthenticated(true);
return $token;
}
}
Bundle Services.yml
parameters:
services:
ws.security.authentication.provider:
#http://blog.vandenbrand.org/2012/06/19/symfony2-authentication-provider-authenticate-against-webservice/
class: Aurora\OurCustomBundle\Security\Authentication\Provider\Provider
arguments: ["bo_remove_this_with_bo_auth_service", "", "#security.user_checker", "", "#security.encoder_factory"]
ws.security.authentication.listener:
class: Aurora\OurCustomBundle\Security\Firewall\Listener
parent: security.authentication.listener.abstract
abstract: true
#arguments: []
arguments: ["#security.context", "#security.authentication.manager", "#security.authentication.session_strategy", "#security.http_utils", "ws.user_provider", "#security.authentication.customized_success_handler", "#main.cas.rest.user.authentication.failure.service"]
ws.user_provider:
class: Aurora\OurCustomBundle\Security\User\UserProvider
And lastly, the UserProvider
class UserProvider implements UserProviderInterface
{
public function loadUserByUsername($username)
{
//Just return a simple user for now.
return new User($username, array('ROLE_USER'));
}
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 === 'Aurora\OurCustomBundle\Security\User\User';
}
}
After many hours of hair pulling, I figured out the problem!
The token implementation was incorrect. Since I was implementing my own Token, which extends from AbstractToken, I needed to also implement the serialize() and unserialize() functions.
Once I did that, the code worked. The updated Token class is below for future reference:
class UserToken extends AbstractToken
{
private $username;
private $password;
private $domain;
private $providerKey;
public function __construct($username, $password, $domain, $provider_key, array $roles = array('ROLE_USER'))
{
parent::__construct($roles);
$this->username = $username;
$this->password = $password;
$this->domain = $domain;
$this->providerKey = $provider_key;
}
public function getCredentials()
{
return '';
}
function getUsername() {
return $this->username;
}
function getDomain() {
return $this->domain;
}
function getPassword() {
return $this->password;
}
function getProviderKey(){
return $this->providerKey;
}
function serialize(){
return serialize(array($this->username, $this->password, $this->domain, parent::serialize()));
}
function unserialize($serialized){
list($this->username, $this->password, $this->domain, $parentSerialization) = unserialize($serialized);
parent::unserialize($parentSerialization);
}
}