From 46aaf46619cab70fa37ddf5c4530dfa61e613fc2 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:50:02 +0100 Subject: [PATCH 01/27] Add API route for retreiving auth token Switch the login process from the web to api controller --- docs/openapi.json | 83 +++++++++++++++++++ public/js/login.js | 36 ++++++++ settings/routes.php | 1 + src/Domain/User/Exception/InvalidTotpCode.php | 11 +++ src/Domain/User/Service/Authentication.php | 34 ++++++-- .../Api/AuthenticationController.php | 66 +++++++++++++++ .../Web/AuthenticationController.php | 1 + .../Web/LandingPageController.php | 17 ---- src/ValueObject/Http/Request.php | 14 +++- templates/page/login.html.twig | 66 +++++++-------- 10 files changed, 268 insertions(+), 61 deletions(-) create mode 100644 public/js/login.js create mode 100644 src/Domain/User/Exception/InvalidTotpCode.php create mode 100644 src/HttpController/Api/AuthenticationController.php diff --git a/docs/openapi.json b/docs/openapi.json index 83a6e182..9a277de7 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1065,6 +1065,89 @@ } } } + }, + "/authentication/request-token": { + "post": { + "description": "Get an authentication token by submitting an email, password and optionally a time-based 6-digit code. This token can be stored and used for future requests to protected endpoints.", + "parameters": [{ + "in": "header", + "name": "X-Movary-Client", + "schema": { + "type": "string" + }, + "required": true, + "example": "Client Name" + }], + "requestBody": { + "description": "The credentials and optionally a two-factor Authentication code", + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "required": ["email", "password"], + "properties": { + "email": { + "type": "string", + "example": "user@email.com", + "description": "An email address" + }, + "password": { + "type": "string", + "example": "mysecurepassword123", + "description": "A password" + }, + "totpCode": { + "type": "integer", + "pattern": "/^[0-9]{6}$/gm", + "example": "123456", + "description": "A 6-digit two-factor authentication code", + "nullable": true + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "The authentication token to be used in future requests." + } + } + } + } + } + }, + "401": { + "description": "An authentication error. Either missing credentials or the credentials were invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "The name of the error" + }, + "message": { + "type": "string", + "description": "The message of the error" + } + } + } + } + } + } + } + } } }, "components": { diff --git a/public/js/login.js b/public/js/login.js new file mode 100644 index 00000000..1ce090e5 --- /dev/null +++ b/public/js/login.js @@ -0,0 +1,36 @@ +const MOVARY_CLIENT_IDENTIFIER = 'Movary Web'; + +async function submitCredentials() { + const request = await fetch('/api/authentication/request-token', { + method: 'POST', + headers: { + 'Content-type': 'application/json', + 'X-Movary-Client': MOVARY_CLIENT_IDENTIFIER + }, + body: JSON.stringify({ + 'email': document.getElementById('email').value, + 'password': document.getElementById('password').value, + 'rememberMe': document.getElementById('rememberMe').checked, + 'totpCode': document.getElementById('totpCode').value + }) + }).then((response) => { + return response; + }); + + if(request.redirected === true) { + window.location.replace(request.url); + } else { + await request.json().then(error => { + if(error['error'] === 'NoVerificationCode') { + document.getElementById('LoginForm').classList.add('d-none'); + document.getElementById('TotpForm').classList.remove('d-none'); + } else if(error['error'] === 'InvalidTotpCode') { + addAlert('totpErrors', error['message'], 'danger', false); + } else { + addAlert('loginErrors', error['message'], 'danger', false); + } + }).catch(error => { + console.error(error); + }); + } +} \ No newline at end of file diff --git a/settings/routes.php b/settings/routes.php index 7c49d23d..745d4ae9 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -203,6 +203,7 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes = RouteList::create(); $routes->add('GET', '/openapi', [Api\OpenApiController::class, 'getSchema']); + $routes->add('POST', '/authentication/request-token', [Api\AuthenticationController::class, 'requestToken']); $routeUserHistory = '/users/{username:[a-zA-Z0-9]+}/history/movies'; $routes->add('GET', $routeUserHistory, [Api\HistoryController::class, 'getHistory'], [Api\Middleware\IsAuthorizedToReadUserData::class]); diff --git a/src/Domain/User/Exception/InvalidTotpCode.php b/src/Domain/User/Exception/InvalidTotpCode.php new file mode 100644 index 00000000..c355440b --- /dev/null +++ b/src/Domain/User/Exception/InvalidTotpCode.php @@ -0,0 +1,11 @@ +getId() === $userId; } - public function login(string $email, string $password, bool $rememberMe) : void + public function login(string $email, string $password, bool $rememberMe, ?int $userTotpInput = null) : void { if ($this->isUserAuthenticated() === true) { return; @@ -127,9 +131,12 @@ public function login(string $email, string $password, bool $rememberMe) : void $totpUri = $this->userApi->findTotpUri($user->getId()); if ($totpUri !== null) { - $this->sessionWrapper->set('totpUserId', $user->getId()); - $this->sessionWrapper->set('rememberMe', $rememberMe); - throw NoVerificationCode::create(); + if(empty($userTotpInput)) { + throw NoVerificationCode::create(); + } + if($this->verifyTotp($user->getId(), $userTotpInput) === false) { + throw InvalidTotpCode::create(); + } } $this->createAuthenticationCookie($user->getId(), $rememberMe); @@ -148,7 +155,8 @@ public function createAuthenticationCookie(int $userId, bool $rememberMe) : void $token = $this->generateToken($userId, DateTime::createFromString((string)$authTokenExpirationDate)); session_regenerate_id(); - setcookie(self::AUTHENTICATION_COOKIE_NAME, $token, $cookieExpiration); + setcookie(self::AUTHENTICATION_COOKIE_NAME, $token, $cookieExpiration, '/'); + // setcookie(self::AUTHENTICATION_COOKIE_NAME, $token, $cookieExpiration, '/', $this->serverSettings->getApplicationUrl(), true, true); $this->sessionWrapper->set('userId', $userId); } @@ -191,7 +199,7 @@ private function generateToken(int $userId, ?DateTime $expirationDate = null) : return $token; } - private function isValidToken(string $token) : bool + public function isValidToken(string $token) : bool { $tokenExpirationDate = $this->repository->findAuthTokenExpirationDate($token); @@ -205,4 +213,18 @@ private function isValidToken(string $token) : bool return true; } + + public function getToken() : ?string + { + return $_COOKIE[self::AUTHENTICATION_COOKIE_NAME]; + } + + + private function verifyTotp($userId, $userTotpInput) : bool + { + if ($this->twoFactorAuthenticationApi->verifyTotpUri($userId, (int)$userTotpInput) === false) { + return false; + } + return true; + } } diff --git a/src/HttpController/Api/AuthenticationController.php b/src/HttpController/Api/AuthenticationController.php new file mode 100644 index 00000000..ae1b125c --- /dev/null +++ b/src/HttpController/Api/AuthenticationController.php @@ -0,0 +1,66 @@ +getBody()); + $headers = $request->getHeaders(); + if($postParameters['email'] === null || $postParameters['password'] === null) { + return Response::createBadRequest('Username or password has not been provided'); + } + $totpCode = $postParameters['totpCode'] ?? 0; + + if(isset($headers['X-Movary-Client']) === false) { + return Response::createBadRequest(); + } + + $client = $headers['X-Movary-Client']; + + try { + $this->authenticationService->login($postParameters['email'], $postParameters['password'], false, (int)$totpCode); + + if($client === 'Movary Web') { + $redirect = $postParameters['redirect'] ?? null; + $target = $redirect ?? $_SERVER['HTTP_REFERER']; + + $urlParts = parse_url($target); + if (is_array($urlParts) === false) { + $urlParts = ['path' => '/']; + } + $query = $urlParts['query'] ?? ''; + + /* @phpstan-ignore-next-line */ + $targetRelativeUrl = $urlParts['path'] . $query; + + return Response::createSeeOther($targetRelativeUrl); + } + + return Response::createJson(Json::encode(['token' => $this->authenticationService->getToken()])); + } catch (InvalidCredentials $e) { + return Response::createBadRequest(Json::encode([ + 'error' => basename(str_replace('\\', '/', get_class($e))), + 'message' => $e->getMessage() + ])); + } + } +} \ No newline at end of file diff --git a/src/HttpController/Web/AuthenticationController.php b/src/HttpController/Web/AuthenticationController.php index faa2dd71..86977af9 100644 --- a/src/HttpController/Web/AuthenticationController.php +++ b/src/HttpController/Web/AuthenticationController.php @@ -4,6 +4,7 @@ use Movary\Domain\SessionService; use Movary\Domain\User\Exception\InvalidCredentials; +use Movary\Domain\User\Exception\InvalidTotpCode; use Movary\Domain\User\Exception\NoVerificationCode; use Movary\Domain\User\Service; use Movary\Util\SessionWrapper; diff --git a/src/HttpController/Web/LandingPageController.php b/src/HttpController/Web/LandingPageController.php index 63fb1842..d5ef5c6e 100644 --- a/src/HttpController/Web/LandingPageController.php +++ b/src/HttpController/Web/LandingPageController.php @@ -22,30 +22,13 @@ public function __construct( public function render() : Response { - $failedLogin = $this->sessionWrapper->has('failedLogin'); $deletedAccount = $this->sessionWrapper->has('deletedAccount'); - $invalidTotpCode = $this->sessionWrapper->has('invalidTotpCode'); - $useTwoFactorAuthentication = $this->sessionWrapper->has('useTwoFactorAuthentication'); - - $this->sessionWrapper->unset('failedLogin', 'deletedAccount', 'invalidTotpCode', 'useTwoFactorAuthentication'); - if ($invalidTotpCode === true) { - $useTwoFactorAuthentication = true; - } - - if ($useTwoFactorAuthentication === false) { - $this->sessionWrapper->unset('rememberMe'); - } return Response::create( StatusCode::createOk(), $this->twig->render('page/login.html.twig', [ - 'failedLogin' => $failedLogin, 'deletedAccount' => $deletedAccount, 'registrationEnabled' => $this->registrationEnabled, - 'defaultEmail' => $this->defaultEmail, - 'defaultPassword' => $this->defaultPassword, - 'useTwoFactorAuthentication' => $useTwoFactorAuthentication, - 'invalidTotpCode' => $invalidTotpCode, ]), ); } diff --git a/src/ValueObject/Http/Request.php b/src/ValueObject/Http/Request.php index b39ea9a8..75ec570d 100644 --- a/src/ValueObject/Http/Request.php +++ b/src/ValueObject/Http/Request.php @@ -15,6 +15,7 @@ private function __construct( private readonly string $body, private readonly array $filesParameters, private readonly array $headers, + private readonly string $userAgent ) { } @@ -27,10 +28,11 @@ public static function createFromGlobals() : self $postParameters = self::extractPostParameter(); $filesParameters = self::extractFilesParameter(); $headers = self::extractHeaders(); + $userAgent = self::extractUserAgent(); $body = (string)file_get_contents('php://input'); - return new self($path, $getParameters, $postParameters, $body, $filesParameters, $headers); + return new self($path, $getParameters, $postParameters, $body, $filesParameters, $headers, $userAgent); } private static function extractFilesParameter() : array @@ -87,6 +89,11 @@ private static function extractRequestUri() : string return self::getServerSetting('REQUEST_URI') ?? ''; } + private static function extractUserAgent() : string + { + return self::getServerSetting('HTTP_USER_AGENT') ?? ''; + } + private static function getServerSetting(string $key) : ?string { return empty($_SERVER[$key]) === false ? (string)$_SERVER[$key] : null; @@ -131,4 +138,9 @@ public function getRouteParameters() : array { return $this->routeParameters; } + + public function getUserAgent() : string + { + return $this->userAgent; + } } diff --git a/templates/page/login.html.twig b/templates/page/login.html.twig index 2b8ef1ab..ca86d15f 100644 --- a/templates/page/login.html.twig +++ b/templates/page/login.html.twig @@ -8,74 +8,66 @@ {% endblock %} +{% block scripts %} + +{% endblock %} + {% block body %} -
- {% if useTwoFactorAuthentication == false %} -
+
+

{{ applicationName ?? 'Movary' }}

- - + +
- - + +
+
+ {% if deletedAccount == true %} {% endif %} - {% if failedLogin == true %} - - {% else %} {% if redirect != false %} {% endif %} - {% endif %} - + {% if registrationEnabled == true %} Create new user {% endif %} - {% elseif useTwoFactorAuthentication == true %} -
+

Enter the 6 digit verification code from your authenticator app.

- {% if invalidTotpCode == true %} - - {% endif %} - + name="totpCode" + class="form-control form-control-lg text-{{ theme == 'dark' ? 'light' : 'dark' }}" + placeholder="Verification code" + maxlength="6" + autocomplete="off" + style="margin-bottom: .7rem;margin-top: .7rem" + id="totpCode" + required> +
+ + Back
- Back - {% endif %} - -
+
{% endblock %} From d4ee3ce6bcb7735708b66947b4a0b6d4a11c5c7e Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:58:13 +0100 Subject: [PATCH 02/27] Clean up --- src/Domain/User/Service/Authentication.php | 3 -- .../Web/AuthenticationController.php | 33 ------------------- templates/page/login.html.twig | 7 ++-- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/src/Domain/User/Service/Authentication.php b/src/Domain/User/Service/Authentication.php index f272f2bb..086fba52 100644 --- a/src/Domain/User/Service/Authentication.php +++ b/src/Domain/User/Service/Authentication.php @@ -9,7 +9,6 @@ use Movary\Domain\User\UserApi; use Movary\Domain\User\UserEntity; use Movary\Domain\User\UserRepository; -use Movary\Service\ServerSettings; use Movary\Util\SessionWrapper; use Movary\ValueObject\DateTime; use Movary\ValueObject\Http\Request; @@ -24,7 +23,6 @@ public function __construct( private readonly UserRepository $repository, private readonly UserApi $userApi, private readonly SessionWrapper $sessionWrapper, - private readonly ServerSettings $serverSettings, private readonly TwoFactorAuthenticationApi $twoFactorAuthenticationApi, ) { } @@ -156,7 +154,6 @@ public function createAuthenticationCookie(int $userId, bool $rememberMe) : void session_regenerate_id(); setcookie(self::AUTHENTICATION_COOKIE_NAME, $token, $cookieExpiration, '/'); - // setcookie(self::AUTHENTICATION_COOKIE_NAME, $token, $cookieExpiration, '/', $this->serverSettings->getApplicationUrl(), true, true); $this->sessionWrapper->set('userId', $userId); } diff --git a/src/HttpController/Web/AuthenticationController.php b/src/HttpController/Web/AuthenticationController.php index 86977af9..460fc2f4 100644 --- a/src/HttpController/Web/AuthenticationController.php +++ b/src/HttpController/Web/AuthenticationController.php @@ -23,39 +23,6 @@ public function __construct( ) { } - public function login(Request $request) : Response - { - $postParameters = $request->getPostParameters(); - - try { - $this->authenticationService->login( - $postParameters['email'], - $postParameters['password'], - isset($postParameters['rememberMe']) === true, - ); - } catch (NoVerificationCode) { - $this->sessionWrapper->set('useTwoFactorAuthentication', true); - } catch (InvalidCredentials) { - $this->sessionWrapper->set('failedLogin', true); - } - $redirect = $postParameters['redirect']; - $target = $redirect ?? $_SERVER['HTTP_REFERER']; - - $urlParts = parse_url($target); - if (is_array($urlParts) === false) { - $urlParts = ['path' => '/']; - } - - /* @phpstan-ignore-next-line */ - $targetRelativeUrl = $urlParts['path'] . $urlParts['query'] ?? ''; - - return Response::create( - StatusCode::createSeeOther(), - null, - [Header::createLocation($targetRelativeUrl)], - ); - } - public function logout() : Response { $this->authenticationService->logout(); diff --git a/templates/page/login.html.twig b/templates/page/login.html.twig index ca86d15f..ba58081a 100644 --- a/templates/page/login.html.twig +++ b/templates/page/login.html.twig @@ -20,19 +20,19 @@

{{ applicationName ?? 'Movary' }}

-
-
@@ -57,7 +57,6 @@

Enter the 6 digit verification code from your authenticator app.

Date: Fri, 26 Jan 2024 12:21:12 +0100 Subject: [PATCH 03/27] Add user agent string and device name to auth token table --- ...11_add_device_name_to_auth_token_table.php | 28 +++++++++++ ...33_add_device_name_to_auth_token_table.php | 48 +++++++++++++++++++ src/Domain/User/Service/Authentication.php | 12 ++--- src/Domain/User/UserRepository.php | 4 +- .../Api/AuthenticationController.php | 4 +- 5 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 db/migrations/mysql/20240126110011_add_device_name_to_auth_token_table.php create mode 100644 db/migrations/sqlite/20240126110433_add_device_name_to_auth_token_table.php diff --git a/db/migrations/mysql/20240126110011_add_device_name_to_auth_token_table.php b/db/migrations/mysql/20240126110011_add_device_name_to_auth_token_table.php new file mode 100644 index 00000000..fe237e99 --- /dev/null +++ b/db/migrations/mysql/20240126110011_add_device_name_to_auth_token_table.php @@ -0,0 +1,28 @@ +execute( + <<execute( + <<execute( + <<execute('INSERT INTO `user_api_token_old` (`user_id`, `token`, `created_at`) SELECT `user_id`, `token`, `created_at` FROM `user_api_token`'); + $this->execute('DROP TABLE `user_api_token`'); + $this->execute('ALTER TABLE `user_api_token_old` RENAME TO `user_api_token`'); + } + + public function up() : void + { + $this->execute( + <<execute('INSERT INTO `user_api_token_new` (`user_id`, `token`, `created_at`) SELECT * FROM `user_api_token`'); + $this->execute('DELETE `user_api_token`'); + $this->execute('ALTER TABLE `user_api_token_new` RENAME TO `user_api_token`'); + } +} diff --git a/src/Domain/User/Service/Authentication.php b/src/Domain/User/Service/Authentication.php index 086fba52..c5d09b68 100644 --- a/src/Domain/User/Service/Authentication.php +++ b/src/Domain/User/Service/Authentication.php @@ -110,7 +110,7 @@ public function isUserPageVisibleForApiRequest(Request $request, UserEntity $tar return $targetUser->getId() === $userId; } - public function login(string $email, string $password, bool $rememberMe, ?int $userTotpInput = null) : void + public function login(string $email, string $password, bool $rememberMe, string $deviceName, string $userAgent, ?int $userTotpInput = null) : void { if ($this->isUserAuthenticated() === true) { return; @@ -137,10 +137,10 @@ public function login(string $email, string $password, bool $rememberMe, ?int $u } } - $this->createAuthenticationCookie($user->getId(), $rememberMe); + $this->createAuthenticationCookie($user->getId(), $rememberMe, $deviceName, $userAgent); } - public function createAuthenticationCookie(int $userId, bool $rememberMe) : void + public function createAuthenticationCookie(int $userId, bool $rememberMe, string $deviceName, string $userAgent) : void { $authTokenExpirationDate = $this->createExpirationDate(); $cookieExpiration = 0; @@ -150,7 +150,7 @@ public function createAuthenticationCookie(int $userId, bool $rememberMe) : void $cookieExpiration = (int)$authTokenExpirationDate->format('U'); } - $token = $this->generateToken($userId, DateTime::createFromString((string)$authTokenExpirationDate)); + $token = $this->generateToken($userId, $deviceName, $userAgent, DateTime::createFromString((string)$authTokenExpirationDate)); session_regenerate_id(); setcookie(self::AUTHENTICATION_COOKIE_NAME, $token, $cookieExpiration, '/'); @@ -183,7 +183,7 @@ public function createExpirationDate(int $days = 1) : DateTime return DateTime::createFromString(date('Y-m-d H:i:s', $timestamp)); } - private function generateToken(int $userId, ?DateTime $expirationDate = null) : string + private function generateToken(int $userId, string $deviceName, string $userAgent, ?DateTime $expirationDate = null) : string { if ($expirationDate === null) { $expirationDate = $this->createExpirationDate(); @@ -191,7 +191,7 @@ private function generateToken(int $userId, ?DateTime $expirationDate = null) : $token = bin2hex(random_bytes(16)); - $this->repository->createAuthToken($userId, $token, $expirationDate); + $this->repository->createAuthToken($userId, $token, $deviceName, $userAgent, $expirationDate); return $token; } diff --git a/src/Domain/User/UserRepository.php b/src/Domain/User/UserRepository.php index 47ecc287..91217cbc 100644 --- a/src/Domain/User/UserRepository.php +++ b/src/Domain/User/UserRepository.php @@ -26,7 +26,7 @@ public function createApiToken(int $userId, string $token) : void ); } - public function createAuthToken(int $userId, string $token, DateTime $expirationDate) : void + public function createAuthToken(int $userId, string $token, string $deviceName, string $userAgent, DateTime $expirationDate) : void { $this->dbConnection->insert( 'user_auth_token', @@ -34,6 +34,8 @@ public function createAuthToken(int $userId, string $token, DateTime $expiration 'user_id' => $userId, 'token' => $token, 'expiration_date' => (string)$expirationDate, + 'device_name' => $deviceName, + 'user_agent_string' => $userAgent, 'created_at' => (string)DateTime::create(), ], ); diff --git a/src/HttpController/Api/AuthenticationController.php b/src/HttpController/Api/AuthenticationController.php index ae1b125c..5dae67e0 100644 --- a/src/HttpController/Api/AuthenticationController.php +++ b/src/HttpController/Api/AuthenticationController.php @@ -29,6 +29,8 @@ public function requestToken(Request $request) : Response return Response::createBadRequest('Username or password has not been provided'); } $totpCode = $postParameters['totpCode'] ?? 0; + $rememberMe = (bool)$postParameters['rememberMe'] ?? false; + $userAgent = $request->getUserAgent(); if(isset($headers['X-Movary-Client']) === false) { return Response::createBadRequest(); @@ -37,7 +39,7 @@ public function requestToken(Request $request) : Response $client = $headers['X-Movary-Client']; try { - $this->authenticationService->login($postParameters['email'], $postParameters['password'], false, (int)$totpCode); + $this->authenticationService->login($postParameters['email'], $postParameters['password'], $rememberMe, $client, $userAgent, (int)$totpCode); if($client === 'Movary Web') { $redirect = $postParameters['redirect'] ?? null; From 7d3301a26b7235220a251443a64c3a5a4ab6630f Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:31:01 +0100 Subject: [PATCH 04/27] Fix migration for sqlite --- ...33_add_device_name_to_auth_token_table.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/db/migrations/sqlite/20240126110433_add_device_name_to_auth_token_table.php b/db/migrations/sqlite/20240126110433_add_device_name_to_auth_token_table.php index f2e35885..167ce526 100644 --- a/db/migrations/sqlite/20240126110433_add_device_name_to_auth_token_table.php +++ b/db/migrations/sqlite/20240126110433_add_device_name_to_auth_token_table.php @@ -10,39 +10,41 @@ public function down() : void { $this->execute( <<execute('INSERT INTO `user_api_token_old` (`user_id`, `token`, `created_at`) SELECT `user_id`, `token`, `created_at` FROM `user_api_token`'); - $this->execute('DROP TABLE `user_api_token`'); - $this->execute('ALTER TABLE `user_api_token_old` RENAME TO `user_api_token`'); + $this->execute('INSERT INTO `user_auth_token_old` (`id`, `user_id`, `token`, `expiration_date`, `created_at`) SELECT `id`, `user_id`, `token`, `expiration_date`, `created_at` FROM `user_auth_token`'); + $this->execute('DROP TABLE `user_auth_token`'); + $this->execute('ALTER TABLE `user_auth_token_old` RENAME TO `user_auth_token`'); } public function up() : void { $this->execute( <<execute('INSERT INTO `user_api_token_new` (`user_id`, `token`, `created_at`) SELECT * FROM `user_api_token`'); - $this->execute('DELETE `user_api_token`'); - $this->execute('ALTER TABLE `user_api_token_new` RENAME TO `user_api_token`'); + $this->execute('INSERT INTO `user_auth_token_new` (`id`, `user_id`, `token`, `expiration_date`, `created_at`) SELECT * FROM `user_auth_token`'); + $this->execute('DROP TABLE `user_auth_token`'); + $this->execute('ALTER TABLE `user_auth_token_new` RENAME TO `user_auth_token`'); } } From ac93a309bf956e0c07fca30760c95a2ce4f8fc28 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:46:39 +0100 Subject: [PATCH 05/27] Fix tests --- settings/routes.php | 1 - src/Domain/User/Service/Authentication.php | 4 +-- .../Api/AuthenticationController.php | 8 +----- .../Web/CreateUserController.php | 5 +++- .../Web/LandingPageController.php | 2 ++ .../Web/TwoFactorAuthenticationController.php | 25 ------------------- 6 files changed, 9 insertions(+), 36 deletions(-) diff --git a/settings/routes.php b/settings/routes.php index 745d4ae9..f2424f85 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -20,7 +20,6 @@ function addWebRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/', [Web\LandingPageController::class, 'render'], [Web\Middleware\UserIsUnauthenticated::class, Web\Middleware\ServerHasNoUsers::class]); $routes->add('GET', '/login', [Web\AuthenticationController::class, 'renderLoginPage'], [Web\Middleware\UserIsUnauthenticated::class]); $routes->add('POST', '/login', [Web\AuthenticationController::class, 'login']); - $routes->add('POST', '/verify-totp', [Web\TwoFactorAuthenticationController::class, 'verifyTotp'], [Web\Middleware\UserIsUnauthenticated::class]); $routes->add('GET', '/logout', [Web\AuthenticationController::class, 'logout']); $routes->add('POST', '/create-user', [Web\CreateUserController::class, 'createUser'], [ Web\Middleware\UserIsUnauthenticated::class, diff --git a/src/Domain/User/Service/Authentication.php b/src/Domain/User/Service/Authentication.php index c5d09b68..f01371cd 100644 --- a/src/Domain/User/Service/Authentication.php +++ b/src/Domain/User/Service/Authentication.php @@ -217,9 +217,9 @@ public function getToken() : ?string } - private function verifyTotp($userId, $userTotpInput) : bool + private function verifyTotp(int $userId, int $userTotpInput) : bool { - if ($this->twoFactorAuthenticationApi->verifyTotpUri($userId, (int)$userTotpInput) === false) { + if ($this->twoFactorAuthenticationApi->verifyTotpUri($userId, $userTotpInput) === false) { return false; } return true; diff --git a/src/HttpController/Api/AuthenticationController.php b/src/HttpController/Api/AuthenticationController.php index 5dae67e0..3724e1e6 100644 --- a/src/HttpController/Api/AuthenticationController.php +++ b/src/HttpController/Api/AuthenticationController.php @@ -3,12 +3,8 @@ namespace Movary\HttpController\Api; use Movary\Domain\User\Exception\InvalidCredentials; -use Movary\Domain\User\Exception\InvalidTotpCode; -use Movary\Domain\User\Exception\NoVerificationCode; use Movary\Domain\User\Service\Authentication; -use Movary\Domain\User\Service\TwoFactorAuthenticationApi; use Movary\Util\Json; -use Movary\Util\SessionWrapper; use Movary\ValueObject\Http\Request; use Movary\ValueObject\Http\Response; @@ -16,8 +12,6 @@ class AuthenticationController { public function __construct( private readonly Authentication $authenticationService, - private readonly TwoFactorAuthenticationApi $twoFactorAuthenticationApi, - private readonly SessionWrapper $sessionWrapper, ) { } @@ -29,7 +23,7 @@ public function requestToken(Request $request) : Response return Response::createBadRequest('Username or password has not been provided'); } $totpCode = $postParameters['totpCode'] ?? 0; - $rememberMe = (bool)$postParameters['rememberMe'] ?? false; + $rememberMe = (bool)$postParameters['rememberMe']; $userAgent = $request->getUserAgent(); if(isset($headers['X-Movary-Client']) === false) { diff --git a/src/HttpController/Web/CreateUserController.php b/src/HttpController/Web/CreateUserController.php index 307afe59..df7f24cd 100644 --- a/src/HttpController/Web/CreateUserController.php +++ b/src/HttpController/Web/CreateUserController.php @@ -18,6 +18,8 @@ class CreateUserController { + private const MOVARY_WEB_CLIENT = 'Movary Web'; + public function __construct( private readonly Environment $twig, private readonly Authentication $authenticationService, @@ -32,6 +34,7 @@ public function createUser(Request $request) : Response $hasUsers = $this->userApi->hasUsers(); $postParameters = $request->getPostParameters(); + $userAgent = $request->getUserAgent(); $email = empty($postParameters['email']) === true ? null : (string)$postParameters['email']; $name = empty($postParameters['name']) === true ? null : (string)$postParameters['name']; $password = empty($postParameters['password']) === true ? null : (string)$postParameters['password']; @@ -60,7 +63,7 @@ public function createUser(Request $request) : Response try { $this->userApi->createUser($email, $password, $name, $hasUsers === false); - $this->authenticationService->login($email, $password, false); + $this->authenticationService->login($email, $password, false, self::MOVARY_WEB_CLIENT, $userAgent); } catch (PasswordTooShort) { $this->sessionWrapper->set('errorPasswordTooShort', true); } catch (UsernameInvalidFormat) { diff --git a/src/HttpController/Web/LandingPageController.php b/src/HttpController/Web/LandingPageController.php index d5ef5c6e..f64ab0ef 100644 --- a/src/HttpController/Web/LandingPageController.php +++ b/src/HttpController/Web/LandingPageController.php @@ -29,6 +29,8 @@ public function render() : Response $this->twig->render('page/login.html.twig', [ 'deletedAccount' => $deletedAccount, 'registrationEnabled' => $this->registrationEnabled, + 'defaultEmail' => $this->defaultEmail, + 'defaultPassword' => $this->defaultPassword ]), ); } diff --git a/src/HttpController/Web/TwoFactorAuthenticationController.php b/src/HttpController/Web/TwoFactorAuthenticationController.php index 0fa6a0ad..a563e5b5 100644 --- a/src/HttpController/Web/TwoFactorAuthenticationController.php +++ b/src/HttpController/Web/TwoFactorAuthenticationController.php @@ -61,29 +61,4 @@ public function enableTOTP(Request $request) : Response return Response::createOk(); } - - public function verifyTotp(Request $request) : Response - { - $userTotpInput = $request->getPostParameters()['totpCode']; - $rememberMe = $this->sessionWrapper->find('rememberMe') ?? false; - $userId = (int)$this->sessionWrapper->find('totpUserId'); - - if ($this->twoFactorAuthenticationApi->verifyTotpUri($userId, (int)$userTotpInput) === false) { - $this->sessionWrapper->set('invalidTotpCode', true); - - return Response::create( - StatusCode::createSeeOther(), - null, - [Header::createLocation($_SERVER['HTTP_REFERER'])], - ); - } - - $this->authenticationService->createAuthenticationCookie($userId, $rememberMe); - - return Response::create( - StatusCode::createSeeOther(), - null, - [Header::createLocation($_SERVER['HTTP_REFERER'])], - ); - } } From d87f8ca1b262d65c6b88417b76f0bc3e08f65176 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:54:13 +0100 Subject: [PATCH 06/27] Fix openapi spec --- docs/openapi.json | 3 +++ src/HttpController/Web/OpenApiController.php | 6 +++--- templates/page/api.html.twig | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 9a277de7..5242d27a 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1068,6 +1068,9 @@ }, "/authentication/request-token": { "post": { + "tags": [ + "Authentication" + ], "description": "Get an authentication token by submitting an email, password and optionally a time-based 6-digit code. This token can be stored and used for future requests to protected endpoints.", "parameters": [{ "in": "header", diff --git a/src/HttpController/Web/OpenApiController.php b/src/HttpController/Web/OpenApiController.php index 0507f799..0aa7be99 100644 --- a/src/HttpController/Web/OpenApiController.php +++ b/src/HttpController/Web/OpenApiController.php @@ -17,17 +17,17 @@ public function __construct( public function renderPage() : Response { - $openAiJsonUrl = '/api/openapi'; + $openApiJsonUrl = '/api/openapi'; $applicationUrl = $this->serverSettings->getApplicationUrl(); if ($applicationUrl !== null) { - $openAiJsonUrl = trim($applicationUrl, '/') . $openAiJsonUrl; + $openApiJsonUrl = trim($applicationUrl, '/') . $openApiJsonUrl; } return Response::create( StatusCode::createOk(), $this->twig->render('page/api.html.twig', [ - 'openAiJsonUrl' => $openAiJsonUrl + 'openApiJsonUrl' => $openApiJsonUrl ]), ); } diff --git a/templates/page/api.html.twig b/templates/page/api.html.twig index 93384f59..05835e0b 100644 --- a/templates/page/api.html.twig +++ b/templates/page/api.html.twig @@ -12,7 +12,7 @@
- +

{{ applicationName ?? 'Movary' }}

+ placeholder="name@example.com" onkeydown="submitCredentialsOnEnter(event)" required>
+ placeholder="Password" onkeydown="submitCredentialsOnEnter(event)" required>
@@ -53,8 +53,8 @@ {% if registrationEnabled == true %} Create new user {% endif %} - -
+
+

Enter the 6 digit verification code from your authenticator app.

Back - +
{% endblock %} From 33cc5b1f1f2ce085c2b25990f2923da30f5ffb1c Mon Sep 17 00:00:00 2001 From: Lee Peuker Date: Sat, 24 Feb 2024 15:22:19 +0100 Subject: [PATCH 25/27] Rename 'create-token' endpoint to only 'token' --- docs/openapi.json | 2 +- public/js/login.js | 2 +- settings/routes.php | 2 +- tests/rest/api/authentication.http | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 2002a9cd..3ddd4746 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1066,7 +1066,7 @@ } } }, - "/authentication/create-token": { + "/authentication/token": { "post": { "tags": [ "Authentication" diff --git a/public/js/login.js b/public/js/login.js index 03f24a8b..8a2e8a94 100644 --- a/public/js/login.js +++ b/public/js/login.js @@ -42,7 +42,7 @@ async function submitCredentials() { } function loginRequest() { - return fetch('/api/authentication/create-token', { + return fetch('/api/authentication/token', { method: 'POST', headers: { 'Content-type': 'application/json', diff --git a/settings/routes.php b/settings/routes.php index a39d9801..6fcac1c1 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -202,7 +202,7 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes = RouteList::create(); $routes->add('GET', '/openapi', [Api\OpenApiController::class, 'getSchema']); - $routes->add('POST', '/authentication/create-token', [Api\AuthenticationController::class, 'createToken']); + $routes->add('POST', '/authentication/token', [Api\AuthenticationController::class, 'createToken']); $routeUserHistory = '/users/{username:[a-zA-Z0-9]+}/history/movies'; $routes->add('GET', $routeUserHistory, [Api\HistoryController::class, 'getHistory'], [Api\Middleware\IsAuthorizedToReadUserData::class]); diff --git a/tests/rest/api/authentication.http b/tests/rest/api/authentication.http index e7cc6cc4..4431792f 100644 --- a/tests/rest/api/authentication.http +++ b/tests/rest/api/authentication.http @@ -1,4 +1,4 @@ -POST http://127.0.0.1/api/authentication/create-token +POST http://127.0.0.1/api/authentication/token Accept: */* Cache-Control: no-cache Content-Type: application/json From 5752eb55e3278b4cd6706df00e012dc0f6584f0f Mon Sep 17 00:00:00 2001 From: Lee Peuker Date: Sat, 24 Feb 2024 15:26:09 +0100 Subject: [PATCH 26/27] Adjust securitySchemes names --- docs/openapi.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 3ddd4746..587233b1 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1370,12 +1370,12 @@ } }, "securitySchemes": { - "authToken": { + "token": { "type": "apiKey", "name": "X-Auth-Token", "in": "header" }, - "cookieauth": { + "cookie": { "type": "apiKey", "name": "id", "in": "cookie" From 325a6442dabe33ea8b385a97ad8d633fb800f6c5 Mon Sep 17 00:00:00 2001 From: Lee Peuker Date: Sat, 24 Feb 2024 15:28:16 +0100 Subject: [PATCH 27/27] Fix openapi naming --- docs/openapi.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 587233b1..964a35b6 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -111,7 +111,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] }, @@ -174,7 +174,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] }, @@ -237,7 +237,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] }, @@ -291,7 +291,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] } @@ -427,7 +427,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] }, @@ -477,7 +477,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] }, @@ -527,7 +527,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] } @@ -677,7 +677,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] }, @@ -745,7 +745,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] }, @@ -813,7 +813,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] }, @@ -871,7 +871,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] } @@ -980,7 +980,7 @@ }, "security": [ { - "authToken": [] + "token": [] } ] }