├── Resources ├── doc │ ├── index.rst │ ├── Hooking into Symfony.md │ ├── Installation.md │ └── changelog.txt ├── views │ ├── Guard │ │ ├── statistics.html.twig │ │ └── statistics_content.html.twig │ └── Entity │ │ └── editrow.html.twig ├── config │ ├── routing.yml │ └── services.yml ├── meta │ └── LICENSE └── translations │ ├── metaclass_auth_guard.zh_CN.yml │ ├── metaclass_auth_guard.en.yml │ └── metaclass_auth_guard.nl.yml ├── .gitignore ├── MetaclassAuthenticationGuardBundle.php ├── Exception ├── UsernameBlockedException.php ├── IpAddressBlockedException.php ├── AuthenticationBlockedException.php ├── UsernameBlockedForIpAddressException.php └── UsernameBlockedForCookieException.php ├── composer.json ├── DependencyInjection ├── MetaclassAuthenticationGuardExtension.php └── Configuration.php ├── Tests └── Service │ └── TresholdsGovernorTest.php ├── Form └── Type │ └── StatsPeriodType.php ├── README.md ├── Service └── UsernamePasswordFormAuthenticationGuard.php └── Controller └── GuardStatsController.php /Resources/doc/index.rst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.buildpath 3 | /.project 4 | /.idea/ 5 | -------------------------------------------------------------------------------- /MetaclassAuthenticationGuardBundle.php: -------------------------------------------------------------------------------- 1 | 5 |
{{ form_label(formrow) }}
6 |
{% if formrow.vars.required %} 7 | * 8 | {% endif %}
9 |
{{ form_widget(formrow) }}
10 | {% if (formrow.vars.errors.0 is defined) %} {# Symfony ~2.3 #} 11 |
{{ formrow.vars.errors.0.message }}
12 | {% elseif formrow.vars.errors.valid() is defined and formrow.vars.errors.valid() %} 13 |
{{ formrow.vars.errors.current().message }}
14 | {% endif %} 15 | 16 | {% endif %} -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metaclass-nl/authentication-guard-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Authentication Guard for Symfony 2, aims to protect authentication against brute force and dictionary attacks", 5 | "keywords": ["security", "brute force", "dictionary attack", "login", "authentication"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Henk Verhoeven", 10 | "homepage": "http://www.metaclass.nl/" 11 | } 12 | ], 13 | "minimum-stability": "dev", 14 | "require": { 15 | "php": ">=5.3.3", 16 | "symfony/symfony": "^2.8.1 || ^3.0 || ^3.1", 17 | "metaclass-nl/tresholds-governor": "^0.3@dev", 18 | "doctrine/dbal": "^2.3.4" 19 | }, 20 | "require-dev": { 21 | }, 22 | "suggest": { 23 | }, 24 | "autoload": { 25 | "psr-0": { "Metaclass\\AuthenticationGuardBundle": "" } 26 | }, 27 | "target-dir": "Metaclass/AuthenticationGuardBundle" 28 | } 29 | -------------------------------------------------------------------------------- /Resources/config/routing.yml: -------------------------------------------------------------------------------- 1 | # Web based wser interface for user administators to look into why a user may have been blocked 2 | # 3 | # WARNING: if you just add this yml as a resource to your routing.yml configuration, 4 | # you need to confige your access control to require authentication and administrative rights, 5 | # otherwise anybody will be able to see the user names, ip addresses, date, time and more 6 | # of all login attemtps!!! 7 | # 8 | Guard_statistics: 9 | path: /statistics 10 | defaults: { _controller: "%metaclass_auth_guard.ui.statistics.controller%:statistics" } 11 | 12 | Guard_history: 13 | path: /history/{ipAddress} 14 | defaults: { _controller: "%metaclass_auth_guard.ui.statistics.controller%:history" } 15 | requirements: 16 | ipAddress: "[^/]+" 17 | 18 | Guard_statisticsByUserName: 19 | path: /statistics/{username} 20 | defaults: { _controller: "%metaclass_auth_guard.ui.statistics.controller%:statisticsByUserName" } 21 | requirements: 22 | username: "[^/]*" 23 | -------------------------------------------------------------------------------- /Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 MetaClass Groningen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Resources/translations/metaclass_auth_guard.zh_CN.yml: -------------------------------------------------------------------------------- 1 | statistics: 2 | title: 认证保护 3 | 4 | history: 5 | title: 认证的历史记录 6 | show: Show authentication history 7 | IP Adres: IP地址 8 | statisticsByUserName: 9 | isUsernameBlocked: 账号是/否锁定 10 | 11 | tresholds_governor_params: 12 | countingSince: 统计时间 13 | blockUsernamesFor: 账号锁定时间 14 | blockIpAddressesFor: 用户IP锁定时间 15 | allowReleasedUserOnAddressFor: 释放用户地址时间 16 | limitPerUserName: 账号最大输入密码输错次数 17 | limitBasePerIpAddress: IP地址最大输入密码输错次数 18 | 19 | secu_requests: 20 | username: 账号名称 21 | loginsFailed: 登录失败 22 | loginsSucceeded: 成功登录 23 | col: 24 | dtFrom: 访问时间 25 | username: 账号名称 26 | ipAddress: 访问IP地址 27 | loginsSucceeded: 成功次数 28 | loginsFailed: 失败次数 29 | ipAddressBlocked: IP锁定次数 30 | usernameBlocked: 账号锁定次数 31 | usernameBlockedForIpAddress: name on addresd 32 | usernameBlockedForCookie: name on cookie 33 | blockedColumns: 请求的详细列表 34 | 35 | countsGroupedByIpAddress: 36 | col: 37 | blocked: 限制 38 | usernames: Names 39 | 40 | StatsPeriod: 41 | From: 从 42 | Until: 到 43 | history: 记录 44 | statistics: IP地址列表 45 | submit: 搜索 46 | 47 | relativeDate: 48 | minutes: 分钟 49 | hours: 小时 50 | days: 天 51 | 52 | boolean: 53 | 0: 否 54 | 1: 是 55 | 56 | -------------------------------------------------------------------------------- /DependencyInjection/MetaclassAuthenticationGuardExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 22 | $container->setParameter('metaclass_auth_guard.db_connection.name', $config['db_connection']['name']); 23 | $container->setParameter('metaclass_auth_guard.ui.dateTimeFormat', $config['ui']['dateTimeFormat']); 24 | $container->setParameter('metaclass_auth_guard.statistics.template', $config['ui']['statistics']['template']); 25 | $container->setParameter('metaclass_auth_guard.tresholds_governor_params', $config['tresholds_governor_params']); 26 | 27 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 28 | $loader->load('services.yml'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Resources/translations/metaclass_auth_guard.en.yml: -------------------------------------------------------------------------------- 1 | statistics: 2 | title: Authentication Guard 3 | 4 | history: 5 | title: Authentication history 6 | show: Show authentication history 7 | 8 | statisticsByUserName: 9 | isUsernameBlocked: Username blocked 10 | 11 | tresholds_governor_params: 12 | countingSince: Counting since 13 | blockUsernamesFor: Block usernames for 14 | blockIpAddressesFor: Block IP addresses for 15 | allowReleasedUserOnAddressFor: Release user on address for 16 | limitPerUserName: Maximum per username 17 | limitBasePerIpAddress: Maximum per IP adress 18 | 19 | secu_requests: 20 | username: Username 21 | loginsFailed: Failed logins 22 | loginsSucceeded: Successfull logins 23 | col: 24 | dtFrom: From 25 | username: Username 26 | ipAddress: Address 27 | loginsSucceeded: Succeeded 28 | loginsFailed: Failed 29 | ipAddressBlocked: address 30 | usernameBlocked: name 31 | usernameBlockedForIpAddress: name on addresd 32 | usernameBlockedForCookie: name on cookie 33 | blockedColumns: Number of attempts blocked for 34 | 35 | countsGroupedByIpAddress: 36 | col: 37 | blocked: Blocked 38 | usernames: Names 39 | 40 | StatsPeriod: 41 | From: From 42 | Until: Until 43 | history: History 44 | statistics: IP Adresses 45 | submit: Show 46 | 47 | relativeDate: 48 | minutes: minutes 49 | hours: hours 50 | days: days 51 | 52 | boolean: 53 | 0: No 54 | 1: Yes 55 | 56 | -------------------------------------------------------------------------------- /Resources/translations/metaclass_auth_guard.nl.yml: -------------------------------------------------------------------------------- 1 | statistics: 2 | title: Inlogbeveiliging 3 | 4 | history: 5 | title: Inloghistorie 6 | show: Toon inloghistorie 7 | 8 | statisticsByUserName: 9 | isUsernameBlocked: Gebruikersnaam geblokkeerd 10 | 11 | tresholds_governor_params: 12 | countingSince: Telt sinds 13 | blockUsernamesFor: Blokkeer gebruikersnamen voor 14 | blockIpAddressesFor: Blokkeer IP-adressen voor 15 | allowReleasedUserOnAddressFor: Gebruikersnaam op adres vrijgeven voor 16 | limitPerUserName: Maximum per gebruikersnaam 17 | limitBasePerIpAddress: Maximum per adres 18 | 19 | secu_requests: 20 | username: Gebruikersnaam 21 | loginsFailed: Mislukte inlogpogingen 22 | loginsSucceeded: Succesvolle inlogpogingen 23 | col: 24 | dtFrom: Vanaf 25 | username: Naam 26 | ipAddress: Adres 27 | loginsSucceeded: Succesvol 28 | loginsFailed: Mislukt 29 | ipAddressBlocked: adres 30 | usernameBlocked: naam 31 | usernameBlockedForIpAddress: naam op adres 32 | usernameBlockedForCookie: naam op cookie 33 | blockedColumns: Aantal pogingen geblokkeerd op 34 | 35 | countsGroupedByIpAddress: 36 | col: 37 | blocked: Blok 38 | usernames: Namen 39 | 40 | StatsPeriod: 41 | From: Van 42 | Until: Tot 43 | history: Historie 44 | statistics: IP-Adressen 45 | submit: Tonen 46 | 47 | relativeDate: 48 | minutes: minuten 49 | hours: uren 50 | days: dagen 51 | 52 | boolean: 53 | 0: Nee 54 | 1: Ja 55 | 56 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | metaclass_auth_guard.manager.class: "Metaclass\\TresholdsGovernor\\Manager\\RdbManager" 3 | metaclass_auth_guard.gateway.class: "Metaclass\\TresholdsGovernor\\Gateway\\RdbGateway" 4 | metaclass_auth_guard.tresholds_governor.class: "Metaclass\\TresholdsGovernor\\Service\\TresholdsGovernor" 5 | metaclass_auth_guard.authentication.listener.form.class: "Metaclass\\AuthenticationGuardBundle\\Service\\UsernamePasswordFormAuthenticationGuard" 6 | 7 | metaclass_auth_guard.ui.statistics.controller: "MetaclassAuthenticationGuardBundle:GuardStats" 8 | metaclass_auth_guard.ui.StatsPeriod.formType: "Metaclass\\AuthenticationGuardBundle\\Form\\Type\\StatsPeriodType" 9 | 10 | services: 11 | 12 | # Entity Manager for Request Counters 13 | metaclass_auth_guard.connection_login: 14 | class: Doctrine\DBAL\Connection 15 | factory: ["@doctrine", getConnection] 16 | arguments: ["%metaclass_auth_guard.db_connection.name%"] 17 | 18 | metaclass_auth_guard.gateway: 19 | class: %metaclass_auth_guard.gateway.class% 20 | arguments: ["@metaclass_auth_guard.connection_login"] 21 | 22 | metaclass_auth_guard.manager: 23 | class: %metaclass_auth_guard.manager.class% 24 | arguments: ["@metaclass_auth_guard.gateway"] 25 | 26 | metaclass_auth_guard.tresholds_governor: 27 | class: "%metaclass_auth_guard.tresholds_governor.class%" 28 | arguments: ["%metaclass_auth_guard.tresholds_governor_params%", "@metaclass_auth_guard.manager"] 29 | 30 | metaclass_auth_guard.statistics_manager: 31 | alias: metaclass_auth_guard.manager -------------------------------------------------------------------------------- /Tests/Service/TresholdsGovernorTest.php: -------------------------------------------------------------------------------- 1 | boot(); 19 | } 20 | if ('AppCache' == get_class($kernel)) { 21 | $kernel = $kernel->getKernel(); 22 | } 23 | $container = $kernel->getContainer(); 24 | $connectionName = $container->getParameter('metaclass_auth_guard.db_connection.name'); 25 | $this->assertNotNull($connectionName, 'metaclass_auth_guard.db_connection.name'); 26 | 27 | $doctrine = $container->get('doctrine'); 28 | $connection = $doctrine->getConnection($connectionName); 29 | $this->assertNotNull($connection, 'connection retieved from doctrine service'); 30 | 31 | $service = $container->get('metaclass_auth_guard.tresholds_governor'); 32 | $this->assertNotNull($service, 'metaclass_auth_guard.tresholds_governor'); 33 | 34 | //we don't want to to use the same governor that may be used in handling the request to the UnitTestController 35 | $this->governor = clone $service; 36 | 37 | $this->governor->dtString = '1980-07-01 00:00:00'; 38 | $this->governor->counterDurationInSeconds = 300; //5 minutes 39 | $this->governor->blockUsernamesFor = '30 days'; 40 | $this->governor->blockIpAddressesFor = '30 days'; //not very realistic, but should still work 41 | $this->governor->allowReleasedUserOnAddressFor = '30 days'; 42 | $this->governor->allowReleasedUserByCookieFor = '10 days'; 43 | 44 | $this->statisticsManager = $container->get('metaclass_auth_guard.statistics_manager'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Form/Type/StatsPeriodType.php: -------------------------------------------------------------------------------- 1 | $options['min'])), 21 | ); 22 | $builder->add('From', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', array( 23 | 'label' => $options['labels']['From'], 24 | 'required' => true, 25 | 'widget' => 'single_text', 26 | 'date_format' => $options['date_format'], 27 | 'format' => $options['dateTimePattern'], 28 | 'constraints' => $constraints, 29 | )); 30 | $builder->add('Until', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', array( 31 | 'label' => $options['labels']['Until'], 32 | 'required' => false, 33 | 'widget' => 'single_text', 34 | 'date_format' => $options['date_format'], 35 | 'format' => $options['dateTimePattern'], 36 | 'constraints' => $constraints, 37 | )); 38 | } 39 | 40 | public function getBlockPrefix() 41 | { 42 | return 'StatsPeriod'; 43 | } 44 | 45 | public function configureOptions(OptionsResolver $resolver) 46 | { 47 | $resolver->setDefaults(array('dateTimePattern' => null, 'date_format' => Type\DateTimeType::DEFAULT_DATE_FORMAT)); 48 | $resolver->setRequired(array('min', 'labels', 'date_format', 'dateTimePattern')); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('metaclass_authentication_guard'); 20 | 21 | $rootNode 22 | ->addDefaultsIfNotSet() 23 | ->children() 24 | ->arrayNode('db_connection') 25 | ->addDefaultsIfNotSet() 26 | ->children() 27 | ->scalarNode('name')->defaultValue('default')->end() 28 | ->end() 29 | ->end() 30 | ->arrayNode('ui') 31 | ->addDefaultsIfNotSet() 32 | ->children() 33 | ->scalarNode('dateTimeFormat')->defaultValue('SHORT')->end() 34 | ->arrayNode('statistics') 35 | ->addDefaultsIfNotSet() 36 | ->children() 37 | ->scalarNode('template')->defaultValue('MetaclassAuthenticationGuardBundle:Guard:statistics.html.twig')->end() 38 | ->end() 39 | ->end() 40 | ->end() 41 | ->end() 42 | ->arrayNode('tresholds_governor_params') 43 | ->addDefaultsIfNotSet() 44 | ->children() 45 | ->scalarNode('counterDurationInSeconds')->defaultValue(180)->end() 46 | ->scalarNode('blockUsernamesFor')->defaultValue('25 minutes')->end() 47 | ->scalarNode('limitPerUserName')->defaultValue(3)->end() 48 | ->scalarNode('blockIpAddressesFor')->defaultValue('17 minutes')->end() 49 | ->scalarNode('limitBasePerIpAddress')->defaultValue(10)->end() 50 | ->scalarNode('allowReleasedUserOnAddressFor')->defaultValue('30 days')->end() 51 | ->scalarNode('releaseUserOnLoginSuccess')->defaultValue(false)->end() 52 | ->scalarNode('keepCountsFor')->defaultValue('4 days')->end() 53 | ->scalarNode('fixedExecutionSeconds')->defaultValue('0.1')->end() 54 | ->scalarNode('randomSleepingNanosecondsMax')->defaultValue('99999')->end() 55 | ->end() 56 | ->end() 57 | ->end(); 58 | 59 | return $treeBuilder; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Resources/views/Guard/statistics_content.html.twig: -------------------------------------------------------------------------------- 1 |

