Completed
Push — master ( a1987d...ef66da )
by Terrence
21:32 queued 06:26
created

TwoFactor::printDuoPage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 65
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 21
nc 2
nop 0
dl 0
loc 65
ccs 0
cts 52
cp 0
crap 6
rs 9.584
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace CILogon\Service;
4
5
use CILogon\Service\Util;
6
use CILogon\Service\Content;
7
use CILogon\Service\DuoConfig;
8
use Duo\Web;
9
use Endroid\QrCode\QrCode;
10
use PHPGangsta_GoogleAuthenticator;
11
12
/**
13
 * TwoFactor
14
 *
15
 * This class contains a bunch of static (class) methods
16
 * for handling two-factor authentication. Information about which
17
 * methods are registered and/or enabled is stored in the datastore.
18
 * However, to speed access to the two-factor information, the session
19
 * variable 'twofactor' contains the current state. To populate the
20
 * 'twofactor' session variable, call the read() method. This is done
21
 * (automatically) by the 'getuser' and 'getopeniduser' servlets when
22
 * the user logs on. If changes are made to the 'twofactor' session
23
 * variable, call the write() method to save the new state to the
24
 * datastore.
25
 */
26
class TwoFactor
27
{
28
    /**
29
     * read
30
     *
31
     * This method reads the two-factor information from the database
32
     * for the current 'uid' session variable. The result is stored in
33
     * the 'twofactor' session variable. If the 'uid' session variable
34
     * is not set, or if there is a problem reading from the database,
35
     * false is returned. Note that this function isn't really needed
36
     * since the 'twofactor' session variable is set when the database
37
     * servlet function 'getUser' is called.
38
     *
39
     * @return bool True if read two-factor info from database and saved
40
     *         in 'twofactor' session variable. False otherwise.
41
     */
42
    public static function read()
43
    {
44
        $retval = false;  // Assume read failed
45
        $uid = Util::getSessionVar('uid');
46
        if (strlen($uid) > 0) {
47
            $dbs = new DBService();
48
            $retval = $dbs->getTwoFactorInfo($uid);
49
            if ($retval) {
50
                Util::setSessionVar('twofactor', $dbs->two_factor);
51
            }
52
        }
53
        return $retval;
54
    }
55
56
    /**
57
     * write
58
     *
59
     * This method writes the 'twofactor' session variable to the
60
     * database for the current 'uid' session variable. If the 'uid'
61
     * session variable is not set, or if there is a problem writing
62
     * to the database, false is returned. Note that this simply saves
63
     * two-factor info to the database. You need to use other methods
64
     * to maintain/update the 'twofactor' session variable.
65
     *
66
     * @return bool True if successfully wrote 'twofactor' session
67
     *         variable to database. False otherwise.
68
     */
69
    public static function write()
70
    {
71
        $retval = false; // Assume write failed
72
        $uid = Util::getSessionVar('uid');
73
        if (strlen($uid) > 0) {
74
            $dbs = new DBService();
75
            $retval = $dbs->setTwoFactorInfo(
76
                $uid,
77
                Util::getSessionVar('twofactor')
78
            );
79
        }
80
        return $retval;
81
    }
82
83
    /**
84
     * setSessVar
85
     *
86
     * This method takes in a 'key' and a 'val' to set 'key=val' in the
87
     * 'twofactor' session variable. If 'val' is empty, then any
88
     * existing 'key=...' entry is deleted. (Thus there are no null
89
     * entries in 'twofactor' such as 'duo='.) The final result is
90
     * saved back to the 'twofactor' session variable.
91
     *
92
     * @param string $key The key in 'key=val' in the 'twofactor' session
93
     *        variable.
94
     * @param string $val The val in 'key=val' in the 'twofactor' session
95
     *        variable. If 'val' is the emtpy string, then remove any
96
     *        existing 'key=...' entries.
97
     */
98
    private static function setSessVar($key, $val)
99
    {
100
        $twofactor = Util::getSessionVar('twofactor');
101
        // Check if $key=... is present in 'twofactor' session variable
102
        if (preg_match("/$key=([^,]*)/", $twofactor, $match)) {
103
            $oldval = $match[1]; // Keep the old value
104
            if (strlen($val) > 0) { // If new val is not empty, set key=val
105
                $twofactor = preg_replace(
106
                    "/$key=$oldval/",
107
                    "$key=$val",
108
                    $twofactor
109
                );
110
            } else { // New val is empty, delete key=oldval (plus commas)
111
                $twofactor = preg_replace("/$key=$oldval,?/", '', $twofactor);
112
                // Remove trailing comma if deleted last key=oldval pair
113
                $twofactor = preg_replace('/,$/', '', $twofactor);
114
            }
115
        } else { // $key does not exist in 'twofactor' session variable
116
            if (strlen($val) > 0) { // Make sure $val is not empty
117
                if (strlen($twofactor) > 0) { // Append comma if other info
118
                    $twofactor .= ',';
119
                }
120
                $twofactor .= "$key=$val";
121
            }
122
        }
123
        Util::setSessionVar('twofactor', $twofactor);
124
    }
125
126
    /**
127
     * getSessVar
128
     *
129
     * This method looks in the 'twofactor' session variable for a
130
     * 'key=val' pair and returns 'val'. If no such pair is found, then
131
     * empty string is returned.
132
     *
133
     * @param string $key The key to look for in the 'twofactor' session
134
     *        variable.
135
     * @return string The val for 'key=val' in the 'twofactor' session
136
     *         variable, or empty string if no such 'key' found.
137
     */
138
    private static function getSessVar($key)
139
    {
140
        $retval = '';
141
        $twofactor = Util::getSessionVar('twofactor');
142
        if (preg_match("/$key=([^,]*)/", $twofactor, $match)) {
143
            $retval = $match[1];
144
        }
145
        return $retval;
146
    }
147
148
    /**
149
     * setRegister
150
     *
151
     * This method sets '$tftype=$key' in the 'twofactor' session
152
     * variable. If $key is empty, it UNverifies the $tftype and then
153
     * deletes the current '$tftype=...' (if any) from the 'twofactor'
154
     * session  variable. Also, if $key is empty, check if $tftype is
155
     * currently 'enabled'. If so, set 'enabled' to 'none'.
156
     *
157
     * @param string $tftype: The two-factor type to set, e.g., 'ga' for
158
     *        Google Authenticator.
159
     * @param string $key The secret registration key to set for the
160
     *        two-factor type. Empty string implies unregister
161
     *        the $tftype.
162
     */
163
    public static function setRegister($tftype, $key)
164
    {
165
        // When deleting the key for tftype, also UNverify and disable.
166
        if (strlen($key) == 0) {
167
            self::setVerified($tftype, false);
168
            if (self::isEnabled($tftype)) {
169
                self::setDisabled($tftype, false);
0 ignored issues
show
Unused Code introduced by
The call to CILogon\Service\TwoFactor::setDisabled() has too many arguments starting with $tftype. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

169
                self::/** @scrutinizer ignore-call */ 
170
                      setDisabled($tftype, false);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
170
            }
171
        }
172
        self::setSessVar($tftype, $key);
173
    }
174
175
    /**
176
     * getRegister
177
     *
178
     * This method looks for '$tftype=...' in the 'twofactor' session
179
     * variable, and returns the '...'. If no such $tftype is found,
180
     * empty string is returned.
181
     *
182
     * @param string $tftype The two-factor type to query, e.g., 'ga' for
183
     *        Google Authenticator.
184
     * @return string The secret registration key for the given two-factor
185
     *         type, or emtpy string if not set.
186
     */
187
    public static function getRegister($tftype)
188
    {
189
        return self::getSessVar($tftype);
190
    }
191
192
    /**
193
     * isRegistered
194
     *
195
     * This method is a convenience function to check if '$tftype=...'
196
     * is present in the 'twofactor' session variable. It does this
197
     * by checking if the string length of the $tftype key is non-zero.
198
     *
199
     * @param string $tftype The two-factor type to query, e.g., 'ga'.
200
     * @return bool True if '$tftype=...' is found in the 'twofactor'
201
     *         session variable. False otherwise.
202
     */
203
    public static function isRegistered($tftype)
204
    {
205
        return (strlen(self::getRegister($tftype)) > 0);
206
    }
207
208
    /**
209
     * setEnabled
210
     *
211
     * This method sets the 'enabled' entry in the 'twofactor' session
212
     * variable. It first checks if the $tftype is registered. If so,
213
     * it then checks if the $tftype is not currently enabled. If both
214
     * conditions are true, then set 'enabled' to $tftype.
215
     *
216
     * @param string $tftype The two-factor type two set enabled, e.g., 'ga'.
217
     */
218
    public static function setEnabled($tftype)
219
    {
220
        if ((self::isRegistered($tftype)) && (!self::isEnabled($tftype))) {
221
            self::setSessVar('en', $tftype);
222
        }
223
    }
224
225
    /**
226
     * setDisabled
227
     *
228
     * This method sets the 'enabled' entry in the 'twofactor' session
229
     * variable to 'none'.
230
     */
231
    public static function setDisabled()
232
    {
233
        self::setSessVar('en', 'none');
234
    }
235
236
    /**
237
     * getEnabled
238
     *
239
     * This function queries the 'enabled' session variable to find out
240
     * which two factor method is enabled (if any). It returns the
241
     * the enabled two factor method, or 'none' if nothing is enabled.
242
     *
243
     * @return string The two factor method, or 'none' if not enabled.
244
     */
245
    public static function getEnabled()
246
    {
247
        $enabled = 'none';  // Default
248
        $enabledvar = self::getSessVar('en');
249
        if (strlen($enabledvar) > 0) {
250
            $enabled = $enabledvar;
251
        }
252
        return $enabled;
253
    }
254
255
    /**
256
     * getEnabledName
257
     *
258
     * This function queries the 'enabled' session variable and returns
259
     * the 'pretty print' name of the enabled two factor method. If
260
     * a parameter is specified (i.e. the result of calling getEnabled),
261
     * then this method simply returns the 'pretty print' name of that
262
     * two factor type.
263
     *
264
     * @param string $enabled (Optional) The short enabled two factor string.
265
     */
266
    public static function getEnabledName($enabled = null)
267
    {
268
        $name = '';
269
        if (is_null($enabled)) { // No parameter means call getEnabled()
270
            $enabled = self::getEnabled();
271
        }
272
        switch ($enabled) {
273
            case 'ga':
274
                $name = 'Google Authenticator';
275
                break;
276
            case 'duo':
277
                $name = 'Duo Security';
278
                break;
279
            default:
280
                $name = 'Disabled';
281
                break;
282
        }
283
        return $name;
284
    }
285
286
    /**
287
     * isEnabled
288
     *
289
     * This method looks in the 'twofactor' session variable for the
290
     * 'en=...' entry. If this matches the passed-in $tftype,
291
     * true is returned. Otherwise, false is returned.
292
     *
293
     * @param string $tftype The two-factor type to query, e.g., 'ga'.
294
     * @return bool True if the $tftype is found enabled in the
295
     *        'twofactor' session variable. False otherwise.
296
     */
297
    public static function isEnabled($tftype)
298
    {
299
        return (self::getSessVar('en') == $tftype);
300
    }
301
302
    /**
303
     * setVerified
304
     *
305
     * This method sets a 'verified' entry for the given $tftype to
306
     * '1' (for $verified=true) or deletes the verified entry (for
307
     * $verified=false). The verified entry in the 'twofactor' session
308
     * variable is the $tftype concatenated with '-v'. So the entry in
309
     * the 'twofactor' session variable looks like 'ga-v=1'. This
310
     * method first checks if the given $tftype is registered. If so,
311
     * then it sets verified appropriately.
312
     *
313
     * @param string $tftype: The two-factor type two set verified, e.g., 'ga'.
314
     * @param bool $verified (Optional) True for set verified, false for set
315
     *        unverified. Defaults to true.
316
     */
317
    public static function setVerified($tftype, $verified = true)
318
    {
319
        if (self::isRegistered($tftype)) {
320
            self::setSessVar($tftype . '-v', ($verified ? '1' : ''));
321
        }
322
    }
323
324
    /**
325
     * isVerified
326
     *
327
     * This method looks in the 'twofactor' session variable for the
328
     * '$tftype-v=...' entry. If this is '1', true is returned.
329
     * Otherwise, false is returned.
330
     *
331
     * @param string $tftype The two-factor type to query, e.g., 'ga'.
332
     * @return bool True if the $tftype is found verified in the
333
     *         'twofactor' session variable. False otherwise.
334
     */
335
    public static function isVerified($tftype)
336
    {
337
        return (self::getSessVar($tftype . '-v') == '1');
338
    }
339
340
    /**
341
     * printPage
342
     *
343
     * This method is called when the user clicks the 'Enable' button
344
     * for a particular two-factor type, or when the user needs to
345
     * log in with an enabled two-factor type. This function does the
346
     * work of figuring out if the user has registered and verified
347
     * a given two-factor authentication and displays the appropriate
348
     * page (for registering or for logging in).
349
     *
350
     * @param string $tftype (Optional) The two-factor type to print out,
351
     *        e.g., 'ga'. If empty, then print the currently enabled
352
     *        two-factor type page.
353
     */
354
    public static function printPage($tftype = '')
355
    {
356
        // If $tftype is not specified, use the currently enabled type
357
        if (strlen($tftype) == 0) {
358
            $tftype = self::getEnabled();
359
        }
360
        if ($tftype == 'ga') { // Google Authenticator
361
            // Check if user clicked the 'Verify' button. We do this by
362
            // looking for the hidden 'verifyga' field, set to '1'. If found,
363
            // set 'verified' for 'ga' so that we progress to the Login page.
364
            $verifyga = Util::getPostVar('verifyga');
365
            if ($verifyga == '1') {
366
                self::setVerified($tftype);
367
                self::write();
368
            }
369
370
            // If user registered and verified Google Authenticator,
371
            // proceed to the Google Authenticator Login page.
372
            if ((self::isRegistered('ga')) &&
373
                (self::isVerified('ga'))) {
374
                self::printGALoginPage();
375
            } else {
376
                self::printGARegisterPage();
377
            }
378
        } elseif ($tftype == 'duo') { // Duo Security
379
            self::printDuoPage();
380
        }
381
    }
382
383
    /**
384
     * printGARegisterPage
385
     *
386
     * This method prints out the page for Google Authenticator
387
     * Registration. Links for the Google Authenticator app are shown
388
     * along with the 'secret' key (and corresponding QR code) to
389
     * register the user's account on their phone.
390
     */
391
    private static function printGARegisterPage()
392
    {
393
        $secret = '';
394
        // Check if the secret key is already 'registered'.
395
        if (self::isRegistered('ga')) {
396
            $secret = self::getRegister('ga');
397
        } else { // Generate a new secret key and save it to the database
398
            $ga = new PHPGangsta_GoogleAuthenticator();
399
            $secret = $ga->createSecret();
400
            self::setRegister('ga', $secret);
401
            self::write();
402
        }
403
404
        $serialstr = Content::getSerialStringFromDN(Util::getSessionVar('dn'));
405
406
407
        Content::printHeader('Google Authenticator Registration');
408
        echo '
409
        <div class="boxed">
410
411
        <h2>Google Authenticator Registration</h2>
412
        <h3>Step 1</h3>
413
        <p>
414
        Download the Google Authenticator app specific to your device.
415
        </p>
416
        <noscript>
417
        <div class="nojs">
418
        Javascript is disabled. In order to view the QR Code Links
419
        below, please enable Javascript in your browser.
420
        </div>
421
        </noscript>
422
        <ul>
423
        <li> <a target="_blank"
424
        href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android
425
        OS (2.1 and higher)</a> (<a
426
        href="javascript:showHideDiv(\'qrandroid\',-1)">QR Code Link</a>)
427
        <div id="qrandroid" style="display:none">
428
        <br/>
429
        <img alt="QR Code for Android" width="200" height="200"
430
        src="' ,
431
        static::getQRCodeString(
432
            'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2'
433
        ) ,
434
        '" />
435
        </div>
436
        </li>
437
        <li><a target="_blank"
438
        href="https://itunes.apple.com/us/app/google-authenticator/id388497605">Apple
439
        iOS (3.1.3 and higher)</a> (<a href="javascript:showHideDiv(\'qrios\',-1)">QR Code Link</a>)
440
        <div id="qrios" style="display:none">
441
        <br/>
442
        <img alt="QR Code for iOS" width="200" height="200"
443
        src="' ,
444
        static::getQRCodeString('https://itunes.apple.com/us/app/google-authenticator/id388497605') ,
445
        '"/>
446
        </div>
447
        </li>
448
        <li><a target="_blank"
449
        href="https://appworld.blackberry.com/webstore/content/29401059/">BlackBerry OS (4.5-6.0)</a> (<a
450
        href="javascript:showHideDiv(\'qrblackberry\',-1)">QR Code Link</a>)
451
        <div id="qrblackberry" style="display:none">
452
        <br/>
453
        <img alt="QR Code for BlackBerry" width="200" height="200"
454
        src="' ,
455
        static::getQRCodeString('https://appworld.blackberry.com/webstore/content/29401059/') ,
456
        '"/>
457
        </div>
458
        </li>
459
        </ul>
460
461
        <h3>Step 2</h3>
462
        <p>
463
        Launch the Google Authenticator app and add your token by scanning the
464
        QR code or by entering the account information shown below.
465
        </p>
466
        <p>
467
        <div style="float:left;margin-left:2em">
468
        <img alt="QR Code" width="200" height="200"
469
        src="' ,
470
        static::getQRCodeString(
471
            'otpauth://totp/' . $serialstr . '@cilogon.org?secret=' .
472
            $secret . '&issuer=CILogon'
473
        ) ,
474
        '"/>
475
        </div>
476
        <div id="securitycode" style="float:left;margin-left:2em">
477
        <table style="margin-left:2em">
478
        <tr>
479
        <th style="text-align:left">Account Name:</th>
480
        <td><tt>',$serialstr,'@cilogon.org</tt></td>
481
        </tr>
482
        <tr>
483
        <th style="text-align:left">Key:</th>
484
        <td><tt>',$secret,'</tt></td>
485
        </tr>
486
        <tr>
487
        <th style="text-align:left">Type of Key:</th>
488
        <td><tt>Time based</tt></td>
489
        </tr>
490
        </table>
491
        </div>
492
        </p>
493
        <br class="clear" />
494
495
        <h3>Step 3</h3>
496
        <p>
497
        Click the "Verify" button to log in with a one-time password generated
498
        by the Google Authenticator app.
499
        </p>
500
        <div>
501
        ';
502
503
        Content::printFormHead();
504
        echo '
505
        <p>
506
        <input type="hidden" name="verifyga" value="1" />
507
        <input type="hidden" name="twofactortype" value="ga" />
508
        <input type="submit" name="submit" class="submit" value="Verify" />
509
        </p>
510
        </form>
511
        </div>
512
        </div> <!-- boxed -->
513
        ';
514
        Content::printFooter();
515
    }
516
517
    /**
518
     * getQRCodeString
519
     *
520
     * This function takes a string and generates a PNG of a QR code.
521
     * This QR code is then base64 encoded and prefixed with 'data'
522
     * needed to put the image in HTML as a Data URI image. See
523
     * https://en.wikipedia.org/wiki/Data_URI_scheme for more info.
524
     *
525
     * @param string $text The text to QR encode.
526
     * @return string A Base64-encoded PNG of the input text QR encoded.
527
     */
528
    private static function getQRCodeString($text)
529
    {
530
        $qr = new QrCode();
531
        $qr->setText($text);
532
        $qr->setSize(200);
533
        $qr->setMargin(0);
534
        return 'data:image/png;base64,' .
535
            base64_encode($qr->writeString());
536
    }
537
538
    /**
539
     * printGALoginPage
540
     *
541
     * This method prints out the Google Authenticator Log In page.
542
     * The user's Account Name is shown, and the user is prompted to
543
     * run the Google Authenticator app on their phone to generate a
544
     * one time password.
545
     */
546
    private static function printGALoginPage()
547
    {
548
        $serialstr = Content::getSerialStringFromDN(Util::getSessionVar('dn'));
549
550
        Content::printHeader('Google Authenticator Login');
551
        echo '
552
        <div class="boxed">
553
554
        <h2>Google Authenticator Login</h2>
555
        <p>
556
        Your account has been configured to use Google Authenticator as a
557
        second authentication factor. Enter a one-time passcode as generated
558
        by the app below.
559
        </p>
560
        <div class="actionbox">
561
        ';
562
563
        Content::printFormHead();
564
        echo '
565
        <p>
566
        <b>Account:</b> ' , $serialstr , '@cilogon.org
567
        </p>
568
        <p>
569
        <b>Passcode:</b> <input type="text" name="gacode"
570
        id="gacode" size="20" />
571
<!--[if IE]><input type="text" style="display:none;" disabled="disabled" size="1"/><![endif]-->
572
        </p>
573
        <p>
574
        <input type="submit" name="submit" class="submit" value="Enter" />
575
        </p>
576
        </form>
577
        </div>
578
        ';
579
580
        self::printForgotPhone();
581
582
        echo '
583
        </div> <!-- boxed -->
584
        ';
585
        Content::printFooter();
586
    }
587
588
    /**
589
     * printDuoPage
590
     *
591
     * This method prints out the Duo Security page. Note that Duo
592
     * manages itself via an iframe and JavaScript, so there is only
593
     * a single page for Duo Security, which handles both registrations
594
     * and authentications.
595
     */
596
    private static function printDuoPage()
597
    {
598
        // Check if the 'secret key' is already 'registered'. This is
599
        // a bit silly since registration is handled on Duo's servers.
600
        if (!self::isRegistered('duo')) {
601
            self::setRegister('duo', '1');
602
            self::setVerified('duo');
603
            self::write();
604
        }
605
606
        $serialstr = Content::getSerialStringFromDN(Util::getSessionVar('dn'));
607
        $duoconfig = new DuoConfig();
608
        $sig_request = Web::signRequest(
609
            $duoconfig->param['ikey'],
610
            $duoconfig->param['skey'],
611
            $duoconfig->param['akey'],
612
            $serialstr . '@cilogon.org'
613
        );
614
615
        Content::printHeader('Duo Security');
616
        echo '
617
        <div class="boxed">
618
619
        <h2>Duo Security</h2>
620
        <p>
621
        Your account has been configured to use Duo Security as a
622
        second authentication factor. Follow the instructions below to use
623
        Duo Security for "' , $duoconfig->param['name'] , '".
624
        </p>
625
626
        <noscript>
627
        <div class="nojs">
628
        Javascript is disabled. In order to use Duo Security, please enable
629
        Javascript in your browser.
630
        </div>
631
        </noscript>
632
633
        <script src="/include/Duo-Web-v2.min.js"></script>
634
        <script>
635
        Duo.init({
636
            \'host\':\'', $duoconfig->param['host'] ,'\',
637
            \'post_action\':\'', Util::getScriptDir() ,'\',
638
            \'sig_request\':\'', $sig_request , '\'
639
        });
640
        </script>
641
642
643
        <div>
644
        <iframe id="duo_iframe" width="450" height="350"
645
        frameborder="0" allowtransparency="true">
646
        </iframe>
647
        </div>
648
649
        <form method="post" id="duo_form" style="display:none">
650
            <input type="hidden" name="SUBMIT" value="EnterDuo" />
651
        ' , Util::getCsrf()->hiddenFormElement() , '
652
        </form>
653
        ';
654
655
        self::printForgotPhone();
656
657
        echo '
658
        </div> <!-- boxed -->
659
        ';
660
        Content::printFooter();
661
    }
662
663
    /**
664
     * printForgotPhone
665
     *
666
     * This method prints out the HTML block for the 'Don't have your
667
     * phone with you' link.
668
     */
669
    private static function printForgotPhone()
670
    {
671
        echo '
672
        <noscript>
673
        <div class="nojs">
674
        Javascript is disabled. In order to activate the link
675
        below, please enable Javascript in your browser.
676
        </div>
677
        </noscript>
678
679
        <p>
680
        <a href="javascript:showHideDiv(\'missingdevice\',-1)">Don\'t have
681
        your phone with you?</a>
682
        </p>
683
        <div id="missingdevice" style="display:none">
684
        <p>
685
        If you temporarily do not have access to your phone,
686
        you can click the "Disable Two-Factor" button below
687
        to disable two-factor authentication. This will
688
        send a message to the email address provided by your Identity
689
        Provider.  You will then proceed to the CILogon
690
        Service without two-factor authentication enabled.
691
        </p>
692
        ';
693
694
        Content::printFormHead();
695
        echo '
696
        <p>
697
        <input type="hidden" name="missingphone" value="1" />
698
        <input type="submit" name="submit" class="submit"
699
        value="Disable Two-Factor"/>
700
        </p>
701
        </form>
702
        </div>
703
        ';
704
    }
705
706
    /**
707
     * sendPhoneAlert
708
     *
709
     * This method is used to send email to the user when he clicks
710
     * either 'Disable Two-Factor' (under 'Don't have your phone with
711
     * you?') or 'I Lost My Phone' (under 'Lost your phone?').
712
     *
713
     * @param string $summary Subject line summary text.
714
     * @param string $detail Detail text in the body of the email.
715
     * @param string $mailto The destination email address.
716
     */
717
    public static function sendPhoneAlert($summary, $detail, $mailto)
718
    {
719
        $mailfrom = 'From: [email protected]' . "\r\n" .
720
                    'X-Mailer: PHP/' . phpversion();
721
        $mailsubj = 'CILogon Service - ' . $summary;
722
        $mailmsg  = '
723
CILogon Service - ' . $summary . '
724
-----------------------------------------------------------
725
' . $detail . '
726
';
727
728
        mail($mailto, $mailsubj, $mailmsg, $mailfrom);
729
    }
730
731
    /**
732
     * writeEnabledAndVerified
733
     *
734
     * This method is called after a two-factor passcode has been
735
     * successfully validated. It updates the database (if necessary)
736
     * by setting verified and enabled to true for the given two-
737
     * factor method.
738
     *
739
     * @param string $tftype The two-factor authentication type (e.g., 'ga').
740
     */
741
    private static function writeEnabledAndVerified($tftype)
742
    {
743
        $needtowrite = false; // Write to database only if necessary
744
        if (!self::isVerified($tftype)) {
745
            self::setVerified($tftype);
746
            $needtowrite = true;
747
        }
748
        if (!self::isEnabled($tftype)) {
749
            self::setEnabled($tftype);
750
            $needtowrite = true;
751
        }
752
        if ($needtowrite) {
753
            self::write();
754
        }
755
    }
756
757
    /**
758
     * isGACodeValid
759
     *
760
     * This method takes in a passcode generated by the Google
761
     * Authenticator app and attempts to verify it. If the passcode is
762
     * valid, the database is updated for GA verified and enabled, and
763
     * true is returned.
764
     *
765
     * @param string $code The six-digit code generated by Google Authenticator
766
     * @return bool True if the input code is valid, false otherwise.
767
     */
768
    public static function isGACodeValid($code)
769
    {
770
        $valid = false;
771
        $secret = self::getRegister('ga');
772
        if ((strlen($code) > 0) && (strlen($secret) > 0)) {
773
            $ga = new PHPGangsta_GoogleAuthenticator();
774
            $valid = $ga->verifyCode($secret, $code, 1);
775
        }
776
777
        if ($valid) {
778
            self::writeEnabledAndVerified('ga');
779
        }
780
781
        return $valid;
782
    }
783
784
    /**
785
     * isDuoCodeValid
786
     *
787
     * This method takes in a 'sig_response' as generated by the
788
     * Duo Security login code and attempts to verify it. If the
789
     * response is valid, the database is updated for Duo verified and
790
     * enabled, and true is returned.
791
     *
792
     * @param string $code A response code generated by Duo Security login
793
     * @return bool True if the duo code is valid, false otherwise.
794
     */
795
    public static function isDuoCodeValid($code)
796
    {
797
        $valid = false;
798
        if (strlen($code) > 0) {
799
            $duoconfig = new DuoConfig();
800
            $resp = Web::verifyResponse(
801
                $duoconfig->param['ikey'],
802
                $duoconfig->param['skey'],
803
                $duoconfig->param['akey'],
804
                $code
805
            );
806
            if ($resp !== null) {
807
                $valid = true;
808
            }
809
        }
810
811
        if ($valid) {
812
            self::writeEnabledAndVerified('duo');
813
        }
814
815
        return $valid;
816
    }
817
818
    /**
819
     * isDuoCodeValidREST
820
     *
821
     * This method is called by ecpCheck to verify Duo Security
822
     * authentication for the current user. The code is either a six-
823
     * digit passcode as generated by the Duo Security app, a seven-
824
     * character code (1 letter + 6 digits) as sent by SMS, or a
825
     * number indicating which authentication method the user wants to
826
     * use (e.g., '1' often indicates 'push' authentication). This
827
     * method calls 'duoconfig->push()' to validate the code.
828
     *
829
     * @param string $code A 6-digit passcode, a 7-character code, or
830
     *        'factor' index to be used when calling 'duoconfig->auth()'
831
     * @return bool True if the user is authenticated by Duo Security.
832
     *         False otherwise.
833
     */
834
    public static function isDuoCodeValidREST($code)
835
    {
836
        $valid = false;
837
        if (strlen($code) > 0) {
838
            $serialstr = Content::getSerialStringFromDN(Util::getSessionVar('dn'));
839
            $duoconfig = new DuoConfig();
840
            $valid = $duoconfig->auth($serialstr . '@cilogon.org', $code);
841
        }
842
        return $valid;
843
    }
844
845
    /**
846
     * ecpCheck
847
     *
848
     * This method is called when a user attempts to get a PKCS12
849
     * credential or a certificate via ECP. It first checks if two-
850
     * factor authentication is enabled. If not, then true is returned.
851
     * If two-factor is enabled, it checks for a form variable
852
     * 'tfpasscode'. If the passcode is '0', two-factor is disabled,
853
     * and true is returned. Otherwise, the method attempts to validate
854
     * the passcode. If successful, true is returned. Otherwise, this
855
     * method outputs a '401' header with the 'realm' set to a user
856
     * prompt, and false is returned.
857
     *
858
     * @return bool True if either (a) two-factor is disabled or
859
     *         (b) two-factor is enabled and a valid passcode
860
     *         was entered. False otherwise.
861
     */
862
    public static function ecpCheck()
863
    {
864
        $retval = false; // Assume ecp twofactor check failed
865
        $tftype = self::getEnabled();
866
867
        if ($tftype == 'none') { // Two-factor disabled => okay to do ecp
868
            $retval = true;
869
        } else {
870
            $valid = false;
871
            // Look for form-submitted passcode
872
            $code = Util::getPostVar('tfpasscode');
873
            if (strlen($code) > 0) { // Passcode was entered
874
                if ($code == '0') {  // '0' => Disable two-factor
875
                    self::setDisabled();
876
                    self::write();
877
                    $valid = true;
878
                } else { // Check if entered passcode is valid
879
                    if ((($tftype == 'ga') &&
880
                         (self::isGACodeValid($code))) ||
881
                        (($tftype == 'duo') &&
882
                         (self::isDuoCodeValidREST($code)))) {
883
                        $valid = true;
884
                    }
885
                }
886
            }
887
888
            if ($valid) { // Entered code was validated correctly
889
                $retval = true;
890
            } else { // Either no code, or code was not valid
891
                // Send '401 Unauthorized' header with user prompt in 'realm'
892
                $realm = '';
893
                $serialstr = Content::getSerialStringFromDN(Util::getSessionVar('dn'));
894
895
                if ($tftype == 'ga') { // Google Authenticator
896
                    $realm = "Two-factor authentication with Google " .
897
                        "Authenticator is required.\nPlease enter a " .
898
                        "passcode for '[email protected]', or\n'0' " .
899
                        "to disable two-factor authentication for your " .
900
                        "account: ";
901
                } elseif ($tftype == 'duo') { // Duo Security
902
                    $duoconfig = new DuoConfig();
903
                    $preauth = $duoconfig->preauth($serialstr . '@cilogon.org');
904
                    if ($preauth === false) { // Problem with Duo
0 ignored issues
show
introduced by
The condition $preauth === false is always false.
Loading history...
905
                        $realm = "Two-factor authentication with Duo " .
906
                        "Security is required.\nHowever, Duo servers are " .
907
                        "not responding. Please try again later " .
908
                        "or\nenter '0' to disable two-factor " .
909
                        "authentication for your account: ";
910
                    } else { // Check response type from Duo
911
                        if ($preauth['response']['result'] == 'allow') {
912
                            $retval = true; // No need for 401 header
913
                        } elseif ($preauth['response']['result'] == 'auth') {
914
                            $realm = $preauth['response']['prompt'];
915
                            // Add an option for '0. Disable two-factor'
916
                            $realm = preg_replace(
917
                                '/(1\. )/',
918
                                "0. Disable two-factor authentication\n $1",
919
                                $realm
920
                            );
921
                            $realm = preg_replace('/\(1-/', '(0-', $realm);
922
                        } else { // Either 'deny' or 'enroll' returned
923
                            $realm = "Two-factor authentication with Duo " .
924
                            "Security is required.\nHowever, Duo " .
925
                            "indicated that access to your account is " .
926
                            "denied.\nPlease contact [email protected] " .
927
                            "for further assistance or\nenter '0' to " .
928
                            "disable two-factor authentication for your " .
929
                            "account: ";
930
                        }
931
                    }
932
                }
933
934
                if (strlen($realm) > 0) {
935
                    // Need to urlencode $realm due to \n and spaces
936
                    header('WWW-Authenticate: Basic realm="' .
937
                           urlencode($realm) . '"');
938
                    header('HTTP/1.0 401 Unauthorized');
939
                }
940
            }
941
        }
942
943
        return $retval;
944
    }
945
}
946