|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/* |
|
4
|
|
|
@license New BSD License - See LICENSE file for details. |
|
5
|
|
|
@copyright (C) 2022 SURF BV |
|
6
|
|
|
*/ |
|
7
|
|
|
|
|
8
|
|
|
namespace TestServer; |
|
9
|
|
|
|
|
10
|
|
|
use Tiqr_AutoLoader; |
|
11
|
|
|
use Tiqr_OCRAWrapper; |
|
12
|
|
|
use Tiqr_Service; |
|
13
|
|
|
use Tiqr_UserStorage; |
|
14
|
|
|
|
|
15
|
|
|
class TestServerController |
|
16
|
|
|
{ |
|
17
|
|
|
private $tiqrService; |
|
18
|
|
|
private $userStorage; |
|
19
|
|
|
private $host_url; |
|
20
|
|
|
|
|
21
|
|
|
/** |
|
22
|
|
|
* @param $host_url This is the URL by which the tiqr client can reach this server, including http(s):// and port. |
|
23
|
|
|
* E.g. 'http://my-laptop.local:8000' |
|
24
|
|
|
* @param $authProtocol This is the app specific url for authentications of the tiqr client, without '://' |
|
|
|
|
|
|
25
|
|
|
* e.g. 'tiqrauth'. This must match what is configured in the tiqr client |
|
26
|
|
|
* @param $enrollProtocol This is the app specific url for enrolling user accounts in the tiqr client, without '://' |
|
27
|
|
|
* e.g. 'tiqrenroll'. This must match what is configured in the tiqr client |
|
28
|
|
|
*/ |
|
29
|
|
|
function __construct(string $host_url, string $authProtocol, string $enrollProtocol) |
|
30
|
|
|
{ |
|
31
|
|
|
$this->host_url = $host_url; |
|
32
|
|
|
$this->initTiqrLibrary(); |
|
33
|
|
|
$this->tiqrService = $this->createTiqrService($host_url, $authProtocol, $enrollProtocol); |
|
34
|
|
|
$this->userStorage = $this->createUserStorage(); |
|
35
|
|
|
} |
|
36
|
|
|
|
|
37
|
|
|
/** Initialize the tiqr-server-libphp's autoloader |
|
38
|
|
|
* @return void |
|
39
|
|
|
*/ |
|
40
|
|
|
private function initTiqrLibrary() |
|
41
|
|
|
{ |
|
42
|
|
|
// Initialise the tiqr-server-library autoloader |
|
43
|
|
|
$tiqr_dir = __DIR__ . '/../library/tiqr'; |
|
44
|
|
|
$vendor_dir = __DIR__ . '/../vendor'; |
|
45
|
|
|
|
|
46
|
|
|
require_once $tiqr_dir . '/Tiqr/AutoLoader.php'; |
|
47
|
|
|
|
|
48
|
|
|
$autoloader = Tiqr_AutoLoader::getInstance([ |
|
49
|
|
|
'tiqr.path' => $tiqr_dir, |
|
50
|
|
|
'phpqrcode.path' => $vendor_dir . '/kairos/phpqrcode', |
|
51
|
|
|
'zend.path' => $vendor_dir . '/zendframework/zendframework1/library' |
|
52
|
|
|
]); |
|
53
|
|
|
$autoloader->setIncludePath(); |
|
54
|
|
|
} |
|
55
|
|
|
|
|
56
|
|
|
/** |
|
57
|
|
|
* @return string Directory path for storing the server's data |
|
58
|
|
|
*/ |
|
59
|
|
|
private function getStorageDir(): string |
|
60
|
|
|
{ |
|
61
|
|
|
$storage_dir = __DIR__ . '/storage'; |
|
62
|
|
|
if (!is_dir($storage_dir)) { |
|
63
|
|
|
if (false == mkdir($storage_dir)) { |
|
|
|
|
|
|
64
|
|
|
TestServerApp::error_exit(500, "Error creating storage directory: $storage_dir"); |
|
65
|
|
|
} |
|
66
|
|
|
} |
|
67
|
|
|
|
|
68
|
|
|
return $storage_dir; |
|
69
|
|
|
} |
|
70
|
|
|
|
|
71
|
|
|
/** |
|
72
|
|
|
* @return Tiqr_Service |
|
73
|
|
|
*/ |
|
74
|
|
|
private function createTiqrService($host, $authProtocol, $enrollProtocol) |
|
75
|
|
|
{ |
|
76
|
|
|
// Derive the identifier from the host |
|
77
|
|
|
$identifier = parse_url($host, PHP_URL_HOST); |
|
78
|
|
|
$storage_dir = $this->getStorageDir(); |
|
|
|
|
|
|
79
|
|
|
|
|
80
|
|
|
$tiqr_service = new Tiqr_Service( |
|
81
|
|
|
[ |
|
82
|
|
|
'auth.protocol' => $authProtocol, |
|
83
|
|
|
'enroll.protocol' => $enrollProtocol, |
|
84
|
|
|
'ocra.suite' => 'OCRA-1:HOTP-SHA1-6:QH10-S', |
|
85
|
|
|
'identifier' => $identifier, |
|
86
|
|
|
'name' => "TestServerController $host", |
|
87
|
|
|
'logoUrl' => "$host/logoUrl", |
|
88
|
|
|
'infoUrl' => "$host/infoUrl", |
|
89
|
|
|
|
|
90
|
|
|
// 'phpqrcode.path' |
|
91
|
|
|
// 'apns.path' |
|
92
|
|
|
// 'apns.certificate' |
|
93
|
|
|
'apns.environment' => 'sandbox', |
|
94
|
|
|
|
|
95
|
|
|
'c2dm.username' => 'test_c2dm_username', |
|
96
|
|
|
'c2dm.password' => 'test_c2dm_password', |
|
97
|
|
|
'c2dm.application' => 'org.example.authenticator.test', |
|
98
|
|
|
|
|
99
|
|
|
// Session storage, always stored in /tmp/tiqr_state_* |
|
100
|
|
|
'statestorage' => array( |
|
101
|
|
|
'type' => 'file', |
|
102
|
|
|
), |
|
103
|
|
|
|
|
104
|
|
|
// Token exchange configuration |
|
105
|
|
|
'devicestorage' => array( |
|
106
|
|
|
'type' => 'dummy', |
|
107
|
|
|
), |
|
108
|
|
|
] |
|
109
|
|
|
); |
|
110
|
|
|
|
|
111
|
|
|
return $tiqr_service; |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
private function createUserStorage() |
|
115
|
|
|
{ |
|
116
|
|
|
$storage_dir = $this->getStorageDir(); |
|
117
|
|
|
$options = array( |
|
118
|
|
|
'type' => 'file', |
|
119
|
|
|
'path' => $storage_dir, |
|
120
|
|
|
); |
|
121
|
|
|
$secretoptions = array( |
|
122
|
|
|
'type' => 'file', |
|
123
|
|
|
'path' => $storage_dir, |
|
124
|
|
|
); |
|
125
|
|
|
return $userStorage = Tiqr_UserStorage::getStorage( |
|
|
|
|
|
|
126
|
|
|
'file', |
|
127
|
|
|
$options, |
|
128
|
|
|
$secretoptions |
|
129
|
|
|
); |
|
130
|
|
|
} |
|
131
|
|
|
|
|
132
|
|
|
|
|
133
|
|
|
public function Route(App $app, string $path) |
|
134
|
|
|
{ |
|
135
|
|
|
$view = new TestServerView(); |
|
136
|
|
|
|
|
137
|
|
|
$app::log_info("host_url=$this->host_url"); |
|
138
|
|
|
switch ($path) { |
|
139
|
|
|
case "/": // Test server home page |
|
140
|
|
|
$view->ShowRoot(); |
|
141
|
|
|
break; |
|
142
|
|
|
|
|
143
|
|
|
case "/list-users": // page showing currently enrolled user accounts |
|
144
|
|
|
$this->list_users($app, $view); |
|
145
|
|
|
break; |
|
146
|
|
|
|
|
147
|
|
|
// Enrollment |
|
148
|
|
|
case "/start-enrollment": // Show enroll page to user |
|
149
|
|
|
$this->start_enrollment($app, $view); |
|
150
|
|
|
break; |
|
151
|
|
|
case "/metadata": // tiqr client gets metadata |
|
152
|
|
|
$this->metadata($app); |
|
153
|
|
|
break; |
|
154
|
|
|
case "/finish-enrollment": // tiqr client posts secret |
|
155
|
|
|
$this->finish_enrollment($app); |
|
156
|
|
|
break; |
|
157
|
|
|
|
|
158
|
|
|
// Render a QR code |
|
159
|
|
|
case "/qr": // used from start-enrollment and start_authenticate views |
|
160
|
|
|
$this->qr($app); |
|
161
|
|
|
break; |
|
162
|
|
|
|
|
163
|
|
|
// Serve test logo |
|
164
|
|
|
case "/logoUrl": // used by tiqr client to download logo, included in metadata |
|
165
|
|
|
$this->logo($app); |
|
166
|
|
|
break; |
|
167
|
|
|
// case "infoUrl": // user in metadata |
|
168
|
|
|
|
|
169
|
|
|
// Authentication |
|
170
|
|
|
case "/start-authenticate": // Show authenticate page to user |
|
171
|
|
|
$this->start_authenticate($app, $view); |
|
172
|
|
|
break; |
|
173
|
|
|
case "/authentication": // tiqr client posts back response |
|
174
|
|
|
$this->authentication($app); |
|
175
|
|
|
break; |
|
176
|
|
|
|
|
177
|
|
|
default: |
|
178
|
|
|
TestServerApp::error_exit(404, "Unknown route '$path'"); |
|
179
|
|
|
} |
|
180
|
|
|
} |
|
181
|
|
|
|
|
182
|
|
|
private function start_enrollment(App $app, TestServerView $view) |
|
183
|
|
|
{ |
|
184
|
|
|
// The session ID is used for communicating enrollment status between this tiqr server and |
|
185
|
|
|
// the web browser displaying the enrollment interface. It is not used between the tiqr client and |
|
186
|
|
|
// this server. We do not use it. |
|
187
|
|
|
$session_id = 'session_id_' . time(); |
|
188
|
|
|
$app::log_info("Created session $session_id"); |
|
189
|
|
|
|
|
190
|
|
|
// The user_id to create. Get it from the request, if it is not there use a test user ID. |
|
191
|
|
|
$user_id = $app->getGET()['user_id'] ?? 'test-user-' . time(); |
|
192
|
|
|
|
|
193
|
|
|
if ($this->userStorage->userExists($user_id)) { |
|
194
|
|
|
$app::log_warning("$user_id already exists"); |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
$user_display_name = $user_id . '\'s display name'; |
|
198
|
|
|
|
|
199
|
|
|
// Create enrollemnt key. The display name we set here is returned in the metadata generated by |
|
200
|
|
|
// getEnrollmentMetadata. |
|
201
|
|
|
// Note: we create the user in the userStorage later with a different display name so the displayname in the |
|
202
|
|
|
// App differs from the user's displayname on the server. |
|
203
|
|
|
$enrollment_key = $this->tiqrService->startEnrollmentSession($user_id, $user_display_name, $session_id); |
|
204
|
|
|
$app::log_info("Started enrollment session $enrollment_key"); |
|
205
|
|
|
$metadataUrl = $this->host_url . "/metadata"; |
|
206
|
|
|
$enroll_string = $this->tiqrService->generateEnrollString($metadataUrl) . "?enrollment_key=$enrollment_key"; |
|
207
|
|
|
$encoded_enroll_string = htmlentities(urlencode($enroll_string)); |
|
208
|
|
|
$image_url = "/qr?code=" . $encoded_enroll_string; |
|
209
|
|
|
|
|
210
|
|
|
$view->StartEnrollment(htmlentities($enroll_string), $image_url); |
|
211
|
|
|
} |
|
212
|
|
|
|
|
213
|
|
|
// Generate a png image QR code with whatever string is given in the code HTTP request parameter. |
|
214
|
|
|
private function qr(App $app): void |
|
215
|
|
|
{ |
|
216
|
|
|
$code = $app->getGET()['code'] ?? ''; |
|
217
|
|
|
if (strlen($code) == 0) { |
|
218
|
|
|
// http://<server>/qr?code=<the string to encode> |
|
219
|
|
|
$app::error_exit(404, "qr: 'code' request parameter not set"); |
|
220
|
|
|
} |
|
221
|
|
|
$this->tiqrService->generateQR($code); |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
// In response to scanning the enrollment QR code the tiqr client makes a GET request to the URL in |
|
225
|
|
|
// the QR code. |
|
226
|
|
|
// This URL is exactly what we put in the QR code we generated in start_enrollment() : |
|
227
|
|
|
// <host_url>/metadata?enrollment_key=<enrollment_key> |
|
228
|
|
|
private function metadata(App $app) |
|
229
|
|
|
{ |
|
230
|
|
|
// The enrollment key must be there, it binds the user's browser session to the request comming from |
|
231
|
|
|
// the user's tiqr client |
|
232
|
|
|
$enrollment_key = $app->getGET()['enrollment_key'] ?? ''; |
|
233
|
|
|
if (strlen($enrollment_key) == 0) { |
|
234
|
|
|
// http://<server>/metadata?enrollment_key=<the enrollment key from the QR code> |
|
235
|
|
|
$app::error_exit(404, "metadata: 'enrollment_key' request parameter not set"); |
|
236
|
|
|
} |
|
237
|
|
|
// Generate an enrollment secret to add to the metadata URL |
|
238
|
|
|
$enrollment_secret = $this->tiqrService->getEnrollmentSecret($enrollment_key); |
|
239
|
|
|
$app::log_info("Created enrollment secret $enrollment_secret for enrollment key $enrollment_key"); |
|
240
|
|
|
|
|
241
|
|
|
// -----BEGIN NOTE----- |
|
242
|
|
|
// The enrollment_secret must be added manually to the enrollment URL. |
|
243
|
|
|
// I do not see why getEnrollmentMetadata cannot handle this for us en return the enrollment_secret |
|
244
|
|
|
// to the tiqr client as part of the $enrollment_metadata. That would make the call to getEnrollmentSecret() |
|
245
|
|
|
// redundant en the gereration of the metadata simpler. The Tiqr client could then return the enrollment_secret |
|
246
|
|
|
// in it's POST back to the $enrollment_url. |
|
247
|
|
|
// -----END NOTE----- |
|
248
|
|
|
|
|
249
|
|
|
// Add enrollment_secret to the $enrollment_url |
|
250
|
|
|
$enrollment_url = $this->host_url . "/finish-enrollment?enrollment_secret=$enrollment_secret"; |
|
251
|
|
|
$authentication_url = $this->host_url . '/authentication'; |
|
252
|
|
|
// Get the enrollment data |
|
253
|
|
|
$enrollment_metadata = $this->tiqrService->getEnrollmentMetadata($enrollment_key, $authentication_url, $enrollment_url); |
|
254
|
|
|
if (false == $enrollment_metadata) { |
|
255
|
|
|
// Happens when calling metadata with the same enrollment key a second time |
|
256
|
|
|
$app::error_exit(500, 'Error getting enrollment metadata'); |
|
257
|
|
|
} |
|
258
|
|
|
// Print the generated enrollment data in a more readable form |
|
259
|
|
|
foreach ($enrollment_metadata as $key1 => $value1) { |
|
260
|
|
|
if (is_array($value1)) { |
|
261
|
|
|
foreach ($value1 as $key2 => $value2) { |
|
262
|
|
|
$app::log_info("Metadata: $key1/$key2=$value2"); |
|
263
|
|
|
} |
|
264
|
|
|
} |
|
265
|
|
|
else { |
|
266
|
|
|
$app::log_info("Metadata: $key1=$value1"); |
|
267
|
|
|
} |
|
268
|
|
|
} |
|
269
|
|
|
// The enrollment metadata must be returned to the client as JSON |
|
270
|
|
|
$enrollment_metadata_json = json_encode($enrollment_metadata); |
|
271
|
|
|
$app::log_info("Return: $enrollment_metadata_json"); |
|
272
|
|
|
header("content-type: application/json"); |
|
273
|
|
|
echo $enrollment_metadata_json; |
|
274
|
|
|
} |
|
275
|
|
|
|
|
276
|
|
|
// After receiving the enrollment metadata the tiqr client generates a secret key and posts |
|
277
|
|
|
// it to the enrollment URL we specified in the metadata together with the information required for sending |
|
278
|
|
|
// it push notification and the user's language preference. |
|
279
|
|
|
private function finish_enrollment(App $app) |
|
280
|
|
|
{ |
|
281
|
|
|
$enrollment_secret = $app->getGET()['enrollment_secret'] ?? ''; |
|
282
|
|
|
if (strlen($enrollment_secret) == 0) { |
|
283
|
|
|
// http://<server>/finish_enrollment?enrollment_secret=<the enrollment secret from the metadata> |
|
284
|
|
|
$app::error_exit(404, "enrollment: 'enrollment_secret' request parameter not set"); |
|
285
|
|
|
} |
|
286
|
|
|
// Validate the enrollment secret we were sent. In return we get the userid back that we set in |
|
287
|
|
|
// start_enrollment using startEnrollmentSession |
|
288
|
|
|
$userid = $this->tiqrService->validateEnrollmentSecret($enrollment_secret); |
|
289
|
|
|
if (false === $userid) { |
|
290
|
|
|
$app::error_exit(404, "Invalid enrollment_secret"); |
|
291
|
|
|
} |
|
292
|
|
|
$app::log_info("userid: $userid"); |
|
293
|
|
|
|
|
294
|
|
|
$secret = $app->getPOST()['secret'] ?? ''; |
|
295
|
|
|
if (strlen($secret) == 0) { |
|
296
|
|
|
$app::error_exit(404, "Missing secret is POST"); |
|
297
|
|
|
} |
|
298
|
|
|
// This is the hex encoded value of the authentication secret that the tiqr client |
|
299
|
|
|
// generated |
|
300
|
|
|
$app::log_info("secret: $secret"); |
|
301
|
|
|
|
|
302
|
|
|
$language = $app->getPOST()['language'] ?? ''; |
|
303
|
|
|
if (strlen($language) == 0) { |
|
304
|
|
|
$app::log_warning("No language in POST"); |
|
305
|
|
|
} |
|
306
|
|
|
// The iso language code e.g. "nl-NL" |
|
307
|
|
|
$app::log_info("language: $language"); |
|
308
|
|
|
|
|
309
|
|
|
$notificationType = $app->getPOST()['notificationType'] ?? ''; |
|
310
|
|
|
if (strlen($notificationType) == 0) { |
|
311
|
|
|
$app::log_warning("No notificationType in POST"); |
|
312
|
|
|
} |
|
313
|
|
|
// The notification message type (APNS, GCM, FCM ...) |
|
314
|
|
|
$app::log_info("notificationType: $notificationType"); |
|
315
|
|
|
|
|
316
|
|
|
$notificationAddress = $app->getPOST()['notificationAddress'] ?? ''; |
|
317
|
|
|
if (strlen($notificationAddress) == 0) { |
|
318
|
|
|
$app::log_warning("No notificationAddress in POST"); |
|
319
|
|
|
} |
|
320
|
|
|
// This is the notification address that the Tiqr Client got from the token exchange (e.g. tx.tiqr.org) |
|
321
|
|
|
$app::log_info("notificationAddress: $notificationAddress"); |
|
322
|
|
|
|
|
323
|
|
|
$version = $app->getPOST()['version'] ?? ''; |
|
324
|
|
|
if (strlen($version) == 0) { |
|
325
|
|
|
$app::log_warning("No version in POST"); |
|
326
|
|
|
} |
|
327
|
|
|
// ? |
|
328
|
|
|
$app::log_info("version: $version"); |
|
329
|
|
|
|
|
330
|
|
|
$operation = $app->getPOST()['operation'] ?? ''; |
|
331
|
|
|
if (strlen($operation) == 0) { |
|
332
|
|
|
$app::log_warning("No operation in POST"); |
|
333
|
|
|
} |
|
334
|
|
|
// Must be "register" |
|
335
|
|
|
$app::log_info("operation: $operation"); |
|
336
|
|
|
if ($operation != 'register') { |
|
337
|
|
|
$app::error_exit(404, "Invalid operation: '$operation'. Expected 'register'"); |
|
338
|
|
|
} |
|
339
|
|
|
|
|
340
|
|
|
// Get User-Agent HTTP header |
|
341
|
|
|
$user_agent = urldecode($_SERVER['HTTP_USER_AGENT'] ?? ''); |
|
342
|
|
|
$app::log_info("User-Agent: $user_agent"); |
|
343
|
|
|
|
|
344
|
|
|
// Create the user. Use the display name to store the version the client POSTed and the user-agent it sent |
|
345
|
|
|
// in this POST request's header. |
|
346
|
|
|
$this->userStorage->createUser($userid, "$version | $user_agent"); |
|
347
|
|
|
$app::log_info("Created user $userid"); |
|
348
|
|
|
|
|
349
|
|
|
// Set the user secret |
|
350
|
|
|
$this->userStorage->setSecret($userid, $secret); |
|
351
|
|
|
$app::log_info("Secret for $userid was stored"); |
|
352
|
|
|
|
|
353
|
|
|
// Store notification type and the notification address that the client sent us |
|
354
|
|
|
$this->userStorage->setNotificationType($userid, $notificationType); |
|
355
|
|
|
$this->userStorage->setNotificationAddress($userid, $notificationAddress); |
|
356
|
|
|
|
|
357
|
|
|
// Finalize the enrollemnt |
|
358
|
|
|
$this->tiqrService->finalizeEnrollment($enrollment_secret); |
|
359
|
|
|
$app::log_info("Enrollment was finalized"); |
|
360
|
|
|
|
|
361
|
|
|
// Must return "OK" to the tiqr client after a successful enrollment |
|
362
|
|
|
echo "OK"; |
|
363
|
|
|
} |
|
364
|
|
|
|
|
365
|
|
|
private function logo(App $app) |
|
|
|
|
|
|
366
|
|
|
{ |
|
367
|
|
|
// Source: https://nl.wikipedia.org/wiki/Bestand:Philips_PM5544.svg |
|
368
|
|
|
$name = __DIR__ . '/Philips_PM5544.jpg'; |
|
369
|
|
|
$fp = fopen($name, 'rb'); |
|
370
|
|
|
header('Content-Type: image/jpeg'); |
|
371
|
|
|
header('Content-Length: ' . filesize($name)); |
|
372
|
|
|
fpassthru($fp); |
|
373
|
|
|
fclose($fp); |
|
374
|
|
|
} |
|
375
|
|
|
|
|
376
|
|
|
function list_users(App $app, TestServerView $view) |
|
|
|
|
|
|
377
|
|
|
{ |
|
378
|
|
|
$storageDir = $this->getStorageDir(); |
|
379
|
|
|
$users = array(); |
|
380
|
|
|
foreach (scandir($storageDir) as $filename) { |
|
381
|
|
|
if (substr($filename, -5, 5) == '.json') { |
|
382
|
|
|
$user = json_decode(file_get_contents($storageDir . '/' . $filename), true); |
|
383
|
|
|
if (($user != NULL) && ($user['secret'])) { |
|
384
|
|
|
foreach ($user as $k => $v) { |
|
385
|
|
|
$user[$k] = htmlentities($v); |
|
386
|
|
|
} |
|
387
|
|
|
$users[] = $user; |
|
388
|
|
|
} |
|
389
|
|
|
} |
|
390
|
|
|
} |
|
391
|
|
|
$view->ListUsers($users); |
|
392
|
|
|
} |
|
393
|
|
|
|
|
394
|
|
|
private function start_authenticate(App $app, TestServerView $view) |
|
395
|
|
|
{ |
|
396
|
|
|
$session_id = 'session_id_' . time(); |
|
397
|
|
|
$app::log_info("Created session $session_id"); |
|
398
|
|
|
|
|
399
|
|
|
// The user_id to authenticate. Get it from the request, if it is not there use an empty user ID |
|
400
|
|
|
// Both scenario's are support by tiqr: |
|
401
|
|
|
// 1. No user-id in the authentication url. This is a login scenario. The tiqr client selects the user id |
|
402
|
|
|
// 2. Specify the user-is in the authentication url. This is the step-up scenario. The tiqr server specifies |
|
403
|
|
|
// the user-id |
|
404
|
|
|
|
|
405
|
|
|
// Get optional user ID |
|
406
|
|
|
$user_id = $app->getGET()['user_id'] ?? ''; |
|
407
|
|
|
if (strlen($user_id) > 0) { |
|
408
|
|
|
$app::log_info("Authenticating user '$user_id'"); |
|
409
|
|
|
} |
|
410
|
|
|
|
|
411
|
|
|
if (!$this->userStorage->userExists($user_id)) { |
|
412
|
|
|
$app::log_warning("'$user_id' is not known on the server"); |
|
413
|
|
|
} |
|
414
|
|
|
|
|
415
|
|
|
// Start authentication session |
|
416
|
|
|
$session_key = $this->tiqrService->startAuthenticationSession($user_id, $session_id); |
|
417
|
|
|
$app::log_info('Started authentication session'); |
|
418
|
|
|
$app::log_info("session_key=$session_key"); |
|
419
|
|
|
|
|
420
|
|
|
// Get authentication URL for the tiqr client (to put in the QR code) |
|
421
|
|
|
$authentication_URL = $this->tiqrService->generateAuthURL($session_key); |
|
422
|
|
|
$app::log_info('Started authentication URL'); |
|
423
|
|
|
$app::log_info("authentication_url=$authentication_URL"); |
|
424
|
|
|
|
|
425
|
|
|
$image_url = "/qr?code=" . htmlentities(urlencode($authentication_URL)); |
|
|
|
|
|
|
426
|
|
|
|
|
427
|
|
|
$response = ''; |
|
428
|
|
|
if (strlen($user_id) > 0) { |
|
429
|
|
|
// Calculate response |
|
430
|
|
|
$app::log_info("Calculating response for $user_id"); |
|
431
|
|
|
$secret = $this->userStorage->getSecret($user_id); |
|
432
|
|
|
$app::log_info("secret=$secret"); |
|
433
|
|
|
$exploded = explode('/', $authentication_URL); |
|
|
|
|
|
|
434
|
|
|
$session_key = $exploded[3]; // hex encoded session |
|
435
|
|
|
$challenge = $exploded[4]; // 10 digit hex challenge |
|
436
|
|
|
$app::log_info("challenge=$challenge"); |
|
437
|
|
|
$ocra = new Tiqr_OCRAWrapper('OCRA-1:HOTP-SHA1-6:QH10-S'); |
|
438
|
|
|
$response = $ocra->calculateResponse($secret, $challenge, $session_key); |
|
439
|
|
|
$app::log_info("response=$response"); |
|
440
|
|
|
} |
|
441
|
|
|
|
|
442
|
|
|
$view->StartAuthenticate(htmlentities($authentication_URL), $image_url, $user_id, $response); |
|
|
|
|
|
|
443
|
|
|
} |
|
444
|
|
|
|
|
445
|
|
|
private function authentication(App $app) |
|
446
|
|
|
{ |
|
447
|
|
|
// This should be the session key from the authentication URL that we generated |
|
448
|
|
|
$sessionKey = $app->getPOST()['sessionKey'] ?? ''; |
|
449
|
|
|
if (strlen($sessionKey) == 0) { |
|
450
|
|
|
$app::error_exit(404, "Missing sessionKey is POST"); |
|
451
|
|
|
} |
|
452
|
|
|
$app::log_info("sessionKey: $sessionKey"); |
|
453
|
|
|
|
|
454
|
|
|
// The userId the client authenticated |
|
455
|
|
|
$userId = $app->getPOST()['userId'] ?? ''; |
|
456
|
|
|
if (strlen($userId) == 0) { |
|
457
|
|
|
$app::error_exit(404, "Missing $userId is POST"); |
|
458
|
|
|
} |
|
459
|
|
|
$app::log_info("userId: $userId"); |
|
460
|
|
|
|
|
461
|
|
|
// Get version from POST |
|
462
|
|
|
$version = $app->getPOST()['version'] ?? ''; |
|
463
|
|
|
if (strlen($version) == 0) { |
|
464
|
|
|
$app::log_warning("No version in POST"); |
|
465
|
|
|
} |
|
466
|
|
|
// ? |
|
467
|
|
|
$app::log_info("version: $version"); |
|
468
|
|
|
|
|
469
|
|
|
// Get operation from POST |
|
470
|
|
|
$operation = $app->getPOST()['operation'] ?? ''; |
|
471
|
|
|
if (strlen($operation) == 0) { |
|
472
|
|
|
$app::log_warning("No operation in POST"); |
|
473
|
|
|
} |
|
474
|
|
|
// Must be "login" |
|
475
|
|
|
$app::log_info("operation: $operation"); |
|
476
|
|
|
if ($operation != 'login') { |
|
477
|
|
|
$app::error_exit(404, "Invalid operation: '$operation'. Expected 'login'"); |
|
478
|
|
|
} |
|
479
|
|
|
|
|
480
|
|
|
// Get response from POST |
|
481
|
|
|
$response = $app->getPOST()['response'] ?? ''; |
|
482
|
|
|
if (strlen($response) == 0) { |
|
483
|
|
|
$app::log_warning("No response in POST"); |
|
484
|
|
|
} |
|
485
|
|
|
$app::log_info("response: $response"); |
|
486
|
|
|
|
|
487
|
|
|
$language = $app->getPOST()['language'] ?? ''; |
|
488
|
|
|
if (strlen($language) == 0) { |
|
489
|
|
|
$app::log_warning("No language in POST"); |
|
490
|
|
|
} |
|
491
|
|
|
// The iso language code e.g. "nl-NL" |
|
492
|
|
|
$app::log_info("language: $language"); |
|
493
|
|
|
|
|
494
|
|
|
$notificationType = $app->getPOST()['notificationType'] ?? ''; |
|
495
|
|
|
if (strlen($notificationType) == 0) { |
|
496
|
|
|
$app::log_warning("No notificationType in POST"); |
|
497
|
|
|
} |
|
498
|
|
|
// The notification message type (APNS, GCM, FCM ...) |
|
499
|
|
|
$app::log_info("notificationType: $notificationType"); |
|
500
|
|
|
|
|
501
|
|
|
$notificationAddress = $app->getPOST()['notificationAddress'] ?? ''; |
|
502
|
|
|
if (strlen($notificationAddress) == 0) { |
|
503
|
|
|
$app::log_warning("No notificationAddress in POST"); |
|
504
|
|
|
} |
|
505
|
|
|
// This is the notification address that the Tiqr Client got from the token exchange (e.g. tx.tiqr.org) |
|
506
|
|
|
$app::log_info("notificationAddress: $notificationAddress"); |
|
507
|
|
|
|
|
508
|
|
|
// Get User-Agent HTTP header |
|
509
|
|
|
$user_agent = urldecode($_SERVER['HTTP_USER_AGENT'] ?? ''); |
|
510
|
|
|
$app::log_info("User-Agent: $user_agent"); |
|
511
|
|
|
|
|
512
|
|
|
$result = 'ERROR'; // Result for the tiqr Client |
|
513
|
|
|
// 'OK', 'INVALID_CHALLENGE', 'INVALID_REQUEST', 'INVALID_RESPONSE', 'INVALID_USER' |
|
514
|
|
|
|
|
515
|
|
|
if (!$this->userStorage->userExists($userId)) { |
|
516
|
|
|
$app::log_error("Unknown user: $userId "); |
|
517
|
|
|
$result = 'INVALID_USER'; |
|
|
|
|
|
|
518
|
|
|
} |
|
519
|
|
|
|
|
520
|
|
|
// Lookup the secret of the user by ID |
|
521
|
|
|
$userSecret = $this->userStorage->getSecret($userId); // Assume this works |
|
522
|
|
|
$app::log_info("userSercret=$userSecret"); |
|
523
|
|
|
|
|
524
|
|
|
$app::log_info("Authenticating user"); |
|
525
|
|
|
$result = $this->tiqrService->authenticate($userId, $userSecret, $sessionKey, $response); |
|
526
|
|
|
$resultStr = 'ERROR'; |
|
527
|
|
|
switch ($result) { |
|
528
|
|
|
case Tiqr_Service::AUTH_RESULT_INVALID_REQUEST: |
|
529
|
|
|
$resultStr = "INVALID_REQUEST"; |
|
530
|
|
|
break; |
|
531
|
|
|
case Tiqr_Service::AUTH_RESULT_AUTHENTICATED: |
|
532
|
|
|
$resultStr = "OK"; |
|
533
|
|
|
break; |
|
534
|
|
|
case Tiqr_Service::AUTH_RESULT_INVALID_RESPONSE: |
|
535
|
|
|
$resultStr = "INVALID_RESPONSE"; |
|
536
|
|
|
break; |
|
537
|
|
|
case Tiqr_Service::AUTH_RESULT_INVALID_CHALLENGE: |
|
538
|
|
|
$resultStr = "INVALID_CHALLENGE"; |
|
539
|
|
|
break; |
|
540
|
|
|
case Tiqr_Service::AUTH_RESULT_INVALID_USERID: |
|
541
|
|
|
$resultStr = "INVALID_USER"; |
|
542
|
|
|
break; |
|
543
|
|
|
} |
|
544
|
|
|
|
|
545
|
|
|
$app::log_info("Returning authentication result '$resultStr'"); |
|
546
|
|
|
echo $resultStr; |
|
547
|
|
|
} |
|
548
|
|
|
} |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths