Skip to content

Commit 6c01bdc

Browse files
committed
Initial commit
0 parents  commit 6c01bdc

File tree

7 files changed

+388
-0
lines changed

7 files changed

+388
-0
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#Module purpose
2+
3+
The aim of this module is to enhance integration with **simplesamlphp_auth** module, by force triggering **SimpleSAML auth page** redirect when certain criteria are met.
4+
5+
#How does it work
6+
7+
Module performs checks on a single redirect triggering page. In order for it to work the cache for anonymous user for that page response is programmatically killed.
8+
9+
The redirect check cannot be done on all pages. Reason for that is the performance. The redirect only works properly when page response cache is killed (otherwise response is cached for all anonymous users), so in order for it to work on all pages anonymous page response caches must be killed (which is the same as disabling page cache entirely).
10+
11+
As a compromise between the functionality and performance it has been decided to use a single page to trigger redirect check.
12+
13+
If the request passes all the criteria (meaning user is anonymous and the IP is within whitelist), request is redirected to **SimpleSAML auth page**.
14+
15+
To improve the performance, the redirect decision is stored in cookies to a limited time.
16+
17+
Additionally module provides a special field for user entity, called **SimpleSAML UID** that allows to create a **SimpleSAML mapping** with the existing Drupal users.
18+
19+
#Additional setings
20+
21+
- **IP's whitelist**
22+
Comma separate values of IP or IP ranges that will be redirected to SimpleSAML auth page.
23+
- **Redirect triggering page**
24+
A certain page that triggers the redirect to SimpleSAML auth page if the criteria pass (_defaults: front page "/"_).
25+
- **Cookies TTL**
26+
Stores the redirect response in the cookies for a certain period of time (_defaults: 5min_).
27+

composer.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "os2web/os2web_simplesaml",
3+
"type": "drupal-module",
4+
"description": "Enhances integration with simplesamlphp_auth module, by force triggering SimpleSAML auth page redirect when certain criteria are met",
5+
"minimum-stability": "dev",
6+
"prefer-stable": true,
7+
"license": "GPL-2.0+",
8+
"require": {
9+
"drupal/simplesamlphp_auth": "^3.1"
10+
}
11+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
redirect_ips: ''
2+
redirect_trigger_path: /
3+
redirect_cookies_ttl: '300'

os2web_simplesaml.info.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: OS2Web SimpleSAML Authentication
2+
type: module
3+
description: Enhances integration with simplesamlphp_auth module, by force triggering SimpleSAML auth page redirect when certain criteria are met.
4+
package: OS2Web
5+
core: 8.x
6+
configure: simplesamlphp_auth.admin_settings_local
7+
8+
dependencies:
9+
- simplesamlphp_auth

