From 6db6bde0690af9bac1d2cd5961ef6ad590c31a65 Mon Sep 17 00:00:00 2001 From: Artem Brezhnev Date: Wed, 8 Mar 2023 22:36:14 +0200 Subject: [PATCH 1/5] Implement resolvers, drop support for Laravel 7 and below --- composer.json | 12 +- config/saml2.php | 17 ++- src/Auth.php | 26 ++-- src/Commands/CreateTenant.php | 6 +- src/Commands/RendersTenants.php | 18 +-- src/Contracts/IdentityProvider.php | 18 +++ src/Contracts/ResolvesIdentityProvider.php | 15 +++ src/Contracts/ResolvesIdpConfig.php | 16 +++ src/Exceptions/IdentityProviderNotFound.php | 10 ++ src/Facades/Auth.php | 4 +- src/Http/Controllers/Saml2Controller.php | 4 +- .../Middleware/ResolveIdentityProvider.php | 65 ++++++++++ src/Http/Middleware/ResolveTenant.php | 115 ------------------ src/Models/Tenant.php | 51 +++++++- src/OneLoginBuilder.php | 94 +++++--------- src/Repositories/TenantRepository.php | 7 +- src/Resolvers/ConfigResolver.php | 50 ++++++++ src/Resolvers/IdentityProviderResolver.php | 69 +++++++++++ src/Saml2User.php | 41 +++---- src/ServiceProvider.php | 25 ++-- 20 files changed, 408 insertions(+), 255 deletions(-) create mode 100644 src/Contracts/IdentityProvider.php create mode 100644 src/Contracts/ResolvesIdentityProvider.php create mode 100644 src/Contracts/ResolvesIdpConfig.php create mode 100644 src/Exceptions/IdentityProviderNotFound.php create mode 100644 src/Http/Middleware/ResolveIdentityProvider.php delete mode 100644 src/Http/Middleware/ResolveTenant.php create mode 100644 src/Resolvers/ConfigResolver.php create mode 100644 src/Resolvers/IdentityProviderResolver.php diff --git a/composer.json b/composer.json index 711898b..ea68190 100644 --- a/composer.json +++ b/composer.json @@ -12,13 +12,13 @@ } ], "require": { - "php": ">=7.1", + "php": ">=7.3", "ext-openssl": "*", - "illuminate/console": "~5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "illuminate/database": "~5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "illuminate/support": "~5.4|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "onelogin/php-saml": "^3.0|^4.0", - "ramsey/uuid": "^3.8|^4.0" + "illuminate/console": "^8.0|^9.0|^10.0|^11.0", + "illuminate/database": "^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "onelogin/php-saml": "^4.0", + "ramsey/uuid": "^4.0" }, "require-dev": { "mockery/mockery": "^0.9.9", diff --git a/config/saml2.php b/config/saml2.php index 541f9fb..0e6f17c 100644 --- a/config/saml2.php +++ b/config/saml2.php @@ -11,7 +11,22 @@ | */ - 'tenantModel' => \Slides\Saml2\Models\Tenant::class, + 'tenantModel' => \Slides\Saml2\Models\IdentityProvider::class, + + /* + |-------------------------------------------------------------------------- + | Classes that implement Identity Provider and config resolution logic. + |-------------------------------------------------------------------------- + | + | Here you may customize the way Identity Provider gets resolved, + | as well as configuration adjustments of the SP once IdP is resolved. + | + */ + + 'resolvers' => [ + 'idp' => \Slides\Saml2\Resolvers\IdentityProviderResolver::class, + 'config' => \Slides\Saml2\Resolvers\ConfigResolver::class + ], /* |-------------------------------------------------------------------------- diff --git a/src/Auth.php b/src/Auth.php index ce94894..2c5c25e 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -4,8 +4,8 @@ use OneLogin\Saml2\Auth as OneLoginAuth; use OneLogin\Saml2\Error as OneLoginError; +use Slides\Saml2\Contracts\IdentityProvider; use Slides\Saml2\Events\SignedOut; -use Slides\Saml2\Models\Tenant; /** * Class Auth @@ -24,20 +24,20 @@ class Auth /** * The resolved tenant. * - * @var Tenant + * @var IdentityProvider */ - protected $tenant; + protected $idp; /** * Auth constructor. * * @param OneLoginAuth $auth - * @param Tenant $tenant + * @param IdentityProvider $idp */ - public function __construct(OneLoginAuth $auth, Tenant $tenant) + public function __construct(OneLoginAuth $auth, IdentityProvider $idp) { $this->base = $auth; - $this->tenant = $tenant; + $this->idp = $idp; } /** @@ -57,7 +57,7 @@ public function isAuthenticated() */ public function getSaml2User() { - return new Saml2User($this->base, $this->tenant); + return new Saml2User($this->base, $this->idp); } /** @@ -225,22 +225,22 @@ public function getBase() /** * Set a tenant * - * @param Tenant $tenant + * @param IdentityProvider $idp * * @return void */ - public function setTenant(Tenant $tenant) + public function setIdp(IdentityProvider $idp) { - $this->tenant = $tenant; + $this->idp = $idp; } /** * Get a resolved tenant. * - * @return Tenant|null + * @return IdentityProvider|null */ - public function getTenant() + public function getIdp() { - return $this->tenant; + return $this->idp; } } diff --git a/src/Commands/CreateTenant.php b/src/Commands/CreateTenant.php index eb8169d..65a6d66 100644 --- a/src/Commands/CreateTenant.php +++ b/src/Commands/CreateTenant.php @@ -3,7 +3,7 @@ namespace Slides\Saml2\Commands; use Slides\Saml2\Helpers\ConsoleHelper; -use Slides\Saml2\Models\Tenant; +use Slides\Saml2\Models\IdentityProvider; use Slides\Saml2\Repositories\TenantRepository; /** @@ -94,7 +94,7 @@ public function handle() return; } - $class = config('saml2.tenantModel', Tenant::class); + $class = config('saml2.tenantModel', IdentityProvider::class); $tenant = new $class([ 'key' => $key, 'uuid' => \Ramsey\Uuid\Uuid::uuid4(), @@ -118,4 +118,4 @@ public function handle() $this->output->newLine(); } -} \ No newline at end of file +} diff --git a/src/Commands/RendersTenants.php b/src/Commands/RendersTenants.php index 5477344..74c8daa 100644 --- a/src/Commands/RendersTenants.php +++ b/src/Commands/RendersTenants.php @@ -2,7 +2,7 @@ namespace Slides\Saml2\Commands; -use Slides\Saml2\Models\Tenant; +use Slides\Saml2\Models\IdentityProvider; use Illuminate\Support\Str; /** @@ -15,15 +15,15 @@ trait RendersTenants /** * Render tenants in a table. * - * @param \Slides\Saml2\Models\Tenant|\Illuminate\Support\Collection $tenants + * @param \Slides\Saml2\Models\IdentityProvider|\Illuminate\Support\Collection $tenants * @param string|null $title * * @return void */ protected function renderTenants($tenants, string $title = null) { - /** @var \Slides\Saml2\Models\Tenant[]|\Illuminate\Database\Eloquent\Collection $tenants */ - $tenants = $tenants instanceof Tenant + /** @var \Slides\Saml2\Models\IdentityProvider[]|\Illuminate\Database\Eloquent\Collection $tenants */ + $tenants = $tenants instanceof IdentityProvider ? collect([$tenants]) : $tenants; @@ -50,11 +50,11 @@ protected function renderTenants($tenants, string $title = null) /** * Get a columns of the Tenant. * - * @param \Slides\Saml2\Models\Tenant $tenant + * @param \Slides\Saml2\Models\IdentityProvider $tenant * * @return array */ - protected function getTenantColumns(Tenant $tenant) + protected function getTenantColumns(IdentityProvider $tenant) { return [ 'ID' => $tenant->id, @@ -76,11 +76,11 @@ protected function getTenantColumns(Tenant $tenant) /** * Render a tenant credentials. * - * @param \Slides\Saml2\Models\Tenant $tenant + * @param \Slides\Saml2\Models\IdentityProvider $tenant * * @return void */ - protected function renderTenantCredentials(Tenant $tenant) + protected function renderTenantCredentials(IdentityProvider $tenant) { $this->output->section('Credentials for the tenant'); @@ -110,4 +110,4 @@ protected function renderArray(array $array) return implode(PHP_EOL, $lines); } -} \ No newline at end of file +} diff --git a/src/Contracts/IdentityProvider.php b/src/Contracts/IdentityProvider.php new file mode 100644 index 0000000..7440983 --- /dev/null +++ b/src/Contracts/IdentityProvider.php @@ -0,0 +1,18 @@ +getTenant()->relay_state_url ?: config('saml2.loginRoute')); + return redirect($auth->getIdp()->relay_state_url ?: config('saml2.loginRoute')); } /** @@ -119,7 +119,7 @@ public function sls(Auth $auth) */ public function login(Request $request, Auth $auth) { - $redirectUrl = $auth->getTenant()->relay_state_url ?: config('saml2.loginRoute'); + $redirectUrl = $auth->getIdp()->relay_state_url ?: config('saml2.loginRoute'); $auth->login($request->query('returnTo', $redirectUrl)); } diff --git a/src/Http/Middleware/ResolveIdentityProvider.php b/src/Http/Middleware/ResolveIdentityProvider.php new file mode 100644 index 0000000..084a9a0 --- /dev/null +++ b/src/Http/Middleware/ResolveIdentityProvider.php @@ -0,0 +1,65 @@ +resolver = $resolver; + $this->builder = $builder; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * + * @throws NotFoundHttpException + * + * @return mixed + */ + public function handle(Request $request, \Closure $next) + { + if (!$idp = $this->resolver->resolve($request)) { + throw new IdentityProviderNotFound(); + } + + if (config('saml2.debug')) { + Log::debug('[Saml2] Tenant resolved', [ + 'uuid' => $idp->idpUuid() + ]); + } + + session()->flash('saml2.tenant.uuid', $idp->idpUuid()); + + $this->builder->configureIdp($idp); + + return $next($request); + } +} diff --git a/src/Http/Middleware/ResolveTenant.php b/src/Http/Middleware/ResolveTenant.php deleted file mode 100644 index 9e3cc1e..0000000 --- a/src/Http/Middleware/ResolveTenant.php +++ /dev/null @@ -1,115 +0,0 @@ -tenants = $tenants; - $this->builder = $builder; - } - - /** - * Handle an incoming request. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * - * @throws NotFoundHttpException - * - * @return mixed - */ - public function handle($request, \Closure $next) - { - if(!$tenant = $this->resolveTenant($request)) { - throw new NotFoundHttpException(); - } - - if (config('saml2.debug')) { - Log::debug('[Saml2] Tenant resolved', [ - 'uuid' => $tenant->uuid, - 'id' => $tenant->id, - 'key' => $tenant->key - ]); - } - - session()->flash('saml2.tenant.uuid', $tenant->uuid); - - $this->builder - ->withTenant($tenant) - ->bootstrap(); - - return $next($request); - } - - /** - * Resolve a tenant by a request. - * - * @param \Illuminate\Http\Request $request - * - * @return \Slides\Saml2\Models\Tenant|null - */ - protected function resolveTenant($request) - { - if(!$uuid = $request->route('uuid')) { - if (config('saml2.debug')) { - Log::debug('[Saml2] Tenant UUID is not present in the URL so cannot be resolved', [ - 'url' => $request->fullUrl() - ]); - } - - return null; - } - - if(!$tenant = $this->tenants->findByUUID($uuid)) { - if (config('saml2.debug')) { - Log::debug('[Saml2] Tenant doesn\'t exist', [ - 'uuid' => $uuid - ]); - } - - return null; - } - - if($tenant->trashed()) { - if (config('saml2.debug')) { - Log::debug('[Saml2] Tenant #' . $tenant->id. ' resolved but marked as deleted', [ - 'id' => $tenant->id, - 'uuid' => $uuid, - 'deleted_at' => $tenant->deleted_at->toDateTimeString() - ]); - } - - return null; - } - - return $tenant; - } -} \ No newline at end of file diff --git a/src/Models/Tenant.php b/src/Models/Tenant.php index 34ba793..0f11ae6 100644 --- a/src/Models/Tenant.php +++ b/src/Models/Tenant.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +use Slides\Saml2\Concerns\IdentityProvider; /** * Class Tenant @@ -24,7 +25,7 @@ * * @package Slides\Saml2\Models */ -class Tenant extends Model +class Tenant extends Model implements IdentityProvider { use SoftDeletes; @@ -60,4 +61,52 @@ class Tenant extends Model protected $casts = [ 'metadata' => 'array' ]; + + /** + * @return string + */ + public function idpUuid(): string + { + return $this->uuid; + } + + /** + * @return string + */ + public function idpEntityId(): string + { + return $this->idp_entity_id; + } + + /** + * @return string + */ + public function idpLoginUrl(): string + { + return $this->idp_login_url; + } + + /** + * @return string + */ + public function idpLogoutUrl(): string + { + return $this->idp_logout_url; + } + + /** + * @return string + */ + public function idpX509cert(): string + { + return $this->idpX509cert(); + } + + /** + * @return string + */ + public function idpNameIdFormat(): string + { + return $this->name_id_format; + } } diff --git a/src/OneLoginBuilder.php b/src/OneLoginBuilder.php index 4e1adf5..5f528ce 100644 --- a/src/OneLoginBuilder.php +++ b/src/OneLoginBuilder.php @@ -6,7 +6,8 @@ use OneLogin\Saml2\Utils as OneLoginUtils; use Illuminate\Support\Facades\URL; use Illuminate\Contracts\Container\Container; -use Slides\Saml2\Models\Tenant; +use Slides\Saml2\Contracts\IdentityProvider; +use Slides\Saml2\Contracts\ResolvesIdpConfig; use Illuminate\Support\Arr; /** @@ -22,83 +23,64 @@ class OneLoginBuilder protected $app; /** - * The resolved tenant. + * The config resolver. * - * @var Tenant + * @var ResolvesIdpConfig */ - protected $tenant; + protected $configResolver; /** * OneLoginBuilder constructor. * * @param Container $app + * @param ResolvesIdpConfig $configResolver */ - public function __construct(Container $app) + public function __construct(Container $app, ResolvesIdpConfig $configResolver) { $this->app = $app; + $this->configResolver = $configResolver; } /** - * Set a tenant. + * Adjust OneLogin configuration according to the given identity provider. * - * @param Tenant $tenant - * - * @return $this - */ - public function withTenant(Tenant $tenant) - { - $this->tenant = $tenant; - - return $this; - } - - /** - * Bootstrap the OneLogin toolkit. - * - * @param Tenant $tenant + * @param IdentityProvider $idp * * @return void */ - public function bootstrap() + public function configureIdp(IdentityProvider $idp) { if ($this->app['config']->get('saml2.proxyVars', false)) { OneLoginUtils::setProxyVars(true); } - $this->app->singleton('OneLogin_Saml2_Auth', function ($app) { + $this->app->singleton(OneLoginAuth::class, function ($app) use ($idp) { $config = $app['config']['saml2']; - $this->setConfigDefaultValues($config); - - $oneLoginConfig = $config; - $oneLoginConfig['idp'] = [ - 'entityId' => $this->tenant->idp_entity_id, - 'singleSignOnService' => ['url' => $this->tenant->idp_login_url], - 'singleLogoutService' => ['url' => $this->tenant->idp_logout_url], - 'x509cert' => $this->tenant->idp_x509_cert - ]; - - $oneLoginConfig['sp']['NameIDFormat'] = $this->resolveNameIdFormatPrefix($this->tenant->name_id_format); + $this->setConfigDefaultValues($idp->idpUuid(), $config); - return new OneLoginAuth($oneLoginConfig); + return new OneLoginAuth( + $this->configResolver->resolve($idp, $config) + ); }); - $this->app->singleton('Slides\Saml2\Auth', function ($app) { - return new \Slides\Saml2\Auth($app['OneLogin_Saml2_Auth'], $this->tenant); + $this->app->singleton(Auth::class, function ($app) use ($idp) { + return new \Slides\Saml2\Auth($app[OneLoginAuth::class], $idp); }); } /** * Set default config values if they weren't set. * + * @param string $uuid * @param array $config * * @return void */ - protected function setConfigDefaultValues(array &$config) + protected function setConfigDefaultValues(string $uuid, array &$config): void { - foreach ($this->configDefaultValues() as $key => $default) { - if(!Arr::get($config, $key)) { + foreach ($this->configDefaultValues($uuid) as $key => $default) { + if (!Arr::get($config, $key)) { Arr::set($config, $key, $default); } } @@ -107,34 +89,16 @@ protected function setConfigDefaultValues(array &$config) /** * Configuration default values that must be replaced with custom ones. * + * @param string $uuid + * * @return array */ - protected function configDefaultValues() + protected function configDefaultValues(string $uuid): array { return [ - 'sp.entityId' => URL::route('saml.metadata', ['uuid' => $this->tenant->uuid]), - 'sp.assertionConsumerService.url' => URL::route('saml.acs', ['uuid' => $this->tenant->uuid]), - 'sp.singleLogoutService.url' => URL::route('saml.sls', ['uuid' => $this->tenant->uuid]) + 'sp.entityId' => URL::route('saml.metadata', compact('uuid')), + 'sp.assertionConsumerService.url' => URL::route('saml.acs', compact('uuid')), + 'sp.singleLogoutService.url' => URL::route('saml.sls', compact('uuid')) ]; } - - /** - * Resolve the Name ID Format prefix. - * - * @param string $format - * - * @return string - */ - protected function resolveNameIdFormatPrefix(string $format): string - { - switch ($format) { - case 'emailAddress': - case 'X509SubjectName': - case 'WindowsDomainQualifiedName': - case 'unspecified': - return 'urn:oasis:names:tc:SAML:1.1:nameid-format:' . $format; - default: - return 'urn:oasis:names:tc:SAML:2.0:nameid-format:'. $format; - } - } -} \ No newline at end of file +} diff --git a/src/Repositories/TenantRepository.php b/src/Repositories/TenantRepository.php index 4734e66..2ee30b2 100644 --- a/src/Repositories/TenantRepository.php +++ b/src/Repositories/TenantRepository.php @@ -4,11 +4,6 @@ use Slides\Saml2\Models\Tenant; -/** - * Class TenantRepository - * - * @package Slides\Saml2\Repositories - */ class TenantRepository { /** @@ -107,4 +102,4 @@ public function findByUUID(string $uuid, bool $withTrashed = true) ->where('uuid', $uuid) ->first(); } -} \ No newline at end of file +} diff --git a/src/Resolvers/ConfigResolver.php b/src/Resolvers/ConfigResolver.php new file mode 100644 index 0000000..b0df125 --- /dev/null +++ b/src/Resolvers/ConfigResolver.php @@ -0,0 +1,50 @@ + $idp->idpEntityId(), + 'singleSignOnService' => ['url' => $idp->idpLoginUrl()], + 'singleLogoutService' => ['url' => $idp->idpLogoutUrl()], + 'x509cert' => $idp->idpX509cert() + ]; + + $config['sp']['NameIDFormat'] = $this->resolveNameIdFormatPrefix($idp->idpNameIdFormat()); + + return $config; + } + + /** + * Resolve the Name ID Format prefix. + * + * @param string $format + * + * @return string + */ + protected function resolveNameIdFormatPrefix(string $format): string + { + switch ($format) { + case 'emailAddress': + case 'X509SubjectName': + case 'WindowsDomainQualifiedName': + case 'unspecified': + return 'urn:oasis:names:tc:SAML:1.1:nameid-format:' . $format; + default: + return 'urn:oasis:names:tc:SAML:2.0:nameid-format:'. $format; + } + } +} diff --git a/src/Resolvers/IdentityProviderResolver.php b/src/Resolvers/IdentityProviderResolver.php new file mode 100644 index 0000000..56c1609 --- /dev/null +++ b/src/Resolvers/IdentityProviderResolver.php @@ -0,0 +1,69 @@ +tenants = $tenants; + } + + /** + * Resolve a tenant from the request. + * + * @param Request $request + * + * @return IdentityProvider|null + */ + public function resolve($request): ?IdentityProvider + { + if (!$uuid = $request->route('uuid')) { + if (config('saml2.debug')) { + Log::debug('[Saml2] Tenant UUID is not present in the URL so cannot be resolved', [ + 'url' => $request->fullUrl() + ]); + } + + return null; + } + + if (!$idp = $this->tenants->findByUUID($uuid)) { + if (config('saml2.debug')) { + Log::debug('[Saml2] Tenant doesn\'t exist', [ + 'uuid' => $uuid + ]); + } + + return null; + } + + if ($idp->trashed()) { + if (config('saml2.debug')) { + Log::debug('[Saml2] Tenant #' . $idp->id. ' resolved but marked as deleted', [ + 'id' => $idp->id, + 'uuid' => $uuid, + 'deleted_at' => $idp->deleted_at->toDateTimeString() + ]); + } + + return null; + } + + return $idp; + } +} diff --git a/src/Saml2User.php b/src/Saml2User.php index ba37f1f..188cf59 100644 --- a/src/Saml2User.php +++ b/src/Saml2User.php @@ -3,13 +3,8 @@ namespace Slides\Saml2; use OneLogin\Saml2\Auth as OneLoginAuth; -use Slides\Saml2\Models\Tenant; +use Slides\Saml2\Contracts\IdentityProvider; -/** - * Class Saml2User - * - * @package Slides\Saml2 - */ class Saml2User { /** @@ -22,20 +17,20 @@ class Saml2User /** * The tenant user belongs to. * - * @var Tenant + * @var IdentityProvider */ - protected $tenant; + protected $idp; /** * Saml2User constructor. * * @param OneLoginAuth $auth - * @param Tenant $tenant + * @param IdentityProvider $idp */ - public function __construct(OneLoginAuth $auth, Tenant $tenant) + public function __construct(OneLoginAuth $auth, IdentityProvider $idp) { $this->auth = $auth; - $this->tenant = $tenant; + $this->idp = $idp; } /** @@ -69,7 +64,7 @@ public function getAttribute($name) { return $this->auth->getAttribute($name); } - + /** * The attributes retrieved from assertion processed this request. * @@ -118,11 +113,11 @@ public function getIntendedUrl() */ public function parseUserAttribute($samlAttribute = null, $propertyName = null) { - if(empty($samlAttribute)) { + if (empty($samlAttribute)) { return null; } - if(empty($propertyName)) { + if (empty($propertyName)) { return $this->getAttribute($samlAttribute); } @@ -138,7 +133,7 @@ public function parseUserAttribute($samlAttribute = null, $propertyName = null) */ public function parseAttributes($attributes = []) { - foreach($attributes as $propertyName => $samlAttribute) { + foreach ($attributes as $propertyName => $samlAttribute) { $this->parseUserAttribute($samlAttribute, $propertyName); } } @@ -148,7 +143,7 @@ public function parseAttributes($attributes = []) * * @return null|string */ - public function getSessionIndex() + public function getSessionIndex(): ?string { return $this->auth->getSessionIndex(); } @@ -158,7 +153,7 @@ public function getSessionIndex() * * @return string */ - public function getNameId() + public function getNameId(): string { return $this->auth->getNameId(); } @@ -166,22 +161,22 @@ public function getNameId() /** * Set a tenant * - * @param Tenant $tenant + * @param IdentityProvider $idp * * @return void */ - public function setTenant(Tenant $tenant) + public function setIdp(IdentityProvider $idp) { - $this->tenant = $tenant; + $this->idp = $idp; } /** * Get a resolved tenant. * - * @return Tenant|null + * @return IdentityProvider|null */ - public function getTenant() + public function getIdp() { - return $this->tenant; + return $this->idp; } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 72a414f..2f57527 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -2,11 +2,6 @@ namespace Slides\Saml2; -/** - * Class ServiceProvider - * - * @package Slides\Saml2 - */ class ServiceProvider extends \Illuminate\Support\ServiceProvider { /** @@ -16,6 +11,18 @@ class ServiceProvider extends \Illuminate\Support\ServiceProvider */ protected $defer = false; + /** + * Register any application services. + * + * @return void + */ + public function register() + { + $this->app->bind(\Slides\Saml2\Contracts\IdentityProvider::class, config('saml2.tenantModel')); + $this->app->bind(\Slides\Saml2\Contracts\ResolvesIdentityProvider::class, config('saml2.resolvers.idp')); + $this->app->bind(\Slides\Saml2\Contracts\ResolvesIdpConfig::class, config('saml2.resolvers.config')); + } + /** * Bootstrap the application events. * @@ -37,7 +44,7 @@ public function boot() */ protected function bootRoutes() { - if($this->app['config']['saml2.useRoutes'] == true) { + if ($this->app['config']['saml2.useRoutes']) { include __DIR__ . '/Http/routes.php'; } } @@ -77,7 +84,7 @@ protected function bootCommands() * * @return void */ - protected function bootMiddleware() + protected function bootMiddleware(): void { $this->app['router']->aliasMiddleware('saml2.resolveTenant', \Slides\Saml2\Http\Middleware\ResolveTenant::class); } @@ -87,7 +94,7 @@ protected function bootMiddleware() * * @return void */ - protected function loadMigrations() + protected function loadMigrations(): void { if (config('saml2.load_migrations', true)) { $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); @@ -99,7 +106,7 @@ protected function loadMigrations() * * @return array */ - public function provides() + public function provides(): array { return []; } From c6b5d94b4d97f576de3c5dd7da2637f04723e2af Mon Sep 17 00:00:00 2001 From: Derek Myers Date: Thu, 8 Sep 2022 16:56:28 -0500 Subject: [PATCH 2/5] Uppercase the full SAML2_SP_CERT_X509 env var name --- config/saml2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/saml2.php b/config/saml2.php index 0e6f17c..6b8ac13 100644 --- a/config/saml2.php +++ b/config/saml2.php @@ -185,7 +185,7 @@ | */ - 'x509cert' => env('SAML2_SP_CERT_x509',''), + 'x509cert' => env('SAML2_SP_CERT_X509',''), 'privateKey' => env('SAML2_SP_CERT_PRIVATEKEY',''), /* From ad3ee7142bffb5d95575a43966cede0bf4c7380f Mon Sep 17 00:00:00 2001 From: Artem Brezhnev Date: Sun, 26 Nov 2023 15:34:20 +0200 Subject: [PATCH 3/5] WIP: improve create/update console commands, allow nullable values when creating a tenant --- ...l_morph_columns_to_saml2_tenants_table.php | 32 ++++ ..._21_205057_create_saml2_sessions_table.php | 32 ++++ ...ble_idp_columns_in_saml2_tenants_table.php | 40 +++++ src/Commands/Create.php | 139 +++++++++++++++ src/Commands/CreateTenant.php | 121 ------------- src/Commands/{ListTenants.php => ListAll.php} | 15 +- src/Commands/RendersTenants.php | 8 +- src/Commands/Update.php | 165 ++++++++++++++++++ src/Commands/UpdateTenant.php | 90 ---------- src/Commands/ValidatesInput.php | 64 ------- src/ServiceProvider.php | 24 +-- 11 files changed, 429 insertions(+), 301 deletions(-) create mode 100644 database/migrations/2023_07_21_203815_add_model_morph_columns_to_saml2_tenants_table.php create mode 100644 database/migrations/2023_07_21_205057_create_saml2_sessions_table.php create mode 100644 database/migrations/2023_11_26_121929_alter_nullable_idp_columns_in_saml2_tenants_table.php create mode 100644 src/Commands/Create.php delete mode 100644 src/Commands/CreateTenant.php rename src/Commands/{ListTenants.php => ListAll.php} (79%) create mode 100644 src/Commands/Update.php delete mode 100644 src/Commands/UpdateTenant.php delete mode 100644 src/Commands/ValidatesInput.php diff --git a/database/migrations/2023_07_21_203815_add_model_morph_columns_to_saml2_tenants_table.php b/database/migrations/2023_07_21_203815_add_model_morph_columns_to_saml2_tenants_table.php new file mode 100644 index 0000000..b995e01 --- /dev/null +++ b/database/migrations/2023_07_21_203815_add_model_morph_columns_to_saml2_tenants_table.php @@ -0,0 +1,32 @@ +nullableMorphs('authenticatable'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('saml2_tenants', function (Blueprint $table) { + $table->dropMorphs('authenticatable'); + }); + } +} diff --git a/database/migrations/2023_07_21_205057_create_saml2_sessions_table.php b/database/migrations/2023_07_21_205057_create_saml2_sessions_table.php new file mode 100644 index 0000000..539b3f7 --- /dev/null +++ b/database/migrations/2023_07_21_205057_create_saml2_sessions_table.php @@ -0,0 +1,32 @@ +id(); + + $table->timestamps('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('saml2_sessions'); + } +} diff --git a/database/migrations/2023_11_26_121929_alter_nullable_idp_columns_in_saml2_tenants_table.php b/database/migrations/2023_11_26_121929_alter_nullable_idp_columns_in_saml2_tenants_table.php new file mode 100644 index 0000000..39011bd --- /dev/null +++ b/database/migrations/2023_11_26_121929_alter_nullable_idp_columns_in_saml2_tenants_table.php @@ -0,0 +1,40 @@ +string('idp_entity_id')->nullable()->change(); + $table->string('idp_login_url')->nullable()->change(); + $table->string('idp_logout_url')->nullable()->change(); + $table->string('name_id_format')->nullable()->change(); + $table->text('idp_x509_cert')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('saml2_tenants', function (Blueprint $table) { + $table->string('idp_entity_id')->nullable(false)->change(); + $table->string('idp_login_url')->nullable(false)->change(); + $table->string('idp_logout_url')->nullable(false)->change(); + $table->string('name_id_format')->nullable(false)->change(); + $table->text('idp_x509_cert')->nullable(false)->change(); + }); + } +} diff --git a/src/Commands/Create.php b/src/Commands/Create.php new file mode 100644 index 0000000..0c60b1d --- /dev/null +++ b/src/Commands/Create.php @@ -0,0 +1,139 @@ +tenants = $tenants; + + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return void + */ + public function handle() + { + $metadata = ConsoleHelper::stringToArray($this->option('metadata')); + + $model = config('saml2.tenantModel'); + $tenant = new $model([ + 'key' => $this->option('key'), + 'uuid' => \Ramsey\Uuid\Uuid::uuid4(), + 'idp_entity_id' => $this->option('entityId'), + 'idp_login_url' => $this->option('loginUrl'), + 'idp_logout_url' => $this->option('logoutUrl'), + 'idp_x509_cert' => $this->option('x509cert'), + 'relay_state_url' => $this->option('relayStateUrl'), + 'name_id_format' => $this->option('nameIdFormat'), + 'metadata' => $metadata, + ]); + + if(!$tenant->save()) { + $this->error('Tenant cannot be saved.'); + return; + } + + $this->info("The tenant #$tenant->id ($tenant->uuid) was successfully created."); + + $this->renderTenantCredentials($tenant); + + $this->output->note('You can share this info with the Identity Provider to retrieve the metadata, and then finish the setup by running:'); + $this->output->block( + sprintf('php artisan saml2:idp-update %d \ + --entityId="%s" \ + --loginUrl="%s" \ + --logoutUrl="%s" \ + --x509cert="%s"', + $tenant->id, + '(received Entity ID)', + '(received SSO URL)', + '(received Logout URL)', + '(received x509 certificate)' + ) + ); + } + + /** + * Validation rules for the user input. + * + * @return array + */ + protected function rules(): array + { + return [ + 'key' => ['string', new Unique(config('saml2.tenantModel'), 'key')], + 'entityId' => 'string', + 'loginUrl' => 'string|url', + 'logoutUrl' => 'string|url', + 'x509cert' => 'string', + 'relayStateUrl' => 'string|url', + 'metadata' => 'string', + 'nameIdFormat' => ['string', new In($this->nameIdFormatValues())] + ]; + } + + /** + * Get the values for the name ID format rule. + * + * @return string[] + */ + protected function nameIdFormatValues(): array + { + return [ + 'persistent', + 'transient', + 'emailAddress', + 'unspecified', + 'X509SubjectName', + 'WindowsDomainQualifiedName', + 'kerberos', + 'entity' + ]; + } +} diff --git a/src/Commands/CreateTenant.php b/src/Commands/CreateTenant.php deleted file mode 100644 index 65a6d66..0000000 --- a/src/Commands/CreateTenant.php +++ /dev/null @@ -1,121 +0,0 @@ -tenants = $tenants; - - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle() - { - if (!$entityId = $this->option('entityId')) { - $this->error('Entity ID must be passed as an option --entityId'); - return; - } - - if (!$loginUrl = $this->option('loginUrl')) { - $this->error('Login URL must be passed as an option --loginUrl'); - return; - } - - if (!$logoutUrl = $this->option('logoutUrl')) { - $this->error('Logout URL must be passed as an option --logoutUrl'); - return; - } - - if (!$x509cert = $this->option('x509cert')) { - $this->error('x509 certificate (base64) must be passed as an option --x509cert'); - return; - } - - $key = $this->option('key'); - $metadata = ConsoleHelper::stringToArray($this->option('metadata')); - - if($key && ($tenant = $this->tenants->findByKey($key))) { - $this->renderTenants($tenant, 'Already found tenant(s) using this key'); - $this->error( - 'Cannot create a tenant because the key is already being associated with other tenants.' - . PHP_EOL . 'Firstly, delete tenant(s) or try to create with another with another key.' - ); - - return; - } - - $class = config('saml2.tenantModel', IdentityProvider::class); - $tenant = new $class([ - 'key' => $key, - 'uuid' => \Ramsey\Uuid\Uuid::uuid4(), - 'idp_entity_id' => $entityId, - 'idp_login_url' => $loginUrl, - 'idp_logout_url' => $logoutUrl, - 'idp_x509_cert' => $x509cert, - 'relay_state_url' => $this->option('relayStateUrl'), - 'name_id_format' => $this->resolveNameIdFormat(), - 'metadata' => $metadata, - ]); - - if(!$tenant->save()) { - $this->error('Tenant cannot be saved.'); - return; - } - - $this->info("The tenant #{$tenant->id} ({$tenant->uuid}) was successfully created."); - - $this->renderTenantCredentials($tenant); - - $this->output->newLine(); - } -} diff --git a/src/Commands/ListTenants.php b/src/Commands/ListAll.php similarity index 79% rename from src/Commands/ListTenants.php rename to src/Commands/ListAll.php index 28b07b5..a0b5f59 100644 --- a/src/Commands/ListTenants.php +++ b/src/Commands/ListAll.php @@ -4,12 +4,7 @@ use Slides\Saml2\Repositories\TenantRepository; -/** - * Class ListTenants - * - * @package Slides\Saml2\Commands - */ -class ListTenants extends \Illuminate\Console\Command +class ListAll extends \Illuminate\Console\Command { use RendersTenants; @@ -18,7 +13,7 @@ class ListTenants extends \Illuminate\Console\Command * * @var string */ - protected $signature = 'saml2:list-tenants'; + protected $signature = 'saml2:idp-list'; /** * The console command description. @@ -30,7 +25,7 @@ class ListTenants extends \Illuminate\Console\Command /** * @var TenantRepository */ - protected $tenants; + protected TenantRepository $tenants; /** * DeleteTenant constructor. @@ -53,11 +48,11 @@ public function handle() { $tenants = $this->tenants->all(); - if($tenants->isEmpty()) { + if ($tenants->isEmpty()) { $this->info('No tenants found'); return; } $this->renderTenants($tenants); } -} \ No newline at end of file +} diff --git a/src/Commands/RendersTenants.php b/src/Commands/RendersTenants.php index 74c8daa..115b6b6 100644 --- a/src/Commands/RendersTenants.php +++ b/src/Commands/RendersTenants.php @@ -2,7 +2,7 @@ namespace Slides\Saml2\Commands; -use Slides\Saml2\Models\IdentityProvider; +use Slides\Saml2\Contracts\IdentityProvider; use Illuminate\Support\Str; /** @@ -15,7 +15,7 @@ trait RendersTenants /** * Render tenants in a table. * - * @param \Slides\Saml2\Models\IdentityProvider|\Illuminate\Support\Collection $tenants + * @param IdentityProvider|\Illuminate\Support\Collection $tenants * @param string|null $title * * @return void @@ -50,7 +50,7 @@ protected function renderTenants($tenants, string $title = null) /** * Get a columns of the Tenant. * - * @param \Slides\Saml2\Models\IdentityProvider $tenant + * @param IdentityProvider $tenant * * @return array */ @@ -76,7 +76,7 @@ protected function getTenantColumns(IdentityProvider $tenant) /** * Render a tenant credentials. * - * @param \Slides\Saml2\Models\IdentityProvider $tenant + * @param IdentityProvider $tenant * * @return void */ diff --git a/src/Commands/Update.php b/src/Commands/Update.php new file mode 100644 index 0000000..4d4b173 --- /dev/null +++ b/src/Commands/Update.php @@ -0,0 +1,165 @@ +tenants = $tenants; + + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return void + */ + public function handle() + { + $tenant = $this->tenants->findById($this->argument('id')); + + $tenant->update(array_filter([ + 'key' => $this->option('key'), + 'idp_entity_id' => $this->option('entityId'), + 'idp_login_url' => $this->option('loginUrl'), + 'idp_logout_url' => $this->option('logoutUrl'), + 'idp_x509_cert' => $this->option('x509cert'), + 'relay_state_url' => $this->option('relayStateUrl'), + 'name_id_format' => $this->option('nameIdFormat'), + 'metadata' => ConsoleHelper::stringToArray($this->option('metadata')) + ])); + + if(!$tenant->save()) { + $this->error('Tenant cannot be saved.'); + return; + } + + $this->info("The tenant #{$tenant->id} ({$tenant->uuid}) was successfully updated."); + + $this->renderTenantCredentials($tenant); + + $this->output->newLine(); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return void + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + if (! $input->getOption('entityId')) { + $input->setOption('entityId', $this->ask('Entity ID (fx. https://sts.windows.net/65b9e948-757b-4431-b140-62a2f8a3fdeb/) (optional)')); + } + + if (! $input->getOption('loginUrl')) { + $input->setOption('loginUrl', $this->ask('Login ID (fx. https://login.microsoftonline.com/65b9e948-757b-4431-b140-62a2f8a3fdeb/saml2)')); + } + + if (! $input->getOption('logoutUrl')) { + $input->setOption('logoutUrl', $this->ask('Logout URL')); + } + + if (! $input->getOption('nameIdFormat')) { + $input->setOption('nameIdFormat', $this->choice('Name ID Format', $this->nameIdFormatValues(), 'persistent')); + } + + if (! $input->getOption('relayStateUrl')) { + $input->setOption('relayStateUrl', $this->ask('Post-login redirect URL (optional)')); + } + + if (! $input->getOption('key')) { + $input->setOption('key', $this->ask('Key/name of the identity provider (optional)')); + } + + if (! $input->getOption('metadata')) { + $input->setOption('metadata', $this->ask('Custom metadata (in format "field:value,anotherfield:value") (optional)')); + } + } + + /** + * Validation rules for the user input. + * + * @return array + */ + protected function rules(): array + { + return [ + 'id' => ['required', 'int', new Exists(config('saml2.tenantModel'))], + 'key' => ['string', new Unique(config('saml2.tenantModel'), 'key')], + 'entityId' => 'string', + 'loginUrl' => 'string|url', + 'logoutUrl' => 'string|url', + 'x509cert' => 'string', + 'relayStateUrl' => 'string|url', + 'metadata' => 'string', + 'nameIdFormat' => ['string', new In($this->nameIdFormatValues())] + ]; + } + + /** + * Get the values for the name ID format rule. + * + * @return string[] + */ + protected function nameIdFormatValues(): array + { + return [ + 'persistent', + 'transient', + 'emailAddress', + 'unspecified', + 'X509SubjectName', + 'WindowsDomainQualifiedName', + 'kerberos', + 'entity' + ]; + } +} diff --git a/src/Commands/UpdateTenant.php b/src/Commands/UpdateTenant.php deleted file mode 100644 index 16f104e..0000000 --- a/src/Commands/UpdateTenant.php +++ /dev/null @@ -1,90 +0,0 @@ -tenants = $tenants; - - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle() - { - if(!$tenant = $this->tenants->findById($this->argument('id'))) { - $this->error('Cannot find a tenant #' . $this->argument('id')); - return; - } - - $tenant->update(array_filter([ - 'key' => $this->option('key'), - 'idp_entity_id' => $this->option('entityId'), - 'idp_login_url' => $this->option('loginUrl'), - 'idp_logout_url' => $this->option('logoutUrl'), - 'idp_x509_cert' => $this->option('x509cert'), - 'relay_state_url' => $this->option('relayStateUrl'), - 'name_id_format' => $this->resolveNameIdFormat(), - 'metadata' => ConsoleHelper::stringToArray($this->option('metadata')) - ])); - - if(!$tenant->save()) { - $this->error('Tenant cannot be saved.'); - return; - } - - $this->info("The tenant #{$tenant->id} ({$tenant->uuid}) was successfully updated."); - - $this->renderTenantCredentials($tenant); - - $this->output->newLine(); - } -} \ No newline at end of file diff --git a/src/Commands/ValidatesInput.php b/src/Commands/ValidatesInput.php deleted file mode 100644 index 7fe5a52..0000000 --- a/src/Commands/ValidatesInput.php +++ /dev/null @@ -1,64 +0,0 @@ -option($option) ?: 'persistent'; - - if ($this->validateNameIdFormat($value)) { - return $value; - } - - $this->error('Name ID format is invalid. Supported values: ' . implode(', ', $this->supportedNameIdFormats())); - - return null; - } - - /** - * Validate Name ID format. - * - * @param string $format - * - * @return bool - */ - protected function validateNameIdFormat(string $format): bool - { - return in_array($format, $this->supportedNameIdFormats()); - } - - /** - * The list of supported Name ID formats. - * - * See https://docs.oracle.com/cd/E19316-01/820-3886/6nfcvtepi/index.html - * - * @return string[]|array - */ - protected function supportedNameIdFormats(): array - { - return [ - 'persistent', - 'transient', - 'emailAddress', - 'unspecified', - 'X509SubjectName', - 'WindowsDomainQualifiedName', - 'kerberos', - 'entity' - ]; - } -} \ No newline at end of file diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 2f57527..7dade42 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -9,7 +9,7 @@ class ServiceProvider extends \Illuminate\Support\ServiceProvider * * @var bool */ - protected $defer = false; + protected bool $defer = false; /** * Register any application services. @@ -18,6 +18,8 @@ class ServiceProvider extends \Illuminate\Support\ServiceProvider */ public function register() { + $this->mergeConfigFrom(__DIR__ . '/../config/saml2.php', 'saml2'); + $this->app->bind(\Slides\Saml2\Contracts\IdentityProvider::class, config('saml2.tenantModel')); $this->app->bind(\Slides\Saml2\Contracts\ResolvesIdentityProvider::class, config('saml2.resolvers.idp')); $this->app->bind(\Slides\Saml2\Contracts\ResolvesIdpConfig::class, config('saml2.resolvers.config')); @@ -30,9 +32,9 @@ public function register() */ public function boot() { + $this->bootPublishes(); $this->bootMiddleware(); $this->bootRoutes(); - $this->bootPublishes(); $this->bootCommands(); $this->loadMigrations(); } @@ -56,10 +58,7 @@ protected function bootRoutes() */ protected function bootPublishes() { - $source = __DIR__ . '/../config/saml2.php'; - - $this->publishes([$source => config_path('saml2.php')]); - $this->mergeConfigFrom($source, 'saml2'); + $this->publishes([__DIR__ . '/../config/saml2.php' => config_path('saml2.php')]); } /** @@ -70,12 +69,13 @@ protected function bootPublishes() protected function bootCommands() { $this->commands([ - \Slides\Saml2\Commands\CreateTenant::class, - \Slides\Saml2\Commands\UpdateTenant::class, - \Slides\Saml2\Commands\DeleteTenant::class, - \Slides\Saml2\Commands\RestoreTenant::class, - \Slides\Saml2\Commands\ListTenants::class, - \Slides\Saml2\Commands\TenantCredentials::class + \Slides\Saml2\Commands\Create::class, + \Slides\Saml2\Commands\Update::class, + \Slides\Saml2\Commands\ListAll::class, +// \Slides\Saml2\Commands\DeleteTenant::class, +// \Slides\Saml2\Commands\RestoreTenant::class, +// \Slides\Saml2\Commands\ListTenants::class, +// \Slides\Saml2\Commands\TenantCredentials::class ]); } From 14b54b9fd551a7dc1eb48aea6eb797526765e1e4 Mon Sep 17 00:00:00 2001 From: Artem Brezhnev Date: Sun, 26 Nov 2023 15:36:38 +0200 Subject: [PATCH 4/5] WIP: resolve tenant/idp using a resolver class --- composer.json | 5 +++-- config/saml2.php | 2 +- .../IdentityProviderAuthenticatable.php | 20 +++++++++++++++++++ src/Http/Controllers/Saml2Controller.php | 17 ++++++++++------ .../Middleware/ResolveIdentityProvider.php | 7 +++---- src/Http/routes.php | 2 +- src/Models/Tenant.php | 19 ++++++++++++++++-- src/Resolvers/IdentityProviderResolver.php | 2 +- src/ServiceProvider.php | 2 +- tests/Saml2AuthTest.php | 5 ++--- 10 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 src/Concerns/IdentityProviderAuthenticatable.php diff --git a/composer.json b/composer.json index ea68190..cd0495d 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,9 @@ "illuminate/console": "^8.0|^9.0|^10.0|^11.0", "illuminate/database": "^8.0|^9.0|^10.0|^11.0", "illuminate/support": "^8.0|^9.0|^10.0|^11.0", - "onelogin/php-saml": "^4.0", - "ramsey/uuid": "^4.0" + "onelogin/php-saml": "^4.1", + "ramsey/uuid": "^4.0", + "cerbero/command-validator": "^2.5" }, "require-dev": { "mockery/mockery": "^0.9.9", diff --git a/config/saml2.php b/config/saml2.php index 6b8ac13..77071a4 100644 --- a/config/saml2.php +++ b/config/saml2.php @@ -11,7 +11,7 @@ | */ - 'tenantModel' => \Slides\Saml2\Models\IdentityProvider::class, + 'tenantModel' => \Slides\Saml2\Models\Tenant::class, /* |-------------------------------------------------------------------------- diff --git a/src/Concerns/IdentityProviderAuthenticatable.php b/src/Concerns/IdentityProviderAuthenticatable.php new file mode 100644 index 0000000..ee697c6 --- /dev/null +++ b/src/Concerns/IdentityProviderAuthenticatable.php @@ -0,0 +1,20 @@ +morphOne(config('saml2.tenantModel'), 'authenticatable'); + } +} diff --git a/src/Http/Controllers/Saml2Controller.php b/src/Http/Controllers/Saml2Controller.php index c8322f5..c3d68d9 100644 --- a/src/Http/Controllers/Saml2Controller.php +++ b/src/Http/Controllers/Saml2Controller.php @@ -2,17 +2,13 @@ namespace Slides\Saml2\Http\Controllers; +use Illuminate\Support\Facades\Log; use Slides\Saml2\Events\SignedIn; use Slides\Saml2\Auth; use Illuminate\Routing\Controller; use Illuminate\Http\Request; use OneLogin\Saml2\Error as OneLoginError; -/** - * Class Saml2Controller - * - * @package Slides\Saml2\Http\Controllers - */ class Saml2Controller extends Controller { /** @@ -47,7 +43,7 @@ public function acs(Auth $auth) { $errors = $auth->acs(); - if (!empty($errors)) { + if ($errors) { $error = $auth->getLastErrorReason(); $uuid = $auth->getTenant()->uuid; @@ -62,6 +58,15 @@ public function acs(Auth $auth) $user = $auth->getSaml2User(); + if (config('saml2.debug')) { + Log::debug('[Saml2] Received login request from a user', [ + 'idpUuid' => $user->getIdp()->idpUuid(), + 'userId' => $user->getUserId(), + 'userAttributes' => $user->getAttributes(), + 'intendedUrl' => $user->getIntendedUrl(), + ]); + } + event(new SignedIn($user, $auth)); $redirectUrl = $user->getIntendedUrl(); diff --git a/src/Http/Middleware/ResolveIdentityProvider.php b/src/Http/Middleware/ResolveIdentityProvider.php index 084a9a0..465ea77 100644 --- a/src/Http/Middleware/ResolveIdentityProvider.php +++ b/src/Http/Middleware/ResolveIdentityProvider.php @@ -3,9 +3,8 @@ namespace Slides\Saml2\Http\Middleware; use Illuminate\Http\Request; -use Slides\Saml2\Concerns\ResolvesIdentityProvider; use Slides\Saml2\Exceptions\IdentityProviderNotFound; -use Slides\Saml2\Repositories\TenantRepository; +use Slides\Saml2\Contracts\ResolvesIdentityProvider; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Slides\Saml2\OneLoginBuilder; @@ -15,12 +14,12 @@ class ResolveIdentityProvider /** * @var ResolvesIdentityProvider */ - protected $resolver; + protected ResolvesIdentityProvider $resolver; /** * @var OneLoginBuilder */ - protected $builder; + protected OneLoginBuilder $builder; /** * ResolveTenant constructor. diff --git a/src/Http/routes.php b/src/Http/routes.php index 90a23b3..0758ec4 100644 --- a/src/Http/routes.php +++ b/src/Http/routes.php @@ -4,7 +4,7 @@ Route::group([ 'prefix' => config('saml2.routesPrefix'), - 'middleware' => array_merge(['saml2.resolveTenant'], config('saml2.routesMiddleware')), + 'middleware' => array_merge(['saml2.resolveIdentityProvider'], config('saml2.routesMiddleware')), ], function () { Route::get('/{uuid}/logout', array( 'as' => 'saml.logout', diff --git a/src/Models/Tenant.php b/src/Models/Tenant.php index 0f11ae6..fcfd21b 100644 --- a/src/Models/Tenant.php +++ b/src/Models/Tenant.php @@ -3,8 +3,9 @@ namespace Slides\Saml2\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\SoftDeletes; -use Slides\Saml2\Concerns\IdentityProvider; +use Slides\Saml2\Contracts\IdentityProvider; /** * Class Tenant @@ -18,6 +19,8 @@ * @property string $idp_x509_cert * @property string $relay_state_url * @property string $name_id_format + * @property int $authenticatable_id + * @property string $authenticatable_type * @property array $metadata * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at @@ -50,6 +53,8 @@ class Tenant extends Model implements IdentityProvider 'idp_x509_cert', 'relay_state_url', 'name_id_format', + 'authenticatable_id', + 'authenticatable_type', 'metadata' ]; @@ -99,7 +104,7 @@ public function idpLogoutUrl(): string */ public function idpX509cert(): string { - return $this->idpX509cert(); + return $this->idp_x509_cert; } /** @@ -109,4 +114,14 @@ public function idpNameIdFormat(): string { return $this->name_id_format; } + + /** + * The authenticatable model. + * + * @return MorphTo + */ + public function authenticatable(): MorphTo + { + return $this->morphTo(); + } } diff --git a/src/Resolvers/IdentityProviderResolver.php b/src/Resolvers/IdentityProviderResolver.php index 56c1609..2ef7a49 100644 --- a/src/Resolvers/IdentityProviderResolver.php +++ b/src/Resolvers/IdentityProviderResolver.php @@ -13,7 +13,7 @@ class IdentityProviderResolver implements ResolvesIdentityProvider /** * @var TenantRepository */ - protected $tenants; + protected TenantRepository $tenants; /** * @param TenantRepository $tenants diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 7dade42..a7afc18 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -86,7 +86,7 @@ protected function bootCommands() */ protected function bootMiddleware(): void { - $this->app['router']->aliasMiddleware('saml2.resolveTenant', \Slides\Saml2\Http\Middleware\ResolveTenant::class); + $this->app['router']->aliasMiddleware('saml2.resolveIdentityProvider', \Slides\Saml2\Http\Middleware\ResolveIdentityProvider::class); } /** diff --git a/tests/Saml2AuthTest.php b/tests/Saml2AuthTest.php index f76bc1a..554e95b 100644 --- a/tests/Saml2AuthTest.php +++ b/tests/Saml2AuthTest.php @@ -190,11 +190,10 @@ protected function mockAuth() /** * Create a fake tenant. * - * @return \Slides\Saml2\Models\Tenant + * @return \Slides\Saml2\Models\IdentityProvider */ protected function mockTenant() { - return new \Slides\Saml2\Models\Tenant(); + return new \Slides\Saml2\Models\IdentityProvider(); } } - From 5dfc4944299d8effd30506f6e84c085be74dff8f Mon Sep 17 00:00:00 2001 From: Artem Brezhnev Date: Sat, 13 Apr 2024 20:21:42 +0300 Subject: [PATCH 5/5] Add user resolvers, capture logins, rename Tenant to IdentityProvider --- README.md | 55 +++---- config/saml2.php | 29 +++- ...l_morph_columns_to_saml2_tenants_table.php | 12 +- ..._21_205057_create_saml2_sessions_table.php | 4 +- ...ble_idp_columns_in_saml2_tenants_table.php | 4 +- src/Auth.php | 83 +++++------ src/Commands/Create.php | 19 +-- src/Commands/RendersTenants.php | 37 ++--- src/Commands/Update.php | 31 ++-- .../IdentityProviderAuthenticatable.php | 8 +- src/Concerns/UserResolverHelpers.php | 141 ++++++++++++++++++ ...ityProvider.php => IdentityProvidable.php} | 4 +- src/Contracts/ResolvesIdentityProvider.php | 6 +- src/Contracts/ResolvesIdpConfig.php | 4 +- src/Contracts/ResolvesUser.php | 17 +++ src/EventServiceProvider.php | 30 ++++ src/Events/IdentityProviderResolved.php | 35 +++++ src/Events/SignedIn.php | 9 +- src/Events/SignedOut.php | 5 - src/Exceptions/ConfigurationException.php | 7 + src/Exceptions/UserResolutionException.php | 25 ++++ src/Helpers/ConsoleHelper.php | 11 +- src/Http/Controllers/Saml2Controller.php | 6 +- .../Middleware/ResolveIdentityProvider.php | 4 +- src/Http/routes.php | 10 +- src/Listeners/LoginUser.php | 95 ++++++++++++ .../{Tenant.php => IdentityProvider.php} | 36 +++-- src/Models/Session.php | 67 +++++++++ src/OneLoginBuilder.php | 6 +- ...ory.php => IdentityProviderRepository.php} | 18 +-- src/Resolvers/ConfigResolver.php | 12 +- src/Resolvers/IdentityProviderResolver.php | 28 ++-- src/Resolvers/UserResolver.php | 38 +++++ src/Saml2User.php | 14 +- src/ServiceProvider.php | 8 +- src/helpers.php | 18 +-- 36 files changed, 707 insertions(+), 229 deletions(-) create mode 100644 src/Concerns/UserResolverHelpers.php rename src/Contracts/{IdentityProvider.php => IdentityProvidable.php} (78%) create mode 100644 src/Contracts/ResolvesUser.php create mode 100644 src/EventServiceProvider.php create mode 100644 src/Events/IdentityProviderResolved.php create mode 100644 src/Exceptions/ConfigurationException.php create mode 100644 src/Exceptions/UserResolutionException.php create mode 100644 src/Listeners/LoginUser.php rename src/Models/{Tenant.php => IdentityProvider.php} (73%) create mode 100644 src/Models/Session.php rename src/Repositories/{TenantRepository.php => IdentityProviderRepository.php} (79%) create mode 100644 src/Resolvers/UserResolver.php diff --git a/README.md b/README.md index a3daa35..3790078 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ This package turns your application into Service Provider with the support of mu ## Requirements -- Laravel 5.4+ -- PHP 7.0+ +- Laravel 8.0+ +- PHP 7.4+ ## Getting Started @@ -26,22 +26,6 @@ This package turns your application into Service Provider with the support of mu composer require 24slides/laravel-saml2 ``` -If you are using Laravel 5.5 and higher, the service provider will be automatically registered. - -For older versions, you have to add the service provider and alias to your `config/app.php`: - -```php -'providers' => [ - ... - Slides\Saml2\ServiceProvider::class, -] - -'alias' => [ - ... - 'Saml2' => Slides\Saml2\Facades\Auth::class, -] -``` - ##### Step 2. Publish the configuration file. ``` @@ -66,18 +50,19 @@ When request comes to an application, the middleware parses UUID and resolves th You can easily manage tenants using the following console commands: -- `artisan saml2:create-tenant` -- `artisan saml2:update-tenant` -- `artisan saml2:delete-tenant` -- `artisan saml2:restore-tenant` -- `artisan saml2:list-tenants` -- `artisan saml2:tenant-credentials` +- `artisan saml2:idp-create` +- `artisan saml2:idp-update` +- `artisan saml2:idp-delete` +- `artisan saml2:idp-restore` +- `artisan saml2:idp-list` +- `artisan saml2:idp-get` > To learn their options, run a command with `-h` parameter. Each Tenant has the following attributes: -- **UUID** — a unique identifier that allows to resolve a tenannt and configure SP correspondingly +- **UUID** — a unique identifier that allows to resolve an Identity Provider and configure SP correspondingly +- **Tenant** — an optional morph relation to your custom model that binds IdP with your application entity (fx. user, organisation, etc.) - **Key** — a custom key to use for application needs - **Entity ID** — [Identity Provider Entity ID](https://spaces.at.internet2.edu/display/InCFederation/Entity+IDs) - **Login URL** — Identity Provider Single Sign On URL @@ -85,6 +70,24 @@ Each Tenant has the following attributes: - **x509 certificate** — The certificate provided by Identity Provider in **base64** format - **Metadata** — Custom parameters for your application needs +```php +use \Slides\Saml2\Concerns\IdentityProviderAuthenticatable; + +class Organization extends \Illuminate\Database\Eloquent\Model +{ + use IdentityProviderAuthenticatable; +} + +$organization->identityProvider->loginUrl(); +$organization->identityProvider->sessions(); + +Saml2::withIdentityProvider($organization->identityProvider) + ->route('custom.route'); + +Saml2::withIdentityProvider($organization->identityProvider) + ->url('custom.route'); +``` + #### Default routes The following routes are registered by default: @@ -283,7 +286,7 @@ If you discover any security related issues, please email **brezzhnev@gmail.com* ## Credits - [aacotroneo][link-original-author] -- [brezzhnev][link-author] +- [breart][link-author] - [All Contributors][link-contributors] ## License diff --git a/config/saml2.php b/config/saml2.php index 77071a4..1309dc6 100644 --- a/config/saml2.php +++ b/config/saml2.php @@ -4,14 +4,14 @@ /* |-------------------------------------------------------------------------- - | Tenant Model + | Identity Provider Model |-------------------------------------------------------------------------- | - | This will allow you to override the tenant model with your own. + | This will allow you to override the Identity Provider model with your own. | */ - 'tenantModel' => \Slides\Saml2\Models\Tenant::class, + 'idpModel' => \Slides\Saml2\Models\IdentityProvider::class, /* |-------------------------------------------------------------------------- @@ -25,7 +25,26 @@ 'resolvers' => [ 'idp' => \Slides\Saml2\Resolvers\IdentityProviderResolver::class, - 'config' => \Slides\Saml2\Resolvers\ConfigResolver::class + 'config' => \Slides\Saml2\Resolvers\ConfigResolver::class, + ], + + /* + |-------------------------------------------------------------------------- + | User authentication settings. + |-------------------------------------------------------------------------- + | + | Here you may specify the settings for default (basic) user authorization. + | + | You can extend this functionality by implementing your own user resolver. + | Or completely disable it and use Slides\Saml2\Events\SignedIn event instead. + | + */ + + 'auth' => [ + 'enabled' => env('SAML2_AUTHORIZE_USER', true), + 'resolver' => \Slides\Saml2\Resolvers\UserResolver::class, + 'userModel' => \App\Models\User::class, + 'createUser' => env('SAML2_CREATE_USER', true), ], /* @@ -408,5 +427,5 @@ | This will allow you to disable or enable the default migrations of the package. | */ - 'load_migrations' => true, + 'loadMigrations' => true, ]; diff --git a/database/migrations/2023_07_21_203815_add_model_morph_columns_to_saml2_tenants_table.php b/database/migrations/2023_07_21_203815_add_model_morph_columns_to_saml2_tenants_table.php index b995e01..dae596f 100644 --- a/database/migrations/2023_07_21_203815_add_model_morph_columns_to_saml2_tenants_table.php +++ b/database/migrations/2023_07_21_203815_add_model_morph_columns_to_saml2_tenants_table.php @@ -13,8 +13,10 @@ class AddModelMorphColumnsToSaml2TenantsTable extends Migration */ public function up() { - Schema::table('saml2_tenants', function (Blueprint $table) { - $table->nullableMorphs('authenticatable'); + Schema::rename('saml2_tenants', 'saml2_identity_providers'); + + Schema::table('saml2_identity_providers', function (Blueprint $table) { + $table->nullableMorphs('tenant'); }); } @@ -25,8 +27,10 @@ public function up() */ public function down() { - Schema::table('saml2_tenants', function (Blueprint $table) { - $table->dropMorphs('authenticatable'); + Schema::table('saml2_identity_providers', function (Blueprint $table) { + $table->dropMorphs('tenant'); }); + + Schema::rename('saml2_identity_providers', 'saml2_tenants'); } } diff --git a/database/migrations/2023_07_21_205057_create_saml2_sessions_table.php b/database/migrations/2023_07_21_205057_create_saml2_sessions_table.php index 539b3f7..bd5403d 100644 --- a/database/migrations/2023_07_21_205057_create_saml2_sessions_table.php +++ b/database/migrations/2023_07_21_205057_create_saml2_sessions_table.php @@ -15,7 +15,9 @@ public function up() { Schema::create('saml2_sessions', function (Blueprint $table) { $table->id(); - + $table->foreignId('idp_id')->constrained('saml2_identity_providers'); + $table->foreignId('user_id')->nullable(); + $table->json('payload'); $table->timestamps('created_at'); }); } diff --git a/database/migrations/2023_11_26_121929_alter_nullable_idp_columns_in_saml2_tenants_table.php b/database/migrations/2023_11_26_121929_alter_nullable_idp_columns_in_saml2_tenants_table.php index 39011bd..452d8bd 100644 --- a/database/migrations/2023_11_26_121929_alter_nullable_idp_columns_in_saml2_tenants_table.php +++ b/database/migrations/2023_11_26_121929_alter_nullable_idp_columns_in_saml2_tenants_table.php @@ -13,7 +13,7 @@ class AlterNullableIdpColumnsInSaml2TenantsTable extends Migration */ public function up() { - Schema::table('saml2_tenants', function (Blueprint $table) { + Schema::table('saml2_identity_providers', function (Blueprint $table) { $table->string('idp_entity_id')->nullable()->change(); $table->string('idp_login_url')->nullable()->change(); $table->string('idp_logout_url')->nullable()->change(); @@ -29,7 +29,7 @@ public function up() */ public function down() { - Schema::table('saml2_tenants', function (Blueprint $table) { + Schema::table('saml2_identity_providers', function (Blueprint $table) { $table->string('idp_entity_id')->nullable(false)->change(); $table->string('idp_login_url')->nullable(false)->change(); $table->string('idp_logout_url')->nullable(false)->change(); diff --git a/src/Auth.php b/src/Auth.php index 2c5c25e..d922f67 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -4,14 +4,9 @@ use OneLogin\Saml2\Auth as OneLoginAuth; use OneLogin\Saml2\Error as OneLoginError; -use Slides\Saml2\Contracts\IdentityProvider; +use Slides\Saml2\Contracts\IdentityProvidable; use Slides\Saml2\Events\SignedOut; -/** - * Class Auth - * - * @package Slides\Saml2 - */ class Auth { /** @@ -19,22 +14,22 @@ class Auth * * @var OneLoginAuth */ - protected $base; + protected OneLoginAuth $base; /** * The resolved tenant. * - * @var IdentityProvider + * @var IdentityProvidable */ - protected $idp; + protected IdentityProvidable $idp; /** * Auth constructor. * * @param OneLoginAuth $auth - * @param IdentityProvider $idp + * @param IdentityProvidable $idp */ - public function __construct(OneLoginAuth $auth, IdentityProvider $idp) + public function __construct(OneLoginAuth $auth, IdentityProvidable $idp) { $this->base = $auth; $this->idp = $idp; @@ -45,7 +40,7 @@ public function __construct(OneLoginAuth $auth, IdentityProvider $idp) * * @return bool */ - public function isAuthenticated() + public function isAuthenticated(): bool { return $this->base->isAuthenticated(); } @@ -55,7 +50,7 @@ public function isAuthenticated() * * @return Saml2User */ - public function getSaml2User() + public function getSaml2User(): Saml2User { return new Saml2User($this->base, $this->idp); } @@ -63,9 +58,9 @@ public function getSaml2User() /** * The ID of the last message processed. * - * @return String + * @return string */ - public function getLastMessageId() + public function getLastMessageId(): string { return $this->base->getLastMessageId(); } @@ -88,13 +83,13 @@ public function getLastMessageId() * @throws OneLoginError */ public function login( - $returnTo = null, - $parameters = array(), - $forceAuthn = false, - $isPassive = false, - $stay = false, - $setNameIdPolicy = true - ) + string $returnTo = null, + array $parameters = [], + bool $forceAuthn = false, + bool $isPassive = false, + bool $stay = false, + bool $setNameIdPolicy = true + ): ?string { return $this->base->login($returnTo, $parameters, $forceAuthn, $isPassive, $stay, $setNameIdPolicy); } @@ -110,18 +105,18 @@ public function login( * @param bool $stay True if we want to stay (returns the url string) False to redirect * @param string|null $nameIdNameQualifier The NameID NameQualifier will be set in the LogoutRequest. * - * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters + * @return string|null If $stay is true, it returns a string with the SLO URL + LogoutRequest + parameters * * @throws OneLoginError */ public function logout( - $returnTo = null, - $nameId = null, - $sessionIndex = null, - $nameIdFormat = null, - $stay = false, - $nameIdNameQualifier = null - ) + string $returnTo = null, + string $nameId = null, + string $sessionIndex = null, + string $nameIdFormat = null, + bool $stay = false, + string $nameIdNameQualifier = null + ): ?string { $auth = $this->base; @@ -136,13 +131,13 @@ public function logout( * @throws OneLoginError * @throws \OneLogin\Saml2\ValidationError */ - public function acs() + public function acs(): ?array { $this->base->processResponse(); $errors = $this->base->getErrors(); - if (!empty($errors)) { + if (!$errors) { return $errors; } @@ -156,7 +151,7 @@ public function acs() /** * Process the SAML Logout Response / Logout Request sent by the IdP. * - * Returns an array with errors if it can not logout. + * Returns an array with errors if it cannot log out. * * @param bool $retrieveParametersFromServer * @@ -164,7 +159,7 @@ public function acs() * * @throws \OneLogin\Saml2\Error */ - public function sls($retrieveParametersFromServer = false) + public function sls(bool $retrieveParametersFromServer = false): array { $this->base->processSLO(false, null, $retrieveParametersFromServer, function () { event(new SignedOut()); @@ -184,13 +179,13 @@ public function sls($retrieveParametersFromServer = false) * @throws \Exception * @throws \InvalidArgumentException If metadata is not correctly set */ - public function getMetadata() + public function getMetadata(): string { $settings = $this->base->getSettings(); $metadata = $settings->getSPMetadata(); $errors = $settings->validateMetadata($metadata); - if (!count($errors)) { + if (!$errors) { return $metadata; } @@ -203,11 +198,9 @@ public function getMetadata() /** * Get the last error reason from \OneLogin_Saml2_Auth, useful for error debugging. * - * @see \OneLogin_Saml2_Auth::getLastErrorReason() - * - * @return string + * @return string|null */ - public function getLastErrorReason() + public function getLastErrorReason(): ?string { return $this->base->getLastErrorReason(); } @@ -217,7 +210,7 @@ public function getLastErrorReason() * * @return OneLoginAuth */ - public function getBase() + public function getBase(): OneLoginAuth { return $this->base; } @@ -225,11 +218,11 @@ public function getBase() /** * Set a tenant * - * @param IdentityProvider $idp + * @param IdentityProvidable $idp * * @return void */ - public function setIdp(IdentityProvider $idp) + public function setIdp(IdentityProvidable $idp) { $this->idp = $idp; } @@ -237,9 +230,9 @@ public function setIdp(IdentityProvider $idp) /** * Get a resolved tenant. * - * @return IdentityProvider|null + * @return IdentityProvidable|null */ - public function getIdp() + public function getIdp(): ?IdentityProvidable { return $this->idp; } diff --git a/src/Commands/Create.php b/src/Commands/Create.php index 0c60b1d..deea6bf 100644 --- a/src/Commands/Create.php +++ b/src/Commands/Create.php @@ -6,7 +6,8 @@ use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\Unique; use Slides\Saml2\Helpers\ConsoleHelper; -use Slides\Saml2\Repositories\TenantRepository; +use Slides\Saml2\Repositories\IdentityProviderRepository; +use Ramsey\Uuid\Uuid; class Create extends \Illuminate\Console\Command { @@ -36,16 +37,16 @@ class Create extends \Illuminate\Console\Command protected $description = 'Create an Identity Provider'; /** - * @var TenantRepository + * @var IdentityProviderRepository */ - protected TenantRepository $tenants; + protected IdentityProviderRepository $tenants; /** * DeleteTenant constructor. * - * @param TenantRepository $tenants + * @param IdentityProviderRepository $tenants */ - public function __construct(TenantRepository $tenants) + public function __construct(IdentityProviderRepository $tenants) { $this->tenants = $tenants; @@ -61,10 +62,10 @@ public function handle() { $metadata = ConsoleHelper::stringToArray($this->option('metadata')); - $model = config('saml2.tenantModel'); + $model = config('saml2.idpModel'); $tenant = new $model([ 'key' => $this->option('key'), - 'uuid' => \Ramsey\Uuid\Uuid::uuid4(), + 'uuid' => Uuid::uuid4(), 'idp_entity_id' => $this->option('entityId'), 'idp_login_url' => $this->option('loginUrl'), 'idp_logout_url' => $this->option('logoutUrl'), @@ -75,7 +76,7 @@ public function handle() ]); if(!$tenant->save()) { - $this->error('Tenant cannot be saved.'); + $this->error('IdentityProvidable cannot be saved.'); return; } @@ -107,7 +108,7 @@ public function handle() protected function rules(): array { return [ - 'key' => ['string', new Unique(config('saml2.tenantModel'), 'key')], + 'key' => ['string', new Unique(config('saml2.idpModel'), 'key')], 'entityId' => 'string', 'loginUrl' => 'string|url', 'logoutUrl' => 'string|url', diff --git a/src/Commands/RendersTenants.php b/src/Commands/RendersTenants.php index 115b6b6..779cf0e 100644 --- a/src/Commands/RendersTenants.php +++ b/src/Commands/RendersTenants.php @@ -2,20 +2,15 @@ namespace Slides\Saml2\Commands; -use Slides\Saml2\Contracts\IdentityProvider; +use Slides\Saml2\Contracts\IdentityProvidable; use Illuminate\Support\Str; -/** - * Class CreateTenant - * - * @package Slides\Saml2\Commands - */ trait RendersTenants { /** * Render tenants in a table. * - * @param IdentityProvider|\Illuminate\Support\Collection $tenants + * @param IdentityProvidable|\Illuminate\Support\Collection $tenants * @param string|null $title * * @return void @@ -23,7 +18,7 @@ trait RendersTenants protected function renderTenants($tenants, string $title = null) { /** @var \Slides\Saml2\Models\IdentityProvider[]|\Illuminate\Database\Eloquent\Collection $tenants */ - $tenants = $tenants instanceof IdentityProvider + $idps = $tenants instanceof IdentityProvidable ? collect([$tenants]) : $tenants; @@ -48,13 +43,13 @@ protected function renderTenants($tenants, string $title = null) } /** - * Get a columns of the Tenant. + * Get a columns of the IdentityProvidable. * - * @param IdentityProvider $tenant + * @param IdentityProvidable $tenant * * @return array */ - protected function getTenantColumns(IdentityProvider $tenant) + protected function getTenantColumns(IdentityProvidable $tenant) { return [ 'ID' => $tenant->id, @@ -74,22 +69,22 @@ protected function getTenantColumns(IdentityProvider $tenant) } /** - * Render a tenant credentials. + * Render IDP credentials. * - * @param IdentityProvider $tenant + * @param IdentityProvidable $idp * * @return void */ - protected function renderTenantCredentials(IdentityProvider $tenant) + protected function renderTenantCredentials(IdentityProvidable $idp) { - $this->output->section('Credentials for the tenant'); + $this->output->section('Identity Provider credentials'); $this->getOutput()->text([ - 'Identifier (Entity ID): ' . route('saml.metadata', ['uuid' => $tenant->uuid]) . '', - 'Reply URL (Assertion Consumer Service URL): ' . route('saml.acs', ['uuid' => $tenant->uuid]) . '', - 'Sign on URL: ' . route('saml.login', ['uuid' => $tenant->uuid]) . '', - 'Logout URL: ' . route('saml.logout', ['uuid' => $tenant->uuid]) . '', - 'Relay State: ' . ($tenant->relay_state_url ?: config('saml2.loginRoute')) . ' (optional)' + 'Identifier (Entity ID): ' . route('saml.metadata', ['uuid' => $idp->uuid]) . '', + 'Reply URL (Assertion Consumer Service URL): ' . route('saml.acs', ['uuid' => $idp->uuid]) . '', + 'Sign on URL: ' . route('saml.login', ['uuid' => $idp->uuid]) . '', + 'Logout URL: ' . route('saml.logout', ['uuid' => $idp->uuid]) . '', + 'Relay State: ' . ($idp->relay_state_url ?: config('saml2.loginRoute')) . ' (optional)' ]); } @@ -100,7 +95,7 @@ protected function renderTenantCredentials(IdentityProvider $tenant) * * @return string */ - protected function renderArray(array $array) + protected function renderArray(array $array): string { $lines = []; diff --git a/src/Commands/Update.php b/src/Commands/Update.php index 4d4b173..0c1a67b 100644 --- a/src/Commands/Update.php +++ b/src/Commands/Update.php @@ -7,7 +7,7 @@ use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\Unique; use Slides\Saml2\Helpers\ConsoleHelper; -use Slides\Saml2\Repositories\TenantRepository; +use Slides\Saml2\Repositories\IdentityProviderRepository; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -28,29 +28,28 @@ class Update extends \Illuminate\Console\Command { --relayStateUrl= : Redirection URL after successful login } { --nameIdFormat= : Name ID Format ("persistent" by default) } { --x509cert= : x509 certificate (base64) } - { --metadata= : A custom metadata } - '; + { --metadata= : A custom metadata }'; /** * The console command description. * * @var string */ - protected $description = 'Update an Entity Provider'; + protected $description = 'Update an existing Identity Provider'; /** - * @var TenantRepository + * @var IdentityProviderRepository */ - protected TenantRepository $tenants; + protected IdentityProviderRepository $repository; /** * DeleteTenant constructor. * - * @param TenantRepository $tenants + * @param IdentityProviderRepository $repository */ - public function __construct(TenantRepository $tenants) + public function __construct(IdentityProviderRepository $repository) { - $this->tenants = $tenants; + $this->repository = $repository; parent::__construct(); } @@ -62,7 +61,7 @@ public function __construct(TenantRepository $tenants) */ public function handle() { - $tenant = $this->tenants->findById($this->argument('id')); + $tenant = $this->repository->findById($this->argument('id')); $tenant->update(array_filter([ 'key' => $this->option('key'), @@ -75,12 +74,12 @@ public function handle() 'metadata' => ConsoleHelper::stringToArray($this->option('metadata')) ])); - if(!$tenant->save()) { - $this->error('Tenant cannot be saved.'); + if (!$tenant->save()) { + $this->error('Identity Provider cannot be saved.'); return; } - $this->info("The tenant #{$tenant->id} ({$tenant->uuid}) was successfully updated."); + $this->info("The tenant #$tenant->id ($tenant->uuid) was successfully updated."); $this->renderTenantCredentials($tenant); @@ -100,7 +99,7 @@ protected function interact(InputInterface $input, OutputInterface $output) } if (! $input->getOption('loginUrl')) { - $input->setOption('loginUrl', $this->ask('Login ID (fx. https://login.microsoftonline.com/65b9e948-757b-4431-b140-62a2f8a3fdeb/saml2)')); + $input->setOption('loginUrl', $this->ask('Login URL (fx. https://login.microsoftonline.com/65b9e948-757b-4431-b140-62a2f8a3fdeb/saml2)')); } if (! $input->getOption('logoutUrl')) { @@ -132,8 +131,8 @@ protected function interact(InputInterface $input, OutputInterface $output) protected function rules(): array { return [ - 'id' => ['required', 'int', new Exists(config('saml2.tenantModel'))], - 'key' => ['string', new Unique(config('saml2.tenantModel'), 'key')], + 'id' => ['required', 'int', new Exists(config('saml2.idpModel'))], + 'key' => ['string', new Unique(config('saml2.idpModel'), 'key')], 'entityId' => 'string', 'loginUrl' => 'string|url', 'logoutUrl' => 'string|url', diff --git a/src/Concerns/IdentityProviderAuthenticatable.php b/src/Concerns/IdentityProviderAuthenticatable.php index ee697c6..c47800b 100644 --- a/src/Concerns/IdentityProviderAuthenticatable.php +++ b/src/Concerns/IdentityProviderAuthenticatable.php @@ -3,18 +3,18 @@ namespace Slides\Saml2\Concerns; use Illuminate\Database\Eloquent\Model; -use Slides\Saml2\Contracts\IdentityProvider; -use Slides\Saml2\Models\Tenant; +use Slides\Saml2\Contracts\IdentityProvidable; +use Slides\Saml2\Models\IdentityProvider; trait IdentityProviderAuthenticatable { /** * The identity provider. * - * @return Model|IdentityProvider|Tenant + * @return Model|IdentityProvider|IdentityProvider */ public function identityProvider() { - $this->morphOne(config('saml2.tenantModel'), 'authenticatable'); + $this->morphOne(config('saml2.idpModel'), 'tenant'); } } diff --git a/src/Concerns/UserResolverHelpers.php b/src/Concerns/UserResolverHelpers.php new file mode 100644 index 0000000..462e967 --- /dev/null +++ b/src/Concerns/UserResolverHelpers.php @@ -0,0 +1,141 @@ +getUserId(), FILTER_VALIDATE_EMAIL)) { + return $user->getUserId(); + } + + // Otherwise, we need to lookup through attributes. + return $this->firstUserAttribute($user, $this->userEmailAttributes); + } + + /** + * Resolve a user name. + * + * @param Saml2User $user + * + * @return mixed|string|null + */ + protected function resolveUserName(Saml2User $user) + { + // First of all, we need to look up through attributes + if ($name = $this->firstUserAttribute($user, $this->userNameAttributes)) { + return $name; + } + + Log::warning('[SSO] Not able to resolve user name, extracting from email.', [ + 'samlAttributes' => $user->getAttributes(), + 'samlUserId' => $user->getUserId(), + 'samlNameId' => $user->getNameId() + ]); + + // Not the best solution, but if user name cannot be resolved, + // we can try to extract it from the email address + return $this->extractNameFromEmail( + $this->resolveUserEmail($user) + ); + } + + /** + * Find a user attribute value using a list of names. + * + * @param Saml2User $user + * @param array $attributes + * @param string|null $default + * + * @return mixed|string|null + */ + protected function firstUserAttribute(Saml2User $user, array $attributes, $default = null) + { + return Arr::first($attributes, fn($attribute) => $this->getUserAttribute($user, $attribute), $default); + } + + /** + * Get user's attribute. + * + * @param Saml2User $user + * @param string $attribute + * @param string|null $default + * + * @return string|mixed|null + */ + protected function getUserAttribute(Saml2User $user, string $attribute, string $default = null): ?string + { + foreach ($user->getAttributes() as $claim => $value) { + $value = $value[0]; + + if (strpos($claim, $attribute) !== false) { + return $value; + } + } + + return $default; + } + + /** + * Attempt to extract full name from the email address. + * + * @param string $email + * + * @return string|null + */ + protected function extractNameFromEmail(string $email): ?string + { + // Extract words from the email name + preg_match_all('/[a-z]+/i', Str::before($email, '@'), $matches); + + $words = $matches[0]; + + if (!$words) { + return null; + } + + // Keep only two first words and capitalize them + $words = array_map( + fn(string $word) => Str::title($word), + array_slice($words, 0, 2) + ); + + return implode(' ', $words); + } +} diff --git a/src/Contracts/IdentityProvider.php b/src/Contracts/IdentityProvidable.php similarity index 78% rename from src/Contracts/IdentityProvider.php rename to src/Contracts/IdentityProvidable.php index 7440983..dd26aff 100644 --- a/src/Contracts/IdentityProvider.php +++ b/src/Contracts/IdentityProvidable.php @@ -2,7 +2,7 @@ namespace Slides\Saml2\Contracts; -interface IdentityProvider +interface IdentityProvidable { public function idpUuid(); @@ -12,7 +12,7 @@ public function idpLoginUrl(): string; public function idpLogoutUrl(): string; - public function idpX509cert(): string; + public function idpX509cert(): ?string; public function idpNameIdFormat(): string; } diff --git a/src/Contracts/ResolvesIdentityProvider.php b/src/Contracts/ResolvesIdentityProvider.php index 0a40baf..5321ee8 100644 --- a/src/Contracts/ResolvesIdentityProvider.php +++ b/src/Contracts/ResolvesIdentityProvider.php @@ -2,6 +2,8 @@ namespace Slides\Saml2\Contracts; +use Illuminate\Http\Request; + interface ResolvesIdentityProvider { /** @@ -9,7 +11,7 @@ interface ResolvesIdentityProvider * * @param Request $request * - * @return IdentityProvider|null + * @return IdentityProvidable|null */ - public function resolve($request): ?IdentityProvider; + public function resolve(Request $request): ?IdentityProvidable; } diff --git a/src/Contracts/ResolvesIdpConfig.php b/src/Contracts/ResolvesIdpConfig.php index 696bcd7..bf53fca 100644 --- a/src/Contracts/ResolvesIdpConfig.php +++ b/src/Contracts/ResolvesIdpConfig.php @@ -7,10 +7,10 @@ interface ResolvesIdpConfig /** * Adjust SAML configuration for the given identity provider. * - * @param IdentityProvider $idp + * @param IdentityProvidable $idp * @param array $config * * @return void */ - public function resolve(IdentityProvider $idp, array $config): array; + public function resolve(IdentityProvidable $idp, array $config): array; } diff --git a/src/Contracts/ResolvesUser.php b/src/Contracts/ResolvesUser.php new file mode 100644 index 0000000..ef3fd3c --- /dev/null +++ b/src/Contracts/ResolvesUser.php @@ -0,0 +1,17 @@ + [ + LoginUser::class, + ] + ]; + + /** + * Register any events for your application. + * + * @return void + */ + public function boot() + { + parent::boot(); + } +} diff --git a/src/Events/IdentityProviderResolved.php b/src/Events/IdentityProviderResolved.php new file mode 100644 index 0000000..8a4e2c2 --- /dev/null +++ b/src/Events/IdentityProviderResolved.php @@ -0,0 +1,35 @@ +auth = $auth; + $this->idp = $idp; + } +} diff --git a/src/Events/SignedIn.php b/src/Events/SignedIn.php index fef1891..6f277cd 100644 --- a/src/Events/SignedIn.php +++ b/src/Events/SignedIn.php @@ -5,11 +5,6 @@ use Slides\Saml2\Saml2User; use Slides\Saml2\Auth; -/** - * Class LoggedIn - * - * @package Slides\Saml2\Events - */ class SignedIn { /** @@ -17,14 +12,14 @@ class SignedIn * * @var Saml2User */ - public $user; + public Saml2User $user; /** * The authentication handler. * * @var Auth */ - public $auth; + public Auth $auth; /** * LoggedIn constructor. diff --git a/src/Events/SignedOut.php b/src/Events/SignedOut.php index bfb81f5..daf4b6b 100644 --- a/src/Events/SignedOut.php +++ b/src/Events/SignedOut.php @@ -2,11 +2,6 @@ namespace Slides\Saml2\Events; -/** - * Class LoggedOut - * - * @package Slides\Saml2\Events - */ class SignedOut { } diff --git a/src/Exceptions/ConfigurationException.php b/src/Exceptions/ConfigurationException.php new file mode 100644 index 0000000..552cd1a --- /dev/null +++ b/src/Exceptions/ConfigurationException.php @@ -0,0 +1,7 @@ + $message, + 'attributes' => $saml2User->getAttributes() + ]); + } + + parent::__construct($message); + } +} diff --git a/src/Helpers/ConsoleHelper.php b/src/Helpers/ConsoleHelper.php index 2fe6576..4695620 100644 --- a/src/Helpers/ConsoleHelper.php +++ b/src/Helpers/ConsoleHelper.php @@ -4,11 +4,6 @@ use Illuminate\Support\Arr; -/** - * Class ConsoleHelper - * - * @package App\Helpers - */ class ConsoleHelper { /** @@ -24,7 +19,7 @@ class ConsoleHelper */ public static function stringToArray(string $string = null, string $valueDelimiter = ':', string $itemDelimiter = ',') { - if(is_null($string)) { + if (is_null($string)) { return []; } @@ -37,7 +32,7 @@ public static function stringToArray(string $string = null, string $valueDelimit $key = Arr::get($item, 0); $value = Arr::get($item, 1); - if(is_null($value)) { + if (is_null($value)) { $value = $key; $key = $index; } @@ -74,4 +69,4 @@ public static function arrayToString(array $array): string return implode(',', $values); } -} \ No newline at end of file +} diff --git a/src/Http/Controllers/Saml2Controller.php b/src/Http/Controllers/Saml2Controller.php index c3d68d9..35c4388 100644 --- a/src/Http/Controllers/Saml2Controller.php +++ b/src/Http/Controllers/Saml2Controller.php @@ -45,7 +45,7 @@ public function acs(Auth $auth) if ($errors) { $error = $auth->getLastErrorReason(); - $uuid = $auth->getTenant()->uuid; + $uuid = $auth->getIdp()->uuid; logger()->error('saml2.error_detail', compact('uuid', 'error')); session()->flash('saml2.error_detail', [$error]); @@ -96,9 +96,9 @@ public function sls(Auth $auth) { $errors = $auth->sls(config('saml2.retrieveParametersFromServer')); - if (!empty($errors)) { + if (count($errors)) { $error = $auth->getLastErrorReason(); - $uuid = $auth->getTenant()->uuid; + $uuid = $auth->getIdp()->uuid; logger()->error('saml2.error_detail', compact('uuid', 'error')); session()->flash('saml2.error_detail', [$error]); diff --git a/src/Http/Middleware/ResolveIdentityProvider.php b/src/Http/Middleware/ResolveIdentityProvider.php index 465ea77..4edda15 100644 --- a/src/Http/Middleware/ResolveIdentityProvider.php +++ b/src/Http/Middleware/ResolveIdentityProvider.php @@ -50,12 +50,12 @@ public function handle(Request $request, \Closure $next) } if (config('saml2.debug')) { - Log::debug('[Saml2] Tenant resolved', [ + Log::debug('[Saml2] Identity Provider resolved', [ 'uuid' => $idp->idpUuid() ]); } - session()->flash('saml2.tenant.uuid', $idp->idpUuid()); + session()->flash('saml2.idp.uuid', $idp->idpUuid()); $this->builder->configureIdp($idp); diff --git a/src/Http/routes.php b/src/Http/routes.php index 0758ec4..8980791 100644 --- a/src/Http/routes.php +++ b/src/Http/routes.php @@ -9,25 +9,25 @@ Route::get('/{uuid}/logout', array( 'as' => 'saml.logout', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@logout', - )); + ))->whereUuid('uuid'); Route::get('/{uuid}/login', array( 'as' => 'saml.login', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@login', - )); + ))->whereUuid('uuid'); Route::get('/{uuid}/metadata', array( 'as' => 'saml.metadata', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@metadata', - )); + ))->whereUuid('uuid'); Route::post('/{uuid}/acs', array( 'as' => 'saml.acs', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@acs', - )); + ))->whereUuid('uuid'); Route::get('/{uuid}/sls', array( 'as' => 'saml.sls', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@sls', - )); + ))->whereUuid('uuid'); }); diff --git a/src/Listeners/LoginUser.php b/src/Listeners/LoginUser.php new file mode 100644 index 0000000..50ed34a --- /dev/null +++ b/src/Listeners/LoginUser.php @@ -0,0 +1,95 @@ +resolveOrCreateUser($event->getAuth()); + + Auth::login($user); + + tap(new Session([ + 'idp_id' => $event->getAuth()->getIdp()->id, + 'user_id' => $user, + 'payload' => $event->getAuth()->getSaml2User()->getAttributes() + ]))->save(); + } + + /** + * Resolve or create a new user. + * + * @param SamlAuth $samlAuth + * + * @return Authenticatable + */ + protected function resolveOrCreateUser(SamlAuth $samlAuth): Authenticatable + { + $resolvedUser = app(ResolvesUser::class)->resolve($samlAuth); + + if ($resolvedUser) { + return $resolvedUser; + } + + if (!config('saml2.auth.createUser')) { + throw new UserResolutionException('Cannot signup a new user. Enable this ability or create your own logic', $samlAuth->getSaml2User()); + } + + if (config('saml2.debug')) { + Log::debug('[Saml2] Creating a new user', [ + 'idpUuid' => $samlAuth->getIdp()->idpUuid(), + 'userId' => $samlAuth->getSaml2User()->getUserId(), + 'userAttributes' => $samlAuth->getSaml2User()->getAttributes(), + ]); + } + + return $this->createUser($samlAuth->getSaml2User()); + } + + /** + * Create a new user. + * + * @param Saml2User $samlUser + * + * @return \Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed + */ + protected function createUser(Saml2User $samlUser) + { + $model = config('saml2.auth.userModel'); + + $user = new $model; + $user->name = $this->resolveUserName($samlUser); + $user->email = $this->resolveUserEmail($samlUser); + $user->password = Hash::make(Str::random()); + $user->save(); + + return $user; + } +} diff --git a/src/Models/Tenant.php b/src/Models/IdentityProvider.php similarity index 73% rename from src/Models/Tenant.php rename to src/Models/IdentityProvider.php index fcfd21b..f593790 100644 --- a/src/Models/Tenant.php +++ b/src/Models/IdentityProvider.php @@ -3,13 +3,12 @@ namespace Slides\Saml2\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\SoftDeletes; -use Slides\Saml2\Contracts\IdentityProvider; +use Slides\Saml2\Contracts\IdentityProvidable; /** - * Class Tenant - * * @property int $id * @property string $uuid * @property string $key @@ -19,16 +18,17 @@ * @property string $idp_x509_cert * @property string $relay_state_url * @property string $name_id_format - * @property int $authenticatable_id - * @property string $authenticatable_type + * @property int|null $owner_id + * @property string|null $owner_type * @property array $metadata * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at * @property \Carbon\Carbon $deleted_at * - * @package Slides\Saml2\Models + * @property-read \Illuminate\Database\Eloquent\Model|null $tenant + * @property-read \Illuminate\Database\Eloquent\Model $sessions */ -class Tenant extends Model implements IdentityProvider +class IdentityProvider extends Model implements IdentityProvidable { use SoftDeletes; @@ -37,7 +37,7 @@ class Tenant extends Model implements IdentityProvider * * @var string */ - protected $table = 'saml2_tenants'; + protected $table = 'saml2_identity_providers'; /** * The attributes that are mass assignable. @@ -53,8 +53,8 @@ class Tenant extends Model implements IdentityProvider 'idp_x509_cert', 'relay_state_url', 'name_id_format', - 'authenticatable_id', - 'authenticatable_type', + 'tenant_id', + 'tenant_type', 'metadata' ]; @@ -102,7 +102,7 @@ public function idpLogoutUrl(): string /** * @return string */ - public function idpX509cert(): string + public function idpX509cert(): ?string { return $this->idp_x509_cert; } @@ -116,12 +116,22 @@ public function idpNameIdFormat(): string } /** - * The authenticatable model. + * The tenant model. * * @return MorphTo */ - public function authenticatable(): MorphTo + public function tenant(): MorphTo { return $this->morphTo(); } + + /** + * The sessions of the tenant. + * + * @return HasMany + */ + public function sessions(): HasMany + { + return $this->hasMany(Session::class); + } } diff --git a/src/Models/Session.php b/src/Models/Session.php new file mode 100644 index 0000000..ee250de --- /dev/null +++ b/src/Models/Session.php @@ -0,0 +1,67 @@ + 'array' + ]; + + /** + * The user model. + * + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(config('saml2.auth.userModel')); + } + + /** + * The tenant model (identity provider). + * + * @return HasOne + */ + public function tenant(): HasOne + { + return $this->hasOne(config('saml2.idpModel'), 'id', 'idp_id'); + } +} diff --git a/src/OneLoginBuilder.php b/src/OneLoginBuilder.php index 5f528ce..e1e9e8d 100644 --- a/src/OneLoginBuilder.php +++ b/src/OneLoginBuilder.php @@ -6,7 +6,7 @@ use OneLogin\Saml2\Utils as OneLoginUtils; use Illuminate\Support\Facades\URL; use Illuminate\Contracts\Container\Container; -use Slides\Saml2\Contracts\IdentityProvider; +use Slides\Saml2\Contracts\IdentityProvidable; use Slides\Saml2\Contracts\ResolvesIdpConfig; use Illuminate\Support\Arr; @@ -44,11 +44,11 @@ public function __construct(Container $app, ResolvesIdpConfig $configResolver) /** * Adjust OneLogin configuration according to the given identity provider. * - * @param IdentityProvider $idp + * @param IdentityProvidable $idp * * @return void */ - public function configureIdp(IdentityProvider $idp) + public function configureIdp(IdentityProvidable $idp) { if ($this->app['config']->get('saml2.proxyVars', false)) { OneLoginUtils::setProxyVars(true); diff --git a/src/Repositories/TenantRepository.php b/src/Repositories/IdentityProviderRepository.php similarity index 79% rename from src/Repositories/TenantRepository.php rename to src/Repositories/IdentityProviderRepository.php index 2ee30b2..018b00c 100644 --- a/src/Repositories/TenantRepository.php +++ b/src/Repositories/IdentityProviderRepository.php @@ -2,9 +2,9 @@ namespace Slides\Saml2\Repositories; -use Slides\Saml2\Models\Tenant; +use Slides\Saml2\Models\IdentityProvider; -class TenantRepository +class IdentityProviderRepository { /** * Create a new query. @@ -15,10 +15,10 @@ class TenantRepository */ public function query(bool $withTrashed = false) { - $class = config('saml2.tenantModel', Tenant::class); + $class = config('saml2.idpModel', IdentityProvider::class); $query = $class::query(); - if($withTrashed) { + if ($withTrashed) { $query->withTrashed(); } @@ -30,7 +30,7 @@ public function query(bool $withTrashed = false) * * @param bool $withTrashed Whether need to include safely deleted records. * - * @return Tenant[]|\Illuminate\Database\Eloquent\Collection + * @return IdentityProvider[]|\Illuminate\Database\Eloquent\Collection */ public function all(bool $withTrashed = true) { @@ -43,7 +43,7 @@ public function all(bool $withTrashed = true) * @param int|string $key ID, key or UUID * @param bool $withTrashed Whether need to include safely deleted records. * - * @return Tenant[]|\Illuminate\Database\Eloquent\Collection + * @return IdentityProvider[]|\Illuminate\Database\Eloquent\Collection */ public function findByAnyIdentifier($key, bool $withTrashed = true) { @@ -64,7 +64,7 @@ public function findByAnyIdentifier($key, bool $withTrashed = true) * @param string $key * @param bool $withTrashed * - * @return Tenant|\Illuminate\Database\Eloquent\Model|null + * @return IdentityProvider|\Illuminate\Database\Eloquent\Model|null */ public function findByKey(string $key, bool $withTrashed = true) { @@ -79,7 +79,7 @@ public function findByKey(string $key, bool $withTrashed = true) * @param int $id * @param bool $withTrashed * - * @return Tenant|\Illuminate\Database\Eloquent\Model|null + * @return IdentityProvider|\Illuminate\Database\Eloquent\Model|null */ public function findById(int $id, bool $withTrashed = true) { @@ -94,7 +94,7 @@ public function findById(int $id, bool $withTrashed = true) * @param int $uuid * @param bool $withTrashed * - * @return Tenant|\Illuminate\Database\Eloquent\Model|null + * @return IdentityProvider|\Illuminate\Database\Eloquent\Model|null */ public function findByUUID(string $uuid, bool $withTrashed = true) { diff --git a/src/Resolvers/ConfigResolver.php b/src/Resolvers/ConfigResolver.php index b0df125..e436860 100644 --- a/src/Resolvers/ConfigResolver.php +++ b/src/Resolvers/ConfigResolver.php @@ -2,20 +2,26 @@ namespace Slides\Saml2\Resolvers; -use Slides\Saml2\Contracts\IdentityProvider; +use Slides\Saml2\Contracts\IdentityProvidable; use Slides\Saml2\Contracts\ResolvesIdpConfig; +use Slides\Saml2\Exceptions\ConfigurationException; + class ConfigResolver implements ResolvesIdpConfig { /** * Adjust SAML configuration for the given identity provider. * - * @param IdentityProvider $idp + * @param IdentityProvidable $idp * @param array $config * * @return array */ - public function resolve(IdentityProvider $idp, array $config): array + public function resolve(IdentityProvidable $idp, array $config): array { + if ($idp->idpX509cert() === null) { + throw new ConfigurationException('Identity Provider certificate is missing'); + } + $config['idp'] = [ 'entityId' => $idp->idpEntityId(), 'singleSignOnService' => ['url' => $idp->idpLoginUrl()], diff --git a/src/Resolvers/IdentityProviderResolver.php b/src/Resolvers/IdentityProviderResolver.php index 2ef7a49..c663c43 100644 --- a/src/Resolvers/IdentityProviderResolver.php +++ b/src/Resolvers/IdentityProviderResolver.php @@ -4,23 +4,23 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; -use Slides\Saml2\Contracts\IdentityProvider; +use Slides\Saml2\Contracts\IdentityProvidable; use Slides\Saml2\Contracts\ResolvesIdentityProvider; -use Slides\Saml2\Repositories\TenantRepository; +use Slides\Saml2\Repositories\IdentityProviderRepository; class IdentityProviderResolver implements ResolvesIdentityProvider { /** - * @var TenantRepository + * @var IdentityProviderRepository */ - protected TenantRepository $tenants; + protected IdentityProviderRepository $repository; /** - * @param TenantRepository $tenants + * @param IdentityProviderRepository $repository */ - public function __construct(TenantRepository $tenants) + public function __construct(IdentityProviderRepository $repository) { - $this->tenants = $tenants; + $this->repository = $repository; } /** @@ -28,13 +28,13 @@ public function __construct(TenantRepository $tenants) * * @param Request $request * - * @return IdentityProvider|null + * @return IdentityProvidable|null */ - public function resolve($request): ?IdentityProvider + public function resolve(Request $request): ?IdentityProvidable { if (!$uuid = $request->route('uuid')) { if (config('saml2.debug')) { - Log::debug('[Saml2] Tenant UUID is not present in the URL so cannot be resolved', [ + Log::debug('[Saml2] Identity Provider UUID is not present in the URL so cannot be resolved', [ 'url' => $request->fullUrl() ]); } @@ -42,11 +42,9 @@ public function resolve($request): ?IdentityProvider return null; } - if (!$idp = $this->tenants->findByUUID($uuid)) { + if (!$idp = $this->repository->findByUUID($uuid)) { if (config('saml2.debug')) { - Log::debug('[Saml2] Tenant doesn\'t exist', [ - 'uuid' => $uuid - ]); + Log::debug('[Saml2] Identity Provider cannot be found', ['uuid' => $uuid]); } return null; @@ -54,7 +52,7 @@ public function resolve($request): ?IdentityProvider if ($idp->trashed()) { if (config('saml2.debug')) { - Log::debug('[Saml2] Tenant #' . $idp->id. ' resolved but marked as deleted', [ + Log::debug("[Saml2] Identity Provider #{$idp->id} resolved but marked as deleted", [ 'id' => $idp->id, 'uuid' => $uuid, 'deleted_at' => $idp->deleted_at->toDateTimeString() diff --git a/src/Resolvers/UserResolver.php b/src/Resolvers/UserResolver.php new file mode 100644 index 0000000..1d1c6e8 --- /dev/null +++ b/src/Resolvers/UserResolver.php @@ -0,0 +1,38 @@ +resolveUserEmail($samlAuth->getSaml2User())) { + throw new UserResolutionException('Unable to resolve user email', $samlAuth->getSaml2User()); + } + + if (!$provider = config('auth.defaults.passwords')) { + throw new ConfigurationException('No default password provider configured'); + } + + // Attempt to retrieve user by email. + return Auth::createUserProvider($provider)->retrieveByCredentials(['email' => $email]); + } +} diff --git a/src/Saml2User.php b/src/Saml2User.php index 188cf59..e92b82a 100644 --- a/src/Saml2User.php +++ b/src/Saml2User.php @@ -3,7 +3,7 @@ namespace Slides\Saml2; use OneLogin\Saml2\Auth as OneLoginAuth; -use Slides\Saml2\Contracts\IdentityProvider; +use Slides\Saml2\Contracts\IdentityProvidable; class Saml2User { @@ -17,7 +17,7 @@ class Saml2User /** * The tenant user belongs to. * - * @var IdentityProvider + * @var IdentityProvidable */ protected $idp; @@ -25,9 +25,9 @@ class Saml2User * Saml2User constructor. * * @param OneLoginAuth $auth - * @param IdentityProvider $idp + * @param IdentityProvidable $idp */ - public function __construct(OneLoginAuth $auth, IdentityProvider $idp) + public function __construct(OneLoginAuth $auth, IdentityProvidable $idp) { $this->auth = $auth; $this->idp = $idp; @@ -161,11 +161,11 @@ public function getNameId(): string /** * Set a tenant * - * @param IdentityProvider $idp + * @param IdentityProvidable $idp * * @return void */ - public function setIdp(IdentityProvider $idp) + public function setIdp(IdentityProvidable $idp) { $this->idp = $idp; } @@ -173,7 +173,7 @@ public function setIdp(IdentityProvider $idp) /** * Get a resolved tenant. * - * @return IdentityProvider|null + * @return IdentityProvidable|null */ public function getIdp() { diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index a7afc18..aabe141 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -20,9 +20,15 @@ public function register() { $this->mergeConfigFrom(__DIR__ . '/../config/saml2.php', 'saml2'); - $this->app->bind(\Slides\Saml2\Contracts\IdentityProvider::class, config('saml2.tenantModel')); + $this->app->bind(\Slides\Saml2\Contracts\IdentityProvidable::class, config('saml2.tenantModel')); $this->app->bind(\Slides\Saml2\Contracts\ResolvesIdentityProvider::class, config('saml2.resolvers.idp')); $this->app->bind(\Slides\Saml2\Contracts\ResolvesIdpConfig::class, config('saml2.resolvers.config')); + + if (config('saml2.auth.enabled')) { + $this->app->bind(\Slides\Saml2\Contracts\ResolvesUser::class, config('saml2.auth.resolver')); + } + + $this->app->register(EventServiceProvider::class); } /** diff --git a/src/helpers.php b/src/helpers.php index 28ec6d7..1d382e4 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -16,8 +16,8 @@ function saml_url(string $path, string $uuid = null, $parameters = [], bool $sec { $target = \Illuminate\Support\Facades\URL::to($path, $parameters, $secure); - if(!$uuid) { - if(!$uuid = saml_tenant_uuid()) { + if (!$uuid) { + if (!$uuid = saml_idp_uuid()) { return $target; } } @@ -41,8 +41,8 @@ function saml_route(string $name, string $uuid = null, $parameters = []) { $target = \Illuminate\Support\Facades\URL::route($name, $parameters, true); - if(!$uuid) { - if(!$uuid = saml_tenant_uuid()) { + if (!$uuid) { + if (!$uuid = saml_idp_uuid()) { return $target; } } @@ -51,15 +51,15 @@ function saml_route(string $name, string $uuid = null, $parameters = []) } } -if (!function_exists('saml_tenant_uuid')) +if (!function_exists('saml_idp_uuid')) { /** - * Get a resolved Tenant UUID based on current URL. + * Get a resolved Identity Provider UUID based on current URL. * * @return string|null */ - function saml_tenant_uuid() + function saml_idp_uuid() { - return session()->get('saml2.tenant.uuid'); + return session()->get('saml2.idp.uuid'); } -} \ No newline at end of file +}