flipboxfactory / saml-sp

SAML Service Provider (SP) Plugin for Craft CMS
https://saml-sp.flipboxfactory.com/
Other
19 stars 5 forks source link

Multi-Environment best practice #208

Open d--j opened 5 months ago

d--j commented 5 months ago

We have multiple environments (prod, staging, pentest) and would like to update the environments occasionally by dumping the production DB to staging/pentest. I have created a Service Provider for all environments (changing the entity ID manually so they point to the right URLs) in the production database and set the default entity ID to an environment variable that is set to the entity ID of the Service Provider for the environment in the environment's .env file.

This all looks good – in the Provider List the correct provider gets the "(My Provider)" annotation. But the login does not work. The error log says

2024-01-31 00:51:35 [web.ERROR] [saml-core] Decryption failed: Algorithm mismatch between input key and key used to encrypt  the symmetric key for the message. Key was: 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'; message was: 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' {"memory":4226296} 

2024-01-31 00:51:35 [web.ERROR] [Exception] Errors during validation: Destination in response "https://pentest.example.com/sso/login/pentest-environemnt-uid" does not match the expected destination "https://prod.example.com/sso/login/pentest-environemnt-uid"Recipient in SubjectConfirmationData ("https://pentest.example.com/sso/login/pentest-environemnt-uid") does not match the current destination ("https://prod.example.com/sso/login/pentest-environemnt-uid") {"trace":["#0 [internal function]: flipbox\\saml\\sp\\controllers\\LoginController->actionIndex()","#1 /var/www/clients/client1/web1/web/releases/13/vendor/yiisoft/yii2/base/InlineAction.php(57): call_user_func_array()","#2 /var/www/clients/client1/web1/web/releases/13/vendor/yiisoft/yii2/base/Controller.php(178): yii\\base\\InlineAction->runWithParams()","#3 /var/www/clients/client1/web1/web/releases/13/vendor/yiisoft/yii2/base/Module.php(552): yii\\base\\Controller->runAction()","#4 /var/www/clients/client1/web1/web/releases/13/vendor/craftcms/cms/src/web/Application.php(305): yii\\base\\Module->runAction()","#5 /var/www/clients/client1/web1/web/releases/13/vendor/yiisoft/yii2/web/Application.php(103): craft\\web\\Application->runAction()","#6 /var/www/clients/client1/web1/web/releases/13/vendor/craftcms/cms/src/web/Application.php(290): yii\\web\\Application->handleRequest()","#7 /var/www/clients/client1/web1/web/releases/13/vendor/yiisoft/yii2/base/Application.php(384): craft\\web\\Application->handleRequest()","#8 /var/www/clients/client1/web1/web/releases/13/web/index.php(12): yii\\base\\Application->run()","#9 {main}"],"memory":3903632,"exception":"[object] (Exception(code: 0): Errors during validation: Destination in response \"https://pentest.example.com/sso/login/pentest-environemnt-uid\" does not match the expected destination \"https://prod.example.com/sso/login/pentest-environemnt-uid\"Recipient in SubjectConfirmationData (\"https://pentest.example.com/sso/login/pentest-environemnt-uid\") does not match the current destination (\"https://prod.example.com/sso/login/pentest-environemnt-uid\") at /var/www/clients/client1/web1/web/releases/13/vendor/flipboxfactory/saml-sp/src/controllers/LoginController.php:115)"} 

It only starts working when I manually save my provider in the control panel of the pentest/staging environment.

When I understand https://github.com/flipboxfactory/saml-core/blob/d97a39bb23ef0b910b78c7f7463fb5e5b11164e9/src/controllers/AbstractMetadataController.php#L69-L81 correctly this is because the cached metadata column of the provider includes hardcoded location URLs that point to the wrong environment (prod.example.com instead of pentest.example.com).

Could https://github.com/flipboxfactory/saml-core/blob/d97a39bb23ef0b910b78c7f7463fb5e5b11164e9/src/helpers/UrlHelper.php#L87-L109 maybe use the provider's entity ID as the base URL? Or can the plugin create/refresh the descriptors part of the metadata on every request? Or could you add a console command that can trigger a metadata refresh of one or all service providers?

Is having all service providers in the production DB the best practtice you reccomand anyway? At least #144 is doing partial DB syncs (excluding the SAML tables) – but this will break the foreign key constrain of the saml_sp_provider_identity table to the users table (and if you are unlucky also the constrain of saml_sp_providers to the sites table).


Anyway my hotfix (since I would eventually forget manually saving the provider in the backend) is this console command:

<?php

namespace modules\app\console\controllers;

use craft\console\Controller;
use flipbox\saml\sp\Saml;
use yii\console\ExitCode;

/**
 * Daily controller
 */
class SsoController extends Controller
{
    /**
     * app/sso/refresh-self command
     *
     * Updates the metadata and stuff of the SAML provider.
     */
    public function actionRefreshSelf(): int
    {
        $plugin = Saml::getInstance();
        $providerRecord = $plugin->getProvider()->findOwn();
        $entityDescriptor = $plugin->getMetadata()->create(
            $plugin->getSettings(),
            $providerRecord
        );

        $provider = $plugin->getProvider()->create(
            $entityDescriptor,
            $providerRecord->keychain
        );

        $providerRecord->entityId = $provider->getEntityId();
        $providerRecord->metadata = $provider->metadata;
        $providerRecord->setMetadataModel($provider->getMetadataModel());
        $plugin->getProvider()->save($providerRecord);
        $this->stderr("Provider refreshed" . PHP_EOL);

        return ExitCode::OK;
    }
}

It gets called with this shell script:


#!/bin/bash

set -ex

# clean pentest DB
mysql --batch pentestdb << '__SQL'
SET FOREIGN_KEY_CHECKS = 0;
SET GROUP_CONCAT_MAX_LEN=32768;
SET @TABLES = NULL;
SELECT GROUP_CONCAT('`', TABLE_NAME, '`') INTO @TABLES
  FROM information_schema.tables
  WHERE table_schema = 'pentestdb';
SELECT IFNULL(@TABLES,'dummy') INTO @TABLES;
SET @TABLES = CONCAT('DROP TABLE IF EXISTS ', @TABLES);
PREPARE stmt FROM @TABLES;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET FOREIGN_KEY_CHECKS = 1;
__SQL

# copy over production db in now empty pentest DB
mysqldump productiondb | mysql pentestdb

# enable the correct SAML providers
mysql --batch pentestdb << '__SQL'
UPDATE saml_sp_providers SET enabled = 1 WHERE uid in ('pentest-sp-uid', 'pentest-idp-uid');
UPDATE saml_sp_providers SET enabled = 0 WHERE uid not in ('pentest-sp-uid', 'pentest-idp-uid');
__SQL

# refresh providers' metadata
cd /pentest/craft/install/current
sudo -u pentestuser ./craft saml-sp/metadata/refresh-with-url pentest-idp-uid --interactive 0
sudo -u pentestuser ./craft app/sso/refresh-self
``
timeverts commented 6 days ago

This frustrates me sometimes too. From what I can see, when a new provider is created for the first time, the base URL of the ACS and SLO is retrieved from Craft using \craft\helpers\UrlHelper::baseUrl(). This typically takes the value of the @web alias. That can be set using an environment variable in general config.

What would be ideal is if the base URL for the provider could be defined in a separate editable field. That way it could be set manually at the time of creation, without being dependent on the URL of the environment it's created in.