Simplication of the User Authentication #359
Unanswered
jbee
asked this question in
Specs & RFCs
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Intro
Login or authentication has grown into a big jungle of classes that is very hard to understand and also very hard to improve.
One such improvement that is very important is to load all the application level access control (ACL+) belonging to a user with a single SQL query. Ideally there would be one place where this happens.
Another improvement is to make sure that if one method of authentication is tested with high coverage for relevant scenarios after a successful login that the coverage equally applies to other methods of authentication, or on other words that the only difference is how the event of authentication works but from there on they are all guaranteed to be the same.
But the current reality is far from that mainly as clarity is obstructed by terrible Spring abstractions that blurs and scatters the code. The good news is that most of them are 100% optional and are not essential to the core mechanism of Spring security. So this proposal is about a better design to move towards that navigates around most of the current issues by simply not using the optional Spring APIs and replace them with straight forward DHIS2 specific solution. With those changes the DHIS2 specific model can easily be simplified to be more clear, slimmer, more accessible and easier to fetch.
What does it mean to be "authenticated"?
In Spring a successful authentication or login is indicated by putting a
Authenticationinstance in theSecurityContextusingsetAuthentication.The only key quality of the
Authenticationis thatisAuthenticated()must returntrueand thatgetAuthorities()returns the set of granted authorities. But even that set is mostly theoretical in the DHIS2 context as DHIS2 does not use Spring mechanisms that directly would call this method to decide access (like@PreAuthorize). While theAuthenticationgetName()should provide a unique name it has no functional role and is only informal in e.g. log messages. A logout means to clear the context and set it tonull.While not essential to Spring security itself the second role of the
Authenticationthat is put in theSecurityContextis to hold application level data linked to the user session. This is done via thegetPrincipal()object. In DHIS this is the ACL relatedUserDetailsdata. But making it a part of theAuthenticationthis data automatically shares the same life-cycle.Important
The above in essence is the entirety of the "mandatory" Spring security mechanism. It is crucial to understand that all the concepts detailed below are just a means to get to that very result of a
Authenticationbeing set in theSecurityContext. But in practice it does not matter how the authentication arrives there.Where things are now
Spring API induced Confusion
AuthenticationManagerThe core Spring API to perform the authentication itself is
AuthenticationManagerwhich has a single method:The first confusion is that input and output are both objects implementing
Authenticationbut they are actually very different things.The input
Authenticationcaptures the user input for the specific form/method of authentication.The output
Authenticationon the other hand is supposed to describe the authenticated user as detailed above in the essential mechanism in terms of thegetAuthorities()and the application specific object hidden behindgetPrincipal().In current code this confusion has caused the input to be extended with authorities and/or the output to drag along details from the authentication user input as if these were needed later on.
Because of this API it is never clear what data is worked with and the code has to perform many casts or
instanceofchecks with often no alternative other than to fail if a type assumption does not hold true.Spring API induced Confusion
AuthenticationProviderThe second confusion originates from the invisible magic of class based dispatch. One
AuthenticationManagerimplements a particular authentication method, like HTTP basic. So there are several implementations but they do not directly indicate themselves what method they can handle. This is task is handled by aAuthenticationProviderwhich has asupportsmethod:The
Classhere are implementing classes ofAuthenticationin their role of a data container for the user input needed by the method to perform the authentication.Note
The irony of the
supportsbased dispatch is that the code constructing the proper input (often called "token" classes) is application code, the code consuming this is also application code (in most instances). So we go through a complicated and confusing generic API just to make a method call that could have been hard coded using a concrete input and output type that enforce a very clear and specific contract and thereby exclude a lot of error surface by design.The process or Authentication
The process in its essence is quite simple but unfortunately Spring provides countless abstractions that complicate the idea (the most central being pointed out above but there are many more).
Conceptually an authentication has these steps
Authenticationwhen usingAuthenticationManagers)AuthenticationManager.authenticate)Authenticationthis time as returned byauthenticate)SecurityContext(and some other stuff with spring level events signaling the login)A a data transformation this look like this
Note
As discussed above using the Spring APIs is more of an obstacle than providing a solution. These APIs provide layers of glue code to the actual implementation of the authorization method itself. But within DHIS2 most methods needed customization anyhow and so it is DHIS2 code that calls the implementation just that each step and layer is wrapped by the Spring API for the concept. At that point all the layers and wrapping is just an obstacle to clarity and source of complications and ceremony bloat.
Proposal
The main idea is to move away from using the optional Spring API layers to gain a simple, easy to follow implementation where each of the steps of an authentication as outlined above is clearly visible as a code component and where inputs and outputs are strongly typed enforcing a proper contract through types and the compiler that excludes most issues by design and which provides a common chain of execution even if the particulars of different authentication methods are vastly different.
Note
The names chosen in the code sketches will lean on the corresponding Spring APIs to make clear which part they correspond to
but in an actual implementation there surely are better more clear names to be found.
A DHIS2 specific interface details the exact contract of all authentication methods using only DHIS2 defined data types
The inputs here would be
records with the essential inputs as they have been extracted from the request filters (mostly from HTTP headers) and not as they might be needed by the underlying implementation (that transformation happens within theauthenticatemethod).For example for HTTP basic authentication:
The other params classes would similarly contains only what is needed in the DHIS2 case for each of the mechanisms.
The output is always a
AuthenticatedUserwhich implementsAuthenticationbut other than that is only a wrapper around theUserDetailsas thegetPrincipal:Before the
AuthServicereturns aAuthenticatedUserit is properly set in theSecurityContextwhich also means there is a single point with the service where based on the user row identified by the authentication method theUserDetailsare loaded and packaged into theAuthenticatedUserproviding a single code path where this occurs.Internally each of the
authenticatemethods performs the authentication either purely using DHIS2 fundamentals (e.g. username+password+2fa) or by providing the adapter/glue to the implementation handled by a 3rd party library. In cases where this is more complicated it seems reasonable to extract a single class for an authentication method that contains all the details of performing the authentication. For example, in case of JWT the process is much more involved than HTTP basic.For
UserDetailsthis means there can be a singleclassorrecordholding only the DHIS2 specific data we need and none of the input to authenticate. Also DHIS2UserDetailsshould not implement Spring'sUserDetails. It is a data container that purely exist to satisfy DHIS2 access control needs (mostly ACL and OU-tree based restrictions) as well as some common identifiers for the current user that are needed as user context.Based on a attempt to refactor to such a single container it could look something like this
Note that I removed most of the questionable fields that have a role in performing
authenticatebut that are (likely) not required once the user is authenticated. I also eliminated sets ofLongIDs. IDs should all beUIDs.In this model the current
SuperUserclass simply becomes a constantSUPER_USERdefined in therecordwith the appropriate values hard coded. Test ideally all useAuthServiceto authenticate, e.g. with username/password using aHTTPBasicParamsinput.Journey
While the goal is pretty clear getting there is quite a lot of work to disentangle from the Spring ceremony and to simplify the code.
Actions identified are as follows (in order)
Actions:
AuthServiceas detailed aboveUserDetailsdata and its loading to occur in single SQL query in a common code path in the new serviceAll reactions