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

Commit 08e1694

Browse files
Merge pull request #10 from pajavyskocil/CSC_MFA
Added process filter for CSCMfa
2 parents ce00d87 + 5f1773e commit 08e1694

File tree

5 files changed

+301
-1
lines changed

5 files changed

+301
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
All notable changes to this project will be documented in this file.
33

44
## [Unreleased]
5+
#### Added
6+
- Added process filter for MFA using CSC MFA OIDC server
7+
58
#### Changed
69
- Using of short array syntax ([] instead of array())
710
- Using imports instead of qualified names

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"require": {
1919
"simplesamlphp/composer-module-installer": "~1.0",
2020
"simplesamlphp/simplesamlphp": "~1.17",
21-
"cesnet/simplesamlphp-module-perun": "~3.0"
21+
"cesnet/simplesamlphp-module-perun": "~3.0",
22+
"ext-json": "*",
23+
"ext-curl": "*"
2224
}
2325
}

config-templates/module_elixir.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/**
4+
* The config template for module ELIXIR
5+
*/
6+
$config = array(
7+
8+
/**
9+
* The clientId from CSC_MFA server
10+
*/
11+
'clientId' => '',
12+
13+
/**
14+
* The clientSecret from CSC_MFA server
15+
*/
16+
'clientSecret' => '',
17+
18+
/**
19+
* List of requested scopes
20+
*/
21+
'requestedScopes' => [],
22+
23+
/**
24+
* The openid configuration url of CSC_MFA server
25+
*/
26+
'openidConfigurationUrl' => ''
27+
28+
);

