Passed
Push — master ( 1c7137...b5ef5e )
by Pieter van der
03:26 queued 14s
created

Tiqr_Service::_getChallengeUrl()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

261
        $this->generateQR(/** @scrutinizer ignore-type */ $challengeUrl);
Loading history...
262
    }
263
264
    /**
265
     * Generate a QR image and send it directly to
266
     * the browser.
267
     *
268
     * @param String $s The string to be encoded in the QR image
269
     */
270
    public function generateQR($s)
271
    {
272
        QRcode::png($s, false, 4, 5);
273
    }
274
275
    /**
276
     * Send a push notification to a user containing an authentication challenge
277
     * @param String $sessionKey          The session key identifying this authentication session
278
     * @param String $notificationType    Notification type, e.g. APNS, C2DM, GCM, (SMS?)
279
     * @param String $notificationAddress Notification address, e.g. device token, phone number etc.
280
     *
281
     * @return boolean True if the notification was sent succesfully, false if not.
282
     *
283
     * @todo Use exceptions in case of errors
284
     */
285
    public function sendAuthNotification($sessionKey, $notificationType, $notificationAddress)
286
    {
287
        try {
288
            $this->_notificationError = null;
289
290
            $class = "Tiqr_Message_{$notificationType}";
291
            if (!class_exists($class)) {
292
                return false;
293
            }
294
295
            $message = new $class($this->_options);
296
            $message->setId(time());
297
            $message->setText("Please authenticate for " . $this->_name);
298
            $message->setAddress($notificationAddress);
299
            $message->setCustomProperty('challenge', $this->_getChallengeUrl($sessionKey));
300
            $message->send();
301
302
            return true;
303
        } catch (Exception $ex) {
304
            $this->setNotificationError($ex);
305
            return false;
306
        }
307
    }
308
309
    /**
310
     * Set the notification exception
311
     *
312
     * @param Exception $ex
313
     */
314
    protected function setNotificationError(Exception $ex)
315
    {
316
        $this->_notificationError = $ex;
317
    }
318
319
    /**
320
     * Get the notification error that occurred
321
     *
322
     * @return array
323
     */
324
    public function getNotificationError()
325
    {
326
        return array(
327
            'code' => $this->_notificationError->getCode(),
328
            'file' => $this->_notificationError->getFile(),
329
            'line' => $this->_notificationError->getLine(),
330
            'message' => $this->_notificationError->getMessage(),
331
            'trace' => $this->_notificationError->getTraceAsString()
332
        );
333
    }
334
335
    /** 
336
     * Generate an authentication challenge URL.
337
     * This URL can be used to link directly to the authentication
338
     * application, for example to create a link in a mobile website on the
339
     * same device as where the application is installed
340
     * @param String $sessionKey The session key identifying this authentication session
341
     * @param String $userId The userId of a pre-authenticated user, if in  
342
     *                       step-up mode. NULL in other scenario's.
343
     * @param String $sessionId The application's session identifier. 
344
     *                          (defaults to php session)
345
     */
346
    public function generateAuthURL($sessionKey)
347
    {
348
        $challengeUrl = $this->_getChallengeUrl($sessionKey);  
349
        
350
        return $challengeUrl;
351
        
352
    }
353
354
    /**
355
     * Start an authentication session. This generates a challenge for this 
356
     * session and stores it in memory. The returned sessionKey should be used
357
     * throughout the authentication process.
358
     * @param String $userId The userId of a pre-authenticated user (optional)
359
     * @param String $sessionId The session id the application uses to 
360
     *                          identify its user sessions; (optional, 
361
     *                          defaults to the php session id).
362
     * @param String $spIdentifier If SP and IDP are 2 different things, pass the url/identifier of the SP the user is logging into.
363
     *                             For setups where IDP==SP, just leave this blank.
364
     */
365
    public function startAuthenticationSession($userId="", $sessionId="", $spIdentifier="")
366
    {
367
        if ($sessionId=="") {
368
            $sessionId = session_id();
369
        }
370
371
        if ($spIdentifier=="") {
372
            $spIdentifier = $this->_identifier;
373
        }
374
375
        $sessionKey = $this->_uniqueSessionKey(self::PREFIX_CHALLENGE);
376
    
377
        $challenge = $this->_ocraService->generateChallenge();
378
        
379
        $data = array("sessionId"=>$sessionId, "challenge"=>$challenge, "spIdentifier" => $spIdentifier);
380
        
381
        if ($userId!="") {
382
            $data["userId"] = $userId;
383
        }
384
        
385
        $this->_stateStorage->setValue(self::PREFIX_CHALLENGE . $sessionKey, $data, self::CHALLENGE_EXPIRE);
386
       
387
        return $sessionKey;
388
    }
