Skip to content

Commit b412855

Browse files
committed
Provide a rate limiting filter.
1 parent 5259a70 commit b412855

File tree

6 files changed

+122
-43
lines changed

6 files changed

+122
-43
lines changed

docs/2 - authentication.md

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Authentication
22

3-
Shield provides a flexible, secure, authentication system for your web apps and API's.
3+
Shield provides a flexible, secure, authentication system for your web apps and API's.
44

55
## Available Authenticators
66

@@ -26,7 +26,7 @@ public $defaultAuthenticator = 'session';
2626

2727
## Auth Helper
2828

29-
The auth functionality is designed to be used with the `auth_helper` that comes with Shield. This
29+
The auth functionality is designed to be used with the `auth_helper` that comes with Shield. This
3030
helper method provides the `auth()` command which returns a convenient interface to the most frequently
3131
used functionality within the auth libraries. This must be loaded before it can be used.
3232

@@ -37,7 +37,7 @@ helper('auth');
3737
auth()->user();
3838

3939
// get the current user's id
40-
user_id()
40+
user_id()
4141
// or
4242
auth()->id()
4343
```
@@ -50,13 +50,13 @@ has the following methods:
5050

5151
### isOK()
5252

53-
Returns a boolean value stating whether the check was successful or not.
53+
Returns a boolean value stating whether the check was successful or not.
5454

5555
### reason()
5656

5757
Returns a message that can be displayed to the user when the check fails.
5858

59-
### extraInfo()
59+
### extraInfo()
6060

6161
Can return a custom bit of information. These will be detailed in the method descriptions below.
6262

@@ -65,7 +65,7 @@ Can return a custom bit of information. These will be detailed in the method des
6565

6666
The Session authenticator stores the user's authentication within the user's session, and on a secure cookie
6767
on their device. This is the standard password-based login used in most web sites. It supports a
68-
secure remember me feature, and more. This can also be used to handle authentication for
68+
secure remember me feature, and more. This can also be used to handle authentication for
6969
single page applications (SPAs).
7070

7171
### attempt()
@@ -75,11 +75,11 @@ on the auth class, passing in their credentials.
7575

7676
```php
7777
$credentials = [
78-
'email' => $this->request->getPost('email'),
78+
'email' => $this->request->getPost('email'),
7979
'password' => $this->request->getPost('password')
8080
];
8181

82-
$loginAttempt = auth()->attempt($credentials);
82+
$loginAttempt = auth()->attempt($credentials);
8383

8484
if (! $loginAttempt->isOK()) {
8585
return redirect()->back()->with('error', $loginAttempt->reason());
@@ -97,11 +97,11 @@ if($result->isOK()) {
9797
}
9898
```
9999

100-
If the attempt fails a `failedLogin` event is triggered with the credentials array as
100+
If the attempt fails a `failedLogin` event is triggered with the credentials array as
101101
the only parameter. Whether or not they pass, a login attempt is recorded in the `auth_logins` table.
102102

103103
If `allowRemembering` is `true` in the `Auth` config file, you can tell the Session authenticator
104-
to set a secure remember-me cookie.
104+
to set a secure remember-me cookie.
105105

106106
```php
107107
$loginAttempt = auth()->remember()->attempt($credentials);
@@ -114,11 +114,11 @@ method.
114114

115115
```php
116116
$credentials = [
117-
'email' => $this->request->getPost('email'),
117+
'email' => $this->request->getPost('email'),
118118
'password' => $this->request->getPost('password')
119119
];
120120

121-
$validCreds? = auth()->check($credentials);
121+
$validCreds? = auth()->check($credentials);
122122

123123
if (! $validCreds->isOK()) {
124124
return redirect()->back()->with('error', $loginAttempt->reason());
@@ -140,7 +140,7 @@ if (auth()->loggedIn()) {
140140
### logout()
141141

142142
You can call the `logout()` method to log the user out of the current session. This will destroy and
143-
regenerate the current session, purge any remember-me tokens current for this user, and trigger a
143+
regenerate the current session, purge any remember-me tokens current for this user, and trigger a
144144
`logout` event.
145145

146146
```php
@@ -149,52 +149,52 @@ auth()->logout();
149149

150150
### forget()
151151

152-
The `forget` method will purge all remember-me tokens for the current user, making it so they
152+
The `forget` method will purge all remember-me tokens for the current user, making it so they
153153
will not be remembered on the next visit to the site.
154154

155155
## Access Token Authenticator
156156

157157
The Access Token authenticator supports the use of revoke-able API tokens without using OAuth. These are commonly
158-
used to provide third-party developers access to your API. These tokens typically have a very long
159-
expiration time, often years.
158+
used to provide third-party developers access to your API. These tokens typically have a very long
159+
expiration time, often years.
160160

161161
These are also suitable for use with mobile applications. In this case, the user would register/sign-in
162162
with their email/password. The application would create a new access token for them, with a recognizable
163163
name, like John's iPhone 12, and return it to the mobile application, where it is stored and used
164164
in all future requests.
165165

166-
### Access Token/API Authentication
166+
### Access Token/API Authentication
167167

168-
Using access tokens requires that you either use/extend `CodeIgniter\Shield\Models\UserModel` or
168+
Using access tokens requires that you either use/extend `CodeIgniter\Shield\Models\UserModel` or
169169
use the `CodeIgniter\Shield\Authentication\Traits\HasAccessTokens` on your own user model. This trait
170170
provides all of the custom methods needed to implement access tokens in your application. The necessary
171-
database table, `auth_access_tokens`, is created in Shield's only migration class, which must be ran
171+
database table, `auth_access_tokens`, is created in Shield's only migration class, which must be ran
172172
before first using any of the features of Shield.
173173

174174
### Generating Access Tokens
175175

176-
Access tokens are created through the `generateAccessToken()` method on the user. This takes a name to
177-
give to the token as the first argument. The name is used to display it to the user so they can
178-
differentiate between multiple tokens.
176+
Access tokens are created through the `generateAccessToken()` method on the user. This takes a name to
177+
give to the token as the first argument. The name is used to display it to the user so they can
178+
differentiate between multiple tokens.
179179

180180
```php
181181
$token = $user->generateAccessToken('Work Laptop');
182-
```
182+
```
183183

184184
This creates the token using a cryptographically secure random string. The token
185-
is hashed (sha256) before saving it to the database. The method returns an instance of
185+
is hashed (sha256) before saving it to the database. The method returns an instance of
186186
`CodeIgniters\Shield\Authentication\Entities\AccessToken`. The only time a plain text
187187
version of the token is available is in the `AccessToken` returned immediately after creation.
188-
**The plain text version should be displayed to the user immediately so they can copy it for
189-
their use.** If a user loses it, they cannot see the raw version anymore, but they can generate
188+
**The plain text version should be displayed to the user immediately so they can copy it for
189+
their use.** If a user loses it, they cannot see the raw version anymore, but they can generate
190190
a new token to use.
191191

192192
```php
193193
$token = $user->generateAccessToken('Work Laptop');
194194

195195
// Only available immediately after creation.
196196
echo $token->raw_token;
197-
```
197+
```
198198

199199
### Revoking Access Tokens
200200

@@ -205,9 +205,9 @@ access token as the only argument. Revoking simply deletes the record from the d
205205
$user->revokeAccessToken($token);
206206
```
207207

208-
Typically, the plain text token is retrieved from the request's headers as part of the authentication
208+
Typically, the plain text token is retrieved from the request's headers as part of the authentication
209209
process. If you need to revoke the token for another user as an admin, and don't have access to the
210-
token, you would need to get the user's access tokens and delete them manually.
210+
token, you would need to get the user's access tokens and delete them manually.
211211

212212
You can revoke all access tokens with the `revokeAllAccessTokens()` method.
213213

@@ -217,7 +217,7 @@ $user->revokeAllAccessTokens($token);
217217

218218
### Retrieving Access Tokens
219219

220-
The following methods are available to help you retrieve a user's access tokens:
220+
The following methods are available to help you retrieve a user's access tokens:
221221

222222
```php
223223
// Retrieve a single token by plain text token
@@ -232,9 +232,9 @@ $tokens = $user->accessTokens();
232232

233233
### Access Token Lifetime
234234

235-
Tokens will expire after a specified amount of time has passed since they have been used.
235+
Tokens will expire after a specified amount of time has passed since they have been used.
236236
By default, this is set to 1 year. You can change this value by setting the `accessTokenLifetime`
237-
value in the `Auth` config file. This is in seconds so that you can use the
237+
value in the `Auth` config file. This is in seconds so that you can use the
238238
[time constants](https://codeigniter.com/user_guide/general/common_functions.html#time-constants)
239239
CodeIgniter provides.
240240

@@ -244,7 +244,7 @@ public $unusedTokenLifetime = YEAR;
244244

245245
### Access Token Scopes
246246

247-
Each token can be given one or more scopes they can be used within. These can be thought of as
247+
Each token can be given one or more scopes they can be used within. These can be thought of as
248248
permissions the token grants to the user. Scopes are provided when the token is generated and
249249
cannot be modified afterword.
250250

@@ -257,9 +257,9 @@ same as:
257257

258258
```php
259259
$token = $user->gererateAccessToken('Work Laptop', ['*']);
260-
```
260+
```
261261

262-
During authentication, the token the user used is stored on the user. Once authenticated, you
262+
During authentication, the token the user used is stored on the user. Once authenticated, you
263263
can use the `tokenCan()` and `tokenCant()` methods on the user to determine if they have access
264264
to the specified scope.
265265

@@ -275,20 +275,37 @@ if ($user->tokenCant('forums.manage')) {
275275

276276
## Controller Filters
277277

278-
Shield provides 3 [Controller Filters](https://codeigniter.com/user_guide/incoming/filters.html) you can
278+
Shield provides 3 [Controller Filters](https://codeigniter.com/user_guide/incoming/filters.html) you can
279279
use to protect your routes, `session`, `tokens`, and `chained`. The first two cover the `Session` and `AccessTokens` authenticators, respectively. The `chained` filter will check both authenticators in sequence
280-
to see if the user is logged in through either of authenticators, allowing a single API endpoint to
280+
to see if the user is logged in through either of authenticators, allowing a single API endpoint to
281281
work for both an SPA using session auth, and a mobile app using access tokens.
282282

283283
These filters are already loaded for you by the registrar class located at `src/Config/Registrar.php`.
284284

285285
```php
286286
public $aliases = [
287287
// ...
288-
'session' => \CodeIgniter\Shield\Filters\SessionAuth::class,
289-
'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class,
290-
'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class,
288+
'session' => \CodeIgniter\Shield\Filters\SessionAuth::class,
289+
'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class,
290+
'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class,
291+
'auth-limit' => \CodeIgniter\Shield\Filters\AuthRates::class,
291292
];
292293
```
293294

294295
These can be used in any of the normal filter config settings, or within the routes file.
296+
297+
### Rate Limiting
298+
299+
To help protect your authentication forms from being spammed by bots, it is recommended that you use
300+
the `auth-limit` filter on all of your authentication routes. This can be done with the following
301+
filter setup:
302+
303+
```php
304+
public $filters = [
305+
'auth-rates' => [
306+
'before' => [
307+
'login*', 'register', 'auth/*'
308+
]
309+
]
310+
];
311+
```

src/Config/Registrar.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace CodeIgniter\Shield\Config;
44

55
use CodeIgniter\Shield\Authentication\Passwords\ValidationRules as PasswordRules;
6+
use CodeIgniter\Shield\Filters\AuthRates;
67
use CodeIgniter\Shield\Filters\ChainAuth;
78
use CodeIgniter\Shield\Filters\SessionAuth;
89
use CodeIgniter\Shield\Filters\TokenAuth;
@@ -16,9 +17,10 @@ public static function Filters(): array
1617
{
1718
return [
1819
'aliases' => [
19-
'session' => SessionAuth::class,
20-
'tokens' => TokenAuth::class,
21-
'chain' => ChainAuth::class,
20+
'session' => SessionAuth::class,
21+
'tokens' => TokenAuth::class,
22+
'chain' => ChainAuth::class,
23+
'auth-limit' => AuthRates::class,
2224
],
2325
];
2426
}

src/Filters/AuthRates.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace CodeIgniter\Shield\Filters;
4+
5+
use CodeIgniter\Filters\FilterInterface;
6+
use CodeIgniter\HTTP\RedirectResponse;
7+
use CodeIgniter\HTTP\RequestInterface;
8+
use CodeIgniter\HTTP\Response;
9+
use CodeIgniter\HTTP\ResponseInterface;
10+
use CodeIgniter\Shield\Auth;
11+
12+
/**
13+
* Chain Authentication Filter.
14+
*
15+
* Checks all authentication systems specified within
16+
* `Config\Auth->authenticationChain`
17+
*/
18+
class AuthRates implements FilterInterface
19+
{
20+
/**
21+
* Intened for use on auth form pages to restrict the number
22+
* of attempts that can be generated. Restricts it to 10 attempts
23+
* per minute, which is what auth0 uses.
24+
*
25+
* @see https://auth0.com/docs/troubleshoot/customer-support/operational-policies/rate-limit-policy/database-connections-rate-limits
26+
*
27+
* @param array|null $arguments
28+
*
29+
* @return RedirectResponse|void
30+
*/
31+
public function before(RequestInterface $request, $arguments = null)
32+
{
33+
$throttler = service('throttler');
34+
35+
// Restrict an IP address to no more than 10 requests
36+
// per minute on any auth-form pages (login, register, forgot, etc).
37+
if ($throttler->check(md5($request->getIPAddress()), 10, MINUTE, 1) === false) {
38+
return service('response')->setStatusCode(
39+
429,
40+
$message = lang('Auth.throttled', [$throttler->getTokenTime()])
41+
);
42+
}
43+
}
44+
45+
/**
46+
* We don't have anything to do here.
47+
*
48+
* @param Response|ResponseInterface $response
49+
* @param array|null $arguments
50+
*
51+
* @return void
52+
*/
53+
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
54+
{
55+
// Nothing required
56+
}
57+
}

src/Language/en/Auth.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
'oldToken' => 'The access token has expired.',
1616
'noUserEntity' => 'User Entity must be provided for password validation.',
1717
'invalidEmail' => 'Unable to verify the email address matches the email on record.',
18+
'throttled' => 'Too many requests made from this IP address. You may try again in {0} seconds.',
1819

1920
'email' => 'Email Address',
2021
'username' => 'Username',

src/Language/fa/Auth.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
'oldToken' => 'توکن دسترسی منقضی شده است.',
2525
'noUserEntity' => 'برای اعتبار سنجی هویت کاربر بایستی رمز عبور ارائه شود',
2626
'invalidEmail' => 'امکان تایید ایمیلی که با آدرس ایمیل ثبت شده یکسان نیست، وجود ندارد.',
27+
'throttled' => 'درخواست های بسیار زیادی از این آدرس IP انجام شده است. می توانید در {0} ثانیه دوباره امتحان کنید.',
2728

2829
'email' => 'آدرس ایمیل',
2930
'username' => 'نام کاربری',

src/Language/ja/Auth.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
'oldToken' => 'アクセストークンの有効期限が切れています。', // 'The access token has expired.',
1616
'noUserEntity' => 'パスワード検証のため、Userエンティティを指定する必要があります。', // 'User Entity must be provided for password validation.',
1717
'invalidEmail' => 'メールアドレスが一致しません。', // 'Unable to verify the email address matches the email on record.',
18+
'throttled' => 'このIPアドレスからのリクエストが多すぎます。 {0}秒後に再試行できます。', // Too many requests made from this IP address. You may try again in {0} seconds.
1819

1920
'email' => 'メールアドレス', // 'Email Address',
2021
'username' => 'ユーザー名', // 'Username',

0 commit comments

Comments
 (0)