MAPG-181 add stuff to authenticate without password

This commit is contained in:
Bence Pőcze 2020-06-28 03:18:51 +02:00
parent 4099f1d962
commit 4333240acc
Signed by: bence
GPG Key ID: AA52B11A3269D1C1
7 changed files with 248 additions and 8 deletions

View File

@ -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();
};
})();

View File

@ -0,0 +1,10 @@
(function () {
if (success) {
window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil);
window.close();
} else {
document.getElementById('closeWindowButton').onclick = function () {
window.close();
}
}
})();

View File

@ -1,16 +1,22 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Authorization\ISecured; use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation; use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
use MapGuesser\Util\JwtParser;
class UserController implements ISecured class UserController implements ISecured
{ {
@ -45,6 +51,63 @@ class UserController implements ISecured
return new HtmlContent('account/account', $data); return new HtmlContent('account/account', $data);
} }
public function getGoogleAuthenticateRedirect(): IRedirect
{
/**
* @var User $user
*/
$user = $this->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 public function getDeleteAccount(): IContent
{ {
/** /**
@ -63,8 +126,13 @@ class UserController implements ISecured
*/ */
$user = $this->request->user(); $user = $this->request->user();
if (!$user->checkPassword($this->request->post('password'))) { if (!$this->confirmUserIdentity(
$data = ['error' => ['errorText' => 'The given current password is wrong.']]; $user,
$this->request->session()->get('authenticated_with_google_until'),
$this->request->post('password'),
$error
)) {
$data = ['error' => ['errorText' => $error]];
return new JsonContent($data); return new JsonContent($data);
} }
@ -84,6 +152,8 @@ class UserController implements ISecured
$this->pdm->saveToDb($user); $this->pdm->saveToDb($user);
$this->request->session()->delete('authenticated_with_google_until');
$data = ['success' => true]; $data = ['success' => true];
return new JsonContent($data); return new JsonContent($data);
} }
@ -95,8 +165,13 @@ class UserController implements ISecured
*/ */
$user = $this->request->user(); $user = $this->request->user();
if (!$user->checkPassword($this->request->post('password'))) { if (!$this->confirmUserIdentity(
$data = ['error' => ['errorText' => 'The given current password is wrong.']]; $user,
$this->request->session()->get('authenticated_with_google_until'),
$this->request->post('password'),
$error
)) {
$data = ['error' => ['errorText' => $error]];
return new JsonContent($data); return new JsonContent($data);
} }
@ -110,7 +185,28 @@ class UserController implements ISecured
\Container::$dbConnection->commit(); \Container::$dbConnection->commit();
$this->request->session()->delete('authenticated_with_google_until');
$data = ['success' => true]; $data = ['success' => true];
return new JsonContent($data); 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;
}
} }

View File

@ -1,10 +1,27 @@
@js('js/account/account.js')
@extends('templates/layout_normal') @extends('templates/layout_normal')
@section('main') @section('main')
<h2>Account</h2> <h2>Account</h2>
<div class="box"> <div class="box">
<form id="accountForm" action="/account" method="post" data-observe-inputs="password_new,password_new_confirm"> <form id="accountForm" action="/account" method="post" data-observe-inputs="password_new,password_new_confirm">
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with your password or with Google to modify your account.</p>
<div class="inputWithButton">
<input type="password" name="password" placeholder="Current password" required minlength="6" autofocus><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div>
<?php elseif ($user['password'] !== null): ?>
<p class="justify small">Please confirm your identity with your password to modify your account.</p>
<input class="big fullWidth" type="password" name="password" placeholder="Current password" required minlength="6" autofocus> <input class="big fullWidth" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
<?php elseif ($user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with Google to modify your account.</p>
<div class="inputWithButton">
<input type="text" name="password" placeholder="Authenticate with Google..." disabled><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div>
<?php endif; ?>
<hr> <hr>
<?php /* TODO: disabled for the time being, email modification should be implemented */ ?> <?php /* TODO: disabled for the time being, email modification should be implemented */ ?>
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled> <input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled>

View File

@ -1,14 +1,32 @@
@js('js/account/account.js')
@extends('templates/layout_normal') @extends('templates/layout_normal')
@section('main') @section('main')
<h2>Delete account</h2> <h2>Delete account</h2>
<div class="box"> <div class="box">
<form id="deleteAccountForm" action="/account/delete" method="post" data-redirect-on-success="/"> <form id="deleteAccountForm" action="/account/delete" method="post" data-redirect-on-success="/">
<p class="justify">Are you sure you want to delete your account? This cannot be undone!</p> <p class="justify marginBottom">Are you sure you want to delete your account? This cannot be undone!</p>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Current password" required minlength="6" autofocus> <?php if ($user['password'] !== null && $user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with your password or with Google to delete your account.</p>
<div class="inputWithButton">
<input type="password" name="password" placeholder="Current password" required minlength="6" autofocus><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div>
<?php elseif ($user['password'] !== null): ?>
<p class="justify small">Please confirm your identity with your password to delete your account.</p>
<input class="big fullWidth" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
<?php elseif ($user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with Google to delete your account.</p>
<div class="inputWithButton">
<input type="text" name="password" placeholder="Authenticate with Google..." disabled><!--
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div>
<?php endif; ?>
<p id="deleteAccountFormError" class="formError justify marginTop"></p> <p id="deleteAccountFormError" class="formError justify marginTop"></p>
<div class="right marginTop"> <div class="right marginTop">
<button class="red" type="submit" name="submit">Delete account</button> <button class="red marginRight" type="submit" name="submit">Delete account</button><!--
--><a class="button gray marginTop" href="/account" title="Back to account">Cancel</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -0,0 +1,28 @@
@js('js/account/google_authenticate.js')
@extends('templates/layout_minimal')
@section('main')
<h2>Authenticate with Google</h2>
<?php if (!$success): ?>
<div class="box">
<p class="error justify">
<?php if (isset($errorText)): ?>
<?= $errorText ?>
<?php else: ?>
Authentication with Google failed.
<?php endif; ?>
Please <a id="closeWindowButton" href="javascript:;" title="Close">close this window/tab</a> and try again!
</p>
</div>
<?php endif; ?>
@endsection
@section('pageScript')
<script>
var success = <?= $success ? 'true' : 'false' ?>;
<?php if (isset($authenticatedWithGoogleUntil)): ?>
var authenticatedWithGoogleUntil = new Date('<?= $authenticatedWithGoogleUntil->format('c') ?>');
<?php endif; ?>
</script>
@endsection

View File

@ -37,6 +37,8 @@ Container::$routeCollection->group('account', function (MapGuesser\Routing\Route
$routeCollection->post('account-action', '', [MapGuesser\Controller\UserController::class, 'saveAccount']); $routeCollection->post('account-action', '', [MapGuesser\Controller\UserController::class, 'saveAccount']);
$routeCollection->get('account.delete', 'delete', [MapGuesser\Controller\UserController::class, 'getDeleteAccount']); $routeCollection->get('account.delete', 'delete', [MapGuesser\Controller\UserController::class, 'getDeleteAccount']);
$routeCollection->post('account.delete-action', 'delete', [MapGuesser\Controller\UserController::class, 'deleteAccount']); $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->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {