diff options
author | Igor Scheller <igor.scheller@igorshp.de> | 2018-11-27 12:01:36 +0100 |
---|---|---|
committer | Igor Scheller <igor.scheller@igorshp.de> | 2019-07-08 01:57:59 +0200 |
commit | bcce2625a8cb0b630d945c6849014049869e10ce (patch) | |
tree | 2031911a85a7a6a85015ff77ca8f9b326fa1da8e | |
parent | fd4303f336173101b84ba21650e451ad536828fe (diff) |
Implemented AuthController for login
* Moved /login functionality to AuthController
* Refactored password handling logic to use the Authenticator
28 files changed, 610 insertions, 264 deletions
diff --git a/config/config.default.php b/config/config.default.php index 693b0d19..9c9505c6 100644 --- a/config/config.default.php +++ b/config/config.default.php @@ -95,13 +95,10 @@ return [ // Number of hours that an angel has to sign out own shifts '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 - // MD5 '$1' - // Blowfish '$2y$13' - // SHA-256 '$5$rounds=5000' - // SHA-512 '$6$rounds=5000' - 'crypt_alg' => '$6$rounds=5000', + // See https://secure.php.net/manual/en/password.constants.php for a complete list + 'password_algorithm' => PASSWORD_DEFAULT, // The minimum length for passwords 'min_password_length' => 8, diff --git a/config/routes.php b/config/routes.php index e999d026..02fd3abd 100644 --- a/config/routes.php +++ b/config/routes.php @@ -9,6 +9,8 @@ $route->get('/', 'HomeController@index'); $route->get('/credits', 'CreditsController@index'); // Authentication +$route->get('/login', 'AuthController@login'); +$route->post('/login', 'AuthController@postLogin'); $route->get('/logout', 'AuthController@logout'); // Stats diff --git a/db/migrations/2018_10_01_000000_create_users_tables.php b/db/migrations/2018_10_01_000000_create_users_tables.php index d8422ca0..52b3658f 100644 --- a/db/migrations/2018_10_01_000000_create_users_tables.php +++ b/db/migrations/2018_10_01_000000_create_users_tables.php @@ -28,7 +28,7 @@ class CreateUsersTables extends Migration $table->string('name', 24)->unique(); $table->string('email', 254)->unique(); - $table->string('password', 128); + $table->string('password', 255); $table->string('api_key', 32); $table->dateTime('last_login_at')->nullable(); diff --git a/includes/controller/users_controller.php b/includes/controller/users_controller.php index 7c6bde02..214998dc 100644 --- a/includes/controller/users_controller.php +++ b/includes/controller/users_controller.php @@ -47,6 +47,7 @@ function users_controller() function user_delete_controller() { $user = auth()->user(); + $auth = auth(); $request = request(); if ($request->has('user_id')) { @@ -68,14 +69,12 @@ function user_delete_controller() if ($request->hasPostData('submit')) { $valid = true; - if ( - !( + if (!( $request->has('password') - && verify_password($request->postData('password'), $user->password, $user->id) - ) - ) { + && $auth->verifyPassword($user, $request->postData('password')) + )) { $valid = false; - error(__('Your password is incorrect. Please try it again.')); + error(__('Your password is incorrect. Please try it again.')); } if ($valid) { @@ -341,7 +340,7 @@ function user_password_recovery_set_new_controller() } if ($valid) { - set_password($passwordReset->user->id, $request->postData('password')); + auth()->setPassword($passwordReset->user, $request->postData('password')); success(__('Password saved.')); $passwordReset->delete(); redirect(page_link_to('login')); diff --git a/includes/pages/admin_user.php b/includes/pages/admin_user.php index e6f94180..8482dea5 100644 --- a/includes/pages/admin_user.php +++ b/includes/pages/admin_user.php @@ -291,8 +291,8 @@ function admin_user() $request->postData('new_pw') != '' && $request->postData('new_pw') == $request->postData('new_pw2') ) { - set_password($user_id, $request->postData('new_pw')); $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)); $html .= success('Passwort neu gesetzt.', true); } else { diff --git a/includes/pages/guest_login.php b/includes/pages/guest_login.php index d152a092..3bc10fc3 100644 --- a/includes/pages/guest_login.php +++ b/includes/pages/guest_login.php @@ -11,14 +11,6 @@ use Engelsystem\Models\User\User; /** * @return string */ -function login_title() -{ - return __('Login'); -} - -/** - * @return string - */ function register_title() { return __('Register'); @@ -226,7 +218,7 @@ function guest_register() // Assign user-group and set password 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 $user_angel_types_info = []; @@ -369,112 +361,3 @@ function entry_required() { 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') . ' »' - ) - ]) - ]) - ]) - ]) - ]); -} - -/** - * @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() . ' »') - ]) - ]); - } - - return error(__('Registration is disabled.'), true); -} diff --git a/includes/pages/user_settings.php b/includes/pages/user_settings.php index ae29e4d8..f6853191 100644 --- a/includes/pages/user_settings.php +++ b/includes/pages/user_settings.php @@ -101,9 +101,10 @@ function user_settings_main($user_source, $enable_tshirt_size, $tshirt_sizes) function user_settings_password($user_source) { $request = request(); + $auth = auth(); if ( !$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.')); } 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')) { error(__('Your passwords don\'t match.')); } else { - set_password($user_source->id, $request->postData('new_password')); + $auth->setPassword($user_source, $request->postData('new_password')); success(__('Password saved.')); } redirect(page_link_to('user_settings')); diff --git a/includes/sys_auth.php b/includes/sys_auth.php index 520b13eb..f0485495 100644 --- a/includes/sys_auth.php +++ b/includes/sys_auth.php @@ -1,74 +1,6 @@ <?php 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 diff --git a/includes/view/AngelTypes_view.php b/includes/view/AngelTypes_view.php index f5434e8f..9f9bd736 100644 --- a/includes/view/AngelTypes_view.php +++ b/includes/view/AngelTypes_view.php @@ -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('login'), login_title()); + $buttons[] = button(page_link_to('login'), __('Login')); } $faqUrl = config('faq_url'); diff --git a/includes/view/User_view.php b/includes/view/User_view.php index 949bba87..21be0c9f 100644 --- a/includes/view/User_view.php +++ b/includes/view/User_view.php @@ -126,7 +126,7 @@ function User_registration_success_view($event_welcome_message) div('col-md-4', [ '<h2>' . __('Login') . '</h2>', form([ - form_text('nick', __('Nick'), ''), + form_text('login', __('Nick'), ''), form_password('password', __('Password')), form_submit('submit', __('Login')), buttons([ diff --git a/resources/lang/de_DE.UTF-8/LC_MESSAGES/default.po b/resources/lang/de_DE.UTF-8/LC_MESSAGES/default.po index d5a7b993..27ceb586 100644 --- a/resources/lang/de_DE.UTF-8/LC_MESSAGES/default.po +++ b/resources/lang/de_DE.UTF-8/LC_MESSAGES/default.po @@ -541,7 +541,7 @@ msgstr "Du kannst Dich nicht selber löschen." #: includes/controller/users_controller.php:78 #: includes/pages/guest_login.php:410 -msgid "Your password is incorrect. Please try it again." +msgid "Your password is incorrect. Please try it again." msgstr "Dein Passwort stimmt nicht. Bitte probiere es nochmal." #: includes/controller/users_controller.php:87 @@ -1530,18 +1530,21 @@ msgid "Entry required!" msgstr "Pflichtfeld!" #: includes/pages/guest_login.php:414 -msgid "Please enter a password." +msgid "auth.no-password" msgstr "Gib bitte ein Passwort ein." #: includes/pages/guest_login.php:418 -msgid "" -"No user was found with that Nickname. Please try again. If you are still " -"having problems, ask a Dispatcher." +msgid "auth.not-found" msgstr "" -"Es wurde kein Engel mit diesem Namen gefunden. Probiere es bitte noch " -"einmal. Wenn das Problem weiterhin besteht, frage einen Dispatcher." +"Es wurde kein Engel gefunden. Probiere es bitte noch einmal. Wenn das Problem " +"weiterhin besteht, melde dich im Himmel." #: 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" 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 " "reimbursed for fuel)" 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)" #: includes/view/UserDriverLicenses_view.php:30 diff --git a/resources/lang/en_US.UTF-8/LC_MESSAGES/default.mo b/resources/lang/en_US.UTF-8/LC_MESSAGES/default.mo Binary files differnew file mode 100644 index 00000000..e95ae703 --- /dev/null +++ b/resources/lang/en_US.UTF-8/LC_MESSAGES/default.mo diff --git a/resources/lang/en_US.UTF-8/LC_MESSAGES/default.po b/resources/lang/en_US.UTF-8/LC_MESSAGES/default.po new file mode 100644 index 00000000..22566e52 --- /dev/null +++ b/resources/lang/en_US.UTF-8/LC_MESSAGES/default.po @@ -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." diff --git a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/default.mo b/resources/lang/pt_BR.UTF.8/LC_MESSAGES/default.mo Binary files differnew file mode 100644 index 00000000..8b864156 --- /dev/null +++ b/resources/lang/pt_BR.UTF.8/LC_MESSAGES/default.mo diff --git a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.po b/resources/lang/pt_BR.UTF.8/LC_MESSAGES/default.po index e7307e5d..b9bf420d 100644 --- a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.po +++ b/resources/lang/pt_BR.UTF.8/LC_MESSAGES/default.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Engelsystem 2.0\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" "Language-Team: \n" "Language: pt_BR\n" @@ -477,7 +477,7 @@ msgstr "Você não pode se deletar." #: includes/controller/users_controller.php:61 #: includes/pages/guest_login.php:315 -msgid "Your password is incorrect. Please try it again." +msgid "Your password is incorrect. Please try it again." msgstr "Sua senha está incorreta. Por favor, tente novamente." #: includes/controller/users_controller.php:71 @@ -1420,19 +1420,17 @@ msgid "Entry required!" msgstr "Campo necessário!" #: includes/pages/guest_login.php:319 -msgid "Please enter a password." +msgid "auth.no-password" msgstr "Por favor digite uma senha." #: includes/pages/guest_login.php:323 -msgid "" -"No user was found with that Nickname. Please try again. If you are still " -"having problems, ask a Dispatcher." +msgid "auth.not-found" 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." #: includes/pages/guest_login.php:327 -msgid "Please enter a nickname." +msgid "auth.no-nickname" msgstr "Por favor digite um apelido." #: includes/pages/guest_login.php:358 includes/view/User_view.php:101 diff --git a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.mo b/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.mo Binary files differdeleted file mode 100644 index 95251feb..00000000 --- a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.mo +++ /dev/null diff --git a/resources/views/errors/405.twig b/resources/views/errors/405.twig new file mode 100644 index 00000000..cbbb94ea --- /dev/null +++ b/resources/views/errors/405.twig @@ -0,0 +1,5 @@ +{% extends "errors/default.twig" %} + +{% block title %}{{ __("405: Method not allowed") }}{% endblock %} + +{% block content_headline_text %}{{ __("405: Method not allowed") }}{% endblock %} diff --git a/resources/views/macros/base.twig b/resources/views/macros/base.twig new file mode 100644 index 00000000..94287bd4 --- /dev/null +++ b/resources/views/macros/base.twig @@ -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 %} diff --git a/resources/views/pages/login.twig b/resources/views/pages/login.twig new file mode 100644 index 00000000..75b98aa1 --- /dev/null +++ b/resources/views/pages/login.twig @@ -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') }} »</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') }} » + </a> + </div> + </div> + </div> + + </div> +{% endblock %} diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index cdaee167..e5fc40e3 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -2,8 +2,12 @@ namespace Engelsystem\Controllers; +use Carbon\Carbon; +use Engelsystem\Helpers\Authenticator; +use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Http\UrlGeneratorInterface; +use Engelsystem\Models\User\User; use Symfony\Component\HttpFoundation\Session\SessionInterface; class AuthController extends BaseController @@ -17,20 +21,100 @@ class AuthController extends BaseController /** @var UrlGeneratorInterface */ 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->session = $session; $this->url = $url; + $this->auth = $auth; + } + + /** + * @return Response + */ + 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() + public function logout(): Response { $this->session->invalidate(); 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; + } } diff --git a/src/Helpers/Authenticator.php b/src/Helpers/Authenticator.php index 61d07980..db33339b 100644 --- a/src/Helpers/Authenticator.php +++ b/src/Helpers/Authenticator.php @@ -25,6 +25,9 @@ class Authenticator /** @var string[] */ protected $permissions; + /** @var int */ + protected $passwordAlgorithm = PASSWORD_DEFAULT; + /** * @param ServerRequestInterface $request * @param Session $session @@ -48,7 +51,7 @@ class Authenticator return $this->user; } - $userId = $this->session->get('uid'); + $userId = $this->session->get('user_id'); if (!$userId) { return null; } @@ -104,17 +107,15 @@ class Authenticator $abilities = (array)$abilities; if (empty($this->permissions)) { - $userId = $this->user ? $this->user->id : $this->session->get('uid'); + $user = $this->user(); - if ($userId) { - if ($user = $this->user()) { - $this->permissions = $this->getPermissionsByUser($user); + if ($user) { + $this->permissions = $this->getPermissionsByUser($user); - $user->last_login_at = new Carbon(); - $user->save(); - } else { - $this->session->remove('uid'); - } + $user->last_login_at = new Carbon(); + $user->save(); + } elseif ($this->session->get('user_id')) { + $this->session->remove('user_id'); } if (empty($this->permissions)) { @@ -132,6 +133,78 @@ class Authenticator } /** + * @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 * @return array * @codeCoverageIgnore diff --git a/src/Helpers/AuthenticatorServiceProvider.php b/src/Helpers/AuthenticatorServiceProvider.php index 715a592f..f06e635d 100644 --- a/src/Helpers/AuthenticatorServiceProvider.php +++ b/src/Helpers/AuthenticatorServiceProvider.php @@ -2,14 +2,18 @@ namespace Engelsystem\Helpers; +use Engelsystem\Config\Config; use Engelsystem\Container\ServiceProvider; class AuthenticatorServiceProvider extends ServiceProvider { public function register() { + /** @var Config $config */ + $config = $this->app->get('config'); /** @var Authenticator $authenticator */ $authenticator = $this->app->make(Authenticator::class); + $authenticator->setPasswordAlgorithm($config->get('password_algorithm')); $this->app->instance(Authenticator::class, $authenticator); $this->app->instance('authenticator', $authenticator); diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index af2c6a70..7adcc88d 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -19,7 +19,6 @@ class LegacyMiddleware implements MiddlewareInterface 'angeltypes', 'atom', 'ical', - 'login', 'public_dashboard', 'rooms', 'shift_entries', @@ -175,10 +174,6 @@ class LegacyMiddleware implements MiddlewareInterface $title = settings_title(); $content = user_settings(); return [$title, $content]; - case 'login': - $title = login_title(); - $content = guest_login(); - return [$title, $content]; case 'register': $title = register_title(); $content = guest_register(); diff --git a/tests/Unit/Controllers/AuthControllerTest.php b/tests/Unit/Controllers/AuthControllerTest.php index c5349cda..0fad3b6d 100644 --- a/tests/Unit/Controllers/AuthControllerTest.php +++ b/tests/Unit/Controllers/AuthControllerTest.php @@ -3,40 +3,154 @@ namespace Engelsystem\Test\Unit\Controllers; use Engelsystem\Controllers\AuthController; +use Engelsystem\Helpers\Authenticator; +use Engelsystem\Http\Request; use Engelsystem\Http\Response; 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\TestCase; use Symfony\Component\HttpFoundation\Session\SessionInterface; class AuthControllerTest extends TestCase { + use HasDatabase; + /** * @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 */ $response = $this->createMock(Response::class); /** @var SessionInterface|MockObject $session */ - $session = $this->getMockForAbstractClass(SessionInterface::class); /** @var UrlGeneratorInterface|MockObject $url */ - $url = $this->getMockForAbstractClass(UrlGeneratorInterface::class); + /** @var Authenticator|MockObject $auth */ + list(, $session, $url, $auth) = $this->getMocks(); - $session->expects($this->once()) - ->method('invalidate'); + $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('https://foo.bar/'); + ->with('news') + ->willReturn($response); + + $session->expects($this->once()) + ->method('invalidate'); + + $session->expects($this->exactly(2)) + ->method('set') + ->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()) ->method('to') ->with('/') ->willReturn('https://foo.bar/'); - $controller = new AuthController($response, $session, $url); - $controller->logout(); + $controller = new AuthController($response, $session, $url, $auth); + $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]; } } diff --git a/tests/Unit/Controllers/Stub/ControllerImplementation.php b/tests/Unit/Controllers/Stub/ControllerImplementation.php index 01d9f250..a8bf538c 100644 --- a/tests/Unit/Controllers/Stub/ControllerImplementation.php +++ b/tests/Unit/Controllers/Stub/ControllerImplementation.php @@ -14,12 +14,4 @@ class ControllerImplementation extends BaseController 'dolor', ], ]; - - /** - * @param array $permissions - */ - public function setPermissions(array $permissions) - { - $this->permissions = $permissions; - } } diff --git a/tests/Unit/Helpers/AuthenticatorServiceProviderTest.php b/tests/Unit/Helpers/AuthenticatorServiceProviderTest.php index b1767ebc..ab9b23ec 100644 --- a/tests/Unit/Helpers/AuthenticatorServiceProviderTest.php +++ b/tests/Unit/Helpers/AuthenticatorServiceProviderTest.php @@ -3,6 +3,7 @@ namespace Engelsystem\Test\Unit\Helpers; use Engelsystem\Application; +use Engelsystem\Config\Config; use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\AuthenticatorServiceProvider; use Engelsystem\Http\Request; @@ -19,11 +20,19 @@ class AuthenticatorServiceProviderTest extends ServiceProviderTest $app = new Application(); $app->bind(ServerRequestInterface::class, Request::class); + $config = new Config(); + $config->set('password_algorithm', PASSWORD_DEFAULT); + $app->instance('config', $config); + $serviceProvider = new AuthenticatorServiceProvider($app); $serviceProvider->register(); $this->assertInstanceOf(Authenticator::class, $app->get(Authenticator::class)); $this->assertInstanceOf(Authenticator::class, $app->get('authenticator')); $this->assertInstanceOf(Authenticator::class, $app->get('auth')); + + /** @var Authenticator $auth */ + $auth = $app->get(Authenticator::class); + $this->assertEquals(PASSWORD_DEFAULT, $auth->getPasswordAlgorithm()); } } diff --git a/tests/Unit/Helpers/AuthenticatorTest.php b/tests/Unit/Helpers/AuthenticatorTest.php index 400278f2..83dc72ad 100644 --- a/tests/Unit/Helpers/AuthenticatorTest.php +++ b/tests/Unit/Helpers/AuthenticatorTest.php @@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Helpers; use Engelsystem\Helpers\Authenticator; use Engelsystem\Models\User\User; +use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\Helpers\Stub\UserModelImplementation; use Engelsystem\Test\Unit\ServiceProviderTest; use PHPUnit\Framework\MockObject\MockObject; @@ -12,6 +13,8 @@ use Symfony\Component\HttpFoundation\Session\Session; class AuthenticatorTest extends ServiceProviderTest { + use HasDatabase; + /** * @covers \Engelsystem\Helpers\Authenticator::__construct( * @covers \Engelsystem\Helpers\Authenticator::user @@ -29,7 +32,7 @@ class AuthenticatorTest extends ServiceProviderTest $session->expects($this->exactly(3)) ->method('get') - ->with('uid') + ->with('user_id') ->willReturnOnConsecutiveCalls( null, 42, @@ -114,16 +117,13 @@ class AuthenticatorTest extends ServiceProviderTest /** @var User|MockObject $user */ $user = $this->createMock(User::class); - $user->expects($this->once()) - ->method('save'); - - $session->expects($this->exactly(2)) + $session->expects($this->once()) ->method('get') - ->with('uid') + ->with('user_id') ->willReturn(42); $session->expects($this->once()) ->method('remove') - ->with('uid'); + ->with('user_id'); /** @var Authenticator|MockObject $auth */ $auth = $this->getMockBuilder(Authenticator::class) @@ -151,4 +151,115 @@ class AuthenticatorTest extends ServiceProviderTest // Permissions cached $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() { } + }; + } } diff --git a/tests/Unit/Http/UrlGeneratorServiceProviderTest.php b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php index 61bf3e7c..6d18f160 100644 --- a/tests/Unit/Http/UrlGeneratorServiceProviderTest.php +++ b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php @@ -19,7 +19,7 @@ class UrlGeneratorServiceProviderTest extends ServiceProviderTest $urlGenerator = $this->getMockBuilder(UrlGenerator::class) ->getMock(); - $app = $this->getApp(); + $app = $this->getApp(['make', 'instance', 'bind']); $this->setExpects($app, 'make', [UrlGenerator::class], $urlGenerator); $app->expects($this->exactly(2)) @@ -29,6 +29,9 @@ class UrlGeneratorServiceProviderTest extends ServiceProviderTest ['http.urlGenerator', $urlGenerator], [UrlGeneratorInterface::class, $urlGenerator] ); + $app->expects($this->once()) + ->method('bind') + ->with(UrlGeneratorInterface::class, UrlGenerator::class); $serviceProvider = new UrlGeneratorServiceProvider($app); $serviceProvider->register(); |