← All security articles
CRITICALWashington+

SAML SSO Misconfiguration & Authentication Bypass

Domain 12: Identity & SSO·

A misconfigured SAML integration is not a weak login; it is a complete authentication bypass. The failures that matter are structural — accepting unsigned assertions, failing to validate the issuer or audience, leaving the recipient URL unchecked, or leaving local authentication enabled as a fallback — and each lets an attacker present a forged identity that the instance accepts as genuine. Any one of them defeats every access control downstream, because the platform never doubts who it thinks you are.

What This Is

Security Assertion Markup Language (SAML) is the primary Single Sign-On (SSO) protocol used by ServiceNow customers. When properly configured, SAML delegates authentication to a trusted Identity Provider (IdP) — Azure AD, Okta, Ping Identity, ADFS — eliminating the need for ServiceNow-local passwords. When misconfigured, SAML becomes a complete authentication bypass, allowing attackers to forge identities and gain unrestricted access to the instance.

ServiceNow SAML Architecture

User → ServiceNow SP (Service Provider)
         → Redirect to IdP (Identity Provider)
            → User authenticates at IdP (MFA, password, etc.)
         ← IdP sends SAML Assertion (signed XML)
      ← ServiceNow validates assertion, creates session
User → Authenticated access to ServiceNow

Where SAML Is Configured in ServiceNow

SAML SSO is Configure-by-Record on Zurich, not property-driven. The configuration lives in per-IdP records spanning two tables, plus the certificate keystore. (Verified on Zurich Patch 6 PDI 2026-04-29.)

Configuration Table / Field What It Controls
Identity Provider master sso_properties (label: "Identity Providers") One row per IdP — name, active, default, is_primary, show_as_login_option, sso_label, sso_script (reference to per-IdP user-provisioning script), user_field
SAML2 IdP extension saml2_update1_properties (label: "Identity Provider", 47 fields) Per-IdP SAML config: idp, idp_authnrequest_url, idp_logout_url, audience, clock_skew, auto_provision, auto_update_user, encrypt_assertion, require_signed_authnrequest, require_signed_logoutrequest, force_authn, is_passive, nameid_policy, transform_map, x509_certificate
IdP signing certificate sys_certificate referenced from saml2_update1_properties.x509_certificate IdP X.509 cert for assertion-signature validation. Assertion-signature validation is implicit when this field is bound — there is no separate "validate signature" toggle
Assertion encryption keystore sys_certificate referenced from saml2_update1_properties.encryption_key_alias + encryption_key_password SP private key for decrypting encrypted assertions
Multi-Provider master toggle glide.authenticate.multisso.enabled (sys_property, REAL_DEFAULT=false) + glide.authenticate.multissov2_feature.enabled Enables multisso plumbing
Per-IdP user-provisioning script sso_properties.sso_script reference → sys_script_include row JIT user creation + role/group mapping logic — NOT a property
SAML debug glide.authenticate.sso.saml2.debug (sys_property, REAL_DEFAULT=false) The only legitimate SAML debug toggle

Common mistake: the glide.authenticate.sso.* and glide.authenticate.sso.user_attribute.* namespaces commonly cited in older guides do not exist on Zurich (verified NOT_FOUND 2026-04-29 across 17 names). Risk-narrative semantics are correct (signature validation, audience restriction, JIT auto-provisioning are all real attack surfaces) but the controls live as per-IdP record fields, not global system properties.

Critical SAML Configuration Parameters

Each parameter is a field on saml2_update1_properties unless noted. Apply the secure setting to every IdP record, not just one — multisso typically runs multiple IdPs in parallel.

