Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: HMAC SHA256 Authentication #795

Merged
merged 41 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0c551dc
Initial work on HMAC implementation.
tswagger Aug 11, 2023
f634172
Authenticator test pass
tswagger Aug 15, 2023
52b5d8b
Authenticator test pass
tswagger Aug 15, 2023
1a7802e
Added HMAC Filter tests. Pass
tswagger Aug 15, 2023
1cd518f
Added documentation
tswagger Aug 18, 2023
91adb87
Added missing helper
tswagger Aug 23, 2023
50d70e2
Minor syntax fix
tswagger Aug 23, 2023
5be8b2c
Codeing standards cleanup
tswagger Aug 23, 2023
89f38a8
Clarified Return statements
tswagger Aug 24, 2023
2adcdbd
Update docs/guides/api_hmac_keys.md
tswagger Aug 28, 2023
bbc2496
Update docs/guides/api_hmac_keys.md
tswagger Aug 28, 2023
1243efc
Minor typo fix, clarification of key vs secretKey
tswagger Aug 30, 2023
498e685
Update docs/authentication.md
tswagger Aug 31, 2023
ac61035
Renamed 'HMAC' in the code to a consistent 'Hmac'
tswagger Aug 31, 2023
c8058f7
Update docs/authentication.md
tswagger Aug 31, 2023
755a99c
Update docs/authentication.md
tswagger Aug 31, 2023
19d7cea
Update docs/authentication.md
tswagger Aug 31, 2023
72b9a70
Update docs/authentication.md
tswagger Aug 31, 2023
daee371
Update docs/guides/api_hmac_keys.md
tswagger Aug 31, 2023
8990cef
Update docs/guides/api_hmac_keys.md
tswagger Aug 31, 2023
5f56a55
Added ToC entries.
tswagger Aug 31, 2023
4b452b9
Added CURLRequest example
tswagger Aug 31, 2023
547e455
Update docs/guides/api_hmac_keys.md
tswagger Sep 5, 2023
65853f7
Update docs/guides/api_hmac_keys.md
tswagger Sep 6, 2023
9f9506d
Improved HMAC Docs
tswagger Sep 6, 2023
7791f87
Updated phpdpd to suppress errors from HMAC Addition
tswagger Sep 6, 2023
b5d7fa1
Updated login recording to match JWT Authorization
tswagger Sep 12, 2023
d8e4262
Update src/Authentication/Authenticators/HmacSha256.php
tswagger Sep 13, 2023
6ac224e
Added HMAC References to installation documentation
tswagger Sep 13, 2023
08e0b8e
Cleaned up table formatting in markdown
tswagger Sep 13, 2023
bbcf8b5
Updated byte size for HMAC Secret Key
tswagger Sep 14, 2023
f47891a
Update tests/Authentication/Authenticators/HmacAuthenticatorTest.php
tswagger Sep 15, 2023
10147cf
Update tests/Authentication/Authenticators/HmacAuthenticatorTest.php
tswagger Sep 15, 2023
2eb3588
Initial fix to PHPStan errors
tswagger Sep 15, 2023
37c9f82
Syntax adjustment
tswagger Sep 15, 2023
58b6042
Added additional test
tswagger Sep 16, 2023
76baf80
Added additional tests
tswagger Sep 16, 2023
f57f45f
Fix to test
tswagger Sep 16, 2023
968997a
Removed redundant comment
tswagger Sep 16, 2023
a1b64db
Added config copy to Setup script
tswagger Sep 18, 2023
9d223a4
Minor fix in docs
tswagger Sep 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/phpcpd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ jobs:
coverage: none

- name: Detect duplicate code
run: phpcpd src/ tests/ --exclude src/Database/Migrations/2020-12-28-223112_create_auth_tables.php
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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ These are much like the access codes that GitHub uses, where they are unique to
can have more than one. This can be used for API authentication of third-party users, and even for allowing
access for a mobile application that you build.

### HMAC - SHA256

This is a slightly more complicated improvement on Access Codes/Tokens. The main advantage with HMAC is the shared Secret Key
is not passed in the request, but is instead used to create a hash signature of the request body.

### JSON Web Tokens