lib/Auth/Process/CSCMfa.php

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
namespace SimpleSAML\Module\elixir\Auth\Process;
3+
4+
use SimpleSAML\Auth\ProcessingFilter;
5+
use SimpleSAML\Auth\State;
6+
use SimpleSAML\Configuration;
7+
use SimpleSAML\Error\Exception;
8+
use SimpleSAML\Logger;
9+
use SimpleSAML\Module;
10+
11+
class CSCMfa extends ProcessingFilter
12+
{
13+
const MFA_IDENTIFIER = 'https://refeds.org/profile/mfa';
14+
const CONFIG_FILE_NAME = 'module_elixir.php';
15+
16+
const CLIENT_ID = 'clientId';
17+
const CLIENT_SECRET = 'clientSecret';
18+
const REQUESTED_SCOPES = 'requestedScopes';
19+
const OPENID_CONFIGURATION_URL = 'openidConfigurationUrl';
20+
const AUTHORIZATION_ENDPOINT = 'authorization_endpoint';
21+
const TOKEN_ENDPOINT = 'token_endpoint';
22+
const USERINFO_ENDPOINT = 'userinfo_endpoint';
23+
24+
private $clientId;
25+
private $requestedScopes;
26+
private $authorizationEndpoint;
27+
private $redirectUri;
28+
29+
public function __construct($config, $reserved)
30+
{
31+
assert('is_array($config)');
32+
parent::__construct($config, $reserved);
33+
34+
$conf = Configuration::getConfig(self::CONFIG_FILE_NAME);
35+
36+
$this->clientId = $conf->getString(self::CLIENT_ID, '');
37+
$this->requestedScopes = $conf->getArray(self::REQUESTED_SCOPES, []);
38+
$openidConfigurationUrl =$conf->getString(self::OPENID_CONFIGURATION_URL, '');
39+
40+
if (empty($this->clientId)) {
41+
throw new Exception(
42+
'elixir:CSCMfa: missing mandatory configuration option "' . self::CLIENT_ID .
43+
'" in configuration file "' . self::CONFIG_FILE_NAME . '".'
44+
);
45+
}
46+
47+
if (empty($this->requestedScopes)) {
48+
throw new Exception(
49+
'elixir:CSCMfa: missing mandatory configuration option "' . self::REQUESTED_SCOPES .
50+
'" in configuration file "' . self::CONFIG_FILE_NAME . '".'
51+
);
52+
}
53+
54+
if (empty($openidConfigurationUrl)) {
55+
throw new Exception(
56+
'elixir:CSCMfa: missing mandatory configuration option "' . self::AUTHORIZATION_ENDPOINT .
57+
'" in configuration file "' . self::CONFIG_FILE_NAME . '".'
58+
);
59+
}
60+
61+
$metadata = json_decode(file_get_contents($openidConfigurationUrl), true);
62+
$this->authorizationEndpoint = $metadata[self::AUTHORIZATION_ENDPOINT];
63+
64+
$this->redirectUri = Module::getModuleURL('elixir').'/CSCMfaContinue.php';
65+
66+
}
67+
68+
69+
public function process(&$request)
70+
{
71+
assert('is_array($request)');
72+
73+
$requestedAuthnContextClassRef = array();
74+
75+
if (isset($request['saml:RequestedAuthnContext']['AuthnContextClassRef'])) {
76+
$requestedAuthnContextClassRef = $request['saml:RequestedAuthnContext']['AuthnContextClassRef'][0];
77+
if (! is_array($requestedAuthnContextClassRef)) {
78+
$requestedAuthnContextClassRef = array($requestedAuthnContextClassRef);
79+
}
80+
}
81+
82+
if (!in_array(self::MFA_IDENTIFIER, $requestedAuthnContextClassRef)) {
83+
# Everything is OK, SP didn't requested MFA
84+
Logger::debug('Multi factor authentication is not required');
85+
return;
86+
}
87+
88+
# Check if IdP did MFA
89+
$authContextClassRef = array();
90+
if (isset($request['saml:sp:AuthnContext'])) {
91+
$authContextClassRef = $request['saml:sp:AuthnContext'];
92+
if (!is_array($authContextClassRef)) {
93+
$authContextClassRef = array($authContextClassRef);
94+
}
95+
}
96+
if (in_array(self::MFA_IDENTIFIER, $authContextClassRef)) {
97+
# MFA was performed on IdP
98+
Logger::debug('Multi factor authentication was performed on Identity provider side');
99+
return;
100+
}
101+
102+
Logger::debug('Multi factor authentication wasn\'t performed and will be performed on CSC side.');
103+
104+
$stateId = State::saveState($request, 'elixir:CSCMfa', true);
105+
106+
if (!isset($request['Attributes']['eduPersonUniqueId'][0])) {
107+
throw new Exception(
108+
'elixir:CSCMfa: missing required attribute "eduPersonUniqueId" in request'
109+
);
110+
}
111+
$elixirId = $request['Attributes']['eduPersonUniqueId'][0];
112+
113+
# Prepare claims
114+
$claims = [
115+
'id_token' => [
116+
'sub' => [
117+
'value' => $stateId
118+
],
119+
'otp_key' => [
120+
'value' => $elixirId
121+
]
122+
]
123+
];
124+
125+
if (isset( $request['Attributes']['mobile'][0])) {
126+
$phoneNumber = $request['Attributes']['mobile'][0];
127+
$claims['id_token']['mobile']['value'] = $phoneNumber;
128+
}
129+
130+
# Prepare params
131+
$params = [
132+
'response_type' => 'code',
133+
'scope' => implode(' ', $this->requestedScopes),
134+
'client_id' => $this->clientId,
135+
'redirect_uri' => $this->redirectUri,
136+
'state' => $stateId,
137+
'claims' => json_encode($claims),
138+
];
139+
140+
$mfa_url = $this->authorizationEndpoint . '?' . http_build_query($params);
141+
142+
Header('Location: ' . $mfa_url);
143+
exit();
144+
145+
}
146+
}
147+
148+
?>

www/CSCMfaContinue.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
use SimpleSAML\Auth\State;
4+
use SimpleSAML\Configuration;
5+
use SimpleSAML\Error\Exception;
6+
use SimpleSAML\Logger;
7+
use SimpleSAML\Module;
8+
use SimpleSAML\Module\elixir\Auth\Process\CSCMfa;
9+
10+
11+
$mfaTokenUrl = null;
12+
$mfaUserInfoUrl = null;
13+
14+
$conf = Configuration::getConfig(CSCMfa::CONFIG_FILE_NAME);
15+
16+
$clientId = $conf->getString(CSCMfa::CLIENT_ID, '');
17+
if (empty($clientId)) {
18+
throw new Exception(
19+
'elixir:CSCMfa_continue: missing mandatory configuration option "' . CSCMfa::CLIENT_ID .
20+
'" in configuration file "' . CSCMfa::CONFIG_FILE_NAME . '".' );
21+
}
22+
23+
$clientSecret = $conf->getString(CSCMfa::CLIENT_SECRET, '');
24+
if (empty($clientSecret)) {
25+
throw new Exception(
26+
'elixir:CSCMfa_continue: missing mandatory configuration option "' . CSCMfa::CLIENT_SECRET .
27+
'" in configuration file "' . CSCMfa::CONFIG_FILE_NAME . '".' );
28+
}
29+
30+
$openidConfigurationUrl = $conf->getString(CSCMfa::OPENID_CONFIGURATION_URL, '');
31+
if (empty($openidConfigurationUrl)) {
32+
throw new Exception(
33+
'elixir:CSCMfa_continue: missing mandatory configuration option "' . CSCMfa::TOKEN_ENDPOINT .
34+
'" in configuration file "' . CSCMfa::CONFIG_FILE_NAME . '".' );
35+
}
36+
37+
$metadata = json_decode(file_get_contents($openidConfigurationUrl), true);
38+
39+
if (isset($metadata[CSCMfa::TOKEN_ENDPOINT])) {
40+
$mfaTokenUrl = $metadata[CSCMfa::TOKEN_ENDPOINT];
41+
}
42+
43+
if (isset($metadata[CSCMfa::USERINFO_ENDPOINT])) {
44+
$mfaUserInfoUrl = $metadata[CSCMfa::USERINFO_ENDPOINT];
45+
}
46+
47+
if ($mfaTokenUrl === null || $mfaUserInfoUrl === null) {
48+
throw new Exception(
49+
'elixir:CSCMfa_continue: Problem to get ' . CSCMfa::TOKEN_ENDPOINT . ' or ' .
50+
CSCMfa::USERINFO_ENDPOINT . ' from Openid configuration.' );
51+
}
52+
53+
$redirectUri = Module::getModuleURL('elixir') . '/CSCMfa_continue.php';
54+
55+
if (!isset($_GET['code'], $_GET['state'] )) {
56+
throw new Exception(
57+
'elixir:CSCMfa_continue: One of following required params: "code", "state" is missing.');
58+
}
59+
60+
$code = $_GET['code'];
61+
$stateId = $_GET['state'];
62+
63+
$state = State::loadState($stateId, 'elixir:CSCMfa');
64+
65+
# Prepare params for token endpoint
66+
$params = [
67+
'code' => $code,
68+
'grant_type' => 'authorization_code',
69+
'redirect_uri' => $redirectUri,
70+
'client_id' => $clientId,
71+
'client_secret' => $clientSecret,
72+
'nonce' => time(),
73+
];
74+
75+
# Request to token endpoint
76+
$ch = curl_init();
77+
curl_setopt($ch, CURLOPT_URL, $mfaTokenUrl);
78+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
79+
curl_setopt($ch, CURLOPT_POST, true);
80+
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
81+
82+
if (($response = curl_exec($ch)) === false) {
83+
throw new \Exception("Request to token endpoint wasn't successful : " . curl_error($ch));
84+
}
85+
$response = json_decode($response, true);
86+
87+
$accessToken = null;
88+
$idToken = null;
89+
if (isset($response['access_token'])) {
90+
$accessToken = $response['access_token'];
91+
}
92+
93+
if (isset($response['id_token'])) {
94+
$idToken = $response['id_token'];
95+
}
96+
97+
$params = array(
98+
'access_token' => $accessToken,
99+
);
100+
101+
# Request to userinfo endpoint
102+
$ch = curl_init();
103+
curl_setopt($ch, CURLOPT_URL, $mfaUserInfoUrl);
104+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
105+
curl_setopt($ch, CURLOPT_POST, true);
106+
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
107+
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $accessToken));
108+
109+
if (($response = curl_exec($ch)) === false) {
110+
throw new \Exception("Request to token endpoint wasn't successful : " . curl_error($ch));
111+
}
112+
113+
curl_close($ch);
114+
115+
$state['saml:sp:AuthnContext'] = CSCMfa::MFA_IDENTIFIER;
116+
117+
SimpleSAML\Auth\ProcessingChain::resumeProcessing($state);
118+
119+
?>

0 commit comments

Comments
 (0)