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/composer.json b/composer.json
index 711898b..cd0495d 100644
--- a/composer.json
+++ b/composer.json
@@ -12,13 +12,14 @@
}
],
"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.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 541f9fb..1309dc6 100644
--- a/config/saml2.php
+++ b/config/saml2.php
@@ -4,14 +4,48 @@
/*
|--------------------------------------------------------------------------
- | 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,
+
+ /*
+ |--------------------------------------------------------------------------
+ | 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,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | 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),
+ ],
/*
|--------------------------------------------------------------------------
@@ -170,7 +204,7 @@
|
*/
- 'x509cert' => env('SAML2_SP_CERT_x509',''),
+ 'x509cert' => env('SAML2_SP_CERT_X509',''),
'privateKey' => env('SAML2_SP_CERT_PRIVATEKEY',''),
/*
@@ -393,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
new file mode 100644
index 0000000..dae596f
--- /dev/null
+++ b/database/migrations/2023_07_21_203815_add_model_morph_columns_to_saml2_tenants_table.php
@@ -0,0 +1,36 @@
+nullableMorphs('tenant');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ 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
new file mode 100644
index 0000000..bd5403d
--- /dev/null
+++ b/database/migrations/2023_07_21_205057_create_saml2_sessions_table.php
@@ -0,0 +1,34 @@
+id();
+ $table->foreignId('idp_id')->constrained('saml2_identity_providers');
+ $table->foreignId('user_id')->nullable();
+ $table->json('payload');
+ $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..452d8bd
--- /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_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();
+ $table->string('name_id_format')->nullable(false)->change();
+ $table->text('idp_x509_cert')->nullable(false)->change();
+ });
+ }
+}
diff --git a/src/Auth.php b/src/Auth.php
index ce94894..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\IdentityProvidable;
use Slides\Saml2\Events\SignedOut;
-use Slides\Saml2\Models\Tenant;
-/**
- * Class Auth
- *
- * @package Slides\Saml2
- */
class Auth
{
/**
@@ -19,25 +14,25 @@ class Auth
*
* @var OneLoginAuth
*/
- protected $base;
+ protected OneLoginAuth $base;
/**
* The resolved tenant.
*
- * @var Tenant
+ * @var IdentityProvidable
*/
- protected $tenant;
+ protected IdentityProvidable $idp;
/**
* Auth constructor.
*
* @param OneLoginAuth $auth
- * @param Tenant $tenant
+ * @param IdentityProvidable $idp
*/
- public function __construct(OneLoginAuth $auth, Tenant $tenant)
+ public function __construct(OneLoginAuth $auth, IdentityProvidable $idp)
{
$this->base = $auth;
- $this->tenant = $tenant;
+ $this->idp = $idp;
}
/**
@@ -45,7 +40,7 @@ public function __construct(OneLoginAuth $auth, Tenant $tenant)
*
* @return bool
*/
- public function isAuthenticated()
+ public function isAuthenticated(): bool
{
return $this->base->isAuthenticated();
}
@@ -55,17 +50,17 @@ public function isAuthenticated()
*
* @return Saml2User
*/
- public function getSaml2User()
+ public function getSaml2User(): Saml2User
{
- return new Saml2User($this->base, $this->tenant);
+ return new Saml2User($this->base, $this->idp);
}
/**
* 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,22 +218,22 @@ public function getBase()
/**
* Set a tenant
*
- * @param Tenant $tenant
+ * @param IdentityProvidable $idp
*
* @return void
*/
- public function setTenant(Tenant $tenant)
+ public function setIdp(IdentityProvidable $idp)
{
- $this->tenant = $tenant;
+ $this->idp = $idp;
}
/**
* Get a resolved tenant.
*
- * @return Tenant|null
+ * @return IdentityProvidable|null
*/
- public function getTenant()
+ public function getIdp(): ?IdentityProvidable
{
- return $this->tenant;
+ return $this->idp;
}
}
diff --git a/src/Commands/Create.php b/src/Commands/Create.php
new file mode 100644
index 0000000..deea6bf
--- /dev/null
+++ b/src/Commands/Create.php
@@ -0,0 +1,140 @@
+tenants = $tenants;
+
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $metadata = ConsoleHelper::stringToArray($this->option('metadata'));
+
+ $model = config('saml2.idpModel');
+ $tenant = new $model([
+ 'key' => $this->option('key'),
+ '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('IdentityProvidable 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.idpModel'), '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 eb8169d..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', Tenant::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();
- }
-}
\ No newline at end of file
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 5477344..779cf0e 100644
--- a/src/Commands/RendersTenants.php
+++ b/src/Commands/RendersTenants.php
@@ -2,28 +2,23 @@
namespace Slides\Saml2\Commands;
-use Slides\Saml2\Models\Tenant;
+use Slides\Saml2\Contracts\IdentityProvidable;
use Illuminate\Support\Str;
-/**
- * Class CreateTenant
- *
- * @package Slides\Saml2\Commands
- */
trait RendersTenants
{
/**
* Render tenants in a table.
*
- * @param \Slides\Saml2\Models\Tenant|\Illuminate\Support\Collection $tenants
+ * @param IdentityProvidable|\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 */
+ $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 \Slides\Saml2\Models\Tenant $tenant
+ * @param IdentityProvidable $tenant
*
* @return array
*/
- protected function getTenantColumns(Tenant $tenant)
+ protected function getTenantColumns(IdentityProvidable $tenant)
{
return [
'ID' => $tenant->id,
@@ -74,22 +69,22 @@ protected function getTenantColumns(Tenant $tenant)
}
/**
- * Render a tenant credentials.
+ * Render IDP credentials.
*
- * @param \Slides\Saml2\Models\Tenant $tenant
+ * @param IdentityProvidable $idp
*
* @return void
*/
- protected function renderTenantCredentials(Tenant $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(Tenant $tenant)
*
* @return string
*/
- protected function renderArray(array $array)
+ protected function renderArray(array $array): string
{
$lines = [];
@@ -110,4 +105,4 @@ protected function renderArray(array $array)
return implode(PHP_EOL, $lines);
}
-}
\ No newline at end of file
+}
diff --git a/src/Commands/Update.php b/src/Commands/Update.php
new file mode 100644
index 0000000..0c1a67b
--- /dev/null
+++ b/src/Commands/Update.php
@@ -0,0 +1,164 @@
+repository = $repository;
+
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $tenant = $this->repository->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('Identity Provider 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 URL (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.idpModel'))],
+ 'key' => ['string', new Unique(config('saml2.idpModel'), '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/Concerns/IdentityProviderAuthenticatable.php b/src/Concerns/IdentityProviderAuthenticatable.php
new file mode 100644
index 0000000..c47800b
--- /dev/null
+++ b/src/Concerns/IdentityProviderAuthenticatable.php
@@ -0,0 +1,20 @@
+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/IdentityProvidable.php b/src/Contracts/IdentityProvidable.php
new file mode 100644
index 0000000..dd26aff
--- /dev/null
+++ b/src/Contracts/IdentityProvidable.php
@@ -0,0 +1,18 @@
+ [
+ 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/Facades/Auth.php b/src/Facades/Auth.php
index 969f6bf..83b5412 100644
--- a/src/Facades/Auth.php
+++ b/src/Facades/Auth.php
@@ -7,7 +7,7 @@
/**
* Class Saml2Auth
*
- * @method static \Slides\Saml2\Models\Tenant|null getTenant()
+ * @method static \Slides\Saml2\Models\IdentityProvider|null getTenant()
*
* @package Slides\Saml2\Facades
*/
@@ -22,4 +22,4 @@ protected static function getFacadeAccessor()
{
return 'Slides\Saml2\Auth';
}
-}
\ No newline at end of file
+}
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 61e913b..35c4388 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,9 +43,9 @@ public function acs(Auth $auth)
{
$errors = $auth->acs();
- if (!empty($errors)) {
+ 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]);
@@ -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();
@@ -70,7 +75,7 @@ public function acs(Auth $auth)
return redirect($redirectUrl);
}
- return redirect($auth->getTenant()->relay_state_url ?: config('saml2.loginRoute'));
+ return redirect($auth->getIdp()->relay_state_url ?: config('saml2.loginRoute'));
}
/**
@@ -91,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]);
@@ -119,7 +124,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..4edda15
--- /dev/null
+++ b/src/Http/Middleware/ResolveIdentityProvider.php
@@ -0,0 +1,64 @@
+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] Identity Provider resolved', [
+ 'uuid' => $idp->idpUuid()
+ ]);
+ }
+
+ session()->flash('saml2.idp.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/Http/routes.php b/src/Http/routes.php
index 90a23b3..8980791 100644
--- a/src/Http/routes.php
+++ b/src/Http/routes.php
@@ -4,30 +4,30 @@
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',
'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/IdentityProvider.php b/src/Models/IdentityProvider.php
new file mode 100644
index 0000000..f593790
--- /dev/null
+++ b/src/Models/IdentityProvider.php
@@ -0,0 +1,137 @@
+ '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->idp_x509_cert;
+ }
+
+ /**
+ * @return string
+ */
+ public function idpNameIdFormat(): string
+ {
+ return $this->name_id_format;
+ }
+
+ /**
+ * The tenant model.
+ *
+ * @return 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/Models/Tenant.php b/src/Models/Tenant.php
deleted file mode 100644
index 34ba793..0000000
--- a/src/Models/Tenant.php
+++ /dev/null
@@ -1,63 +0,0 @@
- 'array'
- ];
-}
diff --git a/src/OneLoginBuilder.php b/src/OneLoginBuilder.php
index 4e1adf5..e1e9e8d 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\IdentityProvidable;
+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 IdentityProvidable $idp
*
* @return void
*/
- public function bootstrap()
+ public function configureIdp(IdentityProvidable $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/IdentityProviderRepository.php
similarity index 79%
rename from src/Repositories/TenantRepository.php
rename to src/Repositories/IdentityProviderRepository.php
index 4734e66..018b00c 100644
--- a/src/Repositories/TenantRepository.php
+++ b/src/Repositories/IdentityProviderRepository.php
@@ -2,14 +2,9 @@
namespace Slides\Saml2\Repositories;
-use Slides\Saml2\Models\Tenant;
+use Slides\Saml2\Models\IdentityProvider;
-/**
- * Class TenantRepository
- *
- * @package Slides\Saml2\Repositories
- */
-class TenantRepository
+class IdentityProviderRepository
{
/**
* Create a new query.
@@ -20,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();
}
@@ -35,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)
{
@@ -48,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)
{
@@ -69,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)
{
@@ -84,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)
{
@@ -99,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)
{
@@ -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..e436860
--- /dev/null
+++ b/src/Resolvers/ConfigResolver.php
@@ -0,0 +1,56 @@
+idpX509cert() === null) {
+ throw new ConfigurationException('Identity Provider certificate is missing');
+ }
+
+ $config['idp'] = [
+ 'entityId' => $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..c663c43
--- /dev/null
+++ b/src/Resolvers/IdentityProviderResolver.php
@@ -0,0 +1,67 @@
+repository = $repository;
+ }
+
+ /**
+ * Resolve a tenant from the request.
+ *
+ * @param Request $request
+ *
+ * @return IdentityProvidable|null
+ */
+ public function resolve(Request $request): ?IdentityProvidable
+ {
+ if (!$uuid = $request->route('uuid')) {
+ if (config('saml2.debug')) {
+ Log::debug('[Saml2] Identity Provider UUID is not present in the URL so cannot be resolved', [
+ 'url' => $request->fullUrl()
+ ]);
+ }
+
+ return null;
+ }
+
+ if (!$idp = $this->repository->findByUUID($uuid)) {
+ if (config('saml2.debug')) {
+ Log::debug('[Saml2] Identity Provider cannot be found', ['uuid' => $uuid]);
+ }
+
+ return null;
+ }
+
+ if ($idp->trashed()) {
+ if (config('saml2.debug')) {
+ Log::debug("[Saml2] Identity Provider #{$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/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 ba37f1f..e92b82a 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\IdentityProvidable;
-/**
- * Class Saml2User
- *
- * @package Slides\Saml2
- */
class Saml2User
{
/**
@@ -22,20 +17,20 @@ class Saml2User
/**
* The tenant user belongs to.
*
- * @var Tenant
+ * @var IdentityProvidable
*/
- protected $tenant;
+ protected $idp;
/**
* Saml2User constructor.
*
* @param OneLoginAuth $auth
- * @param Tenant $tenant
+ * @param IdentityProvidable $idp
*/
- public function __construct(OneLoginAuth $auth, Tenant $tenant)
+ public function __construct(OneLoginAuth $auth, IdentityProvidable $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 IdentityProvidable $idp
*
* @return void
*/
- public function setTenant(Tenant $tenant)
+ public function setIdp(IdentityProvidable $idp)
{
- $this->tenant = $tenant;
+ $this->idp = $idp;
}
/**
* Get a resolved tenant.
*
- * @return Tenant|null
+ * @return IdentityProvidable|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..aabe141 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
{
/**
@@ -14,7 +9,27 @@ class ServiceProvider extends \Illuminate\Support\ServiceProvider
*
* @var bool
*/
- protected $defer = false;
+ protected bool $defer = false;
+
+ /**
+ * Register any application services.
+ *
+ * @return void
+ */
+ public function register()
+ {
+ $this->mergeConfigFrom(__DIR__ . '/../config/saml2.php', 'saml2');
+
+ $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);
+ }
/**
* Bootstrap the application events.
@@ -23,9 +38,9 @@ class ServiceProvider extends \Illuminate\Support\ServiceProvider
*/
public function boot()
{
+ $this->bootPublishes();
$this->bootMiddleware();
$this->bootRoutes();
- $this->bootPublishes();
$this->bootCommands();
$this->loadMigrations();
}
@@ -37,7 +52,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';
}
}
@@ -49,10 +64,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')]);
}
/**
@@ -63,12 +75,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
]);
}
@@ -77,9 +90,9 @@ protected function bootCommands()
*
* @return void
*/
- protected function bootMiddleware()
+ 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);
}
/**
@@ -87,7 +100,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 +112,7 @@ protected function loadMigrations()
*
* @return array
*/
- public function provides()
+ public function provides(): array
{
return [];
}
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
+}
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();
}
}
-