os2web_simplesaml.module

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* OS2Web SimpleSAML functionality module.
6+
*/
7+
8+
use Drupal\Core\Form\FormStateInterface;
9+
use Drupal\Core\Url;
10+
11+
/**
12+
* Implements hook_form_FORM_ID_alter().
13+
*
14+
* Adds redirect IPs settings to simplesamlphp_auth_local_settings_form.
15+
*/
16+
function os2web_simplesaml_form_simplesamlphp_auth_local_settings_form_alter(&$form, FormStateInterface $form_state, $form_id) {
17+
$config = \Drupal::config('os2web_simplesaml.settings');
18+
19+
$form['os2web_simplesaml_additional_settings'] = array(
20+
'#type' => 'fieldset',
21+
'#title' => t('OS2Web SimpleSAML additional settings'),
22+
);
23+
$form['os2web_simplesaml_additional_settings']['redirect_ips'] = array(
24+
'#type' => 'textfield',
25+
'#title' => t("Redirect IP's to SimpleSAML login"),
26+
'#default_value' => $config->get('redirect_ips'),
27+
'#description' => t('Comma separated. Ex. 192.168.1.1,192.168.2.1'),
28+
);
29+
$form['os2web_simplesaml_additional_settings']['redirect_trigger_path'] = array(
30+
'#type' => 'textfield',
31+
'#title' => t('Redirect triggering path'),
32+
'#default_value' => $config->get('redirect_trigger_path'),
33+
'#description' => t('The path that will trigger the redirect. NB! The caching for that path will be programmatically disabled.'),
34+
'#required' => TRUE,
35+
);
36+
$form['os2web_simplesaml_additional_settings']['redirect_cookies_ttl'] = array(
37+
'#type' => 'number',
38+
'#min' => 0,
39+
'#step' => 10,
40+
'#title' => t('Redirect cookies time to live (TTL)'),
41+
'#default_value' => $config->get('redirect_cookies_ttl'),
42+
'#description' => t('Number of seconds, after which the positive or negative redirect decision will expire. Setting long time improves the performance, but IP rules change will take longer to become active for all users.'),
43+
'#required' => TRUE,
44+
);
45+
46+
$form['#validate'][] = 'os2web_simplesaml_form_validate';
47+
$form['#submit'][] = 'os2web_simplesaml_form_submit';
48+
}
49+
50+
/**
51+
* Validation for simplesamlphp_auth_local_settings_form.
52+
*
53+
* Checks provided IP list format.
54+
*/
55+
function os2web_simplesaml_form_validate(&$form, FormStateInterface $form_state) {
56+
if ($form_state->hasValue('redirect_ips')) {
57+
$redirect_ips = $form_state->getValue('redirect_ips');
58+
if (preg_match("/[^0-9.,]/", $redirect_ips)) {
59+
$form_state->setErrorByName('redirect_ips', t('Invalid format, must be comma separated. Ex. 192.168.1.1,192.168.2.1'));
60+
}
61+
}
62+
if ($form_state->hasValue('redirect_trigger_path')) {
63+
$redirect_trigger_path = $form_state->getValue('redirect_trigger_path');
64+
$url = Url::fromUserInput($redirect_trigger_path);
65+
if (!$url->isRouted()) {
66+
$form_state->setErrorByName('redirect_trigger_path', t('Invalid URL, this URL does not exist'));
67+
}
68+
}
69+
}
70+
71+
/**
72+
* Submit for simplesamlphp_auth_local_settings_form.
73+
*
74+
* Saves redirect_ips into configuration.
75+
*/
76+
function os2web_simplesaml_form_submit(&$form, FormStateInterface $form_state) {
77+
$redirect_ips = $form_state->getValue('redirect_ips');
78+
$redirect_trigger_path = $form_state->getValue('redirect_trigger_path');
79+
$redirect_cookies_ttl = $form_state->getValue('redirect_cookies_ttl');
80+
81+
$config = \Drupal::service('config.factory')
82+
->getEditable('os2web_simplesaml.settings');
83+
$config->set('redirect_ips', $redirect_ips);
84+
$config->set('redirect_trigger_path', $redirect_trigger_path);
85+
$config->set('redirect_cookies_ttl', $redirect_cookies_ttl);
86+
$config->save();
87+
88+
// Invalidating router cache, so that new settings are applied.
89+
\Drupal::service("router.builder")->rebuild();
90+
}
91+
92+
/**
93+
* Implements hook_entity_extra_field_info().
94+
*/
95+
function os2web_simplesaml_entity_extra_field_info() {
96+
$fields = [];
97+
$fields['user']['user']['form']['os2web_simplesaml_uid'] = [
98+
'label' => t('SimpleSAML UID'),
99+
'description' => '',
100+
'weight' => 0,
101+
'visible' => TRUE,
102+
];
103+
return $fields;
104+
}
105+
106+
/**
107+
* Implements hook_form_FORM_ID_alter().
108+
*
109+
* @see AccountForm::form()
110+
* @see os2web_simplesaml_user_form_includes()
111+
*/
112+
function os2web_simplesaml_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) {
113+
os2web_simplesaml_user_form_includes($form);
114+
115+
// If the user has a simplesamlphp_auth authmap record, then fetch it and
116+
// prefill the field.
117+
$authmap = \Drupal::service('externalauth.authmap');
118+
$account = $form_state->getFormObject()->getEntity();
119+
$authname = $authmap->get($account->id(), 'simplesamlphp_auth');
120+
if ($authname) {
121+
$form['os2web_simplesaml_uid']['#default_value'] = $authname;
122+
}
123+
}
124+
125+
/**
126+
* Implements hook_form_FORM_ID_alter().
127+
*
128+
* @see AccountForm::form()
129+
* @see os2web_simplesaml_user_form_includes()
130+
*/
131+
function os2web_simplesaml_form_user_register_form_alter(&$form, FormStateInterface $form_state, $form_id) {
132+
os2web_simplesaml_user_form_includes($form);
133+
}
134+
135+
/**
136+
* Helper function to include the BC SimpleSAML on user forms.
137+
*
138+
* Alters the user register form to include a textfield for providing custom
139+
* SimpleSAML value.
140+
*
141+
* @param array $form
142+
* The user account form.
143+
*
144+
* @see os2web_simplesaml_user_form_submit()
145+
*/
146+
function os2web_simplesaml_user_form_includes(&$form) {
147+
// Getting SimpleSAML existing authname to use as an example.
148+
$query = \Drupal::database()->select('authmap', 'am')
149+
->fields('am', ['authname'])
150+
->condition('provider', 'simplesamlphp_auth')
151+
->range(0, 1);
152+
$simplesamlExample = $query->execute()->fetchField();
153+
154+
$form['os2web_simplesaml_uid'] = [
155+
'#type' => 'textfield',
156+
'#title' => t('SimpleSAML UID'),
157+
'#access' => \Drupal::currentUser()->hasPermission('change saml authentication setting'),
158+
'#description' => $simplesamlExample ? t('Example of the existing SimpleSAML entry from authmap table: <b>@simplesaml</b>', array('@simplesaml' => $simplesamlExample)) : t('Provide a string serves the UID of the user.')
159+
];
160+
161+
// Adding custom validation.
162+
$form['#validate'][] = 'os2web_simplesaml_user_form_validate';
163+
164+
// Adding custom submit.
165+
$form['actions']['submit']['#submit'][] = 'os2web_simplesaml_user_form_submit';
166+
}
167+
168+
/**
169+
* Form validation handler for user_form.
170+
*/
171+
function os2web_simplesaml_user_form_validate($form, FormStateInterface $form_state) {
172+
$simplesaml_uid = NULL;
173+
if ($form_state->getValue('os2web_simplesaml_uid')) {
174+
$simplesaml_uid = $form_state->getValue('os2web_simplesaml_uid');
175+
}
176+
177+
if (!$form_state->getValue('simplesamlphp_auth_user_enable') && !empty($simplesaml_uid)) {
178+
$form_state->setErrorByName('os2web_simplesaml_uid', t('Field SimpleSAML UID is provided but SAML authentication is not checked, mapping will not be created'));
179+
}
180+
}
181+
182+
/**
183+
* Form submission handler for user_form.
184+
*/
185+
function os2web_simplesaml_user_form_submit($form, FormStateInterface $form_state) {
186+
$authmap = \Drupal::service('externalauth.authmap');
187+
$externalauth = \Drupal::service('externalauth.externalauth');
188+
189+
// Remove this user from the ExternalAuth authmap table.
190+
$authmap->delete($form_state->getValue('uid'));
191+
192+
if ($form_state->getValue('os2web_simplesaml_uid')) {
193+
$simplesaml_uid = $form_state->getValue('os2web_simplesaml_uid');
194+
}
195+
196+
// Add an authmap entry for this account, so it can leverage SAML
197+
// authentication.
198+
if ($simplesaml_uid) {
199+
$account = $form_state->getFormObject()->getEntity();
200+
$externalauth->linkExistingAccount($simplesaml_uid, 'simplesamlphp_auth', $account);
201+
}
202+
}

