Skip to content

Ajout d'une authentification multi-tenant dans Cywise

Nous voulons permettre à nos clients de se connecter à Cywise en utilisant leur IdP. Nous voulons utiliser le protocole SAML2.

Nous avons plusieurs clients dans l'application Cywise. Chaque client doit pouvoir avoir son IdP donc notre fonctionnalité doit être multi-tenant.

Réflexions

Utiliser une intégration Laravel

Laravel Sanctum permet l'authentification des utilisateurs.

Il est capable de générer des tokens API et aussi d'authentifier les utilisateurs pour une application SPA mais il ne permet pas d'utiliser un IdP SAML (ni un IdP OAuth).

Donc Laravel Sanctum ne nous aidera pas.

Laravel Passport permet d'authentifier les utilisateurs auprès d'un IdP OAuth2.

Mais pas d'intégration SAML possible.

Donc Laravel Passport ne nous aidera pas.

Laravel Socialite

La doc explique que Socialite permet aux utilisateurs de s'authentifier auprès d'un IdP OAuth et ajoute :

Socialite currently supports authentication via Facebook, X, LinkedIn, Google, GitHub, GitLab, Bitbucket, and Slack.

Mais j'ai trouvé des add-ons pour Socialite pour beaucoup d'autres IdP. Dont un pour un IdP SAML : https://socialiteproviders.com/Saml2/

Mais la configuration de l'IdP SAML est mise dans config/services.php et je ne vois pas comment je pourrais en avoir plusieurs pour gérer le multitenant.

Un article sur la façon de l'utiliser : https://tchury.substack.com/p/how-to-add-saml2-login-on-laravel

Librairies pour le SAML

Je recherche donc une librairie qui me permette d'intégrer un IdP SAML2. Avec au moins ces critères :

  • une librairie maintenue
  • une librairie me permettant de configurer l'IdP à la volée (pour le multitenant)

aacotroneo/laravel-saml2

Repo : https://github.com/aacotroneo/laravel-saml2 Licence : MIT

C'est la librairie que j'utilisais dans cf-ui mais cette repo n'est plus maintenue. Le README renvoie vers https://github.com/24Slides/laravel-saml2

Cette librairie encapsulait onelogin/php-saml version 3 pour Laravel.

24Slides/laravel-saml2

Repo : https://github.com/24Slides/laravel-saml2 Licence : MIT

Comme aacotroneo/laravel-saml2, cette librairie encapsule onelogin/php-saml version 3 (ou 4) pour Laravel.

Je vois que dès la version 2, le support multitenant a été ajouté :

Added

Completely changed the way of supporting multiple Identity Providers by adding Tenants
Helper functions saml_url(), saml_route(), saml_tenant_uuid()
Initializing SP in middleware
Database migrations
Console commands saml2:create-tenant, saml2:update-tenant, saml2:delete-tenant, saml2:restore-tenant, saml2:list-tenants, saml2:tenant-credentials

Voir https://github.com/24Slides/laravel-saml2/blob/master/CHANGELOG.md#added-13.

Un article sur son utilisation basique (sans multitenant) : https://joshuajordancallis.medium.com/setup-saml2-with-laravel-acting-as-the-service-provider-d97350d76b32

codegreencreative/laravel-samlidp

Repo : https://github.com/codegreencreative/laravel-samlidp Licence : MIT

Cette librairie permet de faire de l'application Laravel un IdP. Elle utilise la librairie de base litesaml/lightsaml version 4.

Donc ce n'est pas ce dont j'ai besoin.

OneLogin

Repo : https://github.com/SAML-Toolkits/php-saml (https://github.com/onelogin/php-saml) Licence : MIT

C'est une librairie de base que je devrais intégrer à Laravel si je la choisis. Je préfère utiliser une librairie déjà intégrée à Laravel si j'en trouve une qui me convient.

Actions

Je vais faire un POC en utilisant 24Slides/laravel-saml2.

Mes contraintes

La repo est publique donc je ne dois ni stocker les infos de mon SP (certificats, etc) ni les infos des IdP de nos clients.

Comme la lib est multitenant et stocke les infos d'un tenant (un IdP) dans la base, rien ne sera dans le code (ne pas faire de Seeder...).

Et les infos du SP peuvent être configurées grâce à des variables d'environnement que je pourrais mettre dans les secrets de Towerify CLI.

Préparation pour mes tests

Je veux pouvoir tester mon code depuis mon poste mais pour ajouter un SP à Keycloak j'ai besoin d'un domaine et du HTTPS.

Pour avoir un domaine et du HTTPS, j'utilise ngrok et la commande : ngrok http http://127.0.0.1:8080/

Warning

Le domaine change à chaque redémarrage de ngrok

J'ai ajouté un domaine static (gratuit) dans mon compte ngrok et je peux maintenant avoir toujours ce nom de domaine si j'utilise la commande : ngrok http --url=weasel-exotic-llama.ngrok-free.app http://127.0.0.1:8080/

Info

Je me connecte à mon compte ngrok en utilisant mon compte Google pbrisacier@mncc.fr

Je vais créer 2 IdP dans Keycloak : cywise1 et cywise2. Je vais décrire mes configurations pour cywise1 puis je referai la même chose pour cywise2.

J'ajoute le royaume cywise1. J'ai accès au metadata SAML : https://auth.computablefacts.com/realms/cywise1/protocol/saml/descriptor

Je peux y lire les informations dont j'ai besoin pour créer le tenant dans l'application :

php artisan saml2:create-tenant \
  --key=cywise1 \
  --entityId=https://auth.computablefacts.com/realms/cywise1 \
  --loginUrl=https://auth.computablefacts.com/realms/cywise1/protocol/saml \
  --logoutUrl=https://auth.computablefacts.com/realms/cywise1/protocol/saml \
  --x509cert="MIICnTCCAYUCBgGWIBRiSzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjeXdpc2UxMB4XDTI1MDQxMDE0MjAyOVoXDTM1MDQxMDE0MjIwOVowEjEQMA4GA1UEAwwHY3l3aXNlMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL/9UEM235k5H3ntv24kg0SBJkimjEa6LdwwJ31PM/f4KOQAwqF2MnnIessPNgqmeAVx/+VSVOGQJ3AzU6GrIYHDiCO1vfTpWKz5dnNVz9zd8B9y+urrM2vSnOX6nTve9aqWBTO6GqNPYCgsO8X7efeBd8VQQSk7GDTHSyE+2b408fTzNoO7+yVcn1VjVO+KvoT9iBG9ycJWhwnlFgR/cRpq2RzlYuwisua8htInt3nlk3718LaOC058ZOucCkvkOYYoIFxxJombuvbPthhFC55M0++Lj8OQK4NTLHMDIuS3bIbjWHSlO6YeR3IR1lHYUclXR+iH2pDL/knnSLGz3jsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEACljTuMR0PmKBAnXEMmOp6RpP86KMZ0s6mDenm5t5kDxNfCFmNKz1cY/t6FP9anHZBtyHqM0S8mZGPq75JwnsPhtf5SQvBvZSkajImPiiKupkCsTUfFPpJZVnFHKvKjJIwCkN0KNnumLZ7JS582WZeV1PF7bb4AtL8umBhsdtJKpFKUg+8obedOJPrh2GRm325vjDIJVEpGB7REHo+mb4qZvyLfqxAxXTJQU4YUqiIpYN1oAxjuXCtxRWKw2+/ulx9YirJNn1bjtqqmOVMk37h7IfJnFCY7iVLBC0Xe8c1asNhPK+Fj8RLBW6e53glDY4mnGxWJp/OFkG8wY7/u6/CQ=="
Et j'obtiens en réponse les informations :
The tenant #1 (2f57bc95-d2fa-486f-8a88-5f6ded3d764f) was successfully created.

Credentials for the tenant
--------------------------

 Identifier (Entity ID): http://localhost:8178/saml2/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/metadata
 Reply URL (Assertion Consumer Service URL): http://localhost:8178/saml2/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/acs
 Sign on URL: http://localhost:8178/saml2/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/login
 Logout URL: http://localhost:8178/saml2/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/logout
 Relay State:  (optional)

Je change la config pour passer du préfixe /saml2 au préfixe /saml :

'routesPrefix' => '/saml',

Je change APP_URL dans mon .env local :

APP_URL=https://weasel-exotic-llama.ngrok-free.app

Ce qui met à jour les infos du tenant que je peux voir avec php artisan saml2:tenant-credentials 1.

Credentials for the tenant
--------------------------

 Identifier (Entity ID): https://weasel-exotic-llama.ngrok-free.app/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/metadata
 Reply URL (Assertion Consumer Service URL): https://weasel-exotic-llama.ngrok-free.app/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/acs
 Sign on URL: https://weasel-exotic-llama.ngrok-free.app/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/login
 Logout URL: https://weasel-exotic-llama.ngrok-free.app/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/logout
 Relay State:  (optional)

J'en profite pour changer les infos de l'organisation et des contacts en ajoutant ça dans mon .env :

SAML2_CONTACT_TECHNICAL_NAME="Patrick Brisacier"
SAML2_CONTACT_TECHNICAL_EMAIL=pbrisacier+saml@mncc.fr
SAML2_CONTACT_SUPPORT_NAME=Support
SAML2_CONTACT_SUPPORT_EMAIL=support@computablefacts.freshdesk.com
SAML2_ORGANIZATION_NAME=ComputableFacts
SAML2_ORGANIZATION_URL=https://cywise.io/
Car ces infos sont affichées dans les metadata donc dans celle de cywise1 : https://weasel-exotic-llama.ngrok-free.app/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/metadata

Je génère un certificat X509 pour mon SP avec les commandes :

openssl genrsa -out cywise-local.pem 2048
openssl req -new -key cywise-local.pem -out cywise-local.csr
openssl x509 -req -days 3650 -in cywise-local.csr -signkey cywise-local.pem -out cywise-local.crt

Puis j'ajoute le certificat (cywise-local.crt) et la clé privée (cywise-local.pem) dans mon .env :

SAML2_SP_CERT_x509="MIIDFTCC****L5rJIQ=="
SAML2_SP_CERT_PRIVATEKEY="MIIEvAIB****6B7UuA=="

Le certificat apparaît maintenant dans les metadata. Je sauvegarde les metadata dans un fichier cywise1-metadata.xml.

Je crée un client Keycloak cywise-dev-patrick dans cywise1 en important cywise1-metadata.xml.

NOTA : j'ai dû supprimer une balise <script/> dans le fichier cywise1-metadata.xml pour que Keycloak veuille importer le fichier.

Par défaut, Keycloak active l'option (du client) "Client Signature Required". J'ai dû changer les 2 options ci-dessous à true pour activer la signature pour mon SP :

'authnRequestsSigned' => true,
'logoutRequestSigned' => true,
Sinon, keycloak fait une erreur dans ses logs :
ERROR [org.keycloak.protocol.saml.SamlService] (executor-thread-12334) request validation failed: org.keycloak.common.VerificationException: SigAlg was null

En cliquant sur l'URL "Sign on URL", je peux valider un login/password (cywise1 / cywise1-patrick) puis je suis redirigé vers Cywise. Il y a des erreurs mais je n'ai rien codé pour authentifier l'utilisateur.

Modification du process de login de Laravel

Je veux découper la phase de login en 2 parties : - demander l'email uniquement - demander le password ensuite

Cela va me permettre, quand le formulaire de demande de l'email sera posté, de récupérer le domaine et de rediriger l'utilisateur vers l'IdP qui correspond à ce domaine s'il y en a un.

Nous utilisons Laravel UI pour gérer le login, le logout, register, etc.

J'ai refait la commande php artisan ui bootstrap --auth pour voir les fichiers mis en place. Pour les controllers, il y a : HomeController, Auth\LoginController, Auth\RegisterController, Auth\ForgotPasswordController, Auth\ResetPasswordController que nous avions déjà et Auth\ConfirmPasswordController et Auth\VerificationController que nous n'avons pas.

Les routes sont mises en place grâce à Auth::routes(); qui se trouve dans le fichier routes/web.php. Cela appelle la méthode auth() de la classe AuthRouteMethods (vendor/laravel/ui/src/AuthRouteMethods.php).

Comme il n'y a pas d'options passées à la méthode, cela met en place les routes pour login, logout, register et reset. Comme on peut le voir avec cette commande :

$ php artisan route:list | grep Auth
  GET|HEAD        login ........................................ login  Auth\LoginController@showLoginForm
  POST            login ........................................................ Auth\LoginController@login
  POST            logout ............................................. logout  Auth\LoginController@logout
  GET|HEAD        password/confirm ...... password.confirm  Auth\ConfirmPasswordController@showConfirmForm
  POST            password/confirm ................................. Auth\ConfirmPasswordController@confirm
  POST            password/email ........ password.email  Auth\ForgotPasswordController@sendResetLinkEmail
  GET|HEAD        password/reset ..... password.request  Auth\ForgotPasswordController@showLinkRequestForm
  POST            password/reset ..................... password.update  Auth\ResetPasswordController@reset
  GET|HEAD        password/reset/{token} ...... password.reset  Auth\ResetPasswordController@showResetForm
  GET|HEAD        register ........................ register  Auth\RegisterController@showRegistrationForm
  POST            register ............................................... Auth\RegisterController@register

Je vais me concentrer sur le login et le logout mais je devrais gérer les autres correctement plus tard.

Fonctionnement actuel du login : - GET /login -> LoginController@showLoginForm -> view('auth.login') -> valider le formulaire appelle POST /login - POST /login -> LoginController@login -> sendLoginResponse -> redirectTo /home

Fonctionnement voulu du login : - GET /login -> LoginController@showLoginForm -> view('auth.login') -> valider le formulaire appelle POST /login/email - POST /login/email -> LoginController@loginEmail -> redirect GET /login/password - GET /login/password -> LoginController@showLoginPasswordForm -> view('auth.login_password') -> valider le formulaire appelle POST /login - POST /login -> LoginController@login -> sendLoginResponse -> redirectTo /home

Je dois donc - ajouter 2 routes : - POST /login/email -> LoginController@loginEmail - GET /login/password -> LoginController@showLoginPasswordForm - modifier la vue 'auth.login' (pour qu'elle ne demande que l'email et qu'elle le POST vers /login/email) - ajouter la vue 'auth.login_password' (qui demande le password et masque le champ email)

J'ai fait la modification et ça fonctionne.

Redirection vers l'IdP SAML en fonction du domaine de l'email

La librairie 24Slides/laravel-saml2 stocke les tenants SAML dans la table saml2_tenants et utilise le modèle \Slides\Saml2\Models\Tenant::class pour y accéder.

Je vais ajouter une table de correspondance entre un domaine et un tenant SAML. Ainsi, après l'envoi de l'email, à l'étape 1 du process de login, je pourrais extraire le domaine de l'email et renvoyer vers l'IdP (le tenant SAML) correspondant s'il y en a un sinon, je continue le process en demande le password à l'étape 2.

Je vais appeller la table saml2_email_domains et le modèle SamlEmailDomain (sans le 2).

J'ai modifié la méthode loginEmail pour retrouver l'IdP lié au domaine de l'email puis rediriger l'utilisateur vers cet IdP.

Problème avec ngrok et le port

Après authentification dans Keycloak cywise1, j'ai ce message d'erreur :

The response was received at http://weasel-exotic-llama.ngrok-free.app:8080/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/acs 
instead of https://weasel-exotic-llama.ngrok-free.app/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/acs

J'active l'option de la librairie prévue pour ça (This is useful if your application is running behind a load balancer which terminates SSL.) :

'proxyVars' => true,

J'ai maintenant ce message d'erreur :

The response was received at https://weasel-exotic-llama.ngrok-free.app:8080/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/acs 
instead of https://weasel-exotic-llama.ngrok-free.app/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/acs
C'est mieux car l'URL commence par https au lieu de http mais il reste le port 8080 qui est celui que j'utilise en local.

Il faudrait que ngrok passe l'entête X-Forwarded-For: 443 dans les entêtes de la requête envoyée à mon application locale pour que ça fonctionne.

Après un peu de temps dans la doc de ngrok, je vois qu'on peut ajouter, dans le fichier de configuration de ngrok, une section traffic_policy qui permet d'ajouter l'entête.

J'ai d'abord converti la version du fichier de config (seule la version 3 permet d'ajouter une section traffic_policy) avec :

ngrok config upgrade

Puis j'ai modifié le fichier de config avec la commande ngrok config edit pour y mettre :

version: "3"
agent:
    authtoken: '***'
    connect_url: connect.eu.ngrok-agent.com:443
endpoints:
  - name: cywise-saml-debug
    description: Debug Cywise for SAML IdP auth
    url: weasel-exotic-llama.ngrok-free.app
    upstream:
      url: http://127.0.0.1:8080/
    traffic_policy:
      on_http_request:
        - actions:
            - type: add-headers
              config:
                headers:
                  X-Forwarded-Port: ${conn.server_port}

Enfin, je démarre ngrok avec la commande :

ngrok start cywise-saml-debug

Modification dans la base

J'avais créé une table saml2_email_domains et le modèle SamlEmailDomain qui va avec mais j'ai une relation OneToOne entre cette table et celle créée par la librairie (saml2_tenants). Je préfère tout mettre dans la même table (saml2_tenants) en reprenant les colonnes des migrations de la librairie et en ajoutant mes colonnes.

Voilà ma version de la migration :

Schema::create('saml2_tenants', function (Blueprint $table) {
    $table->id();
    $table->timestamps();

    // The associated Cywise tenant
    $table->intOrBigIntBasedOnRelated('tenant_id', Schema::connection(null), 'tenants.id')->nullable();
    $table->foreign('tenant_id')->references('id')->on('tenants')->cascadeOnDelete();

    // The associated customer
    $table->intOrBigIntBasedOnRelated('customer_id', Schema::connection(null), 'customers.id')->unsigned()->nullable();
    $table->foreign('customer_id')->references('id')->on('customers')->cascadeOnDelete();

    // Associated email domains
    $table->string('domain')->default('');
    $table->string('alt_domain1')->default('');

    $table->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->string('relay_state_url')->nullable();
    $table->string('name_id_format')->default('persistent');
    $table->json('metadata');
    $table->softDeletes();
});

J'ai ajouté les colonnes tenant_id et customer_id pour les utiliser si j'ai besoin de créer l'utilisateur (à sa première connexion).

J'ai ajouté les colonnes domain et alt_domain1 pour lier le tenant SAML aux domaines du client (par exemple hermes.com et ext.hermes.com).

J'ai créé un modèle pour aller avec la table en héritant du modèle de la librairie :

namespace App\Models;

use Slides\Saml2\Models\Tenant;

class Saml2Tenant extends Tenant
{
    public static function firstFromDomain(string $domain)
    {
        return self::query()
            ->where('domain', '=', $domain)
            ->orWhere('alt_domain1', '=', $domain)
            ->first();
    }
}
Ce qui me permet d'y ajouter mes méthodes comme cette première qui retrouve le tenant SAML en fonction d'un domaine peu importe dans laquelle des 2 colonnes il se trouve.

Enfin, je modifie les options de la librairie prévue pour ça pour utiliser mon modèle et ne pas faire les migrations par défaut :

'tenantModel' => \App\Models\Saml2Tenant::class,
'load_migrations' => false,

Ajout de claims dans Keycloak

Avec le réglage par défaut de Keycloak, Cywise ne reçoit que l'attribut role et pas l'email ni le nom de l'utilisateur.

J'ai ajouté l'email et le "common name" dans les claims (je me suis inspiré de mes réglages pour ISTA).

Authentifier l'utilisateur dans Cywise

Je dois maintenant authentifier l'utilisateur dans Cywise. Et, avant, créer son compte s'il n'existe pas déjà.

Pour ça, je dois faire un Listener pour l'événement SignedIn de la librairie. Comme je vais devoir aussi faire un Listener pour l'événement SignedOut, j'ai préféré les regrouper dans la même classe app/Listeners/SamlEventSubscriber.php qui ressemble à :

namespace App\Listeners;

use Illuminate\Events\Dispatcher;
use Slides\Saml2\Events\SignedIn;
use Slides\Saml2\Events\SignedOut;

class SamlEventSubscriber
{
    /**
     * Register the listeners for the subscriber.
     *
     * @return array<string, string>
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            SignedIn::class => 'handleSignedIn',
            SignedOut::class => 'handleSignedOut',
        ];
    }
    public function handleSignedIn(SignedIn $event): void {}
    public function handleSignedOut(SignedOut $event): void {}
}

Puis, j'ai enregistré cette classe dans la méthode boot() de la classe app/Providers/AppServiceProvider.php :

// SAML
Event::subscribe(SamlEventSubscriber::class);

Je crée l'utilisateur en passant par la classe invitation :

$invitation = Invitation::query()
    ->where('email', $this->saml2UserEmail)
    ->first();
if (!$invitation) {
    $invitation = InvitationProxy::createInvitation($this->saml2UserEmail, $this->saml2UserName);
}
$user = $invitation->createUser([
    'password' => Str::random(64),
    'tenant_id' => $tenantId,
    'customer_id' => $customerId,
]);

Au départ, j'arrivais à créer l'utilisateur mais il n'était pas connecté à Cywise. Le problème venait des middlewares pour gérer les cookies (pour la session) qui n'était pas actif. J'ai résolu le problème en ajoutant un MiddlewareGroup dans app/Http/Kernel.php :

'saml' => [
    \App\Http\Middleware\EncryptCookies::class,
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
    \Illuminate\Session\Middleware\StartSession::class,
],
Puis en changeant le setting de la librairie pour utiliser ce MiddlewareGroup :
'routesMiddleware' => ['saml'],

Logout

Comme expliqué dans la doc officielle de la librairie, il y a 2 cas : * logout depuis l'IdP, Keycloak pour mes tests * logout depuis Cywise

Pour le logout depuis l'IdP, cela appelle l'URL /saml/{uuid}/slo qui déclenche l'événement SignedOut. Il a donc suffit que je déconnecte l'utilisateur de Cywise pour cet événement :

public function handleSignedOut(SignedOut $event): void
{
    Auth::logout();
    Session::save();
}

Pour le logout depuis Cywise, il faut que j'appelle le logout de l'IdP. Mais seulement si l'utilisateur est connecté depuis un IdP.

Heureusement, si l'utilisateur s'est connecté depuis un IdP, le helper saml_tenant_uuid() renvoit l'UUID du tenant SAML. Et le helper renvoie null si l'utilisateur ne vient pas d'un IdP.

J'ai donc copier la méthode logout() de xxx dans ma classe LoginController pour ajouter ce test au début et rediriger vers le logout de l'IdP :

if ($uuid = saml_tenant_uuid()) {
    return redirect(URL::route('saml.logout', ['uuid' => $uuid]));
}

Ca faisait une erreur jusqu'à ce que je change ce setting de la librairie pour mettre une route de logout par défaut à '/' :

'logoutRoute' => env('SAML2_LOGOUT_URL', '/'),

Settings JSON pour Tenant SAML

Je veux pouvoir changer des paramétrages pour chaque tenant SAML. Par exemple, les noms des claims pour récupérer l'email, le nom et les roles de l'utilisateur. Je vais stocker ces paramétrages dans une colonne JSON dans la table qui contient les tenants SAML (saml2_tenants). La table par défaut de la librairie contenait déjà une colonne JSON appelée metadata que j'ai conservée pour stocker les paramétrages (en passant, metadata est un très mauvais nom car ça n'a rien à voir avec les metadata de l'IdP...)

Je vais faire une méthode dans ma classe Saml2Tenant qui me permettra de récupérer une des clés du JSON (un paramètre) et de renvoyer une valeur par défaut si la clé n'existe pas.

Les requêtes SQL que j'ai faites pour tester les settings actuels :

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.claims', JSON_OBJECT())
WHERE `key`='cywise1' AND JSON_EXTRACT(metadata, '$.claims') IS NULL;

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.claims.email', JSON_OBJECT())
WHERE `key`='cywise1' AND JSON_EXTRACT(metadata, '$.claims.email') IS NULL;

update saml2_tenants set metadata = JSON_SET(metadata, '$.claims.email.friendlyName', 'email', '$.claims.email.name', 'http://schemas.xmlsoap.org/claims/EmailAddress')
where `key`='cywise1';

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.claims.name', JSON_OBJECT())
WHERE `key`='cywise1' AND JSON_EXTRACT(metadata, '$.claims.name') IS NULL;

update saml2_tenants set metadata = JSON_SET(metadata, '$.claims.name.friendlyName', 'name', '$.claims.name.name', 'http://schemas.xmlsoap.org/claims/CommonName')
where `key`='cywise1';

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.claims.role', JSON_OBJECT())
WHERE `key`='cywise1' AND JSON_EXTRACT(metadata, '$.claims.role') IS NULL;

update saml2_tenants set metadata = JSON_SET(metadata, '$.claims.role.friendlyName', 'role', '$.claims.role.name', 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role')
where `key`='cywise1';


UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.updateUser', JSON_OBJECT())
WHERE `key`='cywise1' AND JSON_EXTRACT(metadata, '$.updateUser') IS NULL;

update saml2_tenants set metadata = JSON_SET(metadata, '$.updateUser.putRandomPassword', true)
where `key`='cywise1';
NOTA : il faut créer la clé parent si elle n'existe pas sinon les clés enfant ne sont pas créées.

Puis je lis ces clés avec un code de ce genre :

$roleFriendlyName = $this->saml2Tenant->config('claims.role.friendlyName', 'role');

2025-04-30 - Publication en DEV

J'ai fusionné ma branche avec 0.x et j'ai déployé en DEV.

Je refais une conf vers Keycloak cywise1. Je me suis connecté à yunohost-addapps puis au container app de cywise_ui_dev avec sudo docker exec -it e5529ce4388d /bin/bash.

Cela m'a permis de refaire la commande artisan pour créer le tenant SAML de l'IdP cywise1 avec la même commande que celle que j'avais utilisée en local :

php artisan saml2:create-tenant \
  --key=cywise1 \
  --entityId=https://auth.computablefacts.com/realms/cywise1 \
  --loginUrl=https://auth.computablefacts.com/realms/cywise1/protocol/saml \
  --logoutUrl=https://auth.computablefacts.com/realms/cywise1/protocol/saml \
  --x509cert="MIICnTCCAYUCBgGWIBRiSzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjeXdpc2UxMB4XDTI1MDQxMDE0MjAyOVoXDTM1MDQxMDE0MjIwOVowEjEQMA4GA1UEAwwHY3l3aXNlMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL/9UEM235k5H3ntv24kg0SBJkimjEa6LdwwJ31PM/f4KOQAwqF2MnnIessPNgqmeAVx/+VSVOGQJ3AzU6GrIYHDiCO1vfTpWKz5dnNVz9zd8B9y+urrM2vSnOX6nTve9aqWBTO6GqNPYCgsO8X7efeBd8VQQSk7GDTHSyE+2b408fTzNoO7+yVcn1VjVO+KvoT9iBG9ycJWhwnlFgR/cRpq2RzlYuwisua8htInt3nlk3718LaOC058ZOucCkvkOYYoIFxxJombuvbPthhFC55M0++Lj8OQK4NTLHMDIuS3bIbjWHSlO6YeR3IR1lHYUclXR+iH2pDL/knnSLGz3jsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEACljTuMR0PmKBAnXEMmOp6RpP86KMZ0s6mDenm5t5kDxNfCFmNKz1cY/t6FP9anHZBtyHqM0S8mZGPq75JwnsPhtf5SQvBvZSkajImPiiKupkCsTUfFPpJZVnFHKvKjJIwCkN0KNnumLZ7JS582WZeV1PF7bb4AtL8umBhsdtJKpFKUg+8obedOJPrh2GRm325vjDIJVEpGB7REHo+mb4qZvyLfqxAxXTJQU4YUqiIpYN1oAxjuXCtxRWKw2+/ulx9YirJNn1bjtqqmOVMk37h7IfJnFCY7iVLBC0Xe8c1asNhPK+Fj8RLBW6e53glDY4mnGxWJp/OFkG8wY7/u6/CQ=="

La réponse :

The tenant #1 (565e5bfc-9804-4ed8-b174-c6138c4dec31) was successfully created.

Credentials for the tenant
--------------------------

 Identifier (Entity ID): https://dev.cywise-ui.myapps.addapps.io/saml/565e5bfc-9804-4ed8-b174-c6138c4dec31/metadata
 Reply URL (Assertion Consumer Service URL): https://dev.cywise-ui.myapps.addapps.io/saml/565e5bfc-9804-4ed8-b174-c6138c4dec31/acs
 Sign on URL: https://dev.cywise-ui.myapps.addapps.io/saml/565e5bfc-9804-4ed8-b174-c6138c4dec31/login
 Logout URL: https://dev.cywise-ui.myapps.addapps.io/saml/565e5bfc-9804-4ed8-b174-c6138c4dec31/logout
 Relay State:  (optional)

J'utilise phpMyAdmin pour modifier ce tenant SAML afin d'ajouter le domaine cywise1.io.

J'ai également refait la configuration d'un nouveau client dans Keycloak. Et j'ai pu me connecter avec cywise1 / cywise1-patrick / demo@cywise1.io et voir la page /home.

=> La première fois j'ai eu un timeout (504)

Mais je n'ai pas reproduit le problème les fois suivantes.

2025-04-30 - Publication en PROD

Je refais à nouveau le paramétrage de Keycloak cywise1 pour le domaine cywise1.io :

The tenant #1 (48696b8c-225d-4b98-8102-0f8f9e7f9460) was successfully created.

Credentials for the tenant
--------------------------

 Identifier (Entity ID): https://app.cywise.io/saml/48696b8c-225d-4b98-8102-0f8f9e7f9460/metadata
 Reply URL (Assertion Consumer Service URL): https://app.cywise.io/saml/48696b8c-225d-4b98-8102-0f8f9e7f9460/acs
 Sign on URL: https://app.cywise.io/saml/48696b8c-225d-4b98-8102-0f8f9e7f9460/login
 Logout URL: https://app.cywise.io/saml/48696b8c-225d-4b98-8102-0f8f9e7f9460/logout
 Relay State:  (optional)

Je génère un certificat X509 pour mon SP avec les commandes :

openssl genrsa -out cywise-prod.pem 2048
openssl req -new -key cywise-prod.pem -out cywise-prod.csr
openssl x509 -req -days 3650 -in cywise-prod.csr -signkey cywise-prod.pem -out cywise-prod.crt

Puis j'ajoute le certificat (cywise-prod.crt) et la clé privée (cywise-prod.pem) dans mon .env :

SAML2_SP_CERT_x509="MIIDFTCC****1tXxVA=="
SAML2_SP_CERT_PRIVATEKEY="MIIEvgIB****cbdUx8Gr"

NOTA: je n'ai pas fait cette étape pour Cywise DEV car, je pense, j'ai les variables dans mon .env local et, comme c'est moi qui ai publié, mon .env se trouve sur Cywise en plus des secrets...

Je suis allé modifier directement le fichier /home/yunohost.app/cywise-ui_prod/.env pour y ajouter les 2 clés. Puis j'ai redémarré la stack avec sudo systemctl restart cywise-ui_prod.service pour que le changement soit pris en compte.

J'ai ajouté le domaine cywise1 à ce tenant SAML et j'ai ajouté tenant_id=18 et customer_id=9 car ce sont les IDs des utilisateurs d'Hermès.

customer_id a bien été mis à jour mais pas tenant_id => Normal, tenant_id n'était pas fillable. Cyrille a commité cette modif.

Etrange : je n'ai pas réussi à supprimer mon utilisateur. J'avais une erreur sur la contrainte avec am_assets et sa colonne created_by pourtant, aucun asset de cette table n'avait un created_by=113 (l'ID de mon user). J'ai dû le supprimer en décochant la vérification des contraintes...

Dans app/Listeners/UserInvitationUtilizedListener.php, les tenant_id et customer_id de l'utilisateur créés à partir d'une invitation sont copiés depuis ceux de l'utilisateur qui a créé l'invitation (created_by). Mais, dans mon cas, je n'ai pas de created_by donc pas de copie. La classe met également en place les rôles par défaut et ceux de Hermès si un created_by est défini donc ne le fait pas dans mon cas. Je pense que c'est mieux de ne pas mettre de created_by à mon invitation, comme ça je peux gérer les ID et les rôles indépendamment des invitations "standards".

Je vais stocker dans les settings JSON de mon tenant SAML, les rôles par défaut que je dois attribuer à tous les utilisateurs et les correspondances entre "groupes IdP" et rôles à attribuer à l'utilisateur.

A partir de la chaîne 'App\Models\Role::CYBERBUDDY_ADMIN' je peux retrouver la valeur de la constante avec constant('App\Models\Role::CYBERBUDDY_ADMIN'). NOTA : il faut absoluement mettre le namespace.

Je crée le tenant SAML pour Hermès car je veux envoyer les URLs aujourd'hui. Sur le serveur, dans le container de Cywise PROD app, je fais la commande :

php artisan saml2:create-tenant \
  --key=hermes \
  --entityId=http://fed.hermes.com/adfs/services/trust \
  --loginUrl=https://fed.hermes.com/adfs/ls/ \
  --logoutUrl=https://fed.hermes.com/adfs/ls/ \
  --x509cert="MIIC2DCCAcCgAwIBAgIQJh/EANNXJb5LBvCDXcXOwzANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQDEx1BREZTIFNpZ25pbmcgLSBmZWQuaGVybWVzLmNvbTAeFw0yMzA5MjcwOTM2MjBaFw0yODA5MjcwOTM2MjBaMCgxJjAkBgNVBAMTHUFERlMgU2lnbmluZyAtIGZlZC5oZXJtZXMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwZT8DsXjzQO02YpXZ99PmIXanvWp+dDsP0SQmMJMHoKgsmj5gDlYohkCXlt2K9hYVa84drBYxLZLmY3MotGVoQhwDV/vHIMJOzys2IKMBtHjsPPYVML9tFJRZPtMIxH3Dzp1sPDiDmY67IZhgcEiEzlIuAcsToTUNSZjR+1kd+4j5nuf5NBtV0dc499yOs3s8q4rk4R0fd2BwC2QawBvu2iTT8xsBX8i4N3N3kLhJeSSgHUIg7QZNPNmo+cs6HfGehGhn2jcfAotYLRZfV8V15RK5DIXgruzqGVOgE91twSQKGepFxVrEdgGwszyzEyzaqBkcU3nTl2SEQ3yX7B9uQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBPfTe/w4or4QV0ffysfXBiuisGu7ThZRU6stJ+pO1Y6aYXyM2Xe4PmTKwY0evkSJmLrNWlqRpoNEcR3H3fCDHaMKVWV+/4QJ3W5Xd1bPwX0qYYyHJxn4TkH2q0cmidjYWP1KxL/57uCbzpdwtKBrA/dOi+MlFJ9O1GhAuKk/BC22iXNoROQ6nfL8tJdvWMx2I7HI5CrDZzoYPtH+liJP05rfpSf31FnkKIedPb6FHiSgTkM44XUB9nqml0DPLWvxTXJ4XYmHGmCI9REOTY7YUy7U0wevV7w9elXcr1T3NSDj2y8/avZVhRgjsKsq2DWwewGT5PUazU7MIz6Yq806vF"
The tenant #2 (bce5732e-8860-47d0-89e4-b3adccccb750) was successfully created.

Credentials for the tenant
--------------------------

 Identifier (Entity ID): https://app.cywise.io/saml/bce5732e-8860-47d0-89e4-b3adccccb750/metadata
 Reply URL (Assertion Consumer Service URL): https://app.cywise.io/saml/bce5732e-8860-47d0-89e4-b3adccccb750/acs
 Sign on URL: https://app.cywise.io/saml/bce5732e-8860-47d0-89e4-b3adccccb750/login
 Logout URL: https://app.cywise.io/saml/bce5732e-8860-47d0-89e4-b3adccccb750/logout
 Relay State:  (optional)

Dans la base de données, je change : - tenant_id=18 - customer_id=9 - domain=hermes.com.test - alt_domain1=ext.hermes.com.test - uuid=0d92ec95-af6a-48c4-b890-6f701a6f33fb (je mets celui que j'ai déjà communiqué à Hermès)

Ce qui donne :

root@086bc0971261:/var/www/html# php artisan saml2:tenant-credentials 2

The tenant model
================

+-----------------+-------------------------------------------------------+
| Column          | Value                                                 |
+-----------------+-------------------------------------------------------+
| ID              | 2                                                     |
| UUID            | 0d92ec95-af6a-48c4-b890-6f701a6f33fb                  |
| Key             | hermes                                                |
| Entity ID       | http://fed.hermes.com/adfs/services/trust             |
| Login URL       | https://fed.hermes.com/adfs/ls/                       |
| Logout URL      | https://fed.hermes.com/adfs/ls/                       |
| Relay State URL | (empty)                                               |
| Name ID format  | persistent                                            |
| x509 cert       | MIIC2DCCAcCgAwIBAgIQJh/EANNXJb5LBvCDXcXOwzANBgkqhk... |
| Metadata        | (empty)                                               |
| Created         | 2025-05-02 10:19:22                                   |
| Updated         | 2025-05-02 10:19:22                                   |
| Deleted         | (empty)                                               |
+-----------------+-------------------------------------------------------+

Credentials for the tenant
--------------------------

 Identifier (Entity ID): https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/metadata
 Reply URL (Assertion Consumer Service URL): https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/acs
 Sign on URL: https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/login
 Logout URL: https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/logout
 Relay State:  (optional)

Je ne veux ne pas modifier le mot de passe des comptes Hermès existants, pour le moment, durant la phase de tests avec Amine. Comme la valeur par défaut de updateUser.putRandomPassword est false, je n'ai pas besoin de changer les settings du Tenant SAML.

Je dois faire les réglages des claims. D'après les metadata de l'IdP d'Hermès, je vais paramétrer : - claims.email.friendlyName = email (default) - claims.email.name = http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress - claims.name.friendlyName = name (default) - claims.name.name = http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname - claims.role.friendlyName = group - claims.role.name = http://schemas.xmlsoap.org/claims/Group

J'ai donc changé les settings avec les requêtes SQL ci-dessous :

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.claims', JSON_OBJECT())
WHERE `key`='hermes' AND JSON_EXTRACT(metadata, '$.claims') IS NULL;

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.claims.email', JSON_OBJECT())
WHERE `key`='hermes' AND JSON_EXTRACT(metadata, '$.claims.email') IS NULL;

update saml2_tenants set metadata = JSON_SET(metadata, '$.claims.email.name', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress')
where `key`='hermes';

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.claims.name', JSON_OBJECT())
WHERE `key`='hermes' AND JSON_EXTRACT(metadata, '$.claims.name') IS NULL;

update saml2_tenants set metadata = JSON_SET(metadata, '$.claims.name.name', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname')
where `key`='hermes';

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.claims.role', JSON_OBJECT())
WHERE `key`='hermes' AND JSON_EXTRACT(metadata, '$.claims.role') IS NULL;

update saml2_tenants set metadata = JSON_SET(metadata, '$.claims.role.friendlyName', 'group', '$.claims.role.name', 'http://schemas.xmlsoap.org/claims/Group')
where `key`='hermes';

Mise à jour des rôles

Je veux pouvoir paramétrer les rôles mis en place quand un utilisateur se connecte en passant par un tenant SAML : - ajouter une liste de rôles à tous les utilisateurs - ajouter une liste de rôles pour un ou plusieurs rôles issus de l'IdP

J'ai fait un paramétrage de ce type :

"roles": {
    "default": ["role1", "role2"],
    "idp_roles": ["user", "admin"],
    "user_to_cywise": ["cywise_user"],
    "admin_to_cywise": ["cywise_admin", "cywise_admin2"]
}
La clé default donne la liste des rôles Cywise à ajouter à chaque utilisateur. La clé idp_roles donne la liste des rôles de l'IdP à prendre en compte. Pour chaque rôle de l'IdP, la clé <idp_role>_to_cywise donne la liste des rôles Cywise à ajouter à l'utilisateur s'il a ce rôle <idp_role>.

Le paramétrage fait sur Cywise DEV pour l'IdP cywise1 afin de fait mes tests :

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.roles', JSON_OBJECT())
WHERE `key`='cywise1' AND JSON_EXTRACT(metadata, '$.roles') IS NULL;

update saml2_tenants set metadata = JSON_SET(metadata, '$.roles.default', JSON_ARRAY('App\\Models\\Role::CYBERBUDDY_ONLY'))
where `key`='cywise1';

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.roles.idp_roles', JSON_ARRAY('manage-account'))
WHERE `key`='cywise1';

update saml2_tenants set metadata = JSON_SET(metadata, '$.roles.manage-account_to_cywise', JSON_ARRAY('App\\Models\\Role::CYBERBUDDY_ADMIN'))
where `key`='cywise1';

2025-05-05 - Publication en PROD

Cyrille a fait la MEP ce matin.

Je vais ajouter les paramètres pour la synchronisation des rôles avec l'IdP d'Hermès :

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.roles', JSON_OBJECT())
WHERE `key`='hermes' AND JSON_EXTRACT(metadata, '$.roles') IS NULL;

update saml2_tenants set metadata = JSON_SET(metadata, '$.roles.default', JSON_ARRAY('App\\Models\\Role::CYBERBUDDY_ONLY'))
where `key`='hermes';

UPDATE saml2_tenants SET metadata = JSON_SET(metadata, '$.roles.idp_roles', JSON_ARRAY('SG-WORLD-CU-CYBERBUDDY-ADMIN-SSO'))
WHERE `key`='hermes';

update saml2_tenants set metadata = JSON_SET(metadata, '$.roles.SG-WORLD-CU-CYBERBUDDY-ADMIN-SSO_to_cywise', JSON_ARRAY('App\\Models\\Role::CYBERBUDDY_ADMIN'))
where `key`='hermes';

Avant :

{
  "claims": {
    "email": {
      "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
    }, 
    "name": {
      "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
    }, 
    "role": {
      "friendlyName": "group", 
      "name": "http://schemas.xmlsoap.org/claims/Group"
    }
  }
}

Après :

{
  "claims": {
    "email": {
      "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
    }, 
    "name": {
      "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
    }, 
    "role": {
      "friendlyName": "group", 
      "name": "http://schemas.xmlsoap.org/claims/Group"
    }
  }, 
  "roles": {
    "default": ["App\\Models\\Role::CYBERBUDDY_ONLY"], 
    "idp_roles": ["SG-WORLD-CU-CYBERBUDDY-ADMIN-SSO"], 
    "SG-WORLD-CU-CYBERBUDDY-ADMIN-SSO_to_cywise": ["App\\Models\\Role::CYBERBUDDY_ADMIN"]
  }
}

2025-05-07 - Debug Logout avec Hermès

Etat des lieux : - le login fonctionne - je récupère email et groups (pas firstName et lastName mais ce n'est qu'un problème de réglage) - le logout provoque une erreur 500. A priori un problème de signature que ma lib décode avec SHA1 alors que c'est encodé avec SHA256.

Amine a testé avec un plugin permettant de voir les messages SAML. Lors du logout, il y a bien un paramètre SigAlg dans l'URL. J'ai modifié le paramètre retrieveParametersFromServer à true pour demander à la lib de lire dans la querystring. Je l'ai fait à l'arrache dans mon container et j'ai vérifié avec :

root@dc4e2ac02573:/var/www/html# php artisan tinker
Psy Shell v0.12.7 (PHP 8.3.20 — cli) by Justin Hileman
> config('saml2.retrieveParametersFromServer')
= true
Malgré cela, le logout fait toujours une erreur 500 avec le message : [2025-05-07 10:06:45] prod.ERROR: saml2.error_detail {"uuid":"0d92ec95-af6a-48c4-b890-6f701a6f33fb","error":"Signature validation failed. Logout Response rejected"}

Hermès a détecté un problème avec le NameID dans la requête de logout : Cywise envoi l'EntityID à la place du NameID reçu dans la phase de login ce qui n'est pas normal et provoque une erreur sur le serveur Windows de l'ADFS d'Hermès.

J'ai vu dans le code de ma librairie que l'EntityID est envoyée si aucun NameID n'est fourni à la classe qui construit la requête de logout.

// vendor/onelogin/php-saml/src/Saml2/LogoutRequest.php line 94
if (!empty($nameId)) {
    if (empty($nameIdFormat)
        && $spData['NameIDFormat'] != Constants::NAMEID_UNSPECIFIED) {
        $nameIdFormat = $spData['NameIDFormat'];
    }
} else {
    $nameId = $idpData['entityId'];
    $nameIdFormat = Constants::NAMEID_ENTITY;
}

Fin du meeting

J'installe le plugin "SAML Tracer" sur mon Firefox i.e. le même outil que celui que Nicolas a demandé à Amine d'installer sur son Chrome pendant le meeting. J'ai demandé l'export des logs de cet outil à Amine. Comme ça, je pourrais voir le détail des requêtes et des réponses SAML de ces tests de ce matin.

Je reproduis le problème du NameID en utilisant mon IdP Cywise1 sur Keycloak. La requête de logout contient l'entityID :

<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                     ID="ONELOGIN_cc4cc12edbe8e97a440089832082f1c43965836a"
                     Version="2.0"
                     IssueInstant="2025-05-07T10:45:48Z"
                     Destination="https://auth.computablefacts.com/realms/cywise1/protocol/saml">
  <saml:Issuer>https://app.cywise.io/saml/48696b8c-225d-4b98-8102-0f8f9e7f9460/metadata</saml:Issuer>
  <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://auth.computablefacts.com/realms/cywise1</saml:NameID>

</samlp:LogoutRequest>
Mais cela ne pose pas de problème à Keycloak qui déconnecte l'utilisateur :
<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                      xmlns="urn:oasis:names:tc:SAML:2.0:assertion"
                      xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                      Destination="https://app.cywise.io/saml/48696b8c-225d-4b98-8102-0f8f9e7f9460/sls"
                      ID="ID_5b565ed3-43b1-40e0-ba63-300192a37ef6"
                      InResponseTo="ONELOGIN_cc4cc12edbe8e97a440089832082f1c43965836a"
                      IssueInstant="2025-05-07T10:45:48.500Z"
                      Version="2.0">
  <Issuer>https://auth.computablefacts.com/realms/cywise1</Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status>
</samlp:LogoutResponse>

Suite pour Hermès

Les problèmes à résoudre : - l'erreur de signature à la réponse du logout - le NameID qui n'est pas correcte dans la requête du logout (lié au précédent ?) - récupérer le lastname de l'utilisateur - le groupe CYBERBUDDY_ADMIN ne semble pas avoir été donné à Amine

Ma priorité est le NameID.

Logout - envoyer le NameID reçu au login

Je reproduis aussi le problème avec mon Cywise local et l'IdP Keycloak cywise1.

<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                     ID="ONELOGIN_a0c12251f1173a67eeee348b7770b964033a361f"
                     Version="2.0"
                     IssueInstant="2025-05-07T14:02:09Z"
                     Destination="https://auth.computablefacts.com/realms/cywise1/protocol/saml">
  <saml:Issuer>https://weasel-exotic-llama.ngrok-free.app/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/metadata</saml:Issuer>
  <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://auth.computablefacts.com/realms/cywise1</saml:NameID>

</samlp:LogoutRequest>
Le NameID est l'EntityID.

Pas de problème dans la réponse de Keycloak :

<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                      xmlns="urn:oasis:names:tc:SAML:2.0:assertion"
                      xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                      Destination="https://weasel-exotic-llama.ngrok-free.app/saml/2f57bc95-d2fa-486f-8a88-5f6ded3d764f/sls"
                      ID="ID_11bc21d8-c1e5-4352-93f4-925593310cb2"
                      InResponseTo="ONELOGIN_a0c12251f1173a67eeee348b7770b964033a361f"
                      IssueInstant="2025-05-07T14:02:09.283Z"
                      Version="2.0">
  <Issuer>https://auth.computablefacts.com/realms/cywise1</Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status>
</samlp:LogoutResponse>

Alors que le NameID reçu au login est :

<saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">G-ba95d406-2291-4372-abc2-b6ec359375d1</saml:NameID>
Et, comme le permet le format persistent, j'ai le même NameID à chaque login :
<saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">G-ba95d406-2291-4372-abc2-b6ec359375d1</saml:NameID>

J'ai réolu ce problème car je peux passer un NameID à ma requête de logout mais pour ça, il faut que je stocke le NameID reçu au moment du login. J'ai choisi de le stocker dans la session PHP avec :

// Keep NameID to send it back when user logout
session([
    'saml2NameId' => $this->saml2User->getNameId(),
]);
Puis je le passe en paramètre au moment du logout :
$nameId = session('saml2NameId');
return redirect(URL::route('saml.logout', ['uuid' => $uuid, 'nameId' => $nameId]));

Ca fonctionne durant mes tests et ça fonctionne durant les tests de Amine.

Mais, malgré cela, il y a toujours

Récupération du nom

Dans le debug SAML envoyé par Amine, je vois les attributs suivants :

<AttributeStatement>
    <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress">
        <AttributeValue>amine.benayada@ext.hermes.com</AttributeValue>
    </Attribute>
    <Attribute Name="firstname">
        <AttributeValue>Amine</AttributeValue>
    </Attribute>
    <Attribute Name="lastname">
        <AttributeValue>BENAYADA</AttributeValue>
    </Attribute>
    <Attribute Name="http://schemas.xmlsoap.org/claims/Group">
        <AttributeValue>SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO</AttributeValue>
    </Attribute>
</AttributeStatement>
Je vais donc utiliser lastname comme Name pour aller chercher le nom. Et je vois aussi que le groupe SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO n'est pas exactement celui que j'attendais (SG-WORLD-CU-CYBERBUDDY-ADMIN-SSO). Je vais donc modifier mes réglages pour ça aussi.

Réglages avant :

{
  "claims": {
    "email": {
      "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
    }, 
    "name": {
      "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
    }, 
    "role": {
      "friendlyName": "group", 
      "name": "http://schemas.xmlsoap.org/claims/Group"
    }
  }, 
  "roles": {
    "default": ["App\\Models\\Role::CYBERBUDDY_ONLY"], 
    "idp_roles": ["SG-WORLD-CU-CYBERBUDDY-ADMIN-SSO"], 
    "SG-WORLD-CU-CYBERBUDDY-ADMIN-SSO_to_cywise": ["App\\Models\\Role::CYBERBUDDY_ADMIN"]
  }
}

Réglages après :

{
  "claims": {
    "email": {
      "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
    }, 
    "name": {
      "name": "lastname"
    }, 
    "role": {
      "friendlyName": "group", 
      "name": "http://schemas.xmlsoap.org/claims/Group"
    }
  }, 
  "roles": {
    "default": ["App\\Models\\Role::CYBERBUDDY_ONLY"], 
    "idp_roles": ["SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO"], 
    "SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO_to_cywise": ["App\\Models\\Role::CYBERBUDDY_ADMIN"]
  }
}

Changement fait le 2025-05-07 à 18h10. (j'avais mis firstname à la place de lastname et j'ai modifié à nouveau à 18h37)

Reste le problème de la signature invalide

Je veux calculer moi-même la signature d'une réponse logout pour savoir si elle est vraiment invalide ou non.

Je vais prendre un des messages des logs de Amine :

GET
SAMLResponse: fZJLi9swFIX/itFeliw/EgvHUGZKCaQz0Ayz6Ga4lq46BkcyvnIf/75yQhcDJaCNxDn3HH1SR3CZZn0KP8IavyHNwRNmx8cDe6udAdUOJUdbDbySiHyPsuRgmrpWAI0tK5a94kJj8AemcsmyI9GKR08RfExHUtVcprV7KRqtpK53eVO331n2iBRHD/HqfI9xJi0EzHNu/vwaCfMxiK2YkLZVaNqag2uAV3tT8WHfSt64nSygcWXpBkETsexhK76FrovXAWgk7eGCpKPR509fTzr10+Ym0qunGc3oRrSps/9375dwYM9Pn0/PX45Pb0aZQUGhGqtq54yr0FrYuxRZFYMt3ABQNa4oWPb7MnnSV5D34+clxGDCxPruCmq5We+bgAiXDRTrN1CJk0Obv+OShLkJFwHWkUian6NBEnFZKXbiNr/vbs97jhBX+rh7CBazV5hWvJ9PV7U+ryaNT6BF34mPU8X//lD/Fw==
RelayState: https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/logout
Signature: oRoGUu2Q0vF3FIhwqxV2CmCeSpMoGmsFW/7G+actoPvn4FHyfNRUHeFFbxmxkry6/WN++rQRymsxYAQ8iyn3bRs0nwaTeS349D2apTp2nGB7atwLtx4FOvL2tHHK6rqZmYTiBXGCpYV+83xh9wmiMHQqZ/j6Qa5IrOFF2gHvZNPVHkW5lHDDCBEJLpbkhqkBeeVKqyV/r/uX+qDMwNUr0/xGe5alEIUmXfHm8XGGaPJD4vJJZwdo8S7RDMscb0Qn6HPEzPpdv5A3s9bkaZJB2MB/R0EK6kaQfbR4aixN0HeyJM4/hMhMa6B3naQcJNtTD1tH6SvSAL16cSO7K0A9+Q==
SigAlg: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256

Je décode (base64) la réponse SAML, je décompresse et je calcule le hash SHA256 :

> $decoded = base64_decode('fZJLi9swFIX/itFeliw/EgvHUGZKCaQz0Ayz6Ga4lq46BkcyvnIf/75yQhcDJaCNxDn3HH1SR3CZZn0KP8IavyHNwRNmx8cDe6udAdUOJUdbDbySiHyPsuRgmrpWAI0tK5a94kJj8AemcsmyI9GKR08RfExHUtVcprV7KRqtpK53eVO331n2iBRHD/HqfI9xJi0EzHNu/vwaCfMxiK2YkLZVaNqag2uAV3tT8WHfSt64nSygcWXpBkETsexhK76FrovXAWgk7eGCpKPR509fTzr10+Ym0qunGc3oRrSps/9375dwYM9Pn0/PX45Pb0aZQUGhGqtq54yr0FrYuxRZFYMt3ABQNa4oWPb7MnnSV5D34+clxGDCxPruCmq5We+bgAiXDRTrN1CJk0Obv+OShLkJFwHWkUian6NBEnFZKXbiNr/vbs97jhBX+rh7CBazV5hWvJ9PV7U+ryaNT6BF34mPU8X//lD/Fw==')
= b"}ÆKï█0\x14à èÐ^û,?\x12\vÃPfJ\tñ3ð\f│Þf©û«:\x06G2¥r\x1F ¥rB\x17\x03%áì─9¸\x1C}RGpÖf}\n?┬\x1A┐!═┴\x13fÃÃ\x03{½Ø\x01ı\x0E%G[\r╝Æê|Å▓õ`Ü║V\0ì-+û¢ÔBc­\x07ªr╔▓#ÐèGO\x11|LGRı\ªÁ{)\x1A¡ñ«wySÀ▀Y÷ê\x14G\x0F±Û|Åq&-\x04╠sn■³\x1A\t¾1ê¡ÿÉÂUh┌ÜâkÇW{S±a▀JÌ©Ø,áqeÚ\x06A\x13▒ýa+¥à«ïÎ\x01h$ÝßéñúÐþO_O:§Ëµ&ʽº\x19═ÞF┤®│ w´ùp`¤OƒO¤_ÄOoFÖAAí\x1A½jþî½ðZÏ╗\x14Y\x15â-▄\0P5«(X÷¹2yÊWɸÒþ%─`┬─·¯\nj╣Y´øÇ\x08ù\r\x14Ù7PëôCø┐ÒÆä╣\t\x17\x01ÍæH܃úA\x12qY)vÔ6┐´n¤{Ä\x10W·©{\x08\x16│WÿV╝ƒOWÁ>»&ìOáE▀ëÅS┼ ■P \x17"

> $uncompressed =  gzinflate($decoded)
= "<samlp:LogoutResponse ID="_5fca29b3-ed4b-40ee-8e03-ac6552aa6d34" Version="2.0" IssueInstant="2025-05-07T16:20:57.659Z" Destination="https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/sls" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="ONELOGIN_c2cb2a126d25ffcf4edda8ff3341bd1fbaa46f11" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://fed.hermes.com/adfs/services/trust</Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>"

> $sha256hash = hash('sha256', $uncompressed)
= "522e5fc5ef97ee093d9817c04e6bf6e5d4fdfe9c640758f36bd36f506f6ece13"

Je dois maintenant retrouver ce hash dans la signature. Je mets la signature dans un fichier signature.base64 :

oRoGUu2Q0vF3FIhwqxV2CmCeSpMoGmsFW/7G+actoPvn4FHyfNRUHeFFbxmxkry6/WN++rQRymsxYAQ8iyn3bRs0nwaTeS349D2apTp2nGB7atwLtx4FOvL2tHHK6rqZmYTiBXGCpYV+83xh9wmiMHQqZ/j6Qa5IrOFF2gHvZNPVHkW5lHDDCBEJLpbkhqkBeeVKqyV/r/uX+qDMwNUr0/xGe5alEIUmXfHm8XGGaPJD4vJJZwdo8S7RDMscb0Qn6HPEzPpdv5A3s9bkaZJB2MB/R0EK6kaQfbR4aixN0HeyJM4/hMhMa6B3naQcJNtTD1tH6SvSAL16cSO7K0A9+Q==
Je décode le base64 :
base64 -d signature.base64 > signature.der

openssl pkeyutl -verify -in signature.der -pubin -inkey signature_cle_publique.pem -pkeyopt digest:sha256

Bon, j'arrête là. J'ai une signature invalide mais j'ai aussi une signature invalide pour un autre message SAML qui n'a pas provoqué d'erreur dans les logs (le message d'envoi des attributs). Le plus probable est que ma méthode manuelle n'est pas correcte...

J'ai décidé de changer 2 options dans config/saml2.php :

    /*
    |--------------------------------------------------------------------------
    | Signature validation
    |--------------------------------------------------------------------------
    |
    | Set to true if you want to use parameters from $_SERVER to validate the signature.
    |
    */

    'retrieveParametersFromServer' => true,

    'security' => [
        /*
        |--------------------------------------------------------------------------
        | Lowercase Urlencoding.
        |--------------------------------------------------------------------------
        |
        | ADFS URL-Encodes SAML data as lowercase, and the toolkit by default uses
        | uppercase.
        |
        | Turn it True for ADFS compatibility on signature verification
        |
        */

        'lowercaseUrlencoding' => true,
    ],
Nous avons publié ces modifs en PROD le vendredi 09/05/2025 et Amine a confirmé qu'il n'avait plus l'erreur 500 au logout.

Je pense que c'est la première option qui a résolu le problème car la deuxième concerne l'encodage d'un message partant de Cywise pour aller vers l'IdP.

Ajout contraintes sur le mot de passe

Dans la page register, ajouter les infos avec les contraintes sur le mot de passe : minimum 12 caractères, au moins un chiffre, au moins une lettre minuscule, au moins une lettre majuscule, pas de caractères autres.

J'ai créé une classe PasswordRequirements qui donne, pour chaque règle standard et chaque règle custom (nous en avons créé 5), le texte du requirement et la condition javascript qui correspond. Après modification du template Blade de la page register, cela permet d'afficher les contraintes à l'écran puis de les mettre en rouge ou en vert en fonction du respect de chaque contraintes avec le mot de passe saisi.

Il y a 2 pages où l'utilisateur peut faire une demande de changement de mot de passe : - depuis la phase de login avec "Mot de passe oublié ?" => /password/reset - une fois connecté avec le menu "Réinitialisation du mot de passe" => /reset-password

NOTA : pas de page "Profil" où il pourrait modifier son mot de passe

Dans les 2 cas, cela doit envoyer un mail avec un lien vers la page de reset /password/reset/{id}?email={email}. Pour la page /password/reset, le mail est envoyé mais pas pour la page /reset-password.

J'ai compris pourquoi le mail ne partait pas pour la page /reset-password : un middleware protège la méthode du controller et l'utilisateur ne doit pas être connecté pour que l'email parte. J'ai donc modifié le comportement du menu "Réinitialisation du mot de passe" : je déconnecte l'utilisateur puis je le redirige vers la page de reset du mot de passe.

J'ajoute une invitation ce qui me permet de récupérer un lien du type : /pub/invitation/{id}. C'est la librairie Konekt (konekt/appshell) qui gère cette fonctionnalité. Le template Blade de la page se trouve dans resources/views/vendor/appshell/public-invitation/show.blade.php.

A mettre aussi dans la page invitation.

Logo sur la page invitation

C'est le logo de Towerify qui s'affiche et pas celui de Cywise. Pourquoi ? Le titre n'est pas correct dans la barre de titre. Pourquoi ?

J'ai testé mais je n'ai pas de logo et le titre prend bien en compte le réglage de mon .env pour la clé APP_NAME. Il est possible que le bug ait été dû à l'envoi du .env local dans l'image Docker que j'ai corrigé récemment...

Hermès - Problème avec Microsoft Edge

Amine est revenu vers moi car le SSO ne fonctionne pas avec Microsoft Edge.

Logs Microsoft Edge envoyés par Amine
[
    {
        "guid": "659cc3a9-ac62-52b4-48e0-4d27484c742b",
        "requestId": "38364",
        "timestamp": "2025-05-12T14:41:31.192Z",
        "url": "https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/sls",
        "httpMethod": "GET",
        "cookieRequest": [
            "XSRF-TOKEN=eyJpdiI6ImxCZ0wzcGpEd0N1cXg1dDN5ek5SeHc9PSIsInZhbHVlIjoia2lNUHdmT2FZTGpMK3ByS3dKYnA2ZGY5MG9BNWltSFJQanJwTmhDRzhaSzlScjJ3UGZXMEpqRGRqWmMvR0VSWFFGU2NHTXN6eEtkcGk2dFkra1BjTVhoZUErK0ZaM1pHeFIwbGMwS3Znc1VRSDBYSTRzMDhwRks1MlZKSjhiZVUiLCJtYWMiOiJkNmY3NTM1MzMzNzE5MmNjZmYxOWNmZWE1YWI3MGZjMzU0MDEzMzBiMTRlMzIyYjFjMTg4MmE0YWIxYTQ5ODNkIiwidGFnIjoiIn0%3D; cywise_session=eyJpdiI6IlRxVnJ5TnNiNk5jc1AzMEdrbGZxVGc9PSIsInZhbHVlIjoiNTMyTUcyMkVMb3dkbUZzb3NiUzc2UGxVZFFoSUNwQ0dNZWNqS2RhSjRETERNa3R5eGJsZENZY3d4VUVvdDBSZ2VENjk5TWFPbGZ1OTRjZ2hxMUVZeFRRUk5taDJHMzNHM2xEK2Uyak5QQVVsM0RYZFJLVXhYOEovckpPRnNRaDQiLCJtYWMiOiJhMjQ0OTUxM2Q3N2ZhODQ1MDEwNWRkNmQ2NDIyNTQwZDJkNzdhNWJjNjZkNTdjOTliOGNkNTk2YjVmZTU2ZGM4IiwidGFnIjoiIn0%3D"
        ],
        "cookieResponse": [
            "cywise_session=eyJpdiI6ImZPcW4wczBobnhhZDRlUVBHNnJjOFE9PSIsInZhbHVlIjoiVVpBZ2JMY3hUaDNnRmlOS2lTV05obXFaZXlPU0NlQk41Nkt2RDF0bnNyS0V6MlJrVkFRRG5DRkNJbkxsVjVsQy8yd2VoTk10Y1hnck14OTdTU09VTVRpYjV2Q1A3ZitWRzJsUEovWk9XUFFCeDhwRktjbkVwVzBEMzVPbTdKWVMiLCJtYWMiOiI0Nzk2MDkxOWI1NThiMzUwZGVkYzUyMmQ0NDg1NjlkZTJkMDIyNDc3M2ViNWQ0NDE4NTI2ZGMyNTZiMmIzOGVhIiwidGFnIjoiIn0%3D; expires=Mon, 12 May 2025 16:41:31 GMT; Max-Age=7200; path=/; domain=cywise.io; httponly"
        ],
        "protocol": "SAML",
        "messageType": "Response",
        "messageVerb": "logoutresponse",
        "message": "<samlp:LogoutResponse ID=\"_f9fef065-19e1-44ed-b6f4-b90df6f3736e\" Version=\"2.0\" IssueInstant=\"2025-05-12T14:41:31.162Z\" Destination=\"https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/sls\" Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" InResponseTo=\"ONELOGIN_ce8a59b9b3389335b159355e372ead1bffe6416c\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://fed.hermes.com/adfs/services/trust</Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Requester\" /></samlp:Status></samlp:LogoutResponse>",
        "state": "https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/logout",
        "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
        "signature": "LGgjfDFT9jkkTP7SApdrDsC+JniBH14V96HnaEJ2nvMDHmuuuOWZbEQ9NbeNhooJMT5O48Pe3yc6CKfb+GSBFVR/+HeWDX2ItIXQcxC0tQRlPcG+iX+K2rhENR8RSzGoRDNp0KhqJMHry6JVxN/Z3hennMRGz/WENoQOiyDHBsLgnwCR9qm7oy8SGWt0aTQbhtV5/mmU7Aq3UihWNDKdTuAdIqW+ERhxU/THh5HbUrAm/lbXuMxxDO5XHgiMVQ0ghOVEr3DJck2iPQM8Vt1+dE0kNN9oYhASjgJOPsX+ynJojEs80E9KZjHtPoeYm7j63ufDx3A3KNWHizMips2Izw=="
    },
    {
        "guid": "86b8716a-4007-35f2-6f19-a44fa30dc6bd",
        "requestId": "38364",
        "timestamp": "2025-05-12T14:41:31.158Z",
        "url": "https://fed.hermes.com/adfs/ls/",
        "httpMethod": "GET",
        "cookieRequest": [
            "SamlLogout=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhP09ORUxPR0lOX2QyZjVmZTAwMGVhMjdmOWE2ZjNjZmU0ZTZkZWM2OGU5NDY2ZTNiNGE/aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZmxvZ291dD91cm4lM2FhdXRoMCUzYWN5YmVydmFkaXMtcHJvZCUzYUdTUTdNSTZRUVEyMFlTTVhaOU41JkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mJiYmJl8xYThhYmRmYy03MjU5LTRhZTYtOGNhYS0zYzU5NGFjY2RmY2EmXzc1YWUzMjBjLTM0MzYtNDI4Ny1iNjk2LTU4MTFmM2JkMTcwYSZfZDlmNzg3YmMtODEzMC00OTRlLTgwZGYtYjFkMTFlYjE4MGUwJl9mZTgwNGVlMS05MjNjLTRhMDMtOWVmNy1hNmJhNmVlYWJmNmQmX2JjOWE0YWNmLWU3NDAtNDhiMC04YThlLTYxZjhhMWU4NmJhYSZfMzM5YjAxYzUtZDliYy00NGM0LTllNGEtNTc3YjNiYTAzZGRhJl9mZjg0NGQ0OC02ZTZkLTRhZDEtYjE5MC00YmU0MmI2ZGEyYWU/XzI4MDhhODQwLTM1NTMtNGQwZS1iMTEyLTA4ZjIyMjZlN2M1OT91cm4lM2FvYXNpcyUzYW5hbWVzJTNhdGMlM2FTQU1MJTNhMi4wJTNhc3RhdHVzJTNhU3VjY2Vzcw==; MSISSignoutProtocol=U2FtbA==; MSISAuth=AAEAAGfufZvnuPj4k/3ZlXVbEOSU4pJrkOii3+sEPKbSX11ESqhvljM620lxmq+vh433bWzQVAPHVRevwWCr3oZ+oUH0fuXtUUaCSfh2KBdkFu61u2DwNpVkVJD1kKhGArNbZeEF9Mga5DTIueucGB4pxZJjk60yNuZaXrz2GUtKRP2qaeJWtqk1DuzvKYB5USGHEZxfjQXtKCeWVIjKQ7rFqiQ0KlA+W4Fsf6EjkeHN3VjOgjo82tQSSkes554gxR84OLUHh1+E/F5h6cWqmsOcig4KSA52dkiBKbBhW7nnY4pAAfesCmtfdJ4SpYtvTEui9Whc3itePc9bH6ElFX3UGpbKcgVH7AbwPjPPSbOEvJpCD29woUL/ENs+Bll6HG+/7QABAACOc5XE1qq8pA0U4yw6a6hjoQ6sEbd3VvIf/jzwGO5NUrlmeq/0uH9B8DTPUnvc9eeWHod2NRyx6L+/GXO/G9q2RKd30hmhws8ND8ujuHFrjC8U6TK+bBuTIr/e79CwGwoz6/fkcw/C/4gf3V26SWBYvLcDwBy1C2hlhzXw6H23di8Ke9WSYCLpC3Uevg83xuCl6BxKOwJ/zxOMItUN+gnZdodMykw8S9TLv9V0Et7eLoy73aHNZEps/of/qEQTo93+wV2m9tvQijIOwctpWcwIAZxDXBSBXViNB4N2ErItEOnWfcHtYYnXi4hgpJR9wN51HHbrYpSVN2QI9DkZ7dFcoAMAAPgZKLQp9Mnr8H2XmP5fBYQPEEduT1JmRpH24COGE7AgenYikQFGZi7oBCugmcfqCTzKZW6I+ZmM0Pkaebg/eRg3SdJY3E0GXtJ0LL2vpf2+Kr31bgNP2u5vCyAPvBitUTMgQa2rcAXesCvvvXvzejn0+oam5Be3/kwIwYv5B3H6wvGFBOpQbUIR8z1oTyYitCjqQgDA3d4J6vPgmj9lnK9WlQ/TbeWB650hX6oMq/ILdRscNJPJfbpakFXupdoriLHo5RIjbz8TT5+coIozEsUOEvpWtX3LDclHZMCLL1MlBPPUBi5LIUAvakXXbMXzWO0FjuaBb3xC58tAZv72E3ZmdTrcVK70/mAqL2HJVsi4Gzi35G32FLAkQuFsfvh/xNYlgDuZabwpAuHB/KevWaY76M3v7CnAuvRou9ekYBlTP6Nj5JOgg2O+U8OgBVA1ZxgExDl/v0G118k9GK2TjAWsbd9tZjP2iRcLSBWKleJdv0Q+CawWm/J61egi3d03ES2JbYu6biNu/XKxQ/rsGvx0zjZQpP9FLF+eFnAWbXXN3Icsuzjte9f37wG5Tp6hbwdZUDndkZDtXsmn+N1SZJYZOUuObb7MXgZnlEGpu8K6ruJUOGgwXdQ9EcFjuZpXzJF4AVX6l2aQumDF/nnMWZvzb66rpJ3kqOKI3CDm/YN395kLnbdPKx+755yYsp8iJirD7EPY6jvCWKTeiBIROIwZOQA1hu1ww9brk9queSsEZCPkQGjUOuJxWqDop/Stewq5R6i8Q6O6HJiAVfWAwXwcCOyYQoJT34PjNreVE3qbT0zOGTTuxRhwnjcaDCivD42fWC5CtrDbXuQeV+ex+sdFOvCqj02YNvVb7dLLrYFI+9UURyp3fpr/syxlftNL/CqOunCH0h9U3OnmVzaOzMu24F286sYaEsDv+nYXQHrRboGhoywBvnoe73xNfPjwWBMlU6NJ8Otl4qRpFxeyY4LB53Giw5j338AsvxXo7L3lbjP+YCdD9Ucv+P/IPT2wfr+e+KtAzcwKC5F89JLxqu2hc8mLz0SZZQaGwDT3afVqsE1jSDiKsb+G5J6HBNKZwDJTLk9RF6YCotBIEVaq6EL+m2v+bgXALLC3ZKWbxqE7YSkeDf5W5Frw4RtjjPo8CLzZUqOyChO7vDyp1s+DPz72A5uEYOnbhQZcns9KbkL62L4KSqh/Lm3JzDVGDD4jn6dhGlBu5/VajT+Kn43owCY=; SamlSession=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhJkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mdXJuJTNhb2FzaXMlM2FuYW1lcyUzYXRjJTNhU0FNTCUzYTIuMCUzYW5hbWVpZC1mb3JtYXQlM2FwZXJzaXN0ZW50JiYmJl8yMjM5ZDNhNi04OTY5LTQzZjYtODVjNi05NDIyMzIwZTI3N2UmXzgxM2E0MjFiLTUwNjEtNGViNy04ZjYyLTI3ZTUyZmIxMmNlZCZfZTA5MTZiMjgtNWI0NC00ZGEyLTkyNTktZTllMWQ5NWE3NmI2; MSISAuthenticated=NS8xMi8yMDI1IDI6NDE6MjIgUE0=; MSISLoopDetectionCookie=MjAyNS0wNS0xMjoxNDo0MToyMlpcMQ==; rskxRunCookie=0; rCookie=a9habht7tm711246vvsuosqm8h42wgg; lastRskxRun=1742465769640; datadome=d234~ExLTPjxG2_MIfDRxaJs~Zk~oNvIG8vK3MgnIRN4tBdFp8jj3Ov8BTHQd0s1LVwaZlphOsA~ZROskHMF3U7Q0nNww~FYpWBCdZX80x5wSSZ1MgYmtKC45hYcHGI0"
        ],
        "cookieResponse": [
            "MSISAuth=; expires=Sun, 11 May 2025 14:41:31 GMT; path=/adfs; Secure; SameSite=None",
            "MSISAuthenticated=; expires=Sun, 11 May 2025 14:41:31 GMT; path=/adfs; Secure; SameSite=None",
            "SamlSession=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhJkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mdXJuJTNhb2FzaXMlM2FuYW1lcyUzYXRjJTNhU0FNTCUzYTIuMCUzYW5hbWVpZC1mb3JtYXQlM2FwZXJzaXN0ZW50JiYmJl8yMjM5ZDNhNi04OTY5LTQzZjYtODVjNi05NDIyMzIwZTI3N2UmXzgxM2E0MjFiLTUwNjEtNGViNy04ZjYyLTI3ZTUyZmIxMmNlZCZfZTA5MTZiMjgtNWI0NC00ZGEyLTkyNTktZTllMWQ5NWE3NmI2; path=/adfs; HttpOnly; Secure; SameSite=None",
            "SamlLogout=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhP09ORUxPR0lOX2QyZjVmZTAwMGVhMjdmOWE2ZjNjZmU0ZTZkZWM2OGU5NDY2ZTNiNGE/aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZmxvZ291dD91cm4lM2FhdXRoMCUzYWN5YmVydmFkaXMtcHJvZCUzYUdTUTdNSTZRUVEyMFlTTVhaOU41JkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mJiYmJl8xYThhYmRmYy03MjU5LTRhZTYtOGNhYS0zYzU5NGFjY2RmY2EmXzc1YWUzMjBjLTM0MzYtNDI4Ny1iNjk2LTU4MTFmM2JkMTcwYSZfZDlmNzg3YmMtODEzMC00OTRlLTgwZGYtYjFkMTFlYjE4MGUwJl9mZTgwNGVlMS05MjNjLTRhMDMtOWVmNy1hNmJhNmVlYWJmNmQmX2JjOWE0YWNmLWU3NDAtNDhiMC04YThlLTYxZjhhMWU4NmJhYSZfMzM5YjAxYzUtZDliYy00NGM0LTllNGEtNTc3YjNiYTAzZGRhJl9mZjg0NGQ0OC02ZTZkLTRhZDEtYjE5MC00YmU0MmI2ZGEyYWU/XzI4MDhhODQwLTM1NTMtNGQwZS1iMTEyLTA4ZjIyMjZlN2M1OT91cm4lM2FvYXNpcyUzYW5hbWVzJTNhdGMlM2FTQU1MJTNhMi4wJTNhc3RhdHVzJTNhU3VjY2Vzcw==; expires=Mon, 12 May 2025 14:51:31 GMT; path=/adfs; HttpOnly; Secure; SameSite=None"
        ],
        "protocol": "SAML",
        "messageType": "Request",
        "messageVerb": "logoutrequest",
        "message": "<samlp:LogoutRequest\n    xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"\n    xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\"\n    ID=\"ONELOGIN_ce8a59b9b3389335b159355e372ead1bffe6416c\"\n    Version=\"2.0\"\n    IssueInstant=\"2025-05-12T14:41:31Z\"\n    Destination=\"https://fed.hermes.com/adfs/ls/\">\n    <saml:Issuer>https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/metadata</saml:Issuer>\n    <saml:NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">amine.benayada@ext.hermes.com</saml:NameID>\n    \n</samlp:LogoutRequest>",
        "state": "https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/logout",
        "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
        "signature": "AssFsKX6HQNKq1xojquFDG2r7r0sntlLXzA+fw+PWIlb7qonRxm/3PypUha1F6i28/9N3NCUHd4J4BE3x7xgVBWHebYtxum+xfs4Jt7po4Wd/LgcG5DUTU4C6BFIWUjT9z27MZMQSgOaMFrBebJ5trKXrIKMkxMsdnGhxHLX5KRNJoWRuW9D1P3+5z2RnqvhKTnPsfR1Z2VAXfDnkTjZPClRkdzAAznUWr7EQI+22xKWEMm1/SLnZJEeSmtYGTSsGRNwiUBm5FTphvpPbOHKI2RJSMd95LFqFqYbguuk1WBD+NGJPc5d89qvwB4xObaTVU3fQptrkXge3D2ihfw56w=="
    },
    {
        "guid": "bbb9ae26-231c-b9e3-a4d3-b3e8b1bf0b58",
        "requestId": "38310",
        "timestamp": "2025-05-12T14:41:22.622Z",
        "url": "https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/acs",
        "httpMethod": "POST",
        "cookieRequest": [
            "XSRF-TOKEN=eyJpdiI6IkpIRDhrVWpGYTNlVlRHRWZLK3ZPSVE9PSIsInZhbHVlIjoiN3Bqb0pLZWVUb1VpTFhya3d1NEJoeitvNVVNQXZVMlIwb0tGZUNHVW11UUlmL3NpYStRWnFVV1c5eFYrbjVYdXdIam0wb0NJUHZMZ21BMDc5bGp4MDNlZ28zUVdCRkQ1RHRUUVBpbGlPV29Bc1Q3UTVmT1hSUHk3YWZWdkdTL1YiLCJtYWMiOiI5NjAxMjhhNDk0MjU2YmQ0ZDg4MWRkZjY4MTczZGFhODE5MmYxMWQ5OWU5N2UyMzUzZDAxZDg0MDNiM2ZiYWY1IiwidGFnIjoiIn0%3D; cywise_session=eyJpdiI6ImRPdjRXc2RLd2JFRm1BbGxJSWJrcGc9PSIsInZhbHVlIjoiaXFrcDNocGdUNEFYK1J3STZGZDlKNVpiQU5jWmpsWDZoLzRrS3JMWHF2MUZTa2xEeFRvNjFHYkVGd1h1bzV0YTlyVnBnZlQ1MkRXbC9Qd01Ca0VlS3poWm9hUFdjU2hsQXViSWVnSGFKTXR5bkJ3c3pJcmRLTWZlSGFDYWo4ZGwiLCJtYWMiOiIwNTBhM2JmMDg2MjQ0ODcxMTZmYzIxZmI2ZTk3MjE2YzRmYzdkNmY5YzRkZjJjN2I3ZWQ1YTc4OGUzMmRmNDg2IiwidGFnIjoiIn0%3D"
        ],
        "cookieResponse": [
            "cywise_session=eyJpdiI6ImFBTFNsalIxdHZxUXVaY29kc3QrQVE9PSIsInZhbHVlIjoiNFFwT21hUnB4UVgrYUxBSEp5MytSTFdtSVI0bHN6VU1xQlpYYkFmd1NTRG5vNnIxY3o1RmxXK3R5TU5qZ09FSEdORTloU0lJbkdINnI2NExMb0tBWjc0bzVjV2pLQUdhcm1kamxCTGtud09DbFRNZndFaUg4YUxITjJyd1RvOGYiLCJtYWMiOiJkNjE5MDhmNzE3YWQyNzZlZDBhYTg1ZWIyMTcxOGExMDc2MzEzOTc2NTZiMThiMTJiMTM2MWQ3YmY1ZTU4MTI1IiwidGFnIjoiIn0%3D; expires=Mon, 12 May 2025 16:41:22 GMT; Max-Age=7200; path=/; domain=cywise.io; httponly"
        ],
        "protocol": "SAML",
        "messageType": "Response",
        "messageVerb": "response",
        "message": "<samlp:Response ID=\"_f7728bbc-bc1d-4464-965f-f1505e964c2c\" Version=\"2.0\" IssueInstant=\"2025-05-12T14:41:22.569Z\" Destination=\"https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/acs\" Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" InResponseTo=\"ONELOGIN_7bc388c175dffb87f691ba83f313492e94be52f8\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://fed.hermes.com/adfs/services/trust</Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" /></samlp:Status><Assertion ID=\"_e0916b28-5b44-4da2-9259-e9e1d95a76b6\" IssueInstant=\"2025-05-12T14:41:22.569Z\" Version=\"2.0\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\"><Issuer>http://fed.hermes.com/adfs/services/trust</Issuer><ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\" /><ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\" /><ds:Reference URI=\"#_e0916b28-5b44-4da2-9259-e9e1d95a76b6\"><ds:Transforms><ds:Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\" /><ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\" /></ds:Transforms><ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\" /><ds:DigestValue>aVqkQJUGU1ACSVXRRYdb8j82RtIY33pnGbx3KAKZZ/0=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>ckU0c8pK+7aG7K8DDrRExCPRSQoP8Uc7Akx0ZPUogZ4USRtdCkuzmP7J6iwTPJ5bc3da4i3k1KOxX8PjVdSa/afl37jbcpCMe1OV3M706g9JDIWCeITGgBnKukCH1BjQIdcZsjnGrFqa3jyVabWYyJvgGHridIVKVkTINcJLpu8YHwmTPTF8uRqPJ0aG1dxgkvGQoShFQ9W7eHCZ4Tur0fc54O6yPfksbytORTAWBMXhYqWuK5BIczPVAiRy9Pmey3uw9/58/0m4hcCCvdQhHub+hmv36eSIHYuh8j6xpsuKYCIGxdJFbY08wnNtneH6TGQKcmg5mSl1LcvrcgcsdA==</ds:SignatureValue><KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><ds:X509Data><ds:X509Certificate>MIIC2DCCAcCgAwIBAgIQJh/EANNXJb5LBvCDXcXOwzANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQDEx1BREZTIFNpZ25pbmcgLSBmZWQuaGVybWVzLmNvbTAeFw0yMzA5MjcwOTM2MjBaFw0yODA5MjcwOTM2MjBaMCgxJjAkBgNVBAMTHUFERlMgU2lnbmluZyAtIGZlZC5oZXJtZXMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwZT8DsXjzQO02YpXZ99PmIXanvWp+dDsP0SQmMJMHoKgsmj5gDlYohkCXlt2K9hYVa84drBYxLZLmY3MotGVoQhwDV/vHIMJOzys2IKMBtHjsPPYVML9tFJRZPtMIxH3Dzp1sPDiDmY67IZhgcEiEzlIuAcsToTUNSZjR+1kd+4j5nuf5NBtV0dc499yOs3s8q4rk4R0fd2BwC2QawBvu2iTT8xsBX8i4N3N3kLhJeSSgHUIg7QZNPNmo+cs6HfGehGhn2jcfAotYLRZfV8V15RK5DIXgruzqGVOgE91twSQKGepFxVrEdgGwszyzEyzaqBkcU3nTl2SEQ3yX7B9uQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBPfTe/w4or4QV0ffysfXBiuisGu7ThZRU6stJ+pO1Y6aYXyM2Xe4PmTKwY0evkSJmLrNWlqRpoNEcR3H3fCDHaMKVWV+/4QJ3W5Xd1bPwX0qYYyHJxn4TkH2q0cmidjYWP1KxL/57uCbzpdwtKBrA/dOi+MlFJ9O1GhAuKk/BC22iXNoROQ6nfL8tJdvWMx2I7HI5CrDZzoYPtH+liJP05rfpSf31FnkKIedPb6FHiSgTkM44XUB9nqml0DPLWvxTXJ4XYmHGmCI9REOTY7YUy7U0wevV7w9elXcr1T3NSDj2y8/avZVhRgjsKsq2DWwewGT5PUazU7MIz6Yq806vF</ds:X509Certificate></ds:X509Data></KeyInfo></ds:Signature><Subject><NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">amine.benayada@ext.hermes.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"ONELOGIN_7bc388c175dffb87f691ba83f313492e94be52f8\" NotOnOrAfter=\"2025-05-12T14:46:22.569Z\" Recipient=\"https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/acs\" /></SubjectConfirmation></Subject><Conditions NotBefore=\"2025-05-12T14:41:22.553Z\" NotOnOrAfter=\"2025-05-12T15:41:22.553Z\"><AudienceRestriction><Audience>https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/metadata</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><AttributeValue>amine.benayada@ext.hermes.com</AttributeValue></Attribute><Attribute Name=\"firstname\"><AttributeValue>Amine</AttributeValue></Attribute><Attribute Name=\"lastname\"><AttributeValue>BENAYADA</AttributeValue></Attribute><Attribute Name=\"http://schemas.xmlsoap.org/claims/Group\"><AttributeValue>SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO</AttributeValue></Attribute></AttributeStatement><AuthnStatement AuthnInstant=\"2025-05-12T14:41:22.522Z\" SessionIndex=\"_e0916b28-5b44-4da2-9259-e9e1d95a76b6\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef></AuthnContext></AuthnStatement></Assertion></samlp:Response>",
        "state": "https://app.cywise.io/home",
        "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
        "signature": "ckU0c8pK+7aG7K8DDrRExCPRSQoP8Uc7Akx0ZPUogZ4USRtdCkuzmP7J6iwTPJ5bc3da4i3k1KOxX8PjVdSa/afl37jbcpCMe1OV3M706g9JDIWCeITGgBnKukCH1BjQIdcZsjnGrFqa3jyVabWYyJvgGHridIVKVkTINcJLpu8YHwmTPTF8uRqPJ0aG1dxgkvGQoShFQ9W7eHCZ4Tur0fc54O6yPfksbytORTAWBMXhYqWuK5BIczPVAiRy9Pmey3uw9/58/0m4hcCCvdQhHub+hmv36eSIHYuh8j6xpsuKYCIGxdJFbY08wnNtneH6TGQKcmg5mSl1LcvrcgcsdA=="
    },
    {
        "guid": "92a0e45f-df2c-3bd9-98ca-d7028ca2e34f",
        "requestId": "38308",
        "timestamp": "2025-05-12T14:41:22.552Z",
        "url": "https://fed.hermes.com/adfs/ls/",
        "httpMethod": "GET",
        "cookieRequest": [
            "SamlLogout=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhP09ORUxPR0lOX2QyZjVmZTAwMGVhMjdmOWE2ZjNjZmU0ZTZkZWM2OGU5NDY2ZTNiNGE/aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZmxvZ291dD91cm4lM2FhdXRoMCUzYWN5YmVydmFkaXMtcHJvZCUzYUdTUTdNSTZRUVEyMFlTTVhaOU41JkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mJiYmJl8xYThhYmRmYy03MjU5LTRhZTYtOGNhYS0zYzU5NGFjY2RmY2EmXzc1YWUzMjBjLTM0MzYtNDI4Ny1iNjk2LTU4MTFmM2JkMTcwYSZfZDlmNzg3YmMtODEzMC00OTRlLTgwZGYtYjFkMTFlYjE4MGUwJl9mZTgwNGVlMS05MjNjLTRhMDMtOWVmNy1hNmJhNmVlYWJmNmQmX2JjOWE0YWNmLWU3NDAtNDhiMC04YThlLTYxZjhhMWU4NmJhYSZfMzM5YjAxYzUtZDliYy00NGM0LTllNGEtNTc3YjNiYTAzZGRhJl9mZjg0NGQ0OC02ZTZkLTRhZDEtYjE5MC00YmU0MmI2ZGEyYWU/XzI4MDhhODQwLTM1NTMtNGQwZS1iMTEyLTA4ZjIyMjZlN2M1OT91cm4lM2FvYXNpcyUzYW5hbWVzJTNhdGMlM2FTQU1MJTNhMi4wJTNhc3RhdHVzJTNhU3VjY2Vzcw==; MSISSignoutProtocol=U2FtbA==; SamlSession=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhJkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mdXJuJTNhb2FzaXMlM2FuYW1lcyUzYXRjJTNhU0FNTCUzYTIuMCUzYW5hbWVpZC1mb3JtYXQlM2FwZXJzaXN0ZW50JiYmJl8yMjM5ZDNhNi04OTY5LTQzZjYtODVjNi05NDIyMzIwZTI3N2UmXzgxM2E0MjFiLTUwNjEtNGViNy04ZjYyLTI3ZTUyZmIxMmNlZA==; MSISLoopDetectionCookie=MjAyNS0wNS0xMjoxNDozOTo0NVpcMQ==; MSISAuth=AAEAAGGMr+TFC7DD35hqDaD89eMRfd+/Ln1A43nD26Ep2Ps7U+HAdo8eBvJb7laTKc9v844+XmISZ9H8rG61cg1MP3iwqcHsSoj8C5cgWomcR3ntLNpQIBa1Bf56Ze/piMhFqEqfW7JtKdzY9tuwy4nFJtg96psu+zhoO5wQpPyOZx1dscsb12SNi23jMpGBxldubpeZVp/BA0t1easEE4xnDlWCDED20Xv1UzAPCfglLb3E6RirpXkTlVgtgaiAIJUsyuWRdlI54oW5Pi+QtkuHkh0nlUNzNP58XZICyY7HJhQ67WGkLwhKGVXHWDTmI3Y/rRXiYiDfgzejn2nithlFb/XKcgVH7AbwPjPPSbOEvJpCD29woUL/ENs+Bll6HG+/7QABAAAiktI/gi3OKh1rZUGpX4whnMl1Y6GFt+qh3iwG20a6vLVbylmk1S9t7ujcbL62FwepOMVfz7MMsDIxdLHgPG3A0BEvO/BZnjj1M12wG/po1XmAXRBdIzIpoCwv1dkJKRmqRYjn0TYy1vBeAdbKOsSWTc7pxSv1Y32iVImMvX3gXIK5CaUOP2hrXNiiFJ1BotrNMoFl9C2rFMpvih8+h/Amlw24cr3Bes28b9slKFz0foQ5Ch58KD2BHLv9o9dyWHf8MQaBzMEVu1G8GQ/alLjbaTJh/DrJf2TaSU1s29pFx5Sd0PApvH2XY5D7xW/CMF0RPArDmuB5fM24pQwAb3OqoAMAAAgjDon8s5lJVmhviQYY3bK6WtyX6eGM8puDxcwkIBNbONGRmCaw8npqPJxmstf/ue/IuD/ydFvr+4v1YNVM0NjoOq6y9mQ6B5Sf+MXt5vDA20/KB0U6UoRae4R5yrb7mkAksOEp/NpT0GhhzfEiayYYH1OfOmCBrv+kqBivD6tZz7HooqLcY3pRs3LvzSNJU64jyX8KTd4EJ2q9DhPW4tmddxptSE49BYk6UxaM3SxJ3NP8vhs3oMb/1px8/6daRWxYLpTWVPyk3n6qgdoUUEi46JY+7JVK826/ropV59xuZkDK6lm85auEvSkGsUCEorrOndbkh9NDvFc3n62T243TGl7YnENcT/93rnrd1uaVlaWhla87kN1VtWL17mq+AJU71TzxtGO2Shky7cXF6L45qPdNQahT9KxB4abc4T4Xkiin82tdMTSLu4BYA8fWKjzDsoAtKdTnycp+/qB2hSlCj6nzTjaPQYcJowtenUhzimXRCvw2NQ7pQ6RjQfoM9v5ZvdFN8kt014TIHV3bb8d7WXS8GLjbkMShT/1PFZ1r9BIS7y8J64jIng/UsN+cuDYGUvH358Mj+hr8w+oonxqsPr7ZxiTCbHrHm3ISJ/a0m3XxzN0X7vwxdhTKy7anwH95HpfGW3RA2EZQw/39FgBwnzS+3jVRa8aN9nx2gCrReYTFXjcrkSHNAT+RSYORsoeAzyAS5DsqCQmj7PDosItmlg54cWlRDMxcOE8V84G0FZvvUYqg/4G1WQBSg8xAbmoJbaLcPWPembSW069KgF2P4zsuj4vYE/YW/H4hum3Mc5L0lfQZONFyXKLRW4LqN2tAo0oYKJDOPhAp7xh0EnXZ4x6MYgAdDRfxjgMm1ZeyMoEmVzpoGFhdOWJwF0G9Iqxaf7RNhjTI5fGQof309j3j1CpGR9zu+XcR7xC1wr0OWRmYHeOMOwtU/UOH7oXjqqbvGmdvDborHjhIcUNdr/uz4h/a6y3GifJ0+538zOYfN8nUM7TVzS2ODB51hUtM88mmAzgjeucwcpHI8BoJ9gJDbJA+ZfxTU8gNEyqk4eoBaiRxQ+RZlp6wTNCPoQmeJtHEUEG4XsRLYx8bhCSDFVM6tMx2YtWRawVgCdxYtbVYD1iWe9HCg7N5Y2vudupeK7SMjb/U2Ac2/L5NtYwPTnnemukV76ae+Rvt89DtUIgXCaqJPBGzGKHG3QMxfWzLTxlAE4c6LcrPR9KMoiiHoSE=; rskxRunCookie=0; rCookie=a9habht7tm711246vvsuosqm8h42wgg; lastRskxRun=1742465769640; datadome=d234~ExLTPjxG2_MIfDRxaJs~Zk~oNvIG8vK3MgnIRN4tBdFp8jj3Ov8BTHQd0s1LVwaZlphOsA~ZROskHMF3U7Q0nNww~FYpWBCdZX80x5wSSZ1MgYmtKC45hYcHGI0"
        ],
        "cookieResponse": [
            "MSISAuth=AAEAAGfufZvnuPj4k/3ZlXVbEOSU4pJrkOii3+sEPKbSX11ESqhvljM620lxmq+vh433bWzQVAPHVRevwWCr3oZ+oUH0fuXtUUaCSfh2KBdkFu61u2DwNpVkVJD1kKhGArNbZeEF9Mga5DTIueucGB4pxZJjk60yNuZaXrz2GUtKRP2qaeJWtqk1DuzvKYB5USGHEZxfjQXtKCeWVIjKQ7rFqiQ0KlA+W4Fsf6EjkeHN3VjOgjo82tQSSkes554gxR84OLUHh1+E/F5h6cWqmsOcig4KSA52dkiBKbBhW7nnY4pAAfesCmtfdJ4SpYtvTEui9Whc3itePc9bH6ElFX3UGpbKcgVH7AbwPjPPSbOEvJpCD29woUL/ENs+Bll6HG+/7QABAACOc5XE1qq8pA0U4yw6a6hjoQ6sEbd3VvIf/jzwGO5NUrlmeq/0uH9B8DTPUnvc9eeWHod2NRyx6L+/GXO/G9q2RKd30hmhws8ND8ujuHFrjC8U6TK+bBuTIr/e79CwGwoz6/fkcw/C/4gf3V26SWBYvLcDwBy1C2hlhzXw6H23di8Ke9WSYCLpC3Uevg83xuCl6BxKOwJ/zxOMItUN+gnZdodMykw8S9TLv9V0Et7eLoy73aHNZEps/of/qEQTo93+wV2m9tvQijIOwctpWcwIAZxDXBSBXViNB4N2ErItEOnWfcHtYYnXi4hgpJR9wN51HHbrYpSVN2QI9DkZ7dFcoAMAAPgZKLQp9Mnr8H2XmP5fBYQPEEduT1JmRpH24COGE7AgenYikQFGZi7oBCugmcfqCTzKZW6I+ZmM0Pkaebg/eRg3SdJY3E0GXtJ0LL2vpf2+Kr31bgNP2u5vCyAPvBitUTMgQa2rcAXesCvvvXvzejn0+oam5Be3/kwIwYv5B3H6wvGFBOpQbUIR8z1oTyYitCjqQgDA3d4J6vPgmj9lnK9WlQ/TbeWB650hX6oMq/ILdRscNJPJfbpakFXupdoriLHo5RIjbz8TT5+coIozEsUOEvpWtX3LDclHZMCLL1MlBPPUBi5LIUAvakXXbMXzWO0FjuaBb3xC58tAZv72E3ZmdTrcVK70/mAqL2HJVsi4Gzi35G32FLAkQuFsfvh/xNYlgDuZabwpAuHB/KevWaY76M3v7CnAuvRou9ekYBlTP6Nj5JOgg2O+U8OgBVA1ZxgExDl/v0G118k9GK2TjAWsbd9tZjP2iRcLSBWKleJdv0Q+CawWm/J61egi3d03ES2JbYu6biNu/XKxQ/rsGvx0zjZQpP9FLF+eFnAWbXXN3Icsuzjte9f37wG5Tp6hbwdZUDndkZDtXsmn+N1SZJYZOUuObb7MXgZnlEGpu8K6ruJUOGgwXdQ9EcFjuZpXzJF4AVX6l2aQumDF/nnMWZvzb66rpJ3kqOKI3CDm/YN395kLnbdPKx+755yYsp8iJirD7EPY6jvCWKTeiBIROIwZOQA1hu1ww9brk9queSsEZCPkQGjUOuJxWqDop/Stewq5R6i8Q6O6HJiAVfWAwXwcCOyYQoJT34PjNreVE3qbT0zOGTTuxRhwnjcaDCivD42fWC5CtrDbXuQeV+ex+sdFOvCqj02YNvVb7dLLrYFI+9UURyp3fpr/syxlftNL/CqOunCH0h9U3OnmVzaOzMu24F286sYaEsDv+nYXQHrRboGhoywBvnoe73xNfPjwWBMlU6NJ8Otl4qRpFxeyY4LB53Giw5j338AsvxXo7L3lbjP+YCdD9Ucv+P/IPT2wfr+e+KtAzcwKC5F89JLxqu2hc8mLz0SZZQaGwDT3afVqsE1jSDiKsb+G5J6HBNKZwDJTLk9RF6YCotBIEVaq6EL+m2v+bgXALLC3ZKWbxqE7YSkeDf5W5Frw4RtjjPo8CLzZUqOyChO7vDyp1s+DPz72A5uEYOnbhQZcns9KbkL62L4KSqh/Lm3JzDVGDD4jn6dhGlBu5/VajT+Kn43owCY=; path=/adfs; HttpOnly; Secure; SameSite=None",
            "MSISAuth=; expires=Sun, 11 May 2025 14:41:22 GMT; path=/adfs/ls; Secure; SameSite=None",
            "SamlSession=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhJkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mdXJuJTNhb2FzaXMlM2FuYW1lcyUzYXRjJTNhU0FNTCUzYTIuMCUzYW5hbWVpZC1mb3JtYXQlM2FwZXJzaXN0ZW50JiYmJl8yMjM5ZDNhNi04OTY5LTQzZjYtODVjNi05NDIyMzIwZTI3N2UmXzgxM2E0MjFiLTUwNjEtNGViNy04ZjYyLTI3ZTUyZmIxMmNlZCZfZTA5MTZiMjgtNWI0NC00ZGEyLTkyNTktZTllMWQ5NWE3NmI2; path=/adfs; HttpOnly; Secure; SameSite=None",
            "MSISAuthenticated=NS8xMi8yMDI1IDI6NDE6MjIgUE0=; path=/adfs; HttpOnly; Secure; SameSite=None",
            "MSISLoopDetectionCookie=MjAyNS0wNS0xMjoxNDo0MToyMlpcMQ==; path=/adfs; HttpOnly; Secure; SameSite=None"
        ],
        "protocol": "SAML",
        "messageType": "Request",
        "messageVerb": "authnrequest",
        "message": "<samlp:AuthnRequest\n    xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"\n    xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\"\n    ID=\"ONELOGIN_7bc388c175dffb87f691ba83f313492e94be52f8\"\n    Version=\"2.0\"\n    ProviderName=\"ComputableFacts\"\n    IssueInstant=\"2025-05-12T14:41:05Z\"\n    Destination=\"https://fed.hermes.com/adfs/ls/\"\n    ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n    AssertionConsumerServiceURL=\"https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/acs\">\n    <saml:Issuer>https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/metadata</saml:Issuer>\n    <samlp:NameIDPolicy\n        Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\"\n        AllowCreate=\"true\" />\n    <samlp:RequestedAuthnContext Comparison=\"exact\">\n        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>\n    </samlp:RequestedAuthnContext>\n</samlp:AuthnRequest>",
        "state": "https://app.cywise.io/home",
        "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
        "signature": "sXpOsDJHdDZwiZa2mvzN5c8gjdipNbT1OBnHA/ZFL1tVdhvt/u0aDRGfVJ2DzjX629NoD0GAb87kDjb3EblIfICOibAeOKmPv0HgCNyx2JtFR8qqUD2jkPgfNdhGwNE16VPaIIcMs8RpHCvawd6NSo63teW4C+IM5VhGH+rI0D1LTkugGAam0ruVR0gdLT6R7tyq/lYBm5iem+C0dfkHxmsF83I7ccB3F/hAojIjj0igwBamaGqebk2QsHuqpdX8uqOxfpBipqAnmG4QHsXyTAWE5tWsjlGmt3a0YuXiSAjCMCPjewgfnv50i4q5EwkZVVvp+XLV8LnQJyq26y2w6g=="
    },
    {
        "guid": "b0a0e4ff-12e3-c5e4-b517-4daf30bb3910",
        "requestId": "38290",
        "timestamp": "2025-05-12T14:41:05.439Z",
        "url": "https://fed.hermes.com/adfs/ls/",
        "httpMethod": "GET",
        "cookieRequest": [
            "SamlLogout=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhP09ORUxPR0lOX2QyZjVmZTAwMGVhMjdmOWE2ZjNjZmU0ZTZkZWM2OGU5NDY2ZTNiNGE/aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZmxvZ291dD91cm4lM2FhdXRoMCUzYWN5YmVydmFkaXMtcHJvZCUzYUdTUTdNSTZRUVEyMFlTTVhaOU41JkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mJiYmJl8xYThhYmRmYy03MjU5LTRhZTYtOGNhYS0zYzU5NGFjY2RmY2EmXzc1YWUzMjBjLTM0MzYtNDI4Ny1iNjk2LTU4MTFmM2JkMTcwYSZfZDlmNzg3YmMtODEzMC00OTRlLTgwZGYtYjFkMTFlYjE4MGUwJl9mZTgwNGVlMS05MjNjLTRhMDMtOWVmNy1hNmJhNmVlYWJmNmQmX2JjOWE0YWNmLWU3NDAtNDhiMC04YThlLTYxZjhhMWU4NmJhYSZfMzM5YjAxYzUtZDliYy00NGM0LTllNGEtNTc3YjNiYTAzZGRhJl9mZjg0NGQ0OC02ZTZkLTRhZDEtYjE5MC00YmU0MmI2ZGEyYWU/XzI4MDhhODQwLTM1NTMtNGQwZS1iMTEyLTA4ZjIyMjZlN2M1OT91cm4lM2FvYXNpcyUzYW5hbWVzJTNhdGMlM2FTQU1MJTNhMi4wJTNhc3RhdHVzJTNhU3VjY2Vzcw==; MSISSignoutProtocol=U2FtbA==; SamlSession=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhJkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mdXJuJTNhb2FzaXMlM2FuYW1lcyUzYXRjJTNhU0FNTCUzYTIuMCUzYW5hbWVpZC1mb3JtYXQlM2FwZXJzaXN0ZW50JiYmJl8yMjM5ZDNhNi04OTY5LTQzZjYtODVjNi05NDIyMzIwZTI3N2UmXzgxM2E0MjFiLTUwNjEtNGViNy04ZjYyLTI3ZTUyZmIxMmNlZA==; MSISLoopDetectionCookie=MjAyNS0wNS0xMjoxNDozOTo0NVpcMQ==; rskxRunCookie=0; rCookie=a9habht7tm711246vvsuosqm8h42wgg; lastRskxRun=1742465769640; datadome=d234~ExLTPjxG2_MIfDRxaJs~Zk~oNvIG8vK3MgnIRN4tBdFp8jj3Ov8BTHQd0s1LVwaZlphOsA~ZROskHMF3U7Q0nNww~FYpWBCdZX80x5wSSZ1MgYmtKC45hYcHGI0"
        ],
        "protocol": "SAML",
        "messageType": "Request",
        "messageVerb": "authnrequest",
        "message": "<samlp:AuthnRequest\n    xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"\n    xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\"\n    ID=\"ONELOGIN_7bc388c175dffb87f691ba83f313492e94be52f8\"\n    Version=\"2.0\"\n    ProviderName=\"ComputableFacts\"\n    IssueInstant=\"2025-05-12T14:41:05Z\"\n    Destination=\"https://fed.hermes.com/adfs/ls/\"\n    ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n    AssertionConsumerServiceURL=\"https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/acs\">\n    <saml:Issuer>https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/metadata</saml:Issuer>\n    <samlp:NameIDPolicy\n        Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\"\n        AllowCreate=\"true\" />\n    <samlp:RequestedAuthnContext Comparison=\"exact\">\n        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>\n    </samlp:RequestedAuthnContext>\n</samlp:AuthnRequest>",
        "state": "https://app.cywise.io/home",
        "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
        "signature": "sXpOsDJHdDZwiZa2mvzN5c8gjdipNbT1OBnHA/ZFL1tVdhvt/u0aDRGfVJ2DzjX629NoD0GAb87kDjb3EblIfICOibAeOKmPv0HgCNyx2JtFR8qqUD2jkPgfNdhGwNE16VPaIIcMs8RpHCvawd6NSo63teW4C+IM5VhGH+rI0D1LTkugGAam0ruVR0gdLT6R7tyq/lYBm5iem+C0dfkHxmsF83I7ccB3F/hAojIjj0igwBamaGqebk2QsHuqpdX8uqOxfpBipqAnmG4QHsXyTAWE5tWsjlGmt3a0YuXiSAjCMCPjewgfnv50i4q5EwkZVVvp+XLV8LnQJyq26y2w6g=="
    },
    {
        "guid": "04266403-d13b-4599-ffdb-520b2907cde6",
        "requestId": "38286",
        "timestamp": "2025-05-12T14:41:05.286Z",
        "url": "https://fed.hermes.com/adfs/ls/",
        "httpMethod": "GET",
        "cookieRequest": [
            "SamlLogout=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhP09ORUxPR0lOX2QyZjVmZTAwMGVhMjdmOWE2ZjNjZmU0ZTZkZWM2OGU5NDY2ZTNiNGE/aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZmxvZ291dD91cm4lM2FhdXRoMCUzYWN5YmVydmFkaXMtcHJvZCUzYUdTUTdNSTZRUVEyMFlTTVhaOU41JkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mJiYmJl8xYThhYmRmYy03MjU5LTRhZTYtOGNhYS0zYzU5NGFjY2RmY2EmXzc1YWUzMjBjLTM0MzYtNDI4Ny1iNjk2LTU4MTFmM2JkMTcwYSZfZDlmNzg3YmMtODEzMC00OTRlLTgwZGYtYjFkMTFlYjE4MGUwJl9mZTgwNGVlMS05MjNjLTRhMDMtOWVmNy1hNmJhNmVlYWJmNmQmX2JjOWE0YWNmLWU3NDAtNDhiMC04YThlLTYxZjhhMWU4NmJhYSZfMzM5YjAxYzUtZDliYy00NGM0LTllNGEtNTc3YjNiYTAzZGRhJl9mZjg0NGQ0OC02ZTZkLTRhZDEtYjE5MC00YmU0MmI2ZGEyYWU/XzI4MDhhODQwLTM1NTMtNGQwZS1iMTEyLTA4ZjIyMjZlN2M1OT91cm4lM2FvYXNpcyUzYW5hbWVzJTNhdGMlM2FTQU1MJTNhMi4wJTNhc3RhdHVzJTNhU3VjY2Vzcw==; MSISSignoutProtocol=U2FtbA==; SamlSession=aHR0cHMlM2ElMmYlMmZhcHAuY3l3aXNlLmlvJTJmc2FtbCUyZjBkOTJlYzk1LWFmNmEtNDhjNC1iODkwLTZmNzAxYTZmMzNmYiUyZm1ldGFkYXRhJkZhbHNlJmFtaW5lLmJlbmF5YWRhJTQwZXh0Lmhlcm1lcy5jb20mdXJuJTNhb2FzaXMlM2FuYW1lcyUzYXRjJTNhU0FNTCUzYTIuMCUzYW5hbWVpZC1mb3JtYXQlM2FwZXJzaXN0ZW50JiYmJl8yMjM5ZDNhNi04OTY5LTQzZjYtODVjNi05NDIyMzIwZTI3N2UmXzgxM2E0MjFiLTUwNjEtNGViNy04ZjYyLTI3ZTUyZmIxMmNlZA==; MSISLoopDetectionCookie=MjAyNS0wNS0xMjoxNDozOTo0NVpcMQ==; rskxRunCookie=0; rCookie=a9habht7tm711246vvsuosqm8h42wgg; lastRskxRun=1742465769640; datadome=d234~ExLTPjxG2_MIfDRxaJs~Zk~oNvIG8vK3MgnIRN4tBdFp8jj3Ov8BTHQd0s1LVwaZlphOsA~ZROskHMF3U7Q0nNww~FYpWBCdZX80x5wSSZ1MgYmtKC45hYcHGI0"
        ],
        "protocol": "SAML",
        "messageType": "Request",
        "messageVerb": "authnrequest",
        "message": "<samlp:AuthnRequest\n    xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"\n    xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\"\n    ID=\"ONELOGIN_7bc388c175dffb87f691ba83f313492e94be52f8\"\n    Version=\"2.0\"\n    ProviderName=\"ComputableFacts\"\n    IssueInstant=\"2025-05-12T14:41:05Z\"\n    Destination=\"https://fed.hermes.com/adfs/ls/\"\n    ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n    AssertionConsumerServiceURL=\"https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/acs\">\n    <saml:Issuer>https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/metadata</saml:Issuer>\n    <samlp:NameIDPolicy\n        Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\"\n        AllowCreate=\"true\" />\n    <samlp:RequestedAuthnContext Comparison=\"exact\">\n        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>\n    </samlp:RequestedAuthnContext>\n</samlp:AuthnRequest>",
        "state": "https://app.cywise.io/home",
        "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
        "signature": "sXpOsDJHdDZwiZa2mvzN5c8gjdipNbT1OBnHA/ZFL1tVdhvt/u0aDRGfVJ2DzjX629NoD0GAb87kDjb3EblIfICOibAeOKmPv0HgCNyx2JtFR8qqUD2jkPgfNdhGwNE16VPaIIcMs8RpHCvawd6NSo63teW4C+IM5VhGH+rI0D1LTkugGAam0ruVR0gdLT6R7tyq/lYBm5iem+C0dfkHxmsF83I7ccB3F/hAojIjj0igwBamaGqebk2QsHuqpdX8uqOxfpBipqAnmG4QHsXyTAWE5tWsjlGmt3a0YuXiSAjCMCPjewgfnv50i4q5EwkZVVvp+XLV8LnQJyq26y2w6g=="
    }
]
Logs de Cywise correspondants
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:16 +0000] "POST /logalert/LkCmJX3Fkq6ERg7z HTTP/1.0" 200 1169 "-" "LogAlert/1.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:18 +0000] "POST /logalert/LtAgoGdgm2JAN5tAk6CRzkOVTpRkgR HTTP/1.0" 200 1169 "-" "LogAlert/1.0.0"
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] User Attributes with Friendly Name:  
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] User Attributes with Name: {"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress":["amine.benayada@ext.hermes.com"],"firstname":["Amine"],"lastname":["BENAYADA"],"http://schemas.xmlsoap.org/claims/Group":["SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO"]} 
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] User email found: "amine.benayada@ext.hermes.com" (from name "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")  
[2025-05-12 14:41:22] prod.INFO: [SAML2 Authentication] User email found: "amine.benayada@ext.hermes.com"  
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] User name found: "BENAYADA" (from name "lastname")  
[2025-05-12 14:41:22] prod.INFO: [SAML2 Authentication] User name found: "BENAYADA"  
[2025-05-12 14:41:22] prod.INFO: [SAML2 Authentication] Role attribute NOT found from friendly name "group"  
[2025-05-12 14:41:22] prod.INFO: [SAML2 Authentication] Role attribute found from name "http://schemas.xmlsoap.org/claims/Group"  
[2025-05-12 14:41:22] prod.INFO: [SAML2 Authentication] Role value=SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO  
[2025-05-12 14:41:22] prod.INFO: [SAML2 Authentication] User already exist, we update attributes  
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] Default roles = ["App\\Models\\Role::CYBERBUDDY_ONLY"] 
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] SAML roles = ["SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO"] 
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] IdP roles settings = ["SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO"] 
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] Cywise roles for SG-WORLD-APP-CU-CYBERBUDDY-ADMIN-SSO = ["App\\Models\\Role::CYBERBUDDY_ADMIN"] 
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] New roles = ["App\\Models\\Role::CYBERBUDDY_ONLY","App\\Models\\Role::CYBERBUDDY_ADMIN"] 
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] User roles IDs = [9,10] 
[2025-05-12 14:41:22] prod.DEBUG: [SAML2 Authentication] User roles IDs updated = {"attached":[],"detached":[],"updated":[]} 
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:22 +0000] "POST /saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/acs HTTP/1.0" 302 1070 "https://fed.hermes.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:22 +0000] "GET /home HTTP/1.0" 302 1556 "https://fed.hermes.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:22 +0000] "GET /home?tab=ama HTTP/1.0" 200 6441 "https://fed.hermes.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:23 +0000] "GET /cyber-buddy/chat?conf=%7B%22title%22%3A%22CyberBuddy%22%2C%22aboutText%22%3Anull%2C%22aboutLink%22%3A%22https%3A%2F%2Fapp.cywise.io%22%2C%22userId%22%3A%22106%22%2C%22chatServer%22%3A%22%2Fbotman%22%2C%22bubbleAvatarUrl%22%3A%22%2Fimages%2Ficons%2Fcyber-buddy.svg%22%2C%22frameEndpoint%22%3A%22%2Fcyber-buddy%2Fchat%22%2C%22introMessage%22%3A%22Bonjour!%20Je%20suis%20votre%20cyber%20assistant.%20Que%20puis-je%20faire%20pour%20vous%3F%22%2C%22desktopHeight%22%3A900%2C%22desktopWidth%22%3A1200%2C%22mainColor%22%3A%22%2347627F%22%2C%22bubbleBackground%22%3A%22%2300264b%22%2C%22headerTextColor%22%3A%22white%22%7D HTTP/1.0" 200 1862 "https://app.cywise.io/home?tab=ama" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:23 +0000] "GET /adversary_meter/src/blueprintjs/blueprintjs/blueprint-select.css.map HTTP/1.0" 404 6826 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:23 +0000] "GET /adversary_meter/src/blueprintjs/blueprintjs/blueprint-popover2.css.map HTTP/1.0" 404 6826 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:23 +0000] "GET /adversary_meter/src/blueprintjs/blueprintjs/blueprint.css.map HTTP/1.0" 404 6826 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:23 +0000] "GET /adversary_meter/src/blueprintjs/blueprintjs/blueprint-datetime.css.map HTTP/1.0" 404 6826 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:23 +0000] "GET /adversary_meter/src/blueprintjs/blueprintjs/blueprint-icons.css.map HTTP/1.0" 404 6826 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:23 +0000] "GET /adversary_meter/src/blueprintjs/blueprintjs/table.css.map HTTP/1.0" 404 6826 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
[2025-05-12 14:41:31] prod.DEBUG: [SAML2 Authentication] Logout {"uuid":"0d92ec95-af6a-48c4-b890-6f701a6f33fb","email":"amine.benayada@ext.hermes.com","nameId":"amine.benayada@ext.hermes.com"} 
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:31 +0000] "POST /logout HTTP/1.0" 302 1931 "https://app.cywise.io/home?tab=ama" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:31 +0000] "GET /saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/logout?nameId=amine.benayada%40ext.hermes.com HTTP/1.0" 302 1285 "https://app.cywise.io/home?tab=ama" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
[2025-05-12 14:41:31] prod.ERROR: saml2.error_detail {"uuid":"0d92ec95-af6a-48c4-b890-6f701a6f33fb","error":null} 
[2025-05-12 14:41:31] prod.ERROR: saml2.error ["logout_not_success"] 
[2025-05-12 14:41:31] prod.ERROR: Symfony\Component\HttpFoundation\Response::setContent(): Argument #1 ($content) must be of type ?string, Illuminate\Routing\Redirector given, called in /var/www/html/vendor/laravel/framework/src/Illuminate/Http/Response.php on line 82 {"userId":106,"exception":"[object] (TypeError(code: 0): Symfony\\Component\\HttpFoundation\\Response::setContent(): Argument #1 ($content) must be of type ?string, Illuminate\\Routing\\Redirector given, called in /var/www/html/vendor/laravel/framework/src/Illuminate/Http/Response.php on line 82 at /var/www/html/vendor/symfony/http-foundation/Response.php:418)
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:31 +0000] "GET /saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/sls?SAMLResponse=fVJLS8QwEP4rJfc2TdNmN6FbEBVZWBVUPHiRNJ1ooZvUTurj35vu4mFB9jjD98o3qVHvh1Ht%2fJufwwPg6B1Csr3akFcrLdhcVCmTwNKyhC5thS3TVuadFZavuACSPMOEvXcbUmQ5SbaIM2wdBu1CXOVFleZRoHhipSqZ4ixjonghyRVg6J0OB%2bZ7CCMqSvU4Zubnq0fIek%2bXYDTvZAFGVqm2Qqfl2kT7tcxTYVc50zEEty3FAUlyuQRfTOfJKa%2bxR%2bX0HlAFox4vbncq5lPmCFKzwxFMb3voYmb39%2b4nvyH3d9e7%2b5vt3auBta5kK1vO15LzqmWV5FUFfFWA7lhrLYiSCUOS7%2f3gUB2KPG8%2fTj544wfS1IeipiP1PEkjwrQURZqlqNiThS57hykCM%2bP3VHcWacR89gaQhmnGUNOjflMfz%2fsYdJjxdLr0HSTPepjhvD8e0OoBPuZ4NJhIQpuanurS%2f35R8ws%3d&RelayState=https%3a%2f%2fapp.cywise.io%2fsaml%2f0d92ec95-af6a-48c4-b890-6f701a6f33fb%2flogout&Signature=LGgjfDFT9jkkTP7SApdrDsC%2bJniBH14V96HnaEJ2nvMDHmuuuOWZbEQ9NbeNhooJMT5O48Pe3yc6CKfb%2bGSBFVR%2f%2bHeWDX2ItIXQcxC0tQRlPcG%2biX%2bK2rhENR8RSzGoRDNp0KhqJMHry6JVxN%2fZ3hennMRGz%2fWENoQOiyDHBsLgnwCR9qm7oy8SGWt0aTQbhtV5%2fmmU7Aq3UihWNDKdTuAdIqW%2bERhxU%2fTHh5HbUrAm%2flbXuMxxDO5XHgiMVQ0ghOVEr3DJck2iPQM8Vt1%2bdE0kNN9oYhASjgJOPsX%2bynJojEs80E9KZjHtPoeYm7j63ufDx3A3KNWHizMips2Izw%3d%3d&SigAlg=http%3a%2f%2fwww.w3.org%2f2001%2f04%2fxmldsig-more%23rsa-sha256 HTTP/1.0" 500 7304 "https://app.cywise.io/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:31 +0000] "POST /logalert/ezQbPB8ANHtyEGgBzdEA HTTP/1.0" 200 1169 "-" "LogAlert/1.0.0"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:34 +0000] "GET /home HTTP/1.0" 302 1521 "-" "FreshpingBot/1.0 (+https://freshping.io/)"
10.0.6.31:80 10.0.0.2 - - [12/May/2025:14:41:34 +0000] "GET /login HTTP/1.0" 200 3233 "-" "FreshpingBot/1.0 (+https://freshping.io/)"

J'ai répondu à Amine :


J'ai bien retrouvé la trace de votre test qui a échoué hier à 14:41 UTC. C'est la phase de logout qui ne fonctionne pas :

[2025-05-12 14:41:31] prod.ERROR: saml2.error_detail {"uuid":"0d92ec95-af6a-48c4-b890-6f701a6f33fb","error":null}
[2025-05-12 14:41:31] prod.ERROR: saml2.error ["logout_not_success"]

Cela ne m'aide pas à comprendre l'origine du problème.

J'ai comparé les messages "LogoutRequest" envoyés par mon application :

Logs Edge :

<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                     ID="ONELOGIN_ce8a59b9b3389335b159355e372ead1bffe6416c"
                     Version="2.0"
                     IssueInstant="2025-05-12T14:41:31Z"
                     Destination="https://fed.hermes.com/adfs/ls/"
                     >
    <saml:Issuer>https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/metadata</saml:Issuer>
    <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">amine.benayada@ext.hermes.com</saml:NameID>
</samlp:LogoutRequest>

Logs Chrome :

<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                     xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                     ID="ONELOGIN_9d5260879903e743b71f04dec9b6eadf379052fb"
                     Version="2.0"
                     IssueInstant="2025-05-12T14:25:02Z"
                     Destination="https://fed.hermes.com/adfs/ls/"
                     >
    <saml:Issuer>https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/metadata</saml:Issuer>
    <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">amine.benayada@ext.hermes.com</saml:NameID>
</samlp:LogoutRequest>

Les deux me semblent identiques et, notamment, mon application envoie bien le NameID, votre email, dans les 2 cas.

Mais la réponse renvoyée ("LogoutResponse") par votre ADFS n'est pas la même.

Pour Chrome, j'ai un statut "Success" :

<samlp:LogoutResponse ID="_7c38d1bc-8840-4075-9a76-5d09a7e6d21b"
                      Version="2.0"
                      IssueInstant="2025-05-12T14:25:02.304Z"
                      Destination="https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/sls"
                      Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"
                      InResponseTo="ONELOGIN_9d5260879903e743b71f04dec9b6eadf379052fb"
                      xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                      >
    <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://fed.hermes.com/adfs/services/trust</Issuer>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
    </samlp:Status>
</samlp:LogoutResponse>

Alors que pour Edge, j'ai un statut "Requester" :

<samlp:LogoutResponse ID="_f9fef065-19e1-44ed-b6f4-b90df6f3736e"
                      Version="2.0"
                      IssueInstant="2025-05-12T14:41:31.162Z"
                      Destination="https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/sls"
                      Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"
                      InResponseTo="ONELOGIN_ce8a59b9b3389335b159355e372ead1bffe6416c"
                      xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                      >
    <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://fed.hermes.com/adfs/services/trust</Issuer>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Requester" />
    </samlp:Status>
</samlp:LogoutResponse>
Je pense que ce statut "Requester" indique que l'ADFS a trouvé un problème dans la requête de logout. Je suppose que ce problème doit se trouver dans les logs du serveur ADFS. Pourriez-vous regarder dans les logs du serveur et me dire s'il y a un message d'erreur qui nous mettrait sur une piste ?

Hermès - Point du 2025-05-14

Finalement, le problème avec Edge se situe au moment du logout et est dû à la déconnexion d'un autre service (Cybervadis). En effet, au moment de la déconnexion de CyberBuddy, je déclenche la déconnexion de l'IdP qui déclenche la déconnexion des autres services.

Sur ce point, nous avons décidé que la déconnexion de CyberBuddy ne doit pas déconnecter l'utilisateur de l'IdP.

Amine m'a montré un autre problème : s'il se connecte en premier à Cybervadis puis qu'il se connecte ensuite à CyberBuddy, alors l'ADFS lui demande de saisir à nouveau son login et son mot de passe. Avec le SSO, l'ADFS ne devrait pas le demander puisque l'utilisateur est déjà authentifié.

Amine a tenté la connexion à CyberBuddy en premier puis à Cybervadis et, dans ce sens, l'ADFS ne demande pas le login et le mot de passe : le SSO fonctionne.

Dans les logs SAML Tracer qu'Amine m'a envoyés, je retrouve les LogoutRequest. De Cybervadis :

<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                    Destination="https://fed.hermes.com/adfs/ls/"
                    ID="_f39561983567d69bef91f2181c7b0029"
                    IssueInstant="2025-05-14T09:45:10Z"
                    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
                    Version="2.0"
                    >
    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:auth0:cybervadis-prod:GSQ7MI6QQQ20YSMXZ9N5</saml:Issuer>
</samlp:AuthnRequest>

De CyberBuddy :

<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                    ID="ONELOGIN_f57bdb6ca1db02817a102920c7d52d7301ee02be"
                    Version="2.0"
                    ProviderName="ComputableFacts"
                    IssueInstant="2025-05-14T09:45:25Z"
                    Destination="https://fed.hermes.com/adfs/ls/"
                    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
                    AssertionConsumerServiceURL="https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/acs"
                    >
    <saml:Issuer>https://app.cywise.io/saml/0d92ec95-af6a-48c4-b890-6f701a6f33fb/metadata</saml:Issuer>
    <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
                        AllowCreate="true"
                        />
    <samlp:RequestedAuthnContext Comparison="exact">
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
    </samlp:RequestedAuthnContext>
</samlp:AuthnRequest>

Cela pourrait être le contexte PasswordProtectedTransport ajouté par CyberBuddy qui provoque le demande du login et du mot de passe par l'ADFS. A creuser.

Enfin, nous reparlons des sous-domaines à autoriser et je confirme que je peux les autoriser tous avec quelque chose du genre *.hermes.com

2025-05-14 - regex pour le domaine

J'ai modifié le code pour supporter une regex dans les colonnes domain et alt_domain1 de ma table saml2_tenants. Si le texte commence par ~, je considère que c'est une regex, sinon je fais une simple comparaison (===).

Cette modification a été poussée en PROD le 2025-05-14.

2025-05-15 - Pas de SLO

Le fait de demander à l'IdP de déconnecter l'utilisateur s'appelle le Single LogOut (SLO).

Je vais ajouter une option dans le JSON de configuration d'un Tenant SAML pour activer ou non le SLO.

J'ai ajouté l'option logoutSlo, false par défaut. J'ai validé que cela fonctionne sur mon poste en local.

Pas besoin de changer la configuration en PROD pour Hermès puisque l'option est à false par défaut.

TODO : s'assurer de supprimer saml2NameId de la session quand l'utilisateur se déconnecte.

J'ai changé le code qui met fin à la session Laravel en cas de SLO et la session est bien détruite maintenant.

Cette modification à été poussée en PROD le 2025-05-15.

2025-05-15 - SSO sans demande de login / mot de passe

Je ne reproduis pas le problème avec Keycloak cywise1.

D'après cet article Microsoft, le contexte PasswordProtectedTransport n'est pas supporté.

J'ai modifié une option de ma lib SAML pour ajouter d'autres possibilités :

'requestedAuthnContext' => [
    'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
    'urn:oasis:names:tc:SAML:2.0:ac:classes:X509',
    'urn:federation:authentication:windows',
    'urn:oasis:names:tc:SAML:2.0:ac:classes:Unspecified',
],

J'ai testé que le SSO fonctionnait toujours avec Keycloak cywise1 et c'est le cas mais je ne peux pas vérifier pour l'ADFS de Hermès.

Cette modification à été poussée en PROD le 2025-05-15.