Multitenancy architecture with discriminator column — Part 2
Adding authentication and authorization with Bcrypt and JWT
It’s time for the second part of the article. The first is available here.
The way to run the project is the same as we saw in the first part of the article. The important parts of the project that have been changed and/or added, we will see now.
Deleting the tables in the first part of the article
In the first part of the article we used Flyway to create the tables, but for this part of the article the table structure is different. To make it easier, we will not deal with the migration part of Flyway, we will delete the tables.
For that, if you have run PostgreSQL as I suggested in the first part, just follow the steps below:
docker exec -it postgres psql -U postgres
After executing the above command you will have access to psql
, which is used to execute a command against the database. Now just run:
DROP table tenants, users;
Now you can run the project from the repository below without problems.
Project repository
What is JWT?
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
The above quote was taken from here, where other details, libs and an excellent JWT debugger can be seen.
For the project of this article we will:
- Verify that whoever is making a request is really a user who has access to the system (authentication).
- Check what actions he can perform (authorization).
The library we are going to use is the most famous and complete in Java, the JJWT.
Main points of the project development
In the first part of the article I showed how we can make queries that automatically receive the tenantId
of the logged in user.
Remember how this is done? That’s right:
Any class that inherits from
TenantableRepository
in which the method starts withfind
will be intercepted by theTenantAspect
class and will have thetenantFilter
activated and filled with thetenantId
of the authenticated user.
In order to authenticate the user, we need to find him in the database, right? The point is that UserRepository
inherits from TenantableRepository
and the new method we have to search for users, findByEmail
, starts with… find
! Did you understand the problem?
In authentication we have no
tenantId
!
That’s right: tenantId
comes from the users
table; that’s where we know the user’s tenant! And how did I solve this?
I had to implement a way to allow methods not to have the tenantFilter
activated. Thus the following annotation was born:
This annotation must be used in methods of classes that inherit from TenantableRepository
but we DO NOT want to have the tenantFilter
activated!
In short: Doing a “SELECT” without the tenantId
“WHERE”!
CAUTION: This annotation should be used at very specific and controlled points, to not expose data from a tenant to users of another tenant.
The UserRepository
class now has the findByEmail
method annotated with the DisableTenantFilter
annotation, thus allowing users to search for users only by email. To finish we will see how we disable the tenantFilter
.
This is our famous TenantAspect
class that now has this responsibility:
Check if the executed method is annotated with the annotation
DisableTenantFilter
and, if so, it does not activate thetenantFilter
.
With this point that seems simple, explained, we will continue with the main points of the development.
Below is the SignService
class. It is used by the SignController
class, the only unprotected API route.
- Lines 16 to 18: We check if a header called
Authorization
was passed with a schema calledBasic
*. - Lines 21 to 24: We decode the
Authorization
header that arrived at Base64, thus obtaining the email and password that will be used for authentication. - Lines 25: We search the database for a user with the email obtained in the previous step.
- Line 26: We verify that the password entered is the same password that is recorded in the database.
An important point here is: The password in the database is encrypted, using the Bcrypt algorithm.
NEVER STORE PASSWORDS WITHOUT CRYPTOGRAPHY OR WITH ANY ALGORITHM THAT HAS ALREADY BEEN BROKEN!
- Line 27: Here the JWT is created and returned to the user in a header called
Authorization
with a schema calledBearer
*.
Note that when we create the JWT, we are passing 4 user informations: email, roleId, roleName and tenantId.
We will see below where this informations are used.
* More details about Basic
and Bearer
schemas can be seen here.
The JwtService
class is responsible for creating and verifying JWTs.
- Line 4 and 5: The heart of JWT security: The signing key!
It must be a string, but not an ordinary one: It must be large, difficult to be discovered and the main thing: ALWAYS IN A ENVIRONMENT VARIABLE!
And this is where we do it all: We get the signing key for a property called jwt.signin.key
that is present in the application.properties
file. In this file we have the following:
The property jwt.signin.key
will be filled with the value of the environment variable JWT_SIGNING_KEY
or, if it is not configured, it will have as a default value what comes after the colon.
For something so important, NEVER set a default value!
It's here to facilitate the use of this basic project and the writing of the article, only!
Let’s continue with the JwtService
class:
- Lines 19 to 21: Here we are seeing the 4 informations we talked about in the previous class being used to create the JWT. There are others that are recommended but have been omitted to facilitate explanation. More details about them can be seen here.
- Line 27: This is where we check whether a JWT is valid or not.
If someone tries to create or change a JWT, to pass by another user and/or tenant, without having used our signing key, will receive an error.
This concludes the way we create and how we can validate a JWT.
We will now see where we actually validate the JWT.
We already saw the TenantAuthorizationFilter
class in the first part of the article, but it was quite simple. Now it has many more responsibilities.
Lines 13 to 16: We check if a header called Authorization
was passed with a schema called Bearer
.
Line 26: It’s time for the truth: Here we check if the JWT sent in the request is valid.
Lines 27 to 32: If the JWT is valid, we take the informations email, roleId, roleName and tenantId and create a new object TenantAuthenticationToken.
With that we have an authenticated and authorized user.
Now we will see how we can protect, or not, our API endpoints.
The SecurityConfiguration
class was already explained in the first part of the article, but now it also has more responsibilities.
- Line 14: The only unprotected endpoint we have is
/api/signIn
.
The reason? If we protect this endpoint, how someone will be able to authenticate and get a valid JWT to access the other endpoints? - Line 15: The endpoint
/api/tenants
will be accessed only by users who have the role “SUPER_ADMIN”. - Line 16: The endpoint
/api/users
, in the verb GET, that is, for queries only, will be accessible by users who have the role “SUPER_ADMIN”, “ADMIN” or “USER”. - Line 17: The other APIs not covered by the rules of the lines above will be accessible by users who have the role “SUPER_ADMIN” or “ADMIN”.
Summarizing the 4 points above:
- Any user can try to authenticate.
- Only users with the “SUPER_ADMIN” role can create, list, update and delete tenants.
- Users who have the “SUPER_ADMIN” or “ADMIN” role can create, list, update and delete users.
- Users with the “USER” role can only list users.
That would conclude the authentication and authorization part… but I have to explain one more thing:
Tenant personification
Remember that the design of the first part of the article causes the tenantId
to be inserted automatically in all inserts, selects, updates and deletes?
If you paid attention to the file src/main/resources/db/migration/V1__initial_setup.sql
you will notice that the only user that exists initially is the user “super.admin@demo.com”, whose unencrypted password is “super.admin" who has the role “SUPER_ADMIN” and “lives” in a special tenant, the “Super tenant administrator”.
Now you may be wondering:
How am I going to create a user on another tenant, other than the “Super tenant administrator”?
Well, this is where a choice that I made comes from. I don’t know if it is the best one, but it is the one that met my use case and allowed me not to mess too much with the logic: TENANT PERSONIFICATION!
That’s right: Users with the role “SUPER_ADMIN” can use the endpoint /api/tenants/impersonate/{tenantId}
thus being able to create JWTs with the chosen tenantId
!
It seems simple, but with that we reduced the code that would allow us to accomplish this, but the biggest gain is: Keep the logic of accessing a tenant’s data the same for any user!
Let’s see how it was done.
The class above is very simple: It takes the email of the authenticated user and creates a JWT with the tenantId
passed in the request.
And this service is only used by the TenantController
, which, as we have already seen: Can only be accessed by users who have the role “SUPER_ADMIN”.
Concluding
We now have a project that allows us to have:
- A multitenancy application with a discriminator column.
- User authentication against the database to obtain a valid JWT.
- In possession of a valid JWT, we continue to have authentication (without having to check the database again) and having authorization rules on our endpoints through the claim
roleName
.
Testing
I made available in the directory “src/test/resources/spring-multitenancy-column-discriminator.postman_collection.json” a collection of Postman with all the calls to test everything we saw in this article.
Project bonus
I implemented features, listed below, that are not part of the scope of this article and that would make it bigger than it already is.
- In the
UserService
class there is a treatment for users to be created or updated with roles equal to or lower than the authenticated user.
This is necessary to prevent, for example, a user with the role “USER” creating or updating users with the role “ADMIN” or “SUPER_ADMIN” and so on. - In the same class as above, I put a lock to prevent a user with the role “SUPER_ADMIN” from being created outside the tenant “Super tenant administrator”.
- The reason for the initial user and tenant to start with
id = 0
is not to conflict with theid
of the next records, which will be managed by the database and start at1
.
The above items do not exist, or are implemented differently, in the architecture of my personal project. Feel free to change according to your use case.
- Do you want to disable Flyway and let “Spring Data JPA” create and update the database for you?
Just add these 3 lines to theapplication.properties
file. The comment also goes: Do not forget to remove this when it goes to production or the development is already advanced… it will avoid a LOT of headaches!
# REMOVE WHEN GOING TO PRODUCTION #
spring.flyway.enabled=false
spring.jpa.hibernate.ddl-auto=update
Well, that’s it folks! I really enjoyed studying and developing this architecture, which will serve as the basis for a personal project of mine, but I enjoyed writing about it even more!
I hope you enjoyed it and see you in the next article!