Skip to content

Commit

Permalink
use symfony cache for managing session-side storage (#954)
Browse files Browse the repository at this point in the history
- use symfony cache for managing sessions,
- add upgrade notes,
- add 2 cache parameters in config,
- remove expired cache entries,
- remove some warning messages,
- security: always display the same message: invalid token even if the user is not found in ldap,
- add more logs,
- set an expiration time for each cache entry,
- set symfony/cache version in composer.json,
- adding new cache bundled dependencies in packages and doc
  • Loading branch information
David Coutadeur committed Sep 9, 2024
1 parent b742d6b commit db3a21f
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 106 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"gregwar/captcha": "v1.2.1",
"mxrxdxn/pwned-passwords": "v2.1.0",
"components/jquery": "v3.7.1",
"fortawesome/font-awesome": "6.5.1"
"fortawesome/font-awesome": "6.5.1",
"symfony/cache": "v5.4.42"
},
"scripts": {
"post-update-cmd": [
Expand Down
12 changes: 12 additions & 0 deletions conf/config.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,18 @@
# Token lifetime in seconds
$token_lifetime = "3600";

## Cache
# $cache_token_expiration: integer, duration in seconds of cached objects
# each time a token is involved
# (for example when sending a token by sms or by mail)
# it is recommended to set a value >= $token_lifetime
$cache_token_expiration = 3600;
# $cache_form_expiration: integer, duration in seconds of cached objects
# at some steps when a user has to validate a form
# (for example when validating the email address before we send the mail)
# it is recommended to set a value high enough for a user to fill a form
$cache_form_expiration = 120;

# Reset URL (mandatory)
$reset_url = "http://ssp.example.com/";
# If inside a virtual host
Expand Down
28 changes: 28 additions & 0 deletions docs/config_general.rst
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,34 @@ See `FriendlyCaptcha documentation <https://docs.friendlycaptcha.com/>`_ for mor

You can also integrate any other Captcha module by developping the corresponding plugin. (see :doc:`developpers` )

.. _config_cache:

Cache
-----

self-service-password rely on Symfony cache libraries.

You can define the cache expiration for some objects:

.. code-block:: php
$cache_token_expiration = 3600;
``$cache_token_expiration`` (integer) is the duration in seconds of cached objects each time a token is involved.

For example when sending a token by sms or by mail, it is the time granted to the user for entering the sms code or for clicking on the link in the mail.

it is recommended to set a value >= ``$token_lifetime``

.. code-block:: php
$cache_form_expiration = 120;
``$cache_form_expiration`` (integer) is the duration in seconds of cached objects at some steps when a user has to validate a form.

For example it is the time granted to a user for validating the email address before sending the mail. It is used mainly for avoiding form replay (by user mistake or by a hacker).

it is recommended to set a value high enough for a user to fill a form.

.. |image0| image:: images/br.png
.. |image1| image:: images/catalonia.png
Expand Down
9 changes: 9 additions & 0 deletions docs/config_sms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,12 @@ You can also configure the allowed attempts:
$sms_max_attempts_token = 3;
After these attempts, the sent token is no more valid.

You should also set a token lifetime, so they are invalid after some time. The
value is in seconds:

.. code-block:: php
$token_lifetime = "3600";
If you use tokens, you should also set :ref:`config_cache` parameters accordingly.
5 changes: 2 additions & 3 deletions docs/config_tokens.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,14 @@ You can crypt tokens, to protect the session identifier:
.. warning:: If you enable this option, you must change the default
value of the security keyphrase.

You should set a token lifetime, so they are deleted if unused. The
You should set a token lifetime, so they are invalid after some time. The
value is in seconds:

.. code-block:: php
$token_lifetime = "3600";
.. warning:: Token deletion is managed by PHP session garbage
collector.
If you use tokens, you should also set :ref:`config_cache` parameters accordingly.

Log
---
Expand Down
31 changes: 31 additions & 0 deletions docs/upgrade.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
Upgrade
=======

From 1.6 to 1.7
---------------

If you have configured ``$token_lifetime`` parameter, for example for reset by sms or reset by mail features, you should verify that the duration is coherent with the new cache parameters, and adapt these parameters in your local configuration file if needed:

.. code-block:: php
# $cache_token_expiration: integer, duration in seconds of cached objects
# each time a token is involved
# (for example when sending a token by sms or by mail)
# it is recommended to set a value >= $token_lifetime
$cache_token_expiration = 3600;
# $cache_form_expiration: integer, duration in seconds of cached objects
# at some steps when a user has to validate a form
# (for example when validating the email address before we send the mail)
# it is recommended to set a value high enough for a user to fill a form
$cache_form_expiration = 120;
New bundled dependencies have been added:

* php-symfony-deprecation-contracts = v2.5.3
* php-symfony-var-exporter = v5.4.40
* php-psr-container = 1.1.2
* php-symfony-service-contracts = v2.5.3
* php-psr-cache = 1.0.1
* php-symfony-cache-contracts = v2.5.3
* php-psr-log = 1.1.4
* php-symfony-cache = v5.4.42


From 1.5 to 1.6
---------------

Expand Down
12 changes: 12 additions & 0 deletions htdocs/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
require_once("../vendor/autoload.php");
require_once("../lib/functions.inc.php");

use Symfony\Component\Cache\Adapter\FilesystemAdapter;

#==============================================================================
# VARIABLES
#==============================================================================
Expand Down Expand Up @@ -119,6 +121,16 @@
isset($ldap_krb5ccname) ? $ldap_krb5ccname : null
);

#==============================================================================
# Cache Config
#==============================================================================
$sspCache = new FilesystemAdapter(
$namespace = 'sspCache',
$defaultLifetime = 0,
$directory = null
);
$sspCache->prune();

#==============================================================================
# Captcha Config
#==============================================================================
Expand Down
22 changes: 10 additions & 12 deletions htdocs/resetbytoken.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,15 @@
$tokenid = $token;
}

# select internal session by $tokenid without relying on cookie or url
# select token in the cache
# will gather login,time and smstoken values from session.
ini_set("session.use_cookies",0);
ini_set("session.use_only_cookies",1);

session_id($tokenid);
session_name("token");
session_start();
$login = $_SESSION['login'];
$smstoken = isset($_SESSION['smstoken']) ? $_SESSION['smstoken'] : false;
$cached_token = $sspCache->getItem($tokenid);
$cached_token_content = $cached_token->get();
if($cached_token->isHit())
{
$login = $cached_token_content['login'];
}
$smstoken = isset($cached_token_content['smstoken']) ? $cached_token_content['smstoken'] : false;
$posttoken = isset($_REQUEST['smstoken']) ? $_REQUEST['smstoken'] : 'undefined';

if ( !$login ) {
Expand All @@ -72,7 +71,7 @@
error_log("Token not associated with SMS code ".$posttoken);
} else if (isset($token_lifetime)) {
# Manage lifetime with session content
$tokentime = $_SESSION['time'];
$tokentime = $cached_token_content['time'];
if ( time() - $tokentime > $token_lifetime ) {
$result = "tokennotvalid";
error_log("Token lifetime expired");
Expand Down Expand Up @@ -179,8 +178,7 @@

# Delete token if all is ok
if ( $result === "passwordchanged" ) {
$_SESSION = array();
session_destroy();
$sspCache->deleteItem($tokenid);
}

#==============================================================================
Expand Down
83 changes: 44 additions & 39 deletions htdocs/sendsms.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,47 +83,50 @@

$tokenid = decrypt($token, $keyphrase);

ini_set("session.use_cookies",0);
ini_set("session.use_only_cookies",1);

session_id($tokenid);
session_name("smstoken");
session_start();
$login = $_SESSION['login'];
$sessiontoken = $_SESSION['smstoken'];
$attempts = $_SESSION['attempts'];
# Get session from cache
$cached_token = $sspCache->getItem($tokenid);
$cached_token_content = $cached_token->get();

if($cached_token->isHit())
{
$login = $cached_token_content['login'];
$sessiontoken = $cached_token_content['smstoken'];
$attempts = $cached_token_content['attempts'];
}

if (!$login or !$sessiontoken) {
list($result, $token) = obscure_info_sendsms("tokenattempts",
"tokennotvalid",
$token,
$obscure_notfound_sendsms,
$keyphrase);
error_log("Unable to open session $smstokenid");
error_log("Unable to open session $tokenid");
} elseif ($sessiontoken != $smstoken) {
# To have only x tries and not x+1 tries
if ($attempts < ($sms_max_attempts_token - 1)) {
$_SESSION['attempts'] = $attempts + 1;
$cached_token_content['attempts'] = $attempts + 1;
$cached_token->set($cached_token_content);
$sspCache->save($cached_token);
$result = "tokenattempts";
error_log("SMS token $smstoken not valid, attempt $attempts");
} else {
$result = "tokennotvalid";
error_log("SMS token $smstoken not valid");
}
} elseif (isset($token_lifetime)) {
$tokentime = $_SESSION['time'];
$tokentime = $cached_token_content['time'];
if ( time() - $tokentime > $token_lifetime ) {
$result = "tokennotvalid";
error_log("Token lifetime expired");
}
}
if ( $result === "tokennotvalid" ) {
$_SESSION = array();
session_destroy();
# Remove token
$sspCache->deleteItem($tokenid);
}
if ( $result === "" ) {
$_SESSION = array();
session_destroy();
# Remove token
$sspCache->deleteItem($tokenid);
$result = "buildtoken";
}
} elseif (isset($_REQUEST["encrypted_sms_login"])) {
Expand Down Expand Up @@ -203,15 +206,17 @@
# Generate sms token
$smstoken = generate_sms_token($sms_token_length);
# Create temporary session to avoid token replay
ini_set("session.use_cookies",0);
ini_set("session.use_only_cookies",1);

session_name("smstoken");
session_start();
$_SESSION['login'] = $login;
$_SESSION['smstoken'] = $smstoken;
$_SESSION['time'] = time();
$_SESSION['attempts'] = 0;
$smstoken_session_id = hash('sha256', bin2hex(random_bytes(16)));
$smscached_token = $sspCache->getItem($smstoken_session_id);
$smscached_token->set([
'login' => $login,
'smstoken' => $smstoken,
'time' => time(),
'attempts' => 0
]);
$smscached_token->expiresAfter($cache_token_expiration);
$sspCache->save($smscached_token);
error_log("generated cache entry with id: " . $smstoken_session_id. " for storing step 'send sms' of password reset by sms workflow, valid for $cache_token_expiration s");

$data = array( "sms_attribute" => $sms, "smsresetmessage" => $messages['smsresetmessage'], "smstoken" => $smstoken) ;

Expand All @@ -220,7 +225,7 @@

if ($sms_method === "mail") {
if ($mailer->send_mail($smsmailto, $mail_from, $mail_from_name, $smsmail_subject, $sms_message, $data)) {
$token = encrypt(session_id(), $keyphrase);
$token = encrypt($smstoken_session_id, $keyphrase);
$result = "smssent";
if (!empty($reset_request_log)) {
error_log("Send SMS code $smstoken by $sms_method to $sms\n\n", 3, $reset_request_log);
Expand All @@ -244,7 +249,7 @@
$definedVariables = get_defined_vars(); // get all variables, including configuration
$smsInstance = createSMSInstance($sms_api_lib, $definedVariables);
if ($smsInstance->send_sms_by_api($sms, $sms_message)) {
$token = encrypt(session_id(), $keyphrase);
$token = encrypt($smstoken_session_id, $keyphrase);
$result = "smssent";
if ( !empty($reset_request_log) ) {
error_log("Send SMS code $smstoken by $sms_method to $sms\n\n", 3, $reset_request_log);
Expand All @@ -264,18 +269,18 @@
#==============================================================================
if ($result === "buildtoken") {

# Use PHP session to register token
# We do not generate cookie
ini_set("session.use_cookies",0);
ini_set("session.use_only_cookies",1);

session_name("token");
session_start();
$_SESSION['login'] = $login;
$_SESSION['time'] = time();
$_SESSION['smstoken'] = $smstoken;

$token = encrypt(session_id(), $keyphrase);
$smstoken_session_id = hash('sha256', bin2hex(random_bytes(16)));
$smscached_token = $sspCache->getItem($smstoken_session_id);
$smscached_token->set([
'login' => $login,
'time' => time(),
'smstoken' => $smstoken
]);
$smscached_token->expiresAfter($cache_form_expiration);
$sspCache->save($smscached_token);
error_log("generated cache entry with id: " . $smstoken_session_id. " for storing step 'password change' of password reset by sms workflow, valid for $cache_form_expiration s");

$token = encrypt($smstoken_session_id, $keyphrase);

$result = "redirect";
}
Expand Down
Loading

0 comments on commit db3a21f

Please sign in to comment.