Building Enterprise Authentication with Angular, Spring Boot, SSO, and MFA
Implementing SSO and MFA for an enterprise Angular and Spring Boot platform taught me that authentication is frontend architecture, not a feature you bolt on at the end.
Authentication Isn’t a Feature, It’s Frontend Architecture
Whereas the authentication process can typically be implemented as an afterthought for any development team through just creating a login interface that will be connected to some sort of identity management solution, it will not be as straightforward when there is a need to develop a fully functional enterprise solution with the use of Angular and Spring Boot. From implementing single sign-on and multi-factor authentication in such cases with the use of such a front-end/back-end combination, it became evident that authentication is more of a constraint than a feature.
Why SSO Changes More Than the Login Screen
However, the idea of single sign-on appears to be fairly straightforward – you visit the identity provider and get your token, then that’s all. But that implies that the application will have to start working differently from within. From now on, the routes should have a guard to check if the user is authorized before any component gets initialized. Deep links have to be maintained during the redirect and return from the identity provider, which essentially means that the app needs to maintain the intended destination state in a process it does not control. Plus, since the token comes from an external provider on their own schedule, it needs to be treated by the frontend as an ordinary and expected situation where a token expires.
In our platform, we implemented route guards checking token validity before resolving a protected route and token refreshing in the background mode without any interruptions to the user’s workflow. This part was more important for the whole implementation than the single sign-on flow itself, since a technically correct single sign-on flow that constantly interrupts users is still considered broken.
The refresh mechanism brought its own nuances. In case multiple requests failed due to token expiration, an oblivious interceptor would spawn multiple refresh actions, while the majority of identity providers will revoke all previous refresh tokens upon the generation of a new one. It was resolved by having a request queue, where the first error will cause a single refresh action to be fired, while the rest of the pending requests would wait for this result and get re-fired with a fresh token. Should the refresh action fail, the queue would be cleared, and the user would go through the entire authentication process with his destination saved.
Where Multi-Factor Authentication Adds Real Complexity
One of the features MFA brings to the table is an additional level of state that the frontend has to handle properly: users might be authenticated but not yet fully verified. Confusing these two states is what makes most MFA setups break: they allow access to routes only available to fully authenticated users or require users to go through a verification step that they have already done in the current session.
We considered the authentication state to be a state machine rather than a mere boolean value due to the existence of different states like the state of unauthenticated, the state of authenticated and waiting for verification, and the state of authenticated. Different permissions were applied in each of those states, and different transitions were possible between the different states. It allowed us to easily predict the results of the authentication code analysis and especially testing since we could test each transition between the different states.
Step-up verification introduced yet another complication. Certain activities, like processing an important transaction, needed more recent verification than merely logging in. We simulated this by modeling it as a level of verification tied to a period of time within the session, thus allowing the back-end to request re-verification during certain important tasks while retaining the user’s work at the front-end.
The Spring Boot Side of the Contract
All of that required the backend to implement the exact same model that the frontend expected. The validation of tokens was done by our Spring Boot services on every single request instead of relying only on the frontend, and we were sending different and clearly recognizable error messages for token expiry, invalid tokens, and insufficient verification level instead of simply sending 401 all the time. And these differences are extremely important on the frontend – token expiry causes silent refreshing of tokens, invalid tokens cause the full authentication process, and insufficient verification level causes the user to be forced to pass through the MFA challenge once again, and not start the whole authentication from scratch.
It took us way more coordination and planning to get this contract right than either side expected initially. The backend team could have considered all types of authentication errors to be the same thing and the frontend team could have thought that the error codes would translate perfectly into frontend states without any further checks. We recognized this problem at the very early stages mainly thanks to our SDET testing this boundary explicitly.
What This Means for Compliance-Heavy Environments
As the banking system is a highly regulated one, authentication was not only about the quality of the user experience; it was also an audit problem. With every state transition done, we needed to be able to explain and justify our choice of actions rather than just copying what worked before. This way we had to move toward using the explicit state machine rather than implicit, since the latter was difficult to prove.
The Takeaway
It is the choices about the design of the authentication procedure, the model of the state, semantic error management in the frontend-backend communication, token refreshing, and many more that will define the level of opposition that our team needs to overcome in order to make the app survive. The fact that our approach to the design of the authentication procedure as part of the frontend architecture from the very beginning was the reason for our success.
Opinions expressed by DZone contributors are their own.