Skip to content

Commit 97baedb

Browse files
authored
Merge pull request #216 from openeuropa/contribution/OPES-1515-2fa-conditions
Allow to selectively require two-factor authentication for an account
2 parents b608630 + 494317f commit 97baedb

19 files changed

+1433
-83
lines changed

README.md

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,27 +80,47 @@ authenticate via CAS when they hit all or some of the pages on your site.
8080

8181
See the [Cas module](https://www.drupal.org/project/cas) for more information.
8282

83-
### SSL Verification Setting
83+
### Proxy
8484

85-
The EU Login Authentication server must be accessed over HTTPS and the drupal site will verify the SSL/TLS certificate
86-
of the server to be sure it is authentic.
85+
You can configure the module to "Initialize this client as a proxy" which allows
86+
authentication requests to 3rd party services (e.g. ePOETRY).
8787

88-
For development, you can configure the module to disable this verification:
8988
```php
90-
$config['cas.settings']['server']['verify'] = '2';
89+
$config['cas.settings']['proxy']['initialize'] = TRUE;
9190
```
92-
_NOTE: DO NOT USE IN PRODUCTION!_
9391

9492
See the [Cas module](https://www.drupal.org/project/cas) for more information.
9593

96-
### Proxy
94+
### Two-factor authentication
95+
The module allows to configure if two-factor authentication (in short, 2FA) is required for users to log in.
96+
It supports three modes:
97+
* _Never_: 2FA is never required for any registered user account.
98+
* _Always_: 2FA is always required for any registered user account. A 2FA authentication method will be enforced directly on EU Login.
99+
* _Based on conditions_: 2FA is required only for registered user accounts that match one of the configured conditions. See below.
97100

98-
You can configure the module to "Initialize this client as a proxy" which allows
99-
authentication requests to 3rd party services (e.g. ePOETRY).
101+
**Two-factor authentication conditions**
102+
103+
The conditions are validated after the user logged in via EU Login, and only if
104+
the login was executed without using a 2FA authentication method.\
105+
If at least one of the configured conditions evaluate successfully for the user account, the login
106+
to the website will be rejected.\
107+
A message will be shown to the user to log in again using a suitable method, together
108+
with a link that will allow to choose only 2FA authentication methods on EU Login.
109+
110+
The system will allow to configure any condition plugin (`\Drupal\Core\Condition\ConditionInterface`) that requires a user account as context.\
111+
The default condition available via Drupal core is the User Role condition (`\Drupal\user\Plugin\Condition\UserRole`).
112+
This condition can be configured with the roles that are required to use 2FA authentication methods in order to log in into the website.
113+
114+
### SSL Verification Setting
115+
116+
The EU Login Authentication server must be accessed over HTTPS and the drupal site will verify the SSL/TLS certificate
117+
of the server to be sure it is authentic.
100118

119+
For development, you can configure the module to disable this verification:
101120
```php
102-
$config['cas.settings']['proxy']['initialize'] = TRUE;
121+
$config['cas.settings']['server']['verify'] = '2';
103122
```
123+
_NOTE: DO NOT USE IN PRODUCTION!_
104124

105125
See the [Cas module](https://www.drupal.org/project/cas) for more information.
106126

config/install/oe_authentication.settings.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ assurance_level: TOP
55
ticket_types: SERVICE,PROXY
66
force_2fa: false
77
restrict_user_delete_cancel_methods: true
8+
2fa_conditions: { }
9+
message_login_2fa_required: 'Your account is required to log in using a two-factor authentication method. Please <a href=":login">log in again via this link</a>.'

config/schema/oe_authentication.schema.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
oe_authentication.settings:
22
type: config_object
33
label: 'OpenEuropa Authentication settings'
4+
constraints:
5+
FullyValidatable: ~
46
mapping:
57
protocol:
68
type: string
@@ -23,3 +25,12 @@ oe_authentication.settings:
2325
restrict_user_delete_cancel_methods:
2426
type: boolean
2527
label: 'Restrict access to user cancel methods that permanently delete the account'
28+
2fa_conditions:
29+
type: sequence
30+
label: 'Two-factor authentication conditions'
31+
sequence:
32+
type: condition.plugin.[id]
33+
label: 'Two-factor authentication condition'
34+
message_login_2fa_required:
35+
type: text
36+
label: 'Error message for login denied: two-factor authentication required'

modules/oe_authentication_corporate_roles/tests/src/Kernel/CorporateRolesTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class CorporateRolesTest extends KernelTestBase {
2222
* {@inheritdoc}
2323
*/
2424
protected static $modules = [
25+
'cas',
26+
'externalauth',
2527
'oe_authentication',
2628
'oe_authentication_corporate_roles',
2729
'oe_authentication_user_fields',

oe_authentication.post_update.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,13 @@ function oe_authentication_post_update_00005(): void {
5151
->set('restrict_user_delete_cancel_methods', TRUE)
5252
->save();
5353
}
54+
55+
/**
56+
* Set default values for 2FA conditions and related message.
57+
*/
58+
function oe_authentication_post_update_00006(): void {
59+
\Drupal::configFactory()->getEditable('oe_authentication.settings')
60+
->set('2fa_conditions', [])
61+
->set('message_login_2fa_required', 'Your account is required to log in using a two-factor authentication method. Please <a href=":login">log in again via this link</a>.')
62+
->save();
63+
}

oe_authentication.services.yml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
services:
22
oe_authentication.route_subscriber:
3-
class: \Drupal\oe_authentication\Routing\RouteSubscriber
3+
class: Drupal\oe_authentication\Routing\RouteSubscriber
44
tags:
55
- { name: event_subscriber }
66
oe_authentication.event_subscriber:
7-
class: \Drupal\oe_authentication\Event\EuLoginEventSubscriber
7+
class: Drupal\oe_authentication\Event\EuLoginEventSubscriber
88
tags:
99
- { name: event_subscriber }
10-
arguments: ['@config.factory']
10+
arguments: ['@config.factory', '@request_stack']
1111
oe_authentication.messenger.event_subscriber:
12-
class: \Drupal\oe_authentication\Event\MessengerEuLoginEventSubscriber
12+
class: Drupal\oe_authentication\Event\MessengerEuLoginEventSubscriber
1313
tags:
1414
- { name: event_subscriber }
1515
arguments: ['@messenger']
16+
oe_authentication.subscriber.two_factor_authentication:
17+
class: Drupal\oe_authentication\Event\TwoFactorAuthenticationEventSubscriber
18+
arguments:
19+
- '@config.factory'
20+
- '@plugin.manager.condition'
21+
- '@context.handler'
22+
- '@logger.channel.oe_authentication'
23+
- '@cas.helper'
24+
- '@string_translation'
25+
tags:
26+
- { name: event_subscriber }
27+
logger.channel.oe_authentication:
28+
parent: logger.channel_base
29+
arguments: ['oe_authentication']

runner.yml.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,7 @@ commands:
4747
- { task: "run", command: "setup:behat" }
4848
setup:phpunit:
4949
- { task: "process", source: "phpunit.xml.dist", destination: "phpunit.xml" }
50+
# Generate settings.testing.php, it will be used when running functional tests.
51+
- { task: "process-php", type: "write", config: "drupal.settings", source: "${drupal.root}/sites/default/default.settings.php", destination: "${drupal.root}/sites/default/settings.testing.php", override: true }
5052
setup:behat:
5153
- { task: "process", source: "behat.yml.dist", destination: "behat.yml" }

src/Event/EuLoginEventSubscriber.php

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Drupal\oe_authentication\Event;
66

7+
use Drupal\Component\Utility\UrlHelper;
78
use Drupal\Core\Config\ConfigFactoryInterface;
89
use Drupal\Core\StringTranslation\StringTranslationTrait;
910
use Drupal\cas\Event\CasPostValidateEvent;
@@ -12,8 +13,8 @@
1213
use Drupal\cas\Event\CasPreValidateEvent;
1314
use Drupal\oe_authentication\CasProcessor;
1415
use Drupal\user\UserInterface;
15-
use Symfony\Component\DependencyInjection\ContainerInterface;
1616
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17+
use Symfony\Component\HttpFoundation\RequestStack;
1718

1819
/**
1920
* Event subscriber for CAS module events.
@@ -33,22 +34,20 @@ class EuLoginEventSubscriber implements EventSubscriberInterface {
3334
protected $configFactory;
3435

3536
/**
36-
* Constructors the EuLoginEventSubscriber.
37+
* The request stack.
3738
*
38-
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
39-
* The config factory.
39+
* @var \Symfony\Component\HttpFoundation\RequestStack
4040
*/
41-
public function __construct(ConfigFactoryInterface $configFactory) {
42-
$this->configFactory = $configFactory;
43-
}
41+
protected RequestStack $requestStack;
4442

45-
/**
46-
* {@inheritdoc}
47-
*/
48-
public static function create(ContainerInterface $container) {
49-
return new static(
50-
$container->get('config.factory'),
51-
);
43+
public function __construct(ConfigFactoryInterface $configFactory, ?RequestStack $requestStack = NULL) {
44+
$this->configFactory = $configFactory;
45+
if ($requestStack === NULL) {
46+
// phpcs:ignore Drupal.Semantics.FunctionTriggerError.TriggerErrorTextLayoutRelaxed
47+
@trigger_error('Calling ' . __METHOD__ . '() without the $requestStack argument is deprecated in oe_authentication:1.x and will be required in oe_authentication:2.x.', E_USER_DEPRECATED);
48+
$requestStack = \Drupal::requestStack();
49+
}
50+
$this->requestStack = $requestStack;
5251
}
5352

5453
/**
@@ -110,9 +109,17 @@ public function processUserProperties(CasPreRegisterEvent $event): void {
110109
* The triggered event.
111110
*/
112111
public function forceTwoFactorAuthentication(CasPreRedirectEvent $event): void {
113-
if ($this->configFactory->get('oe_authentication.settings')->get('force_2fa')) {
112+
$config_forced_2fa = $this->configFactory->get('oe_authentication.settings')->get('force_2fa');
113+
$request_forced_2fa = $this->requestStack->getCurrentRequest()->query->has('force_2fa');
114+
115+
if ($config_forced_2fa || $request_forced_2fa) {
114116
$data = $event->getCasRedirectData();
115117
$data->setParameter('authenticationLevel', 'MEDIUM');
118+
119+
// Add a parameter to the service URL to add 2FA during ticket validation.
120+
if ($request_forced_2fa) {
121+
$data->setServiceParameter('force_2fa', 1);
122+
}
116123
}
117124
}
118125

@@ -148,7 +155,12 @@ public function alterValidationPath(CasPreValidateEvent $event): void {
148155
'userDetails' => 'true',
149156
'groups' => '*',
150157
];
151-
if ($config->get('force_2fa')) {
158+
159+
$service_url = UrlHelper::parse($event->getParameters()['service']);
160+
// Add 2FA validation is 2FA is always required via config, or if it has
161+
// been required for this request.
162+
// @see ::forceTwoFactorAuthentication()
163+
if ($config->get('force_2fa') || isset($service_url['query']['force_2fa'])) {
152164
$params['authenticationLevel'] = 'MEDIUM';
153165
}
154166
$event->addParameters($params);

0 commit comments

Comments
 (0)