Passed
Pull Request — develop (#37)
by Pieter van der
03:21
created

Tiqr_Service::generateAuthQR()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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