Added OAuth2 SSO login group mapping
This commit is contained in:
parent
38dda01330
commit
1ba4b57eac
|
@ -66,5 +66,7 @@ return [
|
||||||
// or $function
|
// or $function
|
||||||
// ]
|
// ]
|
||||||
'news.created' => \Engelsystem\Events\Listener\News::class . '@created',
|
'news.created' => \Engelsystem\Events\Listener\News::class . '@created',
|
||||||
|
|
||||||
|
'oauth2.login' => \Engelsystem\Events\Listener\OAuth2::class . '@login',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
@ -105,6 +105,14 @@ return [
|
||||||
'hidden' => false,
|
'hidden' => false,
|
||||||
// Mark user as arrived when using this provider (optional)
|
// Mark user as arrived when using this provider (optional)
|
||||||
'mark_arrived' => false,
|
'mark_arrived' => false,
|
||||||
|
// Auto join teams
|
||||||
|
// Info groups field (optional)
|
||||||
|
'groups' => 'groups',
|
||||||
|
// Groups to team (angeltype) mapping (optional)
|
||||||
|
'teams' => [
|
||||||
|
'/Lorem' => 4, // 4 being the ID of the angeltype
|
||||||
|
'/Foo Mod' => ['id' => 5, 'supporter' => true], // 5 being the ID of the angeltype
|
||||||
|
],
|
||||||
],
|
],
|
||||||
*/
|
*/
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Engelsystem\Events\Listener;
|
||||||
|
|
||||||
|
use Engelsystem\Config\Config;
|
||||||
|
use Engelsystem\Database\Database;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class OAuth2
|
||||||
|
{
|
||||||
|
/** @var array */
|
||||||
|
protected $config;
|
||||||
|
|
||||||
|
/** @var LoggerInterface */
|
||||||
|
protected $log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Config $config
|
||||||
|
* @param LoggerInterface $log
|
||||||
|
*/
|
||||||
|
public function __construct(Config $config, LoggerInterface $log)
|
||||||
|
{
|
||||||
|
$this->config = $config->get('oauth');
|
||||||
|
$this->log = $log;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $event
|
||||||
|
* @param string $provider
|
||||||
|
* @param Collection $data OAuth userdata
|
||||||
|
*/
|
||||||
|
public function login(string $event, string $provider, Collection $data)
|
||||||
|
{
|
||||||
|
/** @var Database $db */
|
||||||
|
$db = app(Database::class);
|
||||||
|
$ssoTeams = $this->getSsoTeams($provider);
|
||||||
|
$user = auth()->user();
|
||||||
|
$currentUserAngeltypes = collect($db->select('SELECT * FROM UserAngelTypes WHERE user_id = ?', [$user->id]));
|
||||||
|
$userGroups = $data->get(($this->config[$provider] ?? [])['groups'] ?? 'groups', []);
|
||||||
|
|
||||||
|
foreach ($userGroups as $groupName) {
|
||||||
|
if (!isset($ssoTeams[$groupName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$team = $ssoTeams[$groupName];
|
||||||
|
$userAngeltype = $currentUserAngeltypes->where('angeltype_id', $team['id'])->first();
|
||||||
|
$supporter = $team['supporter'];
|
||||||
|
$confirmed = $supporter ? $user->id : null;
|
||||||
|
|
||||||
|
if (!$userAngeltype) {
|
||||||
|
$this->log->info(
|
||||||
|
'SSO {provider}: Added to angeltype {angeltype}, confirmed: {confirmed}, supporter: {supporter}',
|
||||||
|
[
|
||||||
|
'provider' => $provider,
|
||||||
|
'angeltype' => AngelType($team['id'])['name'],
|
||||||
|
'confirmed' => $confirmed ? 'yes' : 'no',
|
||||||
|
'supporter' => $supporter ? 'yes' : 'no',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$db->insert(
|
||||||
|
'INSERT INTO UserAngelTypes (user_id, angeltype_id, confirm_user_id, supporter) VALUES (?, ?, ?, ?)',
|
||||||
|
[$user->id, $team['id'], $confirmed, $supporter]
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$supporter) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userAngeltype->supporter != $supporter) {
|
||||||
|
$db->update(
|
||||||
|
'UPDATE UserAngelTypes SET supporter=? WHERE id = ?',
|
||||||
|
[$supporter, $userAngeltype->id]
|
||||||
|
);
|
||||||
|
$this->log->info(
|
||||||
|
'SSO {provider}: Set supporter state for angeltype {angeltype}',
|
||||||
|
[
|
||||||
|
'provider' => $provider,
|
||||||
|
'angeltype' => AngelType($userAngeltype->angeltype_id)['name'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$userAngeltype->confirm_user_id) {
|
||||||
|
$db->update(
|
||||||
|
'UPDATE UserAngelTypes SET confirm_user_id=? WHERE id = ?',
|
||||||
|
[$user->id, $userAngeltype->id]
|
||||||
|
);
|
||||||
|
$this->log->info(
|
||||||
|
'SSO {provider}: Set confirmed state for angeltype {angeltype}',
|
||||||
|
[
|
||||||
|
'provider' => $provider,
|
||||||
|
'angeltype' => AngelType($userAngeltype->angeltype_id)['name'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $provider
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getSsoTeams(string $provider): array
|
||||||
|
{
|
||||||
|
$config = $this->config[$provider] ?? [];
|
||||||
|
|
||||||
|
$teams = [];
|
||||||
|
foreach ($config['teams'] ?? [] as $ssoName => $conf) {
|
||||||
|
$conf = Arr::wrap($conf);
|
||||||
|
$teamId = $conf['id'] ?? $conf[0];
|
||||||
|
$isSupporter = $conf['supporter'] ?? false;
|
||||||
|
|
||||||
|
$teams[$ssoName] = ['id' => $teamId, 'supporter' => $isSupporter];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $teams;
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ $includeFiles = [
|
||||||
__DIR__ . '/../includes/helper/legacy_helper.php',
|
__DIR__ . '/../includes/helper/legacy_helper.php',
|
||||||
__DIR__ . '/../includes/helper/message_helper.php',
|
__DIR__ . '/../includes/helper/message_helper.php',
|
||||||
__DIR__ . '/../includes/helper/email_helper.php',
|
__DIR__ . '/../includes/helper/email_helper.php',
|
||||||
|
__DIR__ . '/../includes/helper/oauth_helper.php',
|
||||||
|
|
||||||
__DIR__ . '/../includes/mailer/shifts_mailer.php',
|
__DIR__ . '/../includes/mailer/shifts_mailer.php',
|
||||||
__DIR__ . '/../includes/mailer/users_mailer.php',
|
__DIR__ . '/../includes/mailer/users_mailer.php',
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Engelsystem\Database\DB;
|
use Engelsystem\Database\DB;
|
||||||
|
use Engelsystem\Events\Listener\OAuth2;
|
||||||
use Engelsystem\Models\OAuth;
|
use Engelsystem\Models\OAuth;
|
||||||
use Engelsystem\Models\User\Contact;
|
use Engelsystem\Models\User\Contact;
|
||||||
use Engelsystem\Models\User\PersonalData;
|
use Engelsystem\Models\User\PersonalData;
|
||||||
|
@ -55,6 +56,16 @@ function guest_register()
|
||||||
|
|
||||||
$angel_types_source = AngelTypes();
|
$angel_types_source = AngelTypes();
|
||||||
$angel_types = [];
|
$angel_types = [];
|
||||||
|
if (!empty($session->get('oauth2_groups'))) {
|
||||||
|
/** @var OAuth2 $oauth */
|
||||||
|
$oauth = app()->get(OAuth2::class);
|
||||||
|
$ssoTeams = $oauth->getSsoTeams($session->get('oauth2_connect_provider'));
|
||||||
|
foreach ($ssoTeams as $name => $team) {
|
||||||
|
if (in_array($name, $session->get('oauth2_groups'))) {
|
||||||
|
$selected_angel_types[] = $team['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
foreach ($angel_types_source as $angel_type) {
|
foreach ($angel_types_source as $angel_type) {
|
||||||
$angel_types[$angel_type['id']] = $angel_type['name']
|
$angel_types[$angel_type['id']] = $angel_type['name']
|
||||||
. ($angel_type['restricted'] ? ' (' . __('Requires introduction') . ')' : '');
|
. ($angel_type['restricted'] ? ' (' . __('Requires introduction') . ')' : '');
|
||||||
|
|
|
@ -106,6 +106,7 @@ class OAuthController extends BaseController
|
||||||
throw new HttpNotFound('oauth.invalid-state');
|
throw new HttpNotFound('oauth.invalid-state');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$accessToken = null;
|
||||||
try {
|
try {
|
||||||
$accessToken = $provider->getAccessToken(
|
$accessToken = $provider->getAccessToken(
|
||||||
'authorization_code',
|
'authorization_code',
|
||||||
|
@ -114,21 +115,15 @@ class OAuthController extends BaseController
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch (IdentityProviderException $e) {
|
} catch (IdentityProviderException $e) {
|
||||||
$response = $e->getResponseBody();
|
$this->handleOAuthError($e, $providerName);
|
||||||
$response = is_array($response) ? json_encode($response) : $response;
|
|
||||||
$this->log->error(
|
|
||||||
'{provider} identity provider error: {error} {description}',
|
|
||||||
[
|
|
||||||
'provider' => $providerName,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'description' => $response,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
throw new HttpNotFound('oauth.provider-error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$resourceOwner = $provider->getResourceOwner($accessToken);
|
$resourceOwner = null;
|
||||||
|
try {
|
||||||
|
$resourceOwner = $provider->getResourceOwner($accessToken);
|
||||||
|
} catch (IdentityProviderException $e) {
|
||||||
|
$this->handleOAuthError($e, $providerName);
|
||||||
|
}
|
||||||
$resourceId = $resourceOwner->getId();
|
$resourceId = $resourceOwner->getId();
|
||||||
|
|
||||||
/** @var OAuth|null $oauth */
|
/** @var OAuth|null $oauth */
|
||||||
|
@ -180,7 +175,11 @@ class OAuthController extends BaseController
|
||||||
$config = $this->config->get('oauth')[$providerName];
|
$config = $this->config->get('oauth')[$providerName];
|
||||||
$userdata = new Collection($resourceOwner->toArray());
|
$userdata = new Collection($resourceOwner->toArray());
|
||||||
if (!$oauth) {
|
if (!$oauth) {
|
||||||
return $this->redirectRegisterOrThrowNotFound(
|
if (!$this->config->get('registration_enabled')) {
|
||||||
|
throw new HttpNotFound('oauth.not-found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectRegister(
|
||||||
$providerName,
|
$providerName,
|
||||||
$resourceOwner->getId(),
|
$resourceOwner->getId(),
|
||||||
$accessToken,
|
$accessToken,
|
||||||
|
@ -193,7 +192,10 @@ class OAuthController extends BaseController
|
||||||
$this->handleArrive($providerName, $oauth, $resourceOwner);
|
$this->handleArrive($providerName, $oauth, $resourceOwner);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->authController->loginUser($oauth->user);
|
$response = $this->authController->loginUser($oauth->user);
|
||||||
|
event('oauth2.login', ['provider' => $providerName, 'data' => $userdata]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -308,6 +310,28 @@ class OAuthController extends BaseController
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IdentityProviderException $e
|
||||||
|
* @param string $providerName
|
||||||
|
*
|
||||||
|
* @throws HttpNotFound
|
||||||
|
*/
|
||||||
|
protected function handleOAuthError(IdentityProviderException $e, string $providerName): void
|
||||||
|
{
|
||||||
|
$response = $e->getResponseBody();
|
||||||
|
$response = is_array($response) ? json_encode($response) : $response;
|
||||||
|
$this->log->error(
|
||||||
|
'{provider} identity provider error: {error} {description}',
|
||||||
|
[
|
||||||
|
'provider' => $providerName,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'description' => $response,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new HttpNotFound('oauth.provider-error');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $providerName
|
* @param string $providerName
|
||||||
* @param string $providerUserIdentifier
|
* @param string $providerUserIdentifier
|
||||||
|
@ -317,19 +341,15 @@ class OAuthController extends BaseController
|
||||||
*
|
*
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
protected function redirectRegisterOrThrowNotFound(
|
protected function redirectRegister(
|
||||||
string $providerName,
|
string $providerName,
|
||||||
string $providerUserIdentifier,
|
string $providerUserIdentifier,
|
||||||
AccessTokenInterface $accessToken,
|
AccessTokenInterface $accessToken,
|
||||||
array $config,
|
array $config,
|
||||||
Collection $userdata
|
Collection $userdata
|
||||||
): Response {
|
): Response {
|
||||||
if (!$this->config->get('registration_enabled')) {
|
|
||||||
throw new HttpNotFound('oauth.not-found');
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = array_merge(
|
$config = array_merge(
|
||||||
['username' => null, 'email' => null, 'first_name' => null, 'last_name' => null],
|
['username' => null, 'email' => null, 'first_name' => null, 'last_name' => null, 'groups' => null],
|
||||||
$config
|
$config
|
||||||
);
|
);
|
||||||
$this->session->set(
|
$this->session->set(
|
||||||
|
@ -341,6 +361,7 @@ class OAuthController extends BaseController
|
||||||
'last_name' => $userdata->get($config['last_name']),
|
'last_name' => $userdata->get($config['last_name']),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
$this->session->set('oauth2_groups', $userdata->get($config['groups'], []));
|
||||||
$this->session->set('oauth2_connect_provider', $providerName);
|
$this->session->set('oauth2_connect_provider', $providerName);
|
||||||
$this->session->set('oauth2_user_id', $providerUserIdentifier);
|
$this->session->set('oauth2_user_id', $providerUserIdentifier);
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ namespace Engelsystem\Test\Unit\Controllers;
|
||||||
use Engelsystem\Config\Config;
|
use Engelsystem\Config\Config;
|
||||||
use Engelsystem\Controllers\AuthController;
|
use Engelsystem\Controllers\AuthController;
|
||||||
use Engelsystem\Controllers\OAuthController;
|
use Engelsystem\Controllers\OAuthController;
|
||||||
|
use Engelsystem\Events\EventDispatcher;
|
||||||
use Engelsystem\Helpers\Authenticator;
|
use Engelsystem\Helpers\Authenticator;
|
||||||
use Engelsystem\Http\Exceptions\HttpNotFound;
|
use Engelsystem\Http\Exceptions\HttpNotFound;
|
||||||
use Engelsystem\Http\Redirector;
|
use Engelsystem\Http\Redirector;
|
||||||
|
@ -124,6 +125,11 @@ class OAuthControllerTest extends TestCase
|
||||||
);
|
);
|
||||||
$this->setExpects($provider, 'getResourceOwner', [$accessToken], $resourceOwner, $this->atLeastOnce());
|
$this->setExpects($provider, 'getResourceOwner', [$accessToken], $resourceOwner, $this->atLeastOnce());
|
||||||
|
|
||||||
|
/** @var EventDispatcher|MockObject $event */
|
||||||
|
$event = $this->createMock(EventDispatcher::class);
|
||||||
|
$this->app->instance('events.dispatcher', $event);
|
||||||
|
$this->setExpects($event, 'dispatch', ['oauth2.login'], null, 4);
|
||||||
|
|
||||||
$this->authController->expects($this->atLeastOnce())
|
$this->authController->expects($this->atLeastOnce())
|
||||||
->method('loginUser')
|
->method('loginUser')
|
||||||
->willReturnCallback(function (User $user) {
|
->willReturnCallback(function (User $user) {
|
||||||
|
@ -241,18 +247,38 @@ class OAuthControllerTest extends TestCase
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \Engelsystem\Controllers\OAuthController::index
|
* @covers \Engelsystem\Controllers\OAuthController::index
|
||||||
|
* @covers \Engelsystem\Controllers\OAuthController::handleOAuthError
|
||||||
*/
|
*/
|
||||||
public function testIndexProviderError()
|
public function testIndexProviderError()
|
||||||
{
|
{
|
||||||
|
/** @var AccessToken|MockObject $accessToken */
|
||||||
|
$accessToken = $this->createMock(AccessToken::class);
|
||||||
|
|
||||||
|
$thrown = false;
|
||||||
/** @var GenericProvider|MockObject $provider */
|
/** @var GenericProvider|MockObject $provider */
|
||||||
$provider = $this->createMock(GenericProvider::class);
|
$provider = $this->createMock(GenericProvider::class);
|
||||||
$provider->expects($this->once())
|
$provider->expects($this->exactly(2))
|
||||||
->method('getAccessToken')
|
->method('getAccessToken')
|
||||||
->with('authorization_code', ['code' => 'lorem-ipsum-code'])
|
->with('authorization_code', ['code' => 'lorem-ipsum-code'])
|
||||||
|
->willReturnCallback(function () use (&$thrown, $accessToken) {
|
||||||
|
if (!$thrown) {
|
||||||
|
$thrown = true;
|
||||||
|
throw new IdentityProviderException(
|
||||||
|
'Oops',
|
||||||
|
42,
|
||||||
|
['error' => 'some_error', 'error_description' => 'Some kind of error']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $accessToken;
|
||||||
|
});
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('getResourceOwner')
|
||||||
|
->with($accessToken)
|
||||||
->willThrowException(new IdentityProviderException(
|
->willThrowException(new IdentityProviderException(
|
||||||
'Oops',
|
'Something\'s wrong!',
|
||||||
42,
|
1337,
|
||||||
['error' => 'some_error', 'error_description' => 'Some kind of error']
|
'500 Internal server error'
|
||||||
));
|
));
|
||||||
|
|
||||||
$this->session->set('oauth2_state', 'some-internal-state');
|
$this->session->set('oauth2_state', 'some-internal-state');
|
||||||
|
@ -263,8 +289,9 @@ class OAuthControllerTest extends TestCase
|
||||||
->withQueryParams(['code' => 'lorem-ipsum-code', 'state' => 'some-internal-state']);
|
->withQueryParams(['code' => 'lorem-ipsum-code', 'state' => 'some-internal-state']);
|
||||||
|
|
||||||
$controller = $this->getMock(['getProvider']);
|
$controller = $this->getMock(['getProvider']);
|
||||||
$this->setExpects($controller, 'getProvider', ['testprovider'], $provider);
|
$this->setExpects($controller, 'getProvider', ['testprovider'], $provider, 2);
|
||||||
|
|
||||||
|
// Invalid state
|
||||||
$exception = null;
|
$exception = null;
|
||||||
try {
|
try {
|
||||||
$controller->index($request);
|
$controller->index($request);
|
||||||
|
@ -276,6 +303,18 @@ class OAuthControllerTest extends TestCase
|
||||||
$this->log->hasErrorThatContains('some_error');
|
$this->log->hasErrorThatContains('some_error');
|
||||||
$this->assertNotNull($exception, 'Exception not thrown');
|
$this->assertNotNull($exception, 'Exception not thrown');
|
||||||
$this->assertEquals('oauth.provider-error', $exception->getMessage());
|
$this->assertEquals('oauth.provider-error', $exception->getMessage());
|
||||||
|
|
||||||
|
// Error while getting data
|
||||||
|
$exception = null;
|
||||||
|
try {
|
||||||
|
$controller->index($request);
|
||||||
|
} catch (HttpNotFound $e) {
|
||||||
|
$exception = $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log->hasErrorThatContains('500');
|
||||||
|
$this->assertNotNull($exception, 'Exception not thrown');
|
||||||
|
$this->assertEquals('oauth.provider-error', $exception->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -325,7 +364,7 @@ class OAuthControllerTest extends TestCase
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \Engelsystem\Controllers\OAuthController::index
|
* @covers \Engelsystem\Controllers\OAuthController::index
|
||||||
* @covers \Engelsystem\Controllers\OAuthController::redirectRegisterOrThrowNotFound
|
* @covers \Engelsystem\Controllers\OAuthController::redirectRegister
|
||||||
*/
|
*/
|
||||||
public function testIndexRedirectRegister()
|
public function testIndexRedirectRegister()
|
||||||
{
|
{
|
||||||
|
|
|
@ -27,6 +27,10 @@ abstract class TestCase extends PHPUnitTestCase
|
||||||
$times = $this->once();
|
$times = $this->once();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_int($times)) {
|
||||||
|
$times = $this->exactly($times);
|
||||||
|
}
|
||||||
|
|
||||||
$invocation = $object->expects($times)
|
$invocation = $object->expects($times)
|
||||||
->method($method);
|
->method($method);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue