Login: Added OAuth support
This commit is contained in:
parent
c57bb9395e
commit
80941c2999
|
@ -33,6 +33,7 @@
|
|||
"illuminate/container": "^7.6",
|
||||
"illuminate/database": "^7.6",
|
||||
"illuminate/support": "^7.6",
|
||||
"league/oauth2-client": "^2.6",
|
||||
"nikic/fast-route": "^1.3",
|
||||
"nyholm/psr7": "^1.1",
|
||||
"psr/container": "^1.0",
|
||||
|
|
|
@ -63,6 +63,41 @@ return [
|
|||
'sendmail' => env('MAIL_SENDMAIL', '/usr/sbin/sendmail -bs'),
|
||||
],
|
||||
|
||||
'oauth' => [
|
||||
// '[name]' => [config]
|
||||
/*
|
||||
'name' => [
|
||||
// Auth client ID
|
||||
'client_id' => 'engelsystem',
|
||||
// Auth client secret
|
||||
'client_secret' => '[generated by provider]',
|
||||
// Authentication URL
|
||||
'url_auth' => '[generated by provider]',
|
||||
// Token URL
|
||||
'url_token' => '[generated by provider]',
|
||||
// User info URL which provides userdata
|
||||
'url_info' => '[generated by provider]',
|
||||
// Info unique user id field
|
||||
'id' => 'uuid',
|
||||
// The following fields are used for registration
|
||||
// Info username field (optional)
|
||||
'username' => 'nickname',
|
||||
// Info email field (optional)
|
||||
'email' => 'email',
|
||||
// Info first name field (optional)
|
||||
'first_name' => 'first-name',
|
||||
// Info last name field (optional)
|
||||
'last_name' => 'last-name',
|
||||
// User URL to provider, shown on provider settings page (optional)
|
||||
'url' => '[provider page]',
|
||||
// Only show after clicking the page title (optional)
|
||||
'hidden' => false,
|
||||
// Mark user as arrived when using this provider (optional)
|
||||
'mark_arrived' => false,
|
||||
],
|
||||
*/
|
||||
],
|
||||
|
||||
// Default theme, 1=style1.css
|
||||
'theme' => env('THEME', 1),
|
||||
|
||||
|
@ -222,5 +257,5 @@ return [
|
|||
'Contribution' => 'Please visit [engelsystem/engelsystem](https://github.com/engelsystem/engelsystem) if '
|
||||
. 'you want to to contribute, have found any [bugs](https://github.com/engelsystem/engelsystem/issues) '
|
||||
. 'or need help.'
|
||||
]
|
||||
],
|
||||
];
|
||||
|
|
|
@ -14,6 +14,14 @@ $route->get('/login', 'AuthController@login');
|
|||
$route->post('/login', 'AuthController@postLogin');
|
||||
$route->get('/logout', 'AuthController@logout');
|
||||
|
||||
// OAuth
|
||||
$route->get('/oauth/{provider:\w+}', 'OAuthController@index');
|
||||
$route->post('/oauth/{provider:\w+}/connect', 'OAuthController@connect');
|
||||
$route->post('/oauth/{provider:\w+}/disconnect', 'OAuthController@disconnect');
|
||||
|
||||
// User settings
|
||||
$route->get('/settings/oauth', 'SettingsController@oauth');
|
||||
|
||||
// Password recovery
|
||||
$route->get('/password/reset', 'PasswordResetController@reset');
|
||||
$route->post('/password/reset', 'PasswordResetController@postReset');
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Engelsystem\Migrations;
|
||||
|
||||
use Engelsystem\Database\Migration\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateOauthTable extends Migration
|
||||
{
|
||||
use Reference;
|
||||
|
||||
/**
|
||||
* Run the migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$this->schema->create('oauth', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$this->referencesUser($table);
|
||||
$table->string('provider');
|
||||
$table->string('identifier');
|
||||
$table->unique(['provider', 'identifier']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$this->schema->drop('oauth');
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use Carbon\Carbon;
|
||||
use Engelsystem\Database\DB;
|
||||
use Engelsystem\Models\OAuth;
|
||||
use Engelsystem\Models\User\Contact;
|
||||
use Engelsystem\Models\User\PersonalData;
|
||||
use Engelsystem\Models\User\Settings;
|
||||
|
@ -224,6 +225,19 @@ function guest_register()
|
|||
->associate($user)
|
||||
->save();
|
||||
|
||||
if ($session->has('oauth2_connect_provider') && $session->has('oauth2_user_id')) {
|
||||
$oauth = new OAuth([
|
||||
'provider' => $session->get('oauth2_connect_provider'),
|
||||
'identifier' => $session->get('oauth2_user_id'),
|
||||
]);
|
||||
$oauth->user()
|
||||
->associate($user)
|
||||
->save();
|
||||
|
||||
$session->remove('oauth2_connect_provider');
|
||||
$session->remove('oauth2_user_id');
|
||||
}
|
||||
|
||||
// Assign user-group and set password
|
||||
DB::insert('INSERT INTO `UserGroups` (`uid`, `group_id`) VALUES (?, -20)', [$user->id]);
|
||||
auth()->setPassword($user, $request->postData('password'));
|
||||
|
@ -254,6 +268,13 @@ function guest_register()
|
|||
info((new Parsedown())->text($message));
|
||||
}
|
||||
|
||||
// Login the user
|
||||
if ($user->oauth->count()) {
|
||||
/** @var OAuth $provider */
|
||||
$provider = $user->oauth->first();
|
||||
throw_redirect(url('/oauth/' . $provider->provider));
|
||||
}
|
||||
|
||||
throw_redirect(page_link_to('/'));
|
||||
}
|
||||
}
|
||||
|
@ -270,6 +291,24 @@ function guest_register()
|
|||
$teardown_end_date = $teardown->getTimestamp();
|
||||
}
|
||||
|
||||
$form_data = $session->get('form_data');
|
||||
$session->remove('form_data');
|
||||
if (!$nick && !empty($form_data['name'])) {
|
||||
$nick = $form_data['name'];
|
||||
}
|
||||
|
||||
if (!$email && !empty($form_data['email'])) {
|
||||
$email = $form_data['email'];
|
||||
}
|
||||
|
||||
if (!$preName && !empty($form_data['first_name'])) {
|
||||
$preName = $form_data['first_name'];
|
||||
}
|
||||
|
||||
if (!$lastName && !empty($form_data['last_name'])) {
|
||||
$lastName = $form_data['last_name'];
|
||||
}
|
||||
|
||||
return page_with_title(register_title(), [
|
||||
__('By completing this form you\'re registering as a Chaos-Angel. This script will create you an account in the angel task scheduler.'),
|
||||
$msg,
|
||||
|
|
|
@ -198,6 +198,7 @@ function user_settings()
|
|||
$enable_tshirt_size = config('enable_tshirt_size');
|
||||
$tshirt_sizes = config('tshirt_sizes');
|
||||
$locales = config('locales');
|
||||
$oauth2_providers = config('oauth');
|
||||
|
||||
$buildup_start_date = null;
|
||||
$teardown_end_date = null;
|
||||
|
@ -230,6 +231,7 @@ function user_settings()
|
|||
$buildup_start_date,
|
||||
$teardown_end_date,
|
||||
$enable_tshirt_size,
|
||||
$tshirt_sizes
|
||||
$tshirt_sizes,
|
||||
$oauth2_providers
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ use Illuminate\Support\Collection;
|
|||
* @param int $teardown_end_date Unix timestamp
|
||||
* @param bool $enable_tshirt_size
|
||||
* @param array $tshirt_sizes
|
||||
* @param array $oauth2_providers
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function User_settings_view(
|
||||
|
@ -25,7 +27,8 @@ function User_settings_view(
|
|||
$buildup_start_date,
|
||||
$teardown_end_date,
|
||||
$enable_tshirt_size,
|
||||
$tshirt_sizes
|
||||
$tshirt_sizes,
|
||||
$oauth2_providers
|
||||
) {
|
||||
$personalData = $user_source->personalData;
|
||||
$enable_user_name = config('enable_user_name');
|
||||
|
@ -33,6 +36,11 @@ function User_settings_view(
|
|||
$enable_dect = config('enable_dect');
|
||||
$enable_planned_arrival = config('enable_planned_arrival');
|
||||
|
||||
$showOauth = false;
|
||||
foreach ($oauth2_providers as $name => $config) {
|
||||
$showOauth = $showOauth || !isset($config['hidden']) || !$config['hidden'];
|
||||
}
|
||||
|
||||
return page_with_title(settings_title(), [
|
||||
msg(),
|
||||
div('row', [
|
||||
|
@ -93,6 +101,11 @@ function User_settings_view(
|
|||
])
|
||||
]),
|
||||
div('col-md-6', [
|
||||
($showOauth ?
|
||||
form_info(__('oauth.login'))
|
||||
. button(url('/settings/oauth'), __('settings.oauth'), 'btn-primary')
|
||||
: ''
|
||||
),
|
||||
form([
|
||||
form_info(__('Here you can change your password.')),
|
||||
form_password('password', __('Old password:')),
|
||||
|
@ -109,7 +122,7 @@ function User_settings_view(
|
|||
form_info(__('Here you can choose your language:')),
|
||||
form_select('language', __('Language:'), $locales, $user_source->settings->language),
|
||||
form_submit('submit_language', __('Save'))
|
||||
])
|
||||
]),
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
|
|
@ -181,6 +181,18 @@ $(function () {
|
|||
$('select').select2();
|
||||
});
|
||||
|
||||
/**
|
||||
* Show oauth buttons on welcome title click
|
||||
*/
|
||||
$(function () {
|
||||
$('#welcome-title').on('click', function () {
|
||||
$('.form-group.btn-group .btn.hidden').removeClass('hidden');
|
||||
});
|
||||
$('#oauth-settings-title').on('click', function () {
|
||||
$('table tr.hidden').removeClass('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Set the filter selects to latest state
|
||||
*
|
||||
|
|
|
@ -258,6 +258,10 @@ h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
|||
margin-bottom: @form-group-margin-bottom;
|
||||
}
|
||||
|
||||
.user-settings .settings-menu ul {
|
||||
margin-top: @line-height-computed;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
|
|
@ -81,3 +81,24 @@ msgid "news.edit.contains-html"
|
|||
msgstr ""
|
||||
"Diese Nachricht beinhaltet HTML. Wenn du sie speicherst gehen diese "
|
||||
"Formatierungen verloren!"
|
||||
|
||||
msgid "oauth.invalid-state"
|
||||
msgstr "Ungültiger OAuth state"
|
||||
|
||||
msgid "oauth.provider-error"
|
||||
msgstr "OAuth provider fehler"
|
||||
|
||||
msgid "oauth.already-connected"
|
||||
msgstr "Dieser Account wurde bereits mit einem anderen Account verbunden!"
|
||||
|
||||
msgid "oauth.connected"
|
||||
msgstr "Login provider verbunden!"
|
||||
|
||||
msgid "oauth.disconnected"
|
||||
msgstr "Login provider getrennt!"
|
||||
|
||||
msgid "oauth.not-found"
|
||||
msgstr "Account nicht gefunden"
|
||||
|
||||
msgid "oauth.provider-not-found"
|
||||
msgstr "OAuth provider nicht gefunden"
|
||||
|
|
|
@ -2892,3 +2892,22 @@ msgstr "Level"
|
|||
|
||||
msgid "log.message"
|
||||
msgstr "Nachricht"
|
||||
|
||||
|
||||
msgid "settings.settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
msgid "settings.oauth"
|
||||
msgstr "OAuth Einstellungen"
|
||||
|
||||
msgid "oauth.login"
|
||||
msgstr "Login mit OAuth"
|
||||
|
||||
msgid "oauth.login-using-provider"
|
||||
msgstr "Login mit %s"
|
||||
|
||||
msgid "form.connect"
|
||||
msgstr "Verbinden"
|
||||
|
||||
msgid "form.disconnect"
|
||||
msgstr "Trennen"
|
||||
|
|
|
@ -77,3 +77,24 @@ msgstr "News successfully deleted."
|
|||
|
||||
msgid "news.edit.contains-html"
|
||||
msgstr "This message contains HTML. After saving the post some formatting will be lost!"
|
||||
|
||||
msgid "oauth.invalid-state"
|
||||
msgstr "Invalid OAuth state"
|
||||
|
||||
msgid "oauth.provider-error"
|
||||
msgstr "OAuth provider error"
|
||||
|
||||
msgid "oauth.already-connected"
|
||||
msgstr "This account is already connected to another account!"
|
||||
|
||||
msgid "oauth.connected"
|
||||
msgstr "Connected login provider!"
|
||||
|
||||
msgid "oauth.disconnected"
|
||||
msgstr "Disconnected login provider!"
|
||||
|
||||
msgid "oauth.not-found"
|
||||
msgstr "Unable to find account"
|
||||
|
||||
msgid "oauth.provider-not-found"
|
||||
msgstr "Unable to find OAuth provider"
|
||||
|
|
|
@ -159,3 +159,21 @@ msgstr "Level"
|
|||
|
||||
msgid "log.message"
|
||||
msgstr "Message"
|
||||
|
||||
msgid "settings.settings"
|
||||
msgstr "Settings"
|
||||
|
||||
msgid "settings.oauth"
|
||||
msgstr "OAuth Settings"
|
||||
|
||||
msgid "oauth.login"
|
||||
msgstr "Login using OAuth"
|
||||
|
||||
msgid "oauth.login-using-provider"
|
||||
msgstr "Login using %s"
|
||||
|
||||
msgid "form.connect"
|
||||
msgstr "Connect"
|
||||
|
||||
msgid "form.disconnect"
|
||||
msgstr "Disconnect"
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
</h2>
|
||||
|
||||
{% block content_text %}
|
||||
{{ content }}
|
||||
{{ __(content) }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content_text %}
|
||||
{{ content }}
|
||||
{{ __(content) }}
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
{% block content %}
|
||||
<div class="col-md-12">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 text-center">
|
||||
<div class="col-sm-12 text-center" id="welcome-title">
|
||||
<h2>{{ __('Welcome to the %s!', [config('name') ~ m.angel() ~ (config('app_name')|upper) ])|raw }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,6 +66,14 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group btn-group btn-group-justified">
|
||||
{% for type,config in config('oauth') %}
|
||||
<a href="{{ url('oauth/' ~ type) }}" class="btn btn-primary btn-lg{% if config.hidden|default(false) %} hidden{% endif %}">
|
||||
{{ __('oauth.login-using-provider', [type|capitalize]) }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{{ url('/password/reset') }}" class="">
|
||||
{{ __('I forgot my password') }}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
{% extends 'pages/settings/settings.twig' %}
|
||||
{% import 'macros/form.twig' as f %}
|
||||
|
||||
{% block title %}{{ __('settings.oauth') }}{% endblock %}
|
||||
|
||||
{% block container_title %}
|
||||
<h1 id="oauth-settings-title">{{ block('title') }}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block row_content %}
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
{% for name,config in providers %}
|
||||
<tr{% if config.hidden|default(false) %} class="hidden"{% endif %}>
|
||||
<th>
|
||||
{% if config.url|default %}
|
||||
<a href="{{ config.url }}" target="_blank" rel="noopener">{{ name|capitalize }}</a>
|
||||
{% else %}
|
||||
{{ name|capitalize }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<td>
|
||||
{% if not user.oauth.contains('provider', name) %}
|
||||
<form method="POST" action="{{ url('/oauth/' ~ name ~ '/connect') }}">
|
||||
{{ csrf() }}
|
||||
|
||||
{{ f.submit(__('form.connect')) }}
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="POST" action="{{ url('/oauth/' ~ name ~ '/disconnect') }}">
|
||||
{{ csrf() }}
|
||||
|
||||
{{ f.submit(__('form.disconnect'), {'btn_type': 'danger'}) }}
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -0,0 +1,35 @@
|
|||
{% extends 'layouts/app.twig' %}
|
||||
|
||||
{% block title %}{{ __('settings') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container user-settings">
|
||||
<div class="row">
|
||||
<div class="col-md-2 settings-menu">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
{% for url,title in {
|
||||
(url('/user-settings')): __('settings.settings'),
|
||||
(url('/settings/oauth')): __('settings.oauth'),
|
||||
} %}
|
||||
<li{% if url == request.url() %} class="active"{% endif %}>
|
||||
<a href="{{ url }}">{{ title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-10">
|
||||
{% block container_title %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% include 'layouts/parts/messages.twig' %}
|
||||
|
||||
<div class="row">
|
||||
{% block row_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -97,6 +97,16 @@ class AuthController extends BaseController
|
|||
return $this->showLogin();
|
||||
}
|
||||
|
||||
return $this->loginUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function loginUser(User $user): Response
|
||||
{
|
||||
$this->session->invalidate();
|
||||
$this->session->set('user_id', $user->id);
|
||||
$this->session->set('locale', $user->settings->language);
|
||||
|
|
|
@ -0,0 +1,314 @@
|
|||
<?php
|
||||
|
||||
namespace Engelsystem\Controllers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Engelsystem\Config\Config;
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Engelsystem\Http\Exceptions\HttpNotFound;
|
||||
use Engelsystem\Http\Redirector;
|
||||
use Engelsystem\Http\Request;
|
||||
use Engelsystem\Http\Response;
|
||||
use Engelsystem\Http\UrlGenerator;
|
||||
use Engelsystem\Models\OAuth;
|
||||
use Illuminate\Support\Collection;
|
||||
use League\OAuth2\Client\Provider\AbstractProvider;
|
||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||
use League\OAuth2\Client\Provider\GenericProvider;
|
||||
use League\OAuth2\Client\Provider\ResourceOwnerInterface as ResourceOwner;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Session as Session;
|
||||
|
||||
class OAuthController extends BaseController
|
||||
{
|
||||
use HasUserNotifications;
|
||||
|
||||
/** @var Authenticator */
|
||||
protected $auth;
|
||||
|
||||
/** @var AuthController */
|
||||
protected $authController;
|
||||
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
/** @var LoggerInterface */
|
||||
protected $log;
|
||||
|
||||
/** @var OAuth */
|
||||
protected $oauth;
|
||||
|
||||
/** @var Redirector */
|
||||
protected $redirector;
|
||||
|
||||
/** @var Session */
|
||||
protected $session;
|
||||
|
||||
/** @var UrlGenerator */
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @param Authenticator $auth
|
||||
* @param AuthController $authController
|
||||
* @param Config $config
|
||||
* @param LoggerInterface $log
|
||||
* @param OAuth $oauth
|
||||
* @param Redirector $redirector
|
||||
* @param Session $session
|
||||
* @param UrlGenerator $url
|
||||
*/
|
||||
public function __construct(
|
||||
Authenticator $auth,
|
||||
AuthController $authController,
|
||||
Config $config,
|
||||
LoggerInterface $log,
|
||||
OAuth $oauth,
|
||||
Redirector $redirector,
|
||||
Session $session,
|
||||
UrlGenerator $url
|
||||
) {
|
||||
$this->auth = $auth;
|
||||
$this->authController = $authController;
|
||||
$this->config = $config;
|
||||
$this->log = $log;
|
||||
$this->redirector = $redirector;
|
||||
$this->oauth = $oauth;
|
||||
$this->session = $session;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$providerName = $request->getAttribute('provider');
|
||||
$provider = $this->getProvider($providerName);
|
||||
|
||||
if (!$request->has('code')) {
|
||||
$authorizationUrl = $provider->getAuthorizationUrl();
|
||||
$this->session->set('oauth2_state', $provider->getState());
|
||||
|
||||
return $this->redirector->to($authorizationUrl);
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->session->get('oauth2_state')
|
||||
|| $request->get('state') !== $this->session->get('oauth2_state')
|
||||
) {
|
||||
$this->session->remove('oauth2_state');
|
||||
|
||||
$this->log->warning('Invalid OAuth state');
|
||||
|
||||
throw new HttpNotFound('oauth.invalid-state');
|
||||
}
|
||||
|
||||
try {
|
||||
$accessToken = $provider->getAccessToken(
|
||||
'authorization_code',
|
||||
[
|
||||
'code' => $request->get('code')
|
||||
]
|
||||
);
|
||||
} catch (IdentityProviderException $e) {
|
||||
$this->log->error(
|
||||
'{provider} identity provider error: {error} {description}',
|
||||
[
|
||||
'provider' => $providerName,
|
||||
'error' => $e->getMessage(),
|
||||
'description' => $e->getResponseBody()['error_description'] ?: '',
|
||||
]
|
||||
);
|
||||
|
||||
throw new HttpNotFound('oauth.provider-error');
|
||||
}
|
||||
|
||||
$resourceOwner = $provider->getResourceOwner($accessToken);
|
||||
|
||||
/** @var OAuth|null $oauth */
|
||||
$oauth = $this->oauth
|
||||
->query()
|
||||
->where('provider', $providerName)
|
||||
->where('identifier', $resourceOwner->getId())
|
||||
->first();
|
||||
|
||||
$user = $this->auth->user();
|
||||
if ($oauth && $user && $user->id != $oauth->user_id) {
|
||||
throw new HttpNotFound('oauth.already-connected');
|
||||
}
|
||||
|
||||
$connectProvider = $this->session->get('oauth2_connect_provider');
|
||||
$this->session->remove('oauth2_connect_provider');
|
||||
if (!$oauth && $user && $connectProvider && $connectProvider == $providerName) {
|
||||
$oauth = new OAuth(['provider' => $providerName, 'identifier' => $resourceOwner->getId()]);
|
||||
$oauth->user()
|
||||
->associate($user)
|
||||
->save();
|
||||
|
||||
$this->log->info(
|
||||
'Connected OAuth user {user} using {provider}',
|
||||
['provider' => $providerName, 'user' => $resourceOwner->getId()]
|
||||
);
|
||||
$this->addNotification('oauth.connected');
|
||||
}
|
||||
|
||||
$config = ($this->config->get('oauth')[$providerName]);
|
||||
$userdata = new Collection($resourceOwner->toArray());
|
||||
if (!$oauth) {
|
||||
return $this->redirectRegisterOrThrowNotFound($providerName, $resourceOwner->getId(), $config, $userdata);
|
||||
}
|
||||
|
||||
if (isset($config['mark_arrived']) && $config['mark_arrived']) {
|
||||
$this->handleArrive($providerName, $oauth, $resourceOwner);
|
||||
}
|
||||
|
||||
return $this->authController->loginUser($oauth->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function connect(Request $request): Response
|
||||
{
|
||||
$provider = $request->getAttribute('provider');
|
||||
$this->requireProvider($provider);
|
||||
|
||||
$this->session->set('oauth2_connect_provider', $provider);
|
||||
|
||||
return $this->index($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function disconnect(Request $request): Response
|
||||
{
|
||||
$provider = $request->getAttribute('provider');
|
||||
|
||||
$this->oauth
|
||||
->whereUserId($this->auth->user()->id)
|
||||
->where('provider', $provider)
|
||||
->delete();
|
||||
|
||||
$this->log->info('Disconnected OAuth from {provider}', ['provider' => $provider]);
|
||||
$this->addNotification('oauth.disconnected');
|
||||
|
||||
return $this->redirector->back();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return AbstractProvider
|
||||
*/
|
||||
protected function getProvider(string $name): AbstractProvider
|
||||
{
|
||||
$this->requireProvider($name);
|
||||
$config = $this->config->get('oauth')[$name];
|
||||
|
||||
return new GenericProvider(
|
||||
[
|
||||
'clientId' => $config['client_id'],
|
||||
'clientSecret' => $config['client_secret'],
|
||||
'redirectUri' => $this->url->to('oauth/' . $name),
|
||||
'urlAuthorize' => $config['url_auth'],
|
||||
'urlAccessToken' => $config['url_token'],
|
||||
'urlResourceOwnerDetails' => $config['url_info'],
|
||||
'responseResourceOwnerId' => $config['id'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $provider
|
||||
*/
|
||||
protected function requireProvider(string $provider): void
|
||||
{
|
||||
if (!$this->isValidProvider($provider)) {
|
||||
throw new HttpNotFound('oauth.provider-not-found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isValidProvider(string $name): bool
|
||||
{
|
||||
$config = $this->config->get('oauth');
|
||||
|
||||
return isset($config[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OAuth $auth
|
||||
* @param string $providerName
|
||||
* @param ResourceOwner $resourceOwner
|
||||
*/
|
||||
protected function handleArrive(
|
||||
string $providerName,
|
||||
OAuth $auth,
|
||||
ResourceOwner $resourceOwner
|
||||
): void {
|
||||
$user = $auth->user;
|
||||
$userState = $user->state;
|
||||
|
||||
if ($userState->arrived) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userState->arrived = true;
|
||||
$userState->arrival_date = new Carbon();
|
||||
$userState->save();
|
||||
|
||||
$this->log->info(
|
||||
'Set user {name} ({id}) as arrived via {provider} user {user}',
|
||||
[
|
||||
'provider' => $providerName,
|
||||
'user' => $resourceOwner->getId(),
|
||||
'name' => $user->name,
|
||||
'id' => $user->id
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $providerName
|
||||
* @param string $providerUserIdentifier
|
||||
* @param array $config
|
||||
* @param Collection $userdata
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function redirectRegisterOrThrowNotFound(
|
||||
string $providerName,
|
||||
string $providerUserIdentifier,
|
||||
array $config,
|
||||
Collection $userdata
|
||||
): Response {
|
||||
if (!$this->config->get('registration_enabled')) {
|
||||
throw new HttpNotFound('oauth.not-found');
|
||||
}
|
||||
|
||||
$this->session->set(
|
||||
'form_data',
|
||||
[
|
||||
'name' => $userdata->get($config['username']),
|
||||
'email' => $userdata->get($config['email']),
|
||||
'first_name' => $userdata->get($config['first_name']),
|
||||
'last_name' => $userdata->get($config['last_name']),
|
||||
],
|
||||
);
|
||||
$this->session->set('oauth2_connect_provider', $providerName);
|
||||
$this->session->set('oauth2_user_id', $providerUserIdentifier);
|
||||
|
||||
return $this->redirector->to('/register');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace Engelsystem\Controllers;
|
||||
|
||||
use Engelsystem\Config\Config;
|
||||
use Engelsystem\Http\Exceptions\HttpNotFound;
|
||||
use Engelsystem\Http\Response;
|
||||
|
||||
class SettingsController extends BaseController
|
||||
{
|
||||
use HasUserNotifications;
|
||||
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
/** @var Response */
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* @param Config $config
|
||||
* @param Response $response
|
||||
*/
|
||||
public function __construct(
|
||||
Config $config,
|
||||
Response $response
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*/
|
||||
public function oauth(): Response
|
||||
{
|
||||
$providers = $this->config->get('oauth');
|
||||
if (empty($providers)) {
|
||||
throw new HttpNotFound();
|
||||
}
|
||||
|
||||
return $this->response->withView(
|
||||
'pages/settings/oauth.twig',
|
||||
[
|
||||
'providers' => $providers,
|
||||
] + $this->getNotifications(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Engelsystem\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Engelsystem\Models\User\UsesUserModel;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $provider
|
||||
* @property string $identifier
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
*
|
||||
* @method static QueryBuilder|OAuth[] whereId($value)
|
||||
* @method static QueryBuilder|OAuth[] whereProvider($value)
|
||||
* @method static QueryBuilder|OAuth[] whereIdentifier($value)
|
||||
*/
|
||||
class OAuth extends BaseModel
|
||||
{
|
||||
use UsesUserModel;
|
||||
|
||||
/** @var string */
|
||||
public $table = 'oauth';
|
||||
|
||||
/** @var bool Enable timestamps */
|
||||
public $timestamps = true;
|
||||
|
||||
/** @var array */
|
||||
protected $fillable = [
|
||||
'provider',
|
||||
'identifier',
|
||||
];
|
||||
}
|
|
@ -7,6 +7,7 @@ use Engelsystem\Models\BaseModel;
|
|||
use Engelsystem\Models\Message;
|
||||
use Engelsystem\Models\News;
|
||||
use Engelsystem\Models\NewsComment;
|
||||
use Engelsystem\Models\OAuth;
|
||||
use Engelsystem\Models\Question;
|
||||
use Engelsystem\Models\Worklog;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
@ -30,10 +31,12 @@ use Illuminate\Database\Query\Builder as QueryBuilder;
|
|||
* @property-read QueryBuilder|State $state
|
||||
* @property-read Collection|News[] $news
|
||||
* @property-read Collection|NewsComment[] $newsComments
|
||||
* @property-read Collection|OAuth[] $oauth
|
||||
* @property-read Collection|Worklog[] $worklogs
|
||||
* @property-read Collection|Worklog[] $worklogsCreated
|
||||
* @property-read int|null $news_count
|
||||
* @property-read int|null $news_comments_count
|
||||
* @property-read int|null $oauth_count
|
||||
* @property-read int|null $worklogs_count
|
||||
* @property-read int|null $worklogs_created_count
|
||||
*
|
||||
|
@ -133,6 +136,14 @@ class User extends BaseModel
|
|||
return $this->hasMany(NewsComment::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany
|
||||
*/
|
||||
public function oauth(): HasMany
|
||||
{
|
||||
return $this->hasMany(OAuth::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany
|
||||
*/
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Engelsystem\Renderer\Twig\Extensions;
|
||||
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Engelsystem\Http\Request;
|
||||
use Twig\Extension\AbstractExtension as TwigExtension;
|
||||
use Twig\Extension\GlobalsInterface as GlobalsInterface;
|
||||
|
||||
|
@ -11,12 +12,17 @@ class Globals extends TwigExtension implements GlobalsInterface
|
|||
/** @var Authenticator */
|
||||
protected $auth;
|
||||
|
||||
/** @var Request */
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @param Authenticator $auth
|
||||
* @param Request $request
|
||||
*/
|
||||
public function __construct(Authenticator $auth)
|
||||
public function __construct(Authenticator $auth, Request $request)
|
||||
{
|
||||
$this->auth = $auth;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,7 +35,8 @@ class Globals extends TwigExtension implements GlobalsInterface
|
|||
$user = $this->auth->user();
|
||||
|
||||
return [
|
||||
'user' => $user ? $user : [],
|
||||
'user' => $user ? $user : [],
|
||||
'request' => $this->request,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,21 +73,7 @@ class AuthControllerTest extends TestCase
|
|||
$validator = new Validator();
|
||||
$session->set('errors', [['bar' => 'some.bar.error']]);
|
||||
$this->app->instance('session', $session);
|
||||
|
||||
$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();
|
||||
$user = $this->createUser();
|
||||
|
||||
$auth->expects($this->exactly(2))
|
||||
->method('authenticate')
|
||||
|
@ -101,14 +87,20 @@ class AuthControllerTest extends TestCase
|
|||
$this->assertArraySubset(['errors' => collect(['some.bar.error', 'auth.not-found'])], $data);
|
||||
return $response;
|
||||
});
|
||||
$redirect->expects($this->once())
|
||||
->method('to')
|
||||
->with('news')
|
||||
|
||||
/** @var AuthController|MockObject $controller */
|
||||
$controller = $this->getMockBuilder(AuthController::class)
|
||||
->setConstructorArgs([$response, $session, $redirect, $config, $auth])
|
||||
->onlyMethods(['loginUser'])
|
||||
->getMock();
|
||||
$controller->setValidator($validator);
|
||||
|
||||
$controller->expects($this->once())
|
||||
->method('loginUser')
|
||||
->with($user)
|
||||
->willReturn($response);
|
||||
|
||||
// No credentials
|
||||
$controller = new AuthController($response, $session, $redirect, $config, $auth);
|
||||
$controller->setValidator($validator);
|
||||
try {
|
||||
$controller->postLogin($request);
|
||||
$this->fail('Login without credentials possible');
|
||||
|
@ -130,7 +122,34 @@ class AuthControllerTest extends TestCase
|
|||
|
||||
// Authenticated user
|
||||
$controller->postLogin($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\AuthController::loginUser
|
||||
*/
|
||||
public function testLoginUser()
|
||||
{
|
||||
$this->initDatabase();
|
||||
|
||||
/** @var Response|MockObject $response */
|
||||
$response = $this->createMock(Response::class);
|
||||
/** @var Redirector|MockObject $redirect */
|
||||
/** @var Config $config */
|
||||
/** @var Authenticator|MockObject $auth */
|
||||
list(, , $redirect, $config, $auth) = $this->getMocks();
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$session->set('foo', 'bar');
|
||||
$user = $this->createUser();
|
||||
|
||||
$redirect->expects($this->once())
|
||||
->method('to')
|
||||
->with('news')
|
||||
->willReturn($response);
|
||||
|
||||
$controller = new AuthController($response, $session, $redirect, $config, $auth);
|
||||
$controller->loginUser($user);
|
||||
|
||||
$this->assertFalse($session->has('foo'));
|
||||
$this->assertNotNull($user->last_login_at);
|
||||
$this->assertEquals(['user_id' => 42, 'locale' => 'de_DE'], $session->all());
|
||||
}
|
||||
|
@ -161,6 +180,29 @@ class AuthControllerTest extends TestCase
|
|||
$this->assertEquals($response, $return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
protected function createUser(): User
|
||||
{
|
||||
$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();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,517 @@
|
|||
<?php
|
||||
|
||||
namespace Engelsystem\Test\Unit\Controllers;
|
||||
|
||||
use Engelsystem\Config\Config;
|
||||
use Engelsystem\Controllers\AuthController;
|
||||
use Engelsystem\Controllers\OAuthController;
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Engelsystem\Http\Exceptions\HttpNotFound;
|
||||
use Engelsystem\Http\Redirector;
|
||||
use Engelsystem\Http\Request;
|
||||
use Engelsystem\Http\Response;
|
||||
use Engelsystem\Http\UrlGenerator;
|
||||
use Engelsystem\Models\OAuth;
|
||||
use Engelsystem\Models\User\User;
|
||||
use Engelsystem\Test\Unit\HasDatabase;
|
||||
use Engelsystem\Test\Unit\TestCase;
|
||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||
use League\OAuth2\Client\Provider\GenericProvider;
|
||||
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
|
||||
use League\OAuth2\Client\Token\AccessToken;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\Test\TestLogger;
|
||||
use Symfony\Component\HttpFoundation\Session\Session as Session;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||
|
||||
class OAuthControllerTest extends TestCase
|
||||
{
|
||||
use HasDatabase;
|
||||
|
||||
/** @var Authenticator|MockObject */
|
||||
protected $auth;
|
||||
|
||||
/** @var AuthController|MockObject */
|
||||
protected $authController;
|
||||
|
||||
/** @var User */
|
||||
protected $authenticatedUser;
|
||||
|
||||
/** @var User */
|
||||
protected $otherAuthenticatedUser;
|
||||
|
||||
/** @var User */
|
||||
protected $otherUser;
|
||||
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
/** @var TestLogger */
|
||||
protected $log;
|
||||
|
||||
/** @var OAuth */
|
||||
protected $oauth;
|
||||
|
||||
/** @var Redirector|MockObject $redirect */
|
||||
protected $redirect;
|
||||
|
||||
/** @var Session */
|
||||
protected $session;
|
||||
|
||||
/** @var UrlGenerator|MockObject */
|
||||
protected $url;
|
||||
|
||||
/** @var string[][] */
|
||||
protected $oauthConfig = [
|
||||
'testprovider' => [
|
||||
'client_id' => 'testsystem',
|
||||
'client_secret' => 'foo-bar-baz',
|
||||
'url_auth' => 'http://localhost/auth',
|
||||
'url_token' => 'http://localhost/token',
|
||||
'url_info' => 'http://localhost/info',
|
||||
'id' => 'uid',
|
||||
'username' => 'user',
|
||||
'email' => 'email',
|
||||
'first_name' => 'given-name',
|
||||
'last_name' => 'last-name',
|
||||
'url' => 'http://localhost/',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\OAuthController::__construct
|
||||
* @covers \Engelsystem\Controllers\OAuthController::index
|
||||
* @covers \Engelsystem\Controllers\OAuthController::handleArrive
|
||||
*/
|
||||
public function testIndexArrive()
|
||||
{
|
||||
$request = new Request();
|
||||
$request = $request
|
||||
->withAttribute('provider', 'testprovider')
|
||||
->withQueryParams(['code' => 'lorem-ipsum-code', 'state' => 'some-internal-state']);
|
||||
|
||||
$this->session->set('oauth2_state', 'some-internal-state');
|
||||
$this->session->set('oauth2_connect_provider', 'testprovider');
|
||||
|
||||
$accessToken = $this->createMock(AccessToken::class);
|
||||
|
||||
/** @var ResourceOwnerInterface|MockObject $resourceOwner */
|
||||
$resourceOwner = $this->createMock(ResourceOwnerInterface::class);
|
||||
$this->setExpects($resourceOwner, 'toArray', null, [], $this->atLeastOnce());
|
||||
$resourceOwner->expects($this->exactly(7))
|
||||
->method('getId')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
'other-provider-user-identifier',
|
||||
'other-provider-user-identifier',
|
||||
'other-provider-user-identifier',
|
||||
'other-provider-user-identifier',
|
||||
'provider-user-identifier',
|
||||
'provider-user-identifier',
|
||||
'provider-user-identifier'
|
||||
);
|
||||
|
||||
/** @var GenericProvider|MockObject $provider */
|
||||
$provider = $this->createMock(GenericProvider::class);
|
||||
$this->setExpects(
|
||||
$provider,
|
||||
'getAccessToken',
|
||||
['authorization_code', ['code' => 'lorem-ipsum-code']],
|
||||
$accessToken,
|
||||
$this->atLeastOnce()
|
||||
);
|
||||
$this->setExpects($provider, 'getResourceOwner', [$accessToken], $resourceOwner, $this->atLeastOnce());
|
||||
|
||||
$this->authController->expects($this->atLeastOnce())
|
||||
->method('loginUser')
|
||||
->willReturnCallback(function (User $user) {
|
||||
$this->assertTrue(in_array(
|
||||
$user->id,
|
||||
[$this->authenticatedUser->id, $this->otherUser->id]
|
||||
));
|
||||
|
||||
return new Response();
|
||||
});
|
||||
|
||||
$this->auth->expects($this->exactly(4))
|
||||
->method('user')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
$this->otherUser,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
$controller = $this->getMock(['getProvider', 'addNotification']);
|
||||
$this->setExpects($controller, 'getProvider', ['testprovider'], $provider, $this->atLeastOnce());
|
||||
$this->setExpects($controller, 'addNotification', ['oauth.connected']);
|
||||
|
||||
// Connect to provider
|
||||
$controller->index($request);
|
||||
$this->assertTrue($this->log->hasInfoThatContains('Connected OAuth'));
|
||||
$this->assertCount(1, $this->otherUser->oauth);
|
||||
|
||||
// Login using provider
|
||||
$controller->index($request);
|
||||
$this->assertFalse($this->session->has('oauth2_connect_provider'));
|
||||
$this->assertFalse((bool)User::find(1)->state->arrived);
|
||||
|
||||
// Mark as arrived
|
||||
$oauthConfig = $this->config->get('oauth');
|
||||
$oauthConfig['testprovider']['mark_arrived'] = true;
|
||||
$this->config->set('oauth', $oauthConfig);
|
||||
$controller->index($request);
|
||||
|
||||
$this->assertTrue((bool)User::find(1)->state->arrived);
|
||||
$this->assertTrue($this->log->hasInfoThatContains('as arrived'));
|
||||
$this->log->reset();
|
||||
|
||||
// Don't set arrived if already done
|
||||
$controller->index($request);
|
||||
$this->assertTrue((bool)User::find(1)->state->arrived);
|
||||
$this->assertFalse($this->log->hasInfoThatContains('as arrived'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\OAuthController::index
|
||||
* @covers \Engelsystem\Controllers\OAuthController::getProvider
|
||||
*/
|
||||
public function testIndexRedirectToProvider()
|
||||
{
|
||||
$this->redirect->expects($this->once())
|
||||
->method('to')
|
||||
->willReturnCallback(function ($url) {
|
||||
$this->assertStringStartsWith('http://localhost/auth', $url);
|
||||
$this->assertStringContainsString('testsystem', $url);
|
||||
$this->assertStringContainsString('code', $url);
|
||||
return new Response();
|
||||
});
|
||||
|
||||
$this->setExpects($this->url, 'to', ['oauth/testprovider'], 'http://localhost/oauth/testprovider');
|
||||
|
||||
$request = new Request();
|
||||
$request = $request
|
||||
->withAttribute('provider', 'testprovider');
|
||||
|
||||
$controller = $this->getMock();
|
||||
|
||||
$controller->index($request);
|
||||
|
||||
$this->assertNotEmpty($this->session->get('oauth2_state'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\OAuthController::index
|
||||
*/
|
||||
public function testIndexInvalidState()
|
||||
{
|
||||
/** @var GenericProvider|MockObject $provider */
|
||||
$provider = $this->createMock(GenericProvider::class);
|
||||
|
||||
$this->session->set('oauth2_state', 'some-internal-state');
|
||||
|
||||
$request = new Request();
|
||||
$request = $request
|
||||
->withAttribute('provider', 'testprovider')
|
||||
->withQueryParams(['code' => 'lorem-ipsum-code', 'state' => 'some-wrong-state']);
|
||||
|
||||
$controller = $this->getMock(['getProvider']);
|
||||
$this->setExpects($controller, 'getProvider', ['testprovider'], $provider);
|
||||
|
||||
$exception = null;
|
||||
try {
|
||||
$controller->index($request);
|
||||
} catch (HttpNotFound $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
$this->assertFalse($this->session->has('oauth2_state'));
|
||||
$this->log->hasWarningThatContains('Invalid');
|
||||
$this->assertNotNull($exception, 'Exception not thrown');
|
||||
$this->assertEquals('oauth.invalid-state', $exception->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\OAuthController::index
|
||||
*/
|
||||
public function testIndexProviderError()
|
||||
{
|
||||
/** @var GenericProvider|MockObject $provider */
|
||||
$provider = $this->createMock(GenericProvider::class);
|
||||
$provider->expects($this->once())
|
||||
->method('getAccessToken')
|
||||
->with('authorization_code', ['code' => 'lorem-ipsum-code'])
|
||||
->willThrowException(new IdentityProviderException(
|
||||
'Oops',
|
||||
42,
|
||||
['error' => 'some_error', 'error_description' => 'Some kind of error']
|
||||
));
|
||||
|
||||
$this->session->set('oauth2_state', 'some-internal-state');
|
||||
|
||||
$request = new Request();
|
||||
$request = $request
|
||||
->withAttribute('provider', 'testprovider')
|
||||
->withQueryParams(['code' => 'lorem-ipsum-code', 'state' => 'some-internal-state']);
|
||||
|
||||
$controller = $this->getMock(['getProvider']);
|
||||
$this->setExpects($controller, 'getProvider', ['testprovider'], $provider);
|
||||
|
||||
$exception = null;
|
||||
try {
|
||||
$controller->index($request);
|
||||
} catch (HttpNotFound $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
$this->log->hasErrorThatContains('Some kind of error');
|
||||
$this->log->hasErrorThatContains('some_error');
|
||||
$this->assertNotNull($exception, 'Exception not thrown');
|
||||
$this->assertEquals('oauth.provider-error', $exception->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\OAuthController::index
|
||||
*/
|
||||
public function testIndexAlreadyConnectedToAUser()
|
||||
{
|
||||
$accessToken = $this->createMock(AccessToken::class);
|
||||
|
||||
/** @var ResourceOwnerInterface|MockObject $resourceOwner */
|
||||
$resourceOwner = $this->createMock(ResourceOwnerInterface::class);
|
||||
$this->setExpects($resourceOwner, 'getId', null, 'provider-user-identifier', $this->atLeastOnce());
|
||||
|
||||
/** @var GenericProvider|MockObject $provider */
|
||||
$provider = $this->createMock(GenericProvider::class);
|
||||
$this->setExpects(
|
||||
$provider,
|
||||
'getAccessToken',
|
||||
['authorization_code', ['code' => 'lorem-ipsum-code']],
|
||||
$accessToken,
|
||||
$this->atLeastOnce()
|
||||
);
|
||||
$this->setExpects($provider, 'getResourceOwner', [$accessToken], $resourceOwner);
|
||||
|
||||
$this->session->set('oauth2_state', 'some-internal-state');
|
||||
|
||||
$this->setExpects($this->auth, 'user', null, $this->otherAuthenticatedUser);
|
||||
|
||||
$request = new Request();
|
||||
$request = $request
|
||||
->withAttribute('provider', 'testprovider')
|
||||
->withQueryParams(['code' => 'lorem-ipsum-code', 'state' => 'some-internal-state']);
|
||||
|
||||
$controller = $this->getMock(['getProvider']);
|
||||
$this->setExpects($controller, 'getProvider', ['testprovider'], $provider);
|
||||
|
||||
$exception = null;
|
||||
try {
|
||||
$controller->index($request);
|
||||
} catch (HttpNotFound $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
$this->assertNotNull($exception, 'Exception not thrown');
|
||||
$this->assertEquals('oauth.already-connected', $exception->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\OAuthController::index
|
||||
* @covers \Engelsystem\Controllers\OAuthController::redirectRegisterOrThrowNotFound
|
||||
*/
|
||||
public function testIndexRedirectRegister()
|
||||
{
|
||||
$accessToken = $this->createMock(AccessToken::class);
|
||||
|
||||
/** @var ResourceOwnerInterface|MockObject $resourceOwner */
|
||||
$resourceOwner = $this->createMock(ResourceOwnerInterface::class);
|
||||
$this->setExpects(
|
||||
$resourceOwner,
|
||||
'getId',
|
||||
null,
|
||||
'provider-not-connected-identifier',
|
||||
$this->atLeastOnce()
|
||||
);
|
||||
$this->setExpects(
|
||||
$resourceOwner,
|
||||
'toArray',
|
||||
null,
|
||||
[
|
||||
'uid' => 'provider-not-connected-identifier',
|
||||
'user' => 'username',
|
||||
'email' => 'foo.bar@localhost',
|
||||
'given-name' => 'Foo',
|
||||
'last-name' => 'Bar',
|
||||
],
|
||||
$this->atLeastOnce()
|
||||
);
|
||||
|
||||
/** @var GenericProvider|MockObject $provider */
|
||||
$provider = $this->createMock(GenericProvider::class);
|
||||
$this->setExpects(
|
||||
$provider,
|
||||
'getAccessToken',
|
||||
['authorization_code', ['code' => 'lorem-ipsum-code']],
|
||||
$accessToken,
|
||||
$this->atLeastOnce()
|
||||
);
|
||||
$this->setExpects($provider, 'getResourceOwner', [$accessToken], $resourceOwner, $this->atLeastOnce());
|
||||
|
||||
$this->session->set('oauth2_state', 'some-internal-state');
|
||||
|
||||
$this->setExpects($this->auth, 'user', null, null, $this->atLeastOnce());
|
||||
|
||||
$this->setExpects($this->redirect, 'to', ['/register']);
|
||||
|
||||
$request = new Request();
|
||||
$request = $request
|
||||
->withAttribute('provider', 'testprovider')
|
||||
->withQueryParams(['code' => 'lorem-ipsum-code', 'state' => 'some-internal-state']);
|
||||
|
||||
$controller = $this->getMock(['getProvider']);
|
||||
$this->setExpects($controller, 'getProvider', ['testprovider'], $provider, $this->atLeastOnce());
|
||||
|
||||
$this->config->set('registration_enabled', true);
|
||||
$controller->index($request);
|
||||
$this->assertEquals('testprovider', $this->session->get('oauth2_connect_provider'));
|
||||
$this->assertEquals('provider-not-connected-identifier', $this->session->get('oauth2_user_id'));
|
||||
$this->assertEquals(
|
||||
[
|
||||
'name' => 'username',
|
||||
'email' => 'foo.bar@localhost',
|
||||
'first_name' => 'Foo',
|
||||
'last_name' => 'Bar',
|
||||
],
|
||||
$this->session->get('form_data')
|
||||
);
|
||||
|
||||
$this->config->set('registration_enabled', false);
|
||||
$this->expectException(HttpNotFound::class);
|
||||
$controller->index($request);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\OAuthController::connect
|
||||
* @covers \Engelsystem\Controllers\OAuthController::requireProvider
|
||||
* @covers \Engelsystem\Controllers\OAuthController::isValidProvider
|
||||
*/
|
||||
public function testConnect()
|
||||
{
|
||||
$controller = $this->getMock(['index']);
|
||||
$this->setExpects($controller, 'index', null, new Response());
|
||||
|
||||
$request = (new Request())
|
||||
->withAttribute('provider', 'testprovider');
|
||||
|
||||
$controller->connect($request);
|
||||
|
||||
$this->assertEquals('testprovider', $this->session->get('oauth2_connect_provider'));
|
||||
|
||||
// Provider not found
|
||||
$request = $request->withAttribute('provider', 'notExistingProvider');
|
||||
$this->expectException(HttpNotFound::class);
|
||||
|
||||
$controller->connect($request);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\OAuthController::disconnect
|
||||
*/
|
||||
public function testDisconnect()
|
||||
{
|
||||
$controller = $this->getMock(['addNotification']);
|
||||
$this->setExpects($controller, 'addNotification', ['oauth.disconnected']);
|
||||
|
||||
$request = (new Request())
|
||||
->withAttribute('provider', 'testprovider');
|
||||
|
||||
$this->setExpects($this->auth, 'user', null, $this->authenticatedUser);
|
||||
$this->setExpects($this->redirect, 'back', null, new Response());
|
||||
|
||||
$controller->disconnect($request);
|
||||
$this->assertCount(1, OAuth::all());
|
||||
$this->log->hasInfoThatContains('Disconnected');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $mockMethods
|
||||
*
|
||||
* @return OAuthController|MockObject
|
||||
*/
|
||||
protected function getMock(array $mockMethods = []): OAuthController
|
||||
{
|
||||
/** @var OAuthController|MockObject $controller */
|
||||
$controller = $this->getMockBuilder(OAuthController::class)
|
||||
->setConstructorArgs([
|
||||
$this->auth,
|
||||
$this->authController,
|
||||
$this->config,
|
||||
$this->log,
|
||||
$this->oauth,
|
||||
$this->redirect,
|
||||
$this->session,
|
||||
$this->url
|
||||
])
|
||||
->onlyMethods($mockMethods)
|
||||
->getMock();
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the DB
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->initDatabase();
|
||||
|
||||
$this->auth = $this->createMock(Authenticator::class);
|
||||
$this->authController = $this->createMock(AuthController::class);
|
||||
$this->config = new Config(['oauth' => $this->oauthConfig]);
|
||||
$this->log = new TestLogger();
|
||||
$this->oauth = new OAuth();
|
||||
$this->redirect = $this->createMock(Redirector::class);
|
||||
$this->session = new Session(new MockArraySessionStorage());
|
||||
$this->url = $this->createMock(UrlGenerator::class);
|
||||
|
||||
$this->app->instance('session', $this->session);
|
||||
|
||||
$this->authenticatedUser = new User([
|
||||
'name' => 'foo',
|
||||
'password' => '',
|
||||
'email' => 'foo@localhost',
|
||||
'api_key' => '',
|
||||
'last_login_at' => null,
|
||||
]);
|
||||
$this->authenticatedUser->save();
|
||||
(new OAuth(['provider' => 'testprovider', 'identifier' => 'provider-user-identifier']))
|
||||
->user()
|
||||
->associate($this->authenticatedUser)
|
||||
->save();
|
||||
|
||||
$this->otherUser = new User([
|
||||
'name' => 'bar',
|
||||
'password' => '',
|
||||
'email' => 'bar@localhost',
|
||||
'api_key' => '',
|
||||
'last_login_at' => null,
|
||||
]);
|
||||
$this->otherUser->save();
|
||||
|
||||
$this->otherAuthenticatedUser = new User([
|
||||
'name' => 'baz',
|
||||
'password' => '',
|
||||
'email' => 'baz@localhost',
|
||||
'api_key' => '',
|
||||
'last_login_at' => null,
|
||||
]);
|
||||
$this->otherAuthenticatedUser->save();
|
||||
(new OAuth(['provider' => 'testprovider', 'identifier' => 'provider-baz-identifier']))
|
||||
->user()
|
||||
->associate($this->otherAuthenticatedUser)
|
||||
->save();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace Engelsystem\Test\Unit\Controllers;
|
||||
|
||||
use Engelsystem\Config\Config;
|
||||
use Engelsystem\Controllers\SettingsController;
|
||||
use Engelsystem\Http\Exceptions\HttpNotFound;
|
||||
use Engelsystem\Http\Response;
|
||||
use Engelsystem\Test\Unit\TestCase;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||
|
||||
class SettingsControllerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\SettingsController::__construct
|
||||
* @covers \Engelsystem\Controllers\SettingsController::oauth
|
||||
*/
|
||||
public function testOauth()
|
||||
{
|
||||
$providers = ['foo' => ['lorem' => 'ipsum']];
|
||||
$config = new Config(['oauth' => $providers]);
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$session->set('information', [['lorem' => 'ipsum']]);
|
||||
$this->app->instance('session', $session);
|
||||
/** @var Response|MockObject $response */
|
||||
$response = $this->createMock(Response::class);
|
||||
$response->expects($this->once())
|
||||
->method('withView')
|
||||
->willReturnCallback(function ($view, $data) use ($response, $providers) {
|
||||
$this->assertEquals('pages/settings/oauth.twig', $view);
|
||||
$this->assertArrayHasKey('information', $data);
|
||||
$this->assertArrayHasKey('providers', $data);
|
||||
$this->assertEquals($providers, $data['providers']);
|
||||
|
||||
return $response;
|
||||
});
|
||||
|
||||
$controller = new SettingsController($config, $response);
|
||||
$controller->oauth();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\SettingsController::oauth
|
||||
*/
|
||||
public function testOauthNotConfigured()
|
||||
{
|
||||
$config = new Config(['oauth' => []]);
|
||||
/** @var Response|MockObject $response */
|
||||
$response = $this->createMock(Response::class);
|
||||
|
||||
$controller = new SettingsController($config, $response);
|
||||
|
||||
$this->expectException(HttpNotFound::class);
|
||||
$controller->oauth();
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
|||
use Engelsystem\Models\BaseModel;
|
||||
use Engelsystem\Models\News;
|
||||
use Engelsystem\Models\NewsComment;
|
||||
use Engelsystem\Models\OAuth;
|
||||
use Engelsystem\Models\Question;
|
||||
use Engelsystem\Models\User\Contact;
|
||||
use Engelsystem\Models\User\HasUserModel;
|
||||
|
@ -165,6 +166,24 @@ class UserTest extends ModelTest
|
|||
$this->assertSame($newsComment->id, $comment->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that accessing OAuth of an User works
|
||||
*
|
||||
* @covers \Engelsystem\Models\User\User::oauth
|
||||
*/
|
||||
public function testOauth(): void
|
||||
{
|
||||
($user = new User($this->data))->save();
|
||||
(new OAuth(['provider' => 'test', 'identifier' => 'LoremIpsumDolor123']))
|
||||
->user()
|
||||
->associate($user)
|
||||
->save();
|
||||
|
||||
$oauth = $user->oauth;
|
||||
|
||||
$this->assertCount(1, $oauth);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Models\User\User::worklogs
|
||||
*/
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions;
|
||||
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Engelsystem\Http\Request;
|
||||
use Engelsystem\Models\User\User;
|
||||
use Engelsystem\Renderer\Twig\Extensions\Globals;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
@ -17,6 +18,8 @@ class GlobalsTest extends ExtensionTest
|
|||
{
|
||||
/** @var Authenticator|MockObject $auth */
|
||||
$auth = $this->createMock(Authenticator::class);
|
||||
/** @var Request|MockObject $request */
|
||||
$request = $this->createMock(Request::class);
|
||||
$user = new User();
|
||||
|
||||
$auth->expects($this->exactly(2))
|
||||
|
@ -26,10 +29,11 @@ class GlobalsTest extends ExtensionTest
|
|||
$user
|
||||
);
|
||||
|
||||
$extension = new Globals($auth);
|
||||
$extension = new Globals($auth, $request);
|
||||
|
||||
$globals = $extension->getGlobals();
|
||||
$this->assertGlobalsExists('user', [], $globals);
|
||||
$this->assertGlobalsExists('request', $request, $globals);
|
||||
|
||||
$globals = $extension->getGlobals();
|
||||
$this->assertGlobalsExists('user', $user, $globals);
|
||||
|
|
Loading…
Reference in New Issue