Passed
Pull Request — develop (#27)
by Michiel
06:20
created

Tiqr_Service::__construct()   F

Complexity

Conditions 14
Paths 3072

Size

Total Lines 81
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 48
CRAP Score 14.0891

Importance

Changes 7
Bugs 1 Features 1
Metric Value
eloc 56
c 7
b 1
f 1
dl 0
loc 81
ccs 48
cts 52
cp 0.9231
rs 2.1
cc 14
nc 3072
nop 3
crap 14.0891

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * 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
     * - c2dm.username: The username for your android c2dm account
143
     * - c2dm.password: The password for your android c2dm account
144
     * - c2dm.application: The application identifier for your android 
145
     *                     app, e.g. com.example.authenticator.
146
     * - statestorage: An array with the configuration of the storage for 
147
     *                 temporary data. It has the following sub keys:
148
     *                 - type: The type of state storage. (default: file) 
149
     *                 - parameters depending on the storage.
150
     *                 See the classes inside the StateStorage folder for 
151
     *                 supported types and their parameters.
152
     * - devicestorage: An array with the configruation of the storage for
153
     *                  device push notification tokens. Only necessary if 
154
     *                  you use the Tiqr Service as step-up authentication
155
     *                  for an already existing user. It has the following 
156
     *                  keys:
157
     *                  - type: The type of  storage. (default: dummy) 
158
     *                  - parameters depending on the storage.
159
     *                 See the classes inside the DeviceStorage folder for 
160
     *                 supported types and their parameters.
161
     *  
162
     * @param array $options
163
     * @param int $version The protocol version to use (defaults to the latest)
164
     */
165 4
    public function __construct(LoggerInterface $logger, $options=array(), $version = 2)
166
    {
167 4
        $this->_options = $options;
168 4
        $this->logger = $logger;
169
        
170 4
        if (isset($options["auth.protocol"])) {
171 3
            $this->_protocolAuth = $options["auth.protocol"];
172
        }
173
        
174 4
        if (isset($options["enroll.protocol"])) {
175 3
            $this->_protocolEnroll = $options["enroll.protocol"];
176
        }
177
        
178 4
        if (isset($options["ocra.suite"])) {
179 3
            $this->_ocraSuite = $options["ocra.suite"];
180
        } else {
181 1
            $this->_ocraSuite = self::DEFAULT_OCRA_SUITE;
182
        }
183
        
184 4
        if (isset($options["identifier"])) { 
185 3
            $this->_identifier = $options["identifier"];
186
        } else {
187 1
            $this->_identifier = $_SERVER["SERVER_NAME"];
188
        }
189
        
190 4
        if (isset($options["name"])) {
191 3
            $this->_name = $options["name"];
192
        } else {
193 1
            $this->_name = $_SERVER["SERVER_NAME"];
194
        }
195
196 4
        if (isset($options["logoUrl"])) { 
197 3
            $this->_logoUrl = $options["logoUrl"];
198
        }
199
200 4
        if (isset($options["infoUrl"])) {
201 3
            $this->_infoUrl = $options["infoUrl"];
202
        }
203
        
204 4
        if (isset($options["statestorage"])) {
205 3
            $type = $options["statestorage"]["type"];
206 3
            $storageOptions = $options["statestorage"];
207
        } else {
208 1
            $this->logger->info('Falling back to file state storage');
209 1
            $type = "file";
210 1
            $storageOptions = array();
211
        }
212
213 4
        $this->logger->info(sprintf('Creating a %s state storage', $type));
214 4
        $this->_stateStorage = Tiqr_StateStorage::getStorage($type, $storageOptions, $logger);
215
        
216 4
        if (isset($options["devicestorage"])) {
217 3
            $type = $options["devicestorage"]["type"];
218 3
            $storageOptions = $options["devicestorage"];
219
        } else {
220 1
            $this->logger->info('Falling back to dummy device storage');
221 1
            $type = "dummy";
222 1
            $storageOptions = array();
223
        }
224 4
        $this->logger->info(sprintf('Creating a %s device storage', $type));
225 4
        $this->_deviceStorage = Tiqr_DeviceStorage::getStorage($type, $storageOptions, $logger);
226
        
227 4
        $this->_protocolVersion = $version;
228 4
        $this->_ocraWrapper = new Tiqr_OCRAWrapper($this->_ocraSuite);
229
230 4
        $type = 'tiqr';
231 4
        if (isset($options['usersecretstorage']) && $options['usersecretstorage']['type'] == 'oathserviceclient') {
232
            $type = 'oathserviceclient';
233
        }
234 4
        $ocraConfig = array();
235 4
        switch ($type) {
236 4
            case 'tiqr':
237 4
                $ocraConfig['ocra.suite'] = $this->_ocraSuite;
238 4
                $ocraConfig['protocolVersion'] = $version;
239 4
                break;
240
            case 'oathserviceclient':
241
                $ocraConfig = $options['usersecretstorage'];
242
                break;
243
        }
244 4
        $this->logger->info(sprintf('Creating a %s ocra service', $type));
245 4
        $this->_ocraService = Tiqr_OcraService::getOcraService($type, $ocraConfig, $logger);
246 4
    }
247
    
248
    /**
249
     * Get the identifier of the service.
250
     * @return String identifier
251
     */
252 2
    public function getIdentifier()
253
    {
254 2
        return $this->_identifier;
255
    }
256
    
257
    /**
258
     * Generate an authentication challenge QR image and send it directly to 
259
     * the browser.
260
     * 
261
     * In normal authentication mode, you would not specify a userId - however
262
     * in step up mode, where a user is already authenticated using a
263
     * different mechanism, pass the userId of the authenticated user to this 
264
     * function. 
265
     * @param String $sessionKey The sessionKey identifying this auth session (typically returned by startAuthenticationSession)
266
     */
267
    public function generateAuthQR($sessionKey)
268
    {
269
        // TODO
270
        $challengeUrl = $this->_getChallengeUrl($sessionKey);
271
272
        $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

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