Actually, I have a listener on AuthenticationEvents::AUTHENTICATION_FAILURE that stores failedLogin in Redis Cache, like:
[
'ip' => [
'xxx.xxx.xxx.xxx' => [
'nbAttempts' => 5,
'lastAttempd' => \DateTime
],
],
'username' => [
'my_login' => [
'nbAttempts' => 3,
'lastAttempd' => \DateTime
],
'my_other_login' => [
'nbAttempts' => 2,
'lastAttempd' => \DateTime
],
]
]
But now, I need to use this list of fails to prevent logins when a user try to connect with a username tries more than x times in n minutes, and the same for an IP (with an other ratio). (later, maybe add a ReCaptcha before block)
To do it, I need to add a custom validation rules on the login. I've found it in the documentation:
http://symfony.com/doc/current/security/custom_password_authenticator.html
https://symfony.com/doc/current/security/guard_authentication.html
But, in both documents, I need to rewrite a lot of things, but I want to keep all the actual behaviors: redirect user on previous page (with referer or on a default page), remember me (in the gurad, I'me forced to return a response on success, else remember me don't work, but I don't really know which response return.... Because if I return null, the redirection work well), messages, etc...
I've search but not found the guard used per default by Symfony to copy/paste it, and just add one rule.
Someone know an other manner, that just consist to rewrite the checkCredential ?
Thanks a lot
EDIT (see the answer at the end):
I've found an advanced guard abstract class: Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator.Then, the authentication work like in Symfony, now, I just need to add my own test in checkCredentials (in my case in the getUser(), I prefer return the error before retrieve the user.
You can listen on the event for failed login attempts. Create a service:
services:
app.failed_login_listener:
class: AppBundle\EventListener\AuthenticationFailureListener
tags:
- { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure }
Then create the listener:
<?php
namespace App\EventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
class AuthenticationFailureListener implements AuthenticationFailureHandlerInterface
{
public function onAuthenticationFailure(
Request $request,
AuthenticationException $exception
) {
// do whatever
}
}
Modify your service definition to inject whatever other services you may need.
If you want to perform actions after the user logs in, you can do that with the security.interactive_login event. Just throw exceptions if you encounter situations where you want the void the user's login, and perhaps remove their security token or whatever else you need. You could even do this in your Controller's login action.
For example:
services:
app.security_listener:
class: AppBundle\EventListener\InteractiveLoginListener
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onInteractiveLogin }
Then have your listener:
<?php
namespace App\EventListener;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class InteractiveLoginListener
{
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
// do whatever
}
}
Again inject dependencies as needed. Also look at Symfony's creating a custom authentication provider documentation.
Finally, I've found a simply way to do by extending this abstract class: Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator. This Authenticator replace the default FormLoginAuthenticator used by Symfony, but is very simple, and we just rewrite few methods.
Maybe just found a way to get config.yml value, to define routes (avoid to write it in this file, because we declare it in config).
My service declaration:
app.security.form_login_authenticator:
class: AppBundle\Security\FormLoginAuthenticator
arguments: ["#router", "#security.password_encoder", "#app.login_brute_force"]
My FormLoginAuthenticator:
<?php
namespace AppBundle\Security;
use AppBundle\Utils\LoginBruteForce;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Router;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
private $router;
private $encoder;
private $loginBruteForce;
public function __construct(Router $router, UserPasswordEncoderInterface $encoder, LoginBruteForce $loginBruteForce)
{
$this->router = $router;
$this->encoder = $encoder;
$this->loginBruteForce = $loginBruteForce;
}
protected function getLoginUrl()
{
return $this->router->generate('login');
}
protected function getDefaultSuccessRedirectUrl()
{
return $this->router->generate('homepage');
}
public function getCredentials(Request $request)
{
if ($request->request->has('_username')) {
return [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
];
}
return;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
// Check if the asked username is under bruteforce attack, or if client process to a bruteforce attack
$this->loginBruteForce->isBruteForce($username);
// Catch the UserNotFound execption, to avoid gie informations about users in database
try {
$user = $userProvider->loadUserByUsername($username);
} catch (UsernameNotFoundException $e) {
throw new AuthenticationException('Bad credentials.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
$passwordValid = $this->encoder->isPasswordValid($user, $credentials['password']);
if (!$passwordValid) {
throw new AuthenticationException('Bad credentials.');
}
return true;
}
}
And, if it's interesting someone, my LoginBruteForce:
<?php
namespace AppBundle\Utils;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class LoginBruteForce
{
// Define constants used to define how many tries we allow per IP and login
// Here: 20/10 mins (IP); 5/10 mins (username)
const MAX_IP_ATTEMPTS = 20;
const MAX_USERNAME_ATTEMPTS = 5;
const TIME_RANGE = 10; // In minutes
private $cacheAdapter;
private $requestStack;
public function __construct(AdapterInterface $cacheAdapter, RequestStack $requestStack)
{
$this->cacheAdapter = $cacheAdapter;
$this->requestStack = $requestStack;
}
private function getFailedLogins()
{
$failedLoginsItem = $this->cacheAdapter->getItem('failedLogins');
$failedLogins = $failedLoginsItem->get();
// If the failedLogins is not an array, contruct it
if (!is_array($failedLogins)) {
$failedLogins = [
'ip' => [],
'username' => [],
];
}
return $failedLogins;
}
private function saveFailedLogins($failedLogins)
{
$failedLoginsItem = $this->cacheAdapter->getItem('failedLogins');
$failedLoginsItem->set($failedLogins);
$this->cacheAdapter->save($failedLoginsItem);
}
private function cleanFailedLogins($failedLogins, $save = true)
{
$actualTime = new \DateTime('now');
foreach ($failedLogins as &$failedLoginsCategory) {
foreach ($failedLoginsCategory as $key => $failedLogin) {
$lastAttempt = clone $failedLogin['lastAttempt'];
$lastAttempt = $lastAttempt->modify('+'.self::TIME_RANGE.' minute');
// If the datetime difference is greatest than 15 mins, delete entry
if ($lastAttempt <= $actualTime) {
unset($failedLoginsCategory[$key]);
}
}
}
if ($save) {
$this->saveFailedLogins($failedLogins);
}
return $failedLogins;
}
public function addFailedLogin(AuthenticationFailureEvent $event)
{
$clientIp = $this->requestStack->getMasterRequest()->getClientIp();
$username = $event->getAuthenticationToken()->getCredentials()['username'];
$failedLogins = $this->getFailedLogins();
// Add clientIP
if (array_key_exists($clientIp, $failedLogins['ip'])) {
$failedLogins['ip'][$clientIp]['nbAttempts'] += 1;
$failedLogins['ip'][$clientIp]['lastAttempt'] = new \DateTime('now');
} else {
$failedLogins['ip'][$clientIp]['nbAttempts'] = 1;
$failedLogins['ip'][$clientIp]['lastAttempt'] = new \DateTime('now');
}
// Add username
if (array_key_exists($username, $failedLogins['username'])) {
$failedLogins['username'][$username]['nbAttempts'] += 1;
$failedLogins['username'][$username]['lastAttempt'] = new \DateTime('now');
} else {
$failedLogins['username'][$username]['nbAttempts'] = 1;
$failedLogins['username'][$username]['lastAttempt'] = new \DateTime('now');
}
$this->saveFailedLogins($failedLogins);
}
// This function can be use, when the user reset his password, or when he is successfully logged
public function resetUsername($username)
{
$failedLogins = $this->getFailedLogins();
if (array_key_exists($username, $failedLogins['username'])) {
unset($failedLogins['username'][$username]);
$this->saveFailedLogins($failedLogins);
}
}
public function isBruteForce($username)
{
$failedLogins = $this->getFailedLogins();
$failedLogins = $this->cleanFailedLogins($failedLogins, true);
$clientIp = $this->requestStack->getMasterRequest()->getClientIp();
// If the IP is in the list
if (array_key_exists($clientIp, $failedLogins['ip'])) {
if ($failedLogins['ip'][$clientIp]['nbAttempts'] >= self::MAX_IP_ATTEMPTS) {
throw new AuthenticationException('Too many login attempts. Please try again in '.self::TIME_RANGE.' minutes.');
}
}
// If the username is in the list
if (array_key_exists($username, $failedLogins['username'])) {
if ($failedLogins['username'][$username]['nbAttempts'] >= self::MAX_USERNAME_ATTEMPTS) {
throw new AuthenticationException('Maximum number of login attempts exceeded for user: "'.$username.'". Please try again in '.self::TIME_RANGE.' minutes.');
}
}
return;
}
}
Related
I am trying to force filters or pagination dynamically using a ContextBuilder.
For example, I want to force pagination for the group public:read:
namespace App\Serializer;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
final class FooContextBuilder implements SerializerContextBuilderInterface
{
private $decorated;
public function __construct(SerializerContextBuilderInterface $decorated)
{
$this->decorated = $decorated;
}
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
if (($resourceClass === Foo::class
$context['operation_type'] ?? null) === 'collection' &&
true === $normalization
) {
if ((isset($context['groups']) &&
in_array('public:read', $context['groups'])
) {
$context['filters']['pagination'] = true;
}
}
return $context;
}
}
services.yml:
services:
...
App\Serializer\RouteContextBuilder:
decorates: 'api_platform.serializer.context_builder'
arguments: [ '#App\Serializer\RouteContextBuilder.inner' ]
autoconfigure: false
Unfortunately, it seems that $context['filters'] is built as a later stage as it is not available in the ContextBuilder yet. $context['filters'] is available later e.g. in a DataProvider.
I tried to change the decoration priority in services.yml without success:
services:
App\Serializer\RouteContextBuilder:
...
decoration_priority: -1
How can I add dynamic filters or pagination through the context? Is there another interface that can be decorated which is called a later stage of the normalization process and before the filters are applied?
The serialization process is executed after data retrieval this can't work. Use a data Provider.
As I said in the title I try to supply the validation context of Sf / Api platform.
More precisely I would like to have different validation groups depending on an entity value.
If i'm a User with ROLE_PRO : then i want validate:pro and
default as validation groups.
If i'm a User with ROLE_USER : then i want default as validation
group.
I tried to create an event based on the following api-platform event but I can't find a way to supply the ExecutionContextInterface with my validation groups
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['addGroups', EventPriorities::PRE_VALIDATE],
];
}
As you can see in api-platform documentation (https://api-platform.com/docs/core/serialization/#changing-the-serialization-context-dynamically) you can manipulate validation groups dynamically with a service.
First of all, in your api-platform configuration, you have to define default validation group:
App\Class\MyClass:
properties:
id:
identifier: true
attributes:
input: false
normalization_context:
groups: ['default']
You need to define a new service which implements SerializerContextBuilderInterface
class ContextBuilder implements SerializerContextBuilderInterface
{
private SerializerContextBuilderInterface $decorated;
private AuthorizationCheckerInterface $authorizationChecker;
public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker)
{
$this->decorated = $decorated;
$this->authorizationChecker = $authorizationChecker;
}
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
if (isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_PRO') && true === $normalization) {
$context['groups'][] = 'validate:pro';
}
return $context;
}
}
Also, you need to configure your new service with a decorator
App\Builder\ContextBuilder:
decorates: 'api_platform.serializer.context_builder'
arguments: [ '#App\Builder\ContextBuilder.inner' ]
What it's happening here is:
You're overriding the ContextBuilder. First of all you create the context from request and from configuration (first line of createFromRequest method) and after this, you modify the context depeding of which user is logged.
Thanks!
I have this situation
abstract class Importer {
const NW = 1;
public static function getInstance($type)
{
switch($type)
{
case(self::NW):
return new NWImporter();
break;
}
}
protected function saveObject(myObject $myObject)
{
//here I need to use doctrine to save on mongodb
}
abstract function import($nid);
}
and
class NWImporter extends Importer
{
public function import($nid)
{
//do some staff, create myObject and call the parent method to save it
parent::saveObject($myObject);
}
}
and I want to use them like this
$importer = Importer::getInstance(Importer::NW);
$importer->import($nid);
my question is: how to inject doctrine to be used in saveObject method?
thanks
You need to configure your importer as a symfony service :
services:
test.common.exporter:
# put the name space of your class
class: Test\CommonBundle\NWImporter
arguments: [ "#doctrine" ]
then in NWImporter define a constructor with a parameter that will have the doctrine instance
public function __construct($doctrine)
{
$this->doctrine= $doctrine;
}
with this solution you can avoid using a factory method as symfony does it for you but if you wanna to keep it, When you call $importer = Importer::getInstance(Importer::NW); from your controller you can inject the doctrine argument in your factory method :
abstract class Importer {
const NW = 1;
public static function getInstance($type, $doctrine)
{
switch($type)
{
case(self::NW):
return new NWImporter($doctrine);
break;
}
}
protected function saveObject(myObject $myObject)
{
//here I need to use doctrine to save on mongodb
}
abstract function import($nid);
}
then in your controller you should to do something like that :
$doctrine = $this->container->get('doctrine');
$importer = Importer::getInstance(Importer::NW, $doctrine);
$importer->import($nid);
Assume we have singleton class
class Registry {
private static $_instance;
private function __construct() {}
private function __wakeup() {}
private function __clone() {}
private $_map = array();
public static function getInstance () {
if (self::$_instance === null)
self::$_instance = new self();
return self::$_instance;
}
public function set ($key, $val) {
self::getInstance()->_map[$key] = $val;
return self::getInstance();
}
public function get($key)
{
if (array_key_exists($key, self::getInstance()->_map))
return self::getInstance()->_map[$key];
return null;
}
}
And we have simple Symfony2 Controller with 2 actions
class IndexController {
public function indexAction () {
Registry::getInstance()->set('key',true);
return new Response(200);
}
public function secondAction () {
$val = Registry::getInstance()->get('key');
return new Response(200);
}
}
I call index action, then second action. But I can't find key, that was set in first action. I think, new instance of singleton creates in my second action. Why object is not saved in memory? What do I do wrong?
If you call indexAction and secondAction in different requests it won't work the way you want it because your Registry instance is not shared between requests.
Singleton itself does not store anything "in memory" (BTW Singleton is now considered as an anti-pattern).
What, I think, you want to achieve can be done by using session storage. Check doc for more info how to implement this.
How would I go about binding a Symfony config tree to a class rather than returning an array?
Using Symfony\Component\Config\Definition\Processor returns an array.
In my case I want the config to be bound to a class so I can use methods to combine parts of the data.
Here is a simple example of my use case. I want the config bound to a class so I can use a method to join table.name and table.version together (my actual use case is more complex, but this is a simple example)
config.yml
db:
table:
name: some_table
version: v2
ConfigurationInterface
class DBConfiguration implements ConfigurationInterface
{
/**
* {#inheritDoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('db');
$rootNode
->children()
->arrayNode('table')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('version')->end()
->end()
->end()
;
return $treeBuilder;
}
}
Class I want to bind the config to
class DB
{
public $table;
public function __construct()
{
$this->table = new Table();
}
}
class Table
{
public $name;
public $version;
/**
* #return string
* Calculate the full table name.
*/
public function getTableName()
{
return $this->name.'-'.$this->version;
}
}
The Symfony Config component doesn't support that.
However, in a Symfony project, this is usually done at the container compile phase. In your bundle's Extension class, you will have access to the configuration tree of your bundle in array form.
You can then take this array and assign it to a service defined in the service container that will create your config object.
This is exactly how DoctrineBundle's configuration class is built:
Abstract services (for the configuration and the factory) are defined in dbal.xml
When loading DoctrineBundle's extension, an instance of the abstract config service is created for each defined connection.
An instance of the abstract factory service is created for each defined connection.
The options array is then passed to the abstract factory service along with the configuration
When creating an instance, the factory then does the necessary transformations.
As far as I know, Symfony has no native support for this, however, you could implement it yourself. You could use subset of Symfony Serializer Component in charge of deserialization, but I think it would be an overkill. Especially since I don't see any PublicPropertyDenormalizer, only GetSetMethodNormalizer (which is denormalizer too). Therefor you would have to either make your config objects have get/set methods or roll PublicPropertyDenormalizer on your own. Possible but it really seems like an overkill and doesn't look like helping much:
Symfony Serializer Component
$array = [
'field1' => 'F1',
'subobject' => [
'subfield1' => 'SF1',
],
];
class MyConfigObject implements Symfony\Component\Serializer\Normalizer\DenormalizableInterface
{
private $field1;
private $subobject;
public function getField1()
{
return $this->field1;
}
public function setField1($field1)
{
$this->field1 = $field1;
}
public function getSubobject()
{
return $this->subobject;
}
public function setSubobject(SubObject $subobject)
{
$this->subobject = $subobject;
}
public function denormalize(\Symfony\Component\Serializer\Normalizer\DenormalizerInterface $denormalizer, $data, $format = null, array $context = array())
{
$obj = new static();
$obj->setField1($data['field1']);
$obj->setSubobject($denormalizer->denormalize($data['subobject'], 'SubObject'));
return $obj;
}
}
class SubObject
{
private $subfield1;
public function getSubfield1()
{
return $this->subfield1;
}
public function setSubfield1($subfield1)
{
$this->subfield1 = $subfield1;
}
}
$normalizer = new \Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer();
$obj = (new MyConfigObject())->denormalize($normalizer, $array);
Native PHP Way
Imo this is a lot easier than above as Symfony Serializer wasn't really ment for that.
$array = [
'field1' => 'F1',
'subobject' => [
'subfield1' => 'SF1',
],
];
trait Denormalizable
{
public function fromArray($array)
{
foreach ($array as $property => $value) {
if (is_array($value)) {
if ($this->$property instanceof ArrayDenormalizableInterface) {
$this->$property->fromArray($value);
} else {
$this->$property = $value;
}
} else {
$this->$property = $value;
}
}
}
}
interface ArrayDenormalizableInterface
{
public function fromArray($array);
}
class MyConfigObject implements ArrayDenormalizableInterface
{
use Denormalizable;
public $field1;
public $subobject;
public function __construct()
{
$this->subobject = new SubObject();
}
}
class SubObject implements ArrayDenormalizableInterface
{
use Denormalizable;
public $subfield1;
}
$myConf = new MyConfigObject();
$myConf->fromArray($array);
Whatever way you choose, you can now just take array returned from symfony processor and turn it into a config object you need.