os2web_simplesaml.services.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
os2web_simplesaml_event_subscriber:
3+
class: Drupal\os2web_simplesaml\EventSubscriber\SimplesamlSubscriber
4+
arguments: ['@simplesamlphp_auth.manager', '@current_user']
5+
tags:
6+
- {name: event_subscriber}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
namespace Drupal\os2web_simplesaml\EventSubscriber;
4+
5+
use Drupal\Core\Session\AccountInterface;
6+
use Drupal\Core\Url;
7+
use Drupal\simplesamlphp_auth\Service\SimplesamlphpAuthManager;
8+
use Symfony\Component\HttpFoundation\RedirectResponse;
9+
use Symfony\Component\HttpKernel\KernelEvents;
10+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
11+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
12+
13+
/**
14+
* Event subscriber subscribing to KernelEvents::REQUEST.
15+
*/
16+
class SimplesamlSubscriber implements EventSubscriberInterface {
17+
18+
/**
19+
* The SimpleSAML Authentication helper service.
20+
*
21+
* @var \Drupal\simplesamlphp_auth\Service\SimplesamlphpAuthManager
22+
*/
23+
protected $simplesaml;
24+
25+
/**
26+
* The current account.
27+
*
28+
* @var \Drupal\Core\Session\AccountInterface
29+
*/
30+
protected $account;
31+
32+
/**
33+
* {@inheritdoc}
34+
*
35+
* @param \Drupal\simplesamlphp_auth\Service\SimplesamlphpAuthManager $simplesaml
36+
* The SimpleSAML Authentication helper service.
37+
* @param \Drupal\Core\Session\AccountInterface $account
38+
* The current account.
39+
*/
40+
public function __construct(SimplesamlphpAuthManager $simplesaml, AccountInterface $account) {
41+
$this->simplesaml = $simplesaml;
42+
$this->account = $account;
43+
}
44+
45+
/**
46+
* Redirect anonymous user to SimpleSAML auth page if IP matches redirect IPs.
47+
*
48+
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
49+
* The subscribed event.
50+
*/
51+
public function redirectToSimplesamlLogin(GetResponseEvent $event) {
52+
// If user is not anonymous, if SimpleSAML is not activated or if PHP_SAPI
53+
// is cli - don't do any redirects.
54+
if (!$this->account->isAnonymous() || !$this->simplesaml->isActivated() || PHP_SAPI === 'cli') {
55+
return;
56+
}
57+
58+
$request = $event->getRequest();
59+
$config = \Drupal::config('os2web_simplesaml.settings');
60+
61+
// Only redirect if we are on redirect triggering page.
62+
if ($request->getRequestUri() == $config->get('redirect_trigger_path')) {
63+
// Killing cache for redirect triggering page.
64+
\Drupal::service('page_cache_kill_switch')->trigger();
65+
66+
// Check has been already performed, wait for the cookies to expire.
67+
if ($request->cookies->has('os2web_simplesaml_redirect_to_saml')) {
68+
return;
69+
}
70+
71+
$simplesamlRedirect = FALSE;
72+
$remoteIp = $request->getClientIp();
73+
74+
$config = \Drupal::config('os2web_simplesaml.settings');
75+
$redirectIps = $config->get('redirect_ips');
76+
77+
if (empty($redirectIps)) {
78+
// No redirect IPs set, then redirect for all IPs.
79+
$simplesamlRedirect = TRUE;
80+
}
81+
else {
82+
$customIps = explode(',', $redirectIps);
83+
84+
// If the client request is from one of the IP's, login using
85+
// SimpleSAMLphp; otherwise use nemid login.
86+
//
87+
// Check performed on parts of the ip address.
88+
// This makes it possible to add only the beginning of the IP range.
89+
// F.ex. 192.168 will allow all ip addresses including 192.168 as part
90+
// of the it.
91+
foreach ($customIps as $customIp) {
92+
if (strpos($remoteIp, $customIp) !== FALSE) {
93+
$simplesamlRedirect = TRUE;
94+
break;
95+
}
96+
}
97+
}
98+
99+
// Getting cookies time to live (TTL).
100+
$cookies_ttl = $config->get('redirect_cookies_ttl');
101+
102+
if ($simplesamlRedirect) {
103+
// Get the path (default: '/saml_login') from the
104+
// 'simplesamlphp_auth.saml_login' route.
105+
$saml_login_path = Url::fromRoute('simplesamlphp_auth.saml_login')->toString();
106+
107+
// Set 5min cookies to prevent further checks and looping redirect.
108+
setrawcookie('os2web_simplesaml_redirect_to_saml', 'TRUE', time() + $cookies_ttl);
109+
110+
// Redirect directly to the external IdP.
111+
$response = new RedirectResponse($saml_login_path, RedirectResponse::HTTP_FOUND);
112+
$event->setResponse($response);
113+
$event->stopPropagation();
114+
}
115+
else {
116+
// Set 5min cookies to prevent further checks and looping redirect.
117+
setrawcookie('os2web_simplesaml_redirect_to_saml', 'FALSE', time() + $cookies_ttl);
118+
}
119+
}
120+
}
121+
122+
/**
123+
* {@inheritdoc}
124+
*/
125+
public static function getSubscribedEvents() {
126+
$events[KernelEvents::REQUEST][] = ['redirectToSimplesamlLogin'];
127+
return $events;
128+
}
129+
130+
}

0 commit comments

Comments
 (0)