Added OAuth2 SSO login group mapping
This commit is contained in:
parent
38dda01330
commit
1ba4b57eac
|
@ -66,5 +66,7 @@ return [
|
|||
// or $function
|
||||
// ]
|
||||
'news.created' => \Engelsystem\Events\Listener\News::class . '@created',
|
||||
|
||||
'oauth2.login' => \Engelsystem\Events\Listener\OAuth2::class . '@login',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -105,6 +105,14 @@ return [
|
|||
'hidden' => false,
|
||||
// Mark user as arrived when using this provider (optional)
|
||||
'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/message_helper.php',
|
||||
__DIR__ . '/../includes/helper/email_helper.php',
|
||||
__DIR__ . '/../includes/helper/oauth_helper.php',
|
||||
|
||||
__DIR__ . '/../includes/mailer/shifts_mailer.php',
|
||||
__DIR__ . '/../includes/mailer/users_mailer.php',
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use Carbon\Carbon;
|
||||
use Engelsystem\Database\DB;
|
||||
use Engelsystem\Events\Listener\OAuth2;
|
||||
use Engelsystem\Models\OAuth;
|
||||
use Engelsystem\Models\User\Contact;
|
||||
use Engelsystem\Models\User\PersonalData;
|
||||
|
@ -55,6 +56,16 @@ function guest_register()
|
|||
|
||||
$angel_types_source = AngelTypes();
|
||||
$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) {
|
||||
$angel_types[$angel_type['id']] = $angel_type['name']
|
||||
. ($angel_type['restricted'] ? ' (' . __('Requires introduction') . ')' : '');
|
||||
|
|
|
@ -106,6 +106,7 @@ class OAuthController extends BaseController
|
|||
throw new HttpNotFound('oauth.invalid-state');
|
||||
}
|
||||
|
||||
$accessToken = null;
|
||||
try {
|
||||
$accessToken = $provider->getAccessToken(
|
||||
'authorization_code',
|
||||
|
@ -114,21 +115,15 @@ class OAuthController extends BaseController
|
|||
]
|
||||
);
|
||||
} catch (IdentityProviderException $e) {
|
||||
$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');
|
||||
$this->handleOAuthError($e, $providerName);
|
||||
}
|
||||
|
||||
$resourceOwner = $provider->getResourceOwner($accessToken);
|
||||
$resourceOwner = null;
|
||||
try {
|
||||
$resourceOwner = $provider->getResourceOwner($accessToken);
|
||||
} catch (IdentityProviderException $e) {
|
||||
$this->handleOAuthError($e, $providerName);
|
||||
}
|
||||
$resourceId = $resourceOwner->getId();
|
||||
|
||||
/** @var OAuth|null $oauth */
|
||||
|
@ -180,7 +175,11 @@ class OAuthController extends BaseController
|
|||
$config = $this->config->get('oauth')[$providerName];
|
||||
$userdata = new Collection($resourceOwner->toArray());
|
||||
if (!$oauth) {
|
||||
return $this->redirectRegisterOrThrowNotFound(
|
||||
if (!$this->config->get('registration_enabled')) {
|
||||
throw new HttpNotFound('oauth.not-found');
|
||||
}
|
||||
|
||||
return $this->redirectRegister(
|
||||
$providerName,
|
||||
$resourceOwner->getId(),
|
||||
$accessToken,
|
||||
|
@ -193,7 +192,10 @@ class OAuthController extends BaseController
|
|||
$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 $providerUserIdentifier
|
||||
|
@ -317,19 +341,15 @@ class OAuthController extends BaseController
|
|||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function redirectRegisterOrThrowNotFound(
|
||||
protected function redirectRegister(
|
||||
string $providerName,
|
||||
string $providerUserIdentifier,
|
||||
AccessTokenInterface $accessToken,
|
||||
array $config,
|
||||
Collection $userdata
|
||||
): Response {
|
||||
if (!$this->config->get('registration_enabled')) {
|
||||
throw new HttpNotFound('oauth.not-found');
|
||||
}
|
||||
|
||||
$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
|
||||
);
|
||||
$this->session->set(
|
||||
|
@ -341,6 +361,7 @@ class OAuthController extends BaseController
|
|||
'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_user_id', $providerUserIdentifier);
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Engelsystem\Test\Unit\Controllers;
|
|||
use Engelsystem\Config\Config;
|
||||
use Engelsystem\Controllers\AuthController;
|
||||
use Engelsystem\Controllers\OAuthController;
|
||||
use Engelsystem\Events\EventDispatcher;
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Engelsystem\Http\Exceptions\HttpNotFound;
|
||||
use Engelsystem\Http\Redirector;
|
||||
|
@ -124,6 +125,11 @@ class OAuthControllerTest extends TestCase
|
|||
);
|
||||
$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())
|
||||
->method('loginUser')
|
||||
->willReturnCallback(function (User $user) {
|
||||
|
@ -241,18 +247,38 @@ class OAuthControllerTest extends TestCase
|
|||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\OAuthController::index
|
||||
* @covers \Engelsystem\Controllers\OAuthController::handleOAuthError
|
||||
*/
|
||||
public function testIndexProviderError()
|
||||
{
|
||||
/** @var AccessToken|MockObject $accessToken */
|
||||
$accessToken = $this->createMock(AccessToken::class);
|
||||
|
||||
$thrown = false;
|
||||
/** @var GenericProvider|MockObject $provider */
|
||||
$provider = $this->createMock(GenericProvider::class);
|
||||
$provider->expects($this->once())
|
||||
$provider->expects($this->exactly(2))
|
||||
->method('getAccessToken')
|
||||
->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(
|
||||
'Oops',
|
||||
42,
|
||||
['error' => 'some_error', 'error_description' => 'Some kind of error']
|
||||
'Something\'s wrong!',
|
||||
1337,
|
||||
'500 Internal server error'
|
||||
));
|
||||
|
||||
$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']);
|
||||
|
||||
$controller = $this->getMock(['getProvider']);
|
||||
$this->setExpects($controller, 'getProvider', ['testprovider'], $provider);
|
||||
$this->setExpects($controller, 'getProvider', ['testprovider'], $provider, 2);
|
||||
|
||||
// Invalid state
|
||||
$exception = null;
|
||||
try {
|
||||
$controller->index($request);
|
||||
|
@ -276,6 +303,18 @@ class OAuthControllerTest extends TestCase
|
|||
$this->log->hasErrorThatContains('some_error');
|
||||
$this->assertNotNull($exception, 'Exception not thrown');
|
||||
$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::redirectRegisterOrThrowNotFound
|
||||
* @covers \Engelsystem\Controllers\OAuthController::redirectRegister
|
||||
*/
|
||||
public function testIndexRedirectRegister()
|
||||
{
|
||||
|
|
|
@ -27,6 +27,10 @@ abstract class TestCase extends PHPUnitTestCase
|
|||
$times = $this->once();
|
||||
}
|
||||
|
||||
if (is_int($times)) {
|
||||
$times = $this->exactly($times);
|
||||
}
|
||||
|
||||
$invocation = $object->expects($times)
|
||||
->method($method);
|
||||
|
||||
|
|
Loading…
Reference in New Issue