From ab342615f93882bc3b24325d32d66aa775031df2 Mon Sep 17 00:00:00 2001 From: Kirill Roskolii Date: Fri, 29 Nov 2024 14:51:06 +1300 Subject: [PATCH 1/4] Avoid DB for configuration --- CHANGELOG.md | 13 ++ README.md | 133 +++++++++--------- composer.json | 5 +- config/saml2.php | 55 +++++--- ...6_24_140207_create_saml2_tenants_table.php | 39 ----- ...tate_url_column_to_saml2_tenants_table.php | 32 ----- ...d_format_column_to_saml2_tenants_table.php | 32 ----- public/.gitkeep | 0 src/Auth.php | 33 ++--- src/Commands/CreateTenant.php | 121 ---------------- src/Commands/DeleteTenant.php | 90 ------------ src/Commands/ListTenants.php | 63 --------- src/Commands/RendersTenants.php | 113 --------------- src/Commands/RestoreTenant.php | 69 --------- src/Commands/TenantCredentials.php | 65 --------- src/Commands/UpdateTenant.php | 90 ------------ src/Commands/ValidatesInput.php | 64 --------- src/Facades/Auth.php | 4 +- src/Helpers/ConsoleHelper.php | 77 ---------- src/Http/Controllers/Saml2Controller.php | 12 +- src/Http/Middleware/ResolveIdp.php | 96 +++++++++++++ src/Http/Middleware/ResolveTenant.php | 115 --------------- src/Http/routes.php | 12 +- src/Models/Tenant.php | 63 --------- src/OneLoginBuilder.php | 63 +++------ src/Repositories/TenantRepository.php | 110 --------------- src/Saml2User.php | 33 +---- src/ServiceProvider.php | 43 +----- src/helpers.php | 34 ++--- tests/Helpers/ConsoleHelperTest.php | 30 ---- 30 files changed, 276 insertions(+), 1433 deletions(-) delete mode 100644 database/migrations/2019_06_24_140207_create_saml2_tenants_table.php delete mode 100644 database/migrations/2020_10_22_140856_add_relay_state_url_column_to_saml2_tenants_table.php delete mode 100644 database/migrations/2020_10_23_072902_add_name_id_format_column_to_saml2_tenants_table.php delete mode 100644 public/.gitkeep delete mode 100644 src/Commands/CreateTenant.php delete mode 100644 src/Commands/DeleteTenant.php delete mode 100644 src/Commands/ListTenants.php delete mode 100644 src/Commands/RendersTenants.php delete mode 100644 src/Commands/RestoreTenant.php delete mode 100644 src/Commands/TenantCredentials.php delete mode 100644 src/Commands/UpdateTenant.php delete mode 100644 src/Commands/ValidatesInput.php delete mode 100644 src/Helpers/ConsoleHelper.php create mode 100644 src/Http/Middleware/ResolveIdp.php delete mode 100644 src/Http/Middleware/ResolveTenant.php delete mode 100644 src/Models/Tenant.php delete mode 100644 src/Repositories/TenantRepository.php delete mode 100644 tests/Helpers/ConsoleHelperTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7701d33..bbf0a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [UNRELEASED] + +### Changed +- IdPs configuration is now stored in the main configuration file +- IdPs are identified by a key instead of UUID +- Name ID format configurable + +### Removed +- Tenant model and repository +- CLI commands + + ## [2.4.0] - 2024-04-13 ### Added diff --git a/README.md b/README.md index a3daa35..16e446b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Code Coverage][ico-code-coverage]][link-code-coverage] [![Total Downloads][ico-downloads]][link-downloads] -An integration to add SSO to your service via SAML2 protocol based on [OneLogin](https://github.com/onelogin/php-saml) toolkit. +An integration to add SSO to your service via SAML2 protocol based on [SAML PHP Toolkit] toolkit. This package turns your application into Service Provider with the support of multiple Identity Providers. @@ -48,52 +48,30 @@ For older versions, you have to add the service provider and alias to your `conf php artisan vendor:publish --provider="Slides\Saml2\ServiceProvider" ``` -##### Step 3. Run migrations - -``` -php artisan migrate -``` ### Configuring -Once you publish `saml2.php` to `app/config`, you need to configure your SP. Most of options are inherited from [OneLogin Toolkit](https://github.com/onelogin/php-saml), so you can check documentation there. - -#### Identity Providers (IdPs) - -To distinguish between identity providers there is an entity called Tenant that represent each IdP. +Once you publish `saml2.php` to `app/config`, you need to configure your service provider (SP). +Most of the options are inherited from [SAML PHP Toolkit], so you can check documentation there. +This relates to identity providers (IdPs) as well. -When request comes to an application, the middleware parses UUID and resolves the Tenant. -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` +#### Identity Providers -> To learn their options, run a command with `-h` parameter. +Identity providers (IdPs) are configured in the same `saml2.php` configuration file under `idps` key. +**N.B.** That it is plural (`idp**S**`), unlike in [SAML PHP Toolkit], because we support multiple IdPs. -Each Tenant has the following attributes: - -- **UUID** — a unique identifier that allows to resolve a tenannt and configure SP correspondingly -- **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 -- **Logout URL** — Identity Provider Logout URL -- **x509 certificate** — The certificate provided by Identity Provider in **base64** format -- **Metadata** — Custom parameters for your application needs #### Default routes The following routes are registered by default: -- `GET saml2/{uuid}/login` -- `GET saml2/{uuid}/logout` -- `GET saml2/{uuid}/metadata` -- `POST saml2/{uuid}/acs` -- `POST saml2/{uuid}/sls` +- `GET saml2/{key}/login` +- `GET saml2/{key}/logout` +- `GET saml2/{key}/metadata` +- `POST saml2/{key}/acs` +- `POST saml2/{key}/sls` You may disable them by setting `saml2.useRoutes` to `false`. @@ -163,7 +141,7 @@ protected $middlewareGroups = [ There are two ways the user can logout: - By logging out in your app. In this case you SHOULD notify the IdP first so it'll close the global session. -- By logging out of the global SSO Session. In this case the IdP will notify you on `/saml2/{uuid}/slo` endpoint (already provided). +- By logging out of the global SSO Session. In this case the IdP will notify you on `/saml2/{key}/sls` endpoint (already provided). For the first case, call `Saml2Auth::logout();` or redirect the user to the route `saml.logout` which does just that. Do not close the session immediately as you need to receive a response confirmation from the IdP (redirection). @@ -171,7 +149,7 @@ That response will be handled by the library at `/saml2/sls` and will fire an ev For the second case you will only receive the event. Both cases receive the same event. -Note that for the second case, you may have to manually save your session to make the logout stick (as the session is saved by middleware, but the OneLogin library will redirect back to your IdP before that happens): +Note that for the second case, you may have to manually save your session to make the logout stick (as the session is saved by middleware, but the [SAML PHP Toolkit] library will redirect back to your IdP before that happens): ```php Event::listen('Slides\Saml2\Events\SignedOut', function (SignedOut $event) { @@ -184,18 +162,18 @@ Event::listen('Slides\Saml2\Events\SignedOut', function (SignedOut $event) { Sometimes, you need to create links to your application with support of SSO lifecycle. It means you expect a user to be signed in once you click on that link. -The most popular example is generating links from emails, where you need to make sure when user goes to your application from email, he will be logged in. -To solve this issue, you can use helpers that allow you create SSO-friendly routes and URLs — `saml_url()` and `saml_route()`. - -To generate a link, you need to call one of functions and pass UUID of the tenant as a second parameter, unless your session knows that user was resolved by SSO. +The most popular example is generating links from emails, where you need to make sure when user goes to your application from email, they will be logged in. +To solve this issue, you can use helpers that allow you to create SSO-friendly routes and URLs — `saml_url()` and `saml_route()`. -> To retrieve UUID based on user, you should implement logic that links your internal user to a tenant. +To generate a link, you need to call one of functions and pass the IdP key as a second parameter, unless your session knows that user was resolved by SSO. Then, it generates a link like this: ``` -https://yourdomain/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/login?returnTo=https://yourdomain.com/your/actual/link +https://yourdomain/saml2/default/login?returnTo=https://yourdomain.com/your/actual/link ``` +where `default` is the IdP key from the `saml2.php` configuration file. + Basically, when user clicks on a link, it initiates SSO login process and redirects it back to your needed URL. ## Examples @@ -215,40 +193,55 @@ You need to retrieve the following parameters: - Logout URL - Certificate (Base64) -##### Step 2. Create a Tenant - -Based on information you received below, create a Tenant, like this: - -``` -php artisan saml2:create-tenant \ - --key=azure_testing \ - --entityId=https://sts.windows.net/fb536a7a-7251-4895-a09a-abd8e614c70b/ \ - --loginUrl=https://login.microsoftonline.com/fb536a7a-7251-4895-a09a-abd8e614c70b/saml2 \ - --logoutUrl=https://login.microsoftonline.com/common/wsfederation?wa=wsignout1.0 \ - --x509cert="MIIC0jCCAbqgAw...CapVR4ncDVjvbq+/S" \ - --metadata="customer:11235,anotherfield:value" // you might add some customer parameters here to simplify logging in your customer afterwards +##### Step 2. Configure Identity Provider + +Based on information you received in step one, configure your IdP like this: + +```shell +cat config/saml2.php +... + + 'idps' => [ + // The key will be used as an IdP identifier as well as in routes. + 'azure_testing' => [ + 'relay_state_url' => env('SAML2_RELAY_STATE_URL', ''), + // Place any other IdP related configuration from the 'idp' section + // in the https://github.com/SAML-Toolkits/php-saml#settings below. + // Identifier of the IdP entity (must be a URI). + 'entityId' => 'https://sts.windows.net/fb536a7a-7251-4895-a09a-abd8e614c70b/', + // SSO endpoint info of the IdP. (Authentication Request protocol) + 'singleSignOnService' => [ + // URL Target of the IdP where the Authentication Request Message will be sent. + 'url' => 'https://login.microsoftonline.com/fb536a7a-7251-4895-a09a-abd8e614c70b/saml2', + ], + // SLO endpoint info of the IdP. + 'singleLogoutService' => [ + // URL Location of the IdP where SLO Request will be sent. + 'url' => 'https://login.microsoftonline.com/common/wsfederation?wa=wsignout1.0', + // URL location of the IdP where SLO Response will be sent (ResponseLocation) + // if not set, url for the SLO Request will be used. + 'responseUrl' => '', + ], + 'x509cert' => env('SAML2_IDP_X509', ''), + ], + ], +]; +printenv SAML2_IDP_X509 +MIIC0jCCAbqgAw... ``` -Once you successfully created the tenant, you will receive the following output: -``` -The tenant #1 (63fffdd1-f416-4bed-b3db-967b6a56896b) was successfully created. +##### Step 3. Register your service provider in Identity Provider -Credentials for the tenant --------------------------- +Assign parameters to your IdP on the application Single-Sign-On settings page. - Identifier (Entity ID): https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/metadata - Reply URL (Assertion Consumer Service URL): https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/acs - Sign on URL: https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/login - Logout URL: https://yourdomain.com/saml/63fffdd1-f416-4bed-b3db-967b6a56896b/logout - Relay State: / (optional) -``` - -##### Step 3. Configure Identity Provider +![Azure AD](https://i.imgur.com/3hkjFLZ.png) -Using the output below, assign parameters to your IdP on application Single-Sign-On settings page. +- Identifier (Entity ID) - `https://yourdomain.com/saml2/azure_testing/metadata` or ID you assigned to your SP in the `saml2.php` +- Reply URL (Assertion Consumer Service URL) - `https://yourdomain.com/saml2/azure_testing/acs` +- Sign on URL - `https://yourdomain.com/saml2/azure_testing/login` +- Logout URL - `https://yourdomain.com/saml2/azure_testing/logout` -![Azure AD](https://i.imgur.com/3hkjFLZ.png) ##### Step 4. Make sure your application accessible by Azure AD @@ -306,3 +299,5 @@ The MIT License (MIT). Please see [License File](LICENSE.md) for more informatio [link-original-author]: https://github.com/aacotroneo [link-author]: https://github.com/brezzhnev [link-contributors]: ../../contributors + +[SAML PHP Toolkit]: https://github.com/SAML-Toolkits/php-saml diff --git a/composer.json b/composer.json index 711898b..96a724e 100644 --- a/composer.json +++ b/composer.json @@ -14,11 +14,8 @@ "require": { "php": ">=7.1", "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" + "onelogin/php-saml": "^3.0|^4.0" }, "require-dev": { "mockery/mockery": "^0.9.9", diff --git a/config/saml2.php b/config/saml2.php index 541f9fb..fe3f239 100644 --- a/config/saml2.php +++ b/config/saml2.php @@ -2,17 +2,6 @@ return [ - /* - |-------------------------------------------------------------------------- - | Tenant Model - |-------------------------------------------------------------------------- - | - | This will allow you to override the tenant model with your own. - | - */ - - 'tenantModel' => \Slides\Saml2\Models\Tenant::class, - /* |-------------------------------------------------------------------------- | Use built-in routes @@ -22,11 +11,11 @@ | | Method | URI | Name | -------|---------------------------------|------------------ - | POST | {routesPrefix}/{uuid}/acs | saml.acs - | GET | {routesPrefix}/{uuid}/login | saml.login - | GET | {routesPrefix}/{uuid}/logout | saml.logout - | GET | {routesPrefix}/{uuid}/metadata | saml.metadata - | GET | {routesPrefix}/{uuid}/sls | saml.sls + | POST | {routesPrefix}/{key}/acs | saml.acs + | GET | {routesPrefix}/{key}/login | saml.login + | GET | {routesPrefix}/{key}/logout | saml.logout + | GET | {routesPrefix}/{key}/metadata | saml.metadata + | GET | {routesPrefix}/{key}/sls | saml.sls | */ @@ -159,6 +148,8 @@ */ 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + // Set to true to use the format specified in the IdP. + 'NameIDFormatFromIdp' => false, /* |-------------------------------------------------------------------------- @@ -387,11 +378,35 @@ /* |-------------------------------------------------------------------------- - | Load default migrations + | Identity providers (IdPs) configuration. |-------------------------------------------------------------------------- | - | This will allow you to disable or enable the default migrations of the package. - | + | N.B. Unlike underlying SAML PHP Toolkit library which supports only + | one IdP at a time, we can handle multiple IdPs and thus the key below + | is plural. */ - 'load_migrations' => true, + 'idps' => [ + // The key will be used as an IdP identifier as well as in routes. + 'default' => [ + 'relay_state_url' => env('SAML2_RELAY_STATE_URL', ''), + // Place any other IdP related configuration from the 'idp' section + // in the https://github.com/SAML-Toolkits/php-saml#settings below. + // Identifier of the IdP entity (must be a URI). + 'entityId' => '', + // SSO endpoint info of the IdP. (Authentication Request protocol). + 'singleSignOnService' => [ + // URL Target of the IdP where the Authentication Request Message + // will be sent. + 'url' => '', + ], + // SLO endpoint info of the IdP. + 'singleLogoutService' => [ + // URL Location of the IdP where SLO Request will be sent. + 'url' => '', + // URL location of the IdP where SLO Response will be sent (ResponseLocation) + // if not set, url for the SLO Request will be used. + 'responseUrl' => '', + ], + ], + ], ]; diff --git a/database/migrations/2019_06_24_140207_create_saml2_tenants_table.php b/database/migrations/2019_06_24_140207_create_saml2_tenants_table.php deleted file mode 100644 index 746d5d0..0000000 --- a/database/migrations/2019_06_24_140207_create_saml2_tenants_table.php +++ /dev/null @@ -1,39 +0,0 @@ -increments('id'); - $table->uuid('uuid'); - $table->string('key')->nullable(); - $table->string('idp_entity_id'); - $table->string('idp_login_url'); - $table->string('idp_logout_url'); - $table->text('idp_x509_cert'); - $table->json('metadata'); - $table->timestamps(); - $table->softDeletes(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('saml2_tenants'); - } -} diff --git a/database/migrations/2020_10_22_140856_add_relay_state_url_column_to_saml2_tenants_table.php b/database/migrations/2020_10_22_140856_add_relay_state_url_column_to_saml2_tenants_table.php deleted file mode 100644 index a7ebf3f..0000000 --- a/database/migrations/2020_10_22_140856_add_relay_state_url_column_to_saml2_tenants_table.php +++ /dev/null @@ -1,32 +0,0 @@ -string('relay_state_url')->nullable(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('saml2_tenants', function (Blueprint $table) { - $table->dropColumn('relay_state_url'); - }); - } -} diff --git a/database/migrations/2020_10_23_072902_add_name_id_format_column_to_saml2_tenants_table.php b/database/migrations/2020_10_23_072902_add_name_id_format_column_to_saml2_tenants_table.php deleted file mode 100644 index 3c95975..0000000 --- a/database/migrations/2020_10_23_072902_add_name_id_format_column_to_saml2_tenants_table.php +++ /dev/null @@ -1,32 +0,0 @@ -string('name_id_format')->default('persistent'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('saml2_tenants', function (Blueprint $table) { - $table->dropColumn('name_id_format'); - }); - } -} diff --git a/public/.gitkeep b/public/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Auth.php b/src/Auth.php index ce94894..65e45df 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -5,7 +5,6 @@ use OneLogin\Saml2\Auth as OneLoginAuth; use OneLogin\Saml2\Error as OneLoginError; use Slides\Saml2\Events\SignedOut; -use Slides\Saml2\Models\Tenant; /** * Class Auth @@ -22,22 +21,22 @@ class Auth protected $base; /** - * The resolved tenant. + * The resolved IdP configuration. * - * @var Tenant + * @var array */ - protected $tenant; + protected $idp; /** * Auth constructor. * * @param OneLoginAuth $auth - * @param Tenant $tenant + * @param array $idp */ - public function __construct(OneLoginAuth $auth, Tenant $tenant) + public function __construct(OneLoginAuth $auth, array $idp) { $this->base = $auth; - $this->tenant = $tenant; + $this->idp = $idp; } /** @@ -57,7 +56,7 @@ public function isAuthenticated() */ public function getSaml2User() { - return new Saml2User($this->base, $this->tenant); + return new Saml2User($this->base); } /** @@ -223,24 +222,22 @@ public function getBase() } /** - * Set a tenant + * Get IDP key. * - * @param Tenant $tenant - * - * @return void + * @return string */ - public function setTenant(Tenant $tenant) + public function getIdpKey() { - $this->tenant = $tenant; + return $this->idp['key']; } /** - * Get a resolved tenant. + * Get IDP relay state URL, if configured. * - * @return Tenant|null + * @return string|null */ - public function getTenant() + public function getIdpRelayStateUrl() { - return $this->tenant; + return $this->idp['relay_state_url'] ?? null; } } 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/DeleteTenant.php b/src/Commands/DeleteTenant.php deleted file mode 100644 index 3a12fb5..0000000 --- a/src/Commands/DeleteTenant.php +++ /dev/null @@ -1,90 +0,0 @@ -tenants = $tenants; - - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle() - { - $tenants = $this->tenants->findByAnyIdentifier($this->argument('tenant'), false); - - if($tenants->isEmpty()) { - $this->error('Cannot find a matching tenant by "' . $this->argument('tenant') . '" identifier'); - return; - } - - $this->renderTenants($tenants, 'Found tenant(s)'); - - if($tenants->count() > 1) { - $deletingId = $this->ask('We have found several tenants, which one would you like to delete? (enter its ID)'); - } - else { - $deletingId = $tenants->first()->id; - } - - $tenant = $tenants->firstWhere('id', $deletingId); - - if($this->option('safe')) { - $tenant->delete(); - - $this->info('The tenant #' . $deletingId . ' safely deleted. To restore it, run:'); - $this->output->block('php artisan saml2:restore-tenant ' . $deletingId); - - return; - } - - if(!$this->confirm('Would you like to forcely delete the tenant #' . $deletingId . '? It cannot be reverted.')) { - return; - } - - $tenant->forceDelete(); - - $this->info('The tenant #' . $deletingId . ' safely deleted.'); - } -} \ No newline at end of file diff --git a/src/Commands/ListTenants.php b/src/Commands/ListTenants.php deleted file mode 100644 index 28b07b5..0000000 --- a/src/Commands/ListTenants.php +++ /dev/null @@ -1,63 +0,0 @@ -tenants = $tenants; - - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle() - { - $tenants = $this->tenants->all(); - - 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 deleted file mode 100644 index 5477344..0000000 --- a/src/Commands/RendersTenants.php +++ /dev/null @@ -1,113 +0,0 @@ -getTenantColumns($tenant) as $column => $value) { - $columns[] = [$column, $value ?: '(empty)']; - } - - if($tenants->last()->id !== $tenant->id) { - $columns[] = new \Symfony\Component\Console\Helper\TableSeparator(); - } - } - - if($title) { - $this->getOutput()->title($title); - } - - $this->table($headers, $columns); - } - - /** - * Get a columns of the Tenant. - * - * @param \Slides\Saml2\Models\Tenant $tenant - * - * @return array - */ - protected function getTenantColumns(Tenant $tenant) - { - return [ - 'ID' => $tenant->id, - 'UUID' => $tenant->uuid, - 'Key' => $tenant->key, - 'Entity ID' => $tenant->idp_entity_id, - 'Login URL' => $tenant->idp_login_url, - 'Logout URL' => $tenant->idp_logout_url, - 'Relay State URL' => $tenant->relay_state_url, - 'Name ID format' => $tenant->name_id_format, - 'x509 cert' => Str::limit($tenant->idp_x509_cert, 50), - 'Metadata' => $this->renderArray($tenant->metadata ?: []), - 'Created' => $tenant->created_at->toDateTimeString(), - 'Updated' => $tenant->updated_at->toDateTimeString(), - 'Deleted' => $tenant->deleted_at ? $tenant->deleted_at->toDateTimeString() : null - ]; - } - - /** - * Render a tenant credentials. - * - * @param \Slides\Saml2\Models\Tenant $tenant - * - * @return void - */ - protected function renderTenantCredentials(Tenant $tenant) - { - $this->output->section('Credentials for the tenant'); - - $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)' - ]); - } - - /** - * Print an array to a string. - * - * @param array $array - * - * @return string - */ - protected function renderArray(array $array) - { - $lines = []; - - foreach ($array as $key => $value) { - $lines[] = "$key: $value"; - } - - return implode(PHP_EOL, $lines); - } -} \ No newline at end of file diff --git a/src/Commands/RestoreTenant.php b/src/Commands/RestoreTenant.php deleted file mode 100644 index 8bf77ca..0000000 --- a/src/Commands/RestoreTenant.php +++ /dev/null @@ -1,69 +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; - } - - $this->renderTenants($tenant, 'Found a deleted tenant'); - - if(!$this->confirm('Would you like to restore it?')) { - return; - } - - $tenant->restore(); - - $this->info('The tenant #' . $tenant->id . ' successfully restored.'); - } -} \ No newline at end of file diff --git a/src/Commands/TenantCredentials.php b/src/Commands/TenantCredentials.php deleted file mode 100644 index 5e5551a..0000000 --- a/src/Commands/TenantCredentials.php +++ /dev/null @@ -1,65 +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; - } - - $this->renderTenants($tenant, 'The tenant model'); - $this->renderTenantCredentials($tenant); - - $this->output->newLine(); - } -} \ No newline at end of file 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/Facades/Auth.php b/src/Facades/Auth.php index 969f6bf..e15bee2 100644 --- a/src/Facades/Auth.php +++ b/src/Facades/Auth.php @@ -7,8 +7,6 @@ /** * Class Saml2Auth * - * @method static \Slides\Saml2\Models\Tenant|null getTenant() - * * @package Slides\Saml2\Facades */ class Auth extends Facade @@ -22,4 +20,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 deleted file mode 100644 index 2fe6576..0000000 --- a/src/Helpers/ConsoleHelper.php +++ /dev/null @@ -1,77 +0,0 @@ - $item) { - $item = explode($valueDelimiter, $item); - - $key = Arr::get($item, 0); - $value = Arr::get($item, 1); - - if(is_null($value)) { - $value = $key; - $key = $index; - } - - $values[trim($key)] = trim($value); - } - - return $values; - } - - /** - * Converts an array to string - * - * ['one', 'two', 'three'] to 'one, two, three', - * ['one' => 1, 'two' => 2, 'three' => 3] to 'one:1,two:2,three:3' - * - * @param array $array - * - * @return string - */ - public static function arrayToString(array $array): string - { - $values = []; - - foreach ($array as $key => $value) { - if(is_array($value)) { - continue; - } - - $values[] = is_string($key) - ? $key . ':' . $value - : $value; - } - - 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..555fdbc 100644 --- a/src/Http/Controllers/Saml2Controller.php +++ b/src/Http/Controllers/Saml2Controller.php @@ -49,9 +49,9 @@ public function acs(Auth $auth) if (!empty($errors)) { $error = $auth->getLastErrorReason(); - $uuid = $auth->getTenant()->uuid; + $key = $auth->getIdpKey(); - logger()->error('saml2.error_detail', compact('uuid', 'error')); + logger()->error('saml2.error_detail', compact('key', 'error')); session()->flash('saml2.error_detail', [$error]); logger()->error('saml2.error', $errors); @@ -70,7 +70,7 @@ public function acs(Auth $auth) return redirect($redirectUrl); } - return redirect($auth->getTenant()->relay_state_url ?: config('saml2.loginRoute')); + return redirect($auth->getIdpRelayStateUrl() ?: config('saml2.loginRoute')); } /** @@ -93,9 +93,9 @@ public function sls(Auth $auth) if (!empty($errors)) { $error = $auth->getLastErrorReason(); - $uuid = $auth->getTenant()->uuid; + $key = $auth->getIdpKey(); - logger()->error('saml2.error_detail', compact('uuid', 'error')); + logger()->error('saml2.error_detail', compact('key', 'error')); session()->flash('saml2.error_detail', [$error]); logger()->error('saml2.error', $errors); @@ -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->getIdpRelayStateUrl() ?: config('saml2.loginRoute'); $auth->login($request->query('returnTo', $redirectUrl)); } diff --git a/src/Http/Middleware/ResolveIdp.php b/src/Http/Middleware/ResolveIdp.php new file mode 100644 index 0000000..af7ed2f --- /dev/null +++ b/src/Http/Middleware/ResolveIdp.php @@ -0,0 +1,96 @@ +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(!$idp = $this->resolveIdp($request)) { + throw new NotFoundHttpException(); + } + + if (config('saml2.debug')) { + Log::debug('[Saml2] IdP resolved', [ + 'key' => $idp['key'], + ]); + } + + session()->flash('saml2.idp.key', $idp['key']); + + $this->builder + ->withIdp($idp) + ->bootstrap(); + + return $next($request); + } + + /** + * Resolve an IdP by a request. + * + * @param \Illuminate\Http\Request $request + * + * @return array|null + */ + protected function resolveIdp($request) + { + if(!$key = $request->route('key')) { + if (config('saml2.debug')) { + Log::debug('[Saml2] IdP key is not present in the URL so cannot be resolved', [ + 'url' => $request->fullUrl() + ]); + } + + return null; + } + + if(!$idp = config("saml2.idps.$key")) { + if (config('saml2.debug')) { + Log::debug('[Saml2] Unknown IdP requested', [ + 'key' => $key + ]); + } + + return null; + } + + $idp['key'] = $key; + + return $idp; + } +} 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..c6dbf8e 100644 --- a/src/Http/routes.php +++ b/src/Http/routes.php @@ -4,29 +4,29 @@ Route::group([ 'prefix' => config('saml2.routesPrefix'), - 'middleware' => array_merge(['saml2.resolveTenant'], config('saml2.routesMiddleware')), + 'middleware' => array_merge(['saml2.resolveIdp'], config('saml2.routesMiddleware')), ], function () { - Route::get('/{uuid}/logout', array( + Route::get('/{key}/logout', array( 'as' => 'saml.logout', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@logout', )); - Route::get('/{uuid}/login', array( + Route::get('/{key}/login', array( 'as' => 'saml.login', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@login', )); - Route::get('/{uuid}/metadata', array( + Route::get('/{key}/metadata', array( 'as' => 'saml.metadata', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@metadata', )); - Route::post('/{uuid}/acs', array( + Route::post('/{key}/acs', array( 'as' => 'saml.acs', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@acs', )); - Route::get('/{uuid}/sls', array( + Route::get('/{key}/sls', array( 'as' => 'saml.sls', 'uses' => 'Slides\Saml2\Http\Controllers\Saml2Controller@sls', )); 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..057d124 100644 --- a/src/OneLoginBuilder.php +++ b/src/OneLoginBuilder.php @@ -6,7 +6,6 @@ use OneLogin\Saml2\Utils as OneLoginUtils; use Illuminate\Support\Facades\URL; use Illuminate\Contracts\Container\Container; -use Slides\Saml2\Models\Tenant; use Illuminate\Support\Arr; /** @@ -22,11 +21,13 @@ class OneLoginBuilder protected $app; /** - * The resolved tenant. + * IdP configuration. * - * @var Tenant + * @var array + * + * @see https://github.com/SAML-Toolkits/php-saml#settings */ - protected $tenant; + protected $idp; /** * OneLoginBuilder constructor. @@ -39,15 +40,15 @@ public function __construct(Container $app) } /** - * Set a tenant. + * Set IdP configuration. * - * @param Tenant $tenant + * @param array $idp * * @return $this */ - public function withTenant(Tenant $tenant) + public function withIdp(array $idp) { - $this->tenant = $tenant; + $this->idp = $idp; return $this; } @@ -55,8 +56,6 @@ public function withTenant(Tenant $tenant) /** * Bootstrap the OneLogin toolkit. * - * @param Tenant $tenant - * * @return void */ public function bootstrap() @@ -67,24 +66,22 @@ public function bootstrap() $this->app->singleton('OneLogin_Saml2_Auth', function ($app) { $config = $app['config']['saml2']; - + // Supply only what is needed. + unset($config['idps']); $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['idp'] = $this->idp; - $oneLoginConfig['sp']['NameIDFormat'] = $this->resolveNameIdFormatPrefix($this->tenant->name_id_format); + if ($config['sp']['NameIDFormatFromIdp']) { + $oneLoginConfig['sp']['NameIDFormat'] = $oneLoginConfig['idp']['NameIDFormat']; + } return new OneLoginAuth($oneLoginConfig); }); $this->app->singleton('Slides\Saml2\Auth', function ($app) { - return new \Slides\Saml2\Auth($app['OneLogin_Saml2_Auth'], $this->tenant); + return new \Slides\Saml2\Auth($app['OneLogin_Saml2_Auth'], $this->idp); }); } @@ -112,29 +109,9 @@ protected function setConfigDefaultValues(array &$config) protected function configDefaultValues() { 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', ['key' => $this->idp['key']]), + 'sp.assertionConsumerService.url' => URL::route('saml.acs', ['key' => $this->idp['key']]), + 'sp.singleLogoutService.url' => URL::route('saml.sls', ['key' => $this->idp['key']]) ]; } - - /** - * 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 deleted file mode 100644 index 4734e66..0000000 --- a/src/Repositories/TenantRepository.php +++ /dev/null @@ -1,110 +0,0 @@ -withTrashed(); - } - - return $query; - } - - /** - * Find all tenants. - * - * @param bool $withTrashed Whether need to include safely deleted records. - * - * @return Tenant[]|\Illuminate\Database\Eloquent\Collection - */ - public function all(bool $withTrashed = true) - { - return $this->query($withTrashed)->get(); - } - - /** - * Find a tenant by any identifier. - * - * @param int|string $key ID, key or UUID - * @param bool $withTrashed Whether need to include safely deleted records. - * - * @return Tenant[]|\Illuminate\Database\Eloquent\Collection - */ - public function findByAnyIdentifier($key, bool $withTrashed = true) - { - $query = $this->query($withTrashed); - - if (is_int($key)) { - return $query->where('id', $key)->get(); - } - - return $query->where('key', $key) - ->orWhere('uuid', $key) - ->get(); - } - - /** - * Find a tenant by the key. - * - * @param string $key - * @param bool $withTrashed - * - * @return Tenant|\Illuminate\Database\Eloquent\Model|null - */ - public function findByKey(string $key, bool $withTrashed = true) - { - return $this->query($withTrashed) - ->where('key', $key) - ->first(); - } - - /** - * Find a tenant by ID. - * - * @param int $id - * @param bool $withTrashed - * - * @return Tenant|\Illuminate\Database\Eloquent\Model|null - */ - public function findById(int $id, bool $withTrashed = true) - { - return $this->query($withTrashed) - ->where('id', $id) - ->first(); - } - - /** - * Find a tenant by UUID. - * - * @param int $uuid - * @param bool $withTrashed - * - * @return Tenant|\Illuminate\Database\Eloquent\Model|null - */ - public function findByUUID(string $uuid, bool $withTrashed = true) - { - return $this->query($withTrashed) - ->where('uuid', $uuid) - ->first(); - } -} \ No newline at end of file diff --git a/src/Saml2User.php b/src/Saml2User.php index ba37f1f..3bc2496 100644 --- a/src/Saml2User.php +++ b/src/Saml2User.php @@ -3,7 +3,6 @@ namespace Slides\Saml2; use OneLogin\Saml2\Auth as OneLoginAuth; -use Slides\Saml2\Models\Tenant; /** * Class Saml2User @@ -19,23 +18,14 @@ class Saml2User */ protected $auth; - /** - * The tenant user belongs to. - * - * @var Tenant - */ - protected $tenant; - /** * Saml2User constructor. * * @param OneLoginAuth $auth - * @param Tenant $tenant */ - public function __construct(OneLoginAuth $auth, Tenant $tenant) + public function __construct(OneLoginAuth $auth) { $this->auth = $auth; - $this->tenant = $tenant; } /** @@ -163,25 +153,4 @@ public function getNameId() return $this->auth->getNameId(); } - /** - * Set a tenant - * - * @param Tenant $tenant - * - * @return void - */ - public function setTenant(Tenant $tenant) - { - $this->tenant = $tenant; - } - - /** - * Get a resolved tenant. - * - * @return Tenant|null - */ - public function getTenant() - { - return $this->tenant; - } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 72a414f..069446a 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -26,8 +26,6 @@ public function boot() $this->bootMiddleware(); $this->bootRoutes(); $this->bootPublishes(); - $this->bootCommands(); - $this->loadMigrations(); } /** @@ -55,23 +53,6 @@ protected function bootPublishes() $this->mergeConfigFrom($source, 'saml2'); } - /** - * Bootstrap the console commands. - * - * @return void - */ - 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 - ]); - } - /** * Bootstrap the console commands. * @@ -79,28 +60,6 @@ protected function bootCommands() */ protected function bootMiddleware() { - $this->app['router']->aliasMiddleware('saml2.resolveTenant', \Slides\Saml2\Http\Middleware\ResolveTenant::class); - } - - /** - * Load the package migrations. - * - * @return void - */ - protected function loadMigrations() - { - if (config('saml2.load_migrations', true)) { - $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); - } - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return []; + $this->app['router']->aliasMiddleware('saml2.resolveIdp', \Slides\Saml2\Http\Middleware\ResolveIdp::class); } } diff --git a/src/helpers.php b/src/helpers.php index 28ec6d7..e8d6765 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -3,63 +3,63 @@ if (!function_exists('saml_url')) { /** - * Generate a URL to saml/{uuid}/login which redirects to a target URL. + * Generate a URL to saml/{key}/login which redirects to a target URL. * * @param string $path - * @param string|null $uuid A tenant UUID. + * @param string|null $key An IdP key. * @param array $parameters * @param bool $secure * * @return string */ - function saml_url(string $path, string $uuid = null, $parameters = [], bool $secure = null) + function saml_url(string $path, string $key = null, array $parameters = [], bool $secure = null) { $target = \Illuminate\Support\Facades\URL::to($path, $parameters, $secure); - if(!$uuid) { - if(!$uuid = saml_tenant_uuid()) { + if(!$key) { + if(!$key = saml_idp_key()) { return $target; } } - return \Illuminate\Support\Facades\URL::route('saml.login', ['uuid' => $uuid, 'returnTo' => $target]); + return \Illuminate\Support\Facades\URL::route('saml.login', ['key' => $key, 'returnTo' => $target]); } } if (!function_exists('saml_route')) { /** - * Generate a URL to saml/{uuid}/login which redirects to a target route. + * Generate a URL to saml/{key}/login which redirects to a target route. * * @param string $name - * @param string|null $uuid A tenant UUID. + * @param string|null $key An IdP key. * @param array $parameters * * @return string */ - function saml_route(string $name, string $uuid = null, $parameters = []) + function saml_route(string $name, string $key = null, array $parameters = []) { $target = \Illuminate\Support\Facades\URL::route($name, $parameters, true); - if(!$uuid) { - if(!$uuid = saml_tenant_uuid()) { + if(!$key) { + if(!$key = saml_idp_key()) { return $target; } } - return \Illuminate\Support\Facades\URL::route('saml.login', ['uuid' => $uuid, 'returnTo' => $target]); + return \Illuminate\Support\Facades\URL::route('saml.login', ['key' => $key, 'returnTo' => $target]); } } -if (!function_exists('saml_tenant_uuid')) +if (!function_exists('saml_idp_key')) { /** - * Get a resolved Tenant UUID based on current URL. + * Get a resolved IdP key based on the current URL. * * @return string|null */ - function saml_tenant_uuid() + function saml_idp_key() { - return session()->get('saml2.tenant.uuid'); + return session()->get('saml2.idp.key'); } -} \ No newline at end of file +} diff --git a/tests/Helpers/ConsoleHelperTest.php b/tests/Helpers/ConsoleHelperTest.php deleted file mode 100644 index 775d589..0000000 --- a/tests/Helpers/ConsoleHelperTest.php +++ /dev/null @@ -1,30 +0,0 @@ - 'value1', 'item2' => 'value2'], - ConsoleHelper::stringToArray('item1:value1,item2:value2') - ); - - static::assertEquals( - ['item1' => 'value1', 'item2' => 'value 2'], - ConsoleHelper::stringToArray(' item1 :value1 , item2 :value 2') - ); - - static::assertEquals( - ['value1', 'value2', 'value3'], - ConsoleHelper::stringToArray('value1,value2,value3') - ); - } -} \ No newline at end of file From 9631ad49e056ff51b9e9ab5c7123d4a49e4ec4df Mon Sep 17 00:00:00 2001 From: Kirill Roskolii Date: Fri, 29 Nov 2024 15:55:20 +1300 Subject: [PATCH 2/4] Declare correct dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 96a724e..5db8c13 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "require": { "php": ">=7.1", "ext-openssl": "*", - "illuminate/support": "~5.4|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "laravel/framework": "~5.4|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "onelogin/php-saml": "^3.0|^4.0" }, "require-dev": { From 455b62bfdcf225036b45e3b9abd5713c9d6d8d14 Mon Sep 17 00:00:00 2001 From: Kirill Roskolii Date: Fri, 29 Nov 2024 16:10:36 +1300 Subject: [PATCH 3/4] Use full namespace specification --- src/Http/Controllers/Saml2Controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/Saml2Controller.php b/src/Http/Controllers/Saml2Controller.php index 555fdbc..113d968 100644 --- a/src/Http/Controllers/Saml2Controller.php +++ b/src/Http/Controllers/Saml2Controller.php @@ -110,7 +110,7 @@ public function sls(Auth $auth) /** * Initiate a login request. * - * @param Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @param Auth $auth * * @return void @@ -127,7 +127,7 @@ public function login(Request $request, Auth $auth) /** * Initiate a logout request. * - * @param Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @param Auth $auth * * @return void From c4e339f53f8cee2beeb5c10923143377b42d5ab6 Mon Sep 17 00:00:00 2001 From: Kirill Roskolii Date: Mon, 2 Dec 2024 07:24:41 +1300 Subject: [PATCH 4/4] Update tests --- tests/Saml2AuthTest.php | 44 ++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/tests/Saml2AuthTest.php b/tests/Saml2AuthTest.php index f76bc1a..1f59ce7 100644 --- a/tests/Saml2AuthTest.php +++ b/tests/Saml2AuthTest.php @@ -15,7 +15,7 @@ public function tearDown(): void public function testIsAuthenticated() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $oneLoginAuth->shouldReceive('isAuthenticated')->andReturn(true); @@ -25,7 +25,7 @@ public function testIsAuthenticated() public function testLogin() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $oneLoginAuth->shouldReceive('login')->once(); @@ -44,7 +44,7 @@ public function testLogout() $expectedNameIdNameQualifier = 'name_id_name_qualifier'; $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $oneLoginAuth->shouldReceive('logout') ->with($expectedReturnTo, [], $expectedNameId, $expectedSessionIndex, $expectedStay, $expectedNameIdFormat, $expectedNameIdNameQualifier) @@ -58,7 +58,7 @@ public function testLogout() public function testAcsError() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $oneLoginAuth->shouldReceive('processResponse')->once(); $oneLoginAuth->shouldReceive('getErrors')->once()->andReturn(array('errors')); @@ -71,7 +71,7 @@ public function testAcsError() public function testAcsNotAutenticated() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $oneLoginAuth->shouldReceive('processResponse')->once(); $oneLoginAuth->shouldReceive('getErrors')->once()->andReturn(null); $oneLoginAuth->shouldReceive('isAuthenticated')->once()->andReturn(false); @@ -84,7 +84,7 @@ public function testAcsNotAutenticated() public function testAcsOK() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $oneLoginAuth->shouldReceive('processResponse')->once(); $oneLoginAuth->shouldReceive('getErrors')->once()->andReturn(null); $oneLoginAuth->shouldReceive('isAuthenticated')->once()->andReturn(true); @@ -97,7 +97,7 @@ public function testAcsOK() public function testSlsError() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $oneLoginAuth->shouldReceive('processSLO')->once(); $oneLoginAuth->shouldReceive('getErrors')->once()->andReturn('errors'); @@ -109,7 +109,7 @@ public function testSlsError() public function testSlsOK() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $oneLoginAuth->shouldReceive('processSLO')->once(); $oneLoginAuth->shouldReceive('getErrors')->once()->andReturn(null); @@ -121,7 +121,7 @@ public function testSlsOK() public function testCanGetLastError() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $oneLoginAuth->shouldReceive('getLastErrorReason')->andReturn('lastError'); @@ -130,7 +130,7 @@ public function testCanGetLastError() public function testGetUserAttribute() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $user = $saml2Auth->getSaml2User(); @@ -143,7 +143,7 @@ public function testGetUserAttribute() { public function testParseSingleUserAttribute() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $user = $saml2Auth->getSaml2User(); @@ -158,7 +158,7 @@ public function testParseSingleUserAttribute() { public function testParseMultipleUserAttributes() { $oneLoginAuth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - $saml2Auth = new Auth($oneLoginAuth, $this->mockTenant()); + $saml2Auth = new Auth($oneLoginAuth, $this->mockIdp()); $user = $saml2Auth->getSaml2User(); @@ -184,17 +184,25 @@ protected function mockAuth() { $auth = \Mockery::mock(\OneLogin\Saml2\Auth::class); - return new Auth($auth, $this->mockTenant()); + return new Auth($auth, $this->mockIdp()); } /** - * Create a fake tenant. + * Create a fake IdP configuration. * - * @return \Slides\Saml2\Models\Tenant + * @return array */ - protected function mockTenant() + protected function mockIdp() { - return new \Slides\Saml2\Models\Tenant(); + return [ + 'key' => 'idp1', + 'entityId' => 'idp.enterprise.com', + 'singleSignOnService' => [ + 'url' => '', + ], + 'singleLogoutService' => [ + 'url' => '', + ], + ]; } } -