Passed
Branch develop (201d1e)
by Pieter van der
11:58 queued 06:01
created

Tiqr_Service::validateEnrollmentSecret()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0932

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 6
c 3
b 0
f 0
dl 0
loc 10
ccs 5
cts 7
cp 0.7143
rs 10
cc 2
nc 2
nop 1
crap 2.0932
1
<?php
2
/**
3
 * This file is part of the tiqr project.
4
 * 
5
 * The tiqr project aims to provide an open implementation for 
6
 * authentication using mobile devices. It was initiated by 
7
 * SURFnet and developed by Egeniq.
8
 *
9
 * More information: http://www.tiqr.org
10
 *
11
 * @author Ivo Jansch <[email protected]>
12
 * 
13
 * @package tiqr
14
 *
15
 * @license New BSD License - See LICENSE file for details.
16
 *
17
 * @copyright (C) 2010-2011 SURFnet BV
18
 */
19
20
/** 
21
 * @internal includes of utility classes
22
 */
23 1
require_once("Tiqr/StateStorage.php");
24 1
require_once("Tiqr/DeviceStorage.php");
25 1
require_once("Tiqr/Random.php");
26
27 1
require_once("Tiqr/OATH/OCRAWrapper.php");
28 1
require_once("Tiqr/OcraService.php");
29
30
use Psr\Log\LoggerInterface;
31
32
/** 
33
 * The main Tiqr Service class.
34
 * This is the class that an application interacts with to provide mobile 
35
 * authentication
36
 * @author ivo
37
 *
38
 */
39
class Tiqr_Service
40
{
41
    /**
42
     * @internal Various variables internal to the service class
43
     */
44
    protected $_options;
45
    
46
    protected $_protocolAuth = "tiqr";
47
    protected $_protocolEnroll = "tiqrenroll";
48
    
49
    protected $_identifier = "";
50
    protected $_ocraSuite = "";
51
    protected $_name = "";
52
    protected $_logoUrl = "";
53
    protected $_infoUrl = "";
54
    protected $_protocolVersion = 0;
55
    
56
    protected $_stateStorage = NULL;
57
    protected $_deviceStorage = NULL;
58
59
    protected $_ocraWrapper;
60
    protected $_ocraService;
61
62
    /**
63
     * The notification exception
64
     *
65
     * @var Exception
66
     */
67
    protected $_notificationError = NULL;
68
69
    /**
70
     * @var LoggerInterface
71
     */
72
    private $logger;
73
74
    /**
75
     * Enrollment status codes
76
     */
77
    const ENROLLMENT_STATUS_IDLE = 1;        // Nothing happens
78
    const ENROLLMENT_STATUS_INITIALIZED = 2; // An enrollment session has begun
79
    const ENROLLMENT_STATUS_RETRIEVED = 3;   // The device has retrieved the metadata
80
    const ENROLLMENT_STATUS_PROCESSED = 4;   // The device has snet back a secret
81
    const ENROLLMENT_STATUS_FINALIZED = 5;   // The application has stored the secret
82
    const ENROLLMENT_STATUS_VALIDATED = 6;   // A first succesful authentication was performed
83
84
    const PREFIX_ENROLLMENT_SECRET = 'enrollsecret';
85
    const PREFIX_ENROLLMENT = 'enroll';
86
    const PREFIX_CHALLENGE = 'challenge';
87
88
    /**
89
     * Default timeout values
90
     */
91
    const ENROLLMENT_EXPIRE = 300; // If enrollment isn't cmpleted within 120 seconds, we discard data
92
    const LOGIN_EXPIRE      = 3600; // Logins timeout after an hour
93
    const CHALLENGE_EXPIRE  = 180; // If login is not performed within 3 minutes, we discard the challenge
94
95
    /**
96
     * Authentication result codes
97
     */
98
    const AUTH_RESULT_INVALID_REQUEST   = 1;
99
    const AUTH_RESULT_AUTHENTICATED     = 2;
100
    const AUTH_RESULT_INVALID_RESPONSE  = 3;
101
    const AUTH_RESULT_INVALID_CHALLENGE = 4;
102
    const AUTH_RESULT_INVALID_USERID    = 5;
103
    
104
    /**
105
     * The default OCRA Suite to use for authentication
106
     */
107
    const DEFAULT_OCRA_SUITE = "OCRA-1:HOTP-SHA1-6:QH10-S";
108
      
109
    /**
110
     * Construct an instance of the Tiqr_Service. 
111
     * The server is configured using an array of options. All options have
112
     * reasonable defaults but it's recommended to at least specify a custom 
113
     * name and identifier and a randomly generated sessions secret.
114
     * If you use the Tiqr Service with your own apps, you must also specify
115
     * a custom auto.protocol and enroll.protocol specifier.
116
     * 
117
     * The options are:
118
     * - auth.protocol: The protocol specifier (e.g. tiqr://) that the 
119
     *                  server uses to communicate challenge urls to the phone. 
120
     *                  This must match the url handler specified in the 
121
     *                  iPhone app's build settings. You do not have to add the
122
     *                  '://', just the protocolname.
123
     *                  Default: tiqr
124
     * - enroll.protocol: The protocol specifier for enrollment urls.
125
     *                    Default: tiqrenroll
126
     * - ocra.suite: The OCRA suite to use. Defaults to OCRA-1:HOTP-SHA1-6:QN10-S.
127
     * - identifier: A short ASCII identifier for your service.
128
     *               Defaults to the SERVER_NAME of the server.
129
     * - name: A longer description of your service.
130
     *         Defaults to the SERVER_NAME of the server.              
131
     * - logoUrl: A full http url pointing to a logo for your service.
132
     * - infoUrl: An http url pointing to an info page of your service
133
     * - phpqrcode.path: The location of the phpqrcode library.
134
     *                   Defaults to ../phpqrcode
135
     * - apns.path: The location of the ApnsPHP library.
136
     *              Defaults to ../apns-php
137
     * - apns.certificate: The location of your Apple push notification
138
     *                     certificate.
139
     *                     Defaults to ../certificates/cert.pem
140
     * - apns.environment: Whether to use apple's "sandbox" or "production" 
141
     *                     apns environment
142
     * - statestorage: An array with the configuration of the storage for 
143
     *                 temporary data. It has the following sub keys:
144
     *                 - type: The type of state storage. (default: file) 
145
     *                 - parameters depending on the storage.
146
     *                 See the classes inside the StateStorage folder for 
147
     *                 supported types and their parameters.
148
     * - devicestorage: An array with the configruation of the storage for
149
     *                  device push notification tokens. Only necessary if 
150
     *                  you use the Tiqr Service as step-up authentication
151
     *                  for an already existing user. It has the following 
152
     *                  keys:
153
     *                  - type: The type of  storage. (default: dummy) 
154
     *                  - parameters depending on the storage.
155
     *                 See the classes inside the DeviceStorage folder for 
156
     *                 supported types and their parameters.
157
     *  
158
     * @param array $options
159
     * @param int $version The protocol version to use (defaults to the latest)
160
     */
161 4
    public function __construct(LoggerInterface $logger, $options=array(), $version = 2)
162
    {
163 4
        $this->_options = $options;
164 4
        $this->logger = $logger;
165
        
166 4
        if (isset($options["auth.protocol"])) {
167 3
            $this->_protocolAuth = $options["auth.protocol"];
168
        }
169
        
170 4
        if (isset($options["enroll.protocol"])) {
171 3
            $this->_protocolEnroll = $options["enroll.protocol"];
172
        }
173
        
174 4
        if (isset($options["ocra.suite"])) {
175 3
            $this->_ocraSuite = $options["ocra.suite"];
176
        } else {
177 1
            $this->_ocraSuite = self::DEFAULT_OCRA_SUITE;
178
        }
179
        
180 4
        if (isset($options["identifier"])) { 
181 3
            $this->_identifier = $options["identifier"];
182
        } else {
183 1
            $this->_identifier = $_SERVER["SERVER_NAME"];
184
        }
185
        
186 4
        if (isset($options["name"])) {
187 3
            $this->_name = $options["name"];
188
        } else {
189 1
            $this->_name = $_SERVER["SERVER_NAME"];
190
        }
191
192 4
        if (isset($options["logoUrl"])) { 
193 3
            $this->_logoUrl = $options["logoUrl"];
194
        }
195
196 4
        if (isset($options["infoUrl"])) {
197 3
            $this->_infoUrl = $options["infoUrl"];
198
        }
199
        
200 4
        if (isset($options["statestorage"])) {
201 3
            $type = $options["statestorage"]["type"];
202 3
            $storageOptions = $options["statestorage"];
203
        } else {
204 1
            $this->logger->info('Falling back to file state storage');
205 1
            $type = "file";
206 1
            $storageOptions = array();
207
        }
208
209 4
        $this->logger->info(sprintf('Creating a %s state storage', $type));
210 4
        $this->_stateStorage = Tiqr_StateStorage::getStorage($type, $storageOptions, $logger);
211
        
212 4
        if (isset($options["devicestorage"])) {
213 3
            $type = $options["devicestorage"]["type"];
214 3
            $storageOptions = $options["devicestorage"];
215
        } else {
216 1
            $this->logger->info('Falling back to dummy device storage');
217 1
            $type = "dummy";
218 1
            $storageOptions = array();
219
        }
220 4
        $this->logger->info(sprintf('Creating a %s device storage', $type));
221 4
        $this->_deviceStorage = Tiqr_DeviceStorage::getStorage($type, $storageOptions, $logger);
222
        
223 4
        $this->_protocolVersion = $version;
224 4
        $this->_ocraWrapper = new Tiqr_OCRAWrapper($this->_ocraSuite);
225
226 4
        $type = 'tiqr';
227 4
        if (isset($options['usersecretstorage']) && $options['usersecretstorage']['type'] == 'oathserviceclient') {
228
            $type = 'oathserviceclient';
229
        }
230 4
        $ocraConfig = array();
231 4
        switch ($type) {
232 4
            case 'tiqr':
233 4
                $ocraConfig['ocra.suite'] = $this->_ocraSuite;
234 4
                $ocraConfig['protocolVersion'] = $version;
235 4
                break;
236
            case 'oathserviceclient':
237
                $ocraConfig = $options['usersecretstorage'];
238
                break;
239
        }
240 4
        $this->logger->info(sprintf('Creating a %s ocra service', $type));
241 4
        $this->_ocraService = Tiqr_OcraService::getOcraService($type, $ocraConfig, $logger);
242 4
    }
243
    
244
    /**
245
     * Get the identifier of the service.
246
     * @return String identifier
247
     */
248 2
    public function getIdentifier()
249
    {
250 2
        return $this->_identifier;
251
    }
252
    
253
    /**
254
     * Generate an authentication challenge QR image and send it directly to 
255
     * the browser.
256
     * 
257
     * In normal authentication mode, you would not specify a userId - however
258
     * in step up mode, where a user is already authenticated using a
259
     * different mechanism, pass the userId of the authenticated user to this 
260
     * function. 
261
     * @param String $sessionKey The sessionKey identifying this auth session (typically returned by startAuthenticationSession)
262
     */
263
    public function generateAuthQR($sessionKey)
264
    {
265
        // TODO
266
        $challengeUrl = $this->_getChallengeUrl($sessionKey);
267
268
        $this->generateQR($challengeUrl);
0 ignored issues
show
Bug introduced by
It seems like $challengeUrl can also be of type false; however, parameter $s of Tiqr_Service::generateQR() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

268
        $this->generateQR(/** @scrutinizer ignore-type */ $challengeUrl);
Loading history...
269
    }
270
271
    /**
272
     * Generate a QR image and send it directly to
273
     * the browser.
274
     *
275
     * @param String $s The string to be encoded in the QR image
276
     */
277
    public function generateQR($s)
278
    {
279
        QRcode::png($s, false, 4, 5);
280
    }
281
282
    /**
283
     * Send a push notification to a user containing an authentication challenge
284
     * @param String $sessionKey          The session key identifying this authentication session
285
     * @param String $notificationType    Notification type, e.g. APNS, FCM
286
     * @param String $notificationAddress Notification address, e.g. device token, phone number etc.
287
     *
288
     * @return boolean True if the notification was sent succesfully, false if not.
289
     *
290
     * @todo Use exceptions in case of errors
291
     */
292
    public function sendAuthNotification($sessionKey, $notificationType, $notificationAddress)
293
    {
294
        try {
295
            $this->_notificationError = null;
296
297
            $class = "Tiqr_Message_{$notificationType}";
298
            if (!class_exists($class)) {
299
                $this->logger->error(sprintf('Unable to create push notification for type "%s"', $notificationType));
300
                return false;
301
            }
302
            $this->logger->info(sprintf('Creating and sending a %s push notification', $notificationType));
303
            $message = new $class($this->_options);
304
            $message->setId(time());
305
            $message->setText("Please authenticate for " . $this->_name);
306
            $message->setAddress($notificationAddress);
307
            $message->setCustomProperty('challenge', $this->_getChallengeUrl($sessionKey));
308
            $message->send();
309
310
            return true;
311
        } catch (Exception $ex) {
312
            $this->setNotificationError($ex);
313
            $this->logger->error(sprintf('Sending push notification failed with message "%s"', $ex->getMessage()));
314
            return false;
315
        }
316
    }
317
318
    /**
319
     * Set the notification exception
320
     *
321
     * @param Exception $ex
322
     */
323
    protected function setNotificationError(Exception $ex)
324
    {
325
        $this->_notificationError = $ex;
326
    }
327
328
    /**
329
     * Get the notification error that occurred
330
     *
331
     * @return array
332
     */
333
    public function getNotificationError()
334
    {
335
        return array(
336
            'code' => $this->_notificationError->getCode(),
337
            'file' => $this->_notificationError->getFile(),
338
            'line' => $this->_notificationError->getLine(),
339
            'message' => $this->_notificationError->getMessage(),
340
            'trace' => $this->_notificationError->getTraceAsString()
341
        );
342
    }
343
344
    /** 
345
     * Generate an authentication challenge URL.
346
     * This URL can be used to link directly to the authentication
347
     * application, for example to create a link in a mobile website on the
348
     * same device as where the application is installed
349
     * @param String $sessionKey The session key identifying this authentication session
350
     * @param String $userId The userId of a pre-authenticated user, if in  
351
     *                       step-up mode. NULL in other scenario's.
352
     * @param String $sessionId The application's session identifier. 
353
     *                          (defaults to php session)
354
     */
355 1
    public function generateAuthURL($sessionKey)
356
    {
357 1
        $challengeUrl = $this->_getChallengeUrl($sessionKey);  
358
        
359 1
        return $challengeUrl;
360
        
361
    }
362
363
    /**
364
     * Start an authentication session. This generates a challenge for this 
365
     * session and stores it in memory. The returned sessionKey should be used
366
     * throughout the authentication process.
367
     * @param String $userId The userId of a pre-authenticated user (optional)
368
     * @param String $sessionId The session id the application uses to 
369
     *                          identify its user sessions; (optional, 
370
     *                          defaults to the php session id).
371
     * @param String $spIdentifier If SP and IDP are 2 different things, pass the url/identifier of the SP the user is logging into.
372
     *                             For setups where IDP==SP, just leave this blank.
373
     */
374 1
    public function startAuthenticationSession($userId="", $sessionId="", $spIdentifier="")
375
    {
376 1
        if ($sessionId=="") {
377
            $sessionId = session_id();
378
        }
379
380 1
        if ($spIdentifier=="") {
381 1
            $spIdentifier = $this->_identifier;
382
        }
383
384 1
        $sessionKey = $this->_uniqueSessionKey(self::PREFIX_CHALLENGE);
385
    
386 1
        $challenge = $this->_ocraService->generateChallenge();
387
        
388 1
        $data = array("sessionId"=>$sessionId, "challenge"=>$challenge, "spIdentifier" => $spIdentifier);
389
        
390 1
        if ($userId!="") {
391 1
            $data["userId"] = $userId;
392
        }
393
        
394 1
        $this->_stateStorage->setValue(self::PREFIX_CHALLENGE . $sessionKey, $data, self::CHALLENGE_EXPIRE);
395
       
396 1
        return $sessionKey;
397
    }
398
    
399
    /**
400
     * Start an enrollment session. This can either be the enrollment of a new 
401
     * user or of an existing user, there is no difference from Tiqr's point
402
     * of view.
403
     * 
404
     * The call returns the temporary enrollmentKey that the phone needs to 
405
     * retrieve the metadata; you must therefor embed this key in the metadata
406
     * URL that you communicate to the phone.
407
     * 
408
     * @param String $userId The user's id
409
     * @param String $displayName The user's full name
410
     * @param String $sessionId The application's session identifier (defaults 
411
     *                           to php session)
412
     * @return String The enrollment key
413
     */
414 1
    public function startEnrollmentSession($userId, $displayName, $sessionId="")
415
    {
416 1
        if ($sessionId=="") {
417
            $sessionId = session_id();
418
        }
419 1
        $enrollmentKey = $this->_uniqueSessionKey(self::PREFIX_ENROLLMENT);
420
        $data = [
421 1
            "userId" => $userId,
422 1
            "displayName" => $displayName,
423 1
            "sessionId" => $sessionId
424
        ];
425 1
        $this->_stateStorage->setValue(self::PREFIX_ENROLLMENT . $enrollmentKey, $data, self::ENROLLMENT_EXPIRE);
426 1
        $this->_setEnrollmentStatus($sessionId, self::ENROLLMENT_STATUS_INITIALIZED);
427
428 1
        return $enrollmentKey;
429
    }
430
431
    /**
432
     * Reset an existing enrollment session. (start over)
433
     * @param $sessionId The application's session identifier (defaults
434
     *                   to php session)
435
     */
436
    public function resetEnrollmentSession($sessionId="")
437
    {
438
        if ($sessionId=="") {
439
            $sessionId = session_id();
440
        }
441
442
        $this->_setEnrollmentStatus($sessionId, self::ENROLLMENT_STATUS_IDLE);
443
    }
444
445
    /**
446
     * Remove enrollment data based on the enrollment key (which is
447
     * encoded in the QR code). This removes both the session data used
448
     * in the polling mechanism and the long term state in the state
449
     * storage (FS/Pdo/Memcache)
450
     */
451
    public function clearEnrollmentState(string $key)
452
    {
453
        $value = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT.$key);
454
        if (is_array($value) && array_key_exists('sessionId', $value)) {
455
            // Reset the enrollment session (used for polling the status of the enrollment)
456
            $this->resetEnrollmentSession($value['sessionId']);
457
        }
458
        // Remove the enrollment data for a specific enrollment key
459
        $this->_stateStorage->unsetValue(self::PREFIX_ENROLLMENT.$key);
460
    }
461
462
    /**
463
     * Retrieve the enrollment status of an enrollment session.
464
     * 
465
     * @param String $sessionId the application's session identifier 
466
     *                          (defaults to php session)
467
     * @return int Enrollment status. Can be any one of these values:
468
     *             - Tiqr_Server::ENROLLMENT_STATUS_IDLE 
469
     *               There is no enrollment going on in this session
470
     *             - Tiqr_Server::ENROLLMENT_STATUS_INITIALIZED
471
     *               An enrollment session was started but the phone has not
472
     *               yet taken action. 
473
     *             - Tiqr_Server::ENROLLMENT_STATUS_RETRIEVED
474
     *               The device has retrieved the metadata
475
     *             - Tiqr_Server::ENROLLMENT_STATUS_PROCESSED
476
     *               The device has sent back a secret for the user
477
     *             - Tiqr_Server::ENROLLMENT_STATUS_FINALIZED
478
     *               The application has stored the secret
479
     *             - Tiqr_Server::ENROLLMENT_STATUS_VALIDATED
480
     *               A first successful authentication was performed 
481
     *               (todo: currently not used)
482
     */
483 1
    public function getEnrollmentStatus($sessionId="")
484
    { 
485 1
        if ($sessionId=="") {
486
            $sessionId = session_id(); 
487
        }
488 1
        $status = $this->_stateStorage->getValue("enrollstatus".$sessionId);
489 1
        if (is_null($status)) return self::ENROLLMENT_STATUS_IDLE;
490 1
        return $status;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $status also could return the type array|string which is incompatible with the documented return type integer.
Loading history...
491
    }
492
        
493
    /**
494
     * Generate an enrollment QR code and send it to the browser.
495
     * @param String $metadataUrl The URL you provide to the phone to retrieve
496
     *                            metadata. This URL must contain the enrollmentKey
497
     *                            provided by startEnrollmentSession (you can choose
498
     *                            the variable name as you are responsible yourself
499
     *                            for retrieving this from the request and passing it
500
     *                            on to the Tiqr server.
501
     */
502
    public function generateEnrollmentQR($metadataUrl) 
503
    { 
504
        $enrollmentString = $this->_getEnrollString($metadataUrl);
505
        
506
        QRcode::png($enrollmentString, false, 4, 5);
507
    }
508
509
    /**
510
     * Generate an enrol string
511
     * This string can be used to feed to a QR code generator
512
     */
513 1
    public function generateEnrollString($metadataUrl)
514
    {
515 1
        return $this->_getEnrollString($metadataUrl);
516
    }
517
    
518
    /**
519
     * Retrieve the metadata for an enrollment session.
520
     * 
521
     * When the phone calls the url that you have passed to 
522
     * generateEnrollmentQR, you must provide it with the output
523
     * of this function. (Don't forget to json_encode the output.)
524
     * 
525
     * Note, you can call this function only once, as the enrollment session
526
     * data will be destroyed as soon as it is retrieved.
527
     * 
528
     * @param String $enrollmentKey The enrollmentKey that the phone has
529
     *                              posted along with its request.
530
     * @param String $authenticationUrl The url you provide to the phone to
531
     *                                  post authentication responses
532
     * @param String $enrollmentUrl The url you provide to the phone to post
533
     *                              the generated user secret. You must include
534
     *                              a temporary enrollment secret in this URL
535
     *                              to make this process secure. This secret
536
     *                              can be generated with the 
537
     *                              getEnrollmentSecret call.
538
     * @return array An array of metadata that the phone needs to complete
539
     *               enrollment. You must encode it in JSON before you send
540
     *               it to the phone.
541
     */
