From 61ec041b3647e64e3c3c6f0d223974dac8599d37 Mon Sep 17 00:00:00 2001 From: dakriy Date: Thu, 4 Jun 2020 21:26:45 -0700 Subject: [PATCH] So apparently I wasn't hashing tokens... oops --- .../app/Http/Controllers/AuthController.php | 4 +- .../app/Http/Controllers/DoorsController.php | 2 +- .../app/Http/Controllers/TokensController.php | 60 +++++++++++++++++-- .../app/Http/Controllers/UsersController.php | 4 +- src/backend/database/seeds/UsersSeeder.php | 1 + src/backend/src/Entities/HashedSearchable.php | 8 +++ src/backend/src/Entities/RawToken.php | 35 +++++++++++ src/backend/src/Entities/Token.php | 46 +++++++------- src/backend/src/Entities/User.php | 2 +- .../Tokens/DatabaseTokensRepository.php | 41 ++++++++----- .../Tokens/InMemoryTokensRepository.php | 23 +++---- .../Gateways/Tokens/LocalTokensRepository.php | 11 ++-- .../src/Gateways/Tokens/TokensRepository.php | 15 +++-- .../Token/Authenticate/Authenticate.php | 9 ++- .../AuthenticateUseCaseServiceProvider.php | 6 +- .../Tokens/CreateToken/CreateToken.php | 6 +- .../Tokens/UpdateToken/UpdateToken.php | 8 +-- .../Users/Authenticate/APIPresenter.php | 3 +- .../Users/Authenticate/Authenticate.php | 17 ++++-- .../AuthenticateUseCaseServiceProvider.php | 3 +- .../Users/Authenticate/ResponseModel.php | 21 ++++++- .../tests/Database/TokenDatabaseTest.php | 51 ++++++++++++---- .../Feature/Api/Auth/AuthControllerTest.php | 20 +++++-- .../Api/Auth/UserAuthenticateUseCaseStub.php | 16 ++--- .../Feature/Api/Groups/CreateGroupApiTest.php | 1 + .../AuthenticatesWithApplicationTestCase.php | 6 +- .../Users/Authenticate/AttemptUseCaseTest.php | 2 +- .../Users/Authenticate/PresenterTest.php | 24 +++++--- .../Users/Authenticate/SamlUseCaseTest.php | 10 ++-- .../Users/Authenticate/UseCaseBaseTest.php | 7 ++- 30 files changed, 326 insertions(+), 136 deletions(-) create mode 100644 src/backend/src/Entities/RawToken.php diff --git a/src/backend/app/Http/Controllers/AuthController.php b/src/backend/app/Http/Controllers/AuthController.php index cb8742ae..5132e504 100644 --- a/src/backend/app/Http/Controllers/AuthController.php +++ b/src/backend/app/Http/Controllers/AuthController.php @@ -53,13 +53,13 @@ class AuthController extends ApiController */ public function login(AuthenticateUseCase $authenticateUseCase): JsonResponse { - $presenter = new APIPresenter(); - $this->validate($this->request, [ 'email' => 'required|string|email', 'password' => 'required|string', ]); + $presenter = new APIPresenter(); + $authenticateUseCase->attempt($presenter, $this->request->all()); return $this->respondWithData($presenter->getViewModel())->withCookie( diff --git a/src/backend/app/Http/Controllers/DoorsController.php b/src/backend/app/Http/Controllers/DoorsController.php index 03514956..985f38f4 100644 --- a/src/backend/app/Http/Controllers/DoorsController.php +++ b/src/backend/app/Http/Controllers/DoorsController.php @@ -38,7 +38,7 @@ class DoorsController extends ApiController * * @authenticated * @paginated - * @queryParam query Searches doors for location, name, and version. + * @queryParam query Searches doors for location, name, and version. Example: bat * * @param \Source\UseCases\Doors\GetDoors\GetDoorsUseCase $getDoors * @return \Illuminate\Http\JsonResponse diff --git a/src/backend/app/Http/Controllers/TokensController.php b/src/backend/app/Http/Controllers/TokensController.php index e1a51954..b2b7af9b 100644 --- a/src/backend/app/Http/Controllers/TokensController.php +++ b/src/backend/app/Http/Controllers/TokensController.php @@ -15,9 +15,25 @@ use Source\UseCases\Tokens\CreateToken\APIPresenter as CreateTokenAPIPresenter; use Source\UseCases\Tokens\ExpireToken\APIPresenter as ExpireTokenAPIPresenter; use Source\UseCases\Tokens\UpdateToken\APIPresenter as UpdateTokenAPIPresenter; +/** + * @group Token Management + * + * This set of routes is responsible for management of tokens for users. You cannot delete tokens for records sake, but + * you can expire them, which makes them unusable. + */ class TokensController extends ApiController { /** + * Filter Tokens + * + * This route filters all tokens by user_id or valid date. If valid_at is set, only tokens valid on that date will + * be returned. + * + * @authenticated + * @paginated + * @queryParam user_id The user id ot filter on. Example: 1 + * @queryParam valid_at The date to filter when tokens are valid: Example: 2020-06-04 19:41:55 + * * @param \Source\UseCases\Tokens\GetTokens\GetTokensUseCase $allTokens * @return \Illuminate\Http\JsonResponse * @throws \Source\Exceptions\AuthorizationException @@ -49,6 +65,15 @@ class TokensController extends ApiController } /** + * Get Token + * + * This endpoint retrieves all metadata about the token. + * + * @authenticated + * @urlParam tokenId required The ID of the token to get information for. Example: 1 + * + * @response 404 {"status":"error","code":404,"message":"Entity not found"} + * * @param \Source\UseCases\Tokens\GetToken\GetTokenUseCase $token * @param string $tokenId * @return \Illuminate\Http\JsonResponse @@ -67,6 +92,18 @@ class TokensController extends ApiController } /** + * Create Token + * + * This route generates a new token for a given user. + * + * @authenticated + * @bodyParam name string required The name of the token for identifying it. Example: CSLab Self-Serve Token + * @bodyParam user_id string required The id of the user the token will authenticate. Example: 1 + * @bodyParam expires_at string The datetime that the token will no longer be usable. Example: 2020-06-04 19:35:05 + * + * @response 422 + * {"message":"The given data was invalid.","errors":{"name":["The name field is required."],"user_id":["The user id field is required."]}} + * * @param \Source\UseCases\Tokens\CreateToken\CreateTokenUseCase $createToken * @return \Illuminate\Http\JsonResponse * @throws \Illuminate\Validation\ValidationException @@ -80,7 +117,7 @@ class TokensController extends ApiController $this->validate($this->request, [ 'name' => 'required|string|max:255', 'user_id' => 'required|numeric', - 'expires_at' => 'date', + 'expires_at' => 'nullable|date', ]); $presenter = new CreateTokenAPIPresenter(); @@ -92,6 +129,14 @@ class TokensController extends ApiController } /** + * Update Token + * + * This route updates a stored token. One can only update the name and expiry date. + * @authenticated + * @urlParam tokenId required The token id to update. Example: 2 + * @bodyParam name string The new name for the token. Example: New token name + * @bodyParam expires_at datetime The new expiry date. Can be null to never expire. Example: 2023-06-04 19:46:40 + * * @param \Source\UseCases\Tokens\UpdateToken\UpdateTokenUseCase $updateToken * @param string $tokenId * @return \Illuminate\Http\JsonResponse @@ -105,19 +150,24 @@ class TokensController extends ApiController $this->validate($this->request, [ 'name' => 'string|max:255', - 'expires_at' => 'date', + 'expires_at' => 'nullable|date', ]); - $attributes = $this->request->all(); - $presenter = new UpdateTokenAPIPresenter(); - $updateToken->update($tokenId, $attributes, $presenter); + $updateToken->update($tokenId, $this->request->all(), $presenter); return $this->respondWithData($presenter->getViewModel()); } /** + * Expire Token + * + * This endpoint will instantly expire the specified token. + * + * @authenticated + * @urlParam tokenId required The id of the token to expire. Example: 2 + * * @param \Source\UseCases\Tokens\ExpireToken\ExpireTokenUseCase $expireToken * @param string $tokenId * @return \Illuminate\Http\JsonResponse diff --git a/src/backend/app/Http/Controllers/UsersController.php b/src/backend/app/Http/Controllers/UsersController.php index 5c72bce3..4084aa33 100644 --- a/src/backend/app/Http/Controllers/UsersController.php +++ b/src/backend/app/Http/Controllers/UsersController.php @@ -36,11 +36,11 @@ class UsersController extends ApiController * List/Search Users * * This endpoint can list/search/query the list of users. If the parameter is not given it returns a paginated list - * of all doors + * of all doors. This endpoint can search first, last, display name, email, and employee id. * * @authenticated * @paginated - * @queryParam query Searches for first, last, and display names as well as email and peoplesoft employee id. + * @queryParam query The query to search on Example: admin * * @param \Source\UseCases\Users\GetUsers\GetUsersUseCase $getAllUsers * @return \Illuminate\Http\JsonResponse diff --git a/src/backend/database/seeds/UsersSeeder.php b/src/backend/database/seeds/UsersSeeder.php index db9ce3a2..c640b029 100644 --- a/src/backend/database/seeds/UsersSeeder.php +++ b/src/backend/database/seeds/UsersSeeder.php @@ -15,6 +15,7 @@ class UsersSeeder extends Seeder * @param \Source\Gateways\GroupUser\DatabaseGroupUserRepository $repository * @return void * @throws \Source\Exceptions\EntityNotFoundException + * @throws \Source\Exceptions\EntityExistsException */ public function run(DatabaseUsersRepository $users, DatabaseGroupUserRepository $repository): void { diff --git a/src/backend/src/Entities/HashedSearchable.php b/src/backend/src/Entities/HashedSearchable.php index 56e3e65e..18a4551c 100644 --- a/src/backend/src/Entities/HashedSearchable.php +++ b/src/backend/src/Entities/HashedSearchable.php @@ -54,4 +54,12 @@ class HashedSearchable { return $this->hash; } + + /** + * @return string + */ + public function __toString(): string + { + return $this->getHash(); + } } diff --git a/src/backend/src/Entities/RawToken.php b/src/backend/src/Entities/RawToken.php new file mode 100644 index 00000000..ad0c1d15 --- /dev/null +++ b/src/backend/src/Entities/RawToken.php @@ -0,0 +1,35 @@ +raw = $raw; + $this->token = $token; + } + + /** + * @return string + */ + public function getRaw(): string + { + return $this->raw; + } + + /** + * @return \Source\Entities\Token + */ + public function getToken(): Token + { + return $this->token; + } +} diff --git a/src/backend/src/Entities/Token.php b/src/backend/src/Entities/Token.php index 0532dd58..cba49976 100644 --- a/src/backend/src/Entities/Token.php +++ b/src/backend/src/Entities/Token.php @@ -23,9 +23,9 @@ class Token protected int $userId; /** - * @var string + * @var \Source\Entities\HashedSearchable */ - protected string $tokenString; + protected HashedSearchable $tokenHash; /** * @var string|null @@ -48,18 +48,18 @@ class Token protected ?Carbon $updatedAt; /** - * @param int $id - * @param int $userId - * @param string|null $name - * @param string $tokenString - * @param Carbon|null $expiresAt - * @param Carbon|null $createdAt - * @param Carbon|null $updatedAt + * @param int $id + * @param int $userId + * @param \Source\Entities\HashedSearchable $tokenHash + * @param string|null $name + * @param Carbon|null $expiresAt + * @param Carbon|null $createdAt + * @param Carbon|null $updatedAt */ public function __construct( int $id, int $userId, - string $tokenString, + HashedSearchable $tokenHash, ?string $name = null, ?Carbon $expiresAt = null, ?Carbon $createdAt = null, @@ -68,19 +68,23 @@ class Token $this->id = $id; $this->userId = $userId; $this->name = $name; - $this->tokenString = $tokenString; + $this->tokenHash = $tokenHash; $this->expiresAt = $expiresAt; $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; } /** - * @param string|null $token + * @param \Source\Entities\HashedSearchable|null $token * @return bool */ - public function matches(?string $token): bool + public function matches(?HashedSearchable $token): bool { - return $this->tokenString === $token; + if (!$token) { + return false; + } + + return $this->getToken()->getHash() === $token->getHash(); } /** @@ -121,11 +125,11 @@ class Token } /** - * @return string + * @return \Source\Entities\HashedSearchable */ - public function getTokenString(): string + public function getToken(): HashedSearchable { - return $this->tokenString; + return $this->tokenHash; } /** @@ -205,12 +209,4 @@ class Token return $this->userId === (int)$id; } - - /** - * @return bool - */ - public function hasName(): bool - { - return (bool)$this->name; - } } diff --git a/src/backend/src/Entities/User.php b/src/backend/src/Entities/User.php index c2595d65..c483541c 100644 --- a/src/backend/src/Entities/User.php +++ b/src/backend/src/Entities/User.php @@ -217,7 +217,7 @@ class User } /** - * @param string $doorcode + * @param \Source\Entities\HashedSearchable|null $doorcode * @return bool */ public function hasDoorcodeOf(?HashedSearchable $doorcode): bool diff --git a/src/backend/src/Gateways/Tokens/DatabaseTokensRepository.php b/src/backend/src/Gateways/Tokens/DatabaseTokensRepository.php index a93e97fe..8889cf9f 100644 --- a/src/backend/src/Gateways/Tokens/DatabaseTokensRepository.php +++ b/src/backend/src/Gateways/Tokens/DatabaseTokensRepository.php @@ -8,6 +8,8 @@ use Carbon\Carbon; use Source\Entities\Token; use Illuminate\Support\Str; use Source\Sanitize\CastsTo; +use Source\Entities\RawToken; +use Source\Entities\HashedSearchable; use Illuminate\Database\Eloquent\Builder; use Source\Exceptions\EntityNotFoundException; @@ -30,7 +32,7 @@ class DatabaseTokensRepository implements TokensRepository $dbToken = new \App\Token(); $dbToken->setAttribute('name', $token->getName()); $dbToken->setAttribute('user_id', $token->getUserId()); - $dbToken->setAttribute('api_token', $token->getTokenString()); + $dbToken->setAttribute('api_token', $token->getToken()->getHash()); $dbToken->setAttribute('expires_at', $token->getExpiresAt()); $dbToken->save(); @@ -40,15 +42,16 @@ class DatabaseTokensRepository implements TokensRepository /** * @inheritDoc */ - public function createLoginToken(string $userId): Token + public function createLoginToken(string $userId, string $salt): RawToken { - return $this->create(new Token( + $raw = self::generateTokenString(); + return new RawToken($raw, $this->create(new Token( 0, $userId, - self::generateTokenString(), + HashedSearchable::hash($salt, $raw), null, Carbon::now()->addDay() - )); + ))); } /** @@ -65,7 +68,7 @@ class DatabaseTokensRepository implements TokensRepository $dbToken->setAttribute('name', $token->getName()); $dbToken->setAttribute('expired_at', $token->getExpiresAt()); - $dbToken->setAttribute('api_token', $token->getTokenString()); + $dbToken->setAttribute('api_token', $token->getToken()->getHash()); $dbToken->save(); return self::dbTokenToToken($dbToken); @@ -95,7 +98,7 @@ class DatabaseTokensRepository implements TokensRepository return new Token( $token->getAttribute('id'), $token->getAttribute('user_id'), - $token->getAttribute('api_token'), + new HashedSearchable($token->getAttribute('api_token')), $token->getAttribute('name'), $token->getAttribute('expires_at'), $token->getAttribute('created_at'), @@ -106,12 +109,16 @@ class DatabaseTokensRepository implements TokensRepository /** * @inheritDoc */ - public function findValidToken(string $token): ?Token + public function findValidToken(?HashedSearchable $hash): ?Token { + if (!$hash) { + return null; + } + /** @var \App\Token|null $found */ - $found = \App\Token::query()->where('api_token', $token)->where('expires_at', '>', Carbon::now())->orWhere( - static function (Builder $query) use ($token) { - $query->where('api_token', $token)->where('expires_at', null); + $found = \App\Token::query()->where('api_token', $hash->getHash())->where('expires_at', '>', Carbon::now())->orWhere( + static function (Builder $query) use ($hash) { + $query->where('api_token', $hash->getHash())->where('expires_at', null); } )->first(); @@ -126,12 +133,16 @@ class DatabaseTokensRepository implements TokensRepository /** * @inheritDoc */ - public function invalidateToken(string $token): void + public function invalidateToken(?HashedSearchable $hash): void { + if (!$hash) { + return; + } + /** @var \App\Token|null $found */ - $found = \App\Token::query()->where('api_token', $token)->where('expires_at', '>', Carbon::now())->orWhere( - static function (Builder $query) use ($token) { - $query->where('api_token', $token)->where('expires_at', null); + $found = \App\Token::query()->where('api_token', $hash)->where('expires_at', '>', Carbon::now())->orWhere( + static function (Builder $query) use ($hash) { + $query->where('api_token', $hash->getHash())->where('expires_at', null); } )->first(); diff --git a/src/backend/src/Gateways/Tokens/InMemoryTokensRepository.php b/src/backend/src/Gateways/Tokens/InMemoryTokensRepository.php index ae45173a..e088785f 100644 --- a/src/backend/src/Gateways/Tokens/InMemoryTokensRepository.php +++ b/src/backend/src/Gateways/Tokens/InMemoryTokensRepository.php @@ -5,6 +5,8 @@ namespace Source\Gateways\Tokens; use Carbon\Carbon; use Source\Entities\Token; +use Source\Entities\RawToken; +use Source\Entities\HashedSearchable; use Source\Exceptions\EntityNotFoundException; class InMemoryTokensRepository implements TokensRepository @@ -31,17 +33,16 @@ class InMemoryTokensRepository implements TokensRepository /** * @inheritDoc */ - public function createLoginToken(string $userId): Token + public function createLoginToken(string $userId, string $salt): RawToken { - $token = new Token( - static::$idCounter++, + $raw = self::generateTokenString(); + return new RawToken($raw, $this->create(new Token( + 0, $userId, - self::generateTokenString(), + HashedSearchable::hash($salt, $raw), null, Carbon::now()->addDay() - ); - $this->tokens[] = $token; - return $token; + ))); } /** @@ -83,10 +84,10 @@ class InMemoryTokensRepository implements TokensRepository } /** @inheritDoc */ - public function findValidToken(string $tokenToMatch): ?Token + public function findValidToken(?HashedSearchable $hash): ?Token { foreach ($this->tokens as $token) { - if ($token->matches($tokenToMatch) && $token->isValidAtTime(Carbon::now())) { + if ($token->matches($hash) && $token->isValidAtTime(Carbon::now())) { return $token; } } @@ -97,9 +98,9 @@ class InMemoryTokensRepository implements TokensRepository /** * @inheritDoc */ - public function invalidateToken(string $token): void + public function invalidateToken(?HashedSearchable $hash): void { - $tok = $this->findValidToken($token); + $tok = $this->findValidToken($hash); if ($tok) { $tok->setExpiresAt(Carbon::now()); diff --git a/src/backend/src/Gateways/Tokens/LocalTokensRepository.php b/src/backend/src/Gateways/Tokens/LocalTokensRepository.php index 56a6637a..efd88fd2 100644 --- a/src/backend/src/Gateways/Tokens/LocalTokensRepository.php +++ b/src/backend/src/Gateways/Tokens/LocalTokensRepository.php @@ -5,6 +5,7 @@ namespace Source\Gateways\Tokens; use Carbon\Carbon; use Source\Entities\Token; +use Source\Entities\HashedSearchable; use Source\Gateways\Users\LocalUsersRepository; class LocalTokensRepository extends InMemoryTokensRepository @@ -18,35 +19,35 @@ class LocalTokensRepository extends InMemoryTokensRepository $this->create(new Token( 0, LocalUsersRepository::getAdminUser()->getId(), - 'token_string_admin', + HashedSearchable::hash(config('app.key'), 'token_string_admin'), 'basic token' )); $this->create(new Token( 0, LocalUsersRepository::getSemiPrivilegedUser()->getId(), - 'token_string_semi', + HashedSearchable::hash(config('app.key'), 'token_string_semi'), 'basic token' )); $this->create(new Token( 0, LocalUsersRepository::getComputerScienceStudent()->getId(), - 'token_string_cs', + HashedSearchable::hash(config('app.key'), 'token_string_cs'), 'basic token' )); $this->create(new Token( 0, LocalUsersRepository::getEngineeringLabAccessStudent()->getId(), - 'token_string_engr', + HashedSearchable::hash(config('app.key'), 'token_string_engr'), 'basic token' )); $this->create(new Token( 0, LocalUsersRepository::getAdminUser()->getId(), - 'token_string_expired', + HashedSearchable::hash(config('app.key'), 'token_string_expired'), 'expired token', Carbon::now()->subDays(3) )); diff --git a/src/backend/src/Gateways/Tokens/TokensRepository.php b/src/backend/src/Gateways/Tokens/TokensRepository.php index 0f7debd6..71898d6c 100644 --- a/src/backend/src/Gateways/Tokens/TokensRepository.php +++ b/src/backend/src/Gateways/Tokens/TokensRepository.php @@ -5,6 +5,8 @@ namespace Source\Gateways\Tokens; use Carbon\Carbon; use Source\Entities\Token; +use Source\Entities\RawToken; +use Source\Entities\HashedSearchable; use Source\Exceptions\EntityNotFoundException; interface TokensRepository @@ -18,10 +20,11 @@ interface TokensRepository /** * @param string $userId - * @return \Source\Entities\Token + * @param string $salt + * @return \Source\Entities\RawToken * @throws \Source\Exceptions\EntityNotFoundException */ - public function createLoginToken(string $userId): Token; + public function createLoginToken(string $userId, string $salt): RawToken; /** * @param string $tokenId @@ -45,15 +48,15 @@ interface TokensRepository public function filter(?string $userId = null, ?Carbon $validAt = null): array; /** - * @param string $token + * @param \Source\Entities\HashedSearchable|null $hash * @return Token|null */ - public function findValidToken(string $token): ?Token; + public function findValidToken(?HashedSearchable $hash): ?Token; /** - * @param string $token + * @param \Source\Entities\HashedSearchable $hash */ - public function invalidateToken(string $token): void; + public function invalidateToken(?HashedSearchable $hash): void; /** * @param string $tokenId diff --git a/src/backend/src/UseCases/Token/Authenticate/Authenticate.php b/src/backend/src/UseCases/Token/Authenticate/Authenticate.php index ccf227a6..b726c80d 100644 --- a/src/backend/src/UseCases/Token/Authenticate/Authenticate.php +++ b/src/backend/src/UseCases/Token/Authenticate/Authenticate.php @@ -2,6 +2,7 @@ namespace Source\UseCases\Token\Authenticate; +use Source\Entities\HashedSearchable; use Source\Gateways\Users\UsersRepository; use Source\Gateways\Tokens\TokensRepository; @@ -11,14 +12,18 @@ class Authenticate implements AuthenticateUseCase protected UsersRepository $users; + protected string $salt; + /** * @param TokensRepository $tokens * @param UsersRepository $users + * @param string $salt */ - public function __construct(TokensRepository $tokens, UsersRepository $users) + public function __construct(TokensRepository $tokens, UsersRepository $users, string $salt) { $this->tokens = $tokens; $this->users = $users; + $this->salt = $salt; } @@ -35,7 +40,7 @@ class Authenticate implements AuthenticateUseCase return; } - $found = $this->tokens->findValidToken($token); + $found = $this->tokens->findValidToken(HashedSearchable::hash($this->salt, $token)); if (!$found) { return; diff --git a/src/backend/src/UseCases/Token/Authenticate/AuthenticateUseCaseServiceProvider.php b/src/backend/src/UseCases/Token/Authenticate/AuthenticateUseCaseServiceProvider.php index a10f7262..69281e1f 100644 --- a/src/backend/src/UseCases/Token/Authenticate/AuthenticateUseCaseServiceProvider.php +++ b/src/backend/src/UseCases/Token/Authenticate/AuthenticateUseCaseServiceProvider.php @@ -24,7 +24,11 @@ class AuthenticateUseCaseServiceProvider extends ServiceProvider implements Defe $this->app->bind( AuthenticateUseCase::class, static function (Application $app) { - return new Authenticate($app->make(TokensRepository::class), $app->make(UsersRepository::class)); + return new Authenticate( + $app->make(TokensRepository::class), + $app->make(UsersRepository::class), + config('app.key') + ); } ); } diff --git a/src/backend/src/UseCases/Tokens/CreateToken/CreateToken.php b/src/backend/src/UseCases/Tokens/CreateToken/CreateToken.php index 2b1058ae..1b775c0e 100644 --- a/src/backend/src/UseCases/Tokens/CreateToken/CreateToken.php +++ b/src/backend/src/UseCases/Tokens/CreateToken/CreateToken.php @@ -42,10 +42,7 @@ class CreateToken implements CreateTokenUseCase $tokenString = $this->tokens::generateTokenString(); - $date = null; - if (isset($attributes['expires_at'])) { - $date = new Carbon($attributes['expires_at']); - } + $date = $this->liberalCastToCarbon($attributes['expires_at'] ?? null); $token = $this->tokens->create( new Token( @@ -57,6 +54,7 @@ class CreateToken implements CreateTokenUseCase ) ); + $response = new ResponseModel($tokenString, $token); $presenter->present($response); diff --git a/src/backend/src/UseCases/Tokens/UpdateToken/UpdateToken.php b/src/backend/src/UseCases/Tokens/UpdateToken/UpdateToken.php index a12120a3..dab93ee8 100644 --- a/src/backend/src/UseCases/Tokens/UpdateToken/UpdateToken.php +++ b/src/backend/src/UseCases/Tokens/UpdateToken/UpdateToken.php @@ -33,9 +33,9 @@ class UpdateToken implements UpdateTokenUseCase throw new EntityNotFoundException(); } - $expiresAt = $attributes['expires_at'] ?? null; - if ($expiresAt) { - $expiresAt = new Carbon($expiresAt); + $expiresAt = null; + if (isset($attributes['expires_at'])) { + $expiresAt = $this->liberalCastToCarbon($expiresAt); } else { $expiresAt = $token->getExpiresAt(); } @@ -43,7 +43,7 @@ class UpdateToken implements UpdateTokenUseCase $token = new Token( $token->getId(), $token->getUserId(), - $token->getTokenString(), + $token->getToken(), $attributes['name'] ?? $token->getName(), $expiresAt, $token->getCreatedAt(), diff --git a/src/backend/src/UseCases/Users/Authenticate/APIPresenter.php b/src/backend/src/UseCases/Users/Authenticate/APIPresenter.php index 57b71f7a..59f04756 100644 --- a/src/backend/src/UseCases/Users/Authenticate/APIPresenter.php +++ b/src/backend/src/UseCases/Users/Authenticate/APIPresenter.php @@ -22,10 +22,9 @@ class APIPresenter extends BasePresenter implements Presenter $expires = $expires->diffInMinutes(Carbon::now()); } - $this->viewModel['user'] = $this->formatFullUser($user); $this->viewModel['token'] = [ - 'value' => $token->getTokenString(), + 'value' => $responseModel->getRawToken(), 'expires_at' => $this->formatDateTime($token->getExpiresAt()), 'minutes' => $expires, ]; diff --git a/src/backend/src/UseCases/Users/Authenticate/Authenticate.php b/src/backend/src/UseCases/Users/Authenticate/Authenticate.php index 82736c2f..bce8fd43 100644 --- a/src/backend/src/UseCases/Users/Authenticate/Authenticate.php +++ b/src/backend/src/UseCases/Users/Authenticate/Authenticate.php @@ -3,6 +3,7 @@ namespace Source\UseCases\Users\Authenticate; use Source\Entities\User; +use Source\Entities\HashedSearchable; use Source\Gateways\Saml\SamlRepository; use Source\Gateways\Users\UsersRepository; use Source\Gateways\Tokens\TokensRepository; @@ -25,16 +26,20 @@ class Authenticate implements AuthenticateUseCase */ protected SamlRepository $saml; + protected string $salt; + /** * @param \Source\Gateways\Users\UsersRepository $users * @param \Source\Gateways\Tokens\TokensRepository $tokens * @param \Source\Gateways\Saml\SamlRepository $saml + * @param string $salt */ - public function __construct(UsersRepository $users, TokensRepository $tokens, SamlRepository $saml) + public function __construct(UsersRepository $users, TokensRepository $tokens, SamlRepository $saml, string $salt) { $this->saml = $saml; $this->users = $users; $this->tokens = $tokens; + $this->salt = $salt; } /** @@ -57,9 +62,9 @@ class Authenticate implements AuthenticateUseCase throw new AuthenticationException(); } - $token = $this->tokens->createLoginToken($user->getId()); + $token = $this->tokens->createLoginToken($user->getId(), $this->salt); - $response = new ResponseModel($user, $token); + $response = new ResponseModel($user, $token->getRaw(), $token->getToken()); $presenter->present($response); } @@ -116,9 +121,9 @@ class Authenticate implements AuthenticateUseCase throw new UserCreationException(); } - $token = $this->tokens->createLoginToken($user->getId()); + $token = $this->tokens->createLoginToken($user->getId(), $this->salt); - $response = new ResponseModel($user, $token); + $response = new ResponseModel($user, $token->getRaw(), $token->getToken()); $presenter->present($response); } @@ -129,7 +134,7 @@ class Authenticate implements AuthenticateUseCase public function samlLogout(?string $token): string { if ($token) { - $this->tokens->invalidateToken($token); + $this->tokens->invalidateToken(HashedSearchable::hash($this->salt, $token)); } return $this->saml->logout(); diff --git a/src/backend/src/UseCases/Users/Authenticate/AuthenticateUseCaseServiceProvider.php b/src/backend/src/UseCases/Users/Authenticate/AuthenticateUseCaseServiceProvider.php index be60b1db..1dfad7d4 100644 --- a/src/backend/src/UseCases/Users/Authenticate/AuthenticateUseCaseServiceProvider.php +++ b/src/backend/src/UseCases/Users/Authenticate/AuthenticateUseCaseServiceProvider.php @@ -26,7 +26,8 @@ class AuthenticateUseCaseServiceProvider extends ServiceProvider implements Defe return new Authenticate( $app->make(UsersRepository::class), $app->make(TokensRepository::class), - $app->make(SamlRepository::class) + $app->make(SamlRepository::class), + config('app.key') ); }); } diff --git a/src/backend/src/UseCases/Users/Authenticate/ResponseModel.php b/src/backend/src/UseCases/Users/Authenticate/ResponseModel.php index 69a50838..1c5ca524 100644 --- a/src/backend/src/UseCases/Users/Authenticate/ResponseModel.php +++ b/src/backend/src/UseCases/Users/Authenticate/ResponseModel.php @@ -18,13 +18,20 @@ class ResponseModel protected Token $token; /** - * @param User $user - * @param Token $token + * @var string */ - public function __construct(User $user, Token $token) + protected string $tokenString; + + /** + * @param User $user + * @param string $tokenString + * @param Token $token + */ + public function __construct(User $user, string $tokenString, Token $token) { $this->user = $user; $this->token = $token; + $this->tokenString = $tokenString; } /** @@ -42,4 +49,12 @@ class ResponseModel { return $this->token; } + + /** + * @return string + */ + public function getRawToken(): string + { + return $this->tokenString; + } } diff --git a/src/backend/tests/Database/TokenDatabaseTest.php b/src/backend/tests/Database/TokenDatabaseTest.php index 84b14eba..9096365c 100644 --- a/src/backend/tests/Database/TokenDatabaseTest.php +++ b/src/backend/tests/Database/TokenDatabaseTest.php @@ -4,9 +4,11 @@ namespace Tests\Database; use Carbon\Carbon; +use RuntimeException; use Source\Entities\User; use Source\Entities\Token; use Tests\DatabaseTestCase; +use Source\Entities\HashedSearchable; use Source\Exceptions\EntityNotFoundException; use Source\Gateways\Users\DatabaseUsersRepository; use Source\Gateways\Tokens\DatabaseTokensRepository; @@ -28,13 +30,38 @@ class TokenDatabaseTest extends DatabaseTestCase */ protected User $user; + public const SALT = '219986'; + + public const VALID_TOKEN = '267270'; + + /** + * @throws \Source\Exceptions\EntityExistsException + */ public function setUp(): void { parent::setUp(); $this->tokens = new DatabaseTokensRepository(); $this->users = new DatabaseUsersRepository(); - $this->user = $this->users->create(new User(0, '', '', '', '', null, null, null)); + $user = $this->users->create(new User(0, '', '', '', '', null, null, null)); + if (!$user) { + throw new RuntimeException('what'); + } + $this->user = $user; + } + + protected function createTokenHash(string $tokenString): HashedSearchable + { + return HashedSearchable::hash(self::SALT, $tokenString); + } + + /** + * @return \Source\Entities\Token + * @throws \Source\Exceptions\EntityNotFoundException + */ + protected function createValidToken(): Token + { + return $this->tokens->create(new Token(0, $this->user->getId(), $this->createTokenHash(self::VALID_TOKEN))); } /** @@ -43,13 +70,13 @@ class TokenDatabaseTest extends DatabaseTestCase */ public function it_creates_and_finds_tokens(): void { - $token = $this->tokens->create(new Token(0, $this->user->getId(), 'token')); + $token = $this->createValidToken(); - $dbToken = $this->tokens->findValidToken('token'); + $dbToken = $this->tokens->findValidToken($this->createTokenHash(self::VALID_TOKEN)); $this->assertEquals($token, $dbToken); - $nullToken = $this->tokens->findValidToken('reeee'); + $nullToken = $this->tokens->findValidToken($this->createTokenHash('reee')); $this->assertNull($nullToken); } @@ -60,9 +87,9 @@ class TokenDatabaseTest extends DatabaseTestCase */ public function it_cannot_find_expired_tokens(): void { - $this->tokens->create(new Token(0, $this->user->getId(), 'token', 'nomen', Carbon::now()->subDays(1))); + $this->tokens->create(new Token(0, $this->user->getId(), $this->createTokenHash(self::VALID_TOKEN), 'nomen', Carbon::now()->subDays(1))); - $token = $this->tokens->findValidToken('token'); + $token = $this->tokens->findValidToken($this->createTokenHash(self::VALID_TOKEN)); $this->assertNull($token); } @@ -73,9 +100,9 @@ class TokenDatabaseTest extends DatabaseTestCase */ public function it_can_find_tokens_with_an_expired_date_in_the_future(): void { - $t = $this->tokens->create(new Token(0, $this->user->getId(), 'token', 'nomen', Carbon::now()->addDays(1))); + $t = $this->tokens->create(new Token(0, $this->user->getId(), $this->createTokenHash(self::VALID_TOKEN), 'nomen', Carbon::now()->addDays(1))); - $token = $this->tokens->findValidToken('token'); + $token = $this->tokens->findValidToken($this->createTokenHash(self::VALID_TOKEN)); $this->assertEquals($t, $token); } @@ -88,7 +115,7 @@ class TokenDatabaseTest extends DatabaseTestCase { $this->expectException(EntityNotFoundException::class); - $this->tokens->create(new Token(0, 999999, 'token')); + $this->tokens->create(new Token(0, 999999, $this->createTokenHash(self::VALID_TOKEN))); } /** @@ -97,10 +124,10 @@ class TokenDatabaseTest extends DatabaseTestCase */ public function it_invalidates_tokens(): void { - $this->tokens->create(new Token(0, $this->user->getId(), 'token', 'nomen', Carbon::now()->addDays(1))); + $this->tokens->create(new Token(0, $this->user->getId(), $this->createTokenHash(self::VALID_TOKEN), 'nomen', Carbon::now()->addDays(1))); - $this->tokens->invalidateToken('token'); + $this->tokens->invalidateToken($this->createTokenHash(self::VALID_TOKEN)); - $this->assertNull($this->tokens->findValidToken('token')); + $this->assertNull($this->tokens->findValidToken($this->createTokenHash(self::VALID_TOKEN))); } } diff --git a/src/backend/tests/Feature/Api/Auth/AuthControllerTest.php b/src/backend/tests/Feature/Api/Auth/AuthControllerTest.php index 8557d259..872d8dbb 100644 --- a/src/backend/tests/Feature/Api/Auth/AuthControllerTest.php +++ b/src/backend/tests/Feature/Api/Auth/AuthControllerTest.php @@ -6,6 +6,8 @@ namespace Tests\Feature\Api\Auth; use Tests\TestCase; use Source\Entities\User; use Source\Entities\Token; +use Source\Entities\RawToken; +use Source\Entities\HashedSearchable; use Source\UseCases\Users\Authenticate\AuthenticateUseCase; class AuthControllerTest extends TestCase @@ -15,6 +17,10 @@ class AuthControllerTest extends TestCase */ protected UserAuthenticateUseCaseStub $useCase; + public const SALT = '219986'; + + public const VALID_TOKEN = '267270'; + public function setUp(): void { parent::setUp(); @@ -33,7 +39,10 @@ class AuthControllerTest extends TestCase { $this->useCase->setUserAndToken( new User(1, 'Tea', '', '', ''), - new Token(1, 1, 'token_string') + new RawToken( + self::VALID_TOKEN, + new Token(1, 1, HashedSearchable::hash(self::SALT, self::VALID_TOKEN)) + ) ); $response = $this->postJson('api/login', ['email' => 'test@test.com', 'password' => 'password']); @@ -43,7 +52,7 @@ class AuthControllerTest extends TestCase ['email' => 'test@test.com', 'password' => 'password'], $this->useCase->getAttemptedCredentials() ); - $response->assertCookie('api_token', 'token_string'); + $response->assertCookie('api_token', self::VALID_TOKEN); $response->assertJsonFragment(['first_name' => 'Tea']); } @@ -77,11 +86,14 @@ class AuthControllerTest extends TestCase { $this->useCase->setUserAndToken( new User(1, 'Tea', '', '', '', ''), - new Token(1, 1, 'token_string') + new RawToken( + self::VALID_TOKEN, + new Token(1, 1, HashedSearchable::hash(self::SALT, self::VALID_TOKEN)) + ) ); $response = $this->get('api/handle-login'); - $response->assertCookie('api_token', 'token_string'); + $response->assertCookie('api_token', self::VALID_TOKEN); $response->assertStatus(302); } diff --git a/src/backend/tests/Feature/Api/Auth/UserAuthenticateUseCaseStub.php b/src/backend/tests/Feature/Api/Auth/UserAuthenticateUseCaseStub.php index 72e97247..4cbcec37 100644 --- a/src/backend/tests/Feature/Api/Auth/UserAuthenticateUseCaseStub.php +++ b/src/backend/tests/Feature/Api/Auth/UserAuthenticateUseCaseStub.php @@ -4,7 +4,7 @@ namespace Tests\Feature\Api\Auth; use Source\Entities\User; -use Source\Entities\Token; +use Source\Entities\RawToken; use Source\UseCases\Users\Authenticate\Presenter; use Source\UseCases\Users\Authenticate\ResponseModel; use Source\UseCases\Users\Authenticate\AuthenticateUseCase; @@ -18,9 +18,9 @@ class UserAuthenticateUseCaseStub implements AuthenticateUseCase protected User $user; /** - * @var \Source\Entities\Token + * @var \Source\Entities\RawToken */ - protected Token $token; + protected RawToken $token; /** * @var array @@ -34,10 +34,10 @@ class UserAuthenticateUseCaseStub implements AuthenticateUseCase public bool $throwCreationException = false; /** - * @param \Source\Entities\User $user - * @param \Source\Entities\Token $token + * @param \Source\Entities\User $user + * @param \Source\Entities\RawToken $token */ - public function setUserAndToken(User $user, Token $token): void + public function setUserAndToken(User $user, RawToken $token): void { $this->user = $user; $this->token = $token; @@ -60,7 +60,7 @@ class UserAuthenticateUseCaseStub implements AuthenticateUseCase { $this->credentials = $credentials; - $response = new ResponseModel($this->user, $this->token); + $response = new ResponseModel($this->user, $this->token->getRaw(), $this->token->getToken()); $presenter->present($response); } @@ -92,7 +92,7 @@ class UserAuthenticateUseCaseStub implements AuthenticateUseCase throw new UserCreationException(); } - $response = new ResponseModel($this->user, $this->token); + $response = new ResponseModel($this->user, $this->token->getRaw(), $this->token->getToken()); $presenter->present($response); } diff --git a/src/backend/tests/Feature/Api/Groups/CreateGroupApiTest.php b/src/backend/tests/Feature/Api/Groups/CreateGroupApiTest.php index eda8e2d6..98e88e6d 100644 --- a/src/backend/tests/Feature/Api/Groups/CreateGroupApiTest.php +++ b/src/backend/tests/Feature/Api/Groups/CreateGroupApiTest.php @@ -50,6 +50,7 @@ class CreateGroupApiTest extends AuthenticatesWithApplicationTestCase /** * @test * @throws \Source\Exceptions\EntityNotFoundException + * @throws \Source\Exceptions\EntityExistsException */ public function it_protects_the_route(): void { diff --git a/src/backend/tests/Feature/AuthenticatesWithApplicationTestCase.php b/src/backend/tests/Feature/AuthenticatesWithApplicationTestCase.php index 2394f42e..cf750039 100644 --- a/src/backend/tests/Feature/AuthenticatesWithApplicationTestCase.php +++ b/src/backend/tests/Feature/AuthenticatesWithApplicationTestCase.php @@ -37,6 +37,8 @@ class AuthenticatesWithApplicationTestCase extends TestCase protected InMemoryDoorsRepository $doorsRepository; + public const VALID_TOKEN = '267270'; + /** * @throws BindingResolutionException */ @@ -64,8 +66,8 @@ class AuthenticatesWithApplicationTestCase extends TestCase $this->usersRepository->create($this->authUser); $this->authorizer->setCurrentUserId($this->authUser->getId()); - $this->tokens->create(new Token(1, 1, 'token', 'name')); - $this->authToken = 'token'; + $this->tokens->create(new Token(1, 1, HashedSearchable::hash(config('app.key'), self::VALID_TOKEN), 'name')); + $this->authToken = self::VALID_TOKEN; } /** diff --git a/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/AttemptUseCaseTest.php b/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/AttemptUseCaseTest.php index 38e68efc..c4ecf508 100644 --- a/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/AttemptUseCaseTest.php +++ b/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/AttemptUseCaseTest.php @@ -109,7 +109,7 @@ class AttemptUseCaseTest extends UseCaseBaseTest $this->assertLessThan(Carbon::now()->addHours(25), $token->getExpiresAt()); $this->assertGreaterThan(Carbon::now()->subHours(23), $token->getExpiresAt()); $this->assertEquals($user->getId(), $token->getUserId()); - $this->assertEquals(60, strlen($token->getTokenString())); + $this->assertNotNull($token->getToken()); $this->assertEquals($token, $this->response->getToken()); } diff --git a/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/PresenterTest.php b/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/PresenterTest.php index e483bdae..6b99da33 100644 --- a/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/PresenterTest.php +++ b/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/PresenterTest.php @@ -7,6 +7,7 @@ use Carbon\Carbon; use Source\Entities\User; use Source\Entities\Token; use Source\Entities\Password; +use Source\Entities\RawToken; use PHPUnit\Framework\TestCase; use Source\Entities\HashedSearchable; use Source\UseCases\Users\Authenticate\APIPresenter; @@ -39,10 +40,11 @@ class PresenterTest extends TestCase /** * @param \Source\Entities\User $user * @param \Source\Entities\Token $token + * @param string $tokenString */ - public function handleTest(User $user, Token $token): void + public function handleTest(User $user, Token $token, string $tokenString): void { - $this->model = new ResponseModel($user, $token); + $this->model = new ResponseModel($user, $tokenString, $token); $this->presenter->present($this->model); @@ -75,9 +77,11 @@ class PresenterTest extends TestCase $expires = new Carbon('2020-03-04'); $user = $this->createUser($expires); - $token = new Token(0, 0, 'token', 'nomen', $expires); + $hash = HashedSearchable::hash('', 'token'); - $this->handleTest($user, $token); + $token = new Token(0, 0, $hash, 'nomen', $expires); + + $this->handleTest($user, $token, 'token'); $minutes = $expires->diffInMinutes(Carbon::now()); @@ -96,9 +100,11 @@ class PresenterTest extends TestCase $expires = null; $user = $this->createUser($expires); - $token = new Token(0, 0, 'token', 'nomen', $expires); + $hash = HashedSearchable::hash('', 'token'); + + $token = new Token(0, 0, $hash, 'nomen', $expires); - $this->handleTest($user, $token); + $this->handleTest($user, $token, 'token'); $this->assertEquals([ 'value' => 'token', @@ -112,9 +118,11 @@ class PresenterTest extends TestCase { $user = $this->createUser(new Carbon('2020-02-02')); - $token = new Token(0, 0, 'token', 'nomen', new Carbon('2020-03-04')); + $hash = HashedSearchable::hash('', 'token'); + + $token = new Token(0, 0, $hash, 'nomen', new Carbon('2020-03-04')); - $this->handleTest($user, $token); + $this->handleTest($user, $token, 'token'); $this->assertEquals([ 'id' => 0, diff --git a/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/SamlUseCaseTest.php b/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/SamlUseCaseTest.php index 5972a93f..7fa862a0 100644 --- a/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/SamlUseCaseTest.php +++ b/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/SamlUseCaseTest.php @@ -7,6 +7,7 @@ use Carbon\Carbon; use Source\Entities\User; use Source\Entities\Token; use Source\Entities\SamlUser; +use Source\Entities\HashedSearchable; use Tests\Doubles\InMemoryUsersRepositoryStub; use Source\UseCases\Users\Authenticate\Authenticate; use Source\UseCases\Users\Authenticate\UserCreationException; @@ -90,8 +91,9 @@ class SamlUseCaseTest extends UseCaseBaseTest public function it_invalidates_token_on_saml_logout(): void { $user = $this->createUser(); - $this->tokens->create(new Token(0, $user->getId(), 'token')); - $this->useCase->samlLogout('token'); + $token = HashedSearchable::hash(self::SALT, self::VALID_TOKEN); + $this->tokens->create(new Token(0, $user->getId(), $token)); + $this->useCase->samlLogout(self::VALID_TOKEN); $tok = $this->tokens->filter()[0]; $this->assertTrue($tok->isInvalid()); } @@ -144,7 +146,7 @@ class SamlUseCaseTest extends UseCaseBaseTest { $samlUser = $this->createSamlUser(); $users = new InMemoryUsersRepositoryStub(); - $this->useCase = new Authenticate($users, $this->tokens, $this->saml); + $this->useCase = new Authenticate($users, $this->tokens, $this->saml, self::SALT); $this->expectException(UserCreationException::class); $this->handleLoginTest($samlUser); } @@ -167,7 +169,7 @@ class SamlUseCaseTest extends UseCaseBaseTest $this->assertLessThan(Carbon::now()->addHours(25), $token->getExpiresAt()); $this->assertGreaterThan(Carbon::now()->subHours(23), $token->getExpiresAt()); $this->assertEquals($user->getId(), $token->getUserId()); - $this->assertEquals(60, strlen($token->getTokenString())); + $this->assertNotNull($token->getToken()); $this->assertEquals($token, $this->response->getToken()); } diff --git a/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/UseCaseBaseTest.php b/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/UseCaseBaseTest.php index 7f4e8afa..d4292b95 100644 --- a/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/UseCaseBaseTest.php +++ b/src/backend/tests/Unit/Source/UseCases/Users/Authenticate/UseCaseBaseTest.php @@ -52,6 +52,11 @@ abstract class UseCaseBaseTest extends TestCase */ protected string $logoutUrl = 'logout url'; + + public const SALT = '93333'; + + public const VALID_TOKEN = '97055'; + public function setUp(): void { parent::setUp(); @@ -59,7 +64,7 @@ abstract class UseCaseBaseTest extends TestCase $this->users = new InMemoryUsersRepository(); $this->tokens = new InMemoryTokensRepository(); $this->saml = new InMemorySamlRepository($this->loginUrl, $this->logoutUrl); - $this->useCase = new Authenticate($this->users, $this->tokens, $this->saml); + $this->useCase = new Authenticate($this->users, $this->tokens, $this->saml, self::SALT); $this->presenter = new PresenterStub(); } } -- GitLab