This PR can be reviewed commit by commit.
Note: security & performance considerations are discussed below.
Changed concept of "cookie authentication". Currently, the token auth is stored insecurely in the piwik_auth cookie. When this cookie is in a request to Piwik, this token auth is used to authenticate the request. The authentication is also delegated to the Auth implementation. This PR changes the strategy completely.
Now, the piwik_auth cookie contains a secure hash of a randomly generated string (via
password_hash($user['session_password']))) and the name of the user (required for "session reauthentication"). If the cookie is present in a request, Piwik core will use
SessionAuth, and bypass plugin based authentication altogether. (Note: session_password is a new column in the user table.)
The session now contains four new pieces of data: the logged in user's username, the IP address of the request that authenticated the session, the user agent of the request that authenticated the session and the time the session was authenticated. When SessionAuth is used, it first checks that the session is authenticated by checking that the username exists in the session.
If it does, SessionAuth then checks that the IP address & user agent of the request and the value of the username cookie match the data stored in the session. If they don't, this is a session hijacking attempt. If they do match, then the current request is allowed to use the authenticated session, and no more work is done.
If the piwik_auth cookie is sent, but the session does not exist, SessionAuth verifies that the secure hash is correct (via
password_verify()) and recreates the authenticated session (referred to as "session reauthentication"; why this was done this way is explained below).
A new column was added to the
ts_password_modified. This column holds the last time the user's password was changed. This column, the session start time & the use of the new "session_password" column results in sessions automatically becoming invalid after a password is changed.
SessionAuth checks that the session start time (stored in the session) is newer than the last time the user's password was modified. So if the password was changed, the session can no longer be used.
When performing "session reauthentication", if the password is changed, the user's session_password will be different. This will cause session reauthentication to fail, making the session invalid.
There is one important existing behavior that influenced the implementation of this PR (specifically the need for "session reauthentication").
In Piwik the session lifetime is not managed server side, but client side through cookie expiration. When the piwik_auth cookie expires, the session has ended. This was likely done in order to circumvent PHP session garbage collection, which Piwik has no control over. If a session is deleted server side, authenticating using the token auth in the cookie allows Piwik to restart the session, avoiding random logouts.
This is why the "session reauthentication" mechanism was implemented in this PR. It maintains this behavior, restarting a session if it is deleted by PHP. This behavior can be abandoned, but then users will have to sync the
[General] login_cookie_expire value with the
session.gc_maxlifetime value (or if they're using PHP 7, do as PHP recommends and set it to 0 & use
session_gc() via cron).
There should be no BC breaks. Plugins do not have to call
Login::initAuthenticationFromCookie() anymore, but they don't have to be modified.
What info is compromised if the piwik_auth cookie is compromised?
Would an attacker be able to use this information to gain access to Piwik?
What would an attacker gain if the server side session variables were somehow compromised?
This solution is more secure than what Piwik currently does (I think), but there are two potential attack vectors that aren't addressed. First is:
password_verify() adds a significant delay to individual requests (as it should). With the lowest bcrypt cost, it would add 3.5ms to each request. This is why it's only done during normal authentication & "session reauthentication" (which should not happen that often). When not doing reauthentication, SessionAuth will be very fast.
Since sessions are not invalidated manually (eg, by iterating over every session), there is no overhead added to the change password workflow, either.
Keeping SessionAuth in core also allows plugin authentication to be more costly if required. Once a session is established, plugin based authentication won't happen again.
Some ideas for making Piwik even more secure:
Common::generateUniqId()'s use of md5 & uniqid w/
random_bytes()(there's are polyfills for PHP 5.*, eg, https://github.com/symfony/polyfill). Would prevent attackers from being able to guess what new token auths / session passwords will be.
Not sure if every test will pass, but this should be good to review.
FYI, going to add another commit, had a new idea.
Nevermind, not as good an idea as I thought.
Actually, nevermind to my nevermind, it might be a good idea...
Noticed an issue, putting back into WIP.