We’ve just released IXP Manager v5.3.0. The headline feature in this release is two-factor authentication (2fa) and user session management. This blog post overviews the PHP elements on how we did that.
While IXP Manager is a Laravel framework application, it uses Doctrine ORM as its database layer via the Laravel Doctrine bridge. For those curious, this really is a carry over from when IXP Manager was a Zend Framework application. For the migration, we concentrated on the controller and view elements of the MVC stack leaving the model layer on Doctrine. Over time we’ll probably migrate the model layer over to Laravel’s Eloquent.
Before reading on, it would be useful to first read the official documentation we have written aroud 2fa and user session management:
- User: https://docs.ixpmanager.org/usage/authentication/
- Developer: https://docs.ixpmanager.org/dev/authentication/
Hopefully the how we did this will be useful for anyone else in the same boat or even just trying to understand the Laravel authentication stack.
Two factor authentication (2fa) strengthens access security by
requiring two methods (also referred to as factors) to verify your
identity. Two factor authentication protects against phishing, social
engineering and password brute force attacks and secures your logins
from attackers exploiting weak or stolen credentials.
User session management allows a user to be logged in and remembered from multiple browsers / devices and to manage those sessions from within IXP Manager.
For 2fa, we used the antonioribeiro/google2fa-laravel package which is built on antonioribeiro/google2fa. If we were 100% in Laravel’s eco-system the would have been easier but because we use Doctrine, we needed to override a number of classes.
Structurally we need a database table to indicate if a user has 2fa enabled and to hold their 2fa secret – for this we created Entities\User2FA. Similarly, we have a controller to handle the UI interaction of enabling, configuring and disabling 2fa: User2FAController – this also includes generating QR codes for the typical 2fa activation process.
On the user session management side, we created Entities\UserRememberToken to hold multiple tokens per user (rather than Laravel’s default single token in a column in the user’s user database entry. For the frontend UI, UserRememberTokenController allows a user to view their active sessions and invalidate (delete) them if required.
The actual mechanism of enforcing 2fa is via middleware: IXP\Http\Middleware\Google2FA. This is added, as appropriate, to web routes via the RouteServiceProvider. This will check the user’s session and if 2fa is enabled but has not been completed, then the middleware will enforce 2fa before granting access to any routes covered by it.
Note that because we also implemented user session management via long-lived cookies and because the fact that a user has passed 2fa or not is held in the session, we need to persistently store the fact in the user’s specific remember token database entry. This is done via the Google2FALoginSucceeded listener. This is then later checked in the SessionGuard – where, if we log a user in via the long-lived cookie, we also make them as having passed 2fa if so set.
Speaking of the SessionGuard, this was one of the bigger changes we had to make – we overrode the
Illuminate\Auth\SessionGuard as we needed to replace a few functions to make 2fa and user session management work. We have kept these to a minimum:
- The user() function – Laravel’s long lived session uses a single token but we require a token per device / browser. We also need to side-step 2fa for existing sessions as discussed above and allow for features such as allowing a user to delete other long-lived sessions and to provide functionality to allow these sessions to expire.
- The ensureRememberTokenIsSet() to actually create per-browser tokens (and to expire old ones).
- The queueRecallerCookie() so we can insert our own token rather than the default Laravel version.
- The cycleRememberToken() which is actually used to invalidae a token by changing it in Laravel. We override to delete the token.
Similarly we have to override the DoctrineUserProvider class to:
- Change retrieveByToken() to use our new database in which a user may have multiple sessions across different browsers / devices.
- Add addRememberToken() and purgeExpiredRememberTokens() to add and remove tokens.
We of course had to ammend the AuthServiceProvider to use our new overridden classes.
The above constitutes a bulk to the changes. Because 2fa can be enforced via middleware, it doesn’t really touch the core Laravel authentication process. The user session management was more invasive and responsible for the bulk of the changes required in the DoctrineUserProvider and SessionGuard.
What’s not mentioned above is the views – these are mainly covered in the views/user-remember-token (with a lot of inheritence from views/frontend) and the views/user/2fa directories.
While there are a lot more changes between v5.2.0 and v5.3.0 than 2fa and user session management, you can see the complete set of changes here.