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')