542 1
    public function getEnrollmentMetadata($enrollmentKey, $authenticationUrl, $enrollmentUrl)
543
    {
544 1
        $data = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT . $enrollmentKey);
545 1
        if (!is_array($data)) {
546
            $this->logger->error('Unable to find enrollment metadata in state storage');
547
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
548
        }
549
550
        $metadata = array("service"=>
551 1
                               array("displayName"       => $this->_name,
552 1
                                     "identifier"        => $this->_identifier,
553 1
                                     "logoUrl"           => $this->_logoUrl,
554 1
                                     "infoUrl"           => $this->_infoUrl,
555 1
                                     "authenticationUrl" => $authenticationUrl,
556 1
                                     "ocraSuite"         => $this->_ocraSuite,
557 1
                                     "enrollmentUrl"     => $enrollmentUrl
558
                               ),
559
                          "identity"=>
560 1
                               array("identifier" =>$data["userId"],
561 1
                                     "displayName"=>$data["displayName"]));
562
563 1
        $this->_stateStorage->unsetValue(self::PREFIX_ENROLLMENT . $enrollmentKey);
564
565 1
        $this->_setEnrollmentStatus($data["sessionId"], self::ENROLLMENT_STATUS_RETRIEVED);
566 1
        return $metadata;
567
    }
