Implemented AuthController for login

* Moved /login functionality to AuthController
* Refactored password handling logic to use the Authenticator
This commit is contained in:
Igor Scheller 2018-11-27 12:01:36 +01:00
parent fd4303f336
commit bcce2625a8
28 changed files with 610 additions and 264 deletions

View File

@ -95,13 +95,10 @@ return [
// Number of hours that an angel has to sign out own shifts // Number of hours that an angel has to sign out own shifts
'last_unsubscribe' => 3, 'last_unsubscribe' => 3,
// Define the algorithm to use for `crypt()` of passwords // Define the algorithm to use for `password_verify()`
// If the user uses an old algorithm the password will be converted to the new format // If the user uses an old algorithm the password will be converted to the new format
// MD5 '$1' // See https://secure.php.net/manual/en/password.constants.php for a complete list
// Blowfish '$2y$13' 'password_algorithm' => PASSWORD_DEFAULT,
// SHA-256 '$5$rounds=5000'
// SHA-512 '$6$rounds=5000'
'crypt_alg' => '$6$rounds=5000',
// The minimum length for passwords // The minimum length for passwords
'min_password_length' => 8, 'min_password_length' => 8,

View File

@ -9,6 +9,8 @@ $route->get('/', 'HomeController@index');
$route->get('/credits', 'CreditsController@index'); $route->get('/credits', 'CreditsController@index');
// Authentication // Authentication
$route->get('/login', 'AuthController@login');
$route->post('/login', 'AuthController@postLogin');
$route->get('/logout', 'AuthController@logout'); $route->get('/logout', 'AuthController@logout');
// Stats // Stats

View File

@ -28,7 +28,7 @@ class CreateUsersTables extends Migration
$table->string('name', 24)->unique(); $table->string('name', 24)->unique();
$table->string('email', 254)->unique(); $table->string('email', 254)->unique();
$table->string('password', 128); $table->string('password', 255);
$table->string('api_key', 32); $table->string('api_key', 32);
$table->dateTime('last_login_at')->nullable(); $table->dateTime('last_login_at')->nullable();

View File

@ -47,6 +47,7 @@ function users_controller()
function user_delete_controller() function user_delete_controller()
{ {
$user = auth()->user(); $user = auth()->user();
$auth = auth();
$request = request(); $request = request();
if ($request->has('user_id')) { if ($request->has('user_id')) {
@ -68,12 +69,10 @@ function user_delete_controller()
if ($request->hasPostData('submit')) { if ($request->hasPostData('submit')) {
$valid = true; $valid = true;
if ( if (!(
!(
$request->has('password') $request->has('password')
&& verify_password($request->postData('password'), $user->password, $user->id) && $auth->verifyPassword($user, $request->postData('password'))
) )) {
) {
$valid = false; $valid = false;
error(__('Your password is incorrect. Please try it again.')); error(__('Your password is incorrect. Please try it again.'));
} }
@ -341,7 +340,7 @@ function user_password_recovery_set_new_controller()
} }
if ($valid) { if ($valid) {
set_password($passwordReset->user->id, $request->postData('password')); auth()->setPassword($passwordReset->user, $request->postData('password'));
success(__('Password saved.')); success(__('Password saved.'));
$passwordReset->delete(); $passwordReset->delete();
redirect(page_link_to('login')); redirect(page_link_to('login'));

View File

@ -291,8 +291,8 @@ function admin_user()
$request->postData('new_pw') != '' $request->postData('new_pw') != ''
&& $request->postData('new_pw') == $request->postData('new_pw2') && $request->postData('new_pw') == $request->postData('new_pw2')
) { ) {
set_password($user_id, $request->postData('new_pw'));
$user_source = User::find($user_id); $user_source = User::find($user_id);
auth()->setPassword($user_source, $request->postData('new_pw'));
engelsystem_log('Set new password for ' . User_Nick_render($user_source, true)); engelsystem_log('Set new password for ' . User_Nick_render($user_source, true));
$html .= success('Passwort neu gesetzt.', true); $html .= success('Passwort neu gesetzt.', true);
} else { } else {

View File

@ -8,14 +8,6 @@ use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\State; use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
/**
* @return string
*/
function login_title()
{
return __('Login');
}
/** /**
* @return string * @return string
*/ */
@ -226,7 +218,7 @@ function guest_register()
// Assign user-group and set password // Assign user-group and set password
DB::insert('INSERT INTO `UserGroups` (`uid`, `group_id`) VALUES (?, -20)', [$user->id]); DB::insert('INSERT INTO `UserGroups` (`uid`, `group_id`) VALUES (?, -20)', [$user->id]);
set_password($user->id, $request->postData('password')); auth()->setPassword($user, $request->postData('password'));
// Assign angel-types // Assign angel-types
$user_angel_types_info = []; $user_angel_types_info = [];
@ -369,112 +361,3 @@ function entry_required()
{ {
return '<span class="text-info glyphicon glyphicon-warning-sign"></span>'; return '<span class="text-info glyphicon glyphicon-warning-sign"></span>';
} }
/**
* @return string
*/
function guest_login()
{
$nick = '';
$request = request();
$session = session();
$valid = true;
$session->remove('uid');
if ($request->hasPostData('submit')) {
if ($request->has('nick') && !empty($request->input('nick'))) {
$nickValidation = User_validate_Nick($request->input('nick'));
$nick = $nickValidation->getValue();
$login_user = User::whereName($nickValidation->getValue())->first();
if ($login_user) {
if ($request->has('password')) {
if (!verify_password($request->postData('password'), $login_user->password, $login_user->id)) {
$valid = false;
error(__('Your password is incorrect. Please try it again.'));
}
} else {
$valid = false;
error(__('Please enter a password.'));
}
} else {
$valid = false;
error(__('No user was found with that Nickname. Please try again. If you are still having problems, ask a Dispatcher.'));
}
} else {
$valid = false;
error(__('Please enter a nickname.'));
}
if ($valid && $login_user) {
$session->set('uid', $login_user->id);
$session->set('locale', $login_user->settings->language);
redirect(page_link_to(config('home_site')));
}
}
return page([
div('col-md-12', [
div('row', [
EventConfig_countdown_page()
]),
div('row', [
div('col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4', [
div('panel panel-primary first', [
div('panel-heading', [
'<span class="icon-icon_angel"></span> ' . __('Login')
]),
div('panel-body', [
msg(),
form([
form_text_placeholder('nick', __('Nick'), $nick),
form_password_placeholder('password', __('Password')),
form_submit('submit', __('Login')),
!$valid ? buttons([
button(page_link_to('user_password_recovery'), __('I forgot my password'))
]) : ''
])
]),
div('panel-footer', [
glyph('info-sign') . __('Please note: You have to activate cookies!')
])
])
])
]),
div('row', [
div('col-sm-6 text-center', [
heading(register_title(), 2),
get_register_hint()
]),
div('col-sm-6 text-center', [
heading(__('What can I do?'), 2),
'<p>' . __('Please read about the jobs you can do to help us.') . '</p>',
buttons([
button(
page_link_to('angeltypes', ['action' => 'about']),
__('Teams/Job description') . ' &raquo;'
)
])
])
])
])
]);
}
/**
* @return string
*/
function get_register_hint()
{
if (auth()->can('register') && config('registration_enabled')) {
return join('', [
'<p>' . __('Please sign up, if you want to help us!') . '</p>',
buttons([
button(page_link_to('register'), register_title() . ' &raquo;')
])
]);
}
return error(__('Registration is disabled.'), true);
}

View File

@ -101,9 +101,10 @@ function user_settings_main($user_source, $enable_tshirt_size, $tshirt_sizes)
function user_settings_password($user_source) function user_settings_password($user_source)
{ {
$request = request(); $request = request();
$auth = auth();
if ( if (
!$request->has('password') !$request->has('password')
|| !verify_password($request->postData('password'), $user_source->password, $user_source->id) || !$auth->verifyPassword($user_source, $request->postData('password'))
) { ) {
error(__('-> not OK. Please try again.')); error(__('-> not OK. Please try again.'));
} elseif (strlen($request->postData('new_password')) < config('min_password_length')) { } elseif (strlen($request->postData('new_password')) < config('min_password_length')) {
@ -111,7 +112,7 @@ function user_settings_password($user_source)
} elseif ($request->postData('new_password') != $request->postData('new_password2')) { } elseif ($request->postData('new_password') != $request->postData('new_password2')) {
error(__('Your passwords don\'t match.')); error(__('Your passwords don\'t match.'));
} else { } else {
set_password($user_source->id, $request->postData('new_password')); $auth->setPassword($user_source, $request->postData('new_password'));
success(__('Password saved.')); success(__('Password saved.'));
} }
redirect(page_link_to('user_settings')); redirect(page_link_to('user_settings'));

View File

@ -1,74 +1,6 @@
<?php <?php
use Engelsystem\Database\DB; use Engelsystem\Database\DB;
use Engelsystem\Models\User\User;
/**
* generate a salt (random string) of arbitrary length suitable for the use with crypt()
*
* @param int $length
* @return string
*/
function generate_salt($length = 16)
{
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
$salt = '';
for ($i = 0; $i < $length; $i++) {
$salt .= $alphabet[rand(0, strlen($alphabet) - 1)];
}
return $salt;
}
/**
* set the password of a user
*
* @param int $uid
* @param string $password
*/
function set_password($uid, $password)
{
$user = User::find($uid);
$user->password = crypt($password, config('crypt_alg') . '$' . generate_salt(16) . '$');
$user->save();
}
/**
* verify a password given a precomputed salt.
* if $uid is given and $salt is an old-style salt (plain md5), we convert it automatically
*
* @param string $password
* @param string $salt
* @param int $uid
* @return bool
*/
function verify_password($password, $salt, $uid = null)
{
$crypt_alg = config('crypt_alg');
$correct = false;
if (substr($salt, 0, 1) == '$') {
// new-style crypt()
$correct = crypt($password, $salt) == $salt;
} elseif (substr($salt, 0, 7) == '{crypt}') {
// old-style crypt() with DES and static salt - not used anymore
$correct = crypt($password, '77') == $salt;
} elseif (strlen($salt) == 32) {
// old-style md5 without salt - not used anymore
$correct = md5($password) == $salt;
}
if ($correct && substr($salt, 0, strlen($crypt_alg)) != $crypt_alg && intval($uid)) {
// this password is stored in another format than we want it to be.
// let's update it!
// we duplicate the query from the above set_password() function to have the extra safety of checking
// the old hash
$user = User::find($uid);
if ($user->password == $salt) {
$user->password = crypt($password, $crypt_alg . '$' . generate_salt() . '$');
$user->save();
}
}
return $correct;
}
/** /**
* @param int $user_id * @param int $user_id

View File

@ -578,7 +578,7 @@ function AngelTypes_about_view($angeltypes, $user_logged_in)
$buttons[] = button(page_link_to('register'), register_title()); $buttons[] = button(page_link_to('register'), register_title());
} }
$buttons[] = button(page_link_to('login'), login_title()); $buttons[] = button(page_link_to('login'), __('Login'));
} }
$faqUrl = config('faq_url'); $faqUrl = config('faq_url');

View File

@ -126,7 +126,7 @@ function User_registration_success_view($event_welcome_message)
div('col-md-4', [ div('col-md-4', [
'<h2>' . __('Login') . '</h2>', '<h2>' . __('Login') . '</h2>',
form([ form([
form_text('nick', __('Nick'), ''), form_text('login', __('Nick'), ''),
form_password('password', __('Password')), form_password('password', __('Password')),
form_submit('submit', __('Login')), form_submit('submit', __('Login')),
buttons([ buttons([

View File

@ -1530,18 +1530,21 @@ msgid "Entry required!"
msgstr "Pflichtfeld!" msgstr "Pflichtfeld!"
#: includes/pages/guest_login.php:414 #: includes/pages/guest_login.php:414
msgid "Please enter a password." msgid "auth.no-password"
msgstr "Gib bitte ein Passwort ein." msgstr "Gib bitte ein Passwort ein."
#: includes/pages/guest_login.php:418 #: includes/pages/guest_login.php:418
msgid "" msgid "auth.not-found"
"No user was found with that Nickname. Please try again. If you are still "
"having problems, ask a Dispatcher."
msgstr "" msgstr ""
"Es wurde kein Engel mit diesem Namen gefunden. Probiere es bitte noch " "Es wurde kein Engel gefunden. Probiere es bitte noch einmal. Wenn das Problem "
"einmal. Wenn das Problem weiterhin besteht, frage einen Dispatcher." "weiterhin besteht, melde dich im Himmel."
#: includes/pages/guest_login.php:451 includes/view/User_view.php:130 #: includes/pages/guest_login.php:451 includes/view/User_view.php:130
msgid "auth.no-nickname"
msgstr "Gib bitte einen Nick an."
#: includes/pages/guest_login.php:481
#: includes/view/User_view.php:122
msgid "I forgot my password" msgid "I forgot my password"
msgstr "Passwort vergessen" msgstr "Passwort vergessen"
@ -2357,7 +2360,7 @@ msgid ""
"I have my own car with me and am willing to use it for the event (You'll get " "I have my own car with me and am willing to use it for the event (You'll get "
"reimbursed for fuel)" "reimbursed for fuel)"
msgstr "" msgstr ""
"Ich habe mein eigenes Auto dabei und möchte würde es zum Fahren für das " "Ich habe mein eigenes Auto dabei und möchte es zum Fahren für das "
"Event verwenden (Du wirst für Spritkosten entschädigt)" "Event verwenden (Du wirst für Spritkosten entschädigt)"
#: includes/view/UserDriverLicenses_view.php:30 #: includes/view/UserDriverLicenses_view.php:30

Binary file not shown.

View File

@ -0,0 +1,26 @@
msgid ""
msgstr ""
"Project-Id-Version: Engelsystem 2.0\n"
"POT-Creation-Date: 2017-12-29 19:01+0100\n"
"PO-Revision-Date: 2018-11-27 00:28+0100\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.11\n"
"X-Poedit-KeywordsList: _;gettext;gettext_noop\n"
"X-Poedit-Basepath: .\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"Last-Translator: \n"
"Language: en_US\n"
"X-Poedit-SearchPath-0: .\n"
msgid "auth.no-nickname"
msgstr "Please enter a nickname."
msgid "auth.no-password"
msgstr "Please enter a password."
msgid "auth.not-found"
msgstr "No user was found. Please try again. If you are still having problems, ask Heaven."

Binary file not shown.

View File

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Engelsystem 2.0\n" "Project-Id-Version: Engelsystem 2.0\n"
"POT-Creation-Date: 2017-04-25 05:23+0200\n" "POT-Creation-Date: 2017-04-25 05:23+0200\n"
"PO-Revision-Date: 2018-10-05 15:35+0200\n" "PO-Revision-Date: 2018-11-27 00:29+0100\n"
"Last-Translator: samba <samba@autistici.org>\n" "Last-Translator: samba <samba@autistici.org>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: pt_BR\n" "Language: pt_BR\n"
@ -1420,19 +1420,17 @@ msgid "Entry required!"
msgstr "Campo necessário!" msgstr "Campo necessário!"
#: includes/pages/guest_login.php:319 #: includes/pages/guest_login.php:319
msgid "Please enter a password." msgid "auth.no-password"
msgstr "Por favor digite uma senha." msgstr "Por favor digite uma senha."
#: includes/pages/guest_login.php:323 #: includes/pages/guest_login.php:323
msgid "" msgid "auth.not-found"
"No user was found with that Nickname. Please try again. If you are still "
"having problems, ask a Dispatcher."
msgstr "" msgstr ""
"Nenhum usuário com esse apelido foi encontrado. Por favor tente novamente. \n" "Nenhum usuário foi encontrado. Por favor tente novamente. \n"
"Se você continuar com problemas, pergunte a um Dispatcher." "Se você continuar com problemas, pergunte a um Dispatcher."
#: includes/pages/guest_login.php:327 #: includes/pages/guest_login.php:327
msgid "Please enter a nickname." msgid "auth.no-nickname"
msgstr "Por favor digite um apelido." msgstr "Por favor digite um apelido."
#: includes/pages/guest_login.php:358 includes/view/User_view.php:101 #: includes/pages/guest_login.php:358 includes/view/User_view.php:101

View File

@ -0,0 +1,5 @@
{% extends "errors/default.twig" %}
{% block title %}{{ __("405: Method not allowed") }}{% endblock %}
{% block content_headline_text %}{{ __("405: Method not allowed") }}{% endblock %}

View File

@ -0,0 +1,11 @@
{% macro angel() %}
<span class="icon-icon_angel"></span>
{% endmacro %}
{% macro glyphicon(glyph) %}
<span class="glyphicon glyphicon-{{ glyph }}"></span>
{% endmacro %}
{% macro alert(message, type) %}
<div class="alert alert-{{ type|default('info') }}">{{ message }}</div>
{% endmacro %}

View File

@ -0,0 +1,104 @@
{% extends "layouts/app.twig" %}
{% import 'macros/base.twig' as m %}
{% block title %}{{ __('Login') }}{% endblock %}
{% block content %}
<div class="col-md-12">
<div class="row">
<div class="col-sm-12 text-center">
<h2>{{ __('Welcome to the %s!', [config('name') ~ m.angel() ~ (config('app_name')|upper) ])|raw }}</h2>
</div>
</div>
<div class="row">
{% for name,date in {
(__('Buildup starts')): config('buildup_start'),
(__('Event starts')): config('event_start'),
(__('Event ends')): config('event_end'),
(__('Teardown ends')): config('teardown_end')
} if date %}
{% if date > date() %}
<div class="col-sm-3 text-center hidden-xs">
<h4>{{ name }}</h4>
<span class="moment-countdown text-big" data-timestamp="{{ date.getTimestamp }}">%c</span>
<small>{{ date.format(__('Y-m-d')) }}</small>
</div>
{% endif %}
{% endfor %}
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4">
<div class="panel panel-primary first">
<div class="panel-heading">{{ m.angel }} {{ __('Login') }}</div>
<div class="panel-body">
{% for message in errors|default([]) %}
{{ m.alert(__(message), 'danger') }}
{% endfor %}
<form action="" enctype="multipart/form-data" method="post">
{{ csrf() }}
<div class="form-group">
<input class="form-control" id="form_nick"
type="text" name="login" value="" placeholder="{{ __('Nick') }}">
</div>
<div class="form-group">
<input class="form-control" id="form_password"
type="password" name="password" value="" placeholder="{{ __('Password') }}">
</div>
<div class="form-group">
<div class="btn-group">
<button class="btn btn-primary" type="submit" name="submit">
{{ __('Login') }}
</button>
{% if show_password_recovery|default(false) %}
<a href="{{ url('user-password-recovery') }}" class="btn btn-default ">
{{ __('I forgot my password') }}
</a>
{% endif %}
</div>
</div>
</form>
</div>
<div class="panel-footer">
{{ m.glyphicon('info-sign') }} {{ __('Please note: You have to activate cookies!') }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6 text-center">
<h2>{{ __('Register') }}</h2>
{% if has_permission_to('register') and config('registration_enabled') %}
<p>{{ __('Please sign up, if you want to help us!') }}</p>
<div class="form-group">
<a href="{{ url('register') }}" class="btn btn-default">{{ __('Register') }} &raquo;</a>
</div>
{% else %}
{{ m.alert(__('Registration is disabled.'), 'danger') }}
{% endif %}
</div>
<div class="col-sm-6 text-center">
<h2>{{ __('What can I do?') }}</h2>
<p>{{ __('Please read about the jobs you can do to help us.') }}</p>
<div class="form-group">
<a href="{{ url('angeltypes', {'action': 'about'}) }}" class="btn btn-default">
{{ __('Teams/Job description') }} &raquo;
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -2,8 +2,12 @@
namespace Engelsystem\Controllers; namespace Engelsystem\Controllers;
use Carbon\Carbon;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response; use Engelsystem\Http\Response;
use Engelsystem\Http\UrlGeneratorInterface; use Engelsystem\Http\UrlGeneratorInterface;
use Engelsystem\Models\User\User;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
class AuthController extends BaseController class AuthController extends BaseController
@ -17,20 +21,100 @@ class AuthController extends BaseController
/** @var UrlGeneratorInterface */ /** @var UrlGeneratorInterface */
protected $url; protected $url;
public function __construct(Response $response, SessionInterface $session, UrlGeneratorInterface $url) /** @var Authenticator */
{ protected $auth;
/** @var array */
protected $permissions = [
'login' => 'login',
'postLogin' => 'login',
];
/**
* @param Response $response
* @param SessionInterface $session
* @param UrlGeneratorInterface $url
* @param Authenticator $auth
*/
public function __construct(
Response $response,
SessionInterface $session,
UrlGeneratorInterface $url,
Authenticator $auth
) {
$this->response = $response; $this->response = $response;
$this->session = $session; $this->session = $session;
$this->url = $url; $this->url = $url;
$this->auth = $auth;
} }
/** /**
* @return Response * @return Response
*/ */
public function logout() public function login()
{
return $this->response->withView('pages/login');
}
/**
* Posted login form
*
* @param Request $request
* @return Response
*/
public function postLogin(Request $request): Response
{
$return = $this->authenticateUser($request->get('login', ''), $request->get('password', ''));
if (!$return instanceof User) {
return $this->response->withView(
'pages/login',
['errors' => [$return], 'show_password_recovery' => true]
);
}
$user = $return;
$this->session->invalidate();
$this->session->set('user_id', $user->id);
$this->session->set('locale', $user->settings->language);
$user->last_login_at = new Carbon();
$user->save(['touch' => false]);
return $this->response->redirectTo('news');
}
/**
* @return Response
*/
public function logout(): Response
{ {
$this->session->invalidate(); $this->session->invalidate();
return $this->response->redirectTo($this->url->to('/')); return $this->response->redirectTo($this->url->to('/'));
} }
/**
* Verify the user and password
*
* @param $login
* @param $password
* @return User|string
*/
protected function authenticateUser(string $login, string $password)
{
if (!$login) {
return 'auth.no-nickname';
}
if (!$password) {
return 'auth.no-password';
}
if (!$user = $this->auth->authenticate($login, $password)) {
return 'auth.not-found';
}
return $user;
}
} }

View File

@ -25,6 +25,9 @@ class Authenticator
/** @var string[] */ /** @var string[] */
protected $permissions; protected $permissions;
/** @var int */
protected $passwordAlgorithm = PASSWORD_DEFAULT;
/** /**
* @param ServerRequestInterface $request * @param ServerRequestInterface $request
* @param Session $session * @param Session $session
@ -48,7 +51,7 @@ class Authenticator
return $this->user; return $this->user;
} }
$userId = $this->session->get('uid'); $userId = $this->session->get('user_id');
if (!$userId) { if (!$userId) {
return null; return null;
} }
@ -104,17 +107,15 @@ class Authenticator
$abilities = (array)$abilities; $abilities = (array)$abilities;
if (empty($this->permissions)) { if (empty($this->permissions)) {
$userId = $this->user ? $this->user->id : $this->session->get('uid'); $user = $this->user();
if ($userId) { if ($user) {
if ($user = $this->user()) {
$this->permissions = $this->getPermissionsByUser($user); $this->permissions = $this->getPermissionsByUser($user);
$user->last_login_at = new Carbon(); $user->last_login_at = new Carbon();
$user->save(); $user->save();
} else { } elseif ($this->session->get('user_id')) {
$this->session->remove('uid'); $this->session->remove('user_id');
}
} }
if (empty($this->permissions)) { if (empty($this->permissions)) {
@ -131,6 +132,78 @@ class Authenticator
return true; return true;
} }
/**
* @param string $login
* @param string $password
* @return User|null
*/
public function authenticate(string $login, string $password)
{
/** @var User $user */
$user = $this->userRepository->whereName($login)->first();
if (!$user) {
$user = $this->userRepository->whereEmail($login)->first();
}
if (!$user) {
return null;
}
if (!$this->verifyPassword($user, $password)) {
return null;
}
return $user;
}
/**
* @param User $user
* @param string $password
* @return bool
*/
public function verifyPassword(User $user, string $password)
{
$algorithm = $this->passwordAlgorithm;
if (!password_verify($password, $user->password)) {
return false;
}
if (password_needs_rehash($user->password, $algorithm)) {
$this->setPassword($user, $password);
}
return true;
}
/**
* @param UserRepository $user
* @param string $password
*/
public function setPassword(User $user, string $password)
{
$algorithm = $this->passwordAlgorithm;
$user->password = password_hash($password, $algorithm);
$user->save();
}
/**
* @return int
*/
public function getPasswordAlgorithm()
{
return $this->passwordAlgorithm;
}
/**
* @param int $passwordAlgorithm
*/
public function setPasswordAlgorithm(int $passwordAlgorithm)
{
$this->passwordAlgorithm = $passwordAlgorithm;
}
/** /**
* @param User $user * @param User $user
* @return array * @return array

View File

@ -2,14 +2,18 @@
namespace Engelsystem\Helpers; namespace Engelsystem\Helpers;
use Engelsystem\Config\Config;
use Engelsystem\Container\ServiceProvider; use Engelsystem\Container\ServiceProvider;
class AuthenticatorServiceProvider extends ServiceProvider class AuthenticatorServiceProvider extends ServiceProvider
{ {
public function register() public function register()
{ {
/** @var Config $config */
$config = $this->app->get('config');
/** @var Authenticator $authenticator */ /** @var Authenticator $authenticator */
$authenticator = $this->app->make(Authenticator::class); $authenticator = $this->app->make(Authenticator::class);
$authenticator->setPasswordAlgorithm($config->get('password_algorithm'));
$this->app->instance(Authenticator::class, $authenticator); $this->app->instance(Authenticator::class, $authenticator);
$this->app->instance('authenticator', $authenticator); $this->app->instance('authenticator', $authenticator);

View File

@ -19,7 +19,6 @@ class LegacyMiddleware implements MiddlewareInterface
'angeltypes', 'angeltypes',
'atom', 'atom',
'ical', 'ical',
'login',
'public_dashboard', 'public_dashboard',
'rooms', 'rooms',
'shift_entries', 'shift_entries',
@ -175,10 +174,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = settings_title(); $title = settings_title();
$content = user_settings(); $content = user_settings();
return [$title, $content]; return [$title, $content];
case 'login':
$title = login_title();
$content = guest_login();
return [$title, $content];
case 'register': case 'register':
$title = register_title(); $title = register_title();
$content = guest_register(); $content = guest_register();

View File

@ -3,40 +3,154 @@
namespace Engelsystem\Test\Unit\Controllers; namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Controllers\AuthController; use Engelsystem\Controllers\AuthController;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response; use Engelsystem\Http\Response;
use Engelsystem\Http\UrlGeneratorInterface; use Engelsystem\Http\UrlGeneratorInterface;
use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
class AuthControllerTest extends TestCase class AuthControllerTest extends TestCase
{ {
use HasDatabase;
/** /**
* @covers \Engelsystem\Controllers\AuthController::__construct * @covers \Engelsystem\Controllers\AuthController::__construct
* @covers \Engelsystem\Controllers\AuthController::logout * @covers \Engelsystem\Controllers\AuthController::login
*/ */
public function testLogout() public function testLogin()
{ {
/** @var Response|MockObject $response */ /** @var Response|MockObject $response */
$response = $this->createMock(Response::class); $response = $this->createMock(Response::class);
/** @var SessionInterface|MockObject $session */ /** @var SessionInterface|MockObject $session */
$session = $this->getMockForAbstractClass(SessionInterface::class);
/** @var UrlGeneratorInterface|MockObject $url */ /** @var UrlGeneratorInterface|MockObject $url */
$url = $this->getMockForAbstractClass(UrlGeneratorInterface::class); /** @var Authenticator|MockObject $auth */
list(, $session, $url, $auth) = $this->getMocks();
$response->expects($this->once())
->method('withView')
->with('pages/login')
->willReturn($response);
$controller = new AuthController($response, $session, $url, $auth);
$controller->login();
}
/**
* @covers \Engelsystem\Controllers\AuthController::postLogin
* @covers \Engelsystem\Controllers\AuthController::authenticateUser
*/
public function testPostLogin()
{
$this->initDatabase();
$request = new Request();
/** @var Response|MockObject $response */
$response = $this->createMock(Response::class);
/** @var SessionInterface|MockObject $session */
/** @var UrlGeneratorInterface|MockObject $url */
/** @var Authenticator|MockObject $auth */
list(, $session, $url, $auth) = $this->getMocks();
$user = new User([
'name' => 'foo',
'password' => '',
'email' => '',
'api_key' => '',
'last_login_at' => null,
]);
$user->forceFill(['id' => 42,]);
$user->save();
$settings = new Settings(['language' => 'de_DE', 'theme' => '']);
$settings->user()
->associate($user)
->save();
$auth->expects($this->exactly(2))
->method('authenticate')
->with('foo', 'bar')
->willReturnOnConsecutiveCalls(null, $user);
$response->expects($this->exactly(3))
->method('withView')
->withConsecutive(
['pages/login', ['errors' => ['auth.no-nickname'], 'show_password_recovery' => true]],
['pages/login', ['errors' => ['auth.no-password'], 'show_password_recovery' => true]],
['pages/login', ['errors' => ['auth.not-found'], 'show_password_recovery' => true]])
->willReturn($response);
$response->expects($this->once())
->method('redirectTo')
->with('news')
->willReturn($response);
$session->expects($this->once()) $session->expects($this->once())
->method('invalidate'); ->method('invalidate');
$response->expects($this->once()) $session->expects($this->exactly(2))
->method('redirectTo') ->method('set')
->with('https://foo.bar/'); ->withConsecutive(
['user_id', 42],
['locale', 'de_DE']
);
$controller = new AuthController($response, $session, $url, $auth);
$controller->postLogin($request);
$request = new Request(['login' => 'foo']);
$controller->postLogin($request);
$request = new Request(['login' => 'foo', 'password' => 'bar']);
// No user found
$controller->postLogin($request);
// Authenticated user
$controller->postLogin($request);
$this->assertNotNull($user->last_login_at);
}
/**
* @covers \Engelsystem\Controllers\AuthController::logout
*/
public function testLogout()
{
/** @var Response $response */
/** @var SessionInterface|MockObject $session */
/** @var UrlGeneratorInterface|MockObject $url */
/** @var Authenticator|MockObject $auth */
list($response, $session, $url, $auth) = $this->getMocks();
$session->expects($this->once())
->method('invalidate');
$url->expects($this->once()) $url->expects($this->once())
->method('to') ->method('to')
->with('/') ->with('/')
->willReturn('https://foo.bar/'); ->willReturn('https://foo.bar/');
$controller = new AuthController($response, $session, $url); $controller = new AuthController($response, $session, $url, $auth);
$controller->logout(); $return = $controller->logout();
$this->assertEquals(['https://foo.bar/'], $return->getHeader('location'));
}
/**
* @return array
*/
protected function getMocks()
{
$response = new Response();
/** @var SessionInterface|MockObject $session */
$session = $this->getMockForAbstractClass(SessionInterface::class);
/** @var UrlGeneratorInterface|MockObject $url */
$url = $this->getMockForAbstractClass(UrlGeneratorInterface::class);
/** @var Authenticator|MockObject $auth */
$auth = $this->createMock(Authenticator::class);
return [$response, $session, $url, $auth];
} }
} }

View File

@ -14,12 +14,4 @@ class ControllerImplementation extends BaseController
'dolor', 'dolor',
], ],
]; ];
/**
* @param array $permissions
*/
public function setPermissions(array $permissions)
{
$this->permissions = $permissions;
}
} }

View File

@ -3,6 +3,7 @@
namespace Engelsystem\Test\Unit\Helpers; namespace Engelsystem\Test\Unit\Helpers;
use Engelsystem\Application; use Engelsystem\Application;
use Engelsystem\Config\Config;
use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\Authenticator;
use Engelsystem\Helpers\AuthenticatorServiceProvider; use Engelsystem\Helpers\AuthenticatorServiceProvider;
use Engelsystem\Http\Request; use Engelsystem\Http\Request;
@ -19,11 +20,19 @@ class AuthenticatorServiceProviderTest extends ServiceProviderTest
$app = new Application(); $app = new Application();
$app->bind(ServerRequestInterface::class, Request::class); $app->bind(ServerRequestInterface::class, Request::class);
$config = new Config();
$config->set('password_algorithm', PASSWORD_DEFAULT);
$app->instance('config', $config);
$serviceProvider = new AuthenticatorServiceProvider($app); $serviceProvider = new AuthenticatorServiceProvider($app);
$serviceProvider->register(); $serviceProvider->register();
$this->assertInstanceOf(Authenticator::class, $app->get(Authenticator::class)); $this->assertInstanceOf(Authenticator::class, $app->get(Authenticator::class));
$this->assertInstanceOf(Authenticator::class, $app->get('authenticator')); $this->assertInstanceOf(Authenticator::class, $app->get('authenticator'));
$this->assertInstanceOf(Authenticator::class, $app->get('auth')); $this->assertInstanceOf(Authenticator::class, $app->get('auth'));
/** @var Authenticator $auth */
$auth = $app->get(Authenticator::class);
$this->assertEquals(PASSWORD_DEFAULT, $auth->getPasswordAlgorithm());
} }
} }