JWT or JSON Web Token is a compact and self-contained way of securely transmitting
Expand All @@ -46,7 +51,7 @@ and authorization purposes in web applications.
* Session-based authentication (traditional email/password with remember me)
* Stateless authentication using Personal Access Tokens
* Optional Email verification on account registration
* Optional Email-based Two Factor Authentication after login
* Optional Email-based Two-Factor Authentication after login
* Magic Login Links when a user forgets their password
* Flexible groups-based access control (think roles, but more flexible)
* Users can be granted additional permissions
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
],
"cs": "php-cs-fixer fix --ansi --verbose --dry-run --diff",
"cs-fix": "php-cs-fixer fix --ansi --verbose --diff",
"deduplicate": "phpcpd app/ src/ --exclude src/Database/Migrations/2020-12-28-223112_create_auth_tables.php",
"deduplicate": "phpcpd app/ src/ --exclude src/Database/Migrations/2020-12-28-223112_create_auth_tables.php --exclude src/Authentication/Authenticators/HmacSha256.php",
"inspect": "deptrac analyze --cache-file=build/deptrac.cache",
"mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit",
"sa": "@analyze",
Expand Down
168 changes: 167 additions & 1 deletion docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
- [Retrieving Access Tokens](#retrieving-access-tokens)
- [Access Token Lifetime](#access-token-lifetime)
- [Access Token Scopes](#access-token-scopes)
- [HMAC SHA256 Token Authenticator](#hmac-sha256-token-authenticator)
- [HMAC Keys/API Authentication](#hmac-keysapi-authentication)
- [Generating HMAC Access Keys](#generating-hmac-access-keys)
- [Revoking HMAC Keys](#revoking-hmac-keys)
- [Retrieving HMAC Keys](#retrieving-hmac-keys)
- [HMAC Keys Lifetime](#hmac-keys-lifetime)
- [HMAC Keys Scopes](#hmac-keys-scopes)

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

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

Tokens will expire after a specified amount of time has passed since they have been used.
By default, this is set to 1 year. You can change this value by setting the `accessTokenLifetime`
By default, this is set to 1 year. You can change this value by setting the `$unusedTokenLifetime`
value in the `Auth` config file. This is in seconds so that you can use the
[time constants](https://codeigniter.com/user_guide/general/common_functions.html#time-constants)
that CodeIgniter provides.
Expand Down Expand Up @@ -303,3 +311,161 @@ if ($user->tokenCant('forums.manage')) {
// do something....
}
```

## HMAC SHA256 Token Authenticator

The HMAC-SHA256 authenticator supports the use of revocable API keys without using OAuth. This provides
an alternative to a token that is passed in every request and instead uses a shared secret that is used to sign
the request in a secure manner. Like authorization tokens, these are commonly used to provide third-party developers
access to your API. These keys typically have a very long expiration time, often years.

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

> **Note**
> For the purpose of this documentation, and to maintain a level of consistency with the Authorization Tokens,
> the term "Token" will be used to represent a set of API Keys (key and secretKey).
### Usage
tswagger marked this conversation as resolved.
Show resolved Hide resolved
tswagger marked this conversation as resolved.
Show resolved Hide resolved

In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request:

```
Authorization: HMAC-SHA256 <key>:<HMAC-HASH-of-request-body>
```

The code to do this will look something like this:

```php
header("Authorization: HMAC-SHA256 {$key}:" . hash_hmac('sha256', $requestBody, $secretKey));
tswagger marked this conversation as resolved.
Show resolved Hide resolved
```

Using the CodeIgniter CURLRequest class:
tswagger marked this conversation as resolved.
Show resolved Hide resolved

```php
<?php

$client = \Config\Services::curlrequest();

$key = 'a6c460151b4cabbe1c1d73e08915ce8e';
$secretKey = '56c85232f0e5b55c05015476cd132c8d';
$requestBody = '{"name":"John","email":"john@example.com"}';

// $hashValue = b22b0ec11ad61cd4488ab1a09c8a0317e896c22adcc5754ea4cfd0f903a0f8c2
$hashValue = hash_hmac('sha256', $requestBody, $secretKey);

$response = $client->setHeader('Authorization', "HMAC-SHA256 {$key}:{$hashValue}")
->setBody($requestBody)
->request('POST', 'https://example.com/api');
```

### HMAC Keys/API Authentication

Using HMAC keys requires that you either use/extend `CodeIgniter\Shield\Models\UserModel` or
use the `CodeIgniter\Shield\Authentication\Traits\HasHmacTokens` on your own user model. This trait
provides all the custom methods needed to implement HMAC keys in your application. The necessary
database table, `auth_identities`, is created in Shield's only migration class, which must be run
before first using any of the features of Shield.

### Generating HMAC Access Keys

Access keys/tokens are created through the `generateHmacToken()` method on the user. This takes a name to
give to the token as the first argument. The name is used to display it to the user, so they can
differentiate between multiple tokens.

```php
$token = $user->generateHmacToken('Work Laptop');
```

This creates the keys/tokens using a cryptographically secure random string. The keys operate as shared keys.
This means they are stored as-is in the database. The method returns an instance of
`CodeIgniters\Shield\Authentication\Entities\AccessToken`. The field `secret` is the 'key' the field `secret2` is
the shared 'secretKey'. Both are required to when using this authentication method.

**The plain text version of these keys should be displayed to the user immediately, so they can copy it for
their use.** It is recommended that after that only the 'key' field is displayed to a user. If a user loses the
'secretKey', they should be required to generate a new set of keys to use.

```php
$token = $user->generateHmacToken('Work Laptop');

echo 'Key: ' . $token->secret;
echo 'SecretKey: ' . $token->secret2;
```

### Revoking HMAC Keys

HMAC keys can be revoked through the `revokeHmacToken()` method. This takes the key as the only
argument. Revoking simply deletes the record from the database.

```php
$user->revokeHmacToken($key);
```

You can revoke all HMAC Keys with the `revokeAllHmacTokens()` method.

```php
$user->revokeAllHmacTokens();
```

### Retrieving HMAC Keys

The following methods are available to help you retrieve a user's HMAC keys:

```php
// Retrieve a set of HMAC Token/Keys by key
$token = $user->getHmacToken($key);

// Retrieve an HMAC token/keys by its database ID
$token = $user->getHmacTokenById($id);

// Retrieve all HMAC tokens as an array of AccessToken instances.
$tokens = $user->hmacTokens();
```

### HMAC Keys Lifetime

HMAC Keys/Tokens will expire after a specified amount of time has passed since they have been used.
This uses the same configuration value as AccessTokens.
tswagger marked this conversation as resolved.
Show resolved Hide resolved

By default, this is set to 1 year. You can change this value by setting the `$unusedTokenLifetime`
value in the `Auth` config file. This is in seconds so that you can use the
[time constants](https://codeigniter.com/user_guide/general/common_functions.html#time-constants)
that CodeIgniter provides.

```php
public $unusedTokenLifetime = YEAR;
```

### HMAC Keys Scopes

Each token (set of keys) can be given one or more scopes they can be used within. These can be thought of as
permissions the token grants to the user. Scopes are provided when the token is generated and
cannot be modified afterword.

```php
$token = $user->gererateHmacToken('Work Laptop', ['posts.manage', 'forums.manage']);
```

By default, a user is granted a wildcard scope which provides access to all scopes. This is the
same as:

```php
$token = $user->gererateHmacToken('Work Laptop', ['*']);
```

During authentication, the HMAC Keys the user used is stored on the user. Once authenticated, you
can use the `hmacTokenCan()` and `hmacTokenCant()` methods on the user to determine if they have access
to the specified scope.

```php
if ($user->hmacTokenCan('posts.manage')) {
// do something....
}

if ($user->hmacTokenCant('forums.manage')) {
// do something....
}
```
115 changes: 115 additions & 0 deletions docs/guides/api_hmac_keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Protecting an API with HMAC Keys

> **Note**
> For the purpose of this documentation and to maintain a level of consistency with the Authorization Tokens,
the term "Token" will be used to represent a set of API Keys (key and secretKey).

HMAC Keys can be used to authenticate users for your own site, or when allowing third-party developers to access your
API. When making requests using HMAC keys, the token should be included in the `Authorization` header as an
`HMAC-SHA256` token.

> **Note**
> By default, `$authenticatorHeader['hmac']` is set to `Authorization`. You can change this value by
> setting the `$authenticatorHeader['hmac']` value in the **app/Config/Auth.php** config file.
Tokens are issued with the `generateHmacToken()` method on the user. This returns a
`CodeIgniter\Shield\Entities\AccessToken` instance. These shared keys are saved to the database in plain text. The
`AccessToken` object returned when you generate it will include a `secret` field which will be the `key` and a `secret2`
field that will be the `secretKey`. You should display the `secretKey` to your user once, so they have a chance to copy
it somewhere safe, as this is the only time you should reveal this key.

The `generateHmacToken()` method requires a name for the token. These are free strings and are often used to identify
the user/device the token was generated from/for, like 'Johns MacBook Air'.

```php
$routes->get('/hmac/token', static function () {
$token = auth()->user()->generateHmacToken(service('request')->getVar('token_name'));

return json_encode(['key' => $token->secret, 'secretKey' => $token->secret2]);
});
```

You can access all the user's HMAC keys with the `hmacTokens()` method on that user.

```php
$tokens = $user->hmacTokens();
foreach ($tokens as $token) {
//
}
```

### Usage
tswagger marked this conversation as resolved.
Show resolved Hide resolved

In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request:

```
Authorization: HMAC-SHA256 <key>:<HMAC HASH of request body>
```

The code to do this will look something like this:

```php
header("Authorization: HMAC-SHA256 {$key}:" . hash_hmac('sha256', $requestBody, $secretKey));
```

## HMAC Keys Permissions

HMAC keys can be given `scopes`, which are basically permission strings, for the HMAC Token/Keys. This is generally not
the same as the permission the user has, but is used to specify the permissions on the API itself. If not specified, the
token is granted all access to all scopes. This might be enough for a smaller API.

```php
$token = $user->generateHmacToken('token-name', ['users-read']);
return json_encode(['key' => $token->secret, 'secretKey' => $token->secret2]);
```

> **Note**
> At this time, scope names should avoid using a colon (`:`) as this causes issues with the route filters being
> correctly recognized.
When handling incoming requests you can check if the token has been granted access to the scope with the `hmacTokenCan()` method.

```php
if ($user->hmacTokenCan('users-read')) {
//
}
```

### Revoking Keys/Tokens

Tokens can be revoked by deleting them from the database with the `revokeHmacToken($key)` or `revokeAllHmacTokens()` methods.

```php
$user->revokeHmacToken($key);
$user->revokeAllHmacTokens();
```

## Protecting Routes

The first way to specify which routes are protected is to use the `hmac` controller filter.

For example, to ensure it protects all routes under the `/api` route group, you would use the `$filters` setting
on **app/Config/Filters.php**.

```php
public $filters = [
'hmac' => ['before' => ['api/*']],
];
```

You can also specify the filter should run on one or more routes within the routes file itself:

```php
$routes->group('api', ['filter' => 'hmac'], function($routes) {
//
});
$routes->get('users', 'UserController::list', ['filter' => 'hmac:users-read']);
```

When the filter runs, it checks the `Authorization` header for a `HMAC-SHA256` value that has the computed token. It then
parses the raw token and looks it up the `key` portion in the database. Once found, it will rehash the body of the request
to validate the remainder of the Authorization raw token. If it passes the signature test it can determine the correct user,
which will then be available through an `auth()->user()` call.

> **Note**
> Currently only a single scope can be used on a route filter. If multiple scopes are passed in, only the first one is checked.
22 changes: 12 additions & 10 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Require it with an explicit version constraint allowing its desired stability.
There are a few setup items to do before you can start using Shield in
your project.

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.
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.
tswagger marked this conversation as resolved.
Show resolved Hide resolved

```php
// new file - app/Config/Auth.php
Expand Down Expand Up @@ -204,6 +204,7 @@ public $aliases = [
// ...
'session' => \CodeIgniter\Shield\Filters\SessionAuth::class,
'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class,
'hmac' => \CodeIgniter\Shield\Filters\HmacAuth::class,
'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class,
'auth-rates' => \CodeIgniter\Shield\Filters\AuthRates::class,
'group' => \CodeIgniter\Shield\Filters\GroupFilter::class,
Expand All @@ -213,15 +214,16 @@ public $aliases = [
];
```

Filters | Description
--- | ---
session and tokens | The `Session` and `AccessTokens` authenticators, respectively.
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.
jwt | The `JWT` authenticator. See [JWT Authentication](./addons/jwt.md).
auth-rates | Provides a good basis for rate limiting of auth-related routes.
group | Checks if the user is in one of the groups passed in.
permission | Checks if the user has the passed permissions.
force-reset | Checks if the user requires a password reset.
| Filters | Description |
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| session and tokens | The `Session` and `AccessTokens` authenticators, respectively. |
| 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. |
| jwt | The `JWT` authenticator. See [JWT Authentication](./addons/jwt.md). |
| hmac | The `HMAC` authenticator. See [HMAC Authentication](./guides/api_hmac_keys.md). |
| auth-rates | Provides a good basis for rate limiting of auth-related routes. |
| group | Checks if the user is in one of the groups passed in. |
| permission | Checks if the user has the passed permissions. |
| force-reset | Checks if the user requires a password reset. |

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).

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ nav:
- Banning Users: banning_users.md
- session_auth_event_and_logging.md
- Guides:
- guides/api_hmac_keys.md
- guides/api_tokens.md
- guides/mobile_apps.md
- guides/strengthen_password.md
Expand Down
Loading
Loading