diff --git a/app/V1Module/presenters/LoginPresenter.php b/app/V1Module/presenters/LoginPresenter.php index be10de65..b3448b46 100644 --- a/app/V1Module/presenters/LoginPresenter.php +++ b/app/V1Module/presenters/LoginPresenter.php @@ -94,13 +94,17 @@ class LoginPresenter extends BasePresenter /** * Sends response with an access token, if the user exists. * @param User $user + * @param int|null $expiration in seconds, null for default expiration * @throws AuthenticationException * @throws ForbiddenRequestException * @throws InvalidAccessTokenException */ - private function sendAccessTokenResponse(User $user) + private function sendAccessTokenResponse(User $user, ?int $expiration = null) { - $token = $this->accessManager->issueToken($user, null, [TokenScope::MASTER, TokenScope::REFRESH]); + if ($expiration !== null && ($expiration > $this->accessManager->getExpiration() || $expiration <= 0)) { + $expiration = null; // invalid values are ignored + } + $token = $this->accessManager->issueToken($user, null, [TokenScope::MASTER, TokenScope::REFRESH], $expiration); $this->getUser()->login(new Identity($user, $this->accessManager->decodeToken($token))); $this->sendSuccessResponse( @@ -121,11 +125,13 @@ private function sendAccessTokenResponse(User $user) */ #[Post("username", new VEmail(), "User's E-mail")] #[Post("password", new VString(1), "Password")] + #[Post("expiration", new VInt(), "Token expiration in seconds (not greater than the default)", required: false)] public function actionDefault() { $req = $this->getRequest(); $username = $req->getPost("username"); $password = $req->getPost("password"); + $expiration = $req->getPost("expiration"); $user = $this->credentialsAuthenticator->authenticate($username, $password); $this->verifyUserIpLock($user); @@ -135,7 +141,7 @@ public function actionDefault() $event = SecurityEvent::createLoginEvent($this->getHttpRequest()->getRemoteAddress(), $user); $this->securityEvents->persist($event); - $this->sendAccessTokenResponse($user); + $this->sendAccessTokenResponse($user, $expiration); } /** @@ -148,6 +154,7 @@ public function actionDefault() * @throws BadRequestException */ #[Post("token", new VString(1), "JWT external authentication token")] + #[Post("expiration", new VInt(), "Token expiration in seconds (not greater than the default)", required: false)] #[Path("authenticatorName", new VString(), "Identifier of the external authenticator", required: true)] public function actionExternal($authenticatorName) { @@ -160,7 +167,7 @@ public function actionExternal($authenticatorName) $event = SecurityEvent::createExternalLoginEvent($this->getHttpRequest()->getRemoteAddress(), $user); $this->securityEvents->persist($event); - $this->sendAccessTokenResponse($user); + $this->sendAccessTokenResponse($user, $req->getPost("expiration")); } public function checkTakeOver($userId) diff --git a/app/V1Module/presenters/UsersPresenter.php b/app/V1Module/presenters/UsersPresenter.php index f6f45894..ba925c95 100644 --- a/app/V1Module/presenters/UsersPresenter.php +++ b/app/V1Module/presenters/UsersPresenter.php @@ -831,6 +831,7 @@ public function actionInvalidateTokens(string $id) $this->sendSuccessResponse( [ + "user" => $this->userViewFactory->getUser($user), "accessToken" => $user === $this->getCurrentUser() ? $this->accessManager->issueRefreshedToken( $token ) : null diff --git a/tests/Presenters/LoginPresenter.phpt b/tests/Presenters/LoginPresenter.phpt index 30a735a4..c808a4a1 100644 --- a/tests/Presenters/LoginPresenter.phpt +++ b/tests/Presenters/LoginPresenter.phpt @@ -85,7 +85,8 @@ class TestLoginPresenter extends Tester\TestCase $events = $this->presenter->securityEvents->findAll(); Assert::count(0, $events); - $request = new Request( + $payload = PresenterTestHelper::performPresenterRequest( + $this->presenter, "V1:Login", "POST", ["action" => "default"], @@ -95,16 +96,47 @@ class TestLoginPresenter extends Tester\TestCase ] ); - /** @var JsonResponse $response */ - $response = $this->presenter->run($request); - Assert::type(JsonResponse::class, $response); - $result = $response->getPayload(); + $user = $this->presenter->users->getByEmail($this->userLogin); + Assert::same($user->getId(), $payload["user"]["id"]); + Assert::true($this->presenter->user->isLoggedIn()); - Assert::same(200, $result["code"]); - Assert::true(array_key_exists("accessToken", $result["payload"])); - Assert::same($this->presenter->users->getByEmail($this->userLogin)->getId(), $result["payload"]["user"]["id"]); + Assert::true(array_key_exists("accessToken", $payload)); + $token = $this->presenter->accessManager->decodeToken($payload["accessToken"]); + Assert::same($user->getId(), $token->getUserId()); + Assert::same($this->presenter->accessManager->getExpiration(), $token->getExpirationTime()); + + $events = $this->presenter->securityEvents->findAll(); + Assert::count(1, $events); + Assert::equal(SecurityEvent::TYPE_LOGIN, $events[0]->getType()); + Assert::equal($this->presenter->user->getId(), $events[0]->getUser()->getId()); + } + + public function testLoginRestrictedExpiration() + { + $events = $this->presenter->securityEvents->findAll(); + Assert::count(0, $events); + + $payload = PresenterTestHelper::performPresenterRequest( + $this->presenter, + "V1:Login", + "POST", + ["action" => "default"], + [ + "username" => $this->userLogin, + "password" => $this->userPassword, + "expiration" => 10, + ] + ); + + $user = $this->presenter->users->getByEmail($this->userLogin); + Assert::same($user->getId(), $payload["user"]["id"]); Assert::true($this->presenter->user->isLoggedIn()); + Assert::true(array_key_exists("accessToken", $payload)); + $token = $this->presenter->accessManager->decodeToken($payload["accessToken"]); + Assert::same($user->getId(), $token->getUserId()); + Assert::same(10, $token->getExpirationTime()); + $events = $this->presenter->securityEvents->findAll(); Assert::count(1, $events); Assert::equal(SecurityEvent::TYPE_LOGIN, $events[0]->getType()); @@ -171,10 +203,66 @@ class TestLoginPresenter extends Tester\TestCase $result = $response->getPayload(); Assert::same(200, $result["code"]); + Assert::equal($user->getId(), $result["payload"]["user"]["id"]); + Assert::true($this->presenter->user->isLoggedIn()); + Assert::true(array_key_exists("accessToken", $result["payload"])); + $token = $this->presenter->accessManager->decodeToken($result["payload"]["accessToken"]); + Assert::same($user->getId(), $token->getUserId()); + Assert::same($this->presenter->accessManager->getExpiration(), $token->getExpirationTime()); + + $events = $this->presenter->securityEvents->findAll(); + Assert::count(1, $events); + Assert::equal(SecurityEvent::TYPE_LOGIN_EXTERNAL, $events[0]->getType()); + Assert::equal($user->getId(), $events[0]->getUser()->getId()); + } + + public function testLoginExternalRestrictedExpiration() + { + $events = $this->presenter->securityEvents->findAll(); + Assert::count(0, $events); + + $authenticator = new ExternalServiceAuthenticator( + [[ + 'name' => 'test-cas', + 'jwtSecret' => 'tajnyRetezec', + ]], + $this->externalLogins, + $this->users, + $this->logins, + $this->instances, + $this->emailVerificationHelper, + $this->failureHelper + ); + + $user = $this->presenter->users->getByEmail($this->userLogin); + + $payload = [ + 'iat' => time(), + 'id' => 'external-id-1', + 'mail' => $this->userLogin, + 'firstName' => $user->getFirstName(), + 'lastName' => $user->getLastName(), + ]; + $token = JWT::encode($payload, 'tajnyRetezec', "HS256"); + + $this->presenter->externalServiceAuthenticator = $authenticator; + + $request = new Request("V1:Login", "POST", ["action" => "external", "authenticatorName" => "test-cas"], ['token' => $token, 'expiration' => 15]); + + $response = $this->presenter->run($request); + Assert::type(JsonResponse::class, $response); + $result = $response->getPayload(); + + Assert::same(200, $result["code"]); Assert::equal($user->getId(), $result["payload"]["user"]["id"]); Assert::true($this->presenter->user->isLoggedIn()); + Assert::true(array_key_exists("accessToken", $result["payload"])); + $token = $this->presenter->accessManager->decodeToken($result["payload"]["accessToken"]); + Assert::same($user->getId(), $token->getUserId()); + Assert::same(15, $token->getExpirationTime()); + $events = $this->presenter->securityEvents->findAll(); Assert::count(1, $events); Assert::equal(SecurityEvent::TYPE_LOGIN_EXTERNAL, $events[0]->getType());