Symfony2 custom password encoder and password hash update - wordpress

I'm learning Symfony2 by moving some wordpress blog to Symfony. I'm stuck with login procedure. Wordpress uses non standard password hashing like $P$.... and I want to check users against old password hash when they login and when password is correct, rehash it to bcrypt. So far I created custome encoder class to use with symfony security mechanism.
<?php
namespace Pkr\BlogUserBundle\Service\Encoder;
use PHPassLib\Application\Context;
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
use Symfony\Component\Security\Core\Util\SecureRandom;
class WpTransitionalEncoder implements PasswordEncoderInterface
{
public function __construct($cost = 13)
{
$secure = new SecureRandom();
$this->_bcryptEncoder = new BCryptPasswordEncoder($secure, $cost);
}
public function isPasswordValid($encoded, $raw, $salt)
{
if (preg_match('^\$P\$', $encoded)) {
$context = new Context();
$context->addConfig('portable');
return $context->verify($raw, $encoded);
}
return $this->_bcryptEncoder->isPasswordValid($encoded, $raw, $salt);
}
public function encodePassword($raw, $salt)
{
return $this->_bcryptEncoder->encodePassword($raw, $salt);
}
}
I'm using it as a service:
#/src/Pkr/BlogUserBundle/Resources/config/services.yml
services:
pkr_blog_user.wp_transitional_encoder:
class: Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder
And in security.yml:
#/app/config/security.yml
security:
encoders:
Pkr\BlogUserBoundle\Entity\User:
id: pkr_blog_user.wp_transitional_encoder
cost: 15
My questions are:
How do I pass parameters to my encoder service form within security.yml?
I'm asking because cost: 15 does not work.
Where should I put password hash update logic? I was thinking that maby just after password validation something like this:
public function isPasswordValid($encoded, $raw, $salt)
{
if (preg_match('^\$P\$', $encoded)) {
$context = new Context();
$context->addConfig('portable');
$isValid = $context->verify($raw, $encoded);
if ($isValid) {
// put logic here...
}
return $isValid;
}
return $this->_bcryptEncoder->isPasswordValid($encoded, $raw, $salt);
}
but it seem somehow like wrong place for it. So what is the right way?

I'll answer my own question.
I placed parameters for my encoder service inside config.yml
pkr_blog_user:
password_encoder:
cost: 17
They will be passed to my bundle extension class:
# /src/Pkr/BlogUserBundle/DependencyInjection/PkrBlogUserExtension.php
namespace Pkr\BlogUserBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
/**
* This is the class that loads and manages your bundle configuration
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class PkrBlogUserExtension extends Extension
{
/**
* {#inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
if ($config['password_encoder']['cost'] < 10) {
$config['password_encoder']['cost'] = sprintf('%02d', $config['password_encoder']['cost']);
}
$container->setParameter('pkr_blog_user.wp_transitional_encoder.cost', $config['password_encoder']['cost']);
}
}
I found out that I could use my own authentication success handler so there is a good place to put password rehash logic. Unfortunately when using custom handler symfony2 won't pass config to class constructor but I found a way to make it work. I described it here:
https://stackoverflow.com/a/15988399/1089412

Related

Symfony 4 Accessing Swift_Mailer in Service

I have been looking at the Symfony 4.1 documentation on using the Swift_mailer. However, it appears the documentation is only assumed it being used in the Controller classes. I'm trying to create a Service with some reusable functions that send email.
I created a EmailService.php file in my service directory. When creating a new instance of this service, it quickly throws and error:
"Too few arguments to function
App\Service\EmailService::__construct(), 0 passed in
*MyApp\src\Controller\TestController.php on line 33
and exactly 1 expected"
I'm not sure how to pass \Swift_Mailer $mailer into the __construct correctly? I have auto wiring enabled in the services.yaml, so i'm not sure what I need to do differently?
class EmailService
{
private $from = 'support#******.com';
private $mailer;
public function __construct(\Swift_Mailer $mailer)
{
$this->mailer = $mailer;
}
How do I pass the \Swift_Mailer into this EmailService construct?
I tried adding this to my config\services.yaml with no success:
App\Service\EmailService:
arguments: ['#mailer']
As mentioned by dbrumann in a comment, I needed to follow the proper way of injecting services.
First, I needed to add the services to config/services.yaml
#config/services.yaml
emailservice:
class: App\Service\EmailService
arguments: ['#swiftmailer.mailer.default', '#twig']
public: true
Second, I need to setup the service to accept both the mailer, and twig for rendering the template.
#App/Service/EmailService.php
<?php
namespace App\Service;
class EmailService
{
private $from = 'support#*****.com';
private $mailer;
private $templating;
public function __construct(\Swift_Mailer $mailer, \Twig\Environment $templating)
{
$this->mailer = $mailer;
$this->templating = $templating;
}
public function userConfirmation(string $recipient, string $confCode) : bool
{
$message = (new \Swift_Message())
->setSubject('Some sort of string')
->setFrom($this->from)
->setTo($recipient)
->setBody(
$this->templating->render(
'email/UserConfirmation.html.twig',
array('confCode' => $confCode)
),
'text/html'
)
/*
* If you also want to include a plaintext version of the message
->addPart(
$this->renderView(
'emails/UserConfirmation.txt.twig',
array('confCode' => $confCode)
),
'text/plain'
)
*/
;
return $this->mailer->send($message);
}
}
Third, to call it from the controller, make sure your controller is extending Controller and not the AbstractController! Crucial step!! Here is an example based on the parameters I require in my service:
public function userConfirmation()
{
$emailService = $this->get('emailservice');
$sent = $emailService->userConfirmation('some#emailaddress.com', '2ndParam');
return new Response('Success') //Or whatever you want to return
}
I hope this helps people. AbstractController does not give you the proper access to the service containers.
#config/services.yaml
App\Service\EmailService
arguments: ['#swiftmailer.mailer.default']
public: true
And in your controller :
public function userConfirmation(EmailService $emailService)
{
$sent = $emailService->userConfirmation('some#emailaddress.com', '2ndParam');
return new Response('Success') //Or whatever you want to return
}
Use FQCN "App\Service\MyService" to declare services in services.yaml and a proper legacy_aliases.yaml file to declare legacy aliases like "app.service.my.service" it helps keep your services.yaml clean...

How to impersonate user by id instead of username in symfony?

I can't figure out how to impersonate a user by user's id instead of user's username in Symfony?
The following trick which works with username can't work with id, as symfony is looking for username:
?_switch_user={id}
This is impossible to do without implementing your own firewall listener, as behind the scenes it loads the user from the userprovider (which only has a loadUserByUsername() method in its interface).
You could however implement your own firewall listener and get inspired by having a look at the code in Symfony\Component\Security\Http\Firewall\SwitchUserListener. For detailed information on implementing your own authentication provider, check the cookbook article.
EDIT:
One possible solution might be registering an extra request listener:
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LookupSwitchUserListener implements EventSubscriberInterface
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['lookup', 12] // before the firewall
];
}
public function lookup(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->has('_switch_user') {
return; // do nothing if already a _switch_user param present
}
if (!$id = $request->query->has('_switch_user_by_id')) {
return; // do nothing if no _switch_user_by_id param
}
// lookup $username by $id using the repository here
$request->attributes->set('_switch_user', $username);
}
}
Now register this listener in the service container:
services:
my_listener:
class: LookupSwitchUserListener
tags:
- { name: kernel.event_subscriber }
Calling a url with the ?_switch_user_by_id=xxx parameter should now correctly look up the username and set it so the SwitchUserListener can switch to the specified user.

Symfony2 conditional service declaration