389
    
390
    /**
391
     * Start an enrollment session. This can either be the enrollment of a new 
392
     * user or of an existing user, there is no difference from Tiqr's point
393
     * of view.
394
     * 
395
     * The call returns the temporary enrollmentKey that the phone needs to 
396
     * retrieve the metadata; you must therefor embed this key in the metadata
397
     * URL that you communicate to the phone.
398
     * 
399
     * @param String $userId The user's id
400
     * @param String $displayName The user's full name
401
     * @param String $sessionId The application's session identifier (defaults 
402
     *                           to php session)
403
     * @return String The enrollment key
404
     */
405
    public function startEnrollmentSession($userId, $displayName, $sessionId="")
406
    {
407
        if ($sessionId=="") {
408
            $sessionId = session_id();
409
        }
410
        $enrollmentKey = $this->_uniqueSessionKey(self::PREFIX_ENROLLMENT);
411
        $data = [
412
            "userId" => $userId,
413
            "displayName" => $displayName,
414
            "sessionId" => $sessionId
415
        ];
416
        $this->_stateStorage->setValue(self::PREFIX_ENROLLMENT . $enrollmentKey, $data, self::ENROLLMENT_EXPIRE);
417
        $this->_setEnrollmentStatus($sessionId, self::ENROLLMENT_STATUS_INITIALIZED);
418
419
        return $enrollmentKey;
420
    }
421
422
    /**
423
     * Reset an existing enrollment session. (start over)
424
     * @param $sessionId The application's session identifier (defaults
0 ignored issues
show
Bug introduced by
The type The was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
425
     *                   to php session)
426
     */
427
    public function resetEnrollmentSession($sessionId="")
428
    {
429
        if ($sessionId=="") {
430
            $sessionId = session_id();
431
        }
432
433
        $this->_setEnrollmentStatus($sessionId, self::ENROLLMENT_STATUS_IDLE);
434
    }
435
436
    /**
437
     * Remove enrollment data based on the enrollment key (which is
438
     * encoded in the QR code). This removes both the session data used
439
     * in the polling mechanism and the long term state in the state
440
     * storage (FS/Pdo/Memcache)
441
     */
442
    public function clearEnrollmentState(string $key)
443
    {
444
        $value = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT.$key);
445
        if (is_array($value) && array_key_exists('sessionId', $value)) {
446
            // Reset the enrollment session (used for polling the status of the enrollment)
447
            $this->resetEnrollmentSession($value['sessionId']);
448
        }
449
        // Remove the enrollment data for a specific enrollment key
450
        $this->_stateStorage->unsetValue(self::PREFIX_ENROLLMENT.$key);
451
    }
452
453
    /**
454
     * Retrieve the enrollment status of an enrollment session.
455
     * 
456
     * @param String $sessionId the application's session identifier 
457
     *                          (defaults to php session)
458
     * @return int Enrollment status. Can be any one of these values:
459
     *             - Tiqr_Server::ENROLLMENT_STATUS_IDLE 
460
     *               There is no enrollment going on in this session
461
     *             - Tiqr_Server::ENROLLMENT_STATUS_INITIALIZED
462
     *               An enrollment session was started but the phone has not
463
     *               yet taken action. 
464
     *             - Tiqr_Server::ENROLLMENT_STATUS_RETRIEVED
465
     *               The device has retrieved the metadata
466
     *             - Tiqr_Server::ENROLLMENT_STATUS_PROCESSED
467
     *               The device has sent back a secret for the user
468
     *             - Tiqr_Server::ENROLLMENT_STATUS_FINALIZED
469
     *               The application has stored the secret
470
     *             - Tiqr_Server::ENROLLMENT_STATUS_VALIDATED
471
     *               A first successful authentication was performed 
472
     *               (todo: currently not used)
473
     */
474
    public function getEnrollmentStatus($sessionId="")
475
    { 
476
        if ($sessionId=="") {
477
            $sessionId = session_id(); 
478
        }
479
        $status = $this->_stateStorage->getValue("enrollstatus".$sessionId);
480
        if (is_null($status)) return self::ENROLLMENT_STATUS_IDLE;
481
        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...
482
    }
483
        
484
    /**
485
     * Generate an enrollment QR code and send it to the browser.
486
     * @param String $metadataUrl The URL you provide to the phone to retrieve
487
     *                            metadata. This URL must contain the enrollmentKey
488
     *                            provided by startEnrollmentSession (you can choose
489
     *                            the variable name as you are responsible yourself
490
     *                            for retrieving this from the request and passing it
491
     *                            on to the Tiqr server.
492
     */
493
    public function generateEnrollmentQR($metadataUrl) 
494
    { 
495
        $enrollmentString = $this->_getEnrollString($metadataUrl);
496
        
497
        QRcode::png($enrollmentString, false, 4, 5);
498
    }
499
500
    /**
501
     * Generate an enrol string
502
     * This string can be used to feed to a QR code generator
503
     */
504
    public function generateEnrollString($metadataUrl)
505
    {
506
        return $this->_getEnrollString($metadataUrl);
507
    }
508
    
509
    /**
510
     * Retrieve the metadata for an enrollment session.
511
     * 
512
     * When the phone calls the url that you have passed to 
513
     * generateEnrollmentQR, you must provide it with the output
514
     * of this function. (Don't forget to json_encode the output.)
515
     * 
516
     * Note, you can call this function only once, as the enrollment session
517
     * data will be destroyed as soon as it is retrieved.
518
     * 
519
     * @param String $enrollmentKey The enrollmentKey that the phone has
520
     *                              posted along with its request.
521
     * @param String $authenticationUrl The url you provide to the phone to
522
     *                                  post authentication responses
523
     * @param String $enrollmentUrl The url you provide to the phone to post
524
     *                              the generated user secret. You must include
525
     *                              a temporary enrollment secret in this URL
526
     *                              to make this process secure. This secret
527
     *                              can be generated with the 
528
     *                              getEnrollmentSecret call.
529
     * @return array An array of metadata that the phone needs to complete
530
     *               enrollment. You must encode it in JSON before you send
531
     *               it to the phone.
532
     */
533
    public function getEnrollmentMetadata($enrollmentKey, $authenticationUrl, $enrollmentUrl)
534
    {
535
        $data = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT . $enrollmentKey);
536
        if (!is_array($data)) {
537
            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...
538
        }
539
540
        $metadata = array("service"=>
541
                               array("displayName"       => $this->_name,
542
                                     "identifier"        => $this->_identifier,
543
                                     "logoUrl"           => $this->_logoUrl,
544
                                     "infoUrl"           => $this->_infoUrl,
545
                                     "authenticationUrl" => $authenticationUrl,
546
                                     "ocraSuite"         => $this->_ocraSuite,
547
                                     "enrollmentUrl"     => $enrollmentUrl
548
                               ),
549
                          "identity"=>
550
                               array("identifier" =>$data["userId"],
551
                                     "displayName"=>$data["displayName"]));
552
553
        $this->_stateStorage->unsetValue(self::PREFIX_ENROLLMENT . $enrollmentKey);
554
555
        $this->_setEnrollmentStatus($data["sessionId"], self::ENROLLMENT_STATUS_RETRIEVED);
556
        return $metadata;
557
    }
558
559
    /** 
560
     * Get a temporary enrollment secret to be able to securely post a user 
561
     * secret.
562
     *
563
     * As part of the enrollment process the phone will send a user secret. 
564
     * This shared secret is used in the authentication process. To make sure
565
     * user secrets can not be posted by malicious hackers, a secret is 
566
     * required. This secret should be included in the enrollmentUrl that is 
567
     * passed to the getMetadata function.
568
     * @param String $enrollmentKey The enrollmentKey generated at the start
569
     *                              of the enrollment process.
570
     * @return String The enrollment secret
571
     */
572
    public function getEnrollmentSecret($enrollmentKey)
573
    {
574
         $data = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT . $enrollmentKey);
575
         $secret = $this->_uniqueSessionKey(self::PREFIX_ENROLLMENT_SECRET);
576
         $enrollmentData = [
577
             "userId" => $data["userId"],
578
             "sessionId" => $data["sessionId"]
579
         ];
580
         $this->_stateStorage->setValue(
581
             self::PREFIX_ENROLLMENT_SECRET . $secret,
582
             $enrollmentData,
583
             self::ENROLLMENT_EXPIRE
584
         );
585
         return $secret;
586
    } 
587
588
    /**
589
     * Validate if an enrollmentSecret that was passed from the phone is valid.
590
     * @param $enrollmentSecret The secret that the phone posted; it must match
591
     *                          the secret that was generated using 
592
     *                          getEnrollmentSecret earlier in the process.
593
     * @return mixed The userid of the user that was being enrolled if the 
594
     *               secret is valid. This userid should be used to store the 
595
     *               user secret that the phone posted.
596
     *               If the enrollmentSecret is invalid, false is returned.
597
     */
598
    public function validateEnrollmentSecret($enrollmentSecret)
599
    {
600
         $data = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT_SECRET.$enrollmentSecret);
601
         if (is_array($data)) { 
602
             // Secret is valid, application may accept the user secret. 
603
             $this->_setEnrollmentStatus($data["sessionId"], self::ENROLLMENT_STATUS_PROCESSED);
604
             return $data["userId"];
605
         }
606
         return false;
607
    }
608
    
609
    /**
610
     * Finalize the enrollment process.
611
     * If the user secret was posted by the phone, was validated using 
612
     * validateEnrollmentSecret AND if the secret was stored securely on the 
613
     * server, you should call finalizeEnrollment. This clears some enrollment
614
     * temporary pieces of data, and sets the status of the enrollment to 
615
     * finalized.
616
     * @param String The enrollment secret that was posted by the phone. This 
617
     *               is the same secret used in the call to 
618
     *               validateEnrollmentSecret.
619
     * @return boolean True if succesful 
620
     */
621
    public function finalizeEnrollment($enrollmentSecret) 
622
    {
623
         $data = $this->_stateStorage->getValue(self::PREFIX_ENROLLMENT_SECRET.$enrollmentSecret);
624
         if (is_array($data)) {
625
             // Enrollment is finalized, destroy our session data.
626
             $this->_setEnrollmentStatus($data["sessionId"], self::ENROLLMENT_STATUS_FINALIZED);
627
             $this->_stateStorage->unsetValue(self::PREFIX_ENROLLMENT_SECRET.$enrollmentSecret);
628
         }
629
         return true;
630
    }
631
632
    /**
633
     * Authenticate a user.
634
     * This method should be called when the phone posts a response to an
635
     * authentication challenge. The method will validate the response and
636
     * mark the user's session as authenticated. This essentially logs the
637
     * user in.
638
     * @param String $userId The userid of the user that should be 
639
     *                       authenticated
640
     * @param String $userSecret The user's secret. This should be the 
641
     *                           secret stored in a secure storage. 
642
     * @param String $sessionKey The phone will post a session key, this 
643
     *                           should be passed to this method in order
644
     *                           for the server to unlock the user's browser
645
     *                           session.
646
     * @param String $response   The response to the challenge that the phone
647
     *                           has posted.
648
     * @return String The result of the authentication. This is one of the
649
     *                AUTH_RESULT_* constants of the Tiqr_Server class.
650
     *                (do not make assumptions on the values of these 
651
     *                constants.)
652
     */
653
    public function authenticate($userId, $userSecret, $sessionKey, $response)
654
    {
655
        $state = $this->_stateStorage->getValue(self::PREFIX_CHALLENGE . $sessionKey);
656
        if (is_null($state)) {
657
            return self::AUTH_RESULT_INVALID_CHALLENGE;
658
        }
659
        
660
        $sessionId       = $state["sessionId"];
661
        $challenge       = $state["challenge"];
662
663
        $challengeUserId = NULL;
664
        if (isset($state["userId"])) {
665
          $challengeUserId = $state["userId"];
666
        }
667
        // Check if we're dealing with a second factor
668
        if ($challengeUserId!=NULL && ($userId != $challengeUserId)) {
669
            return self::AUTH_RESULT_INVALID_USERID; // only allowed to authenticate against the user that's authenticated in the first factor
670
        }
671
672
        $method = $this->_ocraService->getVerificationMethodName();
673
        if ($method == 'verifyResponseWithUserId') {
674
            $equal = $this->_ocraService->$method($response, $userId, $challenge, $sessionKey);
675
        } else {
676
            $equal = $this->_ocraService->$method($response, $userSecret, $challenge, $sessionKey);
677
        }
678
679
        if ($equal) {
680
            $this->_stateStorage->setValue("authenticated_".$sessionId, $userId, self::LOGIN_EXPIRE);
681
            
682
            // Clean up the challenge.
683
            $this->_stateStorage->unsetValue(self::PREFIX_CHALLENGE . $sessionKey);
684
            
685
            return self::AUTH_RESULT_AUTHENTICATED;
686
        }
687
        return self::AUTH_RESULT_INVALID_RESPONSE;
688
    }
689
690
    /**
691
     * Log the user out.
692
     * @param String $sessionId The application's session identifier (defaults
693
     *                          to the php session).
694
     */
