Login: Added OAuth support
parent
c57bb9395e
commit
80941c2999
@ -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');
|
||||
}
|
||||
}
|
@ -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 %}
|
@ -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',
|
||||
];
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue