Passed
Pull Request — develop (#38)
by Pieter van der
03:27
created

Tiqr_Service::getEnrollmentMetadata()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 2.0065

Importance

Changes 5
Bugs 1 Features 0
Metric Value
eloc 18
c 5
b 1
f 0
dl 0
loc 25
ccs 15
cts 17
cp 0.8824
rs 9.6666
cc 2
nc 2
nop 3
crap 2.0065
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 5
    public function __construct(LoggerInterface $logger, $options=array(), $version = 2)
162
    {
163 5
        $this->_options = $options;
164 5
        $this->logger = $logger;
165
        
166 5
        if (isset($options["auth.protocol"])) {
167 3
            $this->_protocolAuth = $options["auth.protocol"];
168
        }
169
        
170 5
        if (isset($options["enroll.protocol"])) {
171 3
            $this->_protocolEnroll = $options["enroll.protocol"];
172
        }
173
        
174 5
        if (isset($options["ocra.suite"])) {
175 3
            $this->_ocraSuite = $options["ocra.suite"];
176
        } else {
177 2
            $this->_ocraSuite = self::DEFAULT_OCRA_SUITE;
178
        }
179
        
180 5
        if (isset($options["identifier"])) { 
181 3
            $this->_identifier = $options["identifier"];
182
        } else {
183 2
            $this->_identifier = $_SERVER["SERVER_NAME"];
184
        }
185
        
186 5
        if (isset($options["name"])) {
187 3
            $this->_name = $options["name"];
188
        } else {
189 2
            $this->_name = $_SERVER["SERVER_NAME"];
190
        }
191
192 5
        if (isset($options["logoUrl"])) { 
193 3
            $this->_logoUrl = $options["logoUrl"];
194
        }
195
196 5
        if (isset($options["infoUrl"])) {
197 3
            $this->_infoUrl = $options["infoUrl"];
198
        }
199
        
200 5
        if (isset($options["statestorage"])) {
201 5
            $type = $options["statestorage"]["type"];
202 5
            $storageOptions = $options["statestorage"];
203
        } else {
204
            throw new RuntimeException('No state storage configuration is configured, please provide one');
205
        }
206
207 5
        $this->logger->info(sprintf('Creating a %s state storage', $type));
208 5
        $this->_stateStorage = Tiqr_StateStorage::getStorage($type, $storageOptions, $logger);
209
        
210 4
        if (isset($options["devicestorage"])) {
211 3
            $type = $options["devicestorage"]["type"];
212 3
            $storageOptions = $options["devicestorage"];
213
        } else {
214 1
            $this->logger->info('Falling back to dummy device storage');
215 1
            $type = "dummy";
216 1
            $storageOptions = array();
217
        }
218 4
        $this->logger->info(sprintf('Creating a %s device storage', $type));
219 4
        $this->_deviceStorage = Tiqr_DeviceStorage::getStorage($type, $storageOptions, $logger);
220
        
221 4
        $this->_protocolVersion = $version;
222 4
        $this->_ocraWrapper = new Tiqr_OCRAWrapper($this->_ocraSuite);
223
224 4
        $type = 'tiqr';
225 4
        if (isset($options['usersecretstorage']) && $options['usersecretstorage']['type'] == 'oathserviceclient') {
226
            $type = 'oathserviceclient';
227
        }
228 4
        $ocraConfig = array();
229 4
        switch ($type) {
230 4
            case 'tiqr':
231 4
                $ocraConfig['ocra.suite'] = $this->_ocraSuite;
232 4
                $ocraConfig['protocolVersion'] = $version;
233 4
                break;
234
            case 'oathserviceclient':
235
                $ocraConfig = $options['usersecretstorage'];
236
                break;
237
        }
238 4
        $this->logger->info(sprintf('Creating a %s ocra service', $type));
239 4
        $this->_ocraService = Tiqr_OcraService::getOcraService($type, $ocraConfig, $logger);
240 4
    }
241
    
242
    /**
243
     * Get the identifier of the service.
244
     * @return String identifier
245
     */
246 2
    public function getIdentifier()
247
    {
248 2
        return $this->_identifier;
249
    }
250
    
251
    /**
252
     * Generate an authentication challenge QR image and send it directly to 
253
     * the browser.
254
     * 
255
     * In normal authentication mode, you would not specify a userId - however
256
     * in step up mode, where a user is already authenticated using a
257
     * different mechanism, pass the userId of the authenticated user to this 
258
     * function. 
259
     * @param String $sessionKey The sessionKey identifying this auth session (typically returned by startAuthenticationSession)
260
     */
261
    public function generateAuthQR($sessionKey)
262
    {
263
        // TODO
264
        $challengeUrl = $this->_getChallengeUrl($sessionKey);
265
266
        $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

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