From f024598ee980df2227883fd161aa6799ae4811d1 Mon Sep 17 00:00:00 2001 From: Simon Asika Date: Tue, 26 Mar 2024 01:57:03 +0800 Subject: [PATCH] async password changed --- etc/app/main.php | 3 ++- routes/api/v1/public/auth.route.php | 3 +++ src/Enum/ApiTokenType.php | 4 +++ src/Enum/ErrorCode.php | 5 +++- src/Middleware/ApiAuthMiddleware.php | 30 ++++++++++++++++------ src/Module/Api/AuthController.php | 11 +++++---- src/Service/JwtAuthService.php | 37 +++++++++++++++++----------- 7 files changed, 63 insertions(+), 30 deletions(-) diff --git a/etc/app/main.php b/etc/app/main.php index d201743..e61e3c0 100644 --- a/etc/app/main.php +++ b/etc/app/main.php @@ -17,7 +17,8 @@ return $cors->allowHeaders( [ 'Authorization', - 'Content-Type' + 'Content-Type', + 'X-Password-Last-Reset' ] ) ->allowMethods('*'); diff --git a/routes/api/v1/public/auth.route.php b/routes/api/v1/public/auth.route.php index 91b1b53..e46ced2 100644 --- a/routes/api/v1/public/auth.route.php +++ b/routes/api/v1/public/auth.route.php @@ -24,4 +24,7 @@ $router->any('/refreshToken') ->handler('refreshToken'); + + $router->any('/me') + ->handler('me'); }); diff --git a/src/Enum/ApiTokenType.php b/src/Enum/ApiTokenType.php index ddc5674..a6f7917 100644 --- a/src/Enum/ApiTokenType.php +++ b/src/Enum/ApiTokenType.php @@ -4,6 +4,7 @@ namespace App\Enum; +use Windwalker\Utilities\Attributes\Enum\Title; use Windwalker\Utilities\Enum\EnumTranslatableInterface; use Windwalker\Utilities\Enum\EnumTranslatableTrait; use Windwalker\Utilities\Contract\LanguageInterface; @@ -12,7 +13,10 @@ enum ApiTokenType: string implements EnumTranslatableInterface { use EnumTranslatableTrait; + #[Title('Access Token')] case ACCESS = 'access'; + + #[Title('Refresh Token')] case REFRESH = 'refresh'; public function trans(LanguageInterface $lang, ...$args): string diff --git a/src/Enum/ErrorCode.php b/src/Enum/ErrorCode.php index 7224cf9..d61ade3 100644 --- a/src/Enum/ErrorCode.php +++ b/src/Enum/ErrorCode.php @@ -24,7 +24,10 @@ enum ErrorCode: int #[Title('Invalid Session.')] case INVALID_SESSION = 40104; + #[Title('Password changed.')] + case PASSWORD_CHANGED = 40105; + // 403 #[Title('This email has been used.')] - case USER_EMAIL_EXISTS = 40303; + case USER_EMAIL_EXISTS = 40301; } diff --git a/src/Middleware/ApiAuthMiddleware.php b/src/Middleware/ApiAuthMiddleware.php index 98cd1dd..222234a 100644 --- a/src/Middleware/ApiAuthMiddleware.php +++ b/src/Middleware/ApiAuthMiddleware.php @@ -4,7 +4,9 @@ namespace App\Middleware; +use App\Entity\User; use App\Enum\ApiTokenType; +use App\Enum\ErrorCode; use App\Service\ApiUserService; use App\Service\JwtAuthService; use Lyrasoft\Luna\User\UserService; @@ -29,18 +31,30 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $authHeader = $request->getHeaderLine('Authorization'); if ($authHeader) { - $payload = $this->jwtAuthService->extractAccessTokenFromHeader($authHeader, $user); + $this->jwtAuthService->extractAccessTokenFromHeader($authHeader, $user); - // If not access token, let's ignore this token - if ($payload->getType() === ApiTokenType::ACCESS) { - if (!$user) { - throw new UnauthorizedException('User not found.'); - } + $this->checkLastReset($request, $user); - $this->userService->setCurrentUser($user); - } + $this->userService->setCurrentUser($user); } return $handler->handle($request); } + + /** + * @param ServerRequestInterface $request + * @param User|null $user + * + * @return void + */ + protected function checkLastReset(ServerRequestInterface $request, ?User $user): void + { + $clientLastReset = $request->getHeaderLine('X-Password-Last-Reset'); + + $serverLastReset = (string) $user->getLastReset()?->toUnix(); + + if ($clientLastReset !== $serverLastReset) { + ErrorCode::PASSWORD_CHANGED->throw(); + } + } } diff --git a/src/Module/Api/AuthController.php b/src/Module/Api/AuthController.php index 6cc1c05..9e76671 100644 --- a/src/Module/Api/AuthController.php +++ b/src/Module/Api/AuthController.php @@ -260,11 +260,7 @@ public function refreshToken( ): array { $authHeader = $app->getHeader('Authorization'); - $payload = $jwtAuthService->extractAccessTokenFromHeader($authHeader, $user); - - if ($payload->getType() !== ApiTokenType::REFRESH) { - throw new \RuntimeException('Token is not refresh token', 400); - } + $payload = $jwtAuthService->extractAccessTokenFromHeader($authHeader, $user, ApiTokenType::REFRESH); $exp = $payload->getExp(); @@ -279,4 +275,9 @@ public function refreshToken( return compact('accessToken', 'refreshToken'); } + + public function me(\CurrentUser $currentUser): \CurrentUser + { + return $currentUser; + } } diff --git a/src/Service/JwtAuthService.php b/src/Service/JwtAuthService.php index 7608d3d..77cbf31 100644 --- a/src/Service/JwtAuthService.php +++ b/src/Service/JwtAuthService.php @@ -7,15 +7,12 @@ use App\Data\ApiTokenPayload; use App\Entity\User; use App\Entity\UserSecret; +use App\Enum\ApiTokenType; use App\Enum\ErrorCode; use Firebase\JWT\ExpiredException; use Firebase\JWT\JWT; use Firebase\JWT\Key; -use JetBrains\PhpStorm\ArrayShape; use Windwalker\Core\DateTime\Chronos; -use Windwalker\Core\Security\Exception\UnauthorizedException; -use Windwalker\Crypt\Hasher\PasswordHasher; -use Windwalker\Crypt\SecretToolkit; use Windwalker\DI\Attributes\Service; use Windwalker\ORM\ORM; use Windwalker\Utilities\TypeCast; @@ -41,7 +38,7 @@ public function createAccessToken(User $user, UserSecret $userSecret): string 'exp' => $now->modify('+7days')->toUnix(), 'email' => $user->getEmail(), 'id' => $user->getId(), - 'type' => 'access' + 'type' => 'access', ]; return JWT::encode( @@ -62,7 +59,7 @@ public function createRefreshToken(User $user, UserSecret $userSecret): string 'exp' => $now->modify('+6month')->toUnix(), 'email' => $user->getEmail(), 'id' => $user->getId(), - 'type' => 'refresh' + 'type' => 'refresh', ]; return JWT::encode( @@ -72,19 +69,25 @@ public function createRefreshToken(User $user, UserSecret $userSecret): string ); } - public function extractAccessTokenFromHeader(string $authorization, ?User &$user = null): ApiTokenPayload - { + public function extractAccessTokenFromHeader( + string $authorization, + ?User &$user = null, + ApiTokenType $type = ApiTokenType::ACCESS + ): ApiTokenPayload { sscanf($authorization, 'Bearer %s', $token); if (!$token) { throw new \RuntimeException('Token is empty.', 400); } - return $this->extractAccessToken((string) $token, $user); + return $this->extractAccessToken((string) $token, $user, $type); } - public function extractAccessToken(string $token, ?User &$user = null): ApiTokenPayload - { + public function extractAccessToken( + string $token, + ?User &$user = null, + ApiTokenType $type = ApiTokenType::ACCESS + ): ApiTokenPayload { $parts = explode('.', $token); if (!isset($parts[1])) { @@ -118,7 +121,13 @@ public function extractAccessToken(string $token, ?User &$user = null): ApiToken throw new \RuntimeException('Invalid Payload', 400); } - $issuedAt = Chronos::createFromFormat('U', (string) $payload->iat); + $payload = ApiTokenPayload::wrap(TypeCast::toArray($payload, true)); + + if ($payload->getType() !== $type) { + throw new \RuntimeException('Token type is not ' . $type->getTitle(), 400); + } + + $issuedAt = Chronos::createFromFormat('U', (string) $payload->getIat()); if ($issuedAt < $user->getSessValidForm()) { $user = null; @@ -128,9 +137,7 @@ public function extractAccessToken(string $token, ?User &$user = null): ApiToken throw $ex; } - return ApiTokenPayload::wrap( - TypeCast::toArray($payload, true) - ); + return $payload; } public static function getIssuer(): string