View File

@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Helpers;
use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\Authenticator;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\Helpers\Stub\UserModelImplementation; use Engelsystem\Test\Unit\Helpers\Stub\UserModelImplementation;
use Engelsystem\Test\Unit\ServiceProviderTest; use Engelsystem\Test\Unit\ServiceProviderTest;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
@ -12,6 +13,8 @@ use Symfony\Component\HttpFoundation\Session\Session;
class AuthenticatorTest extends ServiceProviderTest class AuthenticatorTest extends ServiceProviderTest
{ {
use HasDatabase;
/** /**
* @covers \Engelsystem\Helpers\Authenticator::__construct( * @covers \Engelsystem\Helpers\Authenticator::__construct(
* @covers \Engelsystem\Helpers\Authenticator::user * @covers \Engelsystem\Helpers\Authenticator::user
@ -29,7 +32,7 @@ class AuthenticatorTest extends ServiceProviderTest
$session->expects($this->exactly(3)) $session->expects($this->exactly(3))
->method('get') ->method('get')
->with('uid') ->with('user_id')
->willReturnOnConsecutiveCalls( ->willReturnOnConsecutiveCalls(
null, null,
42, 42,
@ -114,16 +117,13 @@ class AuthenticatorTest extends ServiceProviderTest
/** @var User|MockObject $user */ /** @var User|MockObject $user */
$user = $this->createMock(User::class); $user = $this->createMock(User::class);
$user->expects($this->once()) $session->expects($this->once())
->method('save');
$session->expects($this->exactly(2))
->method('get') ->method('get')
->with('uid') ->with('user_id')
->willReturn(42); ->willReturn(42);
$session->expects($this->once()) $session->expects($this->once())
->method('remove') ->method('remove')
->with('uid'); ->with('user_id');
/** @var Authenticator|MockObject $auth */ /** @var Authenticator|MockObject $auth */
$auth = $this->getMockBuilder(Authenticator::class) $auth = $this->getMockBuilder(Authenticator::class)
@ -151,4 +151,115 @@ class AuthenticatorTest extends ServiceProviderTest
// Permissions cached // Permissions cached
$this->assertTrue($auth->can('bar')); $this->assertTrue($auth->can('bar'));
} }
/**
* @covers \Engelsystem\Helpers\Authenticator::authenticate
*/
public function testAuthenticate()
{
$this->initDatabase();
/** @var ServerRequestInterface|MockObject $request */
$request = $this->getMockForAbstractClass(ServerRequestInterface::class);
/** @var Session|MockObject $session */
$session = $this->createMock(Session::class);
$userRepository = new User();
(new User([
'name' => 'lorem',
'password' => password_hash('testing', PASSWORD_DEFAULT),
'email' => 'lorem@foo.bar',
'api_key' => '',
]))->save();
(new User([
'name' => 'ipsum',
'password' => '',
'email' => 'ipsum@foo.bar',
'api_key' => '',
]))->save();
$auth = new Authenticator($request, $session, $userRepository);
$this->assertNull($auth->authenticate('not-existing', 'foo'));
$this->assertNull($auth->authenticate('ipsum', 'wrong-password'));
$this->assertInstanceOf(User::class, $auth->authenticate('lorem', 'testing'));
$this->assertInstanceOf(User::class, $auth->authenticate('lorem@foo.bar', 'testing'));
}
/**
* @covers \Engelsystem\Helpers\Authenticator::verifyPassword
*/
public function testVerifyPassword()
{
$this->initDatabase();
$password = password_hash('testing', PASSWORD_ARGON2I);
$user = new User([
'name' => 'lorem',
'password' => $password,
'email' => 'lorem@foo.bar',
'api_key' => '',
]);
$user->save();
/** @var Authenticator|MockObject $auth */
$auth = $this->getMockBuilder(Authenticator::class)
->disableOriginalConstructor()
->setMethods(['setPassword'])
->getMock();
$auth->expects($this->once())
->method('setPassword')
->with($user, 'testing');
$auth->setPasswordAlgorithm(PASSWORD_BCRYPT);
$this->assertFalse($auth->verifyPassword($user, 'randomStuff'));
$this->assertTrue($auth->verifyPassword($user, 'testing'));
}
/**
* @covers \Engelsystem\Helpers\Authenticator::setPassword
*/
public function testSetPassword()
{
$this->initDatabase();
$user = new User([
'name' => 'ipsum',
'password' => '',
'email' => 'ipsum@foo.bar',
'api_key' => '',
]);
$user->save();
$auth = $this->getAuthenticator();
$auth->setPasswordAlgorithm(PASSWORD_ARGON2I);
$auth->setPassword($user, 'FooBar');
$this->assertTrue($user->isClean());
$this->assertTrue(password_verify('FooBar', $user->password));
$this->assertFalse(password_needs_rehash($user->password, PASSWORD_ARGON2I));
}
/**
* @covers \Engelsystem\Helpers\Authenticator::setPasswordAlgorithm
* @covers \Engelsystem\Helpers\Authenticator::getPasswordAlgorithm
*/
public function testPasswordAlgorithm()
{
$auth = $this->getAuthenticator();
$auth->setPasswordAlgorithm(PASSWORD_ARGON2I);
$this->assertEquals(PASSWORD_ARGON2I, $auth->getPasswordAlgorithm());
}
/**
* @return Authenticator
*/
protected function getAuthenticator()
{
return new class extends Authenticator
{
/** @noinspection PhpMissingParentConstructorInspection */
public function __construct() { }
};
}
} }

View File

@ -19,7 +19,7 @@ class UrlGeneratorServiceProviderTest extends ServiceProviderTest
$urlGenerator = $this->getMockBuilder(UrlGenerator::class) $urlGenerator = $this->getMockBuilder(UrlGenerator::class)
->getMock(); ->getMock();
$app = $this->getApp(); $app = $this->getApp(['make', 'instance', 'bind']);
$this->setExpects($app, 'make', [UrlGenerator::class], $urlGenerator); $this->setExpects($app, 'make', [UrlGenerator::class], $urlGenerator);
$app->expects($this->exactly(2)) $app->expects($this->exactly(2))
@ -29,6 +29,9 @@ class UrlGeneratorServiceProviderTest extends ServiceProviderTest
['http.urlGenerator', $urlGenerator], ['http.urlGenerator', $urlGenerator],
[UrlGeneratorInterface::class, $urlGenerator] [UrlGeneratorInterface::class, $urlGenerator]
); );
$app->expects($this->once())
->method('bind')
->with(UrlGeneratorInterface::class, UrlGenerator::class);
$serviceProvider = new UrlGeneratorServiceProvider($app); $serviceProvider = new UrlGeneratorServiceProvider($app);
$serviceProvider->register(); $serviceProvider->register();