{{ title }}

2 | {% if fieldSpec is defined %} 3 |
4 |
5 | {% for label, field in fieldSpec %} 6 |
7 | 8 |
9 | {{ fieldValues[field] }} 10 |
11 |
12 | {% endfor %} 13 |
14 |
15 | {% endif %} 16 | {% if form is defined %} 17 |
18 |
19 | {{ form.vars.label|trans({}, 'metaclass_auth_guard') }} 20 |
21 | {% for eachError in form.vars.errors %} 22 |
{{ eachError.message }}
23 | {% endfor %} 24 |
25 | {% for formrow in form.children %} 26 | {{ include("MetaclassAuthenticationGuardBundle:Entity:editrow.html.twig") }} 27 | {% endfor %} 28 | 29 | 30 |
31 |
32 | {% endif %} 33 | {% if columnSpec is defined %} 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for label, key in columnSpec %} 43 | 44 | {% endfor %} 45 | 46 | 47 | 48 | {% for entity in items %} 49 | 50 | {% for label, key in columnSpec %} 51 | 61 | {% endfor %} 62 | 63 | {% endfor %} 64 | 65 |
  {{ 'secu_requests.blockedColumns'|trans({}, 'metaclass_auth_guard') }}
{{ label|trans({}, 'metaclass_auth_guard') }}
{% if (key == 'ipAddress' and route_history is defined) %} 52 | 53 | {{ entity[key] }} 54 | {% elseif (key == 'username' and route_byUsername is defined) %} 55 | 56 | {{ entity[key] }} 57 | {% else %} 58 | {{ entity[key] }} 59 | {% endif %} 60 |
66 |
67 | {% endif %} -------------------------------------------------------------------------------- /Resources/doc/Hooking into Symfony.md: -------------------------------------------------------------------------------- 1 | Hooking into Symfony 2 | ==================== 3 | 4 | Authentication Listener service 5 | ------------------------------- 6 | 7 | To acticate the Guard the following setting replaces the default security.authentication.listener.form service: 8 | 9 | ```yaml 10 | services: 11 | security.authentication.listener.form: 12 | class: %metaclass_auth_guard.authentication.listener.form.class% 13 | parent: "security.authentication.listener.abstract" 14 | abstract: true 15 | calls: 16 | - [setGovenor, ["@metaclass_auth_guard.tresholds_governor"] ] # REQUIRED 17 | ``` 18 | 19 | Instead of an instance of `Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener` 20 | the service will be an instance of `Metaclass\AuthenticationGuardBundle\Service\UsernamePasswordFormAuthenticationGuard`. 21 | 22 | Just like UsernamePasswordFormAuthenticationListener, UsernamePasswordFormAuthenticationGuard extends 23 | `Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener`. Furthermore, some of its code 24 | was copied from UsernamePasswordFormAuthenticationListener. The reason UsernamePasswordFormAuthenticationListener 25 | was not extended is that it makes some properties private that are needed by the Guard, and that 26 | refactoring was needed so that only one small method could be inherited. 27 | 28 | This is where UsernamePasswordFormAuthenticationGuard is different: 29 | 30 | 1. It requires access to a TressholdsGovernor 31 | 32 | 2. Sanitizes the credentials to protect against invalid UTF-8 characters 33 | 34 | 3. It initializes the TressholdsGovernor, 35 | 36 | 4. If the credentials did contain unwanted characters, it registers an authentication failure with the TressholdsGovernor 37 | and throws a BadCredentialsException, 38 | 39 | 5. It lets the TressholdsGovernor check the authentication attempt. (If it rejects the attempt, the TressholdsGovernor 40 | will throw some sort of AuthenticationBlockedException) 41 | 42 | 6. If the Authentication Manager throws an AuthenticationException it will check if the user is to be held responsable 43 | for the exception. If so, it registers an authentication failure with the TressholdsGovernor before it rethrows 44 | the Exception. (imho AuthenticationServiceException and ProviderNotFoundException signal bad service plumming 45 | for which the user should not be blocked later when the problem is solved). 46 | 47 | 7. If the Authentication Manager does not throw an AuthenticationException it registers an authentication success with the 48 | TressholdsGovernor. 49 | 50 | 8. If there still is a old UsernamePasswordToken in the session, and the Authentication Manager has returned a new 51 | UsernamePasswordToken with a different user name, the session is cleared in order to prevent session data from 52 | the old user to leak to the new user*. 53 | 54 | 55 | Doctrine ORM Entity Manager 56 | --------------------------- 57 | 58 | You may want to use a different database user for the authentication then for the application itself. 59 | Then you do not have to GRANT the user of the default entity manager access to the tables where authentication data is stored. 60 | This gives a smaller contact surface wherefrom the sensitive authentication data can be reached. 61 | 62 | And the user of the authentication entity manager does not need to have access to the tables where the application data is stored. 63 | This keeps your application data one step further away from the authentication functions that can after all be accessed by everybody. 64 | 65 | To allow this a specific Entity Manager service is used by the Tresholds Governor. Its Entity Manager name can 66 | be specified by the setting: 67 | 68 | ```yml 69 | entity_manager_login: 70 | name: "" 71 | ``` 72 | The default for this setting is emtpy, resulting in the default Entity Manager to be used. 73 | 74 | Alternatives 75 | ------------ 76 | Ideas for alternatives are discussed [on the wiki](https://github.com/metaclass-nl/MetaclassAuthenticationGuardBundle/wiki/Other-options-for-hooking-the-Guard-into-Symfony%27s-authentication) 77 | 78 | 79 | * ISSUE: Maybe this should be done by the session strategy, not by the Authentication Listener? 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Authentication Guard for Symfony 2 2 | ================================== 3 | This bundle is no longer maintained and the repository will be archived. Symfony has protection against brute force attachs of its own. 4 | 5 | INTRODUCTION 6 | ------------ 7 | The OWASP Guide states "Applications MUST protect credentials from common authentication attacks as detailed 8 | in the Testing Guide". Symfony 2 has a firewall and a series of authentication components, but none to 9 | protect against brute force and dictionary attacks. This Bundle aims to protect user credentials from 10 | these authentication attacks. It is based on the "Tresholds Governer" described in the OWASP Guide. 11 | 12 | FEATURES 13 | -------- 14 | 15 | - Blocks the primary authentication route by both username and client ip address for which authentication failed too often, 16 | 17 | - To hide weather an account actually exists for a username, any username that is tried too often may be blocked, 18 | regardless of the existence and status of an account with that username, 19 | 20 | - Makes a logical difference between failed login lockout (done by this bundle) and eventual administrative lockout 21 | (may be done by the UserBundle), so that re-enabling all usernames en masse does not unlock administratively locked users 22 | (OWASP requirement). 23 | 24 | - Automatic release of username on authentication success, 25 | 26 | - Stores counters instead of individual requests to prevent database flooding from brute force attacks, 27 | 28 | REQUIREMENTS 29 | ------------ 30 | This bundle is for the symfony framework and this version requires Symfony >=2.8.1. 31 | (for Symfony ~2.3 use v0.3, for Symfony 2.7 use v0.4) 32 | Requires metaclass-nl/tresholds-governor 0.3@dev but the service configuration 33 | still requires Doctrine DBAL >=2.3. 34 | 35 | RELEASE NOTES 36 | ------------- 37 | 38 | This is a pre-release version under development. 39 | 40 | Currently the Bundle can only protect form-based authentication using the security.authentication.listener.form service 41 | (Default: Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener). 42 | 43 | Throws specific types of Exceptions for different situations (for logging purposes) and leaves it to the 44 | login form to hide differences between them that should not be reported to users. 45 | 46 | May be vurnerable to enumeration of usernames through timing attacks because of 47 | differences in database query performance for frequently and infrequently used usernames. 48 | This is mitigated by sleeping until a fixed execution time is reached. Under normal circomstances 49 | that should be sufficient if the fixedExecutionSeconds is set long enough, but under 50 | high (database) server loads when performance degrades, under specific conditions 51 | information may still be extractable by timing. Furthermore, the measures against 52 | timing attacks where not tested for practical effectiveness. 53 | 54 | Tested with MySQL 5.5. and 5.7. Tested with PHP7.0.1. Tested with Symfony 3.0.1 and 3.1.3 . (without crsf token) 55 | Tested on Symfony 2.8.1 with FOSUserBundle 1.3.6 and 6ccff96 (> 2.0.0 alpha3). 56 | Tested on Symfony 3.2.12 and 3.3.5 with FOSUserBundle 2.0.1 and php 7.0.18. 57 | 58 | DOCUMENTATION 59 | ------------- 60 | - [Installation and configuration](Resources/doc/Installation.md) 61 | - [Hooking into Symfony](Resources/doc/Hooking into Symfony.md) 62 | - [Underlying Tresholds Governor library](https://github.com/metaclass-nl/tresholds-governor) 63 | 64 | SUPPORT 65 | ------- 66 | 67 | MetaClass offers help and support on a commercial basis with 68 | the application and extension of this bundle and additional 69 | security measures. 70 | 71 | http://www.metaclass.nl/site/index_php/Menu/10/Contact.html 72 | 73 | 74 | COPYRIGHT AND LICENCE 75 | --------------------- 76 | 77 | Unless notified otherwise Copyright (c) 2014 MetaClass Groningen 78 | 79 | This bundle is under the MIT license. See the complete license in the bundle: 80 | 81 | Resources/meta/LICENSE 82 | 83 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 84 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 85 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 86 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 87 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 88 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 89 | THE SOFTWARE. 90 | -------------------------------------------------------------------------------- /Service/UsernamePasswordFormAuthenticationGuard.php: -------------------------------------------------------------------------------- 1 | , 5 | * the rest is (c) MetaClass Groningen. 6 | */ 7 | 8 | namespace Metaclass\AuthenticationGuardBundle\Service; 9 | 10 | use Metaclass\TresholdsGovernor\Result\Rejection; 11 | use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener; 12 | use Symfony\Component\Security\Csrf\CsrfToken; 13 | use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; 17 | use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; 18 | use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; 19 | use Symfony\Component\Security\Http\HttpUtils; 20 | use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; 21 | use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; 22 | use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; 23 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 24 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 25 | use Symfony\Component\Security\Core\Security; 26 | use Symfony\Component\Security\Http\ParameterBagUtils; 27 | use Symfony\Component\Security\Core\Exception\AuthenticationException; 28 | use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; 29 | use Symfony\Component\Security\Core\Exception\ProviderNotFoundException; 30 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; 31 | use Metaclass\TresholdsGovernor\Service\TresholdsGovernor; 32 | 33 | /** 34 | * Service that replaces Symfonies UsernamePasswordFormAuthenticationListener 35 | * in order to count authentication requests and block them to stop eventual 36 | * brute force and/or dictionary attacks. 37 | */ 38 | class UsernamePasswordFormAuthenticationGuard extends AbstractAuthenticationListener 39 | { 40 | /** 41 | * @var CsrfTokenManagerInterface stored once again because inherited variable is private 42 | */ 43 | protected $csrfTokenManager; 44 | 45 | /** 46 | * @var TokenStorageInterface stored once again because inherited variable is private 47 | */ 48 | protected $myTokenStorage; 49 | 50 | /** 51 | * @var TresholdsGovernor that does the counting and may decide to block authentication 52 | */ 53 | protected $governor; 54 | 55 | /** 56 | * In order to hide execution time differences when authentication is not blocked. 57 | * 58 | * @var float How long execution of the authentication process should take 59 | */ 60 | public $authExecutionSeconds; 61 | 62 | /** 63 | * @var string pattern for validating the username 64 | */ 65 | public static $usernamePattern = '/([^\\x20-\\x7E])/u'; //default is to allow all 1 to 1 visible ASCII characters (from space to ~). This excludes CR, LF, Tab , FF. If you want to be able to register e-mail addresses, don't exclude @ 66 | 67 | /** 68 | * @var string|null pattern(s) for validating the password. If not set, usernamePattern is used 69 | */ 70 | public static $passwordPattern; 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | 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) 76 | { 77 | parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array( 78 | 'username_parameter' => '_username', 79 | 'password_parameter' => '_password', 80 | 'csrf_parameter' => '_csrf_token', 81 | 'intention' => 'authenticate', 82 | 'post_only' => true, 83 | ), $options), $logger, $dispatcher); 84 | 85 | $this->csrfTokenManager = $csrfTokenManager; 86 | $this->myTokenStorage = $tokenStorage; 87 | } 88 | 89 | /** 90 | * Sets the TresholdsGovernor. Used on configuration. 91 | * 92 | * @param TresholdsGovernor $governor 93 | * 94 | * @return $this 95 | */ 96 | public function setGovenor(TresholdsGovernor $governor) 97 | { 98 | $this->governor = $governor; 99 | 100 | return $this; 101 | } 102 | 103 | /** Sets the pattern(s) for validating the username and password. 104 | * May be called on configuration. 105 | * Defaults are in the declaration of the variables. 106 | * 107 | * @param string $usernamePattern 108 | * @param string|null $passwordPattern if null the $usernamePattern is also used for validating the password 109 | */ 110 | public function setValidationPatterns($usernamePattern, $passwordPattern = null) 111 | { 112 | self::$usernamePattern = $usernamePattern; 113 | if ($passwordPattern !== null) { 114 | self::$passwordPattern = $passwordPattern; 115 | } 116 | } 117 | 118 | /** 119 | * In order to hide execution time differences when authentication is not blocked. 120 | * 121 | * @var float How long execution of the authentication process should take 122 | */ 123 | public function setAuthExecutionSeconds($seconds) 124 | { 125 | $this->authExecutionSeconds = $seconds; 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | protected function requiresAuthentication(Request $request) 132 | { 133 | if ($this->options['post_only'] && !$request->isMethod('POST')) { 134 | return false; 135 | } 136 | 137 | return parent::requiresAuthentication($request); 138 | } 139 | 140 | /** 141 | * {@inheritdoc} 142 | */ 143 | protected function attemptAuthentication(Request $request) 144 | { 145 | $exception = null; 146 | $originalCred = $this->getCredentials($request); 147 | $filteredCred = $this->filterCredentials($originalCred); 148 | $request->getSession()->set(Security::LAST_USERNAME, $originalCred[0]); 149 | 150 | if (null !== $this->csrfTokenManager) { 151 | $this->checkCrsfToken($request); 152 | } 153 | 154 | //initialize the governor so that we can register a failure 155 | $this->governor->initFor( 156 | $request->getClientIp(), $filteredCred[0], $filteredCred[1], '' //cookieToken not yet used (setting and getting cookies NYI) 157 | ); 158 | 159 | if ($originalCred != $filteredCred) { //we can not accept invalid characters 160 | $this->governor->registerAuthenticationFailure(); 161 | $exception = new BadCredentialsException('Credentials contain invalid character(s)'); 162 | } else { 163 | $exception = $this->getExceptionOnRejection($this->governor->checkAuthentication()); //may register failure 164 | if ($exception === null) { 165 | //not blocked, try to authenticate 166 | try { 167 | $newToken = $this->authenticationManager->authenticate(new UsernamePasswordToken($filteredCred[0], $filteredCred[1], $this->providerKey)); 168 | 169 | //authenticated! No need to hide timing 170 | $this->governor->registerAuthenticationSuccess(); 171 | 172 | return $newToken; 173 | } catch (AuthenticationException $e) { 174 | if ($this->isClientResponsibleFor($e)) { 175 | $this->governor->registerAuthenticationFailure(); 176 | } //else do not register service errors as failures 177 | // wait to hide eventual execution time differences 178 | if ($this->authExecutionSeconds) { 179 | // \Gen::show($this->governor->getSecondsPassedSinceInit()); die(); 180 | $this->governor->sleepUntilSinceInit($this->authExecutionSeconds); 181 | } 182 | throw $e; 183 | } 184 | } 185 | } // end $originalCred != $filteredCred 186 | 187 | $this->governor->sleepUntilFixedExecutionTime(); // hides execution time differences of tresholds governor 188 | 189 | throw $exception; 190 | } 191 | 192 | /** Converts a Rejection from the TresholdsGovernor to an Exception 193 | * using a naming scheme. 194 | * 195 | * @param Rejection $rejection 196 | * 197 | * @return \Metaclass\AuthenticationGuardBundle\Exception\AuthenticationBlockedException if a rejection is passed. 198 | */ 199 | protected function getExceptionOnRejection(Rejection $rejection = null) 200 | { 201 | if ($rejection) { 202 | $exceptionClass = 'Metaclass\\AuthenticationGuardBundle\\Exception\\' 203 | .subStr(get_class($rejection), 35).'Exception'; 204 | 205 | return new $exceptionClass(strtr($rejection->message, $rejection->parameters)); 206 | } 207 | } 208 | 209 | /** Checks if the csrf_token_id is valid as a CSRF token 210 | * @param Request $request 211 | * 212 | * @throws InvalidCsrfTokenException if it is invalid 213 | */ 214 | protected function checkCrsfToken(Request $request) 215 | { 216 | $csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); 217 | 218 | if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $csrfToken))) { 219 | throw new InvalidCsrfTokenException('Invalid CSRF token.'); 220 | } 221 | } 222 | 223 | /** Get the credentials from the request. 224 | * @param Request $request 225 | * 226 | * @return array with the credentials, username at 0, password at 1 (int) 227 | */ 228 | protected function getCredentials(Request $request) 229 | { 230 | if ($this->options['post_only']) { 231 | $username = trim($request->request->get($this->options['username_parameter'], null, true)); 232 | $password = $request->request->get($this->options['password_parameter'], null, true); 233 | } else { 234 | $username = trim($request->get($this->options['username_parameter'], null, true)); 235 | $password = $request->get($this->options['password_parameter'], null, true); 236 | } 237 | 238 | return array($username, $password); 239 | } 240 | 241 | /** Filter the credentials to protect against invalid UTF-8 characters. 242 | * @param array $usernameAndPassword, username at 0, password at 1 (int) 243 | * 244 | * @return array filtered credentials, username at 0, password at 1 (int) 245 | */ 246 | public static function filterCredentials($usernameAndPassword) 247 | { 248 | return array( 249 | self::filterUsername($usernameAndPassword[0]), 250 | self::filterPassword($usernameAndPassword[1]), 251 | ); 252 | } 253 | 254 | /** Filter the username using self::$usernamePattern 255 | * @param string $value 256 | * 257 | * @return string filtered username 258 | */ 259 | public static function filterUsername($value) 260 | { 261 | return preg_replace(self::$usernamePattern, ' ', $value); 262 | } 263 | 264 | /** Filter the password using self::$passwordPattern if not null, else using self::$usernamePattern 265 | * @param string $value 266 | * 267 | * @return string filtered password 268 | */ 269 | public static function filterPassword($value) 270 | { 271 | return preg_replace((self::$passwordPattern === null ? self::$usernamePattern : self::$passwordPattern), ' ', $value); 272 | } 273 | 274 | /** Wheather the client (who sended the request) is reponsible for the authentication failure. 275 | * imho AuthenticationServiceException and ProviderNotFoundException signal bad service plumming, 276 | * and counting them would only lead to blocking legitimate users. 277 | * 278 | * @param AuthenticationException $e 279 | * 280 | * @return bool 281 | */ 282 | public static function isClientResponsibleFor(AuthenticationException $e) 283 | { 284 | return !($e instanceof AuthenticationServiceException 285 | || $e instanceof ProviderNotFoundException); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /Resources/doc/Installation.md: -------------------------------------------------------------------------------- 1 | INSTALLATION AND CONFIGURATION 2 | ============================== 3 | 4 | Installation 5 | ------------ 6 | 7 | 1. Check if you have the following setting in your app/conf/security.yml: 8 | ```yml 9 | firewalls: 10 | secured_area: 11 | form_login: 12 | ``` 13 | If you have no firewall, there is no authentication to guard. If you have some other setting instead of form_login:, 14 | for example http_basic:, http_digest: or x509:, the current version of this bundle can not guard it. (remember_me: 15 | will usually be combined with some other Authentication Listener, currently this bundle can not guard it) 16 | 17 | If you have a setting like this: 18 | ```yml 19 | firewalls: 20 | secured_area: 21 | form_login: 22 | id: somecustomserviceid 23 | ``` 24 | you are using a custom form authenticaton listener service. 25 | May or may not work depending on the configuration of your service. 26 | (works with FOSUserBundle 1.3 and probably with 2.0 too) 27 | 28 | This bundle can only guard your service ig it is configured 29 | to use the default security.authentication.listener.form.class 30 | (Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener) 31 | and you may have to write your own configuration to use instead of the one under step 6 32 | (the one under step 6 did work with FOSUserBundle). 33 | 34 | 2. Require the bundle and the tresholds governor library it needs in your composer.json 35 | ```js 36 | { 37 | "require": { 38 | "metaclass-nl/authentication-guard-bundle": "*@dev", 39 | "metaclass-nl/tresholds-governor": "*@dev" 40 | } 41 | } 42 | ``` 43 | 3. download the bundles by: 44 | 45 | ``` bash 46 | $ php composer.phar update metaclass-nl/authentication-guard-bundle 47 | $ php composer.phar update "metaclass-nl/tresholds-governor 48 | ``` 49 | 50 | Composer will install the bundle and library in your `vendor/metaclass-nl` folder. 51 | 52 | 4. Create the database table 53 | 54 | See step 3 of the Install documentation of the [tresholds-governor]((https://github.com/metaclass-nl/tresholds-governor/) 55 | 56 | 5. Add the bundle to your AppKernel 57 | 58 | ``` php 59 | loadClassCache(); 114 | $kernel->boot(); 115 | $container = $kernel->getContainer(); 116 | 117 | $governor = $container->get('metaclass_auth_guard.tresholds_governor'); 118 | $result = $governor->packData(); 119 | 120 | //if you want to log the result: 121 | $secuLogger = $container->get('monolog.logger.security'); 122 | $secuLogger->info('tresholds_governor deleted requestcounts until '. $result["requestcounts_deleted_until"]->format('Y-m-d H:m:s') ); 123 | $secuLogger->info('tresholds_governor deleted releases until '. $result["releases_deleted_until"]->format('Y-m-d H:m:s') ); 124 | 125 | ``` 126 | 127 | 9. The user interface for user administrators to look into why a user may have been blocked is optional. 128 | To enable it add the following to your app/config/routing.yml: 129 | ```yml 130 | metaclass_auth_guard: 131 | resource: "@MetaclassAuthenticationGuardBundle/Resources/config/routing.yml" 132 | prefix: /guard 133 | ``` 134 | And add the path of the user interface to your firewall in app/config/security.yml: 135 | ```yml 136 | access_control: 137 | - { path: ^/guard, roles: ROLE_ADMIN } 138 | ``` 139 | (there will probably already be an access_control configuration with several paths listed. 140 | Add the above path to the list in an appropriate place. You may have to adapt ROLE_ADMIN to the user role identifier 141 | appropriate for your application's security configuration.) 142 | 143 | The user interface needs translation enabled in your app/config/config.yml: 144 | ```yml 145 | framework: 146 | translator: { fallbacks: ["%locale%"] } 147 | ``` 148 | It has the following entries: 149 | - guard/statistics 150 | - guard/history/ipAddress (replace 'ipAddress' by an actual ip address) 151 | - guard/statistics/username (replace 'username' by an actual username) 152 | 153 | The default template assumes you have base.html.twig still in app/Resources/views. 154 | See configuration 13 to make it extend your applications layout. 155 | 156 | Resources/config/services.yml defines parameters for the controller class and 157 | the StatsPeriod formtype. You may override them to use your own (sub)classes. 158 | 159 | Currently the web based user interface only supports English and Dutch. 160 | Please clone the Bundle on Github and add your own language translation! 161 | 162 | 10. If you want to run the tests you may add the following to the testsuites section of your app/phpunit.xml: 163 | ```xml 164 | 165 | ../vendor/metaclass-nl/authentication-guard-bundle/Metaclass/AuthenticationGuardBundle/Tests 166 | 167 | ``` 168 | 169 | Configurations 170 | -------------- 171 | 172 | 1. The database connection to use 173 | 174 | ```yml 175 | db_connection: 176 | name: "" 177 | ``` 178 | The default for this setting is emtpy, resulting in the default doctrine dbal connection to be used. 179 | If you specify some specific value a specific connection will be used for storing and retieving RequestCounts data. 180 | 181 | 2. Counting duration 182 | 183 | counterDurationInSeconds 184 | 185 | From this setting the Tresholds Governor decides when a new RequestCounts record will be made for the same combination of 186 | username, IP address and user agent. The higher you set this, the less records will be generated, thus the faster counting will be. 187 | But it needs to be substantially shorter then the blockIpAddressesFor and blockUsernamesFor durations not to get too unprecise countings. 188 | 189 | 3. Username blocking duration 190 | 191 | blockUsernamesFor 192 | 193 | The duration for which failed login counters are summed per username. Values like "3 minutes", "12 hours", "5 years" are allowed. 194 | The actual duration of blocking will be up to 'counterDurationInSeconds' shorter. 195 | 196 | The OWASP Guide: 197 | > If necessary, such as for compliance with a national security standard, a configurable soft lockout of approximately 15-30 minutes should apply, with an error message stating the reason and when the account will become active again. 198 | Hoever, many applications block user accounts after three or five attempts until they are reactivated explicitly. 199 | This is not supported, but you may set the duration long. Be aware that the number of counters may have to become 200 | very high, slowing down the authentication process [idea for improvement](https://github.com/metaclass-nl/MetaclassAuthenticationGuardBundle/wiki). 201 | 202 | Counters that start before the system time minus this duration do not count for this purpose. 203 | However, this does not mean that usernames that became blocked will never be blocked after this duration: if more 204 | failed logins where counted afterwards in newer RequestCounts records, these will remain to count while the older 205 | RequestCounts are no longer counted. As long as the total is higher then limitPerUserName, the username will 206 | remain blocked, unless it is released*. 207 | 208 | 209 | 4. Username blocking theshold 210 | 211 | limitPerUserName 212 | 213 | The number of failed login attempts that are allowed per username within the username blocking duration. 214 | If the number of failed logins is higher the user will be blocked, unless his failures are released*. 215 | 216 | 5. IP address blocking duration. 217 | 218 | blockIpAddressesFor 219 | 220 | The duration for which failed login counters are summed per ip addess. Values like "3 minutes", "12 hours", "5 years" are allowed. 221 | The actual duration of blocking will be up to 'counterDurationInSeconds' shorter. 222 | 223 | The OWASP Guide suggests a duration of 15 minutes, but also suggests additional measures that are currenly not supported 224 | by this Bundle. 225 | 226 | Counters that start before the system time minus this duration do not count for this purpose. 227 | However, this does not mean that addresses that became blocked will never be blocked after this duration: if more 228 | failed logins where counted afterwards in newer RequestCounts records, these will remain to count while the older 229 | RequestCounts are no longer counted. As long as the total is higher then limitPerIpAddress, the addresses will 230 | remain blocked, unless it is released*. 231 | 232 | 6. IP address blocking treshold 233 | 234 | limitBasePerIpAddress 235 | 236 | The number of failed login attempts that are allowed per IP address within the IP adress blocking duration. 237 | If the number of failed logins is higher the address will be blocked, unless its failures are released*. 238 | 239 | 7. Release user on login success 240 | 241 | releaseUserOnLoginSuccess 242 | 243 | Most systems that count failed logins per user account only count the failed logins since the last successfull one. 244 | If this option is set to true, you get the same result: each time the user logs in sucessfully, the 245 | username is released for all ip addresses and user agents. And only failures AFTER the last release are counted. 246 | 247 | This allows slow/distributed attacks to go on for a long period when the user logs in frequently. 248 | If this option is set to false, user names are only released for the IP address and user agent the 249 | successfull login was made from. The username may still become blocked for all the other IP addresses 250 | and user agents. The disadvantage is that the user will be blocked when his IP address or user agent changes, 251 | for example because he wants to log in from a different device or connection. 252 | 253 | 8. Username release duration by IP address 254 | 255 | allowReleasedUserOnAddressFor 256 | 257 | For how long a username will remain released per IP address. Values like "3 minutes", "12 hours", "5 years" are allowed. 258 | 259 | If a user logs in frequently this will frequently produce new releases. This allows the user to 260 | log in from the same IP address even if his username is under constant attack, as long as the attacks 261 | do not come from his IP address. However, he may take a vacation and not log in for some weeks or so. 262 | This setting basically says how long this vacation may be and still be allowed to 263 | log in because of his user agent. 264 | 265 | 9. Garbage collection delay 266 | 267 | keepCountsFor 268 | 269 | For how long the requestcounts will be kept before being garbage-collected. Values like "4 days". 270 | 271 | If you have enabled the user interface for user administrators to look into why 272 | a user may have been blocked, this is how long they can look back in time to see 273 | what happened. 274 | 275 | This value must allways be set longer then both blockUsernamesFor and blockIpAddressesFor, 276 | otherwise counters will be deleted before blocking should end and no longer be counted in 277 | for blocking. 278 | 279 | Currently the user interface shows no information about active releases, but for 280 | future extension this value also acts as a minimum for how long releases will be kept before being 281 | garbage collected, but if allowReleasedUserOnAddressFor (or allowReleasedUserByCookieFor) 282 | is set to a longer duration, the releases will be kept longer (according to the longest one). 283 | 284 | 10. Fixed execution time 285 | 286 | fixedExecutionSeconds 287 | 288 | Fixed execution time in order to mitigate timing attacks. To apply, call ::sleepUntilFixedExecutionTime. 289 | 290 | 11. Maximum random sleeping time in nanoseconds 291 | 292 | randomSleepingNanosecondsMax 293 | 294 | Because of doubts about the accurateness of microtime() and to hide system clock 295 | details a random between 0 and this value is added by ::sleepUntilSinceInit (which 296 | is called by ::sleepUntilFixedExecutionTime). 297 | 298 | 12. Datetime format used by the web based user interface 299 | 300 | ```yml 301 | ui: 302 | dateTimeFormat 303 | ``` 304 | \IntlDateFormatter pattern or datetype. If a dattype is set 305 | (FULL, LONG, MEDIUM or SHORT) (case independent) the corresponding 306 | dateformat is used and no pattern so that the formatting will depend 307 | on the locale. Otherwise the parameter is used as pattern with 308 | \Symfony\Component\Form\Extension\Core\Type\DateTimeType::DEFAULT_DATE_FORMAT 309 | as datetype. As timetype DateTimeType::DEFAULT_TIME_FORMAT allways used so that 310 | the formatting is the same as done by the DateTimeType widgets in the Period form. 311 | 312 | If you need specific patterns for different locales you may use your own subclass 313 | of GuardStatsController and override ::initDateFormatAndPattern to set the appropriate 314 | datetype and format, or override ::initDateTimeTransformer to set whatever 315 | transformer you may like (but that will not be used by the DateTimeType widgets in the 316 | Period form so you may want to set your own form type too). 317 | 318 | 13. Template used by the web based user interface for user administrators 319 | 320 | ```yml 321 | ui: 322 | statistics: 323 | template 324 | ``` 325 | Bundlename:views subfolder:template filename 326 | 327 | In an actual application you typically set this to a template of your own. 328 | It will proabbly extend your own layout and may include 329 | MetaclassAuthenticationGuardBundle:Guard:statistics_content.html.twig. 330 | 331 | The default template assumes you have base.html.twig still in app/Resources/views. 332 | 333 | Notes 334 | 335 | - releasing is possible for a username in general, an IP address in general, or for the combination of a username with an ip address 336 | 337 | -------------------------------------------------------------------------------- /Controller/GuardStatsController.php: -------------------------------------------------------------------------------- 1 | initDateTimeTransformer(); 43 | $governor = $this->get('metaclass_auth_guard.tresholds_governor'); 44 | $statsManager = $this->get('metaclass_auth_guard.statistics_manager'); 45 | 46 | $params['title'] = $this->get('translator')->trans('statistics.title', array(), 'metaclass_auth_guard'); 47 | $params['routes']['this'] = 'Guard_statistics'; 48 | $params['action_params'] = array(); 49 | 50 | $this->addStatisticCommonParams($params, $governor); 51 | $countingSince = $governor->getMinBlockingLimit(); 52 | $fieldValues = &$params['fieldValues']; 53 | $fieldValues['countingSince'] = $this->dtTransformer->transform($countingSince); 54 | $fieldValues['failureCount'] = $statsManager->countLoginsFailed($countingSince); 55 | $fieldValues['successCount'] = $statsManager->countLoginsSucceeded($countingSince); 56 | 57 | $limitFrom = $request->isMethod('POST') 58 | ? null 59 | : $countingSince; 60 | $limits = $this->addStatsPeriodForm($params, $request, $governor, 'StatsPeriod.statistics', $limitFrom); 61 | 62 | if (isset($limits['From'])) { 63 | $this->addCountsGroupedTableParams($params, $request, $governor, $limits, $statsManager); 64 | $params['blockedHeaderIndent'] = 6; 65 | $params['labels'] = array('show' => 'history.show'); 66 | $params['route_history'] = 'Guard_history'; 67 | $params['limits']['From'] = $this->dtTransformer->transform($limits['From']); 68 | $params['limits']['Until'] = $this->dtTransformer->transform($limits['Until']); 69 | } 70 | // #TODO: make params testable 71 | return $this->render( 72 | $this->container->getParameter('metaclass_auth_guard.statistics.template'), 73 | $params); 74 | } 75 | 76 | /** 77 | * Shows request counters history for an ip address. 78 | * 79 | * Route("/history/{ipAddress}", name="Guard_history", requirements={"ipAddress" = "[^/]+"}) 80 | */ 81 | public function historyAction(Request $request, $ipAddress) 82 | { 83 | $this->initDateTimeTransformer(); 84 | $governor = $this->get('metaclass_auth_guard.tresholds_governor'); 85 | $statsManager = $this->get('metaclass_auth_guard.statistics_manager'); 86 | 87 | $params['routes']['this'] = 'Guard_history'; 88 | $params['action_params'] = array('ipAddress' => $ipAddress); 89 | $params['title'] = $this->get('translator')->trans('history.title', array(), 'metaclass_auth_guard'); 90 | $this->buildMenu($params, 'Guard_history'); 91 | $params['fieldSpec'] = array( 92 | 'IP Adres' => 'ipAddress', 93 | 'tresholds_governor_params.limitPerUserName' => 'limitPerUserName', 94 | 'tresholds_governor_params.limitBasePerIpAddress' => 'limitBasePerIpAddress', 95 | ); 96 | $params['fieldValues'] = array( 97 | 'ipAddress' => $ipAddress, 98 | 'limitPerUserName' => $governor->limitPerUserName, 99 | 'limitBasePerIpAddress' => $governor->limitBasePerIpAddress, 100 | ); 101 | 102 | $limits = $this->addStatsPeriodForm($params, $request, $governor, 'StatsPeriod.history'); 103 | 104 | if (isset($limits['From'])) { 105 | $history = $statsManager->countsByAddressBetween($ipAddress, $limits['From'], $limits['Until']); 106 | $this->addHistoryTableParams($params, $history, 'username', 'secu_requests.col.username'); 107 | $params['route_byUsername'] = 'Guard_statisticsByUserName'; 108 | $params['labels'] = array('show' => 'history.show'); 109 | $params['limits']['From'] = $this->dtTransformer->transform($limits['From']); 110 | $params['limits']['Until'] = $this->dtTransformer->transform($limits['Until']); 111 | } 112 | // #TODO: make params testable 113 | return $this->render( 114 | $this->container->getParameter('metaclass_auth_guard.statistics.template'), 115 | $params); 116 | } 117 | 118 | /** 119 | * Shows request counterss history for a username. 120 | * 121 | * Route("/statistics/{username}", name="Guard_statisticsByUserName", requirements={"username" = "[^/]*"}) 122 | */ 123 | public function statisticsByUserNameAction(Request $request, $username) 124 | { 125 | $this->initDateTimeTransformer(); 126 | $filtered = UsernamePasswordFormAuthenticationGuard::filterCredentials(array($username, '')); 127 | $username = $filtered[0]; 128 | $governor = $this->get('metaclass_auth_guard.tresholds_governor'); 129 | $statsManager = $this->get('metaclass_auth_guard.statistics_manager'); 130 | 131 | $params['routes']['this'] = 'Guard_statisticsByUserName'; 132 | $params['action_params'] = array('username' => $username); 133 | $params['title'] = $this->get('translator')->trans('history.title', array(), 'metaclass_auth_guard'); 134 | $params['fieldSpec'] = array('secu_requests.username' => 'username'); 135 | 136 | $this->addStatisticCommonParams($params, $governor, 'Guard_statisticsByUserName'); 137 | 138 | $params['fieldSpec']['tresholds_governor_params.allowReleasedUserOnAddressFor'] = 'allowReleasedUserOnAddressFor'; 139 | $params['fieldSpec']['statisticsByUserName.isUsernameBlocked'] = 'usernameBlocked'; 140 | 141 | $countingSince = new \DateTime("$governor->dtString - $governor->blockUsernamesFor"); 142 | $fieldValues = &$params['fieldValues']; 143 | $fieldValues['username'] = $username; 144 | $fieldValues['countingSince'] = $this->dtTransformer->transform($countingSince); 145 | $fieldValues['failureCount'] = $statsManager->countLoginsFailedForUserName($username, $countingSince); 146 | $fieldValues['successCount'] = $statsManager->countLoginsSucceededForUserName($username, $countingSince); 147 | $isUsernameBlocked = $fieldValues['failureCount'] >= $governor->limitPerUserName; 148 | $fieldValues['usernameBlocked'] = $this->booleanLabel($isUsernameBlocked); 149 | $fieldValues['allowReleasedUserOnAddressFor'] = $this->translateRelativeDate($governor->allowReleasedUserOnAddressFor); 150 | 151 | $limitFrom = $request->isMethod('POST') || $request->get('StatsPeriod') 152 | ? null 153 | : $countingSince; 154 | $limits = $this->addStatsPeriodForm($params, $request, $governor, 'Historie', $limitFrom); 155 | if (isset($limits['From'])) { 156 | $params['labels'] = array('show' => 'history.show'); 157 | $params['route_history'] = 'Guard_history'; 158 | $params['limits']['From'] = $this->dtTransformer->transform($limits['From']); 159 | $params['limits']['Until'] = $this->dtTransformer->transform($limits['Until']); 160 | $history = $statsManager->countsByUsernameBetween($username, $limits['From'], $limits['Until']); 161 | $this->addHistoryTableParams($params, $history, 'ipAddress', 'secu_requests.col.ipAddress'); 162 | } 163 | 164 | // #TODO: make params testable 165 | return $this->render( 166 | $this->container->getParameter('metaclass_auth_guard.statistics.template'), 167 | $params); 168 | } 169 | 170 | /** 171 | * @param array $params 172 | * @param array $history of rows (counters from secu_requests) 173 | * @param string $col1Field field to be shown (username or ipAddress) 174 | * @param string $col1Label label for $col1Field 175 | */ 176 | protected function addHistoryTableParams(&$params, $history, $col1Field, $col1Label) 177 | { 178 | $params['columnSpec'] = array( 179 | 'secu_requests.col.dtFrom' => 'dtFrom', 180 | $col1Label => $col1Field, 181 | 'secu_requests.col.loginsSucceeded' => 'loginsSucceeded', 182 | 'secu_requests.col.loginsFailed' => 'loginsFailed', 183 | 'secu_requests.col.ipAddressBlocked' => 'ipAddressBlocked', 184 | 'secu_requests.col.usernameBlocked' => 'usernameBlocked', 185 | 'secu_requests.col.usernameBlockedForIpAddress' => 'usernameBlockedForIpAddress', 186 | 'secu_requests.col.usernameBlockedForCookie' => 'usernameBlockedForCookie', 187 | ); 188 | foreach ($history as $key => $row) { 189 | $dt = new \DateTime($row['dtFrom']); 190 | $history[$key]['dtFrom'] = $this->dtTransformer->transform($dt); 191 | } 192 | $params['items'] = $history; 193 | $params['blockedHeaderIndent'] = 5; 194 | } 195 | 196 | /** Add the statistics period form to the parameters. 197 | * @param array $params to add the form to 198 | * @param Request $request to be handled by the form 199 | * @param TresholdsGovernor $governor used to caluculate the history limit 200 | * @param string $label 201 | * @param \DateTime|null $limitFrom if passed limits are set on the form, 202 | * otherwise the limits from the form are retrieved 203 | * 204 | * @return array('From' => limit from, 'Until' => limit until) 205 | */ 206 | protected function addStatsPeriodForm(&$params, $request, $governor, $label, $limitFrom = null) 207 | { 208 | $limits['Until'] = new \DateTime(); 209 | $labels = array('From' => 'StatsPeriod.From', 'Until' => 'StatsPeriod.Until'); 210 | $historyLimit = new \DateTime("$governor->dtString - $governor->keepCountsFor"); 211 | 212 | $formTypeClass = $this->container->getParameter('metaclass_auth_guard.ui.StatsPeriod.formType'); 213 | if (!class_exists($formTypeClass)) { 214 | throw new RuntimeException("value of metaclass_auth_guard.statistics.StatsPeriod.formType is not a class: '$formTypeClass'"); 215 | } 216 | 217 | $options = array( 218 | 'label' => $label, 219 | 'csrf_protection' => false, 220 | 'translation_domain' => 'metaclass_auth_guard', 221 | 'method' => $request->getMethod(), 222 | // custom options defined by StatsPeriodType::configureOptions: 223 | 'labels' => $labels, 224 | 'min' => $historyLimit, 225 | 'date_format' => $this->dateFormat, 226 | 'dateTimePattern' => $this->dateTimePattern, 227 | ); 228 | $form = $this->createForm($formTypeClass, null, $options); 229 | 230 | if ($limitFrom === null) { 231 | $form->handleRequest($request); 232 | if ($form->isValid()) { 233 | $limits['From'] = $form->get('From')->getData(); 234 | $limits['Until'] = $form->get('Until')->getData(); 235 | } 236 | } else { 237 | $limits['From'] = $limitFrom; 238 | $form->get('From')->setData($limitFrom); 239 | $form->get('Until')->setData($limits['Until']); 240 | } 241 | $params['form'] = $form->createView(); 242 | 243 | return $limits; 244 | } 245 | 246 | /** Add common parameters 247 | * @param $params array to add to 248 | * @param TresholdsGovernorv $governor 249 | */ 250 | protected function addStatisticCommonParams(&$params, $governor) 251 | { 252 | $this->buildMenu($params, 'Guard_show'); 253 | $fieldSpec = array( 254 | 'tresholds_governor_params.countingSince' => 'countingSince', 255 | 'tresholds_governor_params.blockUsernamesFor' => 'blockUsernamesFor', 256 | 'tresholds_governor_params.blockIpAddressesFor' => 'blockIpAddressesFor', 257 | 'secu_requests.loginsSucceeded' => 'successCount', 258 | 'secu_requests.loginsFailed' => 'failureCount', 259 | ); 260 | $params['fieldSpec'] = isset($params['fieldSpec']) 261 | ? array_merge($params['fieldSpec'], $fieldSpec) 262 | : $fieldSpec; 263 | 264 | $fieldValues['blockIpAddressesFor'] = $this->translateRelativeDate($governor->blockIpAddressesFor); 265 | $fieldValues['blockUsernamesFor'] = $this->translateRelativeDate($governor->blockUsernamesFor); 266 | $params['fieldValues'] = $fieldValues; 267 | } 268 | 269 | /** Add parameters for the grouped counts table 270 | * @param array $params to add the parameters to 271 | * @param Request $request 272 | * @param TresholdsGovernor $governor whose limitBasePerIpAddress is used 273 | * @param array $limits array('From' => limit from, 'Until' => limit until) 274 | * @param StatisticsManagerInterface $statsManager 275 | */ 276 | protected function addCountsGroupedTableParams(&$params, $request, $governor, $limits, $statsManager) 277 | { 278 | $countsByIpAddress = $statsManager->countsGroupedByIpAddress($limits['From'], $limits['Until']); 279 | $params['columnSpec'] = array( 280 | 'secu_requests.col.ipAddress' => 'ipAddress', 281 | 'countsGroupedByIpAddress.col.blocked' => 'blocked', 282 | 'countsGroupedByIpAddress.col.usernames' => 'usernames', 283 | 'secu_requests.col.loginsSucceeded' => 'loginsSucceeded', 284 | 'secu_requests.col.loginsFailed' => 'loginsFailed', 285 | 'secu_requests.col.ipAddressBlocked' => 'ipAddressBlocked', 286 | 'secu_requests.col.usernameBlocked' => 'usernameBlocked', 287 | 'secu_requests.col.usernameBlockedForIpAddress' => 'usernameBlockedForIpAddress', 288 | 'secu_requests.col.usernameBlockedForCookie' => 'usernameBlockedForCookie', 289 | ); 290 | if ($request->isMethod('POST')) { 291 | unset($params['columnSpec']['Blok']); 292 | } 293 | foreach ($countsByIpAddress as $key => $row) { 294 | $blocked = $row['loginsFailed'] >= $governor->limitBasePerIpAddress; 295 | $countsByIpAddress[$key]['blocked'] = $this->booleanLabel($blocked); 296 | // Yet to be added: count usernames released, count usernames blocked 297 | } 298 | $params['items'] = $countsByIpAddress; 299 | } 300 | 301 | /** Convert boolean a a label to show to the user 302 | * @param bool $value 303 | * 304 | * @return string like 'Yes' or 'No' 305 | */ 306 | protected function booleanLabel($value) 307 | { 308 | $key = $value ? 'boolean.1' : 'boolean.0'; 309 | 310 | return $this->get('translator')->trans($key, array(), 'metaclass_auth_guard'); 311 | } 312 | 313 | /** 314 | * Initialize $this->dtTransformer with a new DateTimeToLocalizedStringTransformer. 315 | */ 316 | protected function initDateTimeTransformer() 317 | { 318 | $this->initDateFormatAndPattern(); 319 | $this->dtTransformer = new DateTimeToLocalizedStringTransformer( 320 | null, 321 | null, 322 | $this->dateFormat, 323 | DateTimeType::DEFAULT_TIME_FORMAT, // Compatible with DateTimeType 324 | \IntlDateFormatter::GREGORIAN, 325 | $this->dateTimePattern); 326 | } 327 | 328 | /** 329 | * Derives $this->dateFormat and $this->dateTimePattern from 330 | * parameter metaclass_auth_guard.ui.dateTimeFormat. 331 | * If FULL, LONG, MEDIUM or SHORT (case independent) the corresponding 332 | * dateformat is used. Otherwise the parameter is used as pattern. 333 | * 334 | * To be overridden by subclass if pattern depends on locale or varies otherwise 335 | */ 336 | protected function initDateFormatAndPattern() 337 | { 338 | $formatOption = $this->container->getParameter('metaclass_auth_guard.ui.dateTimeFormat'); 339 | $constantOptions = array( 340 | 'FULL' => \IntlDateFormatter::FULL, 341 | 'LONG' => \IntlDateFormatter::LONG, 342 | 'MEDIUM' => \IntlDateFormatter::MEDIUM, 343 | 'SHORT' => \IntlDateFormatter::SHORT, 344 | ); 345 | $dateFormat = null; 346 | $formatOptionUc = strtoupper($formatOption); 347 | if (isset($constantOptions[$formatOptionUc])) { 348 | $this->dateFormat = $constantOptions[$formatOptionUc]; 349 | $this->dateTimePattern = null; 350 | } else { 351 | $this->dateFormat = DateTimeType::DEFAULT_DATE_FORMAT; 352 | $this->dateTimePattern = $formatOption; 353 | } 354 | } 355 | 356 | /** Translate occurences of relative datetime durations 357 | * 'minutes', 'hours', 'days' in a string. 358 | * 359 | * @param string $durationString 360 | * 361 | * @return string with the occurrences replaced 362 | */ 363 | protected function translateRelativeDate($durationString) 364 | { 365 | $toTranslate = array('minutes', 'hours', 'days'); 366 | $translated = $this->translateRelativeDateArray($toTranslate); 367 | 368 | return str_replace( 369 | $toTranslate, 370 | $translated, 371 | $durationString); 372 | } 373 | 374 | /** Translate relative datetime durations 375 | * @param array $toTranslate 376 | * 377 | * @return array with durations translated 378 | */ 379 | protected function translateRelativeDateArray($toTranslate) 380 | { 381 | if (!isset($this->translateRelativeDateArray)) { 382 | $t = $this->get('translator'); 383 | $this->translateRelativeDateArray = array(); 384 | foreach ($toTranslate as $name) { 385 | $this->translateRelativeDateArray[] = $t->trans('relativeDate.'.$name, array(), 'metaclass_auth_guard'); 386 | } 387 | } 388 | 389 | return $this->translateRelativeDateArray; 390 | } 391 | 392 | /** Build a menu. 393 | * @param array $params 394 | * @param string $currentRoute 395 | */ 396 | protected function buildMenu(&$params, $currentRoute) 397 | { 398 | // To be overridden by subclass 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /Resources/doc/changelog.txt: -------------------------------------------------------------------------------- 1 | SecuBundle 2 | x toevoegen 3 | + Entity maken 4 | x UserChecker maken 5 | - username and ip failed login confidence govener 6 | ivm timing attack eerst authentication doen. 7 | x UserAuthenticationProvider::authenticate lekt user not found via timing! 8 | UserProvider returnt niet bestaande user met onmogelijke password hash 9 | x AuthenticationGuard 10 | x class maken en service injecten 11 | x exceptions catchen 12 | x Governer aansturen 13 | x AuthenticationGovernor maken die de beslissingen neemt 14 | x tellingen opslaan en bijwerken in de database 15 | x tellingen ophalen uit de database 16 | x vrijgeven fucnties 17 | x bij succes vrijgeven 18 | x tests uitbreiden voor oude requests en oude releases 19 | x tests mbt undistinctive agent 20 | x mbt blokkeren per user: 21 | x userReleasedForAddressAndAgentAt zetten bij successvolle login 22 | x of userReleasedAt 23 | x tests counting 24 | x tests checkAuthentication 25 | x configuraties naar yml verplaatsen 26 | + user-vanuit-email functie voor vrijgeven username (elk ip address?) 27 | je moet dan wel je wachtwoord wijzigen 28 | + user-vanuit-email functie voor vrijgeven combinatie van username en ip address/agent 29 | als dit is gedaan willen we voor deze username de failedLogins vanaf andere ip adressen/agents niet meer meetellen 30 | SecuBundle: 31 | x AuthenticationGuard: 32 | x clear als andere username na re-authenticate 33 | sessie blijft bestaan als zelfde user. ook als andere user 34 | komt door security: session_fixation_strategy: migrate (alt: invalidate). 35 | nadeel van invalidate is dat we de scouting data zullen gaan verliezen, daarom opgelost in Guard 36 | SecuBundle 37 | x AuthenticationGuard no longer inherits from UsernamePasswordFormAuthenticationListener 38 | x login username en password are filtered and validated 39 | x no longer does authentication when blocking 40 | ------------ 41 | - rename Metaclass\SecuBundle to Metaclass\AuthenticationGuardBundle 42 | - rename Metaclass\\SecuBundle to Metaclass\\AuthenticationGuardBundle 43 | - rename MetaclassSecuBundle to MetaclassAuthenticationGuardBundle 44 | - rename MetaclassSecuExtension to MetaclassAuthenticationGuardExtension 45 | - rename metaclass_secu to metaclass_auth_guard 46 | - rename AuthenticationGuard to UsernamePasswordFormAuthenticationGuard 47 | - rename AuthenticationGovernor to TresholdsGovernor 48 | changed: 49 | app 50 | - appKernel.php 51 | app/cofig 52 | - config_dev.yml 53 | - config_prod.yml 54 | - routing.yml 55 | - security.yml 56 | src/Metaclass/UserBundle/Controller 57 | - LoginController.php 58 | src/Metaclass/SecuBundle to be deleted and replaced by AuthenticationGuardBundle 59 | -------- 60 | AuthenticationGuardBundle 61 | - added configuration option for entitymanager name 62 | - added README.md, LICENSE, composer.json 63 | - removed default view 64 | ---------- 65 | AuthenticationGuardBundle 66 | - TresholdsGovernor::releaseUserName, ::releaseUserNameForIpAddressAndUserAgent, ::adminReleaseIpAddress 67 | bug solved: $dateTime was still formatted as date without time 68 | - RequestCountsRepository::updateColumnWhereColumnNullAfterSupplied now expects $value to be a DateTime object 69 | - UserChecker removed 70 | -------------------- 71 | (several documentation improves, improved default settings) 72 | --------- 73 | Issue #1: 74 | Create indexes in the database, add the Data definition SQL to the 75 | install documentation 76 | --------- 77 | (several attempts to improve documentation layout) 78 | ---------------------- 79 | Issue #2 Make unit tests run also from the command line: 80 | - Tests/Service/TresholdsGovernorTest::setUp added initialization for !isSet($kernel) 81 | - Resources/doc/Installation.md added instructions for adding testsuite to app/phpunit.xml 82 | ---------------------- 83 | Issue #8 limit dependencies from Doctrine to DBAL 84 | - added default 0 for counters to Data Definition 85 | - RequestCountsRepository::createWith now performs INSERT query instead of returning an Entity 86 | - TresholdsGovernor now uses RequestCountsRepository instead of the entity 87 | ::registerAuthenticationSuccess, registerAuthenticationFaulure now just call RequestCountsRepository::createWith to insert 88 | ::__construct now instatiates RequestCountsRepository 89 | - RequestCountsRepository no longer extends EntityRepository 90 | - Entity\RequestCounts removed 91 | ---------------------- 92 | Issue #8 limit dependencies from Doctrine to DBAL 93 | - RequestCountsRepository::__construct now expects a Doctrine\DBAL\Connection 94 | ::getConnection added 95 | replaced $this->getEntityManger()->getConnection() by $this->getConnection() 96 | - TresholdsGovernor now uses Doctrine\DBAL\Connection instead of EntityManager 97 | __construct now expects and stores Connection 98 | - services.yml replaced service metaclass_auth_guard.entity_manager_login by metaclass_auth_guard.connection_login 99 | - Configuration::getConfigTreeBuilder now defines db_connection instead of entity_manager_login 100 | - MetaclassAuthenticationGuardExtension::load now sets parameter metaclass_auth_guard.db_connection.name instead of entity_manager_login.name 101 | - security.yml now: 102 | metaclass_authentication_guard: 103 | db_connection: 104 | name: login 105 | - composer.json replaced dependency "doctrine/orm": ">=2.2.3,<2.4-dev", by "doctrine/dbal": "2.3.*", 106 | ! "doctrine/doctrine-bundle" has require-dev "doctrine/orm" 107 | - doc/Installation.md now documents db_connection setting 108 | ---------------- 109 | Data Definition now includes defaults for counters 110 | -------------- 111 | Issue #3 Remove user agent from RequestCounts 112 | - Data Definition rename 'userReleasedForAddressAndAgentAt' by 'userReleasedForAddressAndCookieAt' 113 | replace column 'agent' by 'cookieToken' varchar 40 114 | long enough to hold bin2hex($this->secureRandom->nextBytes(20)); //CsrfProvider uses SHA1 , is 20 bytes 115 | - RequestCountsRepository replaced agent by cookieToken 116 | ::isUserReleasedForAgentFrom renamed to ::isUserReleasedByCookieFrom 117 | ::getIdWhereDateAndUsernameAndIpAddressAndAgent renamed to ::getIdWhereDateAndUsernameAndIpAddressAndCookie 118 | ::qbWhereDateAndUsernameAndIpAddressAndAgent renamed to qbWhereDateAndUsernameAndIpAddressAndCookie 119 | ::findByDateAndUsernameAndIpAddressAndAgent removed 120 | - TresholdsGovernor now uses UsernameBlockedForCookieException 121 | >>isUserReleasedOnAgent renamed to >>isUserReleasedByCookie, default false 122 | >>failureCountForUserOnAgent renamed to >>failureCountForUserByCookie 123 | >>allowReleasedUserOnAgentFor renamed to >>allowReleasedUserByCookieFor 124 | >>distinctiveAgentMinLength removed 125 | ::isAgentDistinctive removed 126 | ::releaseUserNameForIpAddressAndUserAgent renamed to :: 127 | column 'userReleasedForAddressAndAgentAt' replaced by 'userReleasedForAddressAndCookieAt' 128 | call to :getIdWhereDateAndUsernameAndIpAddressAndAgent replaced by ::getIdWhereDateAndUsernameAndIpAddressAndCookie 129 | >>isUserReleasedOnAddress default false 130 | ::initFor no longer derives isUserReleasedByCookie if not allowReleasedUserByCookieFor 131 | no longer derives isUserReleasedOnAddress if not allowReleasedUserOnAddressFor 132 | - UsernameBlockedForAgentException renamed to UsernameBlockedForCookieException 133 | - UsernamePasswordFormAuthenticationGuard::attemptAuthentication 134 | no longer passes user agent, passes empty string because setting and getting cookies not yet implemented 135 | - Configuration::getConfigTreeBuilder removed allowReleasedUserOnAgentFor, distinctiveAgentMinLength 136 | - TresholdsGovernorTest removed distinctiveAgentMinLength setting 137 | replaced renameed TresholdsGovernor properties and the renamed Exception 138 | ::testCheckAuthenticationWithUserReleasedOnIpAddressAndAgent renamed to ::testCheckAuthenticationWithUserReleasedOnIpAddressAndCookie 139 | ::testCheckAuthenticationWithUserReleasedOnIpAddressAndCookie removed section for $this->governer->distinctiveAgentMinLength = 7; 140 | ::testBlockingDurations because no check for cookieToken min length now expecting: 141 | failureCountForUserName = 6 ; 5 seconds less then 10 days: 2 142 | failureCountForUserByCookie = 2 ; 5 seconds less then 10 days: 1 143 | ::testReleaseDurations because no check for cookieToken min length now expecting: 144 | failureCountForUserByCookie = 2 145 | - doc/Installation.md removed allowReleasedUserOnAgentFor and distinctiveAgentMinLength 146 | - doc/Counting and deciding.md adapted to the release by user agent being replaced by release by cookie token and not being used 147 | - Readme.md changed requirement with respect to doctrine, 148 | removed release note with respect to unit tests not running from the command line 149 | --------------------- 150 | Issue #9 cross framework library refactoring 151 | - Moved TresholdsGovernor, RequestCountsRepository to metaclass-nl/tresholds-governor repository 152 | - UsernamePasswordFormAuthenticationGuard now uses Metaclass\TresholdsGovernor\Service\TresholdsGovernor 153 | ::attemptAuthentication now expects TresholdsGovernor::checkAuthentication to return null 154 | or a Metaclass\TresholdsGovernor\Result\Rejection and throws corresponding exception 155 | - Tests\Service\TresholdsGovernorTest moved actual tests to metaclass-nl/tresholds-governor repository, 156 | added delegations to Metaclass\TresholdsGovernor\Tests\Service\TresholdsGovernorTest 157 | - Resources/config/services.yml adapted 158 | - composer.json adapted 159 | - documentation adapted 160 | ------------------------- 161 | - Readme.md and Resource/doc/Installation.md added metaclass-nl/tresholds-governor 162 | --------------------- 163 | - corrected default settings 164 | ------------------------------- 165 | Issue #6 Add a separate last releases table so that RequestCounts records do not need to be kept for much longer treshold 'allowReleasedUserOnAddressFor' . 166 | - Resources/config/services.yml now creates DbalGateway and passes it to tresholdsGoverner according to modified constructor parameters 167 | - Tests\Service\TresholdsGovernorTest renamed testDelete methods and delegations 168 | ------------------------------------ 169 | Issue #6 Add a function for clean-up of the RequestCounts 170 | - TresholdsGovernorTest::testPackData added 171 | - Resources/doc/Installation.md step 4 now refers to tresholdsgovernor library documentation 172 | ----------------------------------- 173 | Issue #7 facilitate custom NoSQL storage 174 | - services.yml added metaclass_auth_guard.manager.class: "Metaclass\TresholdsGovernor\Manager\RdbManager" 175 | added metaclass_auth_guard.manager service 176 | metaclass_auth_guard.tresholds_governor now uses metaclass_auth_guard.manager 177 | --------------------------------------- 178 | Issue #9 cross framework library refactoring 179 | - corrected in-bundle documentation 180 | Issue #6 Add a function for clean-up of the RequestCounts 181 | - Installation.md added how to call the function from Cron or so 182 | -------------------------------------------- 183 | To support UserController 184 | - UsernamePasswordFormAuthenticationGuard::filterCredentials now public static 185 | - TresholdsGovernorTest adapted 186 | - UsernamePasswordFormAuthenticationGuard::filterCredentials factored out ::filterPassword 187 | To support locking history for administrator 188 | - Configuration added keepCountsFor 189 | - Form\Type\StatsPeriodType added 190 | To correct errorneous application of limits 191 | - TresholdsGovernorTest limits adapted 192 | comitted, not pushed 193 | ---------------------------------------- 194 | - moved GuardStatsController and statistics.html.twig from MetaclassUserBundle 195 | - GuardStatsController>>dateTimeFormat now international format 196 | comitted, pushed 197 | ------------------- 198 | - GuardStatsController::__construct now passes \IntlDateFormatter::GREGORIAN to DateTimeToLocalizedStringTransformer 199 | comitted, pushed 200 | ------------------ 201 | - config/routing.yml added remarks 202 | - README.md added user interface for user administrators to features list 203 | - doc/Installation.md documented configuration for user interface for user administrators 204 | added keepCountsFor parameter to configurations 205 | committed, pushed 206 | ------------------- 207 | - Added supports for fixed execution times to migitate possible timing attacks 208 | committed, pushed 209 | -------------------- 210 | - requires tresholds governor 0.2 211 | comitted, pushed, tagged v0.3 212 | ---------------------- 213 | - composer dependencies removed from composer.json 214 | - requires tresholds governor >=0.2 215 | comitted, pushed 216 | ---------------------- 217 | - superficially adapted to symfony 2.6 (still uses depricated features) 218 | comitted, pushed 219 | ------------------------ 220 | Invalid form caused ContextErrorException: Notice: Undefined index: From 221 | - Controller\GuardStatsController::historyAction checks limits to exist 222 | committed, pushed 223 | ------------------------ 224 | Issue #16: Missing used MenuItem package/dependency 225 | - README.md 226 | removed feature: 227 | - Web based user interface for user administrators 228 | - Resources/doc/Installation.md 229 | 7. You may also add 230 | changed to: You also need to add 231 | 232 | Debugging on clean install: 233 | - Resources/doc/Installation.md 234 | 7. You may also add 235 | db_connection: 236 | name: "default" 237 | 5. Add the bundle to your AppKernel: 238 | removed .php from class name 239 | - DependencyInjection\Configuration::getConfigTreeBuilder 240 | db_connection name '' changed to 'default' 241 | committed, pushed 242 | --------------------------------------- 243 | Issue #16: Missing used MenuItem package/dependency 244 | - removed dependency StatisticsController of MetaclassCoreBundle MenuItem 245 | - statistics.html.twig 246 | . factored out statistics_content.html.twig 247 | . changed dependency of MetaclassCoreBundle::layout.html.twig to base.html.twig 248 | . removed view icon specific to bootstrap layout 249 | . added hyperlinks to history and byUsername 250 | - GuardStatsController actions now use template configuration parameter metaclass_auth_guard.statistics.template 251 | - services.yml now defines default for metaclass_auth_guard.statistics.template 252 | committed, pushed 253 | --------------------------------------------- 254 | Various 255 | - Added .idea to .gitignore 256 | - Corrected README.md (Requirements, Release notes) 257 | - Installation.md Returned instructions with respect to the web based adminstration user interface 258 | - GuardStatsController::statisticsByUserNameAction checks limits to exist 259 | committed 260 | ---------------------------------------------- 261 | Symfony 2.3 compatibility 262 | - UsernamePasswordFormAuthenticationGuard again uses SecurityContextInterface 263 | - Resources/views/Entity/editrow.html.twig 264 | - composer.json now requires symfony >=2.3.8,<3.0-dev 265 | Various 266 | - Corrected README.md (Requirements) 267 | comitted, pushed 268 | ------------------------------------------------ 269 | Revert to targeting Symfony 2.6-2.8 270 | - UsernamePasswordFormAuthenticationGuard 271 | - composer.json 272 | - Corrected README.md (Requirements) 273 | -------------------------------------- 274 | (master - merged changes from branche 0_3) 275 | Issue #15: Remove hard-coded Dutch labels 276 | - Controller\GuardStatsController replaced label values by translation keys 277 | replaced options and their lookup by methods and calls 278 | ::addStatsPeriodForm now passes datetype and datetime pattern to formtype 279 | ::translateRelativeDate adapted 280 | ::initDateFormatAndPattern, ::initDateTimeTransformer now use configuration parameter and support locale-dependent date formatting 281 | - Form\Type\StatsPeriodType now expects and uses datetype and datetime pattern 282 | - Resources/translations/metaclass_auth_guard.nl.yml added with Dutch translations 283 | - Resources/translations/metaclass_auth_guard.en.yml added with English translations 284 | - Resources/views/Guard/statistics_content.html.twig added trans calls 285 | - DependencyInjection\Configuration::getConfigTreeBuilder added ui.dateTimeFormat 286 | - DependencyInjection\MetaclassAuthenticationGuardExtension::load now sets param: 287 | metaclass_auth_guard.ui.dateTimeFormat 288 | - Readme.md restored the Web based user interface feature 289 | - Resources/doc/Installation.md added doc on: 290 | - Bundle parameter metaclass_auth_guard.ui.dateTimeFormat 291 | - services parameter metaclass_auth_guard.statistics.StatsPeriod.formType 292 | - Available translations (&clone me on Github) 293 | 294 | Issue #17: Default configuration set without adding to config.yml 295 | - DependencyInjection\Configuration::getConfigTreeBuilder added ->addDefaultsIfNotSet() to each arraynode 296 | 297 | Make web based user interface more extendable: 298 | - Resources/config/services.yml added parameters metaclass_auth_guard.statistics.StatsPeriod.formType 299 | - Controller\GuardStatsController::addStatsPeriodForm now uses parameter metaclass_auth_guard.statistics.StatsPeriod.formType 300 | ::initDateFormatAndPattern may be overridden for setting locale dependent (custom) patterns 301 | ---- 302 | #14: Make user interface independent of DoctrineBundle 303 | - Controller\GuardStatsController no longer uses annotations 304 | - Resources/config/routing.yml now defines each route individually 305 | - Resources/config/services.yml added param metaclass_auth_guard.ui.statistics.controller 306 | - Resources/doc/Installation.md 307 | 9. The user interface for user administrators 308 | added info about the controller template 309 | 310 | Make web based user interface more extendable: 311 | - DependencyInjection\Configuration::getConfigTreeBuilder added ui.statistics.template 312 | - DependencyInjection\MetaclassAuthenticationGuardExtension::load now sets param: 313 | metaclass_auth_guard.ui.statistics.template 314 | - Resources/config/services.yml 315 | . corrected the param name for the StatsPeriod formType 316 | . removed the template param 317 | - Resources/doc/Installation.md 318 | 9. The user interface for user administrators 319 | removed info about the template parameter 320 | 12. Datetime format used by the web based user interface 321 | title added 322 | 13. Template used by the web based user interface for user administrators 323 | added 324 | comitted, pushed 325 | ------------------------------------------------------ 326 | (master) 327 | Corrected documentation 328 | - Resources/doc/Installation.md 329 | 9. The user interface for user administrators 330 | removed 'experimental', added info on enabling translation 331 | committed, pushed 332 | --------------------------------------------------------- 333 | Adaptation to Symfony 2.8 334 | - Resources/config/services.yml escaped \ in quoted strings 335 | committed, pushed, published 336 | ---------------------------------------------------------- 337 | Layout 338 | - Resources/doc/Installation.md added ```yml markings 339 | committed, pushed, published 340 | ----------------------------------------------------------- 341 | Layout 342 | - Resources/doc/Installation.md some details 343 | committed, pushed, published 344 | created branche 0_4 345 | ------------------------------------------------------- 346 | (master) 347 | #20: Adapt to Symfony 3.0 348 | - composer.json require "symfony/symfony": ">=3.0" 349 | 350 | The methods Definition::setFactoryClass(), Definition::setFactoryMethod(), and Definition::setFactoryService() have been removed in favor of Definition::setFactory(). Services defined using YAML or XML use the same syntax as configurators. 351 | - Resources/config/services.yml doctrine connection service definition now: 352 | metaclass_auth_guard.connection_login: 353 | class: Doctrine\DBAL\Connection 354 | factory: ["@doctrine", getConnection] 355 | arguments: [%metaclass_auth_guard.db_connection.name%] 356 | 357 | Controller::createForm and FormFactory::createNamedBuilder now require fully qualyfied class name of type instead instance 358 | - Form\Type\StatsPeriodType::__construct removed 359 | ::buildForm now obtains parameters from $options 360 | - Controller\GuardStatsController::addStatsPeriodForm 361 | replaced $this->createForm by code from FormFactory::createNamedBuilder to pass options 362 | Maybe we should implement $formTypeClass::configureOptions instead? 363 | 364 | FormBuilderInterface::add now requires fully qualyfied class name of type 365 | - Form\Type\StatsPeriodType::buildForm now uses ::class 366 | 367 | The getBlockPrefix() method was added to the FormTypeInterface in replacement of the getName() method which has been has been removed. 368 | - Form\Type\StatsPeriodType::getName renamed to ::getBlockPrefix 369 | 370 | Passing a Symfony\Component\HttpFoundation\Request instance, as was supported by FormInterface::bind(), is not possible with FormInterface::submit() anymore. You should use FormInterface::handleRequest() instead. 371 | - Controller\GuardStatsController::addStatsPeriodForm 372 | replaced $form->submit($request) by $form->handleRequest($request) 373 | 374 | FormInterface::handleRequest only processes request parameters if form method == request method 375 | - Controller\GuardStatsController::addStatsPeriodForm 376 | added method from request to $builderOptions 377 | 378 | The getRequest method of the base Controller class has been deprecated since Symfony 2.4 and must be therefore removed in 3.0. The only reliable way to get the Request object is to inject it in the action method. 379 | - Controller\GuardStatsController 380 | ::statisticsAction added $request parameter and replaced $this->getRequest() 381 | now passes $request to ::addCountsGroupedTableParams and ::addStatsPeriodForm 382 | ::addCountsGroupedTableParams added parameter $request, replaced $this->getRequest 383 | ::addStatsPeriodForm added parameter $request, replaced $this->getRequest() 384 | ::historyAction added $request parameter, now passes $request to ::addStatsPeriodForm 385 | ::statisticsByUserNameAction added $request parameter and replaced $this->getRequest() 386 | now passes $request to ::addStatsPeriodForm 387 | 388 | The form_enctype helper was removed. 389 | - Resources/views/Guard/statistics_content.html.twig replaced {{ form_enctype(form) }} by enctype="multipart/form-data" 390 | committed, pushed, published 391 | ------------------------------------------ 392 | #20: Adapt to Symfony 3.0 393 | - README.md adapted requirements 394 | committed, pushed, published 395 | ------------------------------------------- 396 | #20: Adapt to Symfony 3.0 397 | Controller::createForm and FormFactory::createNamedBuilder now require fully qualyfied class name of type instead instance 398 | Implement ::configureOptions instead of replacing Controller::createForm 399 | - Form\Type\StatsPeriodType::configureOptions added 400 | - Controller\GuardStatsController::addStatsPeriodForm now passes all options to Controller::createForm 401 | committed, pushed, published 402 | ------------------------------------ 403 | #14: Symfony / Doctrine Compatibility 404 | Adaptation to Symfony 3.0 has made all tests run on Symfony 2.8.1. and the user interface for administrators run without deprication warnings 405 | - composer.json require "symfony/symfony": ">=2.8.1" 406 | - README.md adapted requirements 407 | committed, pushed, published 408 | --------------------------- 409 | #14: Symfony / Doctrine Compatibility 410 | - README.md adapted requirements 411 | committed, pushed 412 | -------------------------- 413 | (merged from branche 0_3) 414 | #21: English boolean translations inverted 415 | - Resources/translations/metaclass_auth_guard.en.yml corrected 416 | ----------------------------------------- 417 | (merged from branche 0_4) 418 | #20: Adapt to Symfony 3.0 419 | Since Symfony 2.6 last conctructor argument may be a CsrfTokenManagerInterface 420 | Since 3.0 it is typehinted CsrfTokenManagerInterface 421 | - Service\UsernamePasswordFormAuthenticationGuard renamed $csrfProvider to $csrfTokenManager 422 | ::__construct replaced CsrfProviderInterface by CsrfTokenManagerInterface 423 | ::checkCrsfToken replaced with code from UsernamePasswordFormAuthenticationListener 424 | comitted, pushed, published 425 | ------------------------------- 426 | Tested with FOSUserBundle 427 | - adapted Installation.md (merged from branche 0_4) 428 | - adapted README.md (master) 429 | ---------------------------------- 430 | (master) 431 | #14: Symfony / Doctrine Compatibility: compatibility with php < 5.5 432 | - Form\Type\StatsPeriodType::buildForm replace ::class by Fully Qualified class names 433 | Other 434 | - GuardStatsController removed use statements for unused classes 435 | comitted, pushed, published/ 436 | ------------------------------------------- 437 | (master) 438 | #20: Adapt to Symfony 3.0 439 | - Framework startup has changed 440 | . Resources/doc/Installation.md adapted 8. From cron or so 441 | -------------------------------------------- 442 | (master) 443 | 唐兵兵 committed 444 | - Add Chinese Translation 445 | ------------------------------------ 446 | (master) 447 | stof committed on GitHub 448 | - Fix the markup in the doc 449 | ----------------------------------- 450 | (master) 451 | Merge pull request #24 from stof/patch-1 452 | - Use bound composer constraints 453 | --------------------------------- 454 | (master) 455 | Merge pull request #25 from stof/patch-2 456 | Fix the markup in the doc 457 | ------------------------------- 458 | (master) 459 | - Chinese translation adapted to google translate 0: 否, 1: 是 460 | ------------------------------------- 461 | (master) 462 | #9: cross framework library refactoring 463 | - TestholdsGovernorTest factored out TresholdsGovernor FunctionalTest 464 | Tresholds Governor Issue 2: support several rdb access interfaces 465 | - Resources/config/services.yml Renamed DbalGateway to RdbGateway 466 | - adapted services.yml, composer.json, README.ms 467 | ------------------------------------ 468 | (master) 469 | Support separate \Metaclass\TresholdsGovernor\Manager\StatisticsManagerInterface 470 | - Added alias metaclass_auth_guard.statistics_manager to services.yml 471 | - TresholdsGovernorTest now uses service metaclass_auth_guard.statistics_manager 472 | - GuardStatsController now uses service metaclass_auth_guard.statistics_manager 473 | Typo in English translation 474 | - secu_requests.blockedColumns corrected 'attempts' 475 | ------------------------------------ 476 | (master) 477 | #10: api docs 478 | - Completed api docs 479 | ------------------------------------- 480 | (master) 481 | #13: PSR-2 482 | - ran PHP Coding Standards Fixer with --level=symfony (https://github.com/FriendsOfPhp/PHP-CS-Fixer) 483 | --------------------------------------- --------------------------------------------------------------------------------