I'm currently trying to find a solid solution to change the dependencies of a Symfony2 service dynamically. In detail: I have a Services which uses a HTTP-Driver to communicate with an external API.
class myAwesomeService
{
private $httpDriver;
public function __construct(
HTTDriverInterface $httpDriver
) {
$this->httpDriver = $httpDriver;
}
public function transmitData($data)
{
$this->httpDriver->dispatch($data);
}
}
While running the Behat tests on the CI, I'd like to use a httpMockDriver instead of the real driver because the external API might be down, slow or even broken and I don't want to break the build.
At the moment I'm doing something like this:
<?php
namespace MyAwesome\TestBundle\DependencyInjection;
class MyAwesomeTestExtension extends Extension
{
/**
* {#inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new
FileLocator(__DIR__.'/../Resources/config'));
$environment = //get environment
if ($environment == 'test') {
$loader->load('services_mock.yml');
} else {
$loader->load('services.yml');
}
}
}
This works for now, but will break for sure. So, is there a more elegant/solid way to change the HTTPDriver dynamically?
I finally found a solution that looks solid to me. As of Symfony 2.4 you can use the expression syntax: Using the Expression Language
So I configured my service this way.
service.yml
parameters:
httpDriver.class: HTTP\Driver\Driver
httpMockDriver.class: HTTP\Driver\MockDriver
myAwesomeService.class: My\Awesome\Service
service:
myAwesomeService:
class: "%myAwesomeService.class%"
arguments:
- "#=service('service_container').get('kernel.environment') == 'test'? service('httpMockDriver) : service('httpDriver)"
This works for me.

Symfony2 access Doctrine Entity Manager in custom class

UPDATE:
In case you need to work with Entity Manager in a custom class, you could go this way:
put this code in your bundle:
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpFoundation\Request;
require_once DIR . '/../../../app/bootstrap.php.cache';
require_once DIR . '/../../../app/AppKernel.php';
class ApplicationBoot {
private static $kernel;
public static function getContainer() {
if(self::$kernel instanceof \AppKernel) {
if(!self::$kernel->getContainer() instanceof Container){
self::$kernel->boot();
}
return self::$kernel->getContainer();
}
$environment = 'prod';
if (!array_key_exists('REMOTE_ADDR', $_SERVER) || in_array(#$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1', 'localhost'))) {
$environment = 'dev';
}
self::$kernel = new \AppKernel($environment, false);
self::$kernel->boot();
return self::$kernel->getContainer();
}
public static function shutDown() {
self::$kernel->shutdown();
}}
So now you can access EntityManager:
$container = ApplicationBoot::getContainer();
$entityManager = $container->get('doctrine')->getEntityManager();
I have not seen a service file like this:
arguments:
entityManager: "#doctrine.orm.entity_manager"
Probably should be:
arguments: [#doctrine.orm.entity_manager]
UPDATE:
Based on some comments it appears that you are trying to do:
$job = new PostJob();
And expecting that entity manager will somehow be passed. And that is just not the way things work. You need to do:
$job = $this->get('postjob.service.id');
In order to have the Symfony 2 dependency injection work. Review the chapter in the manual on services. It might seem a bit over whelming at first but once you get a few services working then it becomes second nature.
To load the services.yml from your bundle, you need to provide an extension class:
// src/Vendor/YourBundle/DedendencyInjection/VendorYourBundleExtension.php
namespace Vendor\YourBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension,
Symfony\Component\DependencyInjection\ContainerBuilder,
Symfony\Component\DependencyInjection\Loader\YamlFileLoader,
Symfony\Component\Config\FileLocator;
class VendorYourBundleExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}

Dynamically adding roles to a user

We are using Symfony2's roles feature to restrict users' access to certain parts of our app. Users can purchase yearly subscriptions and each of our User entities has many Subscription entities that have a start date and an end.
Now, is there a way to dynamically add a role to a user based on whether they have an 'active' subscription? In rails i would simply let the model handle whether it has the necessary rights but I know that by design symfony2 entities are not supposed to have access to Doctrine.
I know that you can access an entity's associations from within an entity instance but that would go through all the user's subscription objects and that seems unnecessaryly cumbersome to me.
I think you would do better setting up a custom voter and attribute.
/**
* #Route("/whatever/")
* #Template
* #Secure("SUBSCRIPTION_X")
*/
public function viewAction()
{
// etc...
}
The SUBSCRIPTION_X role (aka attribute) would need to be handled by a custom voter class.
class SubscriptionVoter implements VoterInterface
{
private $em;
public function __construct($em)
{
$this->em = $em;
}
public function supportsAttribute($attribute)
{
return 0 === strpos($attribute, 'SUBSCRIPTION_');
}
public function supportsClass($class)
{
return true;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
// run your query and return either...
// * VoterInterface::ACCESS_GRANTED
// * VoterInterface::ACCESS_ABSTAIN
// * VoterInterface::ACCESS_DENIED
}
}
You would need to configure and tag your voter:
services:
subscription_voter:
class: SubscriptionVoter
public: false
arguments: [ #doctrine.orm.entity_manager ]
tags:
- { name: security.voter }
Assuming that you have the right relation "subscriptions" in your User Entity.
You can maybe try something like :
public function getRoles()
{
$todayDate = new DateTime();
$activesSubscriptions = $this->subscriptions->filter(function($entity) use ($todayDate) {
return (($todayDate >= $entity->dateBegin()) && ($todayDate < $entity->dateEnd()));
});
if (!isEmpty($activesSubscriptions)) {
return array('ROLE_OK');
}
return array('ROLE_KO');
}
Changing role can be done with :
$sc = $this->get('security.context')
$user = $sc->getToken()->getUser();
$user->setRole('ROLE_NEW');
// Assuming that "main" is your firewall name :
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, null, 'main', $user->getRoles());
$sc->setToken($token);
But after a page change, the refreshUser function of the provider is called and sometimes, as this is the case with EntityUserProvider, the role is overwrite by a query.
You need a custom provider to avoid this.

Resources