diff --git a/public/static/js/account/account.js b/public/static/js/account/account.js new file mode 100644 index 0000000..d0bc3f6 --- /dev/null +++ b/public/static/js/account/account.js @@ -0,0 +1,69 @@ +var Account = { + original: null, + countdown: null, + + openGoogleAuthenticate: function () { + window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600') + }, + + authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) { + var password = document.getElementsByTagName('form')[0].elements.password; + var button = document.getElementById('authenticateWithGoogleButton'); + + Account.original = { + type: password.type, + placeholder: password.placeholder, + required: password.required, + disabled: password.disabled + }; + + password.type = 'text'; + password.placeholder = 'Authenticated!' + password.value = ''; + password.required = false; + password.disabled = true; + + button.disabled = true; + + Account.countdown = setInterval(function () { + var timeLeft = Math.ceil((authenticatedWithGoogleUntil.getTime() - new Date().getTime()) / 1000); + + if (timeLeft > 30) { + return; + } + + if (timeLeft <= 0) { + Account.resetGoogleAuthentication(); + return; + } + + password.placeholder = 'Authenticated! ' + timeLeft + ' seconds left...'; + }, 1000); + }, + + resetGoogleAuthentication: function () { + if (Account.countdown !== null) { + clearInterval(Account.countdown); + } + + var password = document.getElementsByTagName('form')[0].elements.password; + var button = document.getElementById('authenticateWithGoogleButton'); + + password.type = Account.original.type; + password.placeholder = Account.original.placeholder + password.required = Account.original.required; + password.disabled = Account.original.disabled; + + button.disabled = false; + } +}; + +(function () { + document.getElementById('authenticateWithGoogleButton').onclick = function () { + Account.openGoogleAuthenticate(); + }; + + document.getElementsByTagName('form')[0].onreset = function () { + Account.resetGoogleAuthentication(); + }; +})(); diff --git a/public/static/js/account/google_authenticate.js b/public/static/js/account/google_authenticate.js new file mode 100644 index 0000000..aff35b2 --- /dev/null +++ b/public/static/js/account/google_authenticate.js @@ -0,0 +1,10 @@ +(function () { + if (success) { + window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil); + window.close(); + } else { + document.getElementById('closeWindowButton').onclick = function () { + window.close(); + } + } +})(); diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 06d3d35..25d14e9 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -1,16 +1,22 @@ request->user(); + + $state = bin2hex(random_bytes(16)); + + $this->request->session()->set('oauth_state', $state); + + $oAuth = new GoogleOAuth(new Request()); + + $url = $oAuth->getDialogUrl( + $state, + $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(), + $user->getEmail() + ); + + return new Redirect($url, IRedirect::TEMPORARY); + } + + public function authenticateWithGoogle(): IContent + { + /** + * @var User $user + */ + $user = $this->request->user(); + + if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) { + $data = ['success' => false]; + return new HtmlContent('account/google_authenticate', $data); + } + + $oAuth = new GoogleOAuth(new Request()); + $tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()); + + if (!isset($tokenData['id_token'])) { + $data = ['success' => false]; + return new HtmlContent('account/google_authenticate', $data); + } + + $jwtParser = new JwtParser($tokenData['id_token']); + $userData = $jwtParser->getPayload(); + + if ($userData['sub'] !== $user->getGoogleSub()) { + $data = ['success' => false, 'errorText' => 'This Google account is not linked to your account.']; + return new HtmlContent('account/google_authenticate', $data); + } + + $authenticatedWithGoogleUntil = new DateTime('+45 seconds'); + $this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil); + + $data = ['success' => true, 'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil]; + return new HtmlContent('account/google_authenticate', $data); + } + public function getDeleteAccount(): IContent { /** @@ -63,8 +126,13 @@ class UserController implements ISecured */ $user = $this->request->user(); - if (!$user->checkPassword($this->request->post('password'))) { - $data = ['error' => ['errorText' => 'The given current password is wrong.']]; + if (!$this->confirmUserIdentity( + $user, + $this->request->session()->get('authenticated_with_google_until'), + $this->request->post('password'), + $error + )) { + $data = ['error' => ['errorText' => $error]]; return new JsonContent($data); } @@ -84,6 +152,8 @@ class UserController implements ISecured $this->pdm->saveToDb($user); + $this->request->session()->delete('authenticated_with_google_until'); + $data = ['success' => true]; return new JsonContent($data); } @@ -95,8 +165,13 @@ class UserController implements ISecured */ $user = $this->request->user(); - if (!$user->checkPassword($this->request->post('password'))) { - $data = ['error' => ['errorText' => 'The given current password is wrong.']]; + if (!$this->confirmUserIdentity( + $user, + $this->request->session()->get('authenticated_with_google_until'), + $this->request->post('password'), + $error + )) { + $data = ['error' => ['errorText' => $error]]; return new JsonContent($data); } @@ -110,7 +185,28 @@ class UserController implements ISecured \Container::$dbConnection->commit(); + $this->request->session()->delete('authenticated_with_google_until'); + $data = ['success' => true]; return new JsonContent($data); } + + private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, &$error): bool + { + if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) { + return true; + } + + if ($password !== null) { + if ($user->checkPassword($password)) { + return true; + } + + $error = 'The given current password is wrong.'; + return false; + } + + $error = 'Could not confirm your identity. Please try again!'; + return false; + } } diff --git a/views/account/account.php b/views/account/account.php index f6186b6..f1452f6 100644 --- a/views/account/account.php +++ b/views/account/account.php @@ -1,10 +1,27 @@ +@js('js/account/account.js') + @extends('templates/layout_normal') @section('main')

Account

- + +

Please confirm your identity with your password or with Google to modify your account.

+
+ +
+ +

Please confirm your identity with your password to modify your account.

+ + +

Please confirm your identity with Google to modify your account.

+
+ +
+
diff --git a/views/account/delete.php b/views/account/delete.php index 1b190e7..da22d90 100644 --- a/views/account/delete.php +++ b/views/account/delete.php @@ -1,14 +1,32 @@ +@js('js/account/account.js') + @extends('templates/layout_normal') @section('main')

Delete account

-

Are you sure you want to delete your account? This cannot be undone!

- +

Are you sure you want to delete your account? This cannot be undone!

+ +

Please confirm your identity with your password or with Google to delete your account.

+
+ +
+ +

Please confirm your identity with your password to delete your account.

+ + +

Please confirm your identity with Google to delete your account.

+
+ +
+

- + Cancel
diff --git a/views/account/google_authenticate.php b/views/account/google_authenticate.php new file mode 100644 index 0000000..12f889f --- /dev/null +++ b/views/account/google_authenticate.php @@ -0,0 +1,28 @@ +@js('js/account/google_authenticate.js') + +@extends('templates/layout_minimal') + +@section('main') +

Authenticate with Google

+ +
+

+ + + + Authentication with Google failed. + + Please close this window/tab and try again! +

+
+ +@endsection + +@section('pageScript') + +@endsection diff --git a/web.php b/web.php index 5eaffae..6df0859 100644 --- a/web.php +++ b/web.php @@ -37,6 +37,8 @@ Container::$routeCollection->group('account', function (MapGuesser\Routing\Route $routeCollection->post('account-action', '', [MapGuesser\Controller\UserController::class, 'saveAccount']); $routeCollection->get('account.delete', 'delete', [MapGuesser\Controller\UserController::class, 'getDeleteAccount']); $routeCollection->post('account.delete-action', 'delete', [MapGuesser\Controller\UserController::class, 'deleteAccount']); + $routeCollection->get('account.googleAuthenticate', 'googleAuthenticate', [MapGuesser\Controller\UserController::class, 'getGoogleAuthenticateRedirect']); + $routeCollection->get('account.googleAuthenticate-action', 'googleAuthenticate/code', [MapGuesser\Controller\UserController::class, 'authenticateWithGoogle']); }); //Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']); Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {