Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Latest
* [#89](https://github.com/cleverage/ui-process-bundle/issues/89) `ProcessConfigurationsManager`: resolve the `ui.default` option with a normalizer instead of nested options defined with `setDefault()` (deprecated since symfony/options-resolver 7.3, removed in 8.0). With Symfony 8, a process launched with the UI form (`ui_launch_mode: form`) without `ui.default` no longer fails (`Cannot use object of type Closure as array`), and `ui.default` is validated again. Add tests.
* [#91](https://github.com/cleverage/ui-process-bundle/issues/91) `LoginController`: pass `error` and `last_username` (`AuthenticationUtils`) to the login template, so that a failed login displays the error message and keeps the email. Test updated.

## BC break
* [#93](https://github.com/cleverage/ui-process-bundle/issues/93) Protect the login form against CSRF: `enable_csrf` on the prepended `form_login` configuration, `csrf_token_intention` passed to the login template, `symfony/security-csrf` declared. Add tests.
* Please follow [UPGRADE.md v4.0](UPGRADE.md#v40)

v3.0.2
------

Expand Down
25 changes: 25 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
Upgrade Guide
=============

## v4.0

### CSRF protection of the login form

The `form_login` configuration prepended by the bundle now enables `enable_csrf` (token id `authenticate`, parameter
`_csrf_token`), and the login template renders the `_csrf_token` field. The Symfony CSRF protection must be enabled
(`framework.csrf_protection`, enabled by default when the session is).

If you override `@CleverAgeUiProcess/admin/login.html.twig` or submit the login form yourself, add the token:

```twig
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
```

To keep the previous behaviour, disable it on your `main` firewall:

```yaml
# config/packages/security.yaml
security:
firewalls:
main:
form_login:
enable_csrf: false
```

## v3.0

### Import routes
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"symfony/scheduler": "^6.4 || ^7.4 || ^8",
"symfony/security-bundle": "^6.4 || ^7.4 || ^8",
"symfony/security-core": "^6.4 || ^7.4 || ^8",
"symfony/security-csrf": "^6.4 || ^7.4 || ^8",
"symfony/security-http": "^6.4 || ^7.4 || ^8",
"symfony/serializer": "^6.4 || ^7.4 || ^8",
"symfony/string": "^6.4 || ^7.4 || ^8",
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/03-users_and_security.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ security:
form_login:
login_path: process_login
check_path: process_login
enable_csrf: true
logout:
path: process_logout
target: process_login
Expand All @@ -38,6 +39,10 @@ security:

The UI pages are protected by `#[IsGranted]` attributes, no `access_control` rule is required.

The login form is protected by a CSRF token (token id `authenticate`, parameter `_csrf_token`): the Symfony CSRF
protection must be enabled (`framework.csrf_protection`, enabled by default when the session is). If you override the
login template, keep the `_csrf_token` field (the `csrf_token_intention` variable passed by `LoginController`).

Roles
-----

Expand Down
2 changes: 2 additions & 0 deletions src/Controller/Admin/Security/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public function __invoke(AuthenticationUtils $authenticationUtils): Response
'target_path' => '/process',
'error' => $authenticationUtils->getLastAuthenticationError(),
'last_username' => $authenticationUtils->getLastUsername(),
// Token id checked by the form_login authenticator (enable_csrf, see CleverAgeUiProcessExtension)
'csrf_token_intention' => 'authenticate',
]
);
}
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/CleverAgeUiProcessExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public function prepend(ContainerBuilder $container): void
'form_login' => [
'login_path' => 'process_login',
'check_path' => 'process_login',
'enable_csrf' => true,
],
'logout' => [
'path' => 'process_logout',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public function testPrepend(): void
'main' => [
'provider' => 'process_user_provider',
'custom_authenticator' => ['cleverage_ui_process.security.http_process_execution_authenticator'],
'form_login' => ['login_path' => 'process_login', 'check_path' => 'process_login'],
'form_login' => ['login_path' => 'process_login', 'check_path' => 'process_login', 'enable_csrf' => true],
'logout' => ['path' => 'process_logout', 'target' => 'process_login', 'clear_site_data' => '*'],
],
],
Expand Down
17 changes: 17 additions & 0 deletions tests/Functional/SecurityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,23 @@ public function testLoginWithAnInvalidPassword(): void
self::assertResponseRedirects('http://localhost/process/login');
}

public function testLoginWithAnInvalidCsrfToken(): void
{
$this->createUser('admin@example.com', ['ROLE_ADMIN'], 'secret');

$crawler = $this->client->request('GET', '/process/login');
self::assertCount(1, $crawler->filter('input[type="hidden"][name="_csrf_token"]'));
$form = $crawler->filter('form')->form(['_username' => 'admin@example.com', '_password' => 'secret']);
$form['_csrf_token'] = 'invalid';
$this->client->submit($form);

self::assertResponseRedirects('http://localhost/process/login');
$this->client->followRedirect();
self::assertSelectorTextContains('.alert-danger', 'Invalid CSRF token.');
$this->client->request('GET', '/process');
self::assertResponseRedirects('http://localhost/process/login');
}

public function testLogout(): void
{
$this->login();
Expand Down
Loading