00:02
Now there's one more important step that we can take to help make our application even more secure and production ready than it is currently, and that's adding rate limiting.
00:12
Currently on our login form, there's nothing preventing somebody from trying to just go through and find a specific user's password. So we could sit here and hit login,
00:22
and login, and login over and over again, and there's nothing preventing this, and that's where rate limiting comes into play. It allows us to say, hey, you're gonna be allowed to do this,
00:31
but only three times within an hour, two hours, or whatever time duration we deem fit for this action. And then conversely, whenever they go over that limit,
00:39
we can block them to prevent this operation for X amount of time. And like everything else, AdonisJS makes this super simple to add. So within our terminal,
00:47
let's do node ace add at AdonisJS/limiter, and this is their rate limiter package. We'll hit yes to confirm that we want to install it, and then it will ask us which storage layer
00:57
we'll wanna use for it. The database or Redis. Now, Redis is gonna be a little bit quicker, but I don't believe we're using Redis anywhere else inside of the application, so let's stick with the database for this. So we'll hit database there.
01:07
And for AdonisJS/limiter, this is gonna add in a configuration, a start file where we will be able to define global rate limiters inside of our application. It's gonna add itself to our adonisrc.ts file.
01:17
It's gonna give us a migration to be able to store these rate limits inside of our database. If you selected Redis, you won't have this migration. It'll add itself to our EMV file,
01:26
specifying that we're gonna be using the database for this, and then we'll add an EMV.ts record for that new EMV property as well. Cool, so we can clear that out, jump back into our text editor,
01:36
and let's go ahead and open up our file explorer and dive into our start and take a look at that new limiter file. So here is an example throttle rate limit
01:45
that AdonisJS included out of the box in its configuration step. We can apply this throttle to one route or multiple routes, an entire route group, or the entire application as a whole.
01:54
And it's saying that it wants to allow 10 requests every minute. So if I go into the application, make 10 requests,
02:01
I'll be A-OK, but the minute I hit that 11th request, I'll start getting a rate limit exception, and I'll be continuously blocked for whatever route this is applied to
02:10
for whatever that duration cool down period is for the throttle. By default, when omitted, I believe that block duration is just one second,
02:18
but you can customize that with the block for chain to provide whatever duration you want. So if you want to block them for 24 hours, you could do that just like so. So we'll go ahead and take this off.
02:28
All we're going to do here today is apply rate limiting specific to our authentication. Limited at the finest, we see here is going to create a rate limiting middleware
02:36
that we can use like any other middleware on our routes. Additionally, it will also make the rate limiting specific to the user making the request to be their IP address automatically for us.
02:46
So we won't be making use of this right here, but I'll leave it here for reference in case you want to take a look at it later. Let's go ahead and dive into our app actions
02:55
and let's dive into our auth HTTP web login action. What we're looking to do is limit the amount of verify credential calls we do inside of this action for any one user.
03:05
For our authentication here, we want a very specific rate limiter just for this one action. So we'll create the rate limiter directly inside of our handle method.
03:14
So we can do const limit equals, and then use the limiter singleton service exported from AdonisJS limiter services main to create our own rate limiter
03:24
for this one individual action. So we'll call use to do that. And within this use method, we can provide in limiter consumption options. So for our login,
03:32
let's say maybe we want to allow three requests over the course of three hours. And when the user exceeds that limitation,
03:41
we want to block them for 24 hours. The number of requests per duration and then the block period that you define is completely up to you and your security requirements that you want to use.
03:51
Once we have our rate limiter defined, we want to individualize this to a specific user. We definitely don't want this to be broad based across all users.
04:00
So like if user A gets their password wrong, user B gets their password wrong, user C gets their password wrong, we don't want to end up blocking user D from being able to attempt to log in, for example.
04:08
So we'll give this a key of login underscore, and then we can use our HTTP context to grab the request IP address to attempt to individualize this
04:17
to a specific user and or computer. Furthermore, if you want to block this to a specific email attempt, you can do that as well by adding this into the key. So we can add the data email in there as well.
04:27
And this key is what the rate limiter is going to use to look up how many attempts the user has currently made inside of our database to determine whether or not the users allow to make more requests or not.
04:36
So now all that's left to do is to use our actual limiter. Kind of like managed transactions, there's a managed version of this called penalize. So we'll go ahead and try to use that. So let's write it out
04:46
and then we'll walk over what we're doing. So we'll use our limit and then call the penalize method. We'll want to provide our key into that as the first argument, and then a callback into the second.
04:55
Within this callback is where we're going to want to perform our operation that would count as an attempted login, which is our verify credentials method. We can go ahead and return directly back
05:05
the response of our verify credentials method, and this will automatically do the rest for us. All right, so the flow for this is a request comes in attempting to log into our application.
05:14
We hit this limiter line right here, and the penalize method will first check to see if all of the requests have been exhausted. So if the user has performed three requests
05:24
within three hours, or if the limiter had previously blocked them. If they are, then we're automatically just going to get back a rate limit exception via this error right here.
05:33
If they have not already been limited to some fashion, then our callback method will be performed. If our callback method performs successfully and does not throw an exception itself,
05:42
then we'll get back our verified user, the same as we had it before, where we were just taking the user directly back from our verify credentials. If however, our verify credentials throws that exception,
05:51
which is the E invalid credentials exception, that counts against the user's attempts against our rate limiter using the key that we've defined. So that the next time a request comes in
06:01
using that same key, it'll perform that same flow, checking to see whether or not they have then exhausted their limited attempts, so on and so forth. Now, one important distinction is
06:10
if our verify credentials throws an exception, this penalize method will rethrow that exception after it performs its incrementation on the number of attempts the user has. In our case, that is the flow that we want.
06:20
We already had our exception handler in our front end, automatically handling this exception for us. If however, you want to perform some other action beyond your exception handler,
06:30
you want to wrap this in a try catch so that you can still capture the inner exception from your callback method. So with this set, all we need to do now is determine whether or not we got an error.
06:40
So if we did get an error, we'll go ahead and call this CTX session/all, which will flash our input data to the session store, similar to what our validation errors do
06:50
for us automatically. Then we'll call this CTX session/errors to add a new error to our errors bag with the key E,
06:59
too many requests. And then the value for this would be whatever message we want to display to the user. So something like too many login attempts,
07:08
please try again later. Now we have red squigglies down here on our user because the type is currently user or null being returned back from our penalize method
07:16
as our second spread out of our array. However, the return type of our verify credentials is user. There's no null here. Internally within the penalize method,
07:25
if it gives back an error, then user is going to be null. If it does not give back an error, then it's going to be whatever the return type is here. So that's where that or null is coming from.
07:35
And all we need to do is return if we do get an error. And I'm just going to return null here. Really what we want to do is redirect user back to the login page, but we don't want to do that inside of our action.
07:45
We'll leave that for our controller to do. So we'll return null here as a signal to our controller that this action did not perform successfully. And then we can dive into our controllers,
07:55
auth login controller to get that user back. So const user equals await web login handle. If not user, then we're going to want to go ahead
08:04
and return response redirect back. And with all of that set, we should be able to save this. Jump back into our browser now. I'm going to go ahead and type in some credentials.
08:13
So just something there for the email and then whatever for the password. We'll hit login once. And we forgot to run the migration.
08:20
So let's dive into our terminal, node is migration run. There we go. Now we should be good. Our rate limits table exists now inside of our database.
08:30
Let's give that one more go. So log in again. There we go. We got one error. And with how we have our rate limiter set anytime between now and three hours from now, if we try to log in three more times,
08:40
we'll end up getting blocked on that third attempt, unable to move onward. So we go ahead and log in one more time, two more times, three more times. There we go. Too many login attempts.
08:50
Please try again later. We keep trying. We're just going to keep getting that exception down there. And it's actually not even running our verify credentials thanks to that penalize method.
09:00
So if we jump back into our action, the penalize method only performs the callback after it's verified that the request's attempts have not been exhausted. If they have been exhausted,
09:10
then it will throw a rate limit error from the get-go. So our callback's not even being performed whenever we hit that rate limit exception. So right there, not even being performed. And so now we have a hard limit
09:19
on the number of login attempts a user can perform to try and get into our application.