{"id":18165,"plugin_id":"plugins_6a86acf7816881918552f3b43bc0db69","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:14:41.263Z","digest":"2c02802edac5c68d4dc10d039b9f72950498cf6ab9368151807231730e389134","against":null,"payload":{"description":"Configuring Duende IdentityServer as a SAML 2.0 Identity Provider (IdP): service provider registration, SSO and SLO flows, claim mappings, extensibility interfaces, and production deployment patterns.","included_files":[],"name":"identityserver-saml","skill_md_contents":"---\nname: identityserver-saml\ndescription: \"Configuring Duende IdentityServer as a SAML 2.0 Identity Provider (IdP): service provider registration, SSO and SLO flows, claim mappings, extensibility interfaces, and production deployment patterns.\"\ninvocable: false\n---\n\n# SAML 2.0 Identity Provider\n\n## When to Use This Skill\n\n- Setting up IdentityServer as a SAML 2.0 Identity Provider (IdP)\n- Registering SAML Service Providers with the `SamlServiceProvider` model\n- Configuring SP-initiated SSO and Single Logout (SLO) flows\n- Customizing claim-to-attribute mappings via `ClaimMappings` or extensibility interfaces\n- Implementing production SP stores (EF Core, custom `ISamlServiceProviderStore`)\n- Extending SAML behavior (custom NameID generation, signing, metadata, multi-tenant issuer)\n- Linking an external SAML IdP as a federated authentication source (SP mode)\n\n## Core Principles\n\n- SAML 2.0 IdP support is **built into Duende.IdentityServer** (v8.0+) — no separate NuGet package\n- Requires **Standard (add-on), Advanced, or Custom Edition** license\n- SP-initiated SSO is the default; IdP-initiated SSO is opt-in per service provider\n- `SignAssertion` is the default signing behavior; `SignResponse` is recommended for most deployments\n- Use EF Core stores for service providers in production; in-memory is for development only\n- Front-channel SLO uses iframes (not redirect chains); partial logout is expected behavior\n- The claim pipeline flows: AllowedScopes → RequestedClaimTypes → ClaimMappings\n\nDocs: https://docs.duendesoftware.com/identityserver/saml\n\n## Setup\n\n```csharp\nbuilder.Services.AddIdentityServer()\n    .AddInMemoryClients(Config.Clients)\n    .AddInMemoryIdentityResources(Config.IdentityResources)\n    .AddSaml()\n    .AddInMemorySamlServiceProviders(Config.SamlServiceProviders);\n```\n\nUpdate the login page to call `DenyAuthenticationAsync` for SAML cancellation support (when user cancels login during a SAML flow).\n\n## Endpoints\n\n| Endpoint | Path | Purpose |\n|----------|------|---------|\n| Metadata | `/Saml2` | IdP metadata (certificates, endpoints, NameID formats) |\n| Sign-in | `/Saml2/SSO` | Receives AuthnRequest (GET/POST) |\n| Sign-in Callback | `/Saml2/SSO/Callback` | Builds SAML Response after authentication |\n| Logout | `/Saml2/SLO` | Handles LogoutRequest/LogoutResponse |\n| Logout Callback | `/Saml2/SLO/Callback` | Completes SLO round-trip |\n\nPaths are customizable via `SamlOptions.Endpoints`.\n\n### Profile Active Check\n\n`IProfileService.IsActiveAsync` is called on every SSO request, including when the user already has an active session.\nIf `IsActive` returns `false`: passive requests (`IsPassive=true`) receive a SAML `NoPassive` error response; all other requests are redirected to the login page.\nThis is the recommended mechanism for blocking disabled or locked accounts without waiting for session expiry.\n\n### Observability\n\nAll SAML endpoints emit audit events and OpenTelemetry telemetry counters.\nSSO and SLO endpoints participate in distributed tracing via the `Duende.IdentityServer` activity source.\nSee docs for SAML audit events and `TelemetryMetricsCounters.SamlSso`.\n\n## SamlServiceProvider Model\n\n```csharp\nnew SamlServiceProvider\n{\n    // Required\n    EntityId = \"https://sp.example.com\",\n    DisplayName = \"Example SP\",\n\n    // ACS endpoints (HTTP-POST only, indexed)\n    AssertionConsumerServiceUrls =\n    [\n        new IndexedEndpoint\n        {\n            Location = \"https://sp.example.com/acs\",\n            Binding = SamlBinding.HttpPost,\n            Index = 0,\n            IsDefault = true\n        }\n    ],\n\n    // Single Logout (HTTP-Redirect only)\n    SingleLogoutServiceUrls =\n    [\n        new SamlEndpointType\n        {\n            Location = \"https://sp.example.com/saml/slo\",\n            Binding = SamlBinding.HttpRedirect\n        }\n    ],\n\n    // Security\n    SigningBehavior = SamlSigningBehavior.SignAssertion,\n    RequireSignedAuthnRequests = true,\n    Certificates =\n    [\n        new ServiceProviderCertificate\n        {\n            Certificate = spCert,\n            Use = KeyUse.Signing\n        }\n    ],\n\n    // Claims (identity resources the SP can access)\n    AllowedScopes = [\"openid\", \"profile\", \"email\"],\n    RequestedClaimTypes = [\"email\", \"name\"],  // optional narrowing\n    ClaimMappings = new Dictionary<string, string>\n    {\n        [\"email\"] = \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\",\n        [\"name\"] = \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name\"\n    },\n\n    // NameID\n    DefaultNameIdFormat = SamlNameIdFormat.EmailAddress,\n\n    // IdP-Initiated SSO (opt-in)\n    AllowIdpInitiated = false,\n\n    // Lifecycle / display\n    Enabled = true,                   // false → reject all requests from this SP\n    Description = \"Optional notes\",  // human-readable, not sent in SAML responses\n\n    // Per-SP overrides (null = fall back to SamlOptions global default)\n    AssertionLifetime = TimeSpan.FromMinutes(5),   // overrides SamlOptions.DefaultAssertionLifetime\n    EmailNameIdClaimType = \"email\",                // overrides SamlOptions.EmailNameIdClaimType\n    RequireSignedLogoutResponses = true,           // overrides SamlOptions.RequireSignedLogoutResponses\n    AllowedSignatureAlgorithms = [\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"],  // null → IdP default\n    AuthnContextMappings = new Dictionary<string, string>  // overrides SamlOptions.DefaultAuthnContextMappings\n    {\n        [\"pwd\"] = \"urn:oasis:names:tc:SAML:2.0:ac:classes:Password\",\n        [\"mfa\"] = \"urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract\"\n    }\n}\n```\n\n### Claim Pipeline\n\n```\nAllowedScopes (identity resources) → filters available claim types\n    ↓\nRequestedClaimTypes (optional narrowing) → selects specific claims\n    ↓\nClaimMappings (OIDC claim name → SAML attribute URI) → output as <saml:Attribute>\n```\n\nUse `SamlOptions.DefaultClaimMappings` for global defaults; per-SP `ClaimMappings` override them.\n\n## Configuration (SamlOptions)\n\n```csharp\nbuilder.Services.AddIdentityServer()\n    .AddSaml(saml =>\n    {\n        saml.EntityId = \"https://idp.example.com/Saml2\"; // default: {host}/Saml2\n        saml.EntityIdPath = \"/Saml2\";                    // path appended to host URL to form default EntityId\n        saml.WantAuthnRequestsSigned = true;             // default: true\n        saml.RequireSignedLogoutResponses = true;        // default: true\n        saml.DefaultSigningBehavior = SamlSigningBehavior.SignAssertion;\n        saml.DefaultClockSkew = TimeSpan.FromMinutes(5);\n        saml.DefaultRequestMaxAge = TimeSpan.FromMinutes(5);\n        saml.DefaultAssertionLifetime = TimeSpan.FromMinutes(5);\n        saml.SupportedNameIdFormats = [SamlNameIdFormat.EmailAddress, SamlNameIdFormat.Unspecified];\n        saml.MaxRelayStateLength = 80; // SAML spec requirement\n        saml.MaxMessageSize = 1_048_576; // max chars of inbound SAML messages (default: 1 MB)\n\n        // Session/state lifetimes\n        saml.SigninStateLifetime = TimeSpan.FromMinutes(15);   // how long sign-in request state is retained\n        saml.LogoutSessionLifetime = TimeSpan.FromMinutes(5);  // how long SLO session tracking state is retained\n\n        // NameID claim type for email-format NameIDs (default: \"email\")\n        saml.EmailNameIdClaimType = \"email\";\n\n        // Global claim mappings\n        saml.DefaultClaimMappings = new Dictionary<string, string>\n        {\n            [\"name\"] = \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name\",\n            [\"email\"] = \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\",\n            [\"role\"] = \"http://schemas.xmlsoap.org/ws/2005/05/identity/role\"\n        };\n\n        // AuthnContext mappings (acr/amr → SAML AuthnContext URIs)\n        saml.DefaultAuthnContextMappings = new Dictionary<string, string>\n        {\n            [\"pwd\"] = \"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\"\n        };\n\n        // Optional error inspector callbacks for debugging interoperability issues\n        // These can inspect or suppress parse errors on inbound SAML messages\n        saml.AuthnRequestErrorInspector = (context, exception) => { /* inspect/suppress */ };\n        saml.LogoutRequestErrorInspector = (context, exception) => { /* inspect/suppress */ };\n        saml.LogoutResponseErrorInspector = (context, exception) => { /* inspect/suppress */ };\n    });\n```\n\n### Metadata Options\n\n```csharp\nbuilder.Services.AddIdentityServer()\n    .AddSaml(saml =>\n    {\n        saml.Metadata.CacheDuration = TimeSpan.FromHours(12);\n        saml.Metadata.ExpiryDuration = TimeSpan.FromDays(5);\n    });\n```\n\n### Endpoint Options\n\n| Property | Default | Description |\n|----------|---------|-------------|\n| `SingleSignOnServicePath` | `\"/Saml2/SSO\"` | Path for the SSO endpoint |\n| `SingleSignOnServiceBindings` | `[HttpRedirect, HttpPost]` | Bindings advertised in metadata (not which the endpoint accepts) |\n| `SingleSignOnCallbackPath` | `\"/Saml2/SSO/Callback\"` | Internal callback path after user authenticates |\n| `SingleLogoutServicePath` | `\"/Saml2/SLO\"` | Path for the SLO endpoint |\n| `SingleLogoutServiceBindings` | `[HttpRedirect, HttpPost]` | Bindings advertised in metadata for SLO |\n| `SingleLogoutCallbackPath` | `\"/Saml2/SLO/Callback\"` | Internal callback path for SLO completion |\n| `StateIdParameterName` | `\"samlStateId\"` | Query string param name for the SAML sign-in state ID |\n\n```csharp\nbuilder.Services.AddIdentityServer()\n    .AddSaml(saml =>\n    {\n        saml.Endpoints.SingleSignOnServicePath = \"/Saml2/SSO\";\n        saml.Endpoints.SingleLogoutServicePath = \"/Saml2/SLO\";\n    });\n```\n\n## SAML Signing Keys (X.509)\n\nSAML signing **requires an X.509 certificate**. OIDC and SAML share the same signing credentials; rotation timing is governed by Automatic Key Management `PropagationTime` and `RetentionDuration`.\n\n- **Automatic Key Management (RSA)**: auto-generated RSA keys are auto-wrapped into a self-signed X.509 container. You do **not** need `UseX509Certificate` just to enable SAML.\n- **Manual / raw RSA keys — including `AddDeveloperSigningCredential()`**: **cannot** be auto-wrapped. Register an X.509 certificate **with a private key** instead.\n- The default SAML signing service is **RSA-only**. `UseX509Certificate` is **not** supported for EC keys — implement a custom `ISamlSigningService` for EC (or HSM/Key Vault) scenarios.\n\n## Service Provider Stores\n\n### In-Memory (Development)\n\n```csharp\n.AddInMemorySamlServiceProviders(new[]\n{\n    new SamlServiceProvider { EntityId = \"...\", /* ... */ }\n});\n```\n\n### EF Core (Production — Recommended)\n\n```csharp\n.AddConfigurationStore(options =>\n{\n    options.ConfigureDbContext = b =>\n        b.UseSqlServer(connectionString);\n})\n```\n\nRun EF migrations: `dotnet ef migrations add Update_DuendeIdentityServer_v8_0`\n\n### Custom Store\n\n```csharp\n.AddSamlServiceProviderStore<MySamlSpStore>()\n\npublic class MySamlSpStore : ISamlServiceProviderStore\n{\n    public Task<SamlServiceProvider?> FindByEntityIdAsync(\n        string entityId, CancellationToken ct)\n    { /* lookup from your backend */ }\n\n    public IAsyncEnumerable<SamlServiceProvider> GetAllSamlServiceProvidersAsync(\n        CancellationToken ct)\n    { /* stream all SPs */ }\n}\n```\n\n> **Note — Operational store auto-registration**: `AddOperationalStore()` automatically registers EF Core implementations of **both** `ISamlSigninStateStore` **and** `ISamlLogoutSessionStore`. When using the EF operational store, these do not need to be registered separately.\n\n### Caching & Validation\n\n```csharp\n// Add HybridCache layer to any custom store\n.AddSamlServiceProviderStoreCache<MySamlSpStore>()\n```\n\nCache duration is controlled by `IdentityServerOptions.Caching.SamlServiceProviderStoreExpiration` (default: 15 minutes):\n\n```csharp\nbuilder.Services\n    .AddIdentityServer(options =>\n    {\n        options.Caching.SamlServiceProviderStoreExpiration = TimeSpan.FromMinutes(30);\n    })\n    .AddSaml()\n    .AddSamlServiceProviderStoreCache<MySamlServiceProviderStore>();\n```\n\nAll stores are automatically wrapped with `ValidatingSamlServiceProviderStore<T>` that checks: EntityId required, ≥1 ACS URL (HTTP-POST only), ≥1 AllowedScopes, positive lifetimes. Invalid SPs are treated as non-existent.\n\n## Single Logout (SLO)\n\nSLO uses **front-channel logout via iframes** (not redirect chains):\n\n1. SP sends LogoutRequest to `/Saml2/SLO`\n2. IdentityServer ends local session\n3. Renders iframes sending LogoutRequests to all other active SPs\n4. Collects LogoutResponses from SPs\n5. Sends final LogoutResponse to originating SP\n\n**Key points:**\n- Partial logout is normal (some SPs may not respond)\n- User must stay on logout page for iframes to complete\n- Use `ISamlLogoutSessionStore` for distributed deployments (tracks which SPs have active sessions)\n- Short session lifetimes serve as SLO fallback\n\n## IdP-Initiated SSO\n\n> ⚠️ **CSRF Warning**: IdP-initiated SSO is inherently vulnerable to CSRF. There is no SAML-compliant way to implement it without CSRF exposure. Only enable it after careful security review.\n\n**Recommended alternative**: Mimic OIDC third-party initiated login — create a dedicated SP endpoint that accepts a target application hint and redirects the user to the IdP with a standard SP-initiated AuthnRequest. This avoids the CSRF risk entirely.\n\n**Enabling per SP**: If IdP-initiated SSO is genuinely required, set `AllowIdpInitiated = true` on the `SamlServiceProvider`.\n\n**No built-in endpoint**: There is no built-in IdP-initiated SSO endpoint. Implement your own Razor Page or controller and inject `IIdpInitiatedSsoService`:\n\n```csharp\n// Key method on IIdpInitiatedSsoService:\nTask<IdpInitiatedSsoResult> CreateResponseAsync(\n    HttpContext httpContext, string spEntityId, string? relayState, CancellationToken ct);\n```\n\nCall `CreateResponseAsync` from your custom endpoint to generate and return the SAML Response to the SP. The SP must have `AllowIdpInitiated = true`; otherwise the call will fail.\n\n## Extensibility\n\n| Interface | Purpose |\n|-----------|---------|\n| `ISamlNameIdGenerator` | Custom NameID value derivation (e.g., from employee_id claim) |\n| `ISamlSigningService` | HSM/Key Vault signing certificate integration |\n| `ISaml2MetadataResponseGenerator` | Custom metadata extensions (org info, federation) |\n| `ISaml2IssuerNameService` | Multi-tenant: dynamic entity ID per tenant |\n| `ISaml2SsoInteractionResponseGenerator` | Custom step-up auth logic during SSO |\n| `ISaml2SsoResponseGenerator` | Custom SAML Response generation |\n| `ISamlLogoutNotificationService` | Selective SLO targeting; returns `SamlLogoutNotificationResult` (`Messages`: collection of `SamlLogoutRequestContext`, `SkippedCount`: int) |\n| `ISaml2SloResponseGenerator` | Custom SLO `LogoutResponse` generation (success vs partial logout) |\n| `ISamlLogoutSessionStore` | Distributed SLO state (Redis, EF Core); key method: `TryRecordResponseAsync(string requestId, string issuer, bool success, CancellationToken ct)`; `SamlLogoutSession` has `SkippedSpCount` (int), `ExpiresAtUtc` (DateTime), `ExpectedResponses` dictionary |\n| `ISaml2FrontChannelLogoutRequestBuilder` | Custom logout request structure; `BuildLogoutRequestAsync` returns `SamlLogoutRequestContext` (wraps outbound message + `RequestId` + `SpEntityId` for response correlation) |\n| `ISamlResourceResolver` | Dynamic scope filtering per SP |\n| `IIdpInitiatedSsoService` | Portal \"My Apps\" dashboard for IdP-initiated flows |\n| `IAuthnRequestValidator` | Custom SP access rules, IP/time-based controls |\n| `ILogoutRequestValidator` | Custom SLO authorization rules |\n| `ISamlSigninStateStore` | Distributed sign-in state (for multi-node deployments); methods include `UpdateSigninRequestStateAsync` |\n| `ISamlServiceProviderConfigurationValidator` | Custom SP config validation rules |\n\n> **DI ordering is NOT required**: Custom SAML services do **not** need to be registered before `AddSaml()`. Defaults are registered with `TryAdd*` (e.g. `TryAddScoped`), so a custom scoped registration takes precedence regardless of order.\n\n> **State serializer & `Extensions`**: The default `ISamlSigninStateSerializer` **ignores** the `Extensions` property. To persist custom SAML extension data across the sign-in round-trip, implement a custom serializer.\n\n### Example: Custom NameID Generator\n\n```csharp\npublic class EmployeeNameIdGenerator : ISamlNameIdGenerator\n{\n    public Task<NameIdGenerationResult> GenerateAsync(\n        NameIdGenerationContext context, CancellationToken ct)\n    {\n        var employeeId = context.Subject.FindFirst(\"employee_id\")?.Value;\n        if (employeeId is null)\n            return Task.FromResult(NameIdGenerationResult.Failure(\n                StatusCodes.Responder, StatusCodes.UnknownPrincipal,\n                \"Employee ID claim not found.\"));\n\n        return Task.FromResult(NameIdGenerationResult.Success(\n            new NameId(employeeId, context.ResolvedFormat)));\n    }\n}\n```\n\n### SAML Authentication Context in Login UI\n\nInject `IIdentityServerInteractionService` and call `GetAuthenticationContextAsync(returnUrl)`; pattern-match the result to `SamlAuthenticationContext` for customizing login flows per SP.\n\n`SamlAuthenticationContext` properties:\n- `ServiceProvider` — the SP that initiated the request\n- `IdP` (string?) — IdP entity ID from `Scoping`, null if multiple IdPs listed\n- `LoginHint` (string?) — login hint from NameID in AuthnRequest\n- `Tenant` (string?) — tenant identifier from RequestedAuthnContext\n- `PromptModes` — derived from `ForceAuthn` and `IsPassive` flags\n- `RelayState` (string?) — relay state from the AuthnRequest\n- `IsIdpInitiated` (bool) — whether this is an IdP-initiated SSO flow\n- `RequestedAuthnContext` — authentication context requirements from the SP\n- `StateId` (Guid) — identifier for sign-in state entry; needed when calling `DenyAuthenticationAsync`\n\n## Using IdentityServer as a SAML Service Provider (SP Mode)\n\nIdentityServer can consume SAML assertions from external IdPs via federation. Add a SAML authentication handler and configure it as an external provider in IdentityServer's login UI — same pattern as any external authentication scheme.\n\n### Native SAML SP handler (`AddSamlServiceProvider`)\n\nRegister the built-in Duende SAML SP handler as an external scheme feeding the IdentityServer external cookie:\n\n```csharp\nbuilder.Services.AddAuthentication()\n    .AddSamlServiceProvider(\"corporate-idp\", options =>\n    {\n        options.SpEntityId = \"https://sp.example.com\";\n        options.IdpEntityId = \"https://idp.example.com\";\n        options.SingleSignOnServiceUrl = \"https://idp.example.com/sso\";\n        options.SigningCertificatesBase64 = [\"<base64>\"];   // LIST → supports IdP cert rollover\n        options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;\n\n        // IdP-initiated (unsolicited) SSO — opt-in\n        options.AllowUnsolicitedAuthnResponse = true;                 // default false\n        options.IdpInitiatedCallbackUrl = \"/ExternalLogin/Callback\";  // REQUIRED when above is true\n    });\n```\n\n| Property | Default | Notes |\n|----------|---------|-------|\n| `AllowUnsolicitedAuthnResponse` | `false` | Accept IdP-initiated (unsolicited) `AuthnResponse`. When `true`, you **must** set `IdpInitiatedCallbackUrl`. |\n| `IdpInitiatedCallbackUrl` | `null` | **Required** when unsolicited responses are allowed. Relative path (e.g. `/ExternalLogin/Callback`) or absolute http/https URL; redirect target after processing an unsolicited response. |\n| `MaxRelayStateLength` | `1024` | Max bytes of RelayState persisted in auth properties; oversized values are **silently dropped** to avoid cookie bloat. |\n\n**Callback handling (IdP-initiated):** authenticate against `IdentityServerConstants.ExternalCookieAuthenticationScheme`. The IdP-supplied RelayState surfaces at `AuthenticationProperties.Items[\"relayState\"]` (only when ≤ `MaxRelayStateLength`); `\"scheme\"` and `\"returnUrl\"` items are also populated.\n\n> ⚠️ **Security**: treat RelayState as **untrusted input**. Always validate it before using it as a redirect target.\n\n**Dynamic providers**: the dynamic `SamlProvider` model gains the same `AllowUnsolicitedAuthnResponse` and `IdpInitiatedCallbackUrl` properties. Note the dynamic model uses `SigningCertificateBase64` (**singular** string), whereas the static handler uses `SigningCertificatesBase64` (**list**, supports rollover).\n\n### Third-party handlers\n\nAlternatively, use a third-party handler (e.g., `Sustainsys.Saml2` or `ITfoxtec.Identity.Saml2`) configured as an external scheme.\n\nFor step-by-step setup instructions, see the official docs: https://docs.duendesoftware.com/identityserver/ui/login/saml-provider/\n\n> **Managing many SAML IdPs?** For scenarios with a large or changing set of external SAML identity providers, consider using **dynamic providers** instead of static registration. Dynamic providers allow you to manage IdP configurations at runtime without redeployment. See: https://docs.duendesoftware.com/identityserver/ui/login/dynamicproviders/#saml-providers\n\n## Common Anti-Patterns\n\n❌ Enabling `AllowIdpInitiated` on all SPs — only enable where explicitly required (less secure)\n❌ Using `DoNotSign` outside of local testing\n❌ Using in-memory SP stores in production\n❌ Omitting `AllowedScopes` — SP gets no claims in the assertion\n❌ Configuring ACS URLs with HTTP-Redirect binding (only HTTP-POST is supported)\n\n## Common Pitfalls\n\n1. **Edition requirement**: `AddSaml()` requires Standard (add-on), Advanced, or Custom Edition license.\n2. **ACS binding**: Only HTTP-POST is supported for AssertionConsumerServiceUrls. HTTP-Redirect will fail validation.\n3. **Clock skew**: Default 5 minutes. Increase if SPs report \"response not yet valid\" errors.\n4. **Partial SLO**: Front-channel logout via iframes means some SPs may not respond. This is expected — don't treat it as an error.\n5. **DenyAuthenticationAsync**: Login page must call this for SAML cancellation. Without it, users get stuck if they cancel.\n6. **Operational stores**: For multi-node deployments, configure `ISamlSigninStateStore` and `ISamlLogoutSessionStore` (e.g., EF Core, Redis). Without them, SSO/SLO state is lost across nodes.\n7. **Certificate rotation**: Metadata is cached (default 12h). SPs may not pick up new signing certs until cache expires.\n8. **ClaimMappings vs AllowedScopes**: If `AllowedScopes` doesn't include a resource containing a claim type, that claim won't reach `ClaimMappings`.\n\n## Related Skills\n\n- `identityserver-configuration` — IdentityServer host configuration and options\n- `identityserver-stores` — Persistent store patterns (EF Core, custom stores)\n- `identity-security-hardening` — Key rotation, HTTPS enforcement\n- `identityserver-ui-flows` — Login/logout UI flows that SAML integrates with\n- `identityserver-upgrade-v7-to-v8` — Migration guide including SAML EF migrations\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}