568
569
    /** 
570
     * Get a temporary enrollment secret to be able to securely post a user 
571
     * secret.
572
     *
573
     * As part of the enrollment process the phone will send a user secret. 
574
     * This shared secret is used in the authentication process. To make sure
575
     * user secrets can not be posted by malicious hackers, a secret is 
576
     * required. This secret should be included in the enrollmentUrl that is 
577
     * passed to the getMetadata function.
578
     * @param String $enrollmentKey The enrollmentKey generated at the start
579
     *                              of the enrollment process.
580
     * @return String The enrollment secret
581
     */
582 1
    public function getEnrollmentSecret($enrollmentKey)
583
    {
584 1
         $data = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT . $enrollmentKey);
585 1
         $secret = $this->_uniqueSessionKey(self::PREFIX_ENROLLMENT_SECRET);
586
         $enrollmentData = [
587 1
             "userId" => $data["userId"],
588 1
             "sessionId" => $data["sessionId"]
589
         ];
590 1
         $this->_stateStorage->setValue(
591 1
             self::PREFIX_ENROLLMENT_SECRET . $secret,
592 1
             $enrollmentData,
593 1
             self::ENROLLMENT_EXPIRE
594
         );
595 1
         return $secret;
596
    } 
597
598
    /**
599
     * Validate if an enrollmentSecret that was passed from the phone is valid.
600
     * @param $enrollmentSecret The secret that the phone posted; it must match
601
     *                          the secret that was generated using 
602
     *                          getEnrollmentSecret earlier in the process.
603
     * @return mixed The userid of the user that was being enrolled if the 
604
     *               secret is valid. This userid should be used to store the 
605
     *               user secret that the phone posted.
606
     *               If the enrollmentSecret is invalid, false is returned.
607
     */
608 1
    public function validateEnrollmentSecret($enrollmentSecret)
609
    {
610 1
        $data = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT_SECRET.$enrollmentSecret);
611 1
        if (is_array($data)) {
612
            // Secret is valid, application may accept the user secret.
613 1
            $this->_setEnrollmentStatus($data["sessionId"], self::ENROLLMENT_STATUS_PROCESSED);
614 1
            return $data["userId"];
615
        }
616
        $this->logger->info('Validation of enrollment secret failed');
617
        return false;
618
    }
619
    
620
    /**
621
     * Finalize the enrollment process.
622
     * If the user secret was posted by the phone, was validated using 
623
     * validateEnrollmentSecret AND if the secret was stored securely on the 
624
     * server, you should call finalizeEnrollment. This clears some enrollment
625
     * temporary pieces of data, and sets the status of the enrollment to 
626
     * finalized.
627
     * @param String The enrollment secret that was posted by the phone. This 
628
     *               is the same secret used in the call to 
629
     *               validateEnrollmentSecret.
630
     * @return boolean True if succesful 
631
     */
632 1
    public function finalizeEnrollment($enrollmentSecret) 
633
    {
634 1
         $data = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT_SECRET.$enrollmentSecret);
635 1
         if (is_array($data)) {
636
             // Enrollment is finalized, destroy our session data.
637 1
             $this->_setEnrollmentStatus($data["sessionId"], self::ENROLLMENT_STATUS_FINALIZED);
638 1
             $this->_stateStorage->unsetValue(self::PREFIX_ENROLLMENT_SECRET.$enrollmentSecret);
639
         } else {
640
             $this->logger->error(
641
                 'Enrollment status is not finalized, enrollmentsecret was not found in state storage. ' .
642
                 'Warning! the method will still return "true" as a result.'
643
             );
644
         }
645 1
         return true;
646
    }
647
648
    /**
649
     * Authenticate a user.
650
     * This method should be called when the phone posts a response to an
651
     * authentication challenge. The method will validate the response and
652
     * mark the user's session as authenticated. This essentially logs the
653
     * user in.
654
     * @param String $userId The userid of the user that should be 
655
     *                       authenticated
656
     * @param String $userSecret The user's secret. This should be the 
657
     *                           secret stored in a secure storage. 
658
     * @param String $sessionKey The phone will post a session key, this 
659
     *                           should be passed to this method in order
660
     *                           for the server to unlock the user's browser
661
     *                           session.
662
     * @param String $response   The response to the challenge that the phone
663
     *                           has posted.
664
     * @return String The result of the authentication. This is one of the
665
     *                AUTH_RESULT_* constants of the Tiqr_Server class.
666
     *                (do not make assumptions on the values of these 
667
     *                constants.)
668
     */
669 1
    public function authenticate($userId, $userSecret, $sessionKey, $response)
670
    {
671 1
        $state = $this->_stateStorage->getValue(self::PREFIX_CHALLENGE . $sessionKey);
672 1
        if (is_null($state)) {
673
            $this->logger->info('The auth challenge could not be found in the state storage');
674
            return self::AUTH_RESULT_INVALID_CHALLENGE;
675
        }
676
        
677 1
        $sessionId       = $state["sessionId"];
678 1
        $challenge       = $state["challenge"];
679
680 1
        $challengeUserId = NULL;
681 1
        if (isset($state["userId"])) {
682 1
          $challengeUserId = $state["userId"];
683
        }
684
        // Check if we're dealing with a second factor
685 1
        if ($challengeUserId!=NULL && ($userId != $challengeUserId)) {
686 1
            $this->logger->error(
687 1
                'Authentication failed: the first factor user id does not match with that of the second factor'
688
            );
689 1
            return self::AUTH_RESULT_INVALID_USERID; // only allowed to authenticate against the user that's authenticated in the first factor
690
        }
691
692 1
        $method = $this->_ocraService->getVerificationMethodName();
693 1
        if ($method == 'verifyResponseWithUserId') {
694
            $equal = $this->_ocraService->$method($response, $userId, $challenge, $sessionKey);
695
        } else {
696 1
            $equal = $this->_ocraService->$method($response, $userSecret, $challenge, $sessionKey);
697
        }
698
699 1
        if ($equal) {
700 1
            $this->_stateStorage->setValue("authenticated_".$sessionId, $userId, self::LOGIN_EXPIRE);
701
            
702
            // Clean up the challenge.
703 1
            $this->_stateStorage->unsetValue(self::PREFIX_CHALLENGE . $sessionKey);
704 1
            $this->logger->info('Authentication succeeded');
705 1
            return self::AUTH_RESULT_AUTHENTICATED;
706
        }
707 1
        $this->logger->error('Authentication failed: verification failed');
708 1
        return self::AUTH_RESULT_INVALID_RESPONSE;
709
    }
710
711
    /**
712
     * Log the user out.
713
     * @param String $sessionId The application's session identifier (defaults
714
     *                          to the php session).
715
     */
716
    public function logout($sessionId="")
717
    {
718
        if ($sessionId=="") {
719
            $sessionId = session_id(); 
720
        }
721
        
722
        return $this->_stateStorage->unsetValue("authenticated_".$sessionId);
723
    }
724
    
725
    /**
726
     * Exchange a notificationToken for a deviceToken.
727
     * 
728
     * During enrollment, the phone will post a notificationAddress that can be 
729
     * used to send notifications. To actually send the notification, 
730
     * this address should be converted to the real device address.
731
     *
732
     * @param String $notificationType    The notification type.
733
     * @param String $notificationAddress The address that was stored during enrollment.
734
     *
735
     * @return String The device address that can be used to send a notification.
736
     */
737
    public function translateNotificationAddress($notificationType, $notificationAddress)
738
    {
739
        if ($notificationType == 'APNS' || $notificationType == 'FCM') {
740
            return $this->_deviceStorage->getDeviceToken($notificationAddress);
741
        } else {
742
            return $notificationAddress;
743
        }
744
    }
745
    
746
    /**
747
     * Retrieve the currently logged in user.
748
     * @param String $sessionId The application's session identifier (defaults
749
     *                          to the php session).
750
     * @return mixed An array with user data if a user was logged in or NULL if
751
     *               no user is logged in.
752
     */
753 1
    public function getAuthenticatedUser($sessionId="")
754
    {
755 1
        if ($sessionId=="") {
756
            $this->logger->debug('Using the PHP session id, as no session id was provided');
757
            $sessionId = session_id(); 
758
        }
759
        
760
        // Todo, we should return false, not null, to be more consistent
761 1
        return $this->_stateStorage->getValue("authenticated_".$sessionId);
762
    }
763
    
764
    /**
765
     * Generate a challenge URL
766
     * @param String $sessionKey The key that identifies the session.
767
     * @param String $challenge The authentication challenge
768
     * @param String $userId The userid to embed in the challenge url (only
769
     *                       if a user was pre-authenticated)
770
     *
771
     */
772 1
    protected function _getChallengeUrl($sessionKey)
773
    {                
774 1
        $state = $this->_stateStorage->getValue(self::PREFIX_CHALLENGE . $sessionKey);
775 1
        if (is_null($state)) {
776
            $this->logger->error(
777
                'Unable find an existing challenge url in the state storage based on the existing session key'
778
            );
779
            return false;
780
        }
781
        
782 1
        $userId   = NULL;
783 1
        $challenge = $state["challenge"];
784 1
        if (isset($state["userId"])) {
785 1
            $userId = $state["userId"];
786
        }
787 1
        $spIdentifier = $state["spIdentifier"];
788
        
789
        // Last bit is the spIdentifier
790 1
        return $this->_protocolAuth."://".(!is_null($userId)?urlencode($userId).'@':'').$this->getIdentifier()."/".$sessionKey."/".$challenge."/".urlencode($spIdentifier)."/".$this->_protocolVersion;
791
    }
792
793
    /**
794
     * Generate an enrollment string
795
     * @param String $metadataUrl The URL you provide to the phone to retrieve metadata.
796
     */
797 1
    protected function _getEnrollString($metadataUrl)
798
    {
799 1
        return $this->_protocolEnroll."://".$metadataUrl;
800
    }
801
802
    /**
803
     * Generate a unique random key to be used to store temporary session
804
     * data.
805
     * @param String $prefix A prefix for the key (different prefixes should
806
     *                       be used to store different pieces of data).
807
     *                       The function guarantees that the same key is nog
808
     *                       generated for the same prefix.
809
     * @return String The unique session key. (without the prefix!)
810
     */
811 2
    protected function _uniqueSessionKey($prefix)
812
    {      
813 2
        $value = 1;
814 2
        while ($value!=NULL) {
815 2
            $sessionKey = $this->_ocraWrapper->generateSessionKey();
816 2
            $value = $this->_stateStorage->getValue($prefix.$sessionKey);
817
        }
818 2
        return $sessionKey;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sessionKey does not seem to be defined for all execution paths leading up to this point.
Loading history...
819
    }
820
    
821
    /**
822
     * Internal function to set the enrollment status of a session.
823
     * @param String $sessionId The sessionId to set the status for
824
     * @param int $status The new enrollment status (one of the 
825
     *                    self::ENROLLMENT_STATUS_* constants)
826
     */
827 1
    protected function _setEnrollmentStatus($sessionId, $status)
828
    {
829 1
       $this->_stateStorage->setValue("enrollstatus".$sessionId, $status, self::ENROLLMENT_EXPIRE);
830 1
    }
831
}
832