Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add authentication timeout #55

Merged
merged 6 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 4.3.1
* TestServer improvements:
- Make logs available from the test server web UI. The Remote-User HTTP header is used to keep logs separate for
different users.
- Update notificationAddress and notificationType in the UserStorage when the current values are different from the
values in the authentication response.
* Push notifications:
- Add authenticationTimeout as a custom value to the push notification message. authenticationTimeout is the time in
seconds that a user has to start the authentication process after the push notification was sent. The default value
is 150 seconds.
- iOS: Add "mutable-content": 1 option to allow the app to be notified when a push message arrives.
- FCM: Add the ability to send a notification with additional customProperties like authenticationTimeout.
- FCM: Add additional logging.

## 4.3.0
* Add healthCheck() to the UserStorage, UserSecretStorage and StateStorage classes (#54).

Expand All @@ -10,11 +24,14 @@
* Add openssl encryption type to replace the deprecated mcrypt encryption (#50).

## 4.1.0
* Switch to FCM HTTP v1 API for Google push notifications (#52). See the included FCM.md for instructions on how to set up FCM for Tiqr.
* Switch to FCM HTTP v1 API for Google push notifications (#52). See the included FCM.md for instructions on how to set
up FCM for Tiqr.

## 4.0.0
* Switch to the composer autoloader. Removed the `Tiqr_Autoloader` class. This means you must now use composer to use this library, or add your own autoloading.
* Remove support for Apple push notifications (APNS) v1, and the Zend Framework 1 dependency that it required. The library now only supports APNS v2.
* Switch to the composer autoloader. Removed the `Tiqr_Autoloader` class. This means you must now use composer to use
this library, or add your own autoloading.
* Remove support for Apple push notifications (APNS) v1, and the Zend Framework 1 dependency that it required.
The library now only supports APNS v2.
* Add support for PHP 8.x
* Replace the abandoned kairos/phpqrcode QR code library by chillerlan/php-qrcode as suggested by the author
* Documentation updates and corrections. Move security related documentation to SECURITY.md
Expand All @@ -29,13 +46,13 @@
## 3.0.0
Increase the major version because of significant changes in the UserStorage, UserSecretStorage and StateStorage

Review your use of the library from you client code!
Review your use of the library from your client code!
* Almost every function can throw an Exception now!
* Most interfaces changed
* Some functions changed their behaviour

interfaces:
- Remove many differences in behaviour between type, e.g. between UserStorage 'file' and UserStorage 'pdo' types
- Remove many differences in behaviour between types, e.g. between UserStorage 'file' and UserStorage 'pdo' types
- Move interface documentation from the individual drivers to the interfaces
- Always throw an exception when a communicating error with the backend occurs
- Added type declarations to many interfaces
Expand Down Expand Up @@ -63,10 +80,10 @@ UserSecretStorage:
TiqrService:
- More functions throw
- Explicitly describe the functions that will not throw
- Sessions keys used in OCRA and other places in the Tiqr protocol are now 32 hex digits long
(16 bytes of entropy) and are now generated by the Tiqr_Service class, and not by the OCRAWrapper.
The OCRAWrapper_v2 implementation generated sessions keys with a length that depended on the
configured OCRA Suite and used 64 hex digits with the default suite.
- Sessions keys used in OCRA and other places in the Tiqr protocol are now always 32 hex digits long
(i.e. 16 bytes – 128 bits – of entropy) and are now generated by the Tiqr_Service class, and not by the OCRAWrapper.
The OCRAWrapper_v2 implementation generated session keys with a length that depended on the configured OCRA Suite and
used 64 hex digits with the default suite.

OcraWrapper:
- The OcraWapper_* classes for wrapping the OCRA v1 and OCRA v2 implementations were removed
Expand All @@ -92,34 +109,34 @@ to the default (v2) OCRA implementation. No interface changes.
notification directly to the device.

* Add more input validation to the default (v2) OCRA implementation. More methods in the `OCRA` and
`Tiqr_OCRAWrapper` classes can now throw exceptions. Added the One-Way Challenge Response test vectors from the RFC
`Tiqr_OCRAWrapper` classes can now throw exceptions. Added the One-Way Challenge Response test vectors from RFC 6287
to the unit tests.

**Bugfix**
* Fix bug in the OCRA v2 algorithm that computed responses that did not match the RFC reference
implementation when to OCRA suite included a password component that contained an "S" (e.g. PSHA1).
implementation when the OCRA suite included a password component that contained an "S" (e.g. PSHA1).
This does not affect the Tiqr app because password components are not used there.


## 2.0.0
As of release 2.0.0 we started keeping the CHANGELOG.md file. The older entries are copy pasted from the Github release page.
As of release 2.0.0 we started keeping the CHANGELOG.md file. The older entries were copied from the Github release page.

A release with several backward compatibility breaking changes. Most notable are:

1. User and User Secret storage are no longer intertwined. You are now required to create both, the user storage factory no longer creates a user secret storage for you when you have not configured it.
2. Serveral of the Tiqr server library services now require a PSR style logger to function correctly.
2. Several of the Tiqr server library services now require a PSR style logger to function correctly.
3. LDAP support was dropped from the project. If you used it, sorry we no longer ship it as of version 2

Behavioral changes:

1. The code now throws exceptions when unrecoverable runtime issues are encountered. Previously the service would return a 'error-ish' response like null or false. We now throw exceptions in these situations.
1. The code now throws exceptions when unrecoverable runtime issues are encountered. Previously the service would return an 'error-ish' response like null or false. We now throw exceptions in these situations.
2. As mentioned above in the BC breaking changes: the User storage situation changed. More info can be found: #30 and `https://www.pivotaltracker.com/story/show/181525762`

**Features**
* Implement and with it, improve logging #27
* Add a test server for mobile app development #21
* Improve StateStorage File implementation #35
* Throw exceptions when unrecoverable error situation occur #36
* Throw exceptions when an unrecoverable error situation occurs #36
* Move expiry action and make probability of triggering it configurable #25
* State storage pdo expiry #20
* Convert TravisCI to GitHub Actions #34
Expand Down
5 changes: 5 additions & 0 deletions library/tiqr/Tiqr/Message/APNS2.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public function send()
foreach ($this->getCustomProperties() as $name => $value) {
$payload->setCustomValue($name, $value);
}
// set "mutable-content": 1 in the notification. This will call the Tiqr app when the notification arrives so
// that the app can read it (and also modify it). This allows the app to be notified of the arrival of the notification
// without the user interacting with it.
$payload->setMutableContent(true);

$this->logger->debug(sprintf('JSON Payload: %s', $payload->toJson()));
$notification=new Notification($payload, $this->getAddress());
// Set expiration to 30 seconds from now, same as Message_APNS
Expand Down
51 changes: 31 additions & 20 deletions library/tiqr/Tiqr/Message/FCM.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ public function send()
$tokenCacheDir = $options['firebase.tokenCacheDir'] ?? __DIR__;
$translatedAddress = $this->getAddress();
$alertText = $this->getText();
$url = $this->getCustomProperty('challenge');
$properties = $this->getCustomProperties();

$this->_sendFirebase($translatedAddress, $alertText, $url, $projectId, $credentialsFile, $cacheTokens, $tokenCacheDir);
$this->_sendFirebase($translatedAddress, $alertText, $properties, $projectId, $credentialsFile, $cacheTokens, $tokenCacheDir);

$this->logger->notice(sprintf('Successfully sent FCM push notification. projectId: "%s"; deviceToken: "%s"', $projectId, $translatedAddress));
}

/**
Expand All @@ -63,7 +65,7 @@ private function getGoogleAccessToken($credentialsFile, $cacheTokens, $tokenCach
//set up a callback to log token refresh
$logger=$this->logger;
$tokenCallback = function ($cacheKey, $accessToken) use ($logger) {
$logger->info(sprintf('New access token received at cache key %s', $cacheKey));
$logger->notice(sprintf('New access token received at cache key %s', $cacheKey));
};
$client->setTokenCallback($tokenCallback);
$client->setCache($pool);
Expand All @@ -73,7 +75,7 @@ private function getGoogleAccessToken($credentialsFile, $cacheTokens, $tokenCach
try {
$client->setAuthConfig($credentialsFile);
} catch (\Google\Exception $e) {
throw new Tiqr_Message_Exception_SendFailure(sprintf("Error setting Google credentials for FCM : %s", $e->getMessage()), true, $e);
throw new Tiqr_Message_Exception_SendFailure(sprintf("Error setting Google credentials for FCM: %s", $e->getMessage()), true, $e);
}
$client->addScope('https://www.googleapis.com/auth/firebase.messaging');
$client->fetchAccessTokenWithAssertion();
Expand All @@ -84,48 +86,56 @@ private function getGoogleAccessToken($credentialsFile, $cacheTokens, $tokenCach
/**
* Send a message to a device using the firebase API key.
*
* @param $deviceToken string device ID
* @param $alert string alert message
* @param $challenge string tiqr challenge url
* @param $projectId string the id of the firebase project
* @param $credentialsFile string The location of the firebase secret json
* @param $cacheTokens bool Enable caching the accesstokens for accessing the Google API
* @param $tokenCacheDir string Location for storing the accesstoken cache
* @param $retry boolean is this a 2nd attempt
* @param $deviceToken string device ID
* @param $alert string alert message
* @param $customProperties array Additional properties to send with the message like the challenge.
* Array of string->string (property name -> property value)
* @param $projectId string the id of the firebase project
* @param $credentialsFile string The location of the firebase secret json
* @param $cacheTokens bool Enable caching the accesstokens for accessing the Google API
* @param $tokenCacheDir string Location for storing the accesstoken cache
* @param $retry boolean is this a 2nd attempt
* @throws Tiqr_Message_Exception_SendFailure
*/
private function _sendFirebase(string $deviceToken, string $alert, string $challenge, string $projectId, string $credentialsFile, bool $cacheTokens, string $tokenCacheDir, bool $retry=false)
private function _sendFirebase(string $deviceToken, string $alert, array $properties, string $projectId, string $credentialsFile, bool $cacheTokens, string $tokenCacheDir, bool $retry=false)
{
$apiurl = sprintf('https://fcm.googleapis.com/v1/projects/%s/messages:send',$projectId);

$fields = [
'message' => [
'token' => $deviceToken,
'data' => [
'challenge' => $challenge,
'text' => $alert,
],
'data' => array(),
"android" => [
"ttl" => "300s",
],
],
];

// Add custom properties
foreach ($properties as $k => $v) {
$fields['message']['data'][(string)$k] = (string)$v;
}
// Add message
$fields['message']['data']['text'] = $alert;

try {
$headers = array(
'Authorization: Bearer ' . $this->getGoogleAccessToken($credentialsFile, $cacheTokens, $tokenCacheDir),
'Content-Type: application/json',
);
} catch (\Google\Exception $e) {
throw new Tiqr_Message_Exception_SendFailure(sprintf("Error getting Goosle access token : %s", $e->getMessage()), true);
throw new Tiqr_Message_Exception_SendFailure(sprintf("Error getting Google access token : %s", $e->getMessage()), true);
}

$payload = json_encode($fields);
$this->logger->debug(sprintf("JSON payload: %s", $payload));

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiurl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$result = curl_exec($ch);
$errors = curl_error($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Expand All @@ -142,8 +152,9 @@ private function _sendFirebase(string $deviceToken, string $alert, string $chall

// Wait and retry once in case of a 502 Bad Gateway error
if ($statusCode === 502 && !($retry)) {
$this->logger->warning("Received HTTP 502 Bad Gateway error, retrying once");
sleep(2);
$this->_sendFirebase($deviceToken, $alert, $challenge, $projectId, $credentialsFile, $cacheTokens, $tokenCacheDir, true);
$this->_sendFirebase($deviceToken, $alert, $properties, $projectId, $credentialsFile, $cacheTokens, $tokenCacheDir, true);
return;
}

Expand Down
7 changes: 7 additions & 0 deletions library/tiqr/Tiqr/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,18 @@ public function sendAuthNotification(string $sessionKey, string $notificationTyp
throw new InvalidArgumentException("Unsupported notification type '$notificationType'");
}

// Authentication timeout in seconds to send as payload in the push notification to the client. The Tiqr client
// can use this value to stop offering the authentication to the user.
// Use CHALLENGE_EXPIRE - 30 seconds as the maximum timeout to send to the client. This gives the user 30 seconds
// before the authentication session expires at the server. Never send an authenticationTimeout of less than 30 seconds.
$authenticationTimeout = max( 30, self::CHALLENGE_EXPIRE - 30);

$this->logger->info(sprintf('Creating and sending a %s push notification', $notificationType));
$message->setId(time());
$message->setText("Please authenticate for " . $this->_name);
$message->setAddress($notificationAddress);
$message->setCustomProperty('challenge', $this->_getChallengeUrl($sessionKey));
$message->setCustomProperty('authenticationTimeout', $authenticationTimeout);
$message->send();
} catch (Exception $e) {
$this->logger->error(
Expand Down
Loading