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
'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,

View File

@ -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

View File

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

View File

@ -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'));

View File

@ -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 {

View File

@ -8,14 +8,6 @@ use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User;
/**
* @return string
*/
function login_title()
{
return __('Login');
}
/**
* @return string
*/
@ -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);
}

View File

@ -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'));

View File

@ -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

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('login'), login_title());
$buttons[] = button(page_link_to('login'), __('Login'));
}
$faqUrl = config('faq_url');

View File

@ -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([

View File

@ -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

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 ""
"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

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;
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 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();
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[] */
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)) {
@ -131,6 +132,78 @@ class Authenticator
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
* @return array

View File

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

View File

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

View File

@ -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();
$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())
->method('invalidate');
$response->expects($this->once())
->method('redirectTo')
->with('https://foo.bar/');
$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];
}
}

View File

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

View File

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

View File

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

View File

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