695
    public function logout($sessionId="")
696
    {
697
        if ($sessionId=="") {
698
            $sessionId = session_id(); 
699
        }
700
        
701
        return $this->_stateStorage->unsetValue("authenticated_".$sessionId);
702
    }
703
    
704
    /**
705
     * Exchange a notificationToken for a deviceToken.
706
     * 
707
     * During enrollment, the phone will post a notificationAddress that can be 
708
     * used to send notifications. To actually send the notification, 
709
     * this address should be converted to the real device address.
710
     *
711
     * @param String $notificationType    The notification type.
712
     * @param String $notificationAddress The address that was stored during enrollment.
713
     *
714
     * @return String The device address that can be used to send a notification.
715
     */
716
    public function translateNotificationAddress($notificationType, $notificationAddress)
717
    {
718
        if ($notificationType == 'APNS' || $notificationType == 'C2DM' || $notificationType == 'GCM' || $notificationType == 'FCM') {
719
            return $this->_deviceStorage->getDeviceToken($notificationAddress);
0 ignored issues
show
Bug introduced by
The method getDeviceToken() does not exist on null. ( Ignorable by Annotation )

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

719
            return $this->_deviceStorage->/** @scrutinizer ignore-call */ getDeviceToken($notificationAddress);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
720
        } else {
721
            return $notificationAddress;
722
        }
723
    }
724
    
725
    /**
726
     * Retrieve the currently logged in user.
727
     * @param String $sessionId The application's session identifier (defaults
728
     *                          to the php session).
729
     * @return mixed An array with user data if a user was logged in or NULL if
730
     *               no user is logged in.
731
     */
732
    public function getAuthenticatedUser($sessionId="")
733
    {
734
        if ($sessionId=="") {
735
            $sessionId = session_id(); 
736
        }
737
        
738
        // Todo, we should return false, not null, to be more consistent
739
        return $this->_stateStorage->getValue("authenticated_".$sessionId);
740
    }
741
    
742
    /**
743
     * Generate a challenge URL
744
     * @param String $sessionKey The key that identifies the session.
745
     * @param String $challenge The authentication challenge
746
     * @param String $userId The userid to embed in the challenge url (only
747
     *                       if a user was pre-authenticated)
748
     *
749
     */
750
    protected function _getChallengeUrl($sessionKey)
751
    {                
752
        $state = $this->_stateStorage->getValue(self::PREFIX_CHALLENGE . $sessionKey);
753
        if (is_null($state)) {
754
            return false;
755
        }
756
        
757
        $userId   = NULL;
758
        $challenge = $state["challenge"];
759
        if (isset($state["userId"])) {
760
            $userId = $state["userId"];
761
        }
762
        $spIdentifier = $state["spIdentifier"];
763
        
764
        // Last bit is the spIdentifier
765
        return $this->_protocolAuth."://".(!is_null($userId)?urlencode($userId).'@':'').$this->getIdentifier()."/".$sessionKey."/".$challenge."/".urlencode($spIdentifier)."/".$this->_protocolVersion;
766
    }
767
768
    /**
769
     * Generate an enrollment string
770
     * @param String $metadataUrl The URL you provide to the phone to retrieve metadata.
771
     */
772
    protected function _getEnrollString($metadataUrl)
773
    {
774
        return $this->_protocolEnroll."://".$metadataUrl;
775
    }
776
777
    /**
778
     * Generate a unique random key to be used to store temporary session
779
     * data.
780
     * @param String $prefix A prefix for the key (different prefixes should
781
     *                       be used to store different pieces of data).
782
     *                       The function guarantees that the same key is nog
783
     *                       generated for the same prefix.
784
     * @return String The unique session key. (without the prefix!)
785
     */
786
    protected function _uniqueSessionKey($prefix)
787
    {      
788
        $value = 1;
789
        while ($value!=NULL) {
790
            $sessionKey = $this->_ocraWrapper->generateSessionKey();
791
            $value = $this->_stateStorage->getValue($prefix.$sessionKey);
792
        }
793
        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...
794
    }
795
    
796
    /**
797
     * Internal function to set the enrollment status of a session.
798
     * @param String $sessionId The sessionId to set the status for
799
     * @param int $status The new enrollment status (one of the 
800
     *                    self::ENROLLMENT_STATUS_* constants)
801
     */
802
    protected function _setEnrollmentStatus($sessionId, $status)
803
    {
804
       $this->_stateStorage->setValue("enrollstatus".$sessionId, $status, self::ENROLLMENT_EXPIRE);
805
    }
806
}
807