summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIgor Scheller <igor.scheller@igorshp.de>2018-11-27 12:01:36 +0100
committerIgor Scheller <igor.scheller@igorshp.de>2019-07-08 01:57:59 +0200
commitbcce2625a8cb0b630d945c6849014049869e10ce (patch)
tree2031911a85a7a6a85015ff77ca8f9b326fa1da8e
parentfd4303f336173101b84ba21650e451ad536828fe (diff)
Implemented AuthController for login
* Moved /login functionality to AuthController * Refactored password handling logic to use the Authenticator
-rw-r--r--config/config.default.php9
-rw-r--r--config/routes.php2
-rw-r--r--db/migrations/2018_10_01_000000_create_users_tables.php2
-rw-r--r--includes/controller/users_controller.php13
-rw-r--r--includes/pages/admin_user.php2
-rw-r--r--includes/pages/guest_login.php119
-rw-r--r--includes/pages/user_settings.php5
-rw-r--r--includes/sys_auth.php68
-rw-r--r--includes/view/AngelTypes_view.php2
-rw-r--r--includes/view/User_view.php2
-rw-r--r--resources/lang/de_DE.UTF-8/LC_MESSAGES/default.po19
-rw-r--r--resources/lang/en_US.UTF-8/LC_MESSAGES/default.mobin0 -> 745 bytes
-rw-r--r--resources/lang/en_US.UTF-8/LC_MESSAGES/default.po26
-rw-r--r--resources/lang/pt_BR.UTF.8/LC_MESSAGES/default.mobin0 -> 41129 bytes
-rw-r--r--resources/lang/pt_BR.UTF.8/LC_MESSAGES/default.po (renamed from resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.po)14
-rw-r--r--resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.mobin41256 -> 0 bytes
-rw-r--r--resources/views/errors/405.twig5
-rw-r--r--resources/views/macros/base.twig11
-rw-r--r--resources/views/pages/login.twig104
-rw-r--r--src/Controllers/AuthController.php90
-rw-r--r--src/Helpers/Authenticator.php93
-rw-r--r--src/Helpers/AuthenticatorServiceProvider.php4
-rw-r--r--src/Middleware/LegacyMiddleware.php5
-rw-r--r--tests/Unit/Controllers/AuthControllerTest.php132
-rw-r--r--tests/Unit/Controllers/Stub/ControllerImplementation.php8
-rw-r--r--tests/Unit/Helpers/AuthenticatorServiceProviderTest.php9
-rw-r--r--tests/Unit/Helpers/AuthenticatorTest.php125
-rw-r--r--tests/Unit/Http/UrlGeneratorServiceProviderTest.php5
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') . ' &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);
-}
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
new file mode 100644
index 00000000..e95ae703
--- /dev/null
+++ b/resources/lang/en_US.UTF-8/LC_MESSAGES/default.mo
Binary files differ
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
new file mode 100644
index 00000000..8b864156
--- /dev/null
+++ b/resources/lang/pt_BR.UTF.8/LC_MESSAGES/default.mo
Binary files differ
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
deleted file mode 100644
index 95251feb..00000000
--- a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.mo
+++ /dev/null
Binary files differ
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') }} &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 %}
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();