Skip to content

Commit

Permalink
Merge pull request from GHSA-j72f-h752-mx4w
Browse files Browse the repository at this point in the history
fix: raw token logging
  • Loading branch information
kenjis committed Nov 22, 2023
2 parents f77c6ae + 725baaa commit 7e84c3f
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 70 deletions.
28 changes: 26 additions & 2 deletions docs/addons/jwt.md
Expand Up @@ -34,9 +34,9 @@ To use JWT Authentication, you need additional setup and configuration.

```php
<?php

// app/Config/AuthJWT.php

declare(strict_types=1);

namespace Config;
Expand Down Expand Up @@ -128,6 +128,19 @@ php -r 'echo base64_encode(random_bytes(32));'

The secret key is used for signing and validating tokens.

### Login Attempt Logging

By default, only failed login attempts are recorded in the `auth_token_logins` table.

```php
public int $recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_FAILURE;
```

If you don't want any logs, set it to `Auth::RECORD_LOGIN_ATTEMPT_NONE`.

If you want to log all login attempts, set it to `Auth::RECORD_LOGIN_ATTEMPT_ALL`.
It means you log all requests.

## Issuing JWTs

To use JWT Authentication, you need a controller that issues JWTs.
Expand Down Expand Up @@ -351,3 +364,14 @@ It uses the `secret` and `alg` in the `Config\AuthJWT::$keys['default']`.
It sets the `Config\AuthJWT::$defaultClaims` to the token, and sets
`"iat"` (Issued At) and `"exp"` (Expiration Time) claims automatically even if
you don't pass them.

## Logging

Login attempts are recorded in the `auth_token_logins` table, according to the
configuration above.

When a failed login attempt is logged, the raw token value sent is saved in
the `identifier` column.

When a successful login attempt is logged, the SHA256 hash value of the token
sent is saved in the `identifier` column.
18 changes: 3 additions & 15 deletions docs/getting_started/configuration.md
Expand Up @@ -8,20 +8,8 @@ If you have completed the setup according to this documentation, you will have
the following configuration files:

- **app/Config/Auth.php**
- **app/Config/AuthGroups.php** - For Authorization
- **app/Config/AuthToken.php** - For AccessTokens and HmacSha256 Authentication
- **app/Config/AuthJWT.php** - For JWT Authentication
- **app/Config/AuthGroups.php** - For [Authorization](../references/authorization.md)
- **app/Config/AuthToken.php** - For [AccessTokens](../references/authentication/tokens.md#configuration) and [HmacSha256](../references/authentication/hmac.md#configuration) Authentication
- **app/Config/AuthJWT.php** - For [JWT Authentication](../addons/jwt.md#configuration)

Note that you do not need to have configuration files for features you do not use.

This section describes the major Config items that are not described elsewhere.

## AccessTokens Authenticator

### Access Token Lifetime

By default, Access Tokens can be used for 1 year since the last use. This can be easily modified in the **app/Config/AuthToken.php** config file.

```php
public int $unusedTokenLifetime = YEAR;
```
61 changes: 47 additions & 14 deletions docs/references/authentication/hmac.md
Expand Up @@ -112,20 +112,6 @@ $token = $user->getHmacTokenById($id);
$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.

By default, this is set to 1 year. You can change this value by setting the `$unusedTokenLifetime`
value in the **app/Config/AuthToken.php** 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
Expand Down Expand Up @@ -219,3 +205,50 @@ authtoken.hmacEncryptionCurrentKey = k2
Depending on the set length of the Secret Key and the type of encryption used, it is possible for the encrypted value to
exceed the database column character limit of 255 characters. If this happens, creation of a new HMAC identity will
throw a `RuntimeException`.

## Configuration

Configure **app/Config/AuthToken.php** for your needs.

!!! note

Shield does not expect you use the Access Token Authenticator and HMAC Authenticator
at the same time. Therefore, some Config items are common.

### HMAC Keys Lifetime

HMAC Keys/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 `$unusedTokenLifetime`
value. 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;
```

### Login Attempt Logging

By default, only failed login attempts are recorded in the `auth_token_logins` table.
This can be modified by changing the `$recordLoginAttempt` value.

```php
public int $recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_FAILURE;
```

If you don't want any logs, set it to `Auth::RECORD_LOGIN_ATTEMPT_NONE`.

If you want to log all login attempts, set it to `Auth::RECORD_LOGIN_ATTEMPT_ALL`.
It means you log all requests.

## Logging

Login attempts are recorded in the `auth_token_logins` table, according to the
configuration above.

When a failed login attempt is logged, the raw token value sent is saved in
the `identifier` column.

When a successful login attempt is logged, the token name is saved in the
`identifier` column.
61 changes: 49 additions & 12 deletions docs/references/authentication/tokens.md
Expand Up @@ -83,18 +83,6 @@ $token = $user->getAccessTokenById($id);
$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 `$unusedTokenLifetime`
value in the **app/Config/AuthToken.php** 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;
```

## Access Token Scopes

Each token can be given one or more scopes they can be used within. These can be thought of as
Expand Down Expand Up @@ -125,3 +113,52 @@ if ($user->tokenCant('forums.manage')) {
// do something....
}
```

## Configuration

Configure **app/Config/AuthToken.php** for your needs.

!!! note

Shield does not expect you use the Access Token Authenticator and HMAC Authenticator
at the same time. Therefore, some Config items are common.

### 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 `$unusedTokenLifetime` value. 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;
```

### Login Attempt Logging

By default, only failed login attempts are recorded in the `auth_token_logins` table.

This can be modified by changing the `$recordLoginAttempt` value.

```php
public int $recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_FAILURE;
```

If you don't want any logs, set it to `Auth::RECORD_LOGIN_ATTEMPT_NONE`.

If you want to log all login attempts, set it to `Auth::RECORD_LOGIN_ATTEMPT_ALL`.
It means you log all requests.

## Logging

Login attempts are recorded in the `auth_token_logins` table, according to the
configuration above.

When a failed login attempt is logged, the raw token value sent is saved in
the `identifier` column.

When a successful login attempt is logged, the token name is saved in the
`identifier` column.
5 changes: 0 additions & 5 deletions phpstan-baseline.php
Expand Up @@ -361,11 +361,6 @@
'count' => 1,
'path' => __DIR__ . '/src/Models/UserModel.php',
];
$ignoreErrors[] = [
'message' => '#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with \'CodeIgniter\\\\\\\\Shield\\\\\\\\Result\' and CodeIgniter\\\\Shield\\\\Result will always evaluate to true\\.$#',
'count' => 2,
'path' => __DIR__ . '/tests/Authentication/Authenticators/AccessTokenAuthenticatorTest.php',
];
$ignoreErrors[] = [
'message' => '#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with \'CodeIgniter\\\\\\\\Shield\\\\\\\\Result\' and CodeIgniter\\\\Shield\\\\Result will always evaluate to true\\.$#',
'count' => 3,
Expand Down
11 changes: 5 additions & 6 deletions src/Authentication/Authenticators/AccessTokens.php
Expand Up @@ -77,14 +77,15 @@ public function attempt(array $credentials): Result
return $result;
}

$user = $result->extraInfo();
$user = $result->extraInfo();
$token = $user->getAccessToken($this->getBearerToken());

if ($user->isBanned()) {
if ($config->recordLoginAttempt >= Auth::RECORD_LOGIN_ATTEMPT_FAILURE) {
// Record a banned login attempt.
$this->loginModel->recordLoginAttempt(
self::ID_TYPE_ACCESS_TOKEN,
$credentials['token'] ?? '',
$token->name ?? '',
false,
$ipAddress,
$userAgent,
Expand All @@ -100,17 +101,15 @@ public function attempt(array $credentials): Result
]);
}

$user = $user->setAccessToken(
$user->getAccessToken($this->getBearerToken())
);
$user = $user->setAccessToken($token);

$this->login($user);

if ($config->recordLoginAttempt === Auth::RECORD_LOGIN_ATTEMPT_ALL) {
// Record a successful login attempt.
$this->loginModel->recordLoginAttempt(
self::ID_TYPE_ACCESS_TOKEN,
$credentials['token'] ?? '',
$token->name ?? '',
true,
$ipAddress,
$userAgent,
Expand Down
11 changes: 5 additions & 6 deletions src/Authentication/Authenticators/HmacSha256.php
Expand Up @@ -78,14 +78,15 @@ public function attempt(array $credentials): Result
return $result;
}

$user = $result->extraInfo();
$user = $result->extraInfo();
$token = $user->getHmacToken($this->getHmacKeyFromToken());

if ($user->isBanned()) {
if ($config->recordLoginAttempt >= Auth::RECORD_LOGIN_ATTEMPT_FAILURE) {
// Record a banned login attempt.
$this->loginModel->recordLoginAttempt(
self::ID_TYPE_HMAC_TOKEN,
$credentials['token'] ?? '',
$token->name ?? '',
false,
$ipAddress,
$userAgent,
Expand All @@ -101,17 +102,15 @@ public function attempt(array $credentials): Result
]);
}

$user = $user->setHmacToken(
$user->getHmacToken($this->getHmacKeyFromToken())
);
$user = $user->setHmacToken($token);

$this->login($user);

if ($config->recordLoginAttempt === Auth::RECORD_LOGIN_ATTEMPT_ALL) {
// Record a successful login attempt.
$this->loginModel->recordLoginAttempt(
self::ID_TYPE_HMAC_TOKEN,
$credentials['token'] ?? '',
$token->name ?? '',
true,
$ipAddress,
$userAgent,
Expand Down
4 changes: 2 additions & 2 deletions src/Authentication/Authenticators/JWT.php
Expand Up @@ -103,7 +103,7 @@ public function attempt(array $credentials): Result
// Record a banned login attempt.
$this->tokenLoginModel->recordLoginAttempt(
self::ID_TYPE_JWT,
$credentials['token'] ?? '',
'sha256:' . hash('sha256', $credentials['token'] ?? ''),
false,
$ipAddress,
$userAgent,
Expand All @@ -125,7 +125,7 @@ public function attempt(array $credentials): Result
// Record a successful login attempt.
$this->tokenLoginModel->recordLoginAttempt(
self::ID_TYPE_JWT,
$credentials['token'] ?? '',
'sha256:' . hash('sha256', $credentials['token']),
true,
$ipAddress,
$userAgent,
Expand Down
2 changes: 1 addition & 1 deletion src/Config/AuthToken.php
Expand Up @@ -46,7 +46,7 @@ class AuthToken extends BaseAuthToken

/**
* --------------------------------------------------------------------
* Unused Token Lifetime
* Unused Token Lifetime for Token Auth and HMAC Auth
* --------------------------------------------------------------------
* Determines the amount of time, in seconds, that an unused token can
* be used.
Expand Down
Expand Up @@ -17,11 +17,11 @@
use CodeIgniter\Shield\Authentication\Authentication;
use CodeIgniter\Shield\Authentication\Authenticators\AccessTokens;
use CodeIgniter\Shield\Config\Auth;
use CodeIgniter\Shield\Config\AuthToken;
use CodeIgniter\Shield\Entities\AccessToken;
use CodeIgniter\Shield\Entities\User;
use CodeIgniter\Shield\Models\UserIdentityModel;
use CodeIgniter\Shield\Models\UserModel;
use CodeIgniter\Shield\Result;
use CodeIgniter\Test\Mock\MockEvents;
use Config\Services;
use Tests\Support\DatabaseTestCase;
Expand Down Expand Up @@ -182,7 +182,6 @@ public function testAttemptCannotFindUser(): void
'token' => 'abc123',
]);

$this->assertInstanceOf(Result::class, $result);
$this->assertFalse($result->isOK());
$this->assertSame(lang('Auth.badToken'), $result->reason());

Expand All @@ -205,7 +204,6 @@ public function testAttemptSuccess(): void
'token' => $token->raw_token,
]);

$this->assertInstanceOf(Result::class, $result);
$this->assertTrue($result->isOK());

$foundUser = $result->extraInfo();
Expand All @@ -222,6 +220,37 @@ public function testAttemptSuccess(): void
]);
}

public function testAttemptSuccessLog(): void
{
// Change $recordLoginAttempt in Config.
/** @var AuthToken $config */
$config = config('AuthToken');
$config->recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_ALL;

/** @var User $user */
$user = fake(UserModel::class);
$token = $user->generateAccessToken('foo');
$this->setRequestHeader($token->raw_token);

$result = $this->auth->attempt([
'token' => $token->raw_token,
]);

$this->assertTrue($result->isOK());

$foundUser = $result->extraInfo();
$this->assertInstanceOf(User::class, $foundUser);
$this->assertSame($user->id, $foundUser->id);
$this->assertInstanceOf(AccessToken::class, $foundUser->currentAccessToken());
$this->assertSame($token->token, $foundUser->currentAccessToken()->token);

$this->seeInDatabase($this->tables['token_logins'], [
'id_type' => AccessTokens::ID_TYPE_ACCESS_TOKEN,
'identifier' => 'foo',
'success' => 1,
]);
}

protected function setRequestHeader(string $token): void
{
$request = service('request');
Expand Down

0 comments on commit 7e84c3f

Please sign in to comment.