Multitenancy Authentication in NestJs

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello guys a lot of you have been waiting for this video for a long time so here it is in this video we're going to see how we can Implement authentication in a multi tency application in njs to remind you quickly in the last video we saw how in multi-tenancy we can have different databases for different tenants so if you take a look at our Master database which is our main default database you can call it anything you want I called it Master we have a tenants collection in which we have a list of different tenants that our application has so we have for example a company company one which has a tenant ID of ABC and this tenant or this company has its own dedicated database which is tenant ABC so here we're just following a specific convention tenant uncore and then the tenant ID of that company and then we have another company which is Company 2 with a tenant ID of XYZ so it has its own dedicated database as well tenant XYZ and if you take a look at those uh tenants specific databases we can see that they both have a product collection which is maybe the products that these companies sell for example tenant ABC sells book one book two book three and XYZ sells a microwave oven fridge we also said that whenever we're making a request to fetch the products we need to be able to tell which database we should use or which tenant is making the request so to do that we added something in our request headers called X tent ID which specifies the tenant ID that is making the request so that we can tell which connection we should be using or which database we should switch to and then get the correct products I'm going to go quickly over the things that we did and the steps that we followed if you haven't seen that video I highly recommend you watch it to understand everything that we've done here so I'm going to link it down below but basically the first thing we did was create a tenants middleware and this tenants middleware is going to check if this externed ID field exists on our request headers so if you are sending this field right here and if not of course we throw an error and then we are also using a tenant service to make sure that this tenant exists so we pass in the tenant ID here to our tenant service and then we return and check if this tenant exists if that's the case we attached the tenant ID on our request and then call the next function to move on to our route Handler The Next Step was to use this tenant ID that we attached on the request to switch to the correct tenant database Now by default if you we go back to app module we can see that we are registering the Mongoose module here in our app module. TS and we are using some connection string now if you go back to our EnV by default we are connecting to master so we are connecting to our Master database right here so our next step is to actually create a provider that allows us to use the tenant ID from our request to switch to the tenant specific database so to do that we went ahead and created a custom provider we called it tenant connection this is the token name and it uses a factory and it ends up returning a new connection or a connection pointing to a different database and for that to work we depend on the request which is why we injected the request object from njs core and we also depend on the connection that we have made from njs which is the connection connecting us to the master database by default and here what we've done is basically we've checked if the talent ID exists on the request object which is what we do here and side of the talent middleware if that's not the case we throw an error make sure to apply talent middleware and then we use this usdb method which switches to a different database using the same connection pool and since we're following this convention tencore and then the tenant ID this is what we need to do to switch to the correct database and then the next step was to actually access or create a provider for our models so that we can inject them in our servers just like we normally do so just as a quick reminder for example in the tenance module here we are using Mongoose model. for feature and then we are registering a tenants model on the default database connection which is the master database which is why we have this tenants collection here okay and then we are able to inject it here like we normally do in Mongoose with nstgs at inject model and then we inject it here and we can use it but now we need to do something different since we have different models in different databases so after creating the standard connection provider we went ahead and created this product model provider which is the specific model or the specific providers that we inject in our service to use the correct collection here so what we did is we created a custom provider we gave it the token of product model and we used a factory method that depends on the tent connection so T connection is a dependency here it's going to be created before creating this model provider and then here what we did was we got the stand connection so it's pointing to specific database either to tenent ABC or tenent XYZ and then we call this model method which either defines or retrieves a model and we pass in the same parameters that we usually pass in whenever we are registering a uh model with mongus module so we passed in the name and then the schema and then just like we do with any provider before being able to use it as a dependency we go to the module in which we need to register it and inside of the provider's array we went ahead and registered our product model provider and now we are able to inject it in our service here using at inject this time because it's a custom provider and then we pass in the token name which is product model and with this in place we can now use this product model which works and specifies a specific model inside of a specific tenant database just to make sure everything is working I'm going to make a call to/ products and extended ID is set to ABC if I hit send as you can see we got book one book two book three which are the different products that exist under tenant ABC so as you can see our providers are working correctly with that out of the way let's focus on today's purpose which is authentication and multi-tenancy so this get products route which is public now as you can see we didn't pass any access token needs to be protected by an oard so usually we would have a secret key here that we use to generate a certain token and then the same secret key that we use to verify and decode a token to make sure that it is actually valid and give access to a certain route normally we would have something like this so we would have a guard that we apply on some routes that need to be protected and this card of course implements can activate and then it uses a JWT service so that it verifies a token and make sure that this token is valid and belongs or is generated from our server meaning we have signed it with our own secret key so to take it step by step if you go to app module here we are registering the JWT module we set it as Global and then we are specifying the secret that should be used anytime we create a new token to give it to a user after they log in successfully or to verify a certain token and make sure that it is valid inside of our oard just like we did here this is a code from my previous videos I've uploaded a lot of videos that explain normal authentication with access tokens and so on so feel free to watch it but in this video I'm just going to uh focus on the multi-tenancy aspect of things so just like we said usually we would have a guard here that protects our routes and then inside of the Guard we are going to verify a token using the JWT service and the secret that is being used is the same for all users which is defined here which we read from our EnV here the issue with this approach in multi-tenancy is that if we are using the same secret for all of our tenants meaning if tenant John or user John which belongs to company one logs in and they get a token which is signed by this secret key then John can also use his own token to actually Access Data from XYZ because all John would need to do is just come here and then specify ABC or XYZ and then pass in some token here so if we had a better token here if this was signed by the same secret key for all different tenants then any user that logs in and gets that token can literally just change this tenant ID and access every other tenants or company's data which is a huge security breach for this exact reason we need to have different secret keys so every tenant should have their own secret key so if tenant or John belonging to company a logs in he should get a token that only works for tenant ABC which is a company that he belongs to and if another user Jeff for example that belongs to Company 2 were to log in successfully our system should provide an access token with a secret key specific for talent XYZ that way that access token would only work on resources that belong under tenant XYZ and it wouldn't work if we were to try and access tenant ABC's data so let's see how we can actually do that now the first thing that we need to do is inside of our Master database we need to have a users collection in which we have all of our different users that belong to our different companies here and then we need to specify of course each user belongs to which tenant now I asked you guys which use case you'd like to see a user Belongs to Only One tenant meaning one company or a user can belong to many tenants one plus tenants most of you wanted to go with the a user can belong to only one tenant approach so this is going to be a bit different from the second approach in terms of implementation flow and so on if you guys want to see the Second Use case as well please let me know down in the comment section so today we have three main tasks first of all we need to create an API that creates a new tenant okay first of all of course we're going to to create a new user for that tenant we're going to create a new company record in the tenants collection and then finally we're going to generate and store a JWT secret for that specific tenant that we use to handle the tokens for this specific tenant secondly we're going to create another API in a no module to log in a user and then return an access token first of all of course we need to make sure that the credentials are correct then if that's the case we need to fetch the tenant specific jwtc secret that we're going to use to generate a token for that user and then finally of course we need to return that token and the Tenant ID back to the client so that they could they can use both the access token and the Tenant ID in the request and finally we're going to create a noard that is going to verify the access token based on the tenant ID secret so let's see how we can Implement all of that I went ahead and created this users module so we have a controller a module uh and the service file I went ahead and also created a user schema file so our user is going to have a name an email a password and just like we said a tenant ID now in case we had uh multiple tenants so a user belonging to multiple tenants at the same time here we would have an array of tenants instead of one single tenant ID but since we're going with the approach of one user belongs to one tenant then we're just going to have one tenant ID for the user I also went ahead and registered this model here in users module we use module. for feature which is going to use the default connection so pointing at the master database meaning it will generate this collection inside of the master database and I went ahead and injected this model inside of our user service so that we can access this collection and perform operations on our database and now if you open our database as you can see inside of our master's DB we have an empty users collection and now inside of ton controller I went ahead and created a new route Handler which is of HTTP post and the route is create company which is going to take a body of create company dto which is going to take a company name and some user data so I went ahead and created a user dto inside of our users module files which is going to take a name an email and a password the data for a new user now of course in your application you might have different business rules different fields you might even have uh instead of one API here that creates a company you might have different apis that have maybe different steps or anything else but the idea is the same so let's keep going so inside of our tenant service our create company API is going to have a few uh steps first of all we need to verify that the user does not already exist because we said that a user can only belong in one tenant so if you're going to create a new company and then add a user to it this user should not already exist and belong to another company so we need to have this check we also need to create a new tenant ID it's going to be a random string we need to create a tenant secret and store it when we come to this step we're going to talk about it in depth and then of course we need to create a new user with a hashed password and finally create a tenant record inside of our database again here you might have different steps depending on your business rules you might have this as separate apis and each API is a different step in your registration for a new company you might have those in an authentication module it all depends on your business rules but the main idea is the same in terms of handling authentication in multi tency for the use case where one user belongs to one company only for the first point it's very simple all you have to do is check that a user does not exist by checking their email in the user's collection so I went ahead and inside of the user service I created a method called get user by email which takes an email and then tries to find uh a user by this email and of course to be able to use this user service in the tenant service we have to go to the users module and then add this provider or this service inside of the exports array and then in tenant module of course we need to import the user module so that we can inject it here in this Constructor so that's basically what we're doing if a user already exists we're going to throw an error user exists and belong to a company or whatever the next step is pretty simple as well we just need to create a tenant ID which like I said is going to be a random string unique for this tenant so I went ahead and used a package called Nano ID and then I specified the character length to be 12 now I usually use uh the version 3.3.6 because other versions might uh cause some errors and this is the most used version out there so this is step two for now I'm going to skip the create attendant secret step but we'll be back to it I went ahead and created a new user again on the user service I created another method called create user which takes a user which looks like here name email password and then I use bcrypt which is a package for hashing I have a lot of videos covering this in depth basically we're just going to Hash the user password and then the so rounds is going to be 10 and this way we're going to have a secure hashed password that we're going to save using user model. create and then we pass in the user data and the Tenant ID of course uh to which this user is going to belong to this is simple up until now and finally we need to create a tenant record so I went ahead and on our tenant model I just called the create method and passed in the company name and the Tenant ID those are the fields that we have in our tenant schema of course you can have the owner email so the email of the user that have created this and so on but I'm not going to focus on this part of the video so let's go back now before handling the tenant secret let's try to call this API end point so we're going to try and create a company of course here we would use a validator either Joy or maybe the default validation pipe with class validator to actually validate the payload body with decorators and so on but I'm not going to focus on this part in this video I have another tutorial covering this basically we need to pass in a company name and some user data so let's go to postman and do that I have company name Microsoft and then some user data the email name and the password of this user if you go ahead and hit send okay we got an error so let's go to the ten controller take this and go back to our module and then here we need to add it of course and now if we restart our server now hit send as you can see we got some data return the company name the tenant ID that was generated so as you can see here we use Nano ID it's 12 characters as you can see here this is going to be the unique 10 ID for this company and if you go back to our database if we refresh inside of the user's collection we now we now have a user called John this is the password it's hashed and then we have the tenant ID that John belongs to which is this one right here if you go to tenants as you can see we got a third company now which is company name and then tenant ID now if we refresh here you might notice that we don't have a new database created here and that's because we have not yet made a connection to that database but if you were to take this tenant ID and then go back to the get products API and then in X tenant ID set this tenant ID and hit send of course we got an empty array because this tenant does not have any products yet and now if we refresh as you can see we got this new tenant underscore and then the tenant ID and this is the products and now you can see why we actually have a validation on the tenant so basically if we had some random characters here and hit send we would get an error and it would not create a new Tenon database here this is just a side note as to why we actually uh check if a tenant exists before moving on so we don't actually create a connection and create a new database for a tenant that does not exist this is just a side note and now we still need to implement this task we need to create a tenant secret again I want you to understand why we need to have different secrets for different tenants because usually and if you've seen my videos or use authentication before you know that you would have a JWT secret key here that you would use whenever you are generating new access tokens to your app users or whenever you are decoding and verifying a token inside of your o card but in the case of multi-tenancy you need to have different secrets for different tenants so that for example John which belongs to company Microsoft anytime he needs to log in and access his data inside of his company he need to have an access token that only works for his own company he should not be able to call and the products for company tenant ABC for example now there are a few approaches that we could go for for example we could add a secret here of course it could be encrypted or hashed uh we could add it inside of the tenants here but what I'd like to do is actually encapsulate or isolate the secrets inside of each tenant database so instead of having it here which you could do by the way inside of the master database inside of the tenant collection you could have a secret field here just like you have a company name and the Tenant ID but I prefer actually isolating the secrets inside of each database I feel like it would be more secure that way and isolated so let's go ahead and actually create a schema okay which is going to belong inside of tenant specific databases so I went ahead and created a no module using Nest generate resource off and then of course we get a controller a module and the service and then I went ahead and created a secret schema file in which we're going to have one field JWT secret of course here you could have more Fields if you have more API keys or more secrets or anything else this could go here as well inside of the secrets collection and now since the secrets collection is not going to be inside of the master DB like users for example we're not going to register it this way like we did with user because if we if we were to register it this this way this would create a collection or a Secrets collection inside of the master DB which is not what we actually want instead we want to create a collection inside of the databases that are specific to each tenant so the next step is to actually go ahead and create a random tenant secret here and then encrypt it before saving it inside of this tenant specific database so let's see how we can do that step by step the first step is I went to off service and then and I created two different methods the first one called create secret key for new tenant which is going to take the tenant ID all right this way we can actually know which database we should be storing this Secret in okay so this function is just to create a new secret key and store it of course we need to encrypt The Secret inside of this method before saving it in our database and the second method is going to fetch the access token secret key for a specific tenant which is why it also takes a tenent ID so if we were to break it down into smaller tasks the first thing we need to do is generate a random secret key we could go ahead and use Nano ID just like we did with the tenant ID and then we need to encrypt that secret key so we're going to use a package that encrypt this key using some encryption key that we can store in our environment variable securely and then the next step would be to access the tenant specific model so just like we did here with products whenever we need to return the list of products we were actually uh referring the model correctly inside of a tenant ID so we need to do something similar here and then finally we would need to store the encrypted secret key inside of the correct database so the first step is pretty simple I just use Nano ID this is going to give me a 128 random character string which is going to be in plain text which we need to encrypt now for the encryption we're going to use a package called cryp R which uses as 256 to encrypt and decrypt which is one of the best encryption algorithms out there so I created a u types folder in which I have encrypt and decrypt and I installed Crypt R and then here this takes some data which is going to be the string that we need to encrypt and then of course an encryption key that we're going to use to encrypt our data now I followed the documentation this is how you actually use it you create a new instance of Crypt R and you pass in a secret as a string and then you call encrypt on that instance and this is how you get the encrypted version of your string and for the decrypt it's going to be the same but instead of using encrypt we're going to be using decrypt so let's go ahead and do that so here we have decrypt and then here we're going to decrypt the data and we can call this encrypted data for example just to make it more meaningful as a name and this is pretty much it now of course inside of our config let's go and create something here to read that encryption key I'm going to call this security and then inside of security I'm going to have uh let's say encryption secret key or something and then inside of our EnV we're going to read encryption key and here of course we can add this encryption key here it would be some random long string I'm just going to say encryption encryption key and of course this should not be pushed to your GitHub repo this should be save inside of a vault or your EnV and your EnV should not be U it should be inside of get ignore to make sure that you're not pushing it because it's considered a very sensitive piece of data and I just called this encrypt function that we just created now inside of our utiles and of course we need to pass in the JWT secret that we created with Nano ID and then we need to pass in the encryption key that we have just created here inside of our NV and inside of our config so to access that all we had to do was inside of our off service inject the config Service as a dependency of course this works because in the previous video we have actually configured and registered our config module globally so we can use it here and this is going to give us the encrypted secret that we can store now safely in our database now we need to find a way to actually refer to the correct collection here so to the correct Secrets collection ins side of the tenant in hand now you might think that we can simply go ahead and just like we did with product model go ahead and create a second custom provider which we can call for example Secrets model this would create or retrieve a model on the 10 connection and then everything is fine but this would not work and to understand why it wouldn't work with the secrets uh model in this specific scenario let's go back and explain or think about how sjs is actually handling those providers and creating them uh whenever needed so just like I said at the beginning of the video and in the last video okay whenever we have a dependency or we're trying to inject a provider inside of a service so for example let's go to products module or products service here we are injecting the product model provider that we have created that Returns the correct model inside of some tenant database now how this happens behind the scenes is njs finds that a request is going inside of this route Handler for example and this route Handler or this controller depends on the product service and then here it finds those dependencies that it should instantiate so that we can actually use this product service okay and to be able to instantiate this product model if you go back to the providers file here okay we need to have a dependency instantiated before being able to actually get access to this model which is tenant connection why because product model depends on tenant connection meaning logically njs needs to create the tenant connection before creating the product model and if you go to tenant connection if you take a look at the code here we need to actually have a tenant ID attached on the request so that we can actually return the correct connection to a specific tenant database meaning we need to have a tenant ID on the request meaning we should actually apply the tenant middle on that request but this would not work because in the case of create company we're not actually sending a tenant ID inside of the Heathers because we don't even have a tenant ID yet we are trying to create a tenant with a new tenant ID so we don't really have a separate database yet we need to create it here so in this case we can't really use the standard connection provider and we cannot really create another model here which uses this tenant connection because we don't even have a tenant ID so the solution would be to find a way to switch the database connection inside of our business logic meaning inside of our service here we need to find a way to switch to the correct connection here because here we got the tenant ID after having created it inside of our tenant service so here if you go back to our tenant service we created a new tenant ID okay and of course we're going to save it but first we need to go ahead and switch to the correct database or change our connection to point to another database and then create that secret key so how do we do that if you remember in the first video we saw two different approaches to access our correct tenant database and the first approach in which we said was kind of dirty if you remember we used to inject the connection which is from Mongoose the Mongoose connection pointing at the master database and then we wrote a method get tenant connection which takes a tenant ID and then we used use DB to switch to the correct tenant database and then after having this connection inside of our get products method we did tenant connection model and then we specified the name and then we passed in the schema which is kind of similar to what we then ended up doing inside of our custom providers as well we are using model on the tenant connection but this time it's from a different provider but the main difference between those two different approaches is that in this approach we needed to have the tenant ID on the request itself and then this allowed us to instead of injecting the whole database connection and then switching the connection and then creating the model or retrieving it we simply just had to inject this product model in here unlike this case in which we had to inject a connection then switch it and then all of that so this part of the code was applied in our different custom providers but this time since we don't have a tenant ID on the request object itself we have to go with this second approach in which we use inject connection because in this approach we can simply pass in a tenant ID that was not existent in the main request itself just like we have here if you go to postman we don't have an X tenant ID header here so instead of using the custom provider approach which requires us to have a tenant ID on the request we're going to use this approach in which we just inject the connection and then based on the tenant ID passed or created we can get the model I went ahead and created a separate service called tenant connection service of course don't forget to decorated with at injectable so it can be used as a dependency in our product service and then here just like we did before inside of product service we injected the connection so that's what I did here I did add inject connection and then we got the connection and I've created two different methods the first one called get tent connection which switches to this tenant uncore tenant ID database and then we call this method inside of get tenant model which takes a name and a schema for the model and then the tenant ID so so here we're basically just applying the same logic that we did here but instead of having it inside of the product service right away we created a separate service called tenant connection service that gets the tenent connection and then Returns the cor the correct model so we use tenent connection do model and then name and schema and now to be able to use the standon connection service to fetch the correct model for the secrets all we have to do is go to O module and then here of course we need to register the tenant connection service so that we can use it here to get access to the tenant specific model so inside of the Constructor let's go ahead and say private tenant connection service and add it as a dependency and then what you could say is const Secrets model equal wait test T connection service. Get Talent model and of course we need to pass in a name and the schema and for the name just like we usually do we can say secrets. name so Secrets dot name and then of course for the schema we need to pass the secrets schema that we have created right here and finally of course we need to pass in the tenant ID which we are going to be sending to this create secret key for tenant ID and with that in place we now have a model that you can use so here to store the encrypted secret key what we could say is await Secrets model do create and then we can pass in JWT secret because this is the only field that we have so let's go ahead and JWT secret of of course we need to pass in the encrypted uh version of it so encrypted secret and now there's one more thing that we need to do which is call this method from our create company API so here whenever we need to create a tenant secret let's go ahead and say await this dot here we need to use the O service so to do that let's go to O module and then in the exports array let's go ahead and add o servers and of course inside of tenants module we need to import the O module here to use this let's go ahead and add o service which is of type of service and now we can simply just say await this. service. create secret key for new tenant and of course we need to pass in the tenant ID that we have just created for this new company so now if we take a look at what we have the first step is actually checking if a user exists already in our database if that's the case we do not allow this user to create a company because one user should belong to one company then we create a tenant ID which is a random string unique for this tenant and then we need to create a secret for this tenant so inside of the tenant database we need to create a Secrets collection which will have the encrypted secret that we should use to generate JWT tokens for the specific tenant so we have called this Earth service method create secret key for new tenant which is going to generate a long random string and then we're going to encrypt it and then we are going to fetch the correct Secrets model for this tenant by using our tenant connection service right here and we explained why we should do it this way and then after getting this Secrets model we are able to create this new document inside of the correct database Let's test this out and make sure that it works fine I went ahead and deleted the third company and the user and now if you open post and hit send again okay we got an error which comes from the encrypt function we got Crypt R default is not a Constructor to fix this we can go to our TS config Json and then simply just set this field to true if we restart our server if you go to postman and hit send again we got secret must be a non zero length character that's because our encryption key is not being read correctly so let's go back to config and then here we should read from config service. security. encryption secret key let's see what we're doing all right so here we made a mistake it should be security do encryption secret key okay now let's go back and hit send and as you can see we got a company Microsoft and then a new tent ID ending with ou go if you go back to the database refresh we got this new record in our tenants if you go to users we can see that we got a new user that belongs to the same Talent ID and if you were to refresh here we can see that we got a new database and if you open that database we can see that we got a new collection called Secrets which has one document with one JWT secret which is correctly encrypted so we successfully finished our first API which creates a new tenant a new user a database for the tenant and stores the JWT Secret in an encrypted way now we need to create a login API for the user which will return an access token signed by the correct JWT secret and then return it back to the client I went ahead and created a login route Handler inside of our o controller and then it's going to take some credentials from the body and I created this credential dto email and password just to authenticate a user make sure everything is correct and of course you should be using a class validator and so on and here we have a few things that we need to do first of all we need to find if the user exists by email then we need to compare if the entered user email matches the one in the database of course we are comparing the hashed version using bcrypt and then we are going to fetch the tenant specific secret key just like this one and of course we need to decrypt it to get the plain secret key which we will use to generate a JWT access token for our user to use in authenticated apis so for the first task I went ahead and extracted the email and the password from credentials and then I called the user service get user by email method and of course to be able to use the user service we had to inject it as a dependency here and of course in O module we had to import the users module here after doing so we can call this get user by email method that we have created at the beginning of this video and then if the user does not exist throw an error wrong credentials and then the next step is simple we just have to use bcrypt do compare and then pass in the plain password that the user entered in this API request and then the user. password the hashed version that exists in our database and if the passwords do not match we throw an error I went through the same steps in a previous video called authentication which had uh different authentication apis such as login sign up and so on so make sure to watch those videos if you don't really get what I'm doing now now we need to call this fetch access token Secret signing key and then return the correct secret key for this tenant so that we can generate a token so to do that just like we did here for Secrets model we need to get that model so here we could say Secrets model equal A8 okay of course here if you don't want to have to repeat those you can add them in a config and then pass in the config key which would have this nested object you can of course uh refactor this code but with this Secrets model we can do find one to get this document we only have one document in this so we would be able to get this document and on this document we have the JWT secret which is encrypted which we need to decrypt so we can simply call the decrypt function that we have created and of course we need to pass in the encrypted data which is the secret do. JWT secret meaning this value here which is encrypted and then of course we need to pass in the encryption secret key that we pre prly used to encrypt that piece of data which is of course the one that is here and now we can simply just return this uh secret key as a plain text and now we are able to call this fetch access token secret signing key inside of our login API so we could say secret key equal await this do fetch access token secret signing key and of course we need to pass in the tenant ID the tenant ID exists on the user because if you remember whenever we created a user we assigned the tenent ID to which this user belonged to so here we could say user do tant ID and now we simply just need to generate a new access token for this user using this secret key which is specific to this tenant or to the tenant that the user belongs to of course of course to be able to do that we need to install njs SL JWT and once this is done we can go ahead in app module. TS and just in our Imports array say JWT module. register and then here we can pass in some configurations I just went ahead and set this module to be Global now in previous videos I used to set the secret key here which would globally use the same secret key anywhere in our app but of course since we said in multi- tency we need to use different secrets for our different tenant databases or in general I'm not going to set a secret key here I'm going to set it inside of our code first of all let's go ahead and add the JWT Service as a dependency here so JWT service and now what we can do is for example say access token equal await this. JWT service do sign and then here we need to pass in some payload I'm just going to pass in the user ID of the user and then we can pass in some options here such as the secret which is going to be the secret key that we got with this function here and I also just added some expiration date here now what we should do is simply return this access token back to the client and we should also return the tenant ID this is a very important step here you should not forget to return the tenant ID and this is important because if you go back to postman whenever we are making a request to SL products for example to get the correct products of course we would need to know which tenant is making the request which will allow us to connect to the correct database but of course on top of that now we're going to be sending a better token after logging in and this access token would only work on the secret key that was provided from the specific tenant database now let's go ahead and test this API on Postman so now if we go ahead and make a call to us login and then we pass in the email and the password of our user of John if you go ahead and hit send as you can see we got tenent ID which ends with a oou which is the tenent ID that John belongs to which is this one right here which has this secret and then of course we got an access token which belongs to John we're done creating the login API that returns an access token specific to a certain tenant now we still need to create a tenant authentication guard which will verify an access token sent in the request and make sure that this token works correctly with the tenant ID or the tenant making the request I went ahead and created this tenant authentication guard inside of a guards folder just like we did in many previous videos we implemented can activate which is an interface which provides us with the can activate method that we need to uh Implement and of course here we need to return true to give access to the route or false or throw an error to stop the request from proceeding just like we usually do in different videos we inject the JWT service so that we can use it to verify a token but here it's a bit different because we have some extra steps to make sure that the token belong to the correct tenant so as usual first of all we get the request object from using the context. switch to http.get request and now here what we're doing is we are making sure that the request has a tenant ID attached to it and this should be the case every single time we apply the tenants middleware on our route because if you remember this middleware takes the request header's X tenant ID value and then if everything is fine and this tenant does exist correctly it is going to attach that tenant ID on the request object which will then inside of oard of our oard take the Stant ID value and then using the Stant ID value we are going to fetch the secret okay the secret of that tenant so that we can use it to verify that this token really belong to this tenant so let's go back to the code and take it step by step the first thing we're doing is just like every single time in every single oard that we have we fetch the token just by doing request. headers do authorization and then we're doing the split on the space and taking the first index instead of index zero because as you know we have bar space and then the token here so we're just taking this part which is the actual token string and then if this token does not exist of course we're going to throw an ER and if it does exist then I just made a second method called check token validity which takes the token that we just took from the request and then the tenant ID that we also got from the request meaning the tenant ID that the user has set here or the extended ID that the user has set here in the request headers now here I need you to focus with me because this part is uh very important so inside of check token validity okay the first thing we need to do is is fetch the secret because as you know whenever we have or we need to verify a token and we use JWT service. verify we need to pass in the token from the request and then the secret which usually would be in our uh EnV file but since this is a ten multi- tency application of course we're not going to have our secret here instead it's going to be isolated in our uh tenant specific databases just like we have here so the next step would be to actually get the correct secret for this tenant ID again the tenant ID is the one that the user is passing here so if they do manually change this to XYZ for example but they logged in to ABC then this token would not work because the secret for XYZ is not the same secret used when creating the access token of a tenant or of a user belonging in ABC and this is what I was saying when I told you guys that we should have separate JWT secrets to avoid having security breaches so now we're following the correct path now here what we did is we used the off service of course we had to inject it here as a dependency and then we called this fetch access token secret signing key method that we used previously and it takes the tenant ID that we pass and then gets the correct Secrets model for from the correct tenant database and then we're going to get that secret key decrypt it and get the secret key as a plain text and then here having this secret key we can actually verify if this token really was signed by this sec secret key meaning that this token which was uh generated for a specific tenant ID works with this specific secret so if someone from tenant XYZ triy to actually send a request to ABC with a token belonging to XYZ it would not work and we will see that in action but first let's continue with our code whenever we do verify if it works it's going to give us a payload if you remember whenever we created the token we actually added a user ID in it so we can return it here and if it this fails maybe the token expired maybe the secret used was wrong then it is going to throw an error and the request cannot proceed if everything is fine here we're going to attach this user ID on the request object on user info and then I'm going to return through meaning the request can proceed to the route Handler now let's go ahead and apply our tenant authentication guard by using at use guards decorator on top of the controller which will apply this authentication guard on our uh products routes so here if you take a look we got an error saying NES can't resolve dependencies of the talent authentication guard if you take a look JWT service is fine but we have o service which is not known so we have two solutions either we can make us service a global uh service and that way we can use it everywhere or we can simply just in products module in our Imports array just add the O module here and then we should be able to get rid of that error and just to remind you before actually applying this authentication guard Nest is going to apply this middleware because if you can see here we are applying this tenant middleware on the route products controller which again will take the X tenant ID and then add it to the request tenant ID which then the tenant authentication guide is going to take it and using this get the correct secret and then compare it with the token let's go ahead and check this out so I'm going to take this access token for John and then I'm going to add it here in better token okay and then here for the extended ID we should take this one and then add it here if we hit send as you can see we got an empty list of products because we don't really have a products array here I went ahead and added one product which is a hat now if you go back and hit send as you can see we got the Hat now what happens if you don't actually send the extended ID the middleware should throw an error and if you do hit send as you can see extended ID is not provided now if we do provide this but we don't actually send a token what happens if we hit send as you can see we get missing access token from our authentication guard now what if we do have a token but instead of using the correct tenent ID that John received from the login API which is the tenant that John belongs to what if we went ahead and set this to ABC send as you can see we got unauthorized again because the secret that was used to generate this access token for this tenant ID okay which belongs in this database here is different from the one that would have been for tenant ABC and just to show you with an actual uh tenant that we create with our apis let's go ahead and create another company let's say Google for example and then here we have Jeff okay and Jeff creates a new company we got this new tenant ID if you come here go to tenants we can see that we have Google and this is the second tenant ID j7f if we refresh here we can see that we have this new tenant database here with a different secret here so now if Jeff went ahead and actually logged in he should get an access token that was signed with this secret key and this secret key or this access token that belongs to this tenant ID would not work with the one that John belongs to and of course now if you go to the users in master and refresh we can see that we have two different users belonging to two different tenants so now let's do something let's log in with Jeff this time so this is Jeff and this is Jeff's password 1 2 3 if we hit send as you can see the tent ID for Jeff is different from the one from John okay and the access token of course is different as well now if you take the access token of Jeff and go back here to get products and we replace this bar token okay and this time we try to make a request to the tenent ID that John belongs to okay so this is Jeff this is the talent idea of Jeff but Jeff wanted to play smart and actually try to fetch the products of tenant John or of the company that John belongs to meaning the products of Microsoft which should be uh private to Microsoft let's say he went ahead and put this tenent ID and if he tried to hit send as you can see he got unauthorized which is the right behavior however if he try to fetch his own products from his own tenant ID or his own tenant j7f if he hit send as you can see got an empty array which is because we don't have products here but as you can see it works perfectly fine so to summarize we have created a cre create company API inside of our tenants controller which is going to take the company name and the user information and then we are going to verify that this user does not already exist and belong to a company and then we're going to create a new tenant ID which is going to be unique for that specific tenant or company and then of course we need to create a secret key which is specific for that tenant which like we said this is for security purposes we go ahead and create a Secrets collection for every tenant data base so that we can use it to create specific access tokens that belong to a specific tenant isolated from other tenants so we went ahead and generated a random string which is of 128 characters we went ahead and used an encryption algorithm which is AES with a package that is going to encrypt that key and then return it in an encrypted Manner and after that we use the tenant connection service which like we said we had a problem here that on our request we didn't have a ID attached which meant that we cannot actually use this or we cannot actually create a specific custom provider here for our secrets model just like we did with product model and this is because we don't really have a request. tenant ID field here which is why we cannot use this tant connection so we had to do it in another way which was to actually create a service and then inject our connection the connection and then here we could get a tenant ID somehow in our code code in our case it was by creating a new tenent ID which was going to be used for the new company and this is the tenant ID that we sent so again we don't have a tenant ID on the request itself which is why we had to use this uh tenant connection service okay to get the correct model for our database which is this right now the secrets model and then after getting the secret model we went ahead and saved the encrypted JWT Secret inside of our database which is the new document that we're going to use to verify and create new tokens for this or for users that belong to this specific company then of course inside of create company after creating the secret we went ahead and created the user and here we hashed the password for the user to make sure it's protected and then finally we went ahead and created a new tenant model or a new document for this company in our database we save the company name and the tenent ID and of course it would be a good practice to save some more information that might be useful such as the owner email then we had to create a login API so that our users can actually log in and receive a tenant specific token to perform some operations and receive some data so we created a no module here and then we created a login API which takes some credentials and then of course we needed to get the credentials make sure the user exists and then the password is correct and if that's the case we needed to fetch the correct secret key for this user okay from the tenant that they belong to which is why we created this method of course we use the tenant connection service again to get the correct model from the correct tenant connection which we switched to here based on the tenant ID that the user has in in his uh document so here for example John has tenant ID ending with ou and this would be the tenent ID that we use to fetch the correct secret that we're going to use to generate the token for John and this is what we've done here so we got this secret of course we needed to decrypt it first because it's encrypted and then return it back and then here we can finally use the JWT service to sign our token and then we add it on payload here we can add any useful information that we might need of course be careful not to send some sensitive data back and here we pass in the secret key to uh as an option to actually sign it with the correct secret key and finally we return both the tenent ID and the oess token back to the client so that they can use them in the headers when making a request to fetch their data and the final step was to actually create a tenant authentication guard which is going to get the tenant ID from the request of course this works because we have applied the tenant middleware first and then we are going to check the validity of this access token by getting the secret from the tenant database based on the tenant ID passed in the request and then we're going to to use this secret with this token to actually verify if it is valid if that's the case you are going to return the data of the user or to payload back and then attach it on the request and return through and this way we have protected our routes with an access token and this access token is specific to the tenant making the request so a user belonging to company Google cannot use an access token which was generated for company Microsoft in the request this would not work just like we've tried doing here by changing the tenant ID if this video was helpful please make sure to like And subscribe and comment it would really help me out share it with your friends and I hope everything was clear make sure to uh leave your questions down below if you have any questions and if you'd like to see another video where we uh cover the case where one user can belong to an array of tennis so to many companies which would require us to go with a different flow let me know down below and I'll see you next time thank you for watching
Info
Channel: Computerix
Views: 544
Rating: undefined out of 5
Keywords: nestjs, authentication, multitenant, multitenancy, javascript, multi-tenant, multi-tenancy
Id: 7PSboNf4RuQ
Channel Id: undefined
Length: 57min 55sec (3475 seconds)
Published: Wed Jun 19 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.