Europe/London
Mar 2026
Career
2 min read

Setting up dual SAML authentication in Drupal 10 with two identity providers

The samlauth module, custom route subscribers, and the session handling edge case that took a day to find.

Most SAML guides assume a single identity provider. A client's platform, which I worked on, needed two — one for internal staff, one for external delegates — each with different attribute mappings and session lifetimes. Here's the implementation.

Module setup

The samlauth contrib module supports multiple IdP configurations from Drupal 10.1 onwards via config entities. Each IdP gets its own config entity with its own certificate, SSO URL, and attribute mapping.

# Install
composer require drupal/samlauth
drush en samlauth -y

# Two config entities:
# saml_idp.internal_staff
# saml_idp.external_delegates

The attribute mapping problem

Internal staff authenticate through an Azure AD IdP that sends mail and displayName. External delegates authenticate through a custom IdP that sends email and cn. Different attribute names for the same concepts — and the samlauth module needs to know which is which per IdP.

Key insight: Always log the full SAML response during development. Use the samlauth debug mode or add temporary \Drupal::logger calls in your EventSubscriber. The attribute names your IdP sends rarely match what you expect, and silent mapping failures are the most common cause of "user logged in but has no roles."

The session edge case

When a user authenticates via IdP A and then visits a route that triggers IdP B's assertion, Drupal's session handler merges the assertions by default, which corrupts the role assignment. The fix is a custom EventSubscriber on SamlauthUserSyncEvent that checks which IdP originated the assertion before applying roles.

class SamlRoleSubscriber implements EventSubscriberInterface {
  public static function getSubscribedEvents(): array {
    return [SamlauthEvents::USER_SYNC => 'onUserSync'];
  }

  public function onUserSync(SamlauthUserSyncEvent $event): void {
    $idp_id = $event->getSamlData()['idp_entity_id'] ?? '';

    if (str_contains($idp_id, 'internal-staff')) {
      $event->getAccount()->addRole('internal_staff');
    }
    elseif (str_contains($idp_id, 'external')) {
      $event->getAccount()->addRole('external_delegate');
    }
  }
}

"SAML problems are always either a certificate issue, a clock skew issue, or an attribute mapping issue. Usually all three."