Skip to content
Snippets Groups Projects
Commit 51a0e793 authored by Jan-Hendrik Willms's avatar Jan-Hendrik Willms
Browse files

fix 2fa token waiting (and more cleaning up than should be, sorry), fixes #4855

Closes #4855

Merge request studip/studip!3640
parent 60f7675f
No related branches found
No related tags found
No related merge requests found
...@@ -77,6 +77,8 @@ class Request implements ArrayAccess, IteratorAggregate ...@@ -77,6 +77,8 @@ class Request implements ArrayAccess, IteratorAggregate
/** /**
* IteratorAggregate: Create iterator for request parameters. * IteratorAggregate: Create iterator for request parameters.
*
* @return ArrayIterator
*/ */
public function getIterator(): Traversable public function getIterator(): Traversable
{ {
......
...@@ -11,8 +11,6 @@ ...@@ -11,8 +11,6 @@
final class TwoFactorAuth final class TwoFactorAuth
{ {
const SESSION_KEY = 'tfa/confirmed'; const SESSION_KEY = 'tfa/confirmed';
const SESSION_REDIRECT = 'tfa/redirect';
const SESSION_ENFORCE = 'tfa/enforce';
const SESSION_DATA = 'tfa/data'; const SESSION_DATA = 'tfa/data';
const SESSION_CONFIRMATIONS = 'tfa/confirmations'; const SESSION_CONFIRMATIONS = 'tfa/confirmations';
const SESSION_FAILED = 'tfa/failed'; const SESSION_FAILED = 'tfa/failed';
...@@ -26,7 +24,7 @@ final class TwoFactorAuth ...@@ -26,7 +24,7 @@ final class TwoFactorAuth
* Returns an instance of the authentication * Returns an instance of the authentication
* @return TwoFactorAuth object * @return TwoFactorAuth object
*/ */
public static function get() public static function get(): TwoFactorAuth
{ {
if (self::$instance === null) { if (self::$instance === null) {
self::$instance = new self(); self::$instance = new self();
...@@ -39,16 +37,16 @@ final class TwoFactorAuth ...@@ -39,16 +37,16 @@ final class TwoFactorAuth
* user (defaults to current user). The user's permissions decide whether * user (defaults to current user). The user's permissions decide whether
* the two factor authentication is enabled or not. * the two factor authentication is enabled or not.
* *
* @param User $user User to check (optional, defaults to current user) * @param User|null $user User to check (optional, defaults to current user)
* @return boolean * @return boolean
*/ */
public static function isEnabledForUser(User $user = null) public static function isEnabledForUser(User $user = null): bool
{ {
if ($user === null) { if ($user === null) {
$user = User::findCurrent(); $user = User::findCurrent();
} }
$valid_perms = array_filter(array_map('trim', explode(',', Config::get()->TFA_PERMS))); $valid_perms = array_filter(array_map('trim', explode(',', Config::get()->getValue('TFA_PERMS'))));
return in_array($user->perms, $valid_perms); return in_array($user->perms, $valid_perms);
} }
...@@ -67,11 +65,13 @@ final class TwoFactorAuth ...@@ -67,11 +65,13 @@ final class TwoFactorAuth
/** /**
* Private constructor to enforce singleton * Private constructor to enforce singleton
*
* @throws SessionRequiredException
*/ */
private function __construct() private function __construct()
{ {
if (session_status() !== PHP_SESSION_ACTIVE) { if (session_status() !== PHP_SESSION_ACTIVE) {
throw new Exception('2FA requires a valid session'); throw new SessionRequiredException('2FA requires a valid session');
} }
if (!isset($_SESSION[self::SESSION_FAILED])) { if (!isset($_SESSION[self::SESSION_FAILED])) {
...@@ -81,7 +81,7 @@ final class TwoFactorAuth ...@@ -81,7 +81,7 @@ final class TwoFactorAuth
$_SESSION[self::SESSION_FAILED] = array_filter( $_SESSION[self::SESSION_FAILED] = array_filter(
$_SESSION[self::SESSION_FAILED], $_SESSION[self::SESSION_FAILED],
function ($timestamp) { function ($timestamp) {
return $timestamp > time() - Config::get()->TFA_MAX_TRIES_TIMESPAN; return $timestamp > time() - Config::get()->getValue('TFA_MAX_TRIES_TIMESPAN');
} }
); );
} }
...@@ -145,7 +145,7 @@ final class TwoFactorAuth ...@@ -145,7 +145,7 @@ final class TwoFactorAuth
// User has already confirmed this session? // User has already confirmed this session?
if (isset($_SESSION[self::SESSION_KEY])) { if (isset($_SESSION[self::SESSION_KEY])) {
list($code, $timeslice) = array_values($_SESSION[self::SESSION_KEY]); [$code, $timeslice] = array_values($_SESSION[self::SESSION_KEY]);
if ($this->secret->validateToken($code, (int) $timeslice, true)) { if ($this->secret->validateToken($code, (int) $timeslice, true)) {
return; return;
} }
...@@ -155,7 +155,7 @@ final class TwoFactorAuth ...@@ -155,7 +155,7 @@ final class TwoFactorAuth
// Trusted computer? // Trusted computer?
$user_cookie_key = self::COOKIE_KEY . '/' . $GLOBALS['user']->id; $user_cookie_key = self::COOKIE_KEY . '/' . $GLOBALS['user']->id;
if (isset($_COOKIE[$user_cookie_key])) { if (isset($_COOKIE[$user_cookie_key])) {
list($code, $timeslice) = explode(':', $_COOKIE[$user_cookie_key]); [$code, $timeslice] = explode(':', $_COOKIE[$user_cookie_key]);
if ($this->secret->validateToken($code, (int) $timeslice, true)) { if ($this->secret->validateToken($code, (int) $timeslice, true)) {
$this->registerSecretInSession(); $this->registerSecretInSession();
return; return;
...@@ -177,7 +177,7 @@ final class TwoFactorAuth ...@@ -177,7 +177,7 @@ final class TwoFactorAuth
* @param array $data Optional additional data to pass to the * @param array $data Optional additional data to pass to the
* confirmation screen (for internal use) * confirmation screen (for internal use)
*/ */
public function confirm($action, $text, array $data = []): void public function confirm(string $action, string $text, array $data = []): void
{ {
if ( if (
isset($_SESSION[self::SESSION_CONFIRMATIONS]) isset($_SESSION[self::SESSION_CONFIRMATIONS])
...@@ -202,7 +202,7 @@ final class TwoFactorAuth ...@@ -202,7 +202,7 @@ final class TwoFactorAuth
* @param string $text Text to display to the user * @param string $text Text to display to the user
* @param array $data Optional additional data (for internal use) * @param array $data Optional additional data (for internal use)
*/ */
private function showConfirmationScreen($text = '', array $data = []) private function showConfirmationScreen(string $text = '', array $data = [])
{ {
$data = array_merge(['global' => false], $data); $data = array_merge(['global' => false], $data);
...@@ -235,8 +235,8 @@ final class TwoFactorAuth ...@@ -235,8 +235,8 @@ final class TwoFactorAuth
$_SESSION[self::SESSION_DATA] + [ $_SESSION[self::SESSION_DATA] + [
'secret' => $this->secret, 'secret' => $this->secret,
'text' => $text, 'text' => $text,
'blocked' => $this->isBlocked(), 'waittime' => $this->getWaitingTime(),
'duration' => Config::get()->TFA_TRUST_DURATION, 'duration' => Config::get()->getValue('TFA_TRUST_DURATION'),
], ],
'layouts/base.php' 'layouts/base.php'
); );
...@@ -263,7 +263,7 @@ final class TwoFactorAuth ...@@ -263,7 +263,7 @@ final class TwoFactorAuth
*/ */
private function registerSecretInCookie() private function registerSecretInCookie()
{ {
$lifetime_in_days = Config::get()->TFA_TRUST_DURATION; $lifetime_in_days = Config::get()->getValue('TFA_TRUST_DURATION');
$lifetime = $lifetime_in_days > 0 ? strtotime("+{$lifetime_in_days} days") : 2147483647; $lifetime = $lifetime_in_days > 0 ? strtotime("+{$lifetime_in_days} days") : 2147483647;
$timeslice = mt_rand(0, PHP_INT_MAX); $timeslice = mt_rand(0, PHP_INT_MAX);
...@@ -288,7 +288,7 @@ final class TwoFactorAuth ...@@ -288,7 +288,7 @@ final class TwoFactorAuth
private function validateFromRequest() private function validateFromRequest()
{ {
if ( if (
$this->isBlocked() $this->getWaitingTime()
|| !Request::isPost() || !Request::isPost()
|| !Request::submitted('tfacode-input') || !Request::submitted('tfacode-input')
|| !Request::submitted('tfa-nonce') || !Request::submitted('tfa-nonce')
...@@ -341,16 +341,20 @@ final class TwoFactorAuth ...@@ -341,16 +341,20 @@ final class TwoFactorAuth
} }
/** /**
* Returns whether the current session is blocked from any more token * Returns the time the user has to wait before entering a token.
* inputs. This happens if too many false inputs happen in a short amount
* of time and should prevent brute force attacks.
* *
* @return boolean * This happens if too many invalid tokens have been input.
*/ */
private function isBlocked() private function getWaitingTime(): int
{ {
return count($_SESSION[self::SESSION_FAILED]) >= Config::get()->TFA_MAX_TRIES if (count($_SESSION[self::SESSION_FAILED]) < Config::get()->getValue('TFA_MAX_TRIES')) {
? min($_SESSION[self::SESSION_FAILED]) return 0;
: false; }
$min_timestamp = min(array_slice($_SESSION[self::SESSION_FAILED], -Config::get()->getValue('TFA_MAX_TRIES')));
$wait_time = $min_timestamp + Config::get()->getValue('TFA_MAX_TRIES_TIMESPAN') - time();
return ceil($wait_time / 60);
} }
} }
<?php
/**
* @var string $__nonce
* @var int $waittime
* @var string $text
* @var TFASecret $secret
* @var bool|int $global
* @var int $duration
*/
?>
<form action="<?= htmlReady(Request::url()) ?>" method="post" class="default"> <form action="<?= htmlReady(Request::url()) ?>" method="post" class="default">
<input type="hidden" name="tfa-nonce" value="<?= htmlReady($__nonce) ?>"> <input type="hidden" name="tfa-nonce" value="<?= htmlReady($__nonce) ?>">
<fieldset> <fieldset>
<legend><?= _('Zwei-Faktor-Authentifizierung') ?></legend> <legend><?= _('Zwei-Faktor-Authentifizierung') ?></legend>
<? if ($blocked): ?> <? if ($waittime): ?>
<?= MessageBox::warning(_('Sie haben zu viele ungültige Versuche'), [sprintf( <?= MessageBox::warning(_('Sie haben zu viele ungültige Versuche'), [sprintf(
_('Versuchen Sie es in %u Minute(n) erneut'), _('Versuchen Sie es in %u Minute(n) erneut'),
ceil((time() - $blocked) / 60) $waittime
)])->hideClose() ?> )])->hideClose() ?>
<? else: ?> <? else: ?>
<p><?= htmlReady($text ?: _('Bitte geben Sie ein gültiges Token ein')) ?></p> <p><?= htmlReady($text ?: _('Bitte geben Sie ein gültiges Token ein')) ?></p>
<? if ($secret->type === 'app' && !$secret->confirmed): ?> <? if ($secret->type === 'app' && !$secret->confirmed): ?>
<?= formatReady(Config::get()->TFA_TEXT_APP) ?> <?= formatReady(Config::get()->getValue('TFA_TEXT_APP')) ?>
<p> <p>
<?= _('Scannen Sie diesen Code mit Ihrer App ein und geben Sie ' <?= _('Scannen Sie diesen Code mit Ihrer App ein und geben Sie '
. 'anschliessend ein gültiges Token ein.') ?> . 'anschliessend ein gültiges Token ein.') ?>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment