Skip to content

Commit a3030f9

Browse files
authored
Merge pull request #795 from geniza-ai/feature/HMAC
feat: HMAC SHA256 Authentication
2 parents 4dcee8f + 9d223a4 commit a3030f9

File tree

21 files changed

+1650
-16
lines changed

21 files changed

+1650
-16
lines changed

.github/workflows/phpcpd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ jobs:
3333
coverage: none
3434

3535
- name: Detect duplicate code
36-
run: phpcpd src/ tests/ --exclude src/Database/Migrations/2020-12-28-223112_create_auth_tables.php
36+
run: phpcpd src/ tests/ --exclude src/Database/Migrations/2020-12-28-223112_create_auth_tables.php --exclude src/Authentication/Authenticators/HmacSha256.php --exclude tests/Authentication/Authenticators/AccessTokenAuthenticatorTest.php

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ These are much like the access codes that GitHub uses, where they are unique to
3535
can have more than one. This can be used for API authentication of third-party users, and even for allowing
3636
access for a mobile application that you build.
3737

38+
### HMAC - SHA256
39+
40+
This is a slightly more complicated improvement on Access Codes/Tokens. The main advantage with HMAC is the shared Secret Key
41+
is not passed in the request, but is instead used to create a hash signature of the request body.
42+
3843
### JSON Web Tokens
3944

4045
JWT or JSON Web Token is a compact and self-contained way of securely transmitting
@@ -46,7 +51,7 @@ and authorization purposes in web applications.
4651
* Session-based authentication (traditional email/password with remember me)
4752
* Stateless authentication using Personal Access Tokens
4853
* Optional Email verification on account registration
49-
* Optional Email-based Two Factor Authentication after login
54+
* Optional Email-based Two-Factor Authentication after login
5055
* Magic Login Links when a user forgets their password
5156
* Flexible groups-based access control (think roles, but more flexible)
5257
* Users can be granted additional permissions

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
],
9292
"cs": "php-cs-fixer fix --ansi --verbose --dry-run --diff",
9393
"cs-fix": "php-cs-fixer fix --ansi --verbose --diff",
94-
"deduplicate": "phpcpd app/ src/ --exclude src/Database/Migrations/2020-12-28-223112_create_auth_tables.php",
94+
"deduplicate": "phpcpd app/ src/ --exclude src/Database/Migrations/2020-12-28-223112_create_auth_tables.php --exclude src/Authentication/Authenticators/HmacSha256.php",
9595
"inspect": "deptrac analyze --cache-file=build/deptrac.cache",
9696
"mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit",
9797
"sa": "@analyze",

docs/authentication.md

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
- [Retrieving Access Tokens](#retrieving-access-tokens)
2121
- [Access Token Lifetime](#access-token-lifetime)
2222
- [Access Token Scopes](#access-token-scopes)
23+
- [HMAC SHA256 Token Authenticator](#hmac-sha256-token-authenticator)
24+
- [HMAC Keys/API Authentication](#hmac-keysapi-authentication)
25+
- [Generating HMAC Access Keys](#generating-hmac-access-keys)
26+
- [Revoking HMAC Keys](#revoking-hmac-keys)
27+
- [Retrieving HMAC Keys](#retrieving-hmac-keys)
28+
- [HMAC Keys Lifetime](#hmac-keys-lifetime)
29+
- [HMAC Keys Scopes](#hmac-keys-scopes)
2330

2431
Authentication is the process of determining that a visitor actually belongs to your website,
2532
and identifying them. Shield provides a flexible and secure authentication system for your
@@ -38,6 +45,7 @@ public $authenticators = [
3845
// alias => classname
3946
'session' => Session::class,
4047
'tokens' => AccessTokens::class,
48+
'hmac' => HmacSha256::class,
4149
];
4250
```
4351

@@ -264,7 +272,7 @@ $tokens = $user->accessTokens();
264272
### Access Token Lifetime
265273

266274
Tokens will expire after a specified amount of time has passed since they have been used.
267-
By default, this is set to 1 year. You can change this value by setting the `accessTokenLifetime`
275+
By default, this is set to 1 year. You can change this value by setting the `$unusedTokenLifetime`
268276
value in the `Auth` config file. This is in seconds so that you can use the
269277
[time constants](https://codeigniter.com/user_guide/general/common_functions.html#time-constants)
270278
that CodeIgniter provides.
@@ -303,3 +311,161 @@ if ($user->tokenCant('forums.manage')) {
303311
// do something....
304312
}
305313
```
314+
315+
## HMAC SHA256 Token Authenticator
316+
317+
The HMAC-SHA256 authenticator supports the use of revocable API keys without using OAuth. This provides
318+
an alternative to a token that is passed in every request and instead uses a shared secret that is used to sign
319+
the request in a secure manner. Like authorization tokens, these are commonly used to provide third-party developers
320+
access to your API. These keys typically have a very long expiration time, often years.
321+
322+
These are also suitable for use with mobile applications. In this case, the user would register/sign-in
323+
with their email/password. The application would create a new access token for them, with a recognizable
324+
name, like John's iPhone 12, and return it to the mobile application, where it is stored and used
325+
in all future requests.
326+
327+
> **Note**
328+
> For the purpose of this documentation, and to maintain a level of consistency with the Authorization Tokens,
329+
> the term "Token" will be used to represent a set of API Keys (key and secretKey).
330+
331+
### Usage
332+
333+
In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request:
334+
335+
```
336+
Authorization: HMAC-SHA256 <key>:<HMAC-HASH-of-request-body>
337+
```
338+
339+
The code to do this will look something like this:
340+
341+
```php
342+
header("Authorization: HMAC-SHA256 {$key}:" . hash_hmac('sha256', $requestBody, $secretKey));
343+
```
344+
345+
Using the CodeIgniter CURLRequest class:
346+
347+
```php
348+
<?php
349+
350+
$client = \Config\Services::curlrequest();
351+
352+
$key = 'a6c460151b4cabbe1c1d73e08915ce8e';
353+
$secretKey = '56c85232f0e5b55c05015476cd132c8d';
354+
$requestBody = '{"name":"John","email":"[email protected]"}';
355+
356+
// $hashValue = b22b0ec11ad61cd4488ab1a09c8a0317e896c22adcc5754ea4cfd0f903a0f8c2
357+
$hashValue = hash_hmac('sha256', $requestBody, $secretKey);
358+
359+
$response = $client->setHeader('Authorization', "HMAC-SHA256 {$key}:{$hashValue}")
360+
->setBody($requestBody)
361+
->request('POST', 'https://example.com/api');
362+
```
363+
364+
### HMAC Keys/API Authentication
365+
366+
Using HMAC keys requires that you either use/extend `CodeIgniter\Shield\Models\UserModel` or
367+
use the `CodeIgniter\Shield\Authentication\Traits\HasHmacTokens` on your own user model. This trait
368+
provides all the custom methods needed to implement HMAC keys in your application. The necessary
369+
database table, `auth_identities`, is created in Shield's only migration class, which must be run
370+
before first using any of the features of Shield.
371+
372+
### Generating HMAC Access Keys
373+
374+
Access keys/tokens are created through the `generateHmacToken()` method on the user. This takes a name to
375+
give to the token as the first argument. The name is used to display it to the user, so they can
376+
differentiate between multiple tokens.
377+
378+
```php
379+
$token = $user->generateHmacToken('Work Laptop');
380+
```
381+
382+
This creates the keys/tokens using a cryptographically secure random string. The keys operate as shared keys.
383+
This means they are stored as-is in the database. The method returns an instance of
384+
`CodeIgniters\Shield\Authentication\Entities\AccessToken`. The field `secret` is the 'key' the field `secret2` is
385+
the shared 'secretKey'. Both are required to when using this authentication method.
386+
387+
**The plain text version of these keys should be displayed to the user immediately, so they can copy it for
388+
their use.** It is recommended that after that only the 'key' field is displayed to a user. If a user loses the
389+
'secretKey', they should be required to generate a new set of keys to use.
390+
391+
```php
392+
$token = $user->generateHmacToken('Work Laptop');
393+
394+
echo 'Key: ' . $token->secret;
395+
echo 'SecretKey: ' . $token->secret2;
396+
```
397+
398+
### Revoking HMAC Keys
399+
400+
HMAC keys can be revoked through the `revokeHmacToken()` method. This takes the key as the only
401+
argument. Revoking simply deletes the record from the database.
402+
403+
```php
404+
$user->revokeHmacToken($key);
405+
```
406+
407+
You can revoke all HMAC Keys with the `revokeAllHmacTokens()` method.
408+
409+
```php
410+
$user->revokeAllHmacTokens();
411+
```
412+
413+
### Retrieving HMAC Keys
414+
415+
The following methods are available to help you retrieve a user's HMAC keys:
416+
417+
```php
418+
// Retrieve a set of HMAC Token/Keys by key
419+
$token = $user->getHmacToken($key);
420+
421+
// Retrieve an HMAC token/keys by its database ID
422+
$token = $user->getHmacTokenById($id);
423+
424+
// Retrieve all HMAC tokens as an array of AccessToken instances.
425+
$tokens = $user->hmacTokens();
426+
```
427+
428+
### HMAC Keys Lifetime
429+
430+
HMAC Keys/Tokens will expire after a specified amount of time has passed since they have been used.
431+
This uses the same configuration value as AccessTokens.
432+
433+
By default, this is set to 1 year. You can change this value by setting the `$unusedTokenLifetime`
434+
value in the `Auth` config file. This is in seconds so that you can use the
435+
[time constants](https://codeigniter.com/user_guide/general/common_functions.html#time-constants)
436+
that CodeIgniter provides.
437+
438+
```php
439+
public $unusedTokenLifetime = YEAR;
440+
```
441+
442+
### HMAC Keys Scopes
443+
444+
Each token (set of keys) can be given one or more scopes they can be used within. These can be thought of as
445+
permissions the token grants to the user. Scopes are provided when the token is generated and
446+
cannot be modified afterword.
447+
448+
```php
449+
$token = $user->gererateHmacToken('Work Laptop', ['posts.manage', 'forums.manage']);
450+
```
451+
452+
By default, a user is granted a wildcard scope which provides access to all scopes. This is the
453+
same as:
454+
455+
```php
456+
$token = $user->gererateHmacToken('Work Laptop', ['*']);
457+
```
458+
459+
During authentication, the HMAC Keys the user used is stored on the user. Once authenticated, you
460+
can use the `hmacTokenCan()` and `hmacTokenCant()` methods on the user to determine if they have access
461+
to the specified scope.
462+
463+
```php
464+
if ($user->hmacTokenCan('posts.manage')) {
465+
// do something....
466+
}
467+
468+
if ($user->hmacTokenCant('forums.manage')) {
469+
// do something....
470+
}
471+
```

docs/guides/api_hmac_keys.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Protecting an API with HMAC Keys
2+
3+
> **Note**
4+
> For the purpose of this documentation and to maintain a level of consistency with the Authorization Tokens,
5+
the term "Token" will be used to represent a set of API Keys (key and secretKey).
6+
7+
HMAC Keys can be used to authenticate users for your own site, or when allowing third-party developers to access your
8+
API. When making requests using HMAC keys, the token should be included in the `Authorization` header as an
9+
`HMAC-SHA256` token.
10+
11+
> **Note**
12+
> By default, `$authenticatorHeader['hmac']` is set to `Authorization`. You can change this value by
13+
> setting the `$authenticatorHeader['hmac']` value in the **app/Config/Auth.php** config file.
14+
15+
Tokens are issued with the `generateHmacToken()` method on the user. This returns a
16+
`CodeIgniter\Shield\Entities\AccessToken` instance. These shared keys are saved to the database in plain text. The
17+
`AccessToken` object returned when you generate it will include a `secret` field which will be the `key` and a `secret2`
18+
field that will be the `secretKey`. You should display the `secretKey` to your user once, so they have a chance to copy
19+
it somewhere safe, as this is the only time you should reveal this key.
20+
21+
The `generateHmacToken()` method requires a name for the token. These are free strings and are often used to identify
22+
the user/device the token was generated from/for, like 'Johns MacBook Air'.
23+
24+
```php
25+
$routes->get('/hmac/token', static function () {
26+
$token = auth()->user()->generateHmacToken(service('request')->getVar('token_name'));
27+
28+
return json_encode(['key' => $token->secret, 'secretKey' => $token->secret2]);
29+
});
30+
```
31+
32+
You can access all the user's HMAC keys with the `hmacTokens()` method on that user.
33+
34+
```php
35+
$tokens = $user->hmacTokens();
36+
foreach ($tokens as $token) {
37+
//
38+
}
39+
```
40+
41+
### Usage
42+
43+
In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request:
44+
45+
```
46+
Authorization: HMAC-SHA256 <key>:<HMAC HASH of request body>
47+
```
48+
49+
The code to do this will look something like this:
50+
51+
```php
52+
header("Authorization: HMAC-SHA256 {$key}:" . hash_hmac('sha256', $requestBody, $secretKey));
53+
```
54+
55+
## HMAC Keys Permissions
56+
57+
HMAC keys can be given `scopes`, which are basically permission strings, for the HMAC Token/Keys. This is generally not
58+
the same as the permission the user has, but is used to specify the permissions on the API itself. If not specified, the
59+
token is granted all access to all scopes. This might be enough for a smaller API.
60+
61+
```php
62+
$token = $user->generateHmacToken('token-name', ['users-read']);
63+
return json_encode(['key' => $token->secret, 'secretKey' => $token->secret2]);
64+
```
65+
66+
> **Note**
67+
> At this time, scope names should avoid using a colon (`:`) as this causes issues with the route filters being
68+
> correctly recognized.
69+
70+
When handling incoming requests you can check if the token has been granted access to the scope with the `hmacTokenCan()` method.
71+
72+
```php
73+
if ($user->hmacTokenCan('users-read')) {
74+
//
75+
}
76+
```
77+
78+
### Revoking Keys/Tokens
79+
80+
Tokens can be revoked by deleting them from the database with the `revokeHmacToken($key)` or `revokeAllHmacTokens()` methods.
81+
82+
```php
83+
$user->revokeHmacToken($key);
84+
$user->revokeAllHmacTokens();
85+
```
86+
87+
## Protecting Routes
88+
89+
The first way to specify which routes are protected is to use the `hmac` controller filter.
90+
91+
For example, to ensure it protects all routes under the `/api` route group, you would use the `$filters` setting
92+
on **app/Config/Filters.php**.
93+
94+
```php
95+
public $filters = [
96+
'hmac' => ['before' => ['api/*']],
97+
];
98+
```
99+
100+
You can also specify the filter should run on one or more routes within the routes file itself:
101+
102+
```php
103+
$routes->group('api', ['filter' => 'hmac'], function($routes) {
104+
//
105+
});
106+
$routes->get('users', 'UserController::list', ['filter' => 'hmac:users-read']);
107+
```
108+
109+
When the filter runs, it checks the `Authorization` header for a `HMAC-SHA256` value that has the computed token. It then
110+
parses the raw token and looks it up the `key` portion in the database. Once found, it will rehash the body of the request
111+
to validate the remainder of the Authorization raw token. If it passes the signature test it can determine the correct user,
112+
which will then be available through an `auth()->user()` call.
113+
114+
> **Note**
115+
> Currently only a single scope can be used on a route filter. If multiple scopes are passed in, only the first one is checked.

docs/install.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Require it with an explicit version constraint allowing its desired stability.
108108
There are a few setup items to do before you can start using Shield in
109109
your project.
110110

111-
1. Copy the **Auth.php** and **AuthGroups.php** from **vendor/codeigniter4/shield/src/Config/** into your project's config folder and update the namespace to `Config`. You will also need to have these classes extend the original classes. See the example below. These files contain all the settings, group, and permission information for your application and will need to be modified to meet the needs of your site.
111+
1. Copy the **Auth.php**, **AuthGroups.php**, and **AuthToken.php** from **vendor/codeigniter4/shield/src/Config/** into your project's config folder and update the namespace to `Config`. You will also need to have these classes extend the original classes. See the example below. These files contain all the settings, group, and permission information for your application and will need to be modified to meet the needs of your site.
112112

113113
```php
114114
// new file - app/Config/Auth.php
@@ -204,6 +204,7 @@ public $aliases = [
204204
// ...
205205
'session' => \CodeIgniter\Shield\Filters\SessionAuth::class,
206206
'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class,
207+
'hmac' => \CodeIgniter\Shield\Filters\HmacAuth::class,
207208
'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class,
208209
'auth-rates' => \CodeIgniter\Shield\Filters\AuthRates::class,
209210
'group' => \CodeIgniter\Shield\Filters\GroupFilter::class,
@@ -213,15 +214,16 @@ public $aliases = [
213214
];
214215
```
215216

216-
Filters | Description
217-
--- | ---
218-
session and tokens | The `Session` and `AccessTokens` authenticators, respectively.
219-
chained | The filter will check both authenticators in sequence to see if the user is logged in through either of authenticators, allowing a single API endpoint to work for both an SPA using session auth, and a mobile app using access tokens.
220-
jwt | The `JWT` authenticator. See [JWT Authentication](./addons/jwt.md).
221-
auth-rates | Provides a good basis for rate limiting of auth-related routes.
222-
group | Checks if the user is in one of the groups passed in.
223-
permission | Checks if the user has the passed permissions.
224-
force-reset | Checks if the user requires a password reset.
217+
| Filters | Description |
218+
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
219+
| session and tokens | The `Session` and `AccessTokens` authenticators, respectively. |
220+
| chained | The filter will check both authenticators in sequence to see if the user is logged in through either of authenticators, allowing a single API endpoint to work for both an SPA using session auth, and a mobile app using access tokens. |
221+
| jwt | The `JWT` authenticator. See [JWT Authentication](./addons/jwt.md). |
222+
| hmac | The `HMAC` authenticator. See [HMAC Authentication](./guides/api_hmac_keys.md). |
223+
| auth-rates | Provides a good basis for rate limiting of auth-related routes. |
224+
| group | Checks if the user is in one of the groups passed in. |
225+
| permission | Checks if the user has the passed permissions. |
226+
| force-reset | Checks if the user requires a password reset. |
225227

226228
These can be used in any of the [normal filter config settings](https://codeigniter.com/user_guide/incoming/filters.html#globals), or [within the routes file](https://codeigniter.com/user_guide/incoming/routing.html#applying-filters).
227229

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ nav:
5151
- Banning Users: banning_users.md
5252
- session_auth_event_and_logging.md
5353
- Guides:
54+
- guides/api_hmac_keys.md
5455
- guides/api_tokens.md
5556
- guides/mobile_apps.md
5657
- guides/strengthen_password.md

0 commit comments

Comments
 (0)