From 1e4e4081f3d8c82d6cfc01775fc2886f8dc865da Mon Sep 17 00:00:00 2001
From: Pieter van der Meulen
Date: Sun, 4 Aug 2024 17:17:34 +0200
Subject: [PATCH] Condense user-list and add enroll- and authentication status
views
* User list: Make the user-list less wide by moving the secret to the authenticate screen and limiting the PN adress to 20 characters.
* User list: Sort user list in descending order so newest registered users are shown on top
* Add status vieuws: Add views to show status of the authentication and of the enrollment
* Add links to quickly resent push notifications and retstart a authentication
* Show authentication and enrollment timeouts in the UI.
---
TestServer/TestServerController.php | 130 +++++++++++++++++++-----
TestServer/TestServerView.php | 149 ++++++++++++++++++++++++----
2 files changed, 235 insertions(+), 44 deletions(-)
diff --git a/TestServer/TestServerController.php b/TestServer/TestServerController.php
index ee202b8..29cecce 100644
--- a/TestServer/TestServerController.php
+++ b/TestServer/TestServerController.php
@@ -252,9 +252,12 @@ public function Route(App $app, string $path)
case "/finish-enrollment": // tiqr client posts secret
$this->finish_enrollment($app);
break;
+ case '/get-enrollment-status': // Use session to check on enrollment status
+ $this->get_enrollment_status($app, $view);
+ break;
// Render a QR code
- case "/qr": // used from start-enrollment and start_authenticate views
+ case "/qr": // used from StartEnrollment and StartAuthenticate vieuws
$this->qr($app);
break;
@@ -268,16 +271,17 @@ public function Route(App $app, string $path)
case "/start-authenticate": // Show authenticate page to user
$this->start_authenticate($app, $view);
break;
-
- // Send push notification
case "/send-push-notification":
$this->send_push_notification($app, $view);
break;
-
- case "/authentication": // tiqr client posts back response
+ case "/authentication": // tiqr client posts back authentication response
$this->authentication($app);
break;
+ case '/get-authentication-status': // Use session to check on authentication status
+ $this->get_authentication_status($app, $view);
+ break;
+ // Configuration
case '/show-logs':
$this->show_logs($view);
break;
@@ -301,13 +305,15 @@ public function Route(App $app, string $path)
}
}
+ // Start a new enrollment
+ // user_id: (optional) user_id for the new user. If not specified a new user_id is created
private function start_enrollment(App $app, TestServerView $view)
{
// The session ID is used for communicating enrollment status between this tiqr server and
// the web browser displaying the enrollment interface. It is not used between the tiqr client and
- // this server. We do not use it.
- $session_id = 'session_id_' . time();
- $this->logger->info("Created session $session_id");
+ // this server.
+ $session_id = uniqid('session_id_' . time());
+ $this->logger->info("Created enrollment session_id $session_id");
// The user_id to create. Get it from the request, if it is not there use a test user ID.
$user_id = $app->getGET()['user_id'] ?? 'test-user-' . time();
@@ -318,18 +324,16 @@ private function start_enrollment(App $app, TestServerView $view)
$user_display_name = $user_id . '\'s display name';
- // Create enrollemnt key. The display name we set here is returned in the metadata generated by
+ // Create enrollment key. The display name we set here is returned in the metadata generated by
// getEnrollmentMetadata.
// Note: we create the user in the userStorage later with a different display name so the displayname in the
// App differs from the user's displayname on the server.
$enrollment_key = $this->tiqrService->startEnrollmentSession($user_id, $user_display_name, $session_id);
- $this->logger->info("Started enrollment session $enrollment_key");
+ $this->logger->info("Started enrollment session with enrollment_key=$enrollment_key");
$metadataUrl = $this->host_url . "/metadata";
$enroll_string = $this->tiqrService->generateEnrollString("$metadataUrl?enrollment_key=$enrollment_key");
- $encoded_enroll_string = htmlentities(urlencode($enroll_string));
- $image_url = "/qr?code=" . $encoded_enroll_string;
- $view->StartEnrollment(htmlentities($enroll_string), $image_url);
+ $view->StartEnrollment($enroll_string, $user_id, $session_id);
}
// Generate a png image QR code with whatever string is given in the code HTTP request parameter.
@@ -486,6 +490,22 @@ private function finish_enrollment(App $app)
echo "OK";
}
+
+ // Get the status of the enrollment session
+ // session_id: (required) the applications enrollment session_id
+ private function get_enrollment_status(App $app, TestServerView $view)
+ {
+ $session_id = $app->getGET()['session_id'] ?? '';
+ if (strlen($session_id) == 0) {
+ $app::error_exit(404, "get_enrollment_status: 'session_id' request parameter not set");
+ }
+
+ $status = $this->tiqrService->getEnrollmentStatus($session_id); // May throw
+ $this->logger->info("Enrollment status for session_id $session_id: $status");
+
+ $view->ShowEnrollmentStatus($status, $session_id);
+ }
+
private function logo(App $app)
{
// Source: https://nl.wikipedia.org/wiki/Bestand:Philips_PM5544.svg
@@ -512,13 +532,23 @@ function list_users(App $app, TestServerView $view)
}
}
}
+
+ // Reverse-Sort users by user ID
+ usort($users, function ($a, $b) {
+ return strcmp($b['userId'], $a['userId']);
+ });
+
$view->ListUsers($users);
}
+ // Start authentication for a user
+ // user_id: (optional) user ID to authenticate. If not set the tiqr client will select the user ID
private function start_authenticate(App $app, TestServerView $view)
{
- $session_id = 'session_id_' . time();
- $this->logger->info("Created session $session_id");
+ // Create a unique session ID for the authentication session
+ // It can later be used to check for the authentication status using getAuthenticatedUser()
+ $session_id = uniqid('session_id_' . time() );
+ $this->logger->info("Starting authentication session");
// The user_id to authenticate. Get it from the request, if it is not there use an empty user ID
// Both scenario's are support by tiqr:
@@ -536,20 +566,27 @@ private function start_authenticate(App $app, TestServerView $view)
}
}
-
- // Start authentication session
+ // Start authentication session. This will return a session_key that is communicated to through the Tiqr client
+ // embedded in a uri that is sent to the client either using a QR code or a push notification or by opening the
+ // uri on the device where the tiqr client (App) is running.
+ // Note that the $session_key != the $auth_session_id:
+ // - session_key: used between tiqr client and tiqr server library
+ // - auth_session_id: used between tiqr server library and the application
$session_key = $this->tiqrService->startAuthenticationSession($user_id, $session_id);
$this->logger->info('Started authentication session');
- $this->logger->info("session_key=$session_key");
+ $this->logger->info("session_key=$session_key session_id=$session_id");
// Get authentication URL for the tiqr client (to put in the QR code)
$authentication_URL = $this->tiqrService->generateAuthURL($session_key);
$this->logger->info('Started authentication URL');
$this->logger->info("authentication_url=$authentication_URL");
- $image_url = "/qr?code=" . htmlentities(urlencode($authentication_URL));
-
+ // Authentication can be started for any user (i.e. user_id == '') or for a specific user, in which case
+ // user_id is set to the user to request authentication for. If we know the user_id we lookup the user to get
+ // its secret, get the challenge from the authenticationURL and calculate the response so we can show these in the
+ // UI for testing purposes.
$response = '';
+ $secret = '';
if (strlen($user_id) > 0) {
// Calculate response
$this->logger->info("Calculating response for $user_id");
@@ -568,15 +605,21 @@ private function start_authenticate(App $app, TestServerView $view)
$challenge = $exploded[4]; // 10 digit hex challenge
}
$this->logger->info("challenge=$challenge");
- $response=\OCRA::generateOCRA('OCRA-1:HOTP-SHA1-6:QH10-S', $secret, '', $challenge, '', $session_key, '');
+ // Assume the default OCRA suite is used
+ $response=\OCRA::generateOCRA(Tiqr_Service::DEFAULT_OCRA_SUITE, $secret, '', $challenge, '', $session_key, '');
$this->logger->info("response=$response");
}
- $view->StartAuthenticate(htmlentities($authentication_URL), $image_url, $user_id, $response, $session_key);
+ // $user_id, $response and $secret will only be set when when authenticating a specific user
+ $view->StartAuthenticate($authentication_URL, $user_id, $response, $session_key, $secret, $session_id);
}
+ // user_id: (required) The user to send the push notification to
+ // session_key: (required) The session_key of the authentication session
+ // session_id: (optional) The application's authentication session ID, used for the check authentication state option
private function send_push_notification(App $app, TestServerView $view) {
+ // Required to get
$user_id = $app->getGET()['user_id'] ?? '';
if (strlen($user_id) == 0) {
$app::error_exit(404, "Missing user_id in POST");
@@ -589,6 +632,9 @@ private function send_push_notification(App $app, TestServerView $view) {
}
$this->logger->info("session_key = $session_key");
+ // Optional session ID.
+ $session_id = $app->getGET()['session_id'] ?? '';
+
// Get Notification address and type from userid
$notificationType=$this->userStorage->getNotificationType($user_id);
$this->logger->info("notificationType = $notificationType");
@@ -615,7 +661,7 @@ private function send_push_notification(App $app, TestServerView $view) {
$this->tiqrService->sendAuthNotification($session_key, $notificationType, $deviceNotificationAddress);
$this->logger->info("Push notification sent");
- $view->PushResult("Sent $notificationType to $deviceNotificationAddress");
+ $view->PushResult("Sent $notificationType to $deviceNotificationAddress", $session_key, $user_id, $session_id);
}
@@ -765,6 +811,44 @@ private function authentication(App $app)
}
+ // session_id: (required) The application session_id to check the authentication status for
+ // user_id: (optional) The user_id to check the authentication status for
+ private function get_authentication_status(App $app, TestServerView $view)
+ {
+ $session_id
+ = $app->getGET()['session_id'] ?? '';
+ if (strlen($session_id) == 0) {
+ $app::error_exit(404, "Missing session_id in GET");
+ }
+ $this->logger->info("session_id = $session_id");
+
+ // User ID is optional, if set it is the user_id we expect to be authenticated
+ $user_id = $app->getGET()['user_id'] ?? '';
+ $this->logger->info("expected user_id = $user_id");
+
+ // Returns NULL when not authenticated, returns userid when authenticated
+ $status = $this->tiqrService->getAuthenticatedUser($session_id);
+ $statusMsg = '';
+ if ($status === NULL) {
+ $statusMsg = 'User not authenticated';
+ $this->logger->info($statusMsg);
+ }
+ else {
+ $user_id = $status; // $status holds the ID of the authenticated user
+ $statusMsg = "User $status was authenticated.";
+ $this->logger->info($statusMsg);
+ // Check if the user ID from the session matches the user ID from the GET request
+ // If the user_id was provided in the get request these are expected to match
+ if (strlen($user_id)>0 && $status != $user_id) {
+ $this->logger->warning("User ID from session ($status) does not match user ID from GET ($user_id)");
+ $statusMsg .= " Note: the provided user ID ('$status') does not match the authenticated user ID";
+ }
+ }
+
+ $view->ShowAuthenticationStatus($statusMsg, $session_id, $user_id);
+ }
+
+
private function show_logs(TestServerView $view)
{
$logFile = $this->getStorageDir() . '/' . $this->current_user . '.log';
diff --git a/TestServer/TestServerView.php b/TestServer/TestServerView.php
index 107df36..ec1d231 100644
--- a/TestServer/TestServerView.php
+++ b/TestServer/TestServerView.php
@@ -43,7 +43,6 @@ public function ListUsers($users) {
displayName (version | User-Agent) |
notificationType |
notificationAddress |
- secret |
HTML;
foreach ($users as $user) {
@@ -51,14 +50,18 @@ public function ListUsers($users) {
$user['displayName'] = $user['displayName'] ?? '—';
$user['notificationType'] = $user['notificationType'] ?? '—';
$user['notificationAddress'] = $user['notificationAddress'] ?? '—';
+ if (strlen($user['notificationAddress']) > 20) {
+ $user['notificationAddress'] = '' . substr($user['notificationAddress'], 0, 10) . '
...' . substr($user['notificationAddress'], -10) . '
(' . strlen($user['notificationAddress']) . ')';
+ } else {
+ $user['notificationAddress'] = ''.$user['notificationAddress'].'
';
+ }
$user['secret'] = $user['secret'] ?? '—';
echo <<
{$user['userId']} |
{$user['displayName']} |
{$user['notificationType']} |
- {$user['notificationAddress']} |
- {$user['secret']} |
+ {$user['notificationAddress']} |
HTML;
}
@@ -66,22 +69,85 @@ public function ListUsers($users) {
$this->end();
}
- public function StartEnrollment($enroll_string, $image_url) : void {
+ public function StartEnrollment(string $enroll_string, string $user_id, string $session_id) : void {
+ $image_url = "/qr?code=" . urlencode($enroll_string);
+ $expire = htmlentities(\Tiqr_Service::ENROLLMENT_EXPIRE);
+ $enroll_string = htmlentities($enroll_string);
+ $get_enrollment_status = '/get-enrollment-status?session_id=' . urlencode($session_id);
+ $user_id = htmlentities($user_id);
$this->begin();
echo <<Enroll a new user
-Scan the QR code below using the Tiqr app. When using the smart phone's browser you can tap on the QR code to open the link it contains.
-You can use this QR code only once.
+Enroll a new user $user_id
+Scan the QR code below using the Tiqr app to start the enrollment of a new user $user_id
. When using the smart phone's browser you can tap on the QR code to open the link it contains.
+You can use this QR code only once. You must complete the enrollment within $expire seconds.
$enroll_string
-Refresh enrollemnt QR code
+Refresh enrollemnt QR code
+Get enrollment status
HTML;
$this->end();
}
+ public function ShowEnrollmentStatus(string $status, string $session_id)
+ {
+ $statusmap = array(
+ \Tiqr_Service::ENROLLMENT_STATUS_IDLE => 'ENROLLMENT_STATUS_IDLE',
+ \Tiqr_Service::ENROLLMENT_STATUS_INITIALIZED => 'ENROLLMENT_STATUS_INITIALIZED',
+ \Tiqr_Service::ENROLLMENT_STATUS_RETRIEVED => 'ENROLLMENT_STATUS_RETRIEVED',
+ \Tiqr_Service::ENROLLMENT_STATUS_PROCESSED => 'ENROLLMENT_STATUS_PROCESSED',
+ \Tiqr_Service::ENROLLMENT_STATUS_FINALIZED => 'ENROLLMENT_STATUS_FINALIZED',
+ );
+ $statusdescriptionmap = array(
+ \Tiqr_Service::ENROLLMENT_STATUS_IDLE => 'There is no enrollment going on in this session, or there was an error getting the enrollment status',
+ \Tiqr_Service::ENROLLMENT_STATUS_INITIALIZED => 'The enrollment session was started, but the tiqr client has not retrieved the metadata yet',
+ \Tiqr_Service::ENROLLMENT_STATUS_RETRIEVED => 'The tiqr client has retrieved the metadata',
+ \Tiqr_Service::ENROLLMENT_STATUS_PROCESSED => 'The tiqr client has sent back the tiqr authentication secret',
+ \Tiqr_Service::ENROLLMENT_STATUS_FINALIZED => 'The server has stored the authentication secret',
+ );
+
+ $get_enrollment_status = '/get-enrollment-status?session_id=' . urlencode($session_id);
+
+ $this->begin();
+
+ echo <<Enrollment status
+
+Status: $status
+HTML;
+ if (!isset($statusmap[$status])) {
+ echo "ERROR: Unknown status code
";
+ }
+
+ echo "";
+ foreach ($statusmap as $statuscode => $statusconst) {
+ $statusdescription = $statusdescriptionmap[$statuscode];
+ if ($status == $statuscode) {
+ echo "- $statusconst ($statuscode): $statusdescription
";
+ } else {
+ echo "- $statusconst ($statuscode): $statusdescription
";
+ }
+ }
+ echo "
";
+
+ echo <<
Refresh enrollment status
+HTML;
+ $this->end();
+ }
+
+
+ public function ShowQRCode($code) {
+ $this->begin();
+ $codeHTML = htmlentities($code);
+ echo <<
@@ -110,42 +176,61 @@ private function end() {
HTML;
}
- public function StartAuthenticate(string $authentication_URL, string $image_url, string $user_id, string $response, string $session_key)
+ public function StartAuthenticate(string $authenticationURL, string $user_id, string $response, string $session_key, string $secret, string $auth_session_id)
{
- $refreshurl = '/start-authenticate';
+ // This view can handle both authenticating a known user and an unknown user
+ // If the user_id is empty, we're authenticating an unknown user
+ $refreshURL = '/start-authenticate';
+ $authenticationStatusURL = '/get-authentication-status?session_id=' . urlencode($auth_session_id);
if (strlen($user_id) > 0) {
- $refreshurl.= "?user_id=$user_id";
+ $refreshURL.= "?user_id=" . urlencode($user_id);
+ $authenticationStatusURL.= '&user_id=' . urlencode($user_id);
}
+ $sendPushNotificationURL = '/send-push-notification?user_id=' . urlencode($user_id) . '&session_key=' . urlencode($session_key) . '&session_id=' . urlencode($auth_session_id);
+ $image_url = "/qr?code=" . urlencode($authenticationURL);
+ $authenticationURLHTML=htmlentities($authenticationURL);
+ $authentication_timeout=htmlentities(\Tiqr_Service::CHALLENGE_EXPIRE);
+
$this->begin();
echo <<Authenticate user $user_id
Scan the QR code below using the Tiqr app. When using the smartphone's browser, you can tap on the QR code to open the link it contains instead of scanning it.
-This QR code is valid for a limited time.
-
+This QR code is valid for a limited time ($authentication_timeout seconds).
+
-$authentication_URL
+$authenticationURLHTML
HTML;
- if (strlen($response)>0) {
+ // We're authenticating a known user, so we can show the response and secret and offer to send a push notification
+ // to the user to start the authentication process
+ if (strlen($user_id)>0) {
+ $userIdHTML = htmlentities($user_id);
echo <<The correct OCRA response for this authentication (for offline validation) is: $response
-send push notification to the user
+The OCRA secret for this user is: $secret
+Send push notification to user $userIdHTML
HTML;
-
}
echo <<Refresh authentication session and QR code
+Check the authentication status of this session
-Refresh authentication QR code
HTML;
$this->end();
}
- function PushResult(string $notificationresult) {
+ function PushResult(string $notificationresult, string $session_key, string $user_id, string $session_id) {
$this->begin();
- $text = htmlentities($notificationresult);
+ $checkAuthenticationStatusURL = '/get-authentication-status?session_key=' . urlencode($session_key).'&user_id='.urlencode($user_id).'&session_id='.urlencode($session_id);
+ $sendPushNotificationURL = '/send-push-notification?user_id=' . urlencode($user_id) . '&session_key=' . urlencode($session_key).'&session_id='.urlencode($session_id);
+ $textHTML = htmlentities($notificationresult);
+ $userIdHTML = htmlentities($user_id);
echo <<$text
+$textHTML
+Check authentication status of user $userIdHTML
+Resend push notification for $userIdHTML
+Check authentication status
HTML;
$this->end();
}
@@ -181,6 +266,28 @@ public function Exception(string $path, \Exception $e)
$this->end();
}
+
+ public function ShowAuthenticationStatus(string $status, string $session_id, string $user_id)
+ {
+ $this->begin();
+ $statusHTML = htmlentities($status);
+ $userIdHTML = htmlentities($user_id);
+ $authenticationStatusURL = '/get-authentication-status?session_id=' . urlencode($session_id);
+ if (strlen($user_id) > 0) {
+ $authenticationStatusURL .= '&user_id=' . urlencode($user_id);
+ }
+ echo <<Authentication status
+Status: $statusHTML
+Refresh authentication status
+HTML;
+ if (strlen($user_id) > 0) {
+ echo "Start new authentication for user $userIdHTML
";
+ }
+ $this->end();
+ }
+
+
/*
* @param array $logs Array of strings with log entries to show. Entries are ordered newest first
*/