From 0c551dca92323b1f8188ad6eea273a4a3ff9656e Mon Sep 17 00:00:00 2001 From: tswagger Date: Fri, 11 Aug 2023 17:45:36 -0500 Subject: [PATCH 01/41] Initial work on HMAC implementation. This replicates closely the Authorization Token implementation. --- .../Authenticators/HMAC_SHA256.php | 265 ++++++++++++++++++ src/Authentication/Traits/HasHMACTokens.php | 150 ++++++++++ src/Config/Auth.php | 2 + src/Entities/User.php | 2 + src/Filters/HmacAuth.php | 68 +++++ src/Models/UserIdentityModel.php | 142 +++++++++- 6 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 src/Authentication/Authenticators/HMAC_SHA256.php create mode 100644 src/Authentication/Traits/HasHMACTokens.php create mode 100644 src/Filters/HmacAuth.php diff --git a/src/Authentication/Authenticators/HMAC_SHA256.php b/src/Authentication/Authenticators/HMAC_SHA256.php new file mode 100644 index 000000000..f79c22b10 --- /dev/null +++ b/src/Authentication/Authenticators/HMAC_SHA256.php @@ -0,0 +1,265 @@ +provider = $provider; + + $this->loginModel = model(TokenLoginModel::class); + } + + /** + * Attempts to authenticate a user with the given $credentials. + * Logs the user in with a successful check. + * + * @throws AuthenticationException + */ + public function attempt(array $credentials): Result + { + /** @var IncomingRequest $request */ + $request = service('request'); + + $ipAddress = $request->getIPAddress(); + $userAgent = (string) $request->getUserAgent(); + + $result = $this->check($credentials); + + if (! $result->isOK()) { + // Always record a login attempt, whether success or not. + $this->loginModel->recordLoginAttempt( + self::ID_TYPE_HMAC_TOKEN, + $credentials['token'] ?? '', + false, + $ipAddress, + $userAgent + ); + + return $result; + } + + $user = $result->extraInfo(); + + if ($user->isBanned()) { + $this->user = null; + + return new Result([ + 'success' => false, + 'reason' => $user->getBanMessage() ?? lang('Auth.bannedUser'), + ]); + } + + $user = $user->setAccessToken( + $user->getAccessToken($this->getBearerToken()) + ); + + $this->login($user); + + $this->loginModel->recordLoginAttempt( + self::ID_TYPE_HMAC_TOKEN, + $credentials['token'] ?? '', + true, + $ipAddress, + $userAgent, + $this->user->id + ); + + return $result; + } + + /** + * Checks a user's $credentials to see if they match an + * existing user. + * + * In this case, $credentials has only a single valid value: token, + * which is the plain text token to return. + */ + public function check(array $credentials): Result + { + if (! array_key_exists('token', $credentials) || empty($credentials['token'])) { + return new Result([ + 'success' => false, + 'reason' => lang('Auth.noToken', [config('Auth')->authenticatorHeader['tokens']]), + ]); + } + + if (strpos($credentials['token'], 'HMAC-SHA256') === 0) { + $credentials['token'] = trim(substr($credentials['token'], 11)); + } + + // Extract UserToken and HMACSHA256 Signature from Authorization token + [$userToken, $signature] = preg_split('/:/', $credentials['token'], -1, PREG_SPLIT_NO_EMPTY); + + /** @var UserIdentityModel $identityModel */ + $identityModel = model(UserIdentityModel::class); + + $token = $identityModel->getHMACTokenByKey($userToken); + + if ($token === null) { + return new Result([ + 'success' => false, + 'reason' => lang('Auth.badToken'), + ]); + } + + // Check signature... + $hash = hash_hmac('sha256', $credentials['body'], $token->secret2); + if ($hash !== $signature) { + return new Result([ + 'success' => false, + 'reason' => lang('Auth.badToken'), + ]); + } + + assert($token->last_used_at instanceof Time || $token->last_used_at === null); + + // Hasn't been used in a long time + if ( + $token->last_used_at + && $token->last_used_at->isBefore(Time::now()->subSeconds(config('Auth')->unusedTokenLifetime)) + ) { + return new Result([ + 'success' => false, + 'reason' => lang('Auth.oldToken'), + ]); + } + + $token->last_used_at = Time::now()->format('Y-m-d H:i:s'); + + if ($token->hasChanged()) { + $identityModel->save($token); + } + + // Ensure the token is set as the current token + $user = $token->user(); + $user->setAccessToken($token); + + return new Result([ + 'success' => true, + 'extraInfo' => $user, + ]); + } + + /** + * Checks if the user is currently logged in. + * Since AccessToken usage is inherently stateless, + * it runs $this->attempt on each usage. + */ + public function loggedIn(): bool + { + if (! empty($this->user)) { + return true; + } + + /** @var IncomingRequest $request */ + $request = service('request'); + + return $this->attempt([ + 'token' => $request->getHeaderLine(config('Auth')->authenticatorHeader['tokens']), + ])->isOK(); + } + + /** + * Logs the given user in by saving them to the class. + */ + public function login(User $user): void + { + $this->user = $user; + } + + /** + * Logs a user in based on their ID. + * + * @param int|string $userId + * + * @throws AuthenticationException + */ + public function loginById($userId): void + { + $user = $this->provider->findById($userId); + + if (empty($user)) { + throw AuthenticationException::forInvalidUser(); + } + + $user->setAccessToken( + $user->getAccessToken($this->getBearerToken()) + ); + + $this->login($user); + } + + /** + * Logs the current user out. + */ + public function logout(): void + { + $this->user = null; + } + + /** + * Returns the currently logged in user. + */ + public function getUser(): ?User + { + return $this->user; + } + + /** + * Returns the Bearer token from the Authorization header + */ + public function getBearerToken(): ?string + { + /** @var IncomingRequest $request */ + $request = service('request'); + + $header = $request->getHeaderLine(config('Auth')->authenticatorHeader['tokens']); + + if (empty($header)) { + return null; + } + + return trim(substr($header, 6)); // 'Bearer' + } + + /** + * Updates the user's last active date. + */ + public function recordActiveDate(): void + { + if (! $this->user instanceof User) { + throw new InvalidArgumentException( + __METHOD__ . '() requires logged in user before calling.' + ); + } + + $this->user->last_active = Time::now(); + + $this->provider->updateActiveDate($this->user); + } +} diff --git a/src/Authentication/Traits/HasHMACTokens.php b/src/Authentication/Traits/HasHMACTokens.php new file mode 100644 index 000000000..12973de6a --- /dev/null +++ b/src/Authentication/Traits/HasHMACTokens.php @@ -0,0 +1,150 @@ +generateHMACToken($this, $name, $scopes); + } + + /** + * Delete any HMAC tokens for the given raw token. + */ + public function revokeHMACToken(string $rawToken): void + { + /** @var UserIdentityModel $identityModel */ + $identityModel = model(UserIdentityModel::class); + + $identityModel->revokeHMACToken($this, $rawToken); + } + + /** + * Revokes all HMAC tokens for this user. + */ + public function revokeAllHMACTokens(): void + { + /** @var UserIdentityModel $identityModel */ + $identityModel = model(UserIdentityModel::class); + + $identityModel->revokeAllHMACTokens($this); + } + + /** + * Retrieves all personal HMAC tokens for this user. + * + * @return AccessToken[] + */ + public function hmacTokens(): array + { + /** @var UserIdentityModel $identityModel */ + $identityModel = model(UserIdentityModel::class); + + return $identityModel->getAllHMACTokens($this); + } + + /** + * Given a raw token, it will locate it within the system. + */ + public function getHmacToken(?string $rawToken): ?AccessToken + { + if (empty($rawToken)) { + return null; + } + + /** @var UserIdentityModel $identityModel */ + $identityModel = model(UserIdentityModel::class); + + return $identityModel->getHMACToken($this, $rawToken); + } + + /** + * Given the ID, returns the given access token. + */ + public function getHMACTokenById(int $id): ?AccessToken + { + /** @var UserIdentityModel $identityModel */ + $identityModel = model(UserIdentityModel::class); + + return $identityModel->getHMACTokenById($id, $this); + } + + // /** + // * Determines whether the user's token grants permissions to $scope. + // * First checks against $this->activeToken, which is set during + // * authentication. If it hasn't been set, returns false. + // */ + // public function tokenCan(string $scope): bool + // { + // if (! $this->currentAccessToken() instanceof AccessToken) { + // return false; + // } + // + // return $this->currentAccessToken()->can($scope); + // } + // + // /** + // * Determines whether the user's token does NOT grant permissions to $scope. + // * First checks against $this->activeToken, which is set during + // * authentication. If it hasn't been set, returns true. + // */ + // public function tokenCant(string $scope): bool + // { + // if (! $this->currentAccessToken() instanceof AccessToken) { + // return true; + // } + // + // return $this->currentAccessToken()->cant($scope); + // } + + /** + * Returns the current HMAC token for the user. + */ + public function currentHMACToken(): ?AccessToken + { + return $this->currentAccessToken; + } + + /** + * Sets the current active token for this user. + * + * @return $this + */ + public function setHMACToken(?AccessToken $accessToken): self + { + $this->currentAccessToken = $accessToken; + + return $this; + } +} diff --git a/src/Config/Auth.php b/src/Config/Auth.php index 792a8ba83..bf855ee85 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -8,6 +8,7 @@ use CodeIgniter\Shield\Authentication\Actions\ActionInterface; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; use CodeIgniter\Shield\Authentication\Authenticators\AccessTokens; +use CodeIgniter\Shield\Authentication\Authenticators\HMAC_SHA256; use CodeIgniter\Shield\Authentication\Authenticators\JWT; use CodeIgniter\Shield\Authentication\Authenticators\Session; use CodeIgniter\Shield\Authentication\Passwords\CompositionValidator; @@ -134,6 +135,7 @@ class Auth extends BaseConfig public array $authenticators = [ 'tokens' => AccessTokens::class, 'session' => Session::class, + 'hmac' => HMAC_SHA256::class, // 'jwt' => JWT::class, ]; diff --git a/src/Entities/User.php b/src/Entities/User.php index 2c966e424..1b8a326dd 100644 --- a/src/Entities/User.php +++ b/src/Entities/User.php @@ -8,6 +8,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\Session; use CodeIgniter\Shield\Authentication\Traits\HasAccessTokens; +use CodeIgniter\Shield\Authentication\Traits\HasHMACTokens; use CodeIgniter\Shield\Authorization\Traits\Authorizable; use CodeIgniter\Shield\Models\LoginModel; use CodeIgniter\Shield\Models\UserIdentityModel; @@ -28,6 +29,7 @@ class User extends Entity { use Authorizable; use HasAccessTokens; + use HasHMACTokens; use Resettable; use Activatable; use Bannable; diff --git a/src/Filters/HmacAuth.php b/src/Filters/HmacAuth.php new file mode 100644 index 000000000..ad60486f8 --- /dev/null +++ b/src/Filters/HmacAuth.php @@ -0,0 +1,68 @@ +getCookie($sessionConfig->cookieName); + log_message('debug', 'Session Cooky: ' . print_r($sessionCooky, true)); + + $authenticator = auth('HMAC-SHA256')->getAuthenticator(); + + $result = $authenticator->attempt([ + 'token' => $request->getHeaderLine(setting('Auth.authenticatorHeader')['tokens'] ?? 'Authorization'), + 'body' => file_get_contents('php://input'), + ]); + + if (! $result->isOK() || (! empty($arguments) && $result->extraInfo()->tokenCant($arguments[0]))) { + return service('response') + ->setStatusCode(Response::HTTP_UNAUTHORIZED) + ->setJson(['message' => lang('Auth.badToken')]); + } + + if (setting('Auth.recordActiveDate')) { + $authenticator->recordActiveDate(); + } + + // Block inactive users when Email Activation is enabled + $user = $authenticator->getUser(); + if ($user !== null && ! $user->isActivated()) { + $authenticator->logout(); + + return service('response') + ->setStatusCode(Response::HTTP_FORBIDDEN) + ->setJson(['message' => lang('Auth.activationBlocked')]); + } + + return $request; + + } + + /** + * {@inheritDoc} + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void + { + + } +} diff --git a/src/Models/UserIdentityModel.php b/src/Models/UserIdentityModel.php index ee19be499..510de3476 100644 --- a/src/Models/UserIdentityModel.php +++ b/src/Models/UserIdentityModel.php @@ -6,6 +6,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\AccessTokens; +use CodeIgniter\Shield\Authentication\Authenticators\HMAC_SHA256; use CodeIgniter\Shield\Authentication\Authenticators\Session; use CodeIgniter\Shield\Authentication\Passwords; use CodeIgniter\Shield\Entities\AccessToken; @@ -13,7 +14,9 @@ use CodeIgniter\Shield\Entities\UserIdentity; use CodeIgniter\Shield\Exceptions\LogicException; use CodeIgniter\Shield\Exceptions\ValidationException; +use Exception; use Faker\Generator; +use ReflectionException; class UserIdentityModel extends BaseModel { @@ -211,6 +214,143 @@ public function getAllAccessTokens(User $user): array ->findAll(); } + // HMAC + /** + * Find and Retrieve the HMAC AccessToken based on Token alone + * + * @return ?AccessToken + */ + public function getHMACTokenByKey(string $key): ?AccessToken + { + return $this + ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('secret', $key) + ->asObject(AccessToken::class) + ->first(); + } + + /** + * Generates a new personal access token for the user. + * + * @param string $name Token name + * @param string[] $scopes Permissions the token grants + * + * @throws Exception + * @throws ReflectionException + */ + public function generateHMACToken(User $user, string $name, array $scopes = ['*']): AccessToken + { + $this->checkUserId($user); + + // helper('text'); + + $return = $this->insert([ + 'type' => HMAC_SHA256::ID_TYPE_HMAC_TOKEN, + 'user_id' => $user->id, + 'name' => $name, + 'secret' => bin2hex(random_bytes(16)), // Key + 'secret2' => bin2hex(random_bytes(16)), // Secret Key + 'extra' => serialize($scopes), + ]); + + $this->checkQueryReturn($return); + + /** @var AccessToken $token */ + return $this + ->asObject(AccessToken::class) + ->find($this->getInsertID()); + } + + /** + * Retrieve Token object for selected HMAC Token. + * Note: These tokens are not hashed as they are considered shared secrets. + * + * @param User $user User Object + * @param string $key HMAC Key String + * + * @return ?AccessToken + */ + public function getHMACToken(User $user, string $key): ?AccessToken + { + $this->checkUserId($user); + + return $this->where('user_id', $user->id) + ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('secret', $key) + ->asObject(AccessToken::class) + ->first(); + } + + /** + * Given the ID, returns the given access token. + * + * @param int|string $id + * @param User $user User Object + * + * @return ?AccessToken + */ + public function getHMACTokenById($id, User $user): ?AccessToken + { + $this->checkUserId($user); + + return $this->where('user_id', $user->id) + ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('id', $id) + ->asObject(AccessToken::class) + ->first(); + } + + /** + * Retrieve all HMAC tokes for users + * + * @param User $user User object + * + * @return AccessToken[] + */ + public function getAllHMACTokens(User $user): array + { + $this->checkUserId($user); + + return $this + ->where('user_id', $user->id) + ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->orderBy($this->primaryKey) + ->asObject(AccessToken::class) + ->findAll(); + } + + /** + * Delete any HMAC tokens for the given key. + * + * @param User $user User object + * @param string $key HMAC Key + */ + public function revokeHMACToken(User $user, string $key): void + { + $this->checkUserId($user); + + $return = $this->where('user_id', $user->id) + ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('secret', $key) + ->delete(); + + $this->checkQueryReturn($return); + } + + /** + * Revokes all access tokens for this user. + */ + public function revokeAllHMACTokens(User $user): void + { + $this->checkUserId($user); + + $return = $this->where('user_id', $user->id) + ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->delete(); + + $this->checkQueryReturn($return); + } + /** * Used by 'magic-link'. */ @@ -351,7 +491,7 @@ public function forceMultiplePasswordReset(array $userIds): void /** * Force global password reset. * This is useful for enforcing a password reset - * for ALL users incase of a security breach. + * for ALL users in case of a security breach. */ public function forceGlobalPasswordReset(): void { From f6341728f70037030b8f9b81a9f112edc489ae20 Mon Sep 17 00:00:00 2001 From: tswagger Date: Mon, 14 Aug 2023 21:01:18 -0500 Subject: [PATCH 02/41] Authenticator test pass --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d5c97a907..676c7bb2c 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 From 52b5d8be70bfa376ba07dcb6ba471707ea5425b4 Mon Sep 17 00:00:00 2001 From: tswagger Date: Mon, 14 Aug 2023 21:09:46 -0500 Subject: [PATCH 03/41] Authenticator test pass --- .../Authenticators/HMAC_SHA256.php | 71 +++++- src/Authentication/Traits/HasHMACTokens.php | 62 ++--- src/Filters/HmacAuth.php | 5 - .../Authenticators/HMACAuthenticatorTest.php | 238 ++++++++++++++++++ 4 files changed, 329 insertions(+), 47 deletions(-) create mode 100644 tests/Authentication/Authenticators/HMACAuthenticatorTest.php diff --git a/src/Authentication/Authenticators/HMAC_SHA256.php b/src/Authentication/Authenticators/HMAC_SHA256.php index f79c22b10..125bd547c 100644 --- a/src/Authentication/Authenticators/HMAC_SHA256.php +++ b/src/Authentication/Authenticators/HMAC_SHA256.php @@ -74,8 +74,8 @@ public function attempt(array $credentials): Result ]); } - $user = $user->setAccessToken( - $user->getAccessToken($this->getBearerToken()) + $user = $user->setHMACToken( + $user->getHMACToken($this->getAuthKeyFromToken()) ); $this->login($user); @@ -108,12 +108,12 @@ public function check(array $credentials): Result ]); } - if (strpos($credentials['token'], 'HMAC-SHA256') === 0) { - $credentials['token'] = trim(substr($credentials['token'], 11)); + if (strpos($credentials['token'], 'Bearer') === 0) { + $credentials['token'] = trim(substr($credentials['token'], 6)); } // Extract UserToken and HMACSHA256 Signature from Authorization token - [$userToken, $signature] = preg_split('/:/', $credentials['token'], -1, PREG_SPLIT_NO_EMPTY); + [$userToken, $signature] = $this->getHMACAuthTokens($credentials['token']); /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); @@ -157,7 +157,7 @@ public function check(array $credentials): Result // Ensure the token is set as the current token $user = $token->user(); - $user->setAccessToken($token); + $user->setHMACToken($token); return new Result([ 'success' => true, @@ -207,8 +207,8 @@ public function loginById($userId): void throw AuthenticationException::forInvalidUser(); } - $user->setAccessToken( - $user->getAccessToken($this->getBearerToken()) + $user->setHMACToken( + $user->getHMACToken($this->getAuthKeyFromToken()) ); $this->login($user); @@ -223,7 +223,7 @@ public function logout(): void } /** - * Returns the currently logged in user. + * Returns the currently logged-in user. */ public function getUser(): ?User { @@ -231,9 +231,11 @@ public function getUser(): ?User } /** - * Returns the Bearer token from the Authorization header + * Returns the Full Authorization token from the Authorization header + * + * @return ?string */ - public function getBearerToken(): ?string + public function getFullAuthToken(): ?string { /** @var IncomingRequest $request */ $request = service('request'); @@ -244,7 +246,52 @@ public function getBearerToken(): ?string return null; } - return trim(substr($header, 6)); // 'Bearer' + return trim(substr($header, 11)); // 'HMAC-SHA256' + } + + /** + * Get Key and HMAC hash from Auth token + * + * @param ?string $fullToken Full Token + * + * @return ?array [key, hmacHash] + */ + public function getHMACAuthTokens(?string $fullToken = null): ?array + { + if (! isset($fullToken)) { + $fullToken = $this->getFullAuthToken(); + } + + if (isset($fullToken)) { + return preg_split('/:/', $fullToken, -1, PREG_SPLIT_NO_EMPTY); + } + + return null; + + } + + /** + * Retrieve the key from the Auth token + * + * @return ?string + */ + public function getAuthKeyFromToken(): ?string + { + [$key, $hmacHash] = $this->getHMACAuthTokens(); + + return $key; + } + + /** + * Retrieve the HMAC Hash from the Auth token + * + * @return ?string + */ + public function getHMACHashFromToken(): ?string + { + [$key, $hmacHash] = $this->getHMACAuthTokens(); + + return $hmacHash; } /** diff --git a/src/Authentication/Traits/HasHMACTokens.php b/src/Authentication/Traits/HasHMACTokens.php index 12973de6a..6f6fe6626 100644 --- a/src/Authentication/Traits/HasHMACTokens.php +++ b/src/Authentication/Traits/HasHMACTokens.php @@ -21,7 +21,7 @@ trait HasHMACTokens /** * The current access token for the user. */ - private ?AccessToken $currentAccessToken = null; + private ?AccessToken $currentHMACToken = null; /** * Generates a new personal HMAC token for this user. @@ -75,18 +75,18 @@ public function hmacTokens(): array } /** - * Given a raw token, it will locate it within the system. + * Given a secret Key, it will locate it within the system. */ - public function getHmacToken(?string $rawToken): ?AccessToken + public function getHmacToken(?string $secretKey): ?AccessToken { - if (empty($rawToken)) { + if (empty($secretKey)) { return null; } /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); - return $identityModel->getHMACToken($this, $rawToken); + return $identityModel->getHMACToken($this, $secretKey); } /** @@ -100,40 +100,42 @@ public function getHMACTokenById(int $id): ?AccessToken return $identityModel->getHMACTokenById($id, $this); } - // /** - // * Determines whether the user's token grants permissions to $scope. - // * First checks against $this->activeToken, which is set during - // * authentication. If it hasn't been set, returns false. - // */ - // public function tokenCan(string $scope): bool - // { - // if (! $this->currentAccessToken() instanceof AccessToken) { - // return false; + // Commented out as it collides with methods from CodeIgniter\Shield\Authentication\Traits\HasHMACTokens + + // /** + // * Determines whether the user's token grants permissions to $scope. + // * First checks against $this->activeToken, which is set during + // * authentication. If it hasn't been set, returns false. + // */ + // public function tokenCan(string $scope): bool + // { + // if (! $this->currentAccessToken() instanceof AccessToken) { + // return false; + // } + // + // return $this->currentAccessToken()->can($scope); // } // - // return $this->currentAccessToken()->can($scope); - // } + // /** + // * Determines whether the user's token does NOT grant permissions to $scope. + // * First checks against $this->activeToken, which is set during + // * authentication. If it hasn't been set, returns true. + // */ + // public function tokenCant(string $scope): bool + // { + // if (! $this->currentAccessToken() instanceof AccessToken) { + // return true; + // } // - // /** - // * Determines whether the user's token does NOT grant permissions to $scope. - // * First checks against $this->activeToken, which is set during - // * authentication. If it hasn't been set, returns true. - // */ - // public function tokenCant(string $scope): bool - // { - // if (! $this->currentAccessToken() instanceof AccessToken) { - // return true; + // return $this->currentAccessToken()->cant($scope); // } - // - // return $this->currentAccessToken()->cant($scope); - // } /** * Returns the current HMAC token for the user. */ public function currentHMACToken(): ?AccessToken { - return $this->currentAccessToken; + return $this->currentHMACToken; } /** @@ -143,7 +145,7 @@ public function currentHMACToken(): ?AccessToken */ public function setHMACToken(?AccessToken $accessToken): self { - $this->currentAccessToken = $accessToken; + $this->currentHMACToken = $accessToken; return $this; } diff --git a/src/Filters/HmacAuth.php b/src/Filters/HmacAuth.php index ad60486f8..6ffdc708c 100644 --- a/src/Filters/HmacAuth.php +++ b/src/Filters/HmacAuth.php @@ -22,11 +22,6 @@ class HmacAuth implements FilterInterface public function before(RequestInterface $request, $arguments = null) { - $sessionConfig = new \Config\Session(); - - $sessionCooky = $request->getCookie($sessionConfig->cookieName); - log_message('debug', 'Session Cooky: ' . print_r($sessionCooky, true)); - $authenticator = auth('HMAC-SHA256')->getAuthenticator(); $result = $authenticator->attempt([ diff --git a/tests/Authentication/Authenticators/HMACAuthenticatorTest.php b/tests/Authentication/Authenticators/HMACAuthenticatorTest.php new file mode 100644 index 000000000..523c6885c --- /dev/null +++ b/tests/Authentication/Authenticators/HMACAuthenticatorTest.php @@ -0,0 +1,238 @@ +setProvider(model(UserModel::class)); + + /** @var HMAC_SHA256 $authenticator */ + $authenticator = $auth->factory('hmac'); + $this->auth = $authenticator; + + Services::injectMock('events', new MockEvents()); + } + + public function testLogin(): void + { + $user = fake(UserModel::class); + + $this->auth->login($user); + + // Stores the user + $this->assertInstanceOf(User::class, $this->auth->getUser()); + $this->assertSame($user->id, $this->auth->getUser()->id); + } + + public function testLogout(): void + { + // this one's a little odd since it's stateless, but roll with it... + $user = fake(UserModel::class); + + $this->auth->login($user); + $this->assertNotNull($this->auth->getUser()); + + $this->auth->logout(); + $this->assertNull($this->auth->getUser()); + } + + public function testLoginByIdNoToken(): void + { + $user = fake(UserModel::class); + + $this->assertFalse($this->auth->loggedIn()); + + $this->auth->loginById($user->id); + + $this->assertTrue($this->auth->loggedIn()); + $this->assertNull($this->auth->getUser()->currentHMACToken()); + } + + public function testLoginByIdWithToken(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + $token = $user->generateHMACToken('foo'); + + $rawToken = $this->generateRawHeaderToken($token->secret, $token->secret2, 'bar'); + $this->setRequestHeader($rawToken); + + $this->auth->loginById($user->id); + + $this->assertTrue($this->auth->loggedIn()); + $this->assertInstanceOf(AccessToken::class, $this->auth->getUser()->currentHMACToken()); + $this->assertSame($token->id, $this->auth->getUser()->currentHMACToken()->id); + } + + public function testLoginByIdWithMultipleTokens(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + $token1 = $user->generateHMACToken('foo'); + $user->generateHMACToken('bar'); + + $this->setRequestHeader($this->generateRawHeaderToken($token1->secret, $token1->secret2, 'bar')); + + $this->auth->loginById($user->id); + + $this->assertTrue($this->auth->loggedIn()); + $this->assertInstanceOf(AccessToken::class, $this->auth->getUser()->currentHMACToken()); + $this->assertSame($token1->id, $this->auth->getUser()->currentHMACToken()->id); + } + + public function testCheckNoToken(): void + { + $result = $this->auth->check([]); + + $this->assertFalse($result->isOK()); + $this->assertSame(lang('Auth.noToken', [config('Auth')->authenticatorHeader['tokens']]), $result->reason()); + } + + public function testCheckBadToken(): void + { + $result = $this->auth->check([ + 'token' => 'abc123:lasdkjflksjdflksjdf', + 'body' => 'bar', + ]); + + $this->assertFalse($result->isOK()); + $this->assertSame(lang('Auth.badToken'), $result->reason()); + } + + public function testCheckOldToken(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + /** @var UserIdentityModel $identities */ + $identities = model(UserIdentityModel::class); + $token = $user->generateHMACToken('foo'); + // CI 4.2 uses the Chicago timezone that has Daylight Saving Time, + // so subtracts 1 hour to make sure this test passes. + $token->last_used_at = Time::now()->subYears(1)->subHours(1)->subMinutes(1); + $identities->save($token); + + $result = $this->auth->check([ + 'token' => $this->generateRawHeaderToken($token->secret, $token->secret2, 'bar'), + 'body' => 'bar', + ]); + + $this->assertFalse($result->isOK()); + $this->assertSame(lang('Auth.oldToken'), $result->reason()); + } + + public function testCheckSuccess(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + $token = $user->generateHMACToken('foo'); + + $this->seeInDatabase($this->tables['identities'], [ + 'user_id' => $user->id, + 'type' => 'hmac_sha256', + 'last_used_at' => null, + ]); + + $rawToken = $this->generateRawHeaderToken($token->secret, $token->secret2, 'bar'); + + $result = $this->auth->check([ + 'token' => $rawToken, + 'body' => 'bar', + ]); + + $this->assertTrue($result->isOK()); + $this->assertInstanceOf(User::class, $result->extraInfo()); + $this->assertSame($user->id, $result->extraInfo()->id); + + $updatedToken = $result->extraInfo()->currentHMACToken(); + $this->assertNotEmpty($updatedToken->last_used_at); + + // Checking token in the same second does not throw "DataException : There is no data to update." + $this->auth->check(['token' => $rawToken, 'body' => 'bar']); + } + + public function testAttemptCannotFindUser(): void + { + $result = $this->auth->attempt([ + 'token' => 'abc123:lsakdjfljsdflkajsfd', + 'body' => 'bar', + ]); + + $this->assertInstanceOf(Result::class, $result); + $this->assertFalse($result->isOK()); + $this->assertSame(lang('Auth.badToken'), $result->reason()); + + // A login attempt should have always been recorded + $this->seeInDatabase($this->tables['token_logins'], [ + 'id_type' => HMAC_SHA256::ID_TYPE_HMAC_TOKEN, + 'identifier' => 'abc123:lsakdjfljsdflkajsfd', + 'success' => 0, + ]); + } + + public function testAttemptSuccess(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + $token = $user->generateHMACToken('foo'); + $rawToken = $this->generateRawHeaderToken($token->secret, $token->secret2, 'bar'); + $this->setRequestHeader($rawToken); + + $result = $this->auth->attempt([ + 'token' => $rawToken, + 'body' => 'bar', + ]); + + $this->assertInstanceOf(Result::class, $result); + $this->assertTrue($result->isOK()); + + $foundUser = $result->extraInfo(); + $this->assertInstanceOf(User::class, $foundUser); + $this->assertSame($user->id, $foundUser->id); + $this->assertInstanceOf(AccessToken::class, $foundUser->currentHMACToken()); + $this->assertSame($token->token, $foundUser->currentHMACToken()->token); + + // A login attempt should have been recorded + $this->seeInDatabase($this->tables['token_logins'], [ + 'id_type' => HMAC_SHA256::ID_TYPE_HMAC_TOKEN, + 'identifier' => $rawToken, + 'success' => 1, + ]); + } + + protected function setRequestHeader(string $token): void + { + $request = service('request'); + $request->setHeader('Authorization', 'HMAC-SHA256 ' . $token); + } + + protected function generateRawHeaderToken(string $secret, string $secretKey, string $body): string + { + return $secret . ':' . hash_hmac('sha256', $body, $secretKey); + } +} From 1a7802efc7304362db7dcdbddf5bedbc7ddaf682 Mon Sep 17 00:00:00 2001 From: tswagger Date: Tue, 15 Aug 2023 14:03:34 -0500 Subject: [PATCH 04/41] Added HMAC Filter tests. Pass --- .../Authenticators/HMAC_SHA256.php | 6 +- src/Authentication/Traits/HasHMACTokens.php | 56 ++++---- src/Config/Auth.php | 2 + src/Config/Registrar.php | 2 + src/Filters/HmacAuth.php | 12 +- src/Result.php | 2 +- .../Authenticators/HMACAuthenticatorTest.php | 27 +++- .../Authentication/Filters/HMACFilterTest.php | 135 ++++++++++++++++++ 8 files changed, 202 insertions(+), 40 deletions(-) create mode 100644 tests/Authentication/Filters/HMACFilterTest.php diff --git a/src/Authentication/Authenticators/HMAC_SHA256.php b/src/Authentication/Authenticators/HMAC_SHA256.php index 125bd547c..577c7266e 100644 --- a/src/Authentication/Authenticators/HMAC_SHA256.php +++ b/src/Authentication/Authenticators/HMAC_SHA256.php @@ -104,12 +104,12 @@ public function check(array $credentials): Result if (! array_key_exists('token', $credentials) || empty($credentials['token'])) { return new Result([ 'success' => false, - 'reason' => lang('Auth.noToken', [config('Auth')->authenticatorHeader['tokens']]), + 'reason' => lang('Auth.noToken', [config('Auth')->authenticatorHeader['hmac']]), ]); } - if (strpos($credentials['token'], 'Bearer') === 0) { - $credentials['token'] = trim(substr($credentials['token'], 6)); + if (strpos($credentials['token'], 'HMAC-SHA256') === 0) { + $credentials['token'] = trim(substr($credentials['token'], 11)); // HMAC-SHA256 } // Extract UserToken and HMACSHA256 Signature from Authorization token diff --git a/src/Authentication/Traits/HasHMACTokens.php b/src/Authentication/Traits/HasHMACTokens.php index 6f6fe6626..c30d69d4f 100644 --- a/src/Authentication/Traits/HasHMACTokens.php +++ b/src/Authentication/Traits/HasHMACTokens.php @@ -100,35 +100,33 @@ public function getHMACTokenById(int $id): ?AccessToken return $identityModel->getHMACTokenById($id, $this); } - // Commented out as it collides with methods from CodeIgniter\Shield\Authentication\Traits\HasHMACTokens - - // /** - // * Determines whether the user's token grants permissions to $scope. - // * First checks against $this->activeToken, which is set during - // * authentication. If it hasn't been set, returns false. - // */ - // public function tokenCan(string $scope): bool - // { - // if (! $this->currentAccessToken() instanceof AccessToken) { - // return false; - // } - // - // return $this->currentAccessToken()->can($scope); - // } - // - // /** - // * Determines whether the user's token does NOT grant permissions to $scope. - // * First checks against $this->activeToken, which is set during - // * authentication. If it hasn't been set, returns true. - // */ - // public function tokenCant(string $scope): bool - // { - // if (! $this->currentAccessToken() instanceof AccessToken) { - // return true; - // } - // - // return $this->currentAccessToken()->cant($scope); - // } + /** + * Determines whether the user's token grants permissions to $scope. + * First checks against $this->activeToken, which is set during + * authentication. If it hasn't been set, returns false. + */ + public function hmacTokenCan(string $scope): bool + { + if (! $this->currentHMACToken() instanceof AccessToken) { + return false; + } + + return $this->currentHMACToken()->can($scope); + } + + /** + * Determines whether the user's token does NOT grant permissions to $scope. + * First checks against $this->activeToken, which is set during + * authentication. If it hasn't been set, returns true. + */ + public function hmacTokenCant(string $scope): bool + { + if (! $this->currentHMACToken() instanceof AccessToken) { + return true; + } + + return $this->currentHMACToken()->cant($scope); + } /** * Returns the current HMAC token for the user. diff --git a/src/Config/Auth.php b/src/Config/Auth.php index bf855ee85..69cab44a8 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -149,6 +149,7 @@ class Auth extends BaseConfig */ public array $authenticatorHeader = [ 'tokens' => 'Authorization', + 'hmac' => 'Authorization', ]; /** @@ -183,6 +184,7 @@ class Auth extends BaseConfig public array $authenticationChain = [ 'session', 'tokens', + 'hmac', // 'jwt', ]; diff --git a/src/Config/Registrar.php b/src/Config/Registrar.php index 290b036d5..8c366e8cd 100644 --- a/src/Config/Registrar.php +++ b/src/Config/Registrar.php @@ -10,6 +10,7 @@ use CodeIgniter\Shield\Filters\ChainAuth; use CodeIgniter\Shield\Filters\ForcePasswordResetFilter; use CodeIgniter\Shield\Filters\GroupFilter; +use CodeIgniter\Shield\Filters\HmacAuth; use CodeIgniter\Shield\Filters\JWTAuth; use CodeIgniter\Shield\Filters\PermissionFilter; use CodeIgniter\Shield\Filters\SessionAuth; @@ -26,6 +27,7 @@ public static function Filters(): array 'aliases' => [ 'session' => SessionAuth::class, 'tokens' => TokenAuth::class, + 'hmac' => HmacAuth::class, 'chain' => ChainAuth::class, 'auth-rates' => AuthRates::class, 'group' => GroupFilter::class, diff --git a/src/Filters/HmacAuth.php b/src/Filters/HmacAuth.php index 6ffdc708c..4797bb09b 100644 --- a/src/Filters/HmacAuth.php +++ b/src/Filters/HmacAuth.php @@ -22,14 +22,16 @@ class HmacAuth implements FilterInterface public function before(RequestInterface $request, $arguments = null) { - $authenticator = auth('HMAC-SHA256')->getAuthenticator(); + $authenticator = auth('hmac')->getAuthenticator(); - $result = $authenticator->attempt([ - 'token' => $request->getHeaderLine(setting('Auth.authenticatorHeader')['tokens'] ?? 'Authorization'), + $requestParams = [ + 'token' => $request->getHeaderLine(setting('Auth.authenticatorHeader')['hmac'] ?? 'Authorization'), 'body' => file_get_contents('php://input'), - ]); + ]; - if (! $result->isOK() || (! empty($arguments) && $result->extraInfo()->tokenCant($arguments[0]))) { + $result = $authenticator->attempt($requestParams); + + if (! $result->isOK() || (! empty($arguments) && $result->extraInfo()->hmacTokenCant($arguments[0]))) { return service('response') ->setStatusCode(Response::HTTP_UNAUTHORIZED) ->setJson(['message' => lang('Auth.badToken')]); diff --git a/src/Result.php b/src/Result.php index fa0ce1b37..9a3f368ca 100644 --- a/src/Result.php +++ b/src/Result.php @@ -13,7 +13,7 @@ class Result /** * Provides a simple explanation of * the error that happened. - * Typically a single sentence. + * Typically, a single sentence. */ protected ?string $reason = null; diff --git a/tests/Authentication/Authenticators/HMACAuthenticatorTest.php b/tests/Authentication/Authenticators/HMACAuthenticatorTest.php index 523c6885c..c19f700d7 100644 --- a/tests/Authentication/Authenticators/HMACAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/HMACAuthenticatorTest.php @@ -111,10 +111,10 @@ public function testCheckNoToken(): void $result = $this->auth->check([]); $this->assertFalse($result->isOK()); - $this->assertSame(lang('Auth.noToken', [config('Auth')->authenticatorHeader['tokens']]), $result->reason()); + $this->assertSame(lang('Auth.noToken', [config('Auth')->authenticatorHeader['hmac']]), $result->reason()); } - public function testCheckBadToken(): void + public function testCheckBadSignature(): void { $result = $this->auth->check([ 'token' => 'abc123:lasdkjflksjdflksjdf', @@ -176,6 +176,29 @@ public function testCheckSuccess(): void $this->auth->check(['token' => $rawToken, 'body' => 'bar']); } + public function testCheckBadToken(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + $token = $user->generateHMACToken('foo'); + + $this->seeInDatabase($this->tables['identities'], [ + 'user_id' => $user->id, + 'type' => 'hmac_sha256', + 'last_used_at' => null, + ]); + + $rawToken = $this->generateRawHeaderToken($token->secret, $token->secret2, 'foobar'); + + $result = $this->auth->check([ + 'token' => $rawToken, + 'body' => 'bar', + ]); + + $this->assertfalse($result->isOK()); + $this->assertSame(lang('Auth.badToken'), $result->reason()); + } + public function testAttemptCannotFindUser(): void { $result = $this->auth->attempt([ diff --git a/tests/Authentication/Filters/HMACFilterTest.php b/tests/Authentication/Filters/HMACFilterTest.php new file mode 100644 index 000000000..5543e8592 --- /dev/null +++ b/tests/Authentication/Filters/HMACFilterTest.php @@ -0,0 +1,135 @@ +call('get', 'protected-route'); + + $result->assertStatus(401); + + $result = $this->get('open-route'); + $result->assertStatus(200); + $result->assertSee('Open'); + } + + public function testFilterSuccess(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + $token = $user->generateHMACToken('foo'); + + $rawToken = $this->generateRawHeaderToken($token->secret, $token->secret2, ''); + $result = $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $rawToken]) + ->get('protected-route'); + + $result->assertStatus(200); + $result->assertSee('Protected'); + + $this->assertSame($user->id, auth('hmac')->id()); + $this->assertSame($user->id, auth('hmac')->user()->id); + + // User should have the current token set. + $this->assertInstanceOf(AccessToken::class, auth('hmac')->user()->currentHMACToken()); + $this->assertSame($token->id, auth('hmac')->user()->currentHMACToken()->id); + } + + public function testFilterInvalidSignature(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + $token = $user->generateHMACToken('foo'); + + $result = $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $this->generateRawHeaderToken($token->secret, $token->secret2, 'bar')]) + ->get('protected-route'); + + $result->assertStatus(401); + } + + public function testRecordActiveDate(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + $token = $user->generateHMACToken('foo'); + + $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $this->generateRawHeaderToken($token->secret, $token->secret2, '')]) + ->get('protected-route'); + + // Last Active should be greater than 'updated_at' column + $this->assertGreaterThan(auth('hmac')->user()->updated_at, auth('hmac')->user()->last_active); + } + + public function testFiltersProtectsWithScopes(): void + { + /** @var User $user1 */ + $user1 = fake(UserModel::class); + $token1 = $user1->generateHMACToken('foo', ['users-read']); + /** @var User $user2 */ + $user2 = fake(UserModel::class); + $token2 = $user2->generateHMACToken('foo', ['users-write']); + + // User 1 should be able to access the route + $result1 = $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $this->generateRawHeaderToken($token1->secret, $token1->secret2, '')]) + ->get('protected-user-route'); + + $result1->assertStatus(200); + // Last Active should be greater than 'updated_at' column + $this->assertGreaterThan(auth('hmac')->user()->updated_at, auth('hmac')->user()->last_active); + + // User 2 should NOT be able to access the route + $result2 = $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $this->generateRawHeaderToken($token2->secret, $token2->secret2, '')]) + ->get('protected-user-route'); + + $result2->assertStatus(401); + } + + public function testBlocksInactiveUsers(): void + { + /** @var User $user */ + $user = fake(UserModel::class, ['active' => false]); + $token = $user->generateHMACToken('foo'); + + // Activation only required with email activation + setting('Auth.actions', ['register' => null]); + + $result = $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $this->generateRawHeaderToken($token->secret, $token->secret2, '')]) + ->get('protected-route'); + + $result->assertStatus(200); + $result->assertSee('Protected'); + + // Now require user activation and try again + setting('Auth.actions', ['register' => '\CodeIgniter\Shield\Authentication\Actions\EmailActivator']); + + $result = $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $this->generateRawHeaderToken($token->secret, $token->secret2, '')]) + ->get('protected-route'); + + $result->assertStatus(403); + + setting('Auth.actions', ['register' => null]); + } + + protected function generateRawHeaderToken(string $secret, string $secretKey, string $body): string + { + return $secret . ':' . hash_hmac('sha256', $body, $secretKey); + } +} From 1cd518f7ca58f0202a9ac5dfdce78146431e0844 Mon Sep 17 00:00:00 2001 From: tswagger Date: Fri, 18 Aug 2023 15:25:20 -0500 Subject: [PATCH 05/41] Added documentation Cleaned up naming convention. --- docs/authentication.md | 138 ++++++++++++++++++ docs/guides/api_hmac_keys.md | 113 ++++++++++++++ .../Authenticators/HMAC_SHA256.php | 8 +- src/Authentication/Traits/HasHMACTokens.php | 40 ++--- src/Models/UserIdentityModel.php | 14 +- .../Authenticators/HMACAuthenticatorTest.php | 30 ++-- .../Authentication/Filters/HMACFilterTest.php | 16 +- 7 files changed, 305 insertions(+), 54 deletions(-) create mode 100644 docs/guides/api_hmac_keys.md diff --git a/docs/authentication.md b/docs/authentication.md index 3b177d3dc..6255adeb2 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -303,3 +303,141 @@ if ($user->tokenCant('forums.manage')) { // do something.... } ``` + +## HMAC SHA256 Token Authenticator + +The HMAC-SHA256 authenticator supports the use of revoke-able 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 +In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request: + +``` +Authorization: HMAC-SHA256 : +``` + +The code to do this will look something like this: + +```php +generateHmacToken('Work Laptop'); +``` + +This creates the keys/tokens using a cryptographically secure random string. The keys opporate 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 'secredKey'. 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. + +By default, this is set to 1 year. You can change this value by setting the `accessTokenLifetime` +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.... +} +``` diff --git a/docs/guides/api_hmac_keys.md b/docs/guides/api_hmac_keys.md new file mode 100644 index 000000000..5f430c900 --- /dev/null +++ b/docs/guides/api_hmac_keys.md @@ -0,0 +1,113 @@ +# 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 +In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request: + +``` +Authorization: HMAC-SHA256 : +``` + +The code to do this will look something like this: + +```php +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 = [ + 'tokens' => ['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' => 'tokens'], 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. diff --git a/src/Authentication/Authenticators/HMAC_SHA256.php b/src/Authentication/Authenticators/HMAC_SHA256.php index 577c7266e..1ddc2a413 100644 --- a/src/Authentication/Authenticators/HMAC_SHA256.php +++ b/src/Authentication/Authenticators/HMAC_SHA256.php @@ -74,8 +74,8 @@ public function attempt(array $credentials): Result ]); } - $user = $user->setHMACToken( - $user->getHMACToken($this->getAuthKeyFromToken()) + $user = $user->setHmacToken( + $user->getHmacToken($this->getAuthKeyFromToken()) ); $this->login($user); @@ -157,7 +157,7 @@ public function check(array $credentials): Result // Ensure the token is set as the current token $user = $token->user(); - $user->setHMACToken($token); + $user->setHmacToken($token); return new Result([ 'success' => true, @@ -207,7 +207,7 @@ public function loginById($userId): void throw AuthenticationException::forInvalidUser(); } - $user->setHMACToken( + $user->setHmacToken( $user->getHMACToken($this->getAuthKeyFromToken()) ); diff --git a/src/Authentication/Traits/HasHMACTokens.php b/src/Authentication/Traits/HasHMACTokens.php index c30d69d4f..d6eb2df24 100644 --- a/src/Authentication/Traits/HasHMACTokens.php +++ b/src/Authentication/Traits/HasHMACTokens.php @@ -21,7 +21,7 @@ trait HasHMACTokens /** * The current access token for the user. */ - private ?AccessToken $currentHMACToken = null; + private ?AccessToken $currentHmacToken = null; /** * Generates a new personal HMAC token for this user. @@ -31,34 +31,34 @@ trait HasHMACTokens * * @throws ReflectionException */ - public function generateHMACToken(string $name, array $scopes = ['*']): AccessToken + public function generateHmacToken(string $name, array $scopes = ['*']): AccessToken { /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); - return $identityModel->generateHMACToken($this, $name, $scopes); + return $identityModel->generateHmacToken($this, $name, $scopes); } /** - * Delete any HMAC tokens for the given raw token. + * Delete any HMAC tokens for the given key. */ - public function revokeHMACToken(string $rawToken): void + public function revokeHmacToken(string $key): void { /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); - $identityModel->revokeHMACToken($this, $rawToken); + $identityModel->revokeHmacToken($this, $key); } /** * Revokes all HMAC tokens for this user. */ - public function revokeAllHMACTokens(): void + public function revokeAllHmacTokens(): void { /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); - $identityModel->revokeAllHMACTokens($this); + $identityModel->revokeAllHmacTokens($this); } /** @@ -71,7 +71,7 @@ public function hmacTokens(): array /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); - return $identityModel->getAllHMACTokens($this); + return $identityModel->getAllHmacTokens($this); } /** @@ -86,18 +86,18 @@ public function getHmacToken(?string $secretKey): ?AccessToken /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); - return $identityModel->getHMACToken($this, $secretKey); + return $identityModel->getHmacToken($this, $secretKey); } /** * Given the ID, returns the given access token. */ - public function getHMACTokenById(int $id): ?AccessToken + public function getHmacTokenById(int $id): ?AccessToken { /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); - return $identityModel->getHMACTokenById($id, $this); + return $identityModel->getHmacTokenById($id, $this); } /** @@ -107,11 +107,11 @@ public function getHMACTokenById(int $id): ?AccessToken */ public function hmacTokenCan(string $scope): bool { - if (! $this->currentHMACToken() instanceof AccessToken) { + if (! $this->currentHmacToken() instanceof AccessToken) { return false; } - return $this->currentHMACToken()->can($scope); + return $this->currentHmacToken()->can($scope); } /** @@ -121,19 +121,19 @@ public function hmacTokenCan(string $scope): bool */ public function hmacTokenCant(string $scope): bool { - if (! $this->currentHMACToken() instanceof AccessToken) { + if (! $this->currentHmacToken() instanceof AccessToken) { return true; } - return $this->currentHMACToken()->cant($scope); + return $this->currentHmacToken()->cant($scope); } /** * Returns the current HMAC token for the user. */ - public function currentHMACToken(): ?AccessToken + public function currentHmacToken(): ?AccessToken { - return $this->currentHMACToken; + return $this->currentHmacToken; } /** @@ -141,9 +141,9 @@ public function currentHMACToken(): ?AccessToken * * @return $this */ - public function setHMACToken(?AccessToken $accessToken): self + public function setHmacToken(?AccessToken $accessToken): self { - $this->currentHMACToken = $accessToken; + $this->currentHmacToken = $accessToken; return $this; } diff --git a/src/Models/UserIdentityModel.php b/src/Models/UserIdentityModel.php index 510de3476..36d7a0671 100644 --- a/src/Models/UserIdentityModel.php +++ b/src/Models/UserIdentityModel.php @@ -220,7 +220,7 @@ public function getAllAccessTokens(User $user): array * * @return ?AccessToken */ - public function getHMACTokenByKey(string $key): ?AccessToken + public function getHmacTokenByKey(string $key): ?AccessToken { return $this ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) @@ -238,7 +238,7 @@ public function getHMACTokenByKey(string $key): ?AccessToken * @throws Exception * @throws ReflectionException */ - public function generateHMACToken(User $user, string $name, array $scopes = ['*']): AccessToken + public function generateHmacToken(User $user, string $name, array $scopes = ['*']): AccessToken { $this->checkUserId($user); @@ -270,7 +270,7 @@ public function generateHMACToken(User $user, string $name, array $scopes = ['*' * * @return ?AccessToken */ - public function getHMACToken(User $user, string $key): ?AccessToken + public function getHmacToken(User $user, string $key): ?AccessToken { $this->checkUserId($user); @@ -289,7 +289,7 @@ public function getHMACToken(User $user, string $key): ?AccessToken * * @return ?AccessToken */ - public function getHMACTokenById($id, User $user): ?AccessToken + public function getHmacTokenById($id, User $user): ?AccessToken { $this->checkUserId($user); @@ -307,7 +307,7 @@ public function getHMACTokenById($id, User $user): ?AccessToken * * @return AccessToken[] */ - public function getAllHMACTokens(User $user): array + public function getAllHmacTokens(User $user): array { $this->checkUserId($user); @@ -325,7 +325,7 @@ public function getAllHMACTokens(User $user): array * @param User $user User object * @param string $key HMAC Key */ - public function revokeHMACToken(User $user, string $key): void + public function revokeHmacToken(User $user, string $key): void { $this->checkUserId($user); @@ -340,7 +340,7 @@ public function revokeHMACToken(User $user, string $key): void /** * Revokes all access tokens for this user. */ - public function revokeAllHMACTokens(User $user): void + public function revokeAllHmacTokens(User $user): void { $this->checkUserId($user); diff --git a/tests/Authentication/Authenticators/HMACAuthenticatorTest.php b/tests/Authentication/Authenticators/HMACAuthenticatorTest.php index c19f700d7..c8f7fb5ba 100644 --- a/tests/Authentication/Authenticators/HMACAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/HMACAuthenticatorTest.php @@ -71,14 +71,14 @@ public function testLoginByIdNoToken(): void $this->auth->loginById($user->id); $this->assertTrue($this->auth->loggedIn()); - $this->assertNull($this->auth->getUser()->currentHMACToken()); + $this->assertNull($this->auth->getUser()->currentHmacToken()); } public function testLoginByIdWithToken(): void { /** @var User $user */ $user = fake(UserModel::class); - $token = $user->generateHMACToken('foo'); + $token = $user->generateHmacToken('foo'); $rawToken = $this->generateRawHeaderToken($token->secret, $token->secret2, 'bar'); $this->setRequestHeader($rawToken); @@ -86,24 +86,24 @@ public function testLoginByIdWithToken(): void $this->auth->loginById($user->id); $this->assertTrue($this->auth->loggedIn()); - $this->assertInstanceOf(AccessToken::class, $this->auth->getUser()->currentHMACToken()); - $this->assertSame($token->id, $this->auth->getUser()->currentHMACToken()->id); + $this->assertInstanceOf(AccessToken::class, $this->auth->getUser()->currentHmacToken()); + $this->assertSame($token->id, $this->auth->getUser()->currentHmacToken()->id); } public function testLoginByIdWithMultipleTokens(): void { /** @var User $user */ $user = fake(UserModel::class); - $token1 = $user->generateHMACToken('foo'); - $user->generateHMACToken('bar'); + $token1 = $user->generateHmacToken('foo'); + $user->generateHmacToken('bar'); $this->setRequestHeader($this->generateRawHeaderToken($token1->secret, $token1->secret2, 'bar')); $this->auth->loginById($user->id); $this->assertTrue($this->auth->loggedIn()); - $this->assertInstanceOf(AccessToken::class, $this->auth->getUser()->currentHMACToken()); - $this->assertSame($token1->id, $this->auth->getUser()->currentHMACToken()->id); + $this->assertInstanceOf(AccessToken::class, $this->auth->getUser()->currentHmacToken()); + $this->assertSame($token1->id, $this->auth->getUser()->currentHmacToken()->id); } public function testCheckNoToken(): void @@ -131,7 +131,7 @@ public function testCheckOldToken(): void $user = fake(UserModel::class); /** @var UserIdentityModel $identities */ $identities = model(UserIdentityModel::class); - $token = $user->generateHMACToken('foo'); + $token = $user->generateHmacToken('foo'); // CI 4.2 uses the Chicago timezone that has Daylight Saving Time, // so subtracts 1 hour to make sure this test passes. $token->last_used_at = Time::now()->subYears(1)->subHours(1)->subMinutes(1); @@ -150,7 +150,7 @@ public function testCheckSuccess(): void { /** @var User $user */ $user = fake(UserModel::class); - $token = $user->generateHMACToken('foo'); + $token = $user->generateHmacToken('foo'); $this->seeInDatabase($this->tables['identities'], [ 'user_id' => $user->id, @@ -169,7 +169,7 @@ public function testCheckSuccess(): void $this->assertInstanceOf(User::class, $result->extraInfo()); $this->assertSame($user->id, $result->extraInfo()->id); - $updatedToken = $result->extraInfo()->currentHMACToken(); + $updatedToken = $result->extraInfo()->currentHmacToken(); $this->assertNotEmpty($updatedToken->last_used_at); // Checking token in the same second does not throw "DataException : There is no data to update." @@ -180,7 +180,7 @@ public function testCheckBadToken(): void { /** @var User $user */ $user = fake(UserModel::class); - $token = $user->generateHMACToken('foo'); + $token = $user->generateHmacToken('foo'); $this->seeInDatabase($this->tables['identities'], [ 'user_id' => $user->id, @@ -222,7 +222,7 @@ public function testAttemptSuccess(): void { /** @var User $user */ $user = fake(UserModel::class); - $token = $user->generateHMACToken('foo'); + $token = $user->generateHmacToken('foo'); $rawToken = $this->generateRawHeaderToken($token->secret, $token->secret2, 'bar'); $this->setRequestHeader($rawToken); @@ -237,8 +237,8 @@ public function testAttemptSuccess(): void $foundUser = $result->extraInfo(); $this->assertInstanceOf(User::class, $foundUser); $this->assertSame($user->id, $foundUser->id); - $this->assertInstanceOf(AccessToken::class, $foundUser->currentHMACToken()); - $this->assertSame($token->token, $foundUser->currentHMACToken()->token); + $this->assertInstanceOf(AccessToken::class, $foundUser->currentHmacToken()); + $this->assertSame($token->token, $foundUser->currentHmacToken()->token); // A login attempt should have been recorded $this->seeInDatabase($this->tables['token_logins'], [ diff --git a/tests/Authentication/Filters/HMACFilterTest.php b/tests/Authentication/Filters/HMACFilterTest.php index 5543e8592..e46d340c4 100644 --- a/tests/Authentication/Filters/HMACFilterTest.php +++ b/tests/Authentication/Filters/HMACFilterTest.php @@ -36,7 +36,7 @@ public function testFilterSuccess(): void { /** @var User $user */ $user = fake(UserModel::class); - $token = $user->generateHMACToken('foo'); + $token = $user->generateHmacToken('foo'); $rawToken = $this->generateRawHeaderToken($token->secret, $token->secret2, ''); $result = $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $rawToken]) @@ -49,15 +49,15 @@ public function testFilterSuccess(): void $this->assertSame($user->id, auth('hmac')->user()->id); // User should have the current token set. - $this->assertInstanceOf(AccessToken::class, auth('hmac')->user()->currentHMACToken()); - $this->assertSame($token->id, auth('hmac')->user()->currentHMACToken()->id); + $this->assertInstanceOf(AccessToken::class, auth('hmac')->user()->currentHmacToken()); + $this->assertSame($token->id, auth('hmac')->user()->currentHmacToken()->id); } public function testFilterInvalidSignature(): void { /** @var User $user */ $user = fake(UserModel::class); - $token = $user->generateHMACToken('foo'); + $token = $user->generateHmacToken('foo'); $result = $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $this->generateRawHeaderToken($token->secret, $token->secret2, 'bar')]) ->get('protected-route'); @@ -69,7 +69,7 @@ public function testRecordActiveDate(): void { /** @var User $user */ $user = fake(UserModel::class); - $token = $user->generateHMACToken('foo'); + $token = $user->generateHmacToken('foo'); $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $this->generateRawHeaderToken($token->secret, $token->secret2, '')]) ->get('protected-route'); @@ -82,10 +82,10 @@ public function testFiltersProtectsWithScopes(): void { /** @var User $user1 */ $user1 = fake(UserModel::class); - $token1 = $user1->generateHMACToken('foo', ['users-read']); + $token1 = $user1->generateHmacToken('foo', ['users-read']); /** @var User $user2 */ $user2 = fake(UserModel::class); - $token2 = $user2->generateHMACToken('foo', ['users-write']); + $token2 = $user2->generateHmacToken('foo', ['users-write']); // User 1 should be able to access the route $result1 = $this->withHeaders(['Authorization' => 'HMAC-SHA256 ' . $this->generateRawHeaderToken($token1->secret, $token1->secret2, '')]) @@ -106,7 +106,7 @@ public function testBlocksInactiveUsers(): void { /** @var User $user */ $user = fake(UserModel::class, ['active' => false]); - $token = $user->generateHMACToken('foo'); + $token = $user->generateHmacToken('foo'); // Activation only required with email activation setting('Auth.actions', ['register' => null]); From 91adb875db4ebd73b95c27a09eb29c5237105cc1 Mon Sep 17 00:00:00 2001 From: tswagger Date: Wed, 23 Aug 2023 09:16:52 -0500 Subject: [PATCH 06/41] Added missing helper --- src/Filters/HmacAuth.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Filters/HmacAuth.php b/src/Filters/HmacAuth.php index 4797bb09b..b115b84ae 100644 --- a/src/Filters/HmacAuth.php +++ b/src/Filters/HmacAuth.php @@ -24,6 +24,8 @@ public function before(RequestInterface $request, $arguments = null) $authenticator = auth('hmac')->getAuthenticator(); + helper('setting'); + $requestParams = [ 'token' => $request->getHeaderLine(setting('Auth.authenticatorHeader')['hmac'] ?? 'Authorization'), 'body' => file_get_contents('php://input'), From 50d70e23c9404f48a15bf52b8866523505d24497 Mon Sep 17 00:00:00 2001 From: tswagger Date: Wed, 23 Aug 2023 11:33:20 -0500 Subject: [PATCH 07/41] Minor syntax fix --- src/Filters/HmacAuth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Filters/HmacAuth.php b/src/Filters/HmacAuth.php index b115b84ae..c952e601b 100644 --- a/src/Filters/HmacAuth.php +++ b/src/Filters/HmacAuth.php @@ -28,7 +28,7 @@ public function before(RequestInterface $request, $arguments = null) $requestParams = [ 'token' => $request->getHeaderLine(setting('Auth.authenticatorHeader')['hmac'] ?? 'Authorization'), - 'body' => file_get_contents('php://input'), + 'body' => $request->getBody() ?? '', ]; $result = $authenticator->attempt($requestParams); From 5be8b2cf0d2143b69b0422b1ef4233c6d889629e Mon Sep 17 00:00:00 2001 From: tswagger Date: Wed, 23 Aug 2023 18:41:47 -0500 Subject: [PATCH 08/41] Codeing standards cleanup --- src/Authentication/Authenticators/HMAC_SHA256.php | 1 - src/Filters/HmacAuth.php | 3 --- src/Models/UserIdentityModel.php | 1 - 3 files changed, 5 deletions(-) diff --git a/src/Authentication/Authenticators/HMAC_SHA256.php b/src/Authentication/Authenticators/HMAC_SHA256.php index 1ddc2a413..7aa169435 100644 --- a/src/Authentication/Authenticators/HMAC_SHA256.php +++ b/src/Authentication/Authenticators/HMAC_SHA256.php @@ -267,7 +267,6 @@ public function getHMACAuthTokens(?string $fullToken = null): ?array } return null; - } /** diff --git a/src/Filters/HmacAuth.php b/src/Filters/HmacAuth.php index c952e601b..087b4f67f 100644 --- a/src/Filters/HmacAuth.php +++ b/src/Filters/HmacAuth.php @@ -21,7 +21,6 @@ class HmacAuth implements FilterInterface */ public function before(RequestInterface $request, $arguments = null) { - $authenticator = auth('hmac')->getAuthenticator(); helper('setting'); @@ -54,7 +53,6 @@ public function before(RequestInterface $request, $arguments = null) } return $request; - } /** @@ -62,6 +60,5 @@ public function before(RequestInterface $request, $arguments = null) */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void { - } } diff --git a/src/Models/UserIdentityModel.php b/src/Models/UserIdentityModel.php index 36d7a0671..20457a70b 100644 --- a/src/Models/UserIdentityModel.php +++ b/src/Models/UserIdentityModel.php @@ -255,7 +255,6 @@ public function generateHmacToken(User $user, string $name, array $scopes = ['*' $this->checkQueryReturn($return); - /** @var AccessToken $token */ return $this ->asObject(AccessToken::class) ->find($this->getInsertID()); From 89f38a8c66ffc097610f74deb6bcb9b6d8de1f73 Mon Sep 17 00:00:00 2001 From: tswagger Date: Thu, 24 Aug 2023 15:06:40 -0500 Subject: [PATCH 09/41] Clarified Return statements --- src/Authentication/Authenticators/HMAC_SHA256.php | 8 ++++---- src/Models/UserIdentityModel.php | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Authentication/Authenticators/HMAC_SHA256.php b/src/Authentication/Authenticators/HMAC_SHA256.php index 7aa169435..b09819a00 100644 --- a/src/Authentication/Authenticators/HMAC_SHA256.php +++ b/src/Authentication/Authenticators/HMAC_SHA256.php @@ -195,7 +195,7 @@ public function login(User $user): void /** * Logs a user in based on their ID. * - * @param int|string $userId + * @param int|string $userId User ID * * @throws AuthenticationException */ @@ -233,7 +233,7 @@ public function getUser(): ?User /** * Returns the Full Authorization token from the Authorization header * - * @return ?string + * @return ?string Trimmed Authorization Token from Header */ public function getFullAuthToken(): ?string { @@ -272,7 +272,7 @@ public function getHMACAuthTokens(?string $fullToken = null): ?array /** * Retrieve the key from the Auth token * - * @return ?string + * @return ?string HMAC token key */ public function getAuthKeyFromToken(): ?string { @@ -284,7 +284,7 @@ public function getAuthKeyFromToken(): ?string /** * Retrieve the HMAC Hash from the Auth token * - * @return ?string + * @return ?string HMAC signature */ public function getHMACHashFromToken(): ?string { diff --git a/src/Models/UserIdentityModel.php b/src/Models/UserIdentityModel.php index 20457a70b..0ae5ecb36 100644 --- a/src/Models/UserIdentityModel.php +++ b/src/Models/UserIdentityModel.php @@ -218,7 +218,7 @@ public function getAllAccessTokens(User $user): array /** * Find and Retrieve the HMAC AccessToken based on Token alone * - * @return ?AccessToken + * @return ?AccessToken Full HMAC Access Token object */ public function getHmacTokenByKey(string $key): ?AccessToken { @@ -267,7 +267,7 @@ public function generateHmacToken(User $user, string $name, array $scopes = ['*' * @param User $user User Object * @param string $key HMAC Key String * - * @return ?AccessToken + * @return ?AccessToken Full HMAC Access Token */ public function getHmacToken(User $user, string $key): ?AccessToken { @@ -286,7 +286,7 @@ public function getHmacToken(User $user, string $key): ?AccessToken * @param int|string $id * @param User $user User Object * - * @return ?AccessToken + * @return ?AccessToken Full HMAC Access Token */ public function getHmacTokenById($id, User $user): ?AccessToken { From 2adcdbddd1965d05daefeebe670d8173dc54d38d Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Mon, 28 Aug 2023 09:24:07 -0500 Subject: [PATCH 10/41] Update docs/guides/api_hmac_keys.md Co-authored-by: Pooya Parsa --- docs/guides/api_hmac_keys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/api_hmac_keys.md b/docs/guides/api_hmac_keys.md index 5f430c900..278217b6b 100644 --- a/docs/guides/api_hmac_keys.md +++ b/docs/guides/api_hmac_keys.md @@ -91,7 +91,7 @@ on **app/Config/Filters.php**. ```php public $filters = [ - 'tokens' => ['before' => ['api/*']], + 'hmac' => ['before' => ['api/*']], ]; ``` From bbc2496b519bc744d26aeb9f8ebdd0e16ad0f453 Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Mon, 28 Aug 2023 09:24:20 -0500 Subject: [PATCH 11/41] Update docs/guides/api_hmac_keys.md Co-authored-by: Pooya Parsa --- docs/guides/api_hmac_keys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/api_hmac_keys.md b/docs/guides/api_hmac_keys.md index 278217b6b..df517c2d2 100644 --- a/docs/guides/api_hmac_keys.md +++ b/docs/guides/api_hmac_keys.md @@ -98,7 +98,7 @@ public $filters = [ You can also specify the filter should run on one or more routes within the routes file itself: ```php -$routes->group('api', ['filter' => 'tokens'], function($routes) { +$routes->group('api', ['filter' => 'hmac'], function($routes) { // }); $routes->get('users', 'UserController::list', ['filter' => 'hmac:users-read']); From 1243efcdf6c3e6791504f7801722d1df0565c0db Mon Sep 17 00:00:00 2001 From: tswagger Date: Wed, 30 Aug 2023 14:14:39 -0500 Subject: [PATCH 12/41] Minor typo fix, clarification of key vs secretKey Signed-off-by: tswagger --- src/Authentication/Traits/HasHMACTokens.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Authentication/Traits/HasHMACTokens.php b/src/Authentication/Traits/HasHMACTokens.php index d6eb2df24..143955c39 100644 --- a/src/Authentication/Traits/HasHMACTokens.php +++ b/src/Authentication/Traits/HasHMACTokens.php @@ -75,18 +75,18 @@ public function hmacTokens(): array } /** - * Given a secret Key, it will locate it within the system. + * Given an HMAC Key, it will locate it within the system. */ - public function getHmacToken(?string $secretKey): ?AccessToken + public function getHmacToken(?string $key): ?AccessToken { - if (empty($secretKey)) { + if (empty($key)) { return null; } /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); - return $identityModel->getHmacToken($this, $secretKey); + return $identityModel->getHmacToken($this, $key); } /** From 498e68574d35dea939ddfc24d5f5c43dda3fb470 Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Wed, 30 Aug 2023 20:57:45 -0500 Subject: [PATCH 13/41] Update docs/authentication.md Co-authored-by: kenjis --- docs/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authentication.md b/docs/authentication.md index 6255adeb2..b5220c392 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -316,7 +316,7 @@ with their email/password. The application would create a new access token for t 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, +> **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 From ac61035dbe6138051ec477e650673a34bfaafdfe Mon Sep 17 00:00:00 2001 From: tswagger Date: Wed, 30 Aug 2023 21:16:42 -0500 Subject: [PATCH 14/41] Renamed 'HMAC' in the code to a consistent 'Hmac' Signed-off-by: tswagger --- docs/authentication.md | 6 +++--- .../{HMAC_SHA256.php => HmacSha256.php} | 16 ++++++++-------- .../{HasHMACTokens.php => HasHmacTokens.php} | 4 ++-- src/Config/Auth.php | 4 ++-- src/Entities/User.php | 4 ++-- src/Models/UserIdentityModel.php | 16 ++++++++-------- ...ticatorTest.php => HmacAuthenticatorTest.php} | 12 ++++++------ .../{HMACFilterTest.php => HmacFilterTest.php} | 2 +- 8 files changed, 32 insertions(+), 32 deletions(-) rename src/Authentication/Authenticators/{HMAC_SHA256.php => HmacSha256.php} (94%) rename src/Authentication/Traits/{HasHMACTokens.php => HasHmacTokens.php} (98%) rename tests/Authentication/Authenticators/{HMACAuthenticatorTest.php => HmacAuthenticatorTest.php} (96%) rename tests/Authentication/Filters/{HMACFilterTest.php => HmacFilterTest.php} (98%) diff --git a/docs/authentication.md b/docs/authentication.md index b5220c392..fbe542854 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -336,7 +336,7 @@ header("Authorization: HMAC-SHA256 {$key}:" . hash_hmac('sha256', $requestBody, ### 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 +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. @@ -418,14 +418,14 @@ permissions the token grants to the user. Scopes are provided when the token is cannot be modified afterword. ```php -$token = $user->gererateHMACToken('Work Laptop', ['posts.manage', 'forums.manage']); +$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', ['*']); +$token = $user->gererateHmacToken('Work Laptop', ['*']); ``` During authentication, the HMAC Keys the user used is stored on the user. Once authenticated, you diff --git a/src/Authentication/Authenticators/HMAC_SHA256.php b/src/Authentication/Authenticators/HmacSha256.php similarity index 94% rename from src/Authentication/Authenticators/HMAC_SHA256.php rename to src/Authentication/Authenticators/HmacSha256.php index b09819a00..c6cbfb1da 100644 --- a/src/Authentication/Authenticators/HMAC_SHA256.php +++ b/src/Authentication/Authenticators/HmacSha256.php @@ -15,7 +15,7 @@ use CodeIgniter\Shield\Models\UserModel; use CodeIgniter\Shield\Result; -class HMAC_SHA256 implements AuthenticatorInterface +class HmacSha256 implements AuthenticatorInterface { public const ID_TYPE_HMAC_TOKEN = 'hmac_sha256'; @@ -113,12 +113,12 @@ public function check(array $credentials): Result } // Extract UserToken and HMACSHA256 Signature from Authorization token - [$userToken, $signature] = $this->getHMACAuthTokens($credentials['token']); + [$userToken, $signature] = $this->getHmacAuthTokens($credentials['token']); /** @var UserIdentityModel $identityModel */ $identityModel = model(UserIdentityModel::class); - $token = $identityModel->getHMACTokenByKey($userToken); + $token = $identityModel->getHmacTokenByKey($userToken); if ($token === null) { return new Result([ @@ -208,7 +208,7 @@ public function loginById($userId): void } $user->setHmacToken( - $user->getHMACToken($this->getAuthKeyFromToken()) + $user->getHmacToken($this->getAuthKeyFromToken()) ); $this->login($user); @@ -256,7 +256,7 @@ public function getFullAuthToken(): ?string * * @return ?array [key, hmacHash] */ - public function getHMACAuthTokens(?string $fullToken = null): ?array + public function getHmacAuthTokens(?string $fullToken = null): ?array { if (! isset($fullToken)) { $fullToken = $this->getFullAuthToken(); @@ -276,7 +276,7 @@ public function getHMACAuthTokens(?string $fullToken = null): ?array */ public function getAuthKeyFromToken(): ?string { - [$key, $hmacHash] = $this->getHMACAuthTokens(); + [$key, $hmacHash] = $this->getHmacAuthTokens(); return $key; } @@ -286,9 +286,9 @@ public function getAuthKeyFromToken(): ?string * * @return ?string HMAC signature */ - public function getHMACHashFromToken(): ?string + public function getHmacHashFromToken(): ?string { - [$key, $hmacHash] = $this->getHMACAuthTokens(); + [$key, $hmacHash] = $this->getHmacAuthTokens(); return $hmacHash; } diff --git a/src/Authentication/Traits/HasHMACTokens.php b/src/Authentication/Traits/HasHmacTokens.php similarity index 98% rename from src/Authentication/Traits/HasHMACTokens.php rename to src/Authentication/Traits/HasHmacTokens.php index 143955c39..f41accfcc 100644 --- a/src/Authentication/Traits/HasHMACTokens.php +++ b/src/Authentication/Traits/HasHmacTokens.php @@ -9,14 +9,14 @@ use ReflectionException; /** - * Trait HasHMACTokens + * Trait HasHmacTokens * * Provides functionality needed to generate, revoke, * and retrieve Personal Access Tokens. * * Intended to be used with User entities. */ -trait HasHMACTokens +trait HasHmacTokens { /** * The current access token for the user. diff --git a/src/Config/Auth.php b/src/Config/Auth.php index 69cab44a8..0a3bfa374 100644 --- a/src/Config/Auth.php +++ b/src/Config/Auth.php @@ -8,7 +8,7 @@ use CodeIgniter\Shield\Authentication\Actions\ActionInterface; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; use CodeIgniter\Shield\Authentication\Authenticators\AccessTokens; -use CodeIgniter\Shield\Authentication\Authenticators\HMAC_SHA256; +use CodeIgniter\Shield\Authentication\Authenticators\HmacSha256; use CodeIgniter\Shield\Authentication\Authenticators\JWT; use CodeIgniter\Shield\Authentication\Authenticators\Session; use CodeIgniter\Shield\Authentication\Passwords\CompositionValidator; @@ -135,7 +135,7 @@ class Auth extends BaseConfig public array $authenticators = [ 'tokens' => AccessTokens::class, 'session' => Session::class, - 'hmac' => HMAC_SHA256::class, + 'hmac' => HmacSha256::class, // 'jwt' => JWT::class, ]; diff --git a/src/Entities/User.php b/src/Entities/User.php index 1b8a326dd..1aff561ab 100644 --- a/src/Entities/User.php +++ b/src/Entities/User.php @@ -8,7 +8,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\Session; use CodeIgniter\Shield\Authentication\Traits\HasAccessTokens; -use CodeIgniter\Shield\Authentication\Traits\HasHMACTokens; +use CodeIgniter\Shield\Authentication\Traits\HasHmacTokens; use CodeIgniter\Shield\Authorization\Traits\Authorizable; use CodeIgniter\Shield\Models\LoginModel; use CodeIgniter\Shield\Models\UserIdentityModel; @@ -29,7 +29,7 @@ class User extends Entity { use Authorizable; use HasAccessTokens; - use HasHMACTokens; + use HasHmacTokens; use Resettable; use Activatable; use Bannable; diff --git a/src/Models/UserIdentityModel.php b/src/Models/UserIdentityModel.php index 0ae5ecb36..71274cd2e 100644 --- a/src/Models/UserIdentityModel.php +++ b/src/Models/UserIdentityModel.php @@ -6,7 +6,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authenticators\AccessTokens; -use CodeIgniter\Shield\Authentication\Authenticators\HMAC_SHA256; +use CodeIgniter\Shield\Authentication\Authenticators\HmacSha256; use CodeIgniter\Shield\Authentication\Authenticators\Session; use CodeIgniter\Shield\Authentication\Passwords; use CodeIgniter\Shield\Entities\AccessToken; @@ -223,7 +223,7 @@ public function getAllAccessTokens(User $user): array public function getHmacTokenByKey(string $key): ?AccessToken { return $this - ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('type', HmacSha256::ID_TYPE_HMAC_TOKEN) ->where('secret', $key) ->asObject(AccessToken::class) ->first(); @@ -245,7 +245,7 @@ public function generateHmacToken(User $user, string $name, array $scopes = ['*' // helper('text'); $return = $this->insert([ - 'type' => HMAC_SHA256::ID_TYPE_HMAC_TOKEN, + 'type' => HmacSha256::ID_TYPE_HMAC_TOKEN, 'user_id' => $user->id, 'name' => $name, 'secret' => bin2hex(random_bytes(16)), // Key @@ -274,7 +274,7 @@ public function getHmacToken(User $user, string $key): ?AccessToken $this->checkUserId($user); return $this->where('user_id', $user->id) - ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('type', HmacSha256::ID_TYPE_HMAC_TOKEN) ->where('secret', $key) ->asObject(AccessToken::class) ->first(); @@ -293,7 +293,7 @@ public function getHmacTokenById($id, User $user): ?AccessToken $this->checkUserId($user); return $this->where('user_id', $user->id) - ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('type', HmacSha256::ID_TYPE_HMAC_TOKEN) ->where('id', $id) ->asObject(AccessToken::class) ->first(); @@ -312,7 +312,7 @@ public function getAllHmacTokens(User $user): array return $this ->where('user_id', $user->id) - ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('type', HmacSha256::ID_TYPE_HMAC_TOKEN) ->orderBy($this->primaryKey) ->asObject(AccessToken::class) ->findAll(); @@ -329,7 +329,7 @@ public function revokeHmacToken(User $user, string $key): void $this->checkUserId($user); $return = $this->where('user_id', $user->id) - ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('type', HmacSha256::ID_TYPE_HMAC_TOKEN) ->where('secret', $key) ->delete(); @@ -344,7 +344,7 @@ public function revokeAllHmacTokens(User $user): void $this->checkUserId($user); $return = $this->where('user_id', $user->id) - ->where('type', HMAC_SHA256::ID_TYPE_HMAC_TOKEN) + ->where('type', HmacSha256::ID_TYPE_HMAC_TOKEN) ->delete(); $this->checkQueryReturn($return); diff --git a/tests/Authentication/Authenticators/HMACAuthenticatorTest.php b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php similarity index 96% rename from tests/Authentication/Authenticators/HMACAuthenticatorTest.php rename to tests/Authentication/Authenticators/HmacAuthenticatorTest.php index c8f7fb5ba..fc0ede3d1 100644 --- a/tests/Authentication/Authenticators/HMACAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php @@ -6,7 +6,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authentication; -use CodeIgniter\Shield\Authentication\Authenticators\HMAC_SHA256; +use CodeIgniter\Shield\Authentication\Authenticators\HmacSha256; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Entities\AccessToken; use CodeIgniter\Shield\Entities\User; @@ -20,9 +20,9 @@ /** * @internal */ -final class HMACAuthenticatorTest extends DatabaseTestCase +final class HmacAuthenticatorTest extends DatabaseTestCase { - private HMAC_SHA256 $auth; + private HmacSha256 $auth; protected function setUp(): void { @@ -32,7 +32,7 @@ protected function setUp(): void $auth = new Authentication($config); $auth->setProvider(model(UserModel::class)); - /** @var HMAC_SHA256 $authenticator */ + /** @var HmacSha256 $authenticator */ $authenticator = $auth->factory('hmac'); $this->auth = $authenticator; @@ -212,7 +212,7 @@ public function testAttemptCannotFindUser(): void // A login attempt should have always been recorded $this->seeInDatabase($this->tables['token_logins'], [ - 'id_type' => HMAC_SHA256::ID_TYPE_HMAC_TOKEN, + 'id_type' => HmacSha256::ID_TYPE_HMAC_TOKEN, 'identifier' => 'abc123:lsakdjfljsdflkajsfd', 'success' => 0, ]); @@ -242,7 +242,7 @@ public function testAttemptSuccess(): void // A login attempt should have been recorded $this->seeInDatabase($this->tables['token_logins'], [ - 'id_type' => HMAC_SHA256::ID_TYPE_HMAC_TOKEN, + 'id_type' => HmacSha256::ID_TYPE_HMAC_TOKEN, 'identifier' => $rawToken, 'success' => 1, ]); diff --git a/tests/Authentication/Filters/HMACFilterTest.php b/tests/Authentication/Filters/HmacFilterTest.php similarity index 98% rename from tests/Authentication/Filters/HMACFilterTest.php rename to tests/Authentication/Filters/HmacFilterTest.php index e46d340c4..4a7acb9bc 100644 --- a/tests/Authentication/Filters/HMACFilterTest.php +++ b/tests/Authentication/Filters/HmacFilterTest.php @@ -13,7 +13,7 @@ /** * @internal */ -final class HMACFilterTest extends AbstractFilterTestCase +final class HmacFilterTest extends AbstractFilterTestCase { use DatabaseTestTrait; From c8058f734b7d88268eb1e1a6e4bdc4652efb0ab4 Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Thu, 31 Aug 2023 08:32:54 -0500 Subject: [PATCH 15/41] Update docs/authentication.md Co-authored-by: John Paul E. Balandan, CPA --- docs/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authentication.md b/docs/authentication.md index fbe542854..c4ad34abf 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -306,7 +306,7 @@ if ($user->tokenCant('forums.manage')) { ## HMAC SHA256 Token Authenticator -The HMAC-SHA256 authenticator supports the use of revoke-able API keys without using OAuth. This provides +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. From 755a99c50a55601d3ca68b2ecec3e4029f1780de Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Thu, 31 Aug 2023 08:33:06 -0500 Subject: [PATCH 16/41] Update docs/authentication.md Co-authored-by: John Paul E. Balandan, CPA --- docs/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authentication.md b/docs/authentication.md index c4ad34abf..c8032f46e 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -323,7 +323,7 @@ the term "Token" will be used to represent a set of API Keys (key and secretKey) In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request: ``` -Authorization: HMAC-SHA256 : +Authorization: HMAC-SHA256 : ``` The code to do this will look something like this: From 19d7cea5ac067bb5b6e03ee9d4b99d3aeec74970 Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Thu, 31 Aug 2023 08:40:06 -0500 Subject: [PATCH 17/41] Update docs/authentication.md Co-authored-by: John Paul E. Balandan, CPA --- docs/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authentication.md b/docs/authentication.md index c8032f46e..921dcaf9c 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -351,7 +351,7 @@ differentiate between multiple tokens. $token = $user->generateHmacToken('Work Laptop'); ``` -This creates the keys/tokens using a cryptographically secure random string. The keys opporate as shared keys. +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 'secredKey'. Both are required to when using this authentication method. From 72b9a70176cdd504bea66739f7fcdf2ad648ccfd Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Thu, 31 Aug 2023 08:40:16 -0500 Subject: [PATCH 18/41] Update docs/authentication.md Co-authored-by: John Paul E. Balandan, CPA --- docs/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authentication.md b/docs/authentication.md index 921dcaf9c..d47ba4afc 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -354,7 +354,7 @@ $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 'secredKey'. Both are required to when using this authentication method. +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 From daee37140b7b65e301bcd9e156600ee6c17462fa Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Thu, 31 Aug 2023 08:40:27 -0500 Subject: [PATCH 19/41] Update docs/guides/api_hmac_keys.md Co-authored-by: John Paul E. Balandan, CPA --- docs/guides/api_hmac_keys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/api_hmac_keys.md b/docs/guides/api_hmac_keys.md index df517c2d2..9b801392c 100644 --- a/docs/guides/api_hmac_keys.md +++ b/docs/guides/api_hmac_keys.md @@ -20,7 +20,7 @@ The `generateHmacToken()` method requires a name for the token. These are free s the user/device the token was generated from/for, like 'Johns MacBook Air'. ```php -$routes->get('/hmac/token', static function() { +$routes->get('/hmac/token', static function () { $token = auth()->user()->generateHmacToken(service('request')->getVar('token_name')); return json_encode(['key' => $token->secret, 'secretKey' => $token->secret2]); From 8990cef1932c54405572d79957021fff7c84bcb3 Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Thu, 31 Aug 2023 08:40:38 -0500 Subject: [PATCH 20/41] Update docs/guides/api_hmac_keys.md Co-authored-by: John Paul E. Balandan, CPA --- docs/guides/api_hmac_keys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/api_hmac_keys.md b/docs/guides/api_hmac_keys.md index 9b801392c..d749eef2c 100644 --- a/docs/guides/api_hmac_keys.md +++ b/docs/guides/api_hmac_keys.md @@ -31,7 +31,7 @@ You can access all the user's HMAC keys with the `hmacTokens()` method on that u ```php $tokens = $user->hmacTokens(); -foreach($tokens as $token) { +foreach ($tokens as $token) { // } ``` From 5f56a55e8c83f4ddf6f1868c5e968ba08b7e445f Mon Sep 17 00:00:00 2001 From: tswagger Date: Thu, 31 Aug 2023 09:24:38 -0500 Subject: [PATCH 21/41] Added ToC entries. Signed-off-by: tswagger --- docs/authentication.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index d47ba4afc..3e71aa96a 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -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 @@ -316,8 +323,8 @@ with their email/password. The application would create a new access token for t 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). +> **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 In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request: @@ -341,7 +348,7 @@ provides all the custom methods needed to implement HMAC keys in your applicatio 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 Access Keys +### 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 From 4b452b9163c52cb84ea3a26ed13e2971bade6ce7 Mon Sep 17 00:00:00 2001 From: tswagger Date: Thu, 31 Aug 2023 12:28:46 -0500 Subject: [PATCH 22/41] Added CURLRequest example Signed-off-by: tswagger --- docs/authentication.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/authentication.md b/docs/authentication.md index 3e71aa96a..5c022c1c3 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -45,6 +45,7 @@ public $authenticators = [ // alias => classname 'session' => Session::class, 'tokens' => AccessTokens::class, + 'hmac' => HmacSha256::class, ]; ``` @@ -337,8 +338,27 @@ The code to do this will look something like this: ```php setHeader('Authorization', "HMAC-SHA256 {$key}:{$hashValue}") + ->setBody($requestBody) + ->request('POST', 'https://example.com/api'); +``` ### HMAC Keys/API Authentication From 547e4559feb41ab784f91d49410538977409cb42 Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Tue, 5 Sep 2023 08:56:55 -0500 Subject: [PATCH 23/41] Update docs/guides/api_hmac_keys.md Co-authored-by: kenjis --- docs/guides/api_hmac_keys.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guides/api_hmac_keys.md b/docs/guides/api_hmac_keys.md index d749eef2c..8a6053ede 100644 --- a/docs/guides/api_hmac_keys.md +++ b/docs/guides/api_hmac_keys.md @@ -1,6 +1,7 @@ # Protecting an API with HMAC Keys -> **Note** for the purpose of this documentation and to maintain a level of consistency with the Authorization Tokens, +> **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 From 65853f7721aad1c478726d3673a1fc1a15f25ba0 Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Wed, 6 Sep 2023 12:10:29 -0500 Subject: [PATCH 24/41] Update docs/guides/api_hmac_keys.md Co-authored-by: kenjis --- docs/guides/api_hmac_keys.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/guides/api_hmac_keys.md b/docs/guides/api_hmac_keys.md index 8a6053ede..327cf4ff4 100644 --- a/docs/guides/api_hmac_keys.md +++ b/docs/guides/api_hmac_keys.md @@ -8,8 +8,9 @@ HMAC Keys can be used to authenticate users for your own site, or when allowing 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. +> **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 From 9f9506dc21c056ac344df2de10005c3574ae5f04 Mon Sep 17 00:00:00 2001 From: tswagger Date: Wed, 6 Sep 2023 12:37:35 -0500 Subject: [PATCH 25/41] Improved HMAC Docs Signed-off-by: tswagger --- docs/authentication.md | 13 +++++++------ docs/guides/api_hmac_keys.md | 6 +++--- mkdocs.yml | 1 + src/Models/UserIdentityModel.php | 2 -- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index 5c022c1c3..7a5f026e1 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -272,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. @@ -324,10 +324,12 @@ with their email/password. The application would create a new access token for t 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, +> **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 + In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request: ``` @@ -337,10 +339,9 @@ Authorization: HMAC-SHA256 : The code to do this will look something like this: ```php -hmacTokens(); 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 `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. diff --git a/docs/guides/api_hmac_keys.md b/docs/guides/api_hmac_keys.md index 327cf4ff4..3f9732814 100644 --- a/docs/guides/api_hmac_keys.md +++ b/docs/guides/api_hmac_keys.md @@ -1,6 +1,6 @@ # Protecting an API with HMAC Keys -> **Note** +> **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). @@ -8,7 +8,7 @@ HMAC Keys can be used to authenticate users for your own site, or when allowing API. When making requests using HMAC keys, the token should be included in the `Authorization` header as an `HMAC-SHA256` token. -> **Note** +> **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. @@ -39,6 +39,7 @@ foreach ($tokens as $token) { ``` ### Usage + In order to use HMAC Keys/Token the `Authorization` header will be set to the following in the request: ``` @@ -48,7 +49,6 @@ Authorization: HMAC-SHA256 : The code to do this will look something like this: ```php -checkUserId($user); - // helper('text'); - $return = $this->insert([ 'type' => HmacSha256::ID_TYPE_HMAC_TOKEN, 'user_id' => $user->id, From 7791f8736a82155b870c60314c285c11c3c3f992 Mon Sep 17 00:00:00 2001 From: tswagger Date: Wed, 6 Sep 2023 14:44:25 -0500 Subject: [PATCH 26/41] Updated phpdpd to suppress errors from HMAC Addition Signed-off-by: tswagger --- .github/workflows/phpcpd.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpcpd.yml b/.github/workflows/phpcpd.yml index f7063e715..2ffa76d70 100644 --- a/.github/workflows/phpcpd.yml +++ b/.github/workflows/phpcpd.yml @@ -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 diff --git a/composer.json b/composer.json index c3759ba90..57d6dba92 100644 --- a/composer.json +++ b/composer.json @@ -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", From b5d7fa16b7623317bf8cec8b4baf3afb6c81c451 Mon Sep 17 00:00:00 2001 From: tswagger Date: Tue, 12 Sep 2023 12:09:32 -0500 Subject: [PATCH 27/41] Updated login recording to match JWT Authorization Added AuthToken config as a separate config for Token/HMAC auth from JWT Updated test to reflect logging adjustment change. Signed-off-by: tswagger --- .../Authenticators/HmacSha256.php | 53 +++++++++++++------ src/Config/AuthToken.php | 24 +++++++++ .../Authenticators/HmacAuthenticatorTest.php | 2 + 3 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 src/Config/AuthToken.php diff --git a/src/Authentication/Authenticators/HmacSha256.php b/src/Authentication/Authenticators/HmacSha256.php index c6cbfb1da..27e85c886 100644 --- a/src/Authentication/Authenticators/HmacSha256.php +++ b/src/Authentication/Authenticators/HmacSha256.php @@ -8,6 +8,8 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; +use CodeIgniter\Shield\Config\Auth; +use CodeIgniter\Shield\Config\AuthToken; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\InvalidArgumentException; use CodeIgniter\Shield\Models\TokenLoginModel; @@ -42,6 +44,8 @@ public function __construct(UserModel $provider) */ public function attempt(array $credentials): Result { + $config = config(AuthToken::class); + /** @var IncomingRequest $request */ $request = service('request'); @@ -51,14 +55,16 @@ public function attempt(array $credentials): Result $result = $this->check($credentials); if (! $result->isOK()) { - // Always record a login attempt, whether success or not. - $this->loginModel->recordLoginAttempt( - self::ID_TYPE_HMAC_TOKEN, - $credentials['token'] ?? '', - false, - $ipAddress, - $userAgent - ); + if ($config->recordLoginAttempt >= Auth::RECORD_LOGIN_ATTEMPT_FAILURE) { + // Record all failed login attempts. + $this->loginModel->recordLoginAttempt( + self::ID_TYPE_HMAC_TOKEN, + $credentials['token'] ?? '', + false, + $ipAddress, + $userAgent + ); + } return $result; } @@ -66,6 +72,18 @@ public function attempt(array $credentials): Result $user = $result->extraInfo(); 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'] ?? '', + false, + $ipAddress, + $userAgent, + $user->id + ); + } + $this->user = null; return new Result([ @@ -80,14 +98,17 @@ public function attempt(array $credentials): Result $this->login($user); - $this->loginModel->recordLoginAttempt( - self::ID_TYPE_HMAC_TOKEN, - $credentials['token'] ?? '', - true, - $ipAddress, - $userAgent, - $this->user->id - ); + if ($config->recordLoginAttempt === Auth::RECORD_LOGIN_ATTEMPT_ALL) { + // Record a successful login attempt. + $this->loginModel->recordLoginAttempt( + self::ID_TYPE_HMAC_TOKEN, + $credentials['token'] ?? '', + true, + $ipAddress, + $userAgent, + $this->user->id + ); + } return $result; } diff --git a/src/Config/AuthToken.php b/src/Config/AuthToken.php new file mode 100644 index 000000000..3f879e196 --- /dev/null +++ b/src/Config/AuthToken.php @@ -0,0 +1,24 @@ +setProvider(model(UserModel::class)); + Config('AuthToken')->recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_ALL; + /** @var HmacSha256 $authenticator */ $authenticator = $auth->factory('hmac'); $this->auth = $authenticator; From d8e4262d912aec0c6423a476da3fa26ea990ff1c Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Wed, 13 Sep 2023 10:08:42 -0500 Subject: [PATCH 28/41] Update src/Authentication/Authenticators/HmacSha256.php Co-authored-by: kenjis --- src/Authentication/Authenticators/HmacSha256.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authentication/Authenticators/HmacSha256.php b/src/Authentication/Authenticators/HmacSha256.php index 27e85c886..bbd457497 100644 --- a/src/Authentication/Authenticators/HmacSha256.php +++ b/src/Authentication/Authenticators/HmacSha256.php @@ -44,7 +44,7 @@ public function __construct(UserModel $provider) */ public function attempt(array $credentials): Result { - $config = config(AuthToken::class); + $config = config('AuthToken'); /** @var IncomingRequest $request */ $request = service('request'); From 6ac224ebc5dff62ffbf973bb8ec953f84e47113f Mon Sep 17 00:00:00 2001 From: tswagger Date: Wed, 13 Sep 2023 10:43:39 -0500 Subject: [PATCH 29/41] Added HMAC References to installation documentation Signed-off-by: tswagger --- docs/install.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 74f5b3af4..cbd34bb11 100644 --- a/docs/install.md +++ b/docs/install.md @@ -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. ```php // new file - app/Config/Auth.php @@ -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, @@ -218,6 +219,7 @@ 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. From 08e0b8ef19f1ed15ea83e9a86ea51df78ce20694 Mon Sep 17 00:00:00 2001 From: tswagger Date: Wed, 13 Sep 2023 11:00:48 -0500 Subject: [PATCH 30/41] Cleaned up table formatting in markdown Signed-off-by: tswagger --- docs/install.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/install.md b/docs/install.md index cbd34bb11..98a9505a7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -214,16 +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). -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. +| 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). From bbcf8b5a399ea65accca6a6624db8809fe8ecc33 Mon Sep 17 00:00:00 2001 From: tswagger Date: Thu, 14 Sep 2023 08:27:47 -0500 Subject: [PATCH 31/41] Updated byte size for HMAC Secret Key Signed-off-by: tswagger --- src/Authentication/Authenticators/HmacSha256.php | 1 - src/Config/AuthToken.php | 13 ++++++++++++- src/Models/UserIdentityModel.php | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Authentication/Authenticators/HmacSha256.php b/src/Authentication/Authenticators/HmacSha256.php index bbd457497..4bd409a59 100644 --- a/src/Authentication/Authenticators/HmacSha256.php +++ b/src/Authentication/Authenticators/HmacSha256.php @@ -9,7 +9,6 @@ use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\AuthenticatorInterface; use CodeIgniter\Shield\Config\Auth; -use CodeIgniter\Shield\Config\AuthToken; use CodeIgniter\Shield\Entities\User; use CodeIgniter\Shield\Exceptions\InvalidArgumentException; use CodeIgniter\Shield\Models\TokenLoginModel; diff --git a/src/Config/AuthToken.php b/src/Config/AuthToken.php index 3f879e196..2d8254471 100644 --- a/src/Config/AuthToken.php +++ b/src/Config/AuthToken.php @@ -4,10 +4,12 @@ namespace CodeIgniter\Shield\Config; +use CodeIgniter\Config\BaseConfig; + /** * Authenticator Configuration for Token Auth and HMAC Auth */ -class AuthToken +class AuthToken extends BaseConfig { /** * -------------------------------------------------------------------- @@ -21,4 +23,13 @@ class AuthToken * - Auth::RECORD_LOGIN_ATTEMPT_ALL */ public int $recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_FAILURE; + + /** + * -------------------------------------------------------------------- + * HMAC secret key byte size + * -------------------------------------------------------------------- + * Specify in integer the desired byte size of the + * HMAC SHA256 byte size + */ + public int $hmacSecretKeyByteSize = 32; } diff --git a/src/Models/UserIdentityModel.php b/src/Models/UserIdentityModel.php index e81cb8533..45c5ee4f3 100644 --- a/src/Models/UserIdentityModel.php +++ b/src/Models/UserIdentityModel.php @@ -247,7 +247,7 @@ public function generateHmacToken(User $user, string $name, array $scopes = ['*' 'user_id' => $user->id, 'name' => $name, 'secret' => bin2hex(random_bytes(16)), // Key - 'secret2' => bin2hex(random_bytes(16)), // Secret Key + 'secret2' => bin2hex(random_bytes(config('AuthToken')->hmacSecretKeyByteSize)), // Secret Key 'extra' => serialize($scopes), ]); From f47891a5cc944d5b5795f2eb65bfc9da1248f76c Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Fri, 15 Sep 2023 14:27:33 -0500 Subject: [PATCH 32/41] Update tests/Authentication/Authenticators/HmacAuthenticatorTest.php Co-authored-by: Pooya Parsa --- tests/Authentication/Authenticators/HmacAuthenticatorTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php index cfb4a5139..71f10b8e3 100644 --- a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php @@ -43,6 +43,7 @@ protected function setUp(): void public function testLogin(): void { + /** @var User $user */ $user = fake(UserModel::class); $this->auth->login($user); From 10147cf87b275b36ad618b643743e8a31c0b4a82 Mon Sep 17 00:00:00 2001 From: Tim Swagger Date: Fri, 15 Sep 2023 14:27:42 -0500 Subject: [PATCH 33/41] Update tests/Authentication/Authenticators/HmacAuthenticatorTest.php Co-authored-by: Pooya Parsa --- tests/Authentication/Authenticators/HmacAuthenticatorTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php index 71f10b8e3..bbd2b1a83 100644 --- a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php @@ -67,6 +67,7 @@ public function testLogout(): void public function testLoginByIdNoToken(): void { + /** @var User $user */ $user = fake(UserModel::class); $this->assertFalse($this->auth->loggedIn()); From 2eb3588e8355d53a7817bb2f843f3a61a0cd7921 Mon Sep 17 00:00:00 2001 From: tswagger Date: Fri, 15 Sep 2023 16:48:51 -0500 Subject: [PATCH 34/41] Initial fix to PHPStan errors Signed-off-by: tswagger --- src/Authentication/Authenticators/HmacSha256.php | 10 +++++----- src/Authentication/Traits/HasHmacTokens.php | 2 +- src/Filters/HmacAuth.php | 6 +++--- .../Authenticators/HmacAuthenticatorTest.php | 7 ++----- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Authentication/Authenticators/HmacSha256.php b/src/Authentication/Authenticators/HmacSha256.php index 4bd409a59..1722bb3c0 100644 --- a/src/Authentication/Authenticators/HmacSha256.php +++ b/src/Authentication/Authenticators/HmacSha256.php @@ -121,7 +121,7 @@ public function attempt(array $credentials): Result */ public function check(array $credentials): Result { - if (! array_key_exists('token', $credentials) || empty($credentials['token'])) { + if (! array_key_exists('token', $credentials) || $credentials['token'] === '') { return new Result([ 'success' => false, 'reason' => lang('Auth.noToken', [config('Auth')->authenticatorHeader['hmac']]), @@ -160,7 +160,7 @@ public function check(array $credentials): Result // Hasn't been used in a long time if ( - $token->last_used_at + isset($token->last_used_at) && $token->last_used_at->isBefore(Time::now()->subSeconds(config('Auth')->unusedTokenLifetime)) ) { return new Result([ @@ -192,7 +192,7 @@ public function check(array $credentials): Result */ public function loggedIn(): bool { - if (! empty($this->user)) { + if (isset($this->user)) { return true; } @@ -223,7 +223,7 @@ public function loginById($userId): void { $user = $this->provider->findById($userId); - if (empty($user)) { + if ($user === null) { throw AuthenticationException::forInvalidUser(); } @@ -262,7 +262,7 @@ public function getFullAuthToken(): ?string $header = $request->getHeaderLine(config('Auth')->authenticatorHeader['tokens']); - if (empty($header)) { + if ($header === '') { return null; } diff --git a/src/Authentication/Traits/HasHmacTokens.php b/src/Authentication/Traits/HasHmacTokens.php index f41accfcc..435a19bd2 100644 --- a/src/Authentication/Traits/HasHmacTokens.php +++ b/src/Authentication/Traits/HasHmacTokens.php @@ -79,7 +79,7 @@ public function hmacTokens(): array */ public function getHmacToken(?string $key): ?AccessToken { - if (empty($key)) { + if (! isset($key) || $key === '') { return null; } diff --git a/src/Filters/HmacAuth.php b/src/Filters/HmacAuth.php index 087b4f67f..c67c6ef97 100644 --- a/src/Filters/HmacAuth.php +++ b/src/Filters/HmacAuth.php @@ -32,10 +32,10 @@ public function before(RequestInterface $request, $arguments = null) $result = $authenticator->attempt($requestParams); - if (! $result->isOK() || (! empty($arguments) && $result->extraInfo()->hmacTokenCant($arguments[0]))) { + if (! $result->isOK() || ($arguments !== null && count($arguments) > 0 && $result->extraInfo()->hmacTokenCant($arguments[0]))) { return service('response') ->setStatusCode(Response::HTTP_UNAUTHORIZED) - ->setJson(['message' => lang('Auth.badToken')]); + ->setJSON(['message' => lang('Auth.badToken')]); } if (setting('Auth.recordActiveDate')) { @@ -49,7 +49,7 @@ public function before(RequestInterface $request, $arguments = null) return service('response') ->setStatusCode(Response::HTTP_FORBIDDEN) - ->setJson(['message' => lang('Auth.activationBlocked')]); + ->setJSON(['message' => lang('Auth.activationBlocked')]); } return $request; diff --git a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php index bbd2b1a83..940cfadd4 100644 --- a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php @@ -12,7 +12,6 @@ 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; @@ -32,7 +31,7 @@ protected function setUp(): void $auth = new Authentication($config); $auth->setProvider(model(UserModel::class)); - Config('AuthToken')->recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_ALL; + config('AuthToken')->recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_ALL; /** @var HmacSha256 $authenticator */ $authenticator = $auth->factory('hmac'); @@ -199,7 +198,7 @@ public function testCheckBadToken(): void 'body' => 'bar', ]); - $this->assertfalse($result->isOK()); + $this->assertFalse($result->isOK()); $this->assertSame(lang('Auth.badToken'), $result->reason()); } @@ -210,7 +209,6 @@ public function testAttemptCannotFindUser(): void 'body' => 'bar', ]); - $this->assertInstanceOf(Result::class, $result); $this->assertFalse($result->isOK()); $this->assertSame(lang('Auth.badToken'), $result->reason()); @@ -235,7 +233,6 @@ public function testAttemptSuccess(): void 'body' => 'bar', ]); - $this->assertInstanceOf(Result::class, $result); $this->assertTrue($result->isOK()); $foundUser = $result->extraInfo(); From 37c9f82d5cc313e06e2de3c2fa1af539cd55f88d Mon Sep 17 00:00:00 2001 From: tswagger Date: Fri, 15 Sep 2023 18:22:14 -0500 Subject: [PATCH 35/41] Syntax adjustment Signed-off-by: tswagger --- src/Filters/HmacAuth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Filters/HmacAuth.php b/src/Filters/HmacAuth.php index c67c6ef97..f655d0264 100644 --- a/src/Filters/HmacAuth.php +++ b/src/Filters/HmacAuth.php @@ -32,7 +32,7 @@ public function before(RequestInterface $request, $arguments = null) $result = $authenticator->attempt($requestParams); - if (! $result->isOK() || ($arguments !== null && count($arguments) > 0 && $result->extraInfo()->hmacTokenCant($arguments[0]))) { + if (! $result->isOK() || ($arguments !== null && $arguments !== [] && $result->extraInfo()->hmacTokenCant($arguments[0]))) { return service('response') ->setStatusCode(Response::HTTP_UNAUTHORIZED) ->setJSON(['message' => lang('Auth.badToken')]); From 58b604238a1082b12ed83b4a6a6de229790dafba Mon Sep 17 00:00:00 2001 From: tswagger Date: Fri, 15 Sep 2023 19:18:42 -0500 Subject: [PATCH 36/41] Added additional test Signed-off-by: tswagger --- tests/Authentication/HasHmacTokensTest.php | 151 +++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tests/Authentication/HasHmacTokensTest.php diff --git a/tests/Authentication/HasHmacTokensTest.php b/tests/Authentication/HasHmacTokensTest.php new file mode 100644 index 000000000..35f0b7153 --- /dev/null +++ b/tests/Authentication/HasHmacTokensTest.php @@ -0,0 +1,151 @@ +user = fake(UserModel::class); + $this->db->table($this->tables['identities'])->truncate(); + } + + public function testGenerateHmacToken(): void + { + $token = $this->user->generateHmacToken('foo'); + + $this->assertSame('foo', $token->name); + $this->assertNull($token->expires); + + $this->assertIsString($token->secret); + $this->assertIsString($token->secret2); + + // All scopes are assigned by default via wildcard + $this->assertSame(['*'], $token->scopes); + } + + public function testHmacTokens(): void + { + // Should return empty array when none exist + $this->assertSame([], $this->user->accessTokens()); + + // Give the user a couple of access tokens + /** @var AccessToken $token1 */ + $token1 = fake( + UserIdentityModel::class, + ['user_id' => $this->user->id, 'type' => 'hmac_sha256', 'secret' => 'key1', 'secret2' => 'secretKey1'] + ); + + /** @var AccessToken $token2 */ + $token2 = fake( + UserIdentityModel::class, + ['user_id' => $this->user->id, 'type' => 'hmac_sha256', 'secret' => 'key2', 'secret2' => 'secretKey2'] + ); + + /** @var AccessToken[] $tokens */ + $tokens = $this->user->hmacTokens(); + + $this->assertCount(2, $tokens); + $this->assertSame($token1->id, $tokens[0]->id); + $this->assertSame($token1->secret, $tokens[0]->secret); // Key + $this->assertSame($token1->secret2, $tokens[0]->secret2); // Secret Key + $this->assertSame($token2->id, $tokens[1]->id); + $this->assertSame($token2->secret, $tokens[1]->secret); + $this->assertSame($token2->secret2, $tokens[1]->secret2); + } + + public function testGetHmacToken(): void + { + // Should return null when not found + $this->assertNull($this->user->getHmacToken('foo')); + + $token = $this->user->generateHmacToken('foo'); + + $found = $this->user->getHmacToken($token->secret); + + $this->assertInstanceOf(AccessToken::class, $found); + $this->assertSame($token->id, $found->id); + $this->assertSame($token->secret, $found->secret); // Key + $this->assertSame($token->secret2, $found->secret2); // Secret Key + } + + public function testGetHmacTokenById(): void + { + // Should return null when not found + $this->assertNull($this->user->getHmacTokenById(123)); + + $token = $this->user->generateHmacToken('foo'); + $found = $this->user->getHmacTokenById($token->id); + + $this->assertInstanceOf(AccessToken::class, $found); + $this->assertSame($token->id, $found->id); + $this->assertSame($token->secret, $found->secret); // Key + $this->assertSame($token->secret2, $found->secret2); // Secret Key + } + + public function testRevokeHmacToken(): void + { + $token = $this->user->generateHmacToken('foo'); + + $this->assertCount(1, $this->user->hmacTokens()); + + $this->user->revokeHmacToken($token->secret); + + $this->assertCount(0, $this->user->hmacTokens()); + } + + public function testRevokeAllHmacTokens(): void + { + $this->user->generateHmacToken('foo'); + $this->user->generateHmacToken('foo'); + + $this->assertCount(2, $this->user->hmacTokens()); + + $this->user->revokeAllHmacTokens(); + + $this->assertCount(0, $this->user->hmacTokens()); + } + + public function testHmacTokenCanNoTokenSet(): void + { + $this->assertFalse($this->user->hmacTokenCan('foo')); + } + + public function testHmacTokenCanBasics(): void + { + $token = $this->user->generateHmacToken('foo', ['foo:bar']); + $this->user->setHmacToken($token); + + $this->assertTrue($this->user->hmacTokenCan('foo:bar')); + $this->assertFalse($this->user->hmacTokenCan('foo:baz')); + } + + public function testHmacTokenCantNoTokenSet(): void + { + $this->assertTrue($this->user->hmacTokenCant('foo')); + } + + public function testHmacTokenCant(): void + { + $token = $this->user->generateHmacToken('foo', ['foo:bar']); + $this->user->setHmacToken($token); + + $this->assertFalse($this->user->hmacTokenCant('foo:bar')); + $this->assertTrue($this->user->hmacTokenCant('foo:baz')); + } +} From 76baf8063b6c09760fbab230e04eccdf691721ef Mon Sep 17 00:00:00 2001 From: tswagger Date: Fri, 15 Sep 2023 21:36:37 -0500 Subject: [PATCH 37/41] Added additional tests Signed-off-by: tswagger --- .../Authenticators/HmacSha256.php | 22 ++++---- .../Authenticators/HmacAuthenticatorTest.php | 55 +++++++++++++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/Authentication/Authenticators/HmacSha256.php b/src/Authentication/Authenticators/HmacSha256.php index 1722bb3c0..3e6d7a260 100644 --- a/src/Authentication/Authenticators/HmacSha256.php +++ b/src/Authentication/Authenticators/HmacSha256.php @@ -92,7 +92,7 @@ public function attempt(array $credentials): Result } $user = $user->setHmacToken( - $user->getHmacToken($this->getAuthKeyFromToken()) + $user->getHmacToken($this->getHmacKeyFromToken()) ); $this->login($user); @@ -228,7 +228,7 @@ public function loginById($userId): void } $user->setHmacToken( - $user->getHmacToken($this->getAuthKeyFromToken()) + $user->getHmacToken($this->getHmacKeyFromToken()) ); $this->login($user); @@ -251,16 +251,16 @@ public function getUser(): ?User } /** - * Returns the Full Authorization token from the Authorization header + * Returns the Full HMAC Authorization token from the Authorization header * * @return ?string Trimmed Authorization Token from Header */ - public function getFullAuthToken(): ?string + public function getFullHmacToken(): ?string { /** @var IncomingRequest $request */ $request = service('request'); - $header = $request->getHeaderLine(config('Auth')->authenticatorHeader['tokens']); + $header = $request->getHeaderLine(config('Auth')->authenticatorHeader['hmac']); if ($header === '') { return null; @@ -279,7 +279,7 @@ public function getFullAuthToken(): ?string public function getHmacAuthTokens(?string $fullToken = null): ?array { if (! isset($fullToken)) { - $fullToken = $this->getFullAuthToken(); + $fullToken = $this->getFullHmacToken(); } if (isset($fullToken)) { @@ -294,9 +294,9 @@ public function getHmacAuthTokens(?string $fullToken = null): ?array * * @return ?string HMAC token key */ - public function getAuthKeyFromToken(): ?string + public function getHmacKeyFromToken(): ?string { - [$key, $hmacHash] = $this->getHmacAuthTokens(); + [$key, $secretKey] = $this->getHmacAuthTokens(); return $key; } @@ -304,13 +304,13 @@ public function getAuthKeyFromToken(): ?string /** * Retrieve the HMAC Hash from the Auth token * - * @return ?string HMAC signature + * @return ?string HMAC Hash */ public function getHmacHashFromToken(): ?string { - [$key, $hmacHash] = $this->getHmacAuthTokens(); + [$key, $hash] = $this->getHmacAuthTokens(); - return $hmacHash; + return $hash; } /** diff --git a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php index 940cfadd4..34d0a2c38 100644 --- a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php @@ -6,6 +6,7 @@ use CodeIgniter\I18n\Time; use CodeIgniter\Shield\Authentication\Authentication; +use CodeIgniter\Shield\Authentication\AuthenticationException; use CodeIgniter\Shield\Authentication\Authenticators\HmacSha256; use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Entities\AccessToken; @@ -77,6 +78,23 @@ public function testLoginByIdNoToken(): void $this->assertNull($this->auth->getUser()->currentHmacToken()); } + public function testLoginByIdBadId(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + + $this->assertFalse($this->auth->loggedIn()); + + try { + $this->auth->loginById(0); + } catch (AuthenticationException $e) { + // Failed login + } + + $this->assertFalse($this->auth->loggedIn()); + $this->assertNull($this->auth->getUser()); + } + public function testLoginByIdWithToken(): void { /** @var User $user */ @@ -247,6 +265,43 @@ public function testAttemptSuccess(): void 'identifier' => $rawToken, 'success' => 1, ]); + + // Check get key Method + $key = $this->auth->getHmacKeyFromToken(); + $this->assertSame($token->secret, $key); + + // Check get hash method + [, $hash] = explode(':', $rawToken); + $secretKey = $this->auth->getHmacHashFromToken(); + $this->assertSame($hash, $secretKey); + } + + public function testAttemptBanned(): void + { + /** @var User $user */ + $user = fake(UserModel::class); + $user->ban('Test ban.'); + + $token = $user->generateHmacToken('foo'); + $rawToken = $this->generateRawHeaderToken($token->secret, $token->secret2, 'bar'); + $this->setRequestHeader($rawToken); + + $result = $this->auth->attempt([ + 'token' => $rawToken, + 'body' => 'bar', + ]); + + $this->assertFalse($result->isOK()); + + $foundUser = $result->extraInfo(); + $this->assertNull($foundUser); + + // A login attempt should have been recorded + $this->seeInDatabase($this->tables['token_logins'], [ + 'id_type' => HmacSha256::ID_TYPE_HMAC_TOKEN, + 'identifier' => $rawToken, + 'success' => 0, + ]); } protected function setRequestHeader(string $token): void From f57f45fee96e6f637681e33a0b2abd2ae65705b5 Mon Sep 17 00:00:00 2001 From: tswagger Date: Fri, 15 Sep 2023 22:04:33 -0500 Subject: [PATCH 38/41] Fix to test Signed-off-by: tswagger --- tests/Authentication/Authenticators/HmacAuthenticatorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php index 34d0a2c38..ffb3e6c6d 100644 --- a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php @@ -81,7 +81,7 @@ public function testLoginByIdNoToken(): void public function testLoginByIdBadId(): void { /** @var User $user */ - $user = fake(UserModel::class); + fake(UserModel::class); $this->assertFalse($this->auth->loggedIn()); From 968997a730ae2ea9a18f3882ff374d3174a284d9 Mon Sep 17 00:00:00 2001 From: tswagger Date: Sat, 16 Sep 2023 06:29:25 -0500 Subject: [PATCH 39/41] Removed redundant comment Signed-off-by: tswagger --- tests/Authentication/Authenticators/HmacAuthenticatorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php index ffb3e6c6d..9ea22d19b 100644 --- a/tests/Authentication/Authenticators/HmacAuthenticatorTest.php +++ b/tests/Authentication/Authenticators/HmacAuthenticatorTest.php @@ -80,7 +80,6 @@ public function testLoginByIdNoToken(): void public function testLoginByIdBadId(): void { - /** @var User $user */ fake(UserModel::class); $this->assertFalse($this->auth->loggedIn()); From a1b64dba0175f23e2b96592fcd1f6a920da89ea7 Mon Sep 17 00:00:00 2001 From: tswagger Date: Mon, 18 Sep 2023 09:07:05 -0500 Subject: [PATCH 40/41] Added config copy to Setup script Signed-off-by: tswagger --- src/Commands/Setup.php | 13 +++++++++++++ tests/Commands/SetupTest.php | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/src/Commands/Setup.php b/src/Commands/Setup.php index c9ced3efd..ce052b714 100644 --- a/src/Commands/Setup.php +++ b/src/Commands/Setup.php @@ -83,6 +83,7 @@ private function publishConfig(): void { $this->publishConfigAuth(); $this->publishConfigAuthGroups(); + $this->publishConfigAuthToken(); $this->setupHelper(); $this->setupRoutes(); @@ -131,6 +132,18 @@ private function publishConfigAuthGroups(): void $this->copyAndReplace($file, $replaces); } + private function publishConfigAuthToken(): void + { + $file = 'Config/AuthToken.php'; + $replaces = [ + 'namespace CodeIgniter\Shield\Config' => 'namespace Config', + 'use CodeIgniter\\Config\\BaseConfig;' => 'use CodeIgniter\\Shield\\Config\\AuthToken as ShieldAuthToken;', + 'extends BaseConfig' => 'extends ShieldAuthToken', + ]; + + $this->copyAndReplace($file, $replaces); + } + /** * Write a file, catching any exceptions and showing a * nicely formatted error. diff --git a/tests/Commands/SetupTest.php b/tests/Commands/SetupTest.php index 43225adf9..9a4281715 100644 --- a/tests/Commands/SetupTest.php +++ b/tests/Commands/SetupTest.php @@ -68,6 +68,10 @@ public function testRun(): void $this->assertStringContainsString('namespace Config;', $auth); $this->assertStringContainsString('use CodeIgniter\Shield\Config\Auth as ShieldAuth;', $auth); + $authToken = file_get_contents($appFolder . 'Config/AuthToken.php'); + $this->assertStringContainsString('namespace Config;', $authToken); + $this->assertStringContainsString('use CodeIgniter\Shield\Config\AuthToken as ShieldAuthToken;', $authToken); + $routes = file_get_contents($appFolder . 'Config/Routes.php'); $this->assertStringContainsString('service(\'auth\')->routes($routes);', $routes); @@ -79,6 +83,7 @@ public function testRun(): void $this->assertStringContainsString( ' Created: vfs://root/Config/Auth.php Created: vfs://root/Config/AuthGroups.php + Created: vfs://root/Config/AuthToken.php Updated: vfs://root/Controllers/BaseController.php Updated: vfs://root/Config/Routes.php Updated: We have updated file \'vfs://root/Config/Security.php\' for security reasons.', From 9d223a4b4c39970f29a823ba02e273bbef44927f Mon Sep 17 00:00:00 2001 From: tswagger Date: Mon, 18 Sep 2023 09:11:18 -0500 Subject: [PATCH 41/41] Minor fix in docs Signed-off-by: tswagger --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 98a9505a7..c3452f19c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -219,7 +219,7 @@ public $aliases = [ | 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). | +| 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. |