Parameter Real location Secure Setting Risk If Misconfigured
Assertion-signature validation x509_certificate reference (sys_certificate) Bound to a non-expired IdP cert Unsigned assertions can be forged when this is empty
AuthnRequest signing requirement require_signed_authnrequest (boolean) true IdP-spoofing risk if false
LogoutRequest signing requirement require_signed_logoutrequest (boolean) true Logout-injection risk if false
Audience restriction audience (string) Set to SP entity ID Assertions from other SPs accepted when empty
Clock skew tolerance clock_skew (integer, default 180 sec) ≤120 sec Wider window enables replay
Force re-authentication force_authn (boolean) true for high-trust IdPs Stale-session bypass possible if false
Passive auth is_passive (boolean) false true causes ServiceNow to accept whatever IdP returns without re-prompting
Assertion encryption encrypt_assertion (boolean) + encryption_key_alias true with cert bound Assertions readable in transit (PII exposure)
NameID policy nameid_policy (string) Match IdP's emitted format Identity confusion attacks
JIT auto-provisioning auto_provision (boolean) false unless required Attacker-controlled attributes create accounts
JIT auto-update auto_update_user (boolean) false IdP-controlled overwrite of user fields on every login
Failed-requirement redirect failed_requirement_redirect (string) Internal URL only Open-redirect / SSRF risk if external
Continuous-auth pathway continuous_authentication_configured + continuous_auth_sso_consumer_url + continuous_auth_sso_script Disabled unless reviewed Novel attack surface — review per-IdP

Why This Is Dangerous

Attack Scenario: SAML Assertion Forgery via Missing Signature Validation

Precondition: ServiceNow SAML configuration does not enforce assertion signature validation. This can occur when:

Attack chain:

  1. Attacker captures a legitimate SAML assertion via browser interceptor, network MITM, or from application logs:

    <!-- Legitimate SAML assertion captured from browser -->
    <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
        ID="_assertion123" Version="2.0"
        IssueInstant="2026-02-26T10:00:00Z">
      <saml:Issuer>https://idp.company.com</saml:Issuer>
      <ds:Signature>...valid signature...</ds:Signature>
      <saml:Subject>
        <saml:NameID>regular.user@company.com</saml:NameID>
      </saml:Subject>
      <saml:AttributeStatement>
        <saml:Attribute Name="role">
          <saml:AttributeValue>itil</saml:AttributeValue>
        </saml:Attribute>
      </saml:AttributeStatement>
    </saml:Assertion>
    
  2. Attacker modifies the assertion — changing the NameID to an admin account and removing the signature:

    <!-- Forged SAML assertion -->
    <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
        ID="_forged456" Version="2.0"
        IssueInstant="2026-02-26T10:01:00Z">
      <saml:Issuer>https://idp.company.com</saml:Issuer>
      <!-- No signature — or signature for a different assertion -->
      <saml:Subject>
        <saml:NameID>admin@company.com</saml:NameID>
      </saml:Subject>
      <saml:AttributeStatement>
        <saml:Attribute Name="role">
          <saml:AttributeValue>admin</saml:AttributeValue>
        </saml:Attribute>
      </saml:AttributeStatement>
    </saml:Assertion>
    
  3. Attacker submits the forged assertion to ServiceNow's ACS endpoint. Without signature validation, ServiceNow accepts it as legitimate.

  4. Attacker is authenticated as admin@company.com with full admin privileges.

Impact: Complete instance takeover via SAML assertion forgery. No IdP compromise required — only a missing configuration checkbox.

Attack Scenario: XML Signature Wrapping (XSW) Attack

Precondition: ServiceNow validates the XML signature but does not properly bind the signature to the processed assertion element. This is a well-documented class of SAML vulnerabilities.

Attack chain:

  1. Attacker captures a legitimately signed SAML response.

  2. Attacker performs XML Signature Wrapping — moves the legitimate signed assertion to a location where the signature validation code checks it, but injects a forged assertion in the location where the authentication code processes it:

    <!-- XSW Attack: Two assertions in one response -->
    <samlp:Response>
      <!-- Forged assertion (processed for authentication) -->
      <saml:Assertion ID="_forged">
        <saml:Subject>
          <saml:NameID>admin@company.com</saml:NameID>
        </saml:Subject>
      </saml:Assertion>
    
      <!-- Original signed assertion (validated for signature) -->
      <saml:Assertion ID="_original">
        <ds:Signature>
          <ds:Reference URI="#_original">...</ds:Reference>
        </ds:Signature>
        <saml:Subject>
          <saml:NameID>regular.user@company.com</saml:NameID>
        </saml:Subject>
      </saml:Assertion>
    </samlp:Response>
    
  3. Signature validation succeeds (against the original, legitimately signed assertion). Authentication proceeds using the forged assertion's NameID (admin@company.com).

Impact: Authentication bypass even when signature validation is enabled. Requires deeper SAML implementation hardening beyond just enabling signature checks.

Attack Scenario: SAML Replay Attack via Missing Time Validation

Precondition: ServiceNow does not enforce SAML assertion time validity (NotBefore/NotOnOrAfter) or does not track used assertion IDs.

  1. Attacker captures a valid SAML assertion during a legitimate login (via network intercept or browser extension).

  2. Hours or days later, the attacker replays the exact same assertion to the ACS endpoint.

  3. Without time validation and assertion ID tracking, ServiceNow accepts the replayed assertion and creates a new authenticated session.

Impact: Persistent unauthorized access using a single captured assertion. The attacker can replay the assertion indefinitely.

Attack Scenario: Auto-Provisioning Privilege Escalation

Precondition: A SAML IdP record has saml2_update1_properties.auto_provision = true (per-IdP boolean, NOT a global property), auto_update_user = true, and the per-IdP user-provisioning script (sso_properties.sso_script reference) maps SAML attributes to roles or groups via groups_for_imported_users or via direct sys_user_has_role writes.

  1. If the IdP is compromised or the attacker controls a federated IdP, they can set arbitrary SAML attributes including roles.

  2. The auto-provisioning logic creates a user with the attacker-specified roles — including admin or security_admin.

  3. Even if the IdP is not compromised, a misconfigured attribute mapping that trusts the "role" attribute from the IdP can be exploited if any of the federated IdPs allows users to set their own attributes.

Impact: Attacker gains admin access to ServiceNow via a trusted but misconfigured federation path.

How to Detect

SAML SSO Configuration Audit (Configure-by-Record idiom)

SAML SSO is per-IdP record-driven on Zurich. The audit walks each active IdP in sso_properties, joins to its saml2_update1_properties extension row, validates the per-IdP fields, and resolves the bound certificate. Property-only audit scripts produce silent-pass / silent- fail because the controls don't live in sys_properties — they live in records.

/*
 * IDN-001 Detection Script — Configure-by-Record SAML SSO audit
 * Walks per-IdP records and validates the real control surface on
 * Zurich Patch 6: sso_properties (master) + saml2_update1_properties
 * (SAML2 extension) + sys_certificate (signing/encryption keys).
 *
 * Run as: Background script with admin role
 * Impact: Read-only, safe for production
 * Versions: Washington+ (record schema verified Zurich Patch 6 2026-04-29)
 */

gs.info('=== IDN-001: SAML SSO CONFIGURATION AUDIT (per-IdP) ===');
gs.info('Scan started: ' + new GlideDateTime().getDisplayValue());
gs.info('');

var totalFindings = 0;

// 1. Multi-provider SSO master toggle (the only legitimate property
//    surface for SSO on Zurich)
gs.info('--- MULTISSO MASTER TOGGLE ---');
var msEnabled = gs.getProperty('glide.authenticate.multisso.enabled', 'NOT_SET');
var msV2 = gs.getProperty('glide.authenticate.multissov2_feature.enabled', 'NOT_SET');
gs.info('  glide.authenticate.multisso.enabled = ' + msEnabled);
gs.info('  glide.authenticate.multissov2_feature.enabled = ' + msV2);
if (msEnabled === 'NOT_SET' || msV2 === 'NOT_SET') {
    gs.info('  [WARNING] Multisso plumbing properties not found — multisso plugin may not be active');
    totalFindings++;
}
gs.info('');

// 2. Walk each active IdP record (sso_properties is the master table)
gs.info('--- PER-IDP CONFIGURATION AUDIT ---');
var idp = new GlideRecord('sso_properties');
idp.addQuery('active', true);
idp.query();
var idpCount = 0;
while (idp.next()) {
    idpCount++;
    var idpName = idp.getValue('name');
    var idpSysId = idp.getUniqueValue();
    gs.info('IdP #' + idpCount + ': ' + idpName +
        ' | default=' + idp.getValue('default') +
        ' | is_primary=' + idp.getValue('is_primary') +
        ' | show_as_login_option=' + idp.getValue('show_as_login_option'));

    // Find SAML2 extension row for this IdP. The extension table
    // saml2_update1_properties extends sso_properties via sys_class_name
    // inheritance — same sys_id when present.
    var saml = new GlideRecord('saml2_update1_properties');
    if (!saml.get(idpSysId)) {
        gs.info('  [INFO] No SAML2 extension row for this IdP — likely OIDC or LDAP-as-IdP. Skip SAML checks.');
        continue;
    }

    // 2a. Assertion-signature validation = bound x509_certificate
    var certRef = saml.getValue('x509_certificate');
    if (!certRef) {
        gs.info('  [CRITICAL] x509_certificate is empty — assertion-signature validation is implicit when this is bound, so empty = no validation. Forgery possible.');
        totalFindings++;
    } else {
        // Resolve cert and check expiry
        var cert = new GlideRecord('sys_certificate');
        if (cert.get(certRef)) {
            var expiry = cert.getValue('expiration');
            if (expiry) {
                var daysLeft = Math.round(
                    (new GlideDateTime(expiry).getNumericValue() - new GlideDateTime().getNumericValue()) / 86400000
                );
                if (daysLeft < 0) {
                    gs.info('  [CRITICAL] IdP cert ' + cert.getValue('name') + ' EXPIRED ' + Math.abs(daysLeft) + ' days ago');
                    totalFindings++;
                } else if (daysLeft < 30) {
                    gs.info('  [WARNING] IdP cert ' + cert.getValue('name') + ' expires in ' + daysLeft + ' days');
                    totalFindings++;
                } else {
                    gs.info('  [OK] IdP cert valid (' + daysLeft + ' days)');
                }
            }
        }
    }

    // 2b. AuthnRequest / LogoutRequest signing requirements
    var checks = [
        { f: 'require_signed_authnrequest', expect: 'true', label: 'Require signed AuthnRequest' },
        { f: 'require_signed_logoutrequest', expect: 'true', label: 'Require signed LogoutRequest' },
        { f: 'encrypt_assertion', expect: 'true', label: 'Require encrypted assertion' },
        { f: 'auto_provision', expect: 'false', label: 'JIT auto-provision (off unless required)' },
        { f: 'auto_update_user', expect: 'false', label: 'JIT auto-update on every login' },
        { f: 'is_passive', expect: 'false', label: 'Passive auth (off — accepts whatever IdP returns)' },
        { f: 'force_authn', expect: 'true', label: 'Force re-authentication' }
    ];
    for (var i = 0; i < checks.length; i++) {
        var v = saml.getValue(checks[i].f);
        var status = (v === checks[i].expect) ? '[OK]' : '[MISCONFIGURED]';
        if (v !== checks[i].expect) totalFindings++;
        gs.info('  ' + status + ' ' + checks[i].label + ' (' + checks[i].f + ') = ' + v);
    }

    // 2c. Audience restriction (string field — validation implicit when set)
    var audience = saml.getValue('audience');
    if (!audience) {
        gs.info('  [CRITICAL] audience is empty — audience-restriction validation requires a non-empty SP entity ID');
        totalFindings++;
    } else {
        gs.info('  [OK] audience = ' + audience);
    }

    // 2d. Clock skew tolerance (integer seconds — replay window)
    var skew = parseInt(saml.getValue('clock_skew') || '180', 10);
    if (skew > 120) {
        gs.info('  [WARNING] clock_skew = ' + skew + ' (>120) — wider replay window than recommended');
        totalFindings++;
    } else {
        gs.info('  [OK] clock_skew = ' + skew);
    }

    // 2e. Failed-requirement redirect — open-redirect / SSRF surface
    var failRedirect = saml.getValue('failed_requirement_redirect');
    if (failRedirect && failRedirect.match(/^https?:\/\/(?!.*\.(your\.instance|service-now\.com))/i)) {
        gs.info('  [WARNING] failed_requirement_redirect points off-instance: ' + failRedirect);
        totalFindings++;
    }

    // 2f. Per-IdP user-provisioning script
    var ssoScript = idp.getValue('sso_script');
    if (ssoScript) {
        gs.info('  [REVIEW] sso_script = ' + idp.getDisplayValue('sso_script') + ' — review JIT role/group mapping logic');
    }
}
gs.info('Total active IdPs: ' + idpCount);
if (idpCount === 0) {
    gs.info('[INFO] No active IdPs — SSO not configured. Local authentication is the only path.');
}
gs.info('');

// 3. SAML debug — only legitimate property-layer SAML toggle on Zurich
gs.info('--- SAML DEBUG STATUS ---');
var debugProp = gs.getProperty('glide.authenticate.sso.saml2.debug', 'false');
if (debugProp === 'true') {
    gs.info('[WARNING] glide.authenticate.sso.saml2.debug = true — debug logs may expose assertions');
    totalFindings++;
} else {
    gs.info('[OK] SAML debug disabled');
}
gs.info('');

// 4. Local admin accounts that bypass SSO entirely
gs.info('--- LOCAL ADMIN ACCOUNTS (SSO BYPASS RISK) ---');
var localAdmins = new GlideRecord('sys_user');
localAdmins.addQuery('active', true);
localAdmins.addEncodedQuery('sso_source=^ORsso_sourceISEMPTY');
localAdmins.query();
var localAdminCount = 0;
while (localAdmins.next()) {
    var hasAdmin = new GlideRecord('sys_user_has_role');
    hasAdmin.addQuery('user', localAdmins.getUniqueValue());
    hasAdmin.addQuery('role.name', 'admin');
    hasAdmin.query();
    if (hasAdmin.next()) {
        localAdminCount++;
        gs.info('  Local admin: ' + localAdmins.getValue('user_name') +
            ' | Last login: ' + localAdmins.getValue('last_login_time'));
    }
}
if (localAdminCount > 2) {
    gs.info('[WARNING] ' + localAdminCount + ' local admin accounts bypass SSO');
    totalFindings++;
}
gs.info('');

// Summary
gs.info('=== SUMMARY ===');
gs.info('Total SAML configuration findings: ' + totalFindings);
gs.info('Audit pattern: Configure-by-Record (per-IdP record audit, NOT property toggle audit).');
gs.info('');
gs.info('CRITICAL ACTIONS (apply per-IdP, not globally):');
gs.info('1. Bind a non-expired x509_certificate to each saml2_update1_properties row');
gs.info('2. Set require_signed_authnrequest = true and require_signed_logoutrequest = true on every IdP');
gs.info('3. Set audience to the SP entity ID on every IdP');
gs.info('4. Lower clock_skew to ≤120 sec on every IdP');
gs.info('5. Rotate expiring IdP certificates BEFORE they expire (NEVER bypass via debug mode)');
gs.info('6. Set auto_provision and auto_update_user to false unless JIT is reviewed and approved');
gs.info('7. Set is_passive = false; set force_authn = true for high-trust IdPs');
gs.info('8. Disable glide.authenticate.sso.saml2.debug in production');
gs.info('9. Review every per-IdP sso_script for JIT role/group mapping logic');
gs.info('10. Audit local admin accounts that bypass SSO entirely');

Remediation

Remediation is per-IdP record, not global property. Iterate every active row in sso_properties and apply each step to its saml2_update1_properties extension. Multisso instances commonly run multiple IdPs in parallel; missing one creates a bypass path.

Step 1: Bind IdP Signing Certificate (assertion-signature validation)

CRITICAL — this is the most important SAML security control:

1. For each active IdP in sso_properties, open the matching
   saml2_update1_properties row.
2. Set saml2_update1_properties.x509_certificate to a sys_certificate
   reference holding the IdP's current X.509 signing certificate.
   (Assertion-signature validation is implicit when this is bound —
   binding the cert IS enabling validation. There is no separate toggle.)
3. Set saml2_update1_properties.require_signed_authnrequest = true.
   This forces the IdP to sign its inbound AuthnRequests, preventing
   IdP-spoofing attacks.
4. Set saml2_update1_properties.require_signed_logoutrequest = true.
5. Upload the IdP's current cert to sys_certificate BEFORE rotating;
   keep the previous cert briefly for graceful cutover.

IMPORTANT: ServiceNow validates the signature using the bound cert AND
ensures the signed element is the one being processed (XSW protection).
Test with SAML testing tools (SAML Raider, SAMLExtractor) and verify
that XSW-style two-assertion responses are rejected.

Step 2: Audience, Clock Skew, and Replay Protection

On each saml2_update1_properties row:

1. Set audience to the SP entity ID. Audience-restriction validation
   is implicit when this string is set; an empty audience disables it.
2. Set clock_skew to a value ≤120 seconds (default is 180). This is
   the NotBefore / NotOnOrAfter tolerance window.
3. Set is_passive = false. Passive auth causes ServiceNow to accept
   whatever the IdP returns without re-prompting; the IdP's own
   freshness checks become the only line of defense.
4. Set force_authn = true on high-trust IdPs (admin / privileged-role
   IdPs) to require fresh authentication on every login.

Replay protection at the assertion-ID layer is built into ServiceNow's
SAML processing — there is no record-level toggle. Verify with the IdP
team that each assertion is issued with a unique ID; replays of the
same ID within the clock_skew window are rejected automatically.

Step 3: Secure JIT Auto-Provisioning

If JIT auto-provisioning is required:

1. On each saml2_update1_properties row, set auto_provision = true and
   auto_update_user = true ONLY where required (per-IdP, not global).
2. Set saml2_update1_properties.groups_for_imported_users to a curated
   list of low-privilege groups (e.g., ess only). This is the per-IdP
   default group list.
3. Do NOT map SAML role/group attributes to ServiceNow roles inside
   the per-IdP user-provisioning script (sso_properties.sso_script).
   Use group-membership rules + named approval workflows instead.
4. Audit sys_user weekly for auto-created records (filter by sso_source
   field, recent created_on).

If JIT is NOT required:

1. On each saml2_update1_properties row: auto_provision = false,
   auto_update_user = false.
2. Set sso_properties.sso_script to a no-op or remove the reference.
3. Pre-create user accounts via LDAP, SCIM, or a managed transform map.

Step 4: Certificate Management

1. Maintain inventory of all IdP certs bound via
   saml2_update1_properties.x509_certificate (and the corresponding
   sys_certificate rows).
2. Set calendar reminders for certificate expiry (90 / 60 / 30 days).
3. Plan certificate rotation BEFORE expiry:
   - Upload new cert to sys_certificate alongside the existing cert
   - Coordinate with IdP team for cutover date
   - Test in sub-production by binding the new cert reference there
   - Update production saml2_update1_properties.x509_certificate
     reference during a brief maintenance window
4. NEVER bypass the failure by setting glide.authenticate.sso.saml2.debug
   to true or by clearing the x509_certificate reference. Either action
   creates the most dangerous SAML misconfiguration — assertion forgery
   becomes possible until rotation is complete.

Step 5: Per-IdP Provisioning-Script Review

sso_properties.sso_script references a Script Include that runs on
every JIT user creation / update. This is the real surface for "SAML
attribute mapping to roles" attacks — NOT a property.

For each active IdP:

1. Open sso_properties.sso_script (reference to sys_script_include).
2. Audit the script for:
   - Direct sys_user_has_role.insert() calls based on SAML attributes
   - Trust-by-default attribute consumption (e.g.,
     setRoles(assertion.attributes.role)) without allowlisting
   - Missing approval-workflow integration on role grants
3. Replace direct role assignment with group-membership rules
   (sys_user_grmember) and group-to-role mapping that goes through
   normal ServiceNow access governance.

Post-Remediation Verification

/*
 * Verify SAML SSO hardening is complete (per-IdP)
 */
gs.info('=== IDN-001: POST-REMEDIATION CHECK ===');
var issues = 0;
var idpCount = 0;

var idp = new GlideRecord('sso_properties');
idp.addQuery('active', true);
idp.query();
while (idp.next()) {
    idpCount++;
    var idpName = idp.getValue('name');
    var saml = new GlideRecord('saml2_update1_properties');
    if (!saml.get(idp.getUniqueValue())) continue;

    var checks = [
        { f: 'x509_certificate', test: function(v) { return v && v.length > 0; }, label: 'x509_certificate bound' },
        { f: 'require_signed_authnrequest', test: function(v) { return v === 'true'; }, label: 'require_signed_authnrequest=true' },
        { f: 'require_signed_logoutrequest', test: function(v) { return v === 'true'; }, label: 'require_signed_logoutrequest=true' },
        { f: 'audience', test: function(v) { return v && v.length > 0; }, label: 'audience set' },
        { f: 'clock_skew', test: function(v) { return parseInt(v || '180', 10) <= 120; }, label: 'clock_skew ≤120' },
        { f: 'is_passive', test: function(v) { return v === 'false'; }, label: 'is_passive=false' }
    ];
    for (var i = 0; i < checks.length; i++) {
        var v = saml.getValue(checks[i].f);
        if (checks[i].test(v)) {
            gs.info('PASS [' + idpName + '] ' + checks[i].label + ' (' + v + ')');
        } else {
            gs.info('FAIL [' + idpName + '] ' + checks[i].label + ' (' + v + ')');
            issues++;
        }
    }
}

var dbg = gs.getProperty('glide.authenticate.sso.saml2.debug', 'false');
if (dbg === 'true') {
    gs.info('FAIL: glide.authenticate.sso.saml2.debug = true');
    issues++;
} else {
    gs.info('PASS: glide.authenticate.sso.saml2.debug = false');
}

if (idpCount === 0) {
    gs.info('NO ACTIVE IDPS — SSO not configured');
} else if (issues === 0) {
    gs.info('PASS: SAML SSO hardening meets minimum requirements across ' + idpCount + ' IdP(s)');
} else {
    gs.info('FAIL: ' + issues + ' SAML configuration issues across ' + idpCount + ' IdP(s)');
}

Regulatory Impact

NIS2 Mapping

Article Requirement How SAML Misconfiguration Violates It Evidence After Fix
Art.21§2(a) Risk analysis and IS security policies SAML misconfiguration creates authentication bypass risk not captured in risk assessment SAML configuration audit completed, all signature and time validation enabled, documented in risk register

DORA Mapping

Article Requirement How SAML Misconfiguration Violates It Evidence After Fix
Art.9§4(c) Detect anomalous activities SAML assertion forgery bypasses authentication controls undetected Signature validation enforced, assertion replay detection enabled, SAML debug events forwarded to SIEM

ISO 27001:2022 Mapping

Control Requirement How SAML Misconfiguration Violates It Evidence After Fix
A.8.5 Secure authentication SAML without signature validation is not secure authentication — assertions can be forged All SAML security controls enabled: signed assertions, signed responses, audience restriction, time validation, encrypted assertions

Expert Notes

Practitioner annotations pending — article content has been technically validated.