├── enable
├── .gitignore
├── default-disable
├── dictionaries
├── errors.definition.json
├── errors.translation.json
├── login.definition.json
└── login.translation.json
├── composer.json
├── README.md
├── templates
└── login.php
├── www
└── login.php
└── lib
└── Auth
└── Source
└── authtfaga.php
/enable:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | vendor
3 |
--------------------------------------------------------------------------------
/default-disable:
--------------------------------------------------------------------------------
1 | This file indicates that the default state of this module
2 | is disabled. To enable, create a file named enable in the
3 | same directory as this file.
4 |
--------------------------------------------------------------------------------
/dictionaries/errors.definition.json:
--------------------------------------------------------------------------------
1 | {
2 | "title_WRONGOTP": {
3 | "en": "Invalid verification code!"
4 | },
5 | "descr_WRONGOTP": {
6 | "en": "The Google Authenticator based one time password validation failed. Try again, or contact the administrators!"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "niif/simplesamlphp-module-authtfaga",
3 | "description": "Two-factor authentication module for simpleSAMLphp using Google Authenticator",
4 | "type": "simplesamlphp-module",
5 | "require": {
6 | "simplesamlphp/composer-module-installer": "^1.1"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/dictionaries/errors.translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "title_WRONGOTP": {
3 | "hu": "A megadott ellenőrzőkód hibás!"
4 | },
5 | "descr_WRONGOTP": {
6 | "hu": "A Google Authenticator alapú egyszer használatos jelszó hibás! Próbáld újra, vagy vedd fel a kapcsolatot az adminisztátorral!"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Two-factor authentication module for simpleSAMLphp using Google Authenticator
2 |
3 | ## Usage
4 |
5 | Configure it by adding an entry to `config/authsources.php` such as this:
6 |
7 | ```
8 | 'authtfaga' => array(
9 | 'authtfaga:authtfaga',
10 |
11 | 'db.dsn' => 'mysql:host=db.example.com;port=3306;dbname=idpauthtfaga',
12 | 'db.username' => 'simplesaml',
13 | 'db.password' => 'bigsecret',
14 | 'mainAuthSource' => 'ldap',
15 | 'uidField' => 'uid',
16 | 'totpIssuer' => 'dev_aai_teszt_IdP'
17 | ),
18 | ```
19 |
--------------------------------------------------------------------------------
/dictionaries/login.definition.json:
--------------------------------------------------------------------------------
1 | {
2 | "otpfield": {
3 | "en": "Verificaton code"
4 | },
5 | "chooseOTP": {
6 | "en": "Would you like to use two-factor authentication based on Google Authenticator?"
7 | },
8 | "yes": {
9 | "en": "Yes"
10 | },
11 | "no": {
12 | "en": "No"
13 | },
14 | "next": {
15 | "en": "Next"
16 | },
17 | "2factor_title": {
18 | "en": "Set up two-factor authentication"
19 | },
20 | "authentication": {
21 | "en": "Authentication"
22 | },
23 | "qrcode": {
24 | "en": "Read the QR code with Google Authenticator app, and type the verification code below"
25 | },
26 | "verificationcode": {
27 | "en": "Type your verifying code"
28 | },
29 | "2factor_login": {
30 | "en": "Login with verification code."
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/dictionaries/login.translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "otpfield": {
3 | "hu": "Ellenőrző kód"
4 | },
5 | "chooseOTP": {
6 | "hu": "Szeretnéd használni a jövőben Google Authenticator-féle kétfaktoros azonosítási lehetőséget?"
7 | },
8 | "yes": {
9 | "hu": "Igen"
10 | },
11 | "no": {
12 | "hu": "Nem"
13 | },
14 | "next": {
15 | "hu": "Tovább"
16 | },
17 | "2factor_title": {
18 | "hu": "Kétfaktoros azonosítás - beállítás"
19 | },
20 | "authentication": {
21 | "hu": "Bejelentkezés"
22 | },
23 | "qrcode": {
24 | "hu": "Olvasd be a QR-kódot a mobilodon lévő Google Authenticator alkalmazással, majd add meg az ellenőrző kódot alább"
25 | },
26 | "verificationcode": {
27 | "hu": "Add meg az aktuális ellenőrző kódot"
28 | },
29 | "2factor_login": {
30 | "hu": "Bejelentkezés ellenőrző kóddal"
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/templates/login.php:
--------------------------------------------------------------------------------
1 | includeAtTemplateBase('includes/header.php');
4 | $this->data['header'] = $this->t('{authtfaga:login:authentication}');
5 | ?>
6 |
7 | data['errorcode'] !== NULL) :?>
8 |
9 |

10 |
t('{login:error_header}'); ?>
11 |
t('{authtfaga:errors:title_' . $this->data['errorcode'] . '}'); ?>
12 |
t('{authtfaga:errors:descr_' . $this->data['errorcode'] . '}'); ?>
13 |
14 |
15 |
16 |
56 |
57 | includeAtTemplateBase('includes/footer.php');
59 | ?>
60 |
--------------------------------------------------------------------------------
/www/login.php:
--------------------------------------------------------------------------------
1 | getValue($authId);
20 |
21 | // Use 2 factor authentication classvar_dump($authId);
22 | /** @noinspection PhpUnhandledExceptionInspection */
23 | /** @var sspmod_authtfaga_Auth_Source_authtfaga $gaLogin */
24 | $gaLogin = SimpleSAML_Auth_Source::getById($authId, 'sspmod_authtfaga_Auth_Source_authtfaga');
25 | if ($gaLogin === null) {
26 | /** @noinspection PhpUnhandledExceptionInspection */
27 | throw new Exception('Invalid authentication source: ' . $authId);
28 | }
29 |
30 | // Init template
31 | $template = 'authtfaga:login.php';
32 | /** @noinspection PhpUnhandledExceptionInspection */
33 | $globalConfig = SimpleSAML_Configuration::getInstance();
34 | /** @noinspection PhpParamsInspection */
35 | $t = new SimpleSAML_XHTML_Template($globalConfig, $template);
36 |
37 | $errorCode = null;
38 |
39 | //If user doesn't have session, force to use the main authentication method
40 | if (!$session->isValid($as['mainAuthSource'])) {
41 | /** @noinspection PhpUnhandledExceptionInspection */
42 | $mainLogin = SimpleSAML_Auth_Source::getById($as['mainAuthSource']);
43 | $mainLogin->initLogin(SimpleSAML\Utils\HTTP::getSelfURL());
44 | }
45 |
46 | $attributes = $session->getAuthData($as['mainAuthSource'], 'Attributes');
47 | $state['Attributes'] = $attributes;
48 |
49 | $uid = $attributes[$as['uidField']][0];
50 | $state['UserID'] = $uid;
51 | $isEnabled = $gaLogin->isEnabled2fa($uid);
52 |
53 | if (is_null($isEnabled) || isset($_GET['postSetEnable2fa'])) {
54 | //If the user has not set his preference of 2 factor authentication, redirect to settings page
55 | if (isset($_POST['setEnable2f'])) {
56 | if ($_POST['setEnable2f'] == 1) {
57 | $gaKey = $gaLogin->createSecret();
58 | $gaLogin->registerGAkey($uid, $gaKey);
59 |
60 | $gaLogin->enable2fa($uid);
61 | $t->data['todo'] = 'generateGA';
62 | $t->data['autofocus'] = 'otp';
63 | $totpIssuer = empty($as['totpIssuer']) ? 'dev_aai_teszt_IdP' : $as['totpIssuer'];
64 | $t->data['qrcode'] = $gaLogin->getQRCodeGoogleUrl($totpIssuer . ':' . $uid, $totpIssuer, $gaKey);
65 | } elseif ($_POST['setEnable2f'] == 0) {
66 | $gaLogin->disable2fa($uid);
67 | SimpleSAML_Auth_Source::completeAuth($state);
68 | }
69 | } else {
70 | $t->data['todo'] = 'choose2enable';
71 | }
72 | } elseif ($isEnabled == 1) {
73 | //Show the second factor form
74 | if (isset($_POST['otp'])) {
75 | $secret = $gaLogin->getGAkeyFromUID($uid);
76 | $loggedIn = $gaLogin->verifyCode($secret, $_POST['otp']);
77 |
78 | if ($loggedIn) {
79 | $state['saml:AuthnContextClassRef'] = $gaLogin->tfa_authencontextclassref;
80 |
81 | if (isset($state['Attributes']['userCertificate;binary'])) {
82 | unset($state['Attributes']['userCertificate;binary']);
83 | }
84 |
85 | SimpleSAML_Auth_Source::completeAuth($state);
86 | } else {
87 | $errorCode = 'WRONGOTP';
88 | $t->data['todo'] = 'loginOTP';
89 | }
90 | } else {
91 | $t->data['autofocus'] = 'otp';
92 | $t->data['todo'] = 'loginOTP';
93 | }
94 | } else {
95 |
96 | if (isset($state['Attributes']['userCertificate;binary'])) {
97 | unset($state['Attributes']['userCertificate;binary']);
98 | }
99 |
100 | // User has set up not to use 2 factor, so he is logged in
101 | SimpleSAML_Auth_Source::completeAuth($state);
102 | }
103 |
104 | $t->data['stateparams'] = array('AuthState' => $authStateId);
105 | $t->data['errorcode'] = $errorCode;
106 | $t->show();
107 | exit();
108 |
--------------------------------------------------------------------------------
/lib/Auth/Source/authtfaga.php:
--------------------------------------------------------------------------------
1 | array(
11 | * 'authtfaga:authtfaga',
12 | *
13 | * 'db.dsn' => 'mysql:host=db.example.com;port=3306;dbname=idpauthtfaga',
14 | * 'db.username' => 'simplesaml',
15 | * 'db.password' => 'bigsecret',
16 | * 'mainAuthSource' => 'ldap',
17 | * 'uidField' => 'uid'
18 | * ),
19 | *
20 | */
21 |
22 | class sspmod_authtfaga_Auth_Source_authtfaga extends SimpleSAML_Auth_Source
23 | {
24 | /**
25 | * The string used to identify our states.
26 | */
27 | const STAGEID = 'authtfaga.stage';
28 |
29 | /**
30 | * The number of characters of the OTP that is the secure token.
31 | * The rest is the user id.
32 | */
33 | const TOKENSIZE = 32;
34 |
35 | /**
36 | * The key of the AuthId field in the state.
37 | */
38 | const AUTHID = 'sspmod_authtfaga_Auth_Source_authtfaga.AuthId';
39 |
40 | /**
41 | * sstc-saml-loa-authncontext-profile-draft.odt.
42 | */
43 | const TFAAUTHNCONTEXTCLASSREF = 'urn:oasis:names:tc:SAML:2.0:post:ac:classes:nist-800-63:3';
44 |
45 | private $db_dsn;
46 | private $db_username;
47 | private $db_password;
48 | private $dbh;
49 |
50 | public $tfa_authencontextclassref;
51 |
52 | //Google Authenticator code length
53 | protected $_codeLength = 6;
54 |
55 | /**
56 | * Constructor for this authentication source.
57 | *
58 | * @param array $info Information about this authentication source.
59 | * @param array $config Configuration.
60 | */
61 | public function __construct($info, $config)
62 | {
63 | assert('is_array($info)');
64 | assert('is_array($config)');
65 |
66 | // Call the parent constructor first, as required by the interface.
67 | parent::__construct($info, $config);
68 |
69 | if (array_key_exists('db.dsn', $config)) {
70 | $this->db_dsn = $config['db.dsn'];
71 | }
72 | if (array_key_exists('db.username', $config)) {
73 | $this->db_username = $config['db.username'];
74 | }
75 | if (array_key_exists('db.password', $config)) {
76 | $this->db_password = $config['db.password'];
77 | }
78 |
79 | $this->tfa_authencontextclassref = self::TFAAUTHNCONTEXTCLASSREF;
80 | try {
81 | $this->dbh = new PDO($this->db_dsn, $this->db_username, $this->db_password);
82 | } catch (PDOException $e) {
83 | echo 'Connection failed: '.$e->getMessage();
84 | }
85 | $this->createTables();
86 | }
87 |
88 | public function authenticate(&$state)
89 | {
90 | assert('is_array($state)');
91 |
92 | // We are going to need the authId in order to retrieve this authentication source later.
93 | $state[self::AUTHID] = $this->authId;
94 |
95 | $id = SimpleSAML_Auth_State::saveState($state, self::STAGEID);
96 |
97 | $url = SimpleSAML_Module::getModuleURL('authtfaga/login.php');
98 | SimpleSAML\Utils\HTTP::redirectTrustedURL($url, array('AuthState' => $id));
99 | }
100 |
101 | private function createTables()
102 | {
103 | $q = 'CREATE TABLE IF NOT EXISTS sspga_gakeys (
104 | gakey VARCHAR (20),
105 | uid VARCHAR(60),
106 | PRIMARY KEY(gakey)
107 | );';
108 | $this->dbh->query($q);
109 | $q = 'CREATE TABLE IF NOT EXISTS sspga_status (
110 | uid VARCHAR(60),
111 | enable INT,
112 | PRIMARY KEY(uid)
113 | );';
114 | $this->dbh->query($q);
115 | }
116 |
117 | public function enable2fa($uid)
118 | {
119 | $q = "REPLACE INTO sspga_status (enable, uid) VALUES (1, '$uid')";
120 | $result = $this->dbh->query($q);
121 | if($result===false) throw new Exception('Enable TFA failed '.$q);
122 | SimpleSAML_Logger::info('authtfaga: '.$uid.' turns ON the two-factor authentication.');
123 |
124 | return true;
125 | }
126 |
127 | public function disable2fa($uid)
128 | {
129 | $q = "REPLACE INTO sspga_status (enable, uid) VALUES (0, '$uid')";
130 | $this->dbh->query($q);
131 | SimpleSAML_Logger::info('authtfaga: '.$uid.' turns OFF the two-factor authentication.');
132 |
133 | return true;
134 | }
135 |
136 | public function isEnabled2fa($uid)
137 | {
138 | $q = "SELECT * FROM sspga_status WHERE uid='$uid'";
139 | $result = $this->dbh->query($q);
140 | $row = $result->fetch();
141 | $enabled = $row['enable'];
142 |
143 | return $enabled;
144 | }
145 |
146 | public function registerGAkey($uid, $ga_id)
147 | {
148 | if (!$ga_id) {
149 | return false;
150 | }
151 |
152 | $q = 'REPLACE INTO sspga_gakeys (uid,gakey) VALUES ("'.$uid.'","'.$ga_id.'");';
153 | $this->dbh->query($q);
154 | SimpleSAML_Logger::info('authtfaga: '.$uid.' register his gakey: '.$ga_id);
155 |
156 | return true;
157 | }
158 |
159 | public function deletegakey($uid, $ga_id)
160 | {
161 | $q = 'DELETE FROM sspga_gakeys WHERE uid="'.$uid.'" AND gakey="'.$ga_id.'";';
162 | $this->dbh->query($q);
163 | SimpleSAML_Logger::info('authtfaga: '.$uid.' delete his gakey: '.$ga_id);
164 |
165 | return true;
166 | }
167 |
168 | public function getGAkeyFromUID($uid)
169 | {
170 | $q = "SELECT gakey FROM sspga_gakeys WHERE uid='$uid'";
171 | $result = $this->dbh->query($q);
172 | $row = $result->fetch();
173 |
174 | return $row['gakey'];
175 | }
176 |
177 | /**
178 | * Below this line there is PHP Class for handling Google Authenticator 2-factor authentication.
179 | *
180 | * @author Michael Kliewe
181 | * @copyright 2012 Michael Kliewe
182 | * @license http://www.opensource.org/licenses/bsd-license.php BSD License
183 | *
184 | * @link http://www.phpgangsta.de/
185 | */
186 |
187 | /**
188 | * Create new secret.
189 | * 16 characters, randomly chosen from the allowed base32 characters.
190 | *
191 | * @param int $secretLength
192 | *
193 | * @return string
194 | */
195 | public function createSecret($secretLength = 16)
196 | {
197 | $validChars = $this->_getBase32LookupTable();
198 | unset($validChars[32]);
199 |
200 | $secret = '';
201 | for ($i = 0; $i < $secretLength; ++$i) {
202 | $secret .= $validChars[array_rand($validChars)];
203 | }
204 |
205 | return $secret;
206 | }
207 |
208 | /**
209 | * Calculate the code, with given secret and point in time.
210 | *
211 | * @param string $secret
212 | * @param int|null $timeSlice
213 | *
214 | * @return string
215 | */
216 | public function getCode($secret, $timeSlice = null)
217 | {
218 | if ($timeSlice === null) {
219 | $timeSlice = floor(time() / 30);
220 | }
221 |
222 | $secretkey = $this->_base32Decode($secret);
223 |
224 | // Pack time into binary string
225 | $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
226 | // Hash it with users secret key
227 | $hm = hash_hmac('SHA1', $time, $secretkey, true);
228 | // Use last nipple of result as index/offset
229 | $offset = ord(substr($hm, -1)) & 0x0F;
230 | // grab 4 bytes of the result
231 | $hashpart = substr($hm, $offset, 4);
232 |
233 | // Unpak binary value
234 | $value = unpack('N', $hashpart);
235 | $value = $value[1];
236 | // Only 32 bits
237 | $value = $value & 0x7FFFFFFF;
238 |
239 | $modulo = pow(10, $this->_codeLength);
240 |
241 | return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
242 | }
243 |
244 | /**
245 | * Get QR-Code URL for image, from google charts.
246 | *
247 | * @param string $name
248 | * @param string $issuer
249 | * @param string $secret
250 | *
251 | * @return string
252 | */
253 | public function getQRCodeGoogleUrl($name, $issuer, $secret)
254 | {
255 | $urlencoded = rawurlencode('otpauth://totp/'.$name.'?secret='.$secret.'&issuer='.$issuer);
256 |
257 | return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.'';
258 | }
259 |
260 | /**
261 | * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now.
262 | *
263 | * @param string $secret
264 | * @param string $code
265 | * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
266 | *
267 | * @return bool
268 | */
269 | public function verifyCode($secret, $code, $discrepancy = 1)
270 | {
271 | $currentTimeSlice = floor(time() / 30);
272 |
273 | for ($i = -$discrepancy; $i <= $discrepancy; ++$i) {
274 | $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
275 | if ($calculatedCode == $code) {
276 | return true;
277 | }
278 | }
279 |
280 | return false;
281 | }
282 |
283 | /**
284 | * Set the code length, should be >=6.
285 | *
286 | * @param int $length
287 | *
288 | * @return sspmod_authtfaga_Auth_Source_authtfaga
289 | */
290 | public function setCodeLength($length)
291 | {
292 | $this->_codeLength = $length;
293 |
294 | return $this;
295 | }
296 |
297 | /**
298 | * Helper class to decode base32.
299 | *
300 | * @param $secret
301 | *
302 | * @return bool|string
303 | */
304 | protected function _base32Decode($secret)
305 | {
306 | if (empty($secret)) {
307 | return '';
308 | }
309 |
310 | $base32chars = $this->_getBase32LookupTable();
311 | $base32charsFlipped = array_flip($base32chars);
312 |
313 | $paddingCharCount = substr_count($secret, $base32chars[32]);
314 | $allowedValues = array(6, 4, 3, 1, 0);
315 | if (!in_array($paddingCharCount, $allowedValues)) {
316 | return false;
317 | }
318 | for ($i = 0; $i < 4; ++$i) {
319 | if ($paddingCharCount == $allowedValues[$i] &&
320 | substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) {
321 | return false;
322 | }
323 | }
324 | $secret = str_replace('=', '', $secret);
325 | $secret = str_split($secret);
326 | $binaryString = '';
327 | for ($i = 0; $i < count($secret); $i = $i + 8) {
328 | $x = '';
329 | if (!in_array($secret[$i], $base32chars)) {
330 | return false;
331 | }
332 | for ($j = 0; $j < 8; ++$j) {
333 | $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
334 | }
335 | $eightBits = str_split($x, 8);
336 | for ($z = 0; $z < count($eightBits); ++$z) {
337 | $binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
338 | }
339 | }
340 |
341 | return $binaryString;
342 | }
343 |
344 | /**
345 | * Helper class to encode base32.
346 | *
347 | * @param string $secret
348 | * @param bool $padding
349 | *
350 | * @return string
351 | */
352 | protected function _base32Encode($secret, $padding = true)
353 | {
354 | if (empty($secret)) {
355 | return '';
356 | }
357 |
358 | $base32chars = $this->_getBase32LookupTable();
359 |
360 | $secret = str_split($secret);
361 | $binaryString = '';
362 | for ($i = 0; $i < count($secret); ++$i) {
363 | $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
364 | }
365 | $fiveBitBinaryArray = str_split($binaryString, 5);
366 | $base32 = '';
367 | $i = 0;
368 | while ($i < count($fiveBitBinaryArray)) {
369 | $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
370 | ++$i;
371 | }
372 | if ($padding && ($x = strlen($binaryString) % 40) != 0) {
373 | if ($x == 8) {
374 | $base32 .= str_repeat($base32chars[32], 6);
375 | } elseif ($x == 16) {
376 | $base32 .= str_repeat($base32chars[32], 4);
377 | } elseif ($x == 24) {
378 | $base32 .= str_repeat($base32chars[32], 3);
379 | } elseif ($x == 32) {
380 | $base32 .= $base32chars[32];
381 | }
382 | }
383 |
384 | return $base32;
385 | }
386 |
387 | /**
388 | * Get array with all 32 characters for decoding from/encoding to base32.
389 | *
390 | * @return array
391 | */
392 | protected function _getBase32LookupTable()
393 | {
394 | return array(
395 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
396 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
397 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
398 | 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
399 | '=', // padding char
400 | );
401 | }
402 | }
403 |
--------------------------------------------------------------------------------