Skip to content
This repository was archived by the owner on Sep 19, 2022. It is now read-only.

Commit b31976a

Browse files
author
Dominik Frantisek Bucik
committed
feat: 🎸 AuthProcFilter PerunUser - identify user from Perun
1 parent 6c6110f commit b31976a

File tree

8 files changed

+273
-1
lines changed

8 files changed

+273
-1
lines changed

config-templates/processFilterConfigurations-example.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,5 +179,28 @@ Configuration options:
179179
'fail_on_nonexisting_keys' => 'true',
180180
'default_value' => null,
181181
],
182+
```
183+
184+
## PerunUser
182185

186+
Filter tries to identify the Perun user. It uses the combination of user identifier and IdP identifier to find the user (or to be more precise, the user identity and associated user account). If it can, the user object is set to `$request` parameter into `$request[PerunConstants::PERUN][PerunConstants::USER]`. Otherwise, user is forwarded to configured registration.
187+
188+
Configuration options:
189+
* `interface`: specifies what interface of Perun should be used to fetch data. See class `SimpleSAML\Module\perun\PerunAdapter` for more details.
190+
* `uid_attrs`: list of attributes that contain user identifiers to be used for identification. The order of the items in the list represents the priority.
191+
* `idp_id_attr`: name of the attribute (from `$request['Attributes']` array), which holds EntityID of the identity provider that has performed the authentication.
192+
* `register_url`: URL to which the user will be forwarded for registration. Leave empty to use the Perun registrar.
193+
* `callback_parameter_name`: name of the parameter wich will hold callback URL, where the user should be redirected after the registration on URL configured in the `register_url` property.
194+
* `perun_register_url`: the complete URL (including vo and group) to which user will be redirected, if `register_url` has not been configured. Parameters targetnew, targetexisting and targetextended will be set to callback URL to continue after the registration is completed.
195+
196+
```php
197+
2 => [
198+
'class' => 'perun:PerunUser',
199+
'interface' => 'LDAP',
200+
'uid_attrs' => ['eduPersonUniqueId', 'eduPersonPrincipalName'],
201+
'idp_id_attr' => 'authenticating_idp',
202+
'register_url' => 'https://signup.cesnet.cz/',
203+
'callback_parameter_name' => 'callback',
204+
'perun_register_url' => 'https://signup.perun.cesnet.cz/fed/registrar/?vo=cesnet'
205+
],
183206
```

dictionaries/perun.definition.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,13 @@
9494
"unauthorized-access_redirect_to_registration": {
9595
"en": "Now you will be redirected to registration to Perun system.",
9696
"cs": "Nyní budete přesměrování na registraci do systému Perun."
97+
},
98+
"register_text": {
99+
"en": "Oops! It seems you have tried to access service via Perun AAI, but yo do not have an account. Let's fix that!",
100+
"cs": "Ups! Zdá se, že jste se pokusil(a) přihlásit ke službě skrz Perun AAI, no nemáte uživatelský účet. Pojďme to napravit!"
101+
},
102+
"register_button": {
103+
"en": "Proceed to register for an account",
104+
"cs": "Pokračovat na registraci ůčtu"
97105
}
98106
}

lib/Adapter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static function getInstance($interface)
4848

4949
/**
5050
* @param string $idpEntityId entity id of hosted idp used as extSourceName
51-
* @param string $uids list of user identifiers received from remote idp used as userExtSourceLogin
51+
* @param array $uids list of user identifiers received from remote idp used as userExtSourceLogin
5252
*
5353
* @return User or null if not exists
5454
*/

lib/Auth/Process/PerunUser.php

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\perun\Auth\Process;
6+
7+
use SimpleSAML\Auth\ProcessingFilter;
8+
use SimpleSAML\Auth\State;
9+
use SimpleSAML\Configuration;
10+
use SimpleSAML\Error\Exception;
11+
use SimpleSAML\Logger;
12+
use SimpleSAML\Module;
13+
use SimpleSAML\Module\perun\Adapter;
14+
use SimpleSAML\Module\perun\model\User;
15+
use SimpleSAML\Module\perun\PerunConstants;
16+
use SimpleSAML\Utils\HTTP;
17+
18+
/**
19+
* Class tries to find user in Perun using the extLogin and extSourceName (in case of RPC adapter).
20+
*
21+
* If the user cannot be found, it redirects user to the registration URL.
22+
*/
23+
class PerunUser extends ProcessingFilter
24+
{
25+
public const STAGE = 'perun:PerunUser';
26+
public const DEBUG_PREFIX = self::STAGE . ' - ';
27+
28+
public const CALLBACK = 'perun/perun_user_callback.php';
29+
public const REDIRECT = 'perun/perun_user.php';
30+
public const TEMPLATE = 'perun:perun-user-tpl.php';
31+
32+
public const PARAM_REGISTRATION_URL = 'registrationUrl';
33+
public const PARAM_STATE_ID = PerunConstants::STATE_ID;
34+
35+
public const INTERFACE = 'interface';
36+
public const UID_ATTRS = 'uid_attrs';
37+
public const IDP_ID_ATTR = 'idp_id_attr';
38+
public const REGISTER_URL = 'register_url';
39+
public const CALLBACK_PARAMETER_NAME = 'callback_parameter_name';
40+
public const PERUN_REGISTER_URL = 'perun_register_url';
41+
42+
private $adapter;
43+
private $idpEntityIdAttr;
44+
private $userIdAttrs;
45+
private $registerUrl;
46+
private $callbackParameterName;
47+
private $perunRegisterUrl;
48+
private $config;
49+
private $filterConfig;
50+
51+
public function __construct($config, $reserved)
52+
{
53+
parent::__construct($config, $reserved);
54+
$this->config = $config;
55+
$this->filterConfig = Configuration::loadFromArray($config);
56+
57+
$interface = $this->filterConfig->getString(self::INTERFACE, Adapter::RPC);
58+
59+
$this->adapter = Adapter::getInstance($interface);
60+
$this->userIdAttrs = $this->filterConfig->getArray(self::UID_ATTRS, []);
61+
if (empty($this->userIdAttrs)) {
62+
throw new Exception(
63+
self::DEBUG_PREFIX . 'Invalid configuration: no attributes configured for ' . 'extracting UID. Use option \'' . self::UID_ATTRS . '\' to configure list of attributes, ' . 'that should be considered as IDs for a user'
64+
);
65+
}
66+
$this->idpEntityIdAttr = $this->filterConfig->getString(self::IDP_ID_ATTR, null);
67+
if (empty($this->idpEntityIdAttr)) {
68+
throw new Exception(
69+
self::DEBUG_PREFIX . 'Invalid configuration: no attribute containing IDP ' . 'ID has been configured. Use option \'' . self::IDP_ID_ATTR . '\' to configure the name of the ' . 'attribute, that has been previously used in the configuration of filter \'perun:ExtractIdpEntityId\''
70+
);
71+
}
72+
$this->registerUrl = $this->filterConfig->getString(self::REGISTER_URL, null);
73+
$this->callbackParameterName = $this->filterConfig->getString(self::CALLBACK_PARAMETER_NAME, null);
74+
$this->perunRegisterUrl = $this->filterConfig->getString(self::PERUN_REGISTER_URL, null);
75+
if (empty($this->registerUrl) && empty($this->callbackParameterName) && empty($this->perunRegisterUrl)) {
76+
throw new Exception(
77+
self::DEBUG_PREFIX . 'Invalid configuration: no URL where user should register for the ' . 'account has been configured. Use option \'' . self::REGISTER_URL . '\' to configure the URL and ' . 'option \'' . self::CALLBACK_PARAMETER_NAME . '\' to configure name of the callback parameter.
78+
. If you wish to use the Perun registrar, use the option \'' . self::PERUN_REGISTER_URL . '\'.'
79+
);
80+
}
81+
}
82+
83+
public function process(&$request)
84+
{
85+
assert(is_array($request));
86+
assert(array_key_exists(PerunConstants::ATTRIBUTES, $request));
87+
88+
$uids = [];
89+
foreach ($this->userIdAttrs as $uidAttr) {
90+
if (isset($request[PerunConstants::ATTRIBUTES][$uidAttr][0])) {
91+
$uids[] = $request[PerunConstants::ATTRIBUTES][$uidAttr][0];
92+
}
93+
}
94+
if (empty($uids)) {
95+
throw new Exception(self::DEBUG_PREFIX . 'missing at least one of mandatory attributes [' . implode(
96+
', ',
97+
$this->userIdAttrs
98+
) . '] in request.');
99+
}
100+
101+
if (!empty($request[PerunConstants::ATTRIBUTES][$this->idpEntityIdAttr][0])) {
102+
$idpEntityId = $request[PerunConstants::ATTRIBUTES][$this->idpEntityIdAttr][0];
103+
} else {
104+
throw new Exception(
105+
self::DEBUG_PREFIX . 'Cannot find entityID of source IDP. Did you properly configure ' . ExtractRequestAttribute::STAGE . ' filter before this filter in the processing chain?'
106+
);
107+
}
108+
109+
$user = $this->adapter->getPerunUser($idpEntityId, $uids);
110+
111+
if (!empty($user)) {
112+
Logger::debug(self::DEBUG_PREFIX . 'User identified, setting Perun user into request.');
113+
$this->processUser($request, $user, $uids);
114+
115+
return;
116+
}
117+
Logger::debug(self::DEBUG_PREFIX . 'User not identified, redirecting to registration.');
118+
$this->register($request, $uids);
119+
}
120+
121+
private function processUser(array &$request, User $user, array $uids): void
122+
{
123+
if (!isset($request[PerunConstants::PERUN])) {
124+
$request[PerunConstants::PERUN] = [];
125+
}
126+
127+
$request[PerunConstants::PERUN][PerunConstants::USER] = $user;
128+
129+
Logger::info(
130+
self::DEBUG_PREFIX . 'Perun user with identity/ies: ' . implode(',', $uids)
131+
. ' has been found. Setting user ' . $user->getName() . ' with id: ' . $user->getId() . ' to the request.'
132+
);
133+
}
134+
135+
private function register(array &$request, array $uids): void
136+
{
137+
$request[PerunConstants::CONTINUE_FILTER_CONFIG] = $this->config;
138+
$stateId = State::saveState($request, self::STAGE);
139+
$callback = Module::getModuleURL(self::CALLBACK, [
140+
self::PARAM_STATE_ID => $stateId,
141+
]);
142+
Logger::debug(self::DEBUG_PREFIX . 'Produced callback URL \'' . $callback . '\'');
143+
144+
if (!empty($this->registerUrl) && !empty($this->callbackParameterName)) {
145+
Logger::debug(
146+
self::DEBUG_PREFIX . 'Redirecting to \'' . $this->registerUrl . ', callback parameter \''
147+
. $this->callbackParameterName . '\' with value \'' . $callback . '\''
148+
);
149+
HTTP::redirectTrustedURL($this->registerUrl, [
150+
$this->callbackParameterName => $callback,
151+
]);
152+
} elseif (!empty($this->perunRegisterUrl)) {
153+
$params[PerunConstants::TARGET_NEW] = $callback;
154+
$params[PerunConstants::TARGET_EXISTING] = $callback;
155+
$params[PerunConstants::TARGET_EXTENDED] = $callback;
156+
157+
$url = Module::getModuleURL(self::REDIRECT);
158+
$registrationUrl = HTTP::addURLParameters($this->perunRegisterUrl, $params);
159+
Logger::debug(
160+
self::DEBUG_PREFIX . 'Redirecting to \'' . self::REDIRECT . ', registration URL \''
161+
. $registrationUrl . '\''
162+
);
163+
HTTP::redirectTrustedURL($url, [
164+
self::PARAM_REGISTRATION_URL => $registrationUrl,
165+
]);
166+
} else {
167+
throw new Exception(self::DEBUG_PREFIX . 'No configuration for registration enabled. Cannot proceed');
168+
}
169+
Logger::info(
170+
self::DEBUG_PREFIX . 'Perun user with identity/ies: ' . implode(',', $uids) .
171+
' has not been found. Redirecting to registration.'
172+
);
173+
}
174+
}

lib/PerunConstants.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace SimpleSAML\Module\perun;
46

57
class PerunConstants
@@ -12,4 +14,14 @@ class PerunConstants
1214

1315
public const TARGET_EXTENDED = 'targetextended';
1416

17+
public const PERUN = 'perun';
18+
19+
public const USER = 'user';
20+
21+
public const STATE_ID = 'stateId';
22+
23+
public const CONFIG = 'config';
24+
25+
public const CONTINUE_FILTER_CONFIG = 'continueFilterConfig';
26+
1527
}

templates/perun-user-tpl.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
3+
use SimpleSAML\Module\perun\Auth\Process\PerunUser;
4+
5+
$this->includeAtTemplateBase('includes/header.php');
6+
7+
?>
8+
9+
<div class="row">
10+
<div class="offset-1 col-10 offset-sm-1 col-sm-10 offset-md-2 col-md-8 offset-lg-3 col-lg-6 offset-xl-3 col-xl-6">
11+
<p><?php echo $this->t('{perun:perun:register_text}'); ?></p>
12+
<a class="btn btn-block" href="<?php echo $this->data[PerunUser::PARAM_REGISTRATION_URL]; ?>">
13+
<?php echo $this->t('{perun:perun:register_button}'); ?>
14+
</a>
15+
</div>
16+
</div>
17+
18+
19+
<?php
20+
21+
$this->includeAtTemplateBase('includes/footer.php');

www/perun_user.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use SimpleSAML\Configuration;
6+
use SimpleSAML\Module\perun\Auth\Process\PerunUser;
7+
use SimpleSAML\XHTML\Template;
8+
9+
$config = Configuration::getInstance();
10+
$t = new Template($config, PerunUser::TEMPLATE);
11+
$t->data[PerunUser::PARAM_REGISTRATION_URL] = $_REQUEST[PerunUser::PARAM_REGISTRATION_URL];
12+
13+
$t->show();

www/perun_user_callback.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use SimpleSAML\Auth\ProcessingChain;
6+
use SimpleSAML\Auth\State;
7+
use SimpleSAML\Error\BadRequest;
8+
use SimpleSAML\Module\perun\Auth\Process\PerunUser;
9+
use SimpleSAML\Module\perun\PerunConstants;
10+
11+
if (empty($_REQUEST[PerunUser::PARAM_STATE_ID])) {
12+
throw new BadRequest('Missing required \'' . PerunUser::PARAM_STATE_ID . '\' query parameter.');
13+
}
14+
$state = State::loadState($_REQUEST[PerunUser::PARAM_STATE_ID], PerunUser::STAGE);
15+
16+
$filterConfig = $state[PerunConstants::CONTINUE_FILTER_CONFIG];
17+
$perunUser = new PerunUser($filterConfig, null);
18+
$perunUser->process($state);
19+
20+
// we have not been redirected, continue processing
21+
ProcessingChain::resumeProcessing($state);

0 commit comments

Comments
 (0)