diff --git a/astro/src/content/docs/identityserver/saml/concepts.md b/astro/src/content/docs/identityserver/saml/concepts.md index 9fe3b3db..f8d0a4f2 100644 --- a/astro/src/content/docs/identityserver/saml/concepts.md +++ b/astro/src/content/docs/identityserver/saml/concepts.md @@ -9,7 +9,7 @@ sidebar: Added in 8.0 (prerelease) -SAML 2.0 is an XML-based federation protocol widely used in enterprise, government, healthcare, and education environments. This page explains the nine core concepts you need to understand when working with IdentityServer as a [SAML 2.0 Identity Provider](/identityserver/saml/index.md). Once you are familiar with these concepts, see the [configuration reference](/identityserver/saml/configuration.md) to put them into practice. +SAML 2.0 is an XML-based federation protocol widely used in enterprise, government, healthcare, and education environments. This page explains the core concepts you need to understand when working with SAML 2.0 federation. Where relevant, each section links to the corresponding IdentityServer [configuration](/identityserver/saml/configuration.md) so you can put these concepts into practice. ## Assertions @@ -23,21 +23,40 @@ An assertion contains three key parts: * **Attribute Statement**: carries user properties such as email address, roles, group memberships, and department. * **Conditions**: constrain where and when the assertion is valid. `NotBefore` and `NotOnOrAfter` define a time window (typically minutes), and `AudienceRestriction` limits which recipients can accept it. -The Identity Provider signs the assertion with its private key. The Service Provider validates the signature before trusting any claims inside. IdentityServer builds assertions automatically when it processes a SAML sign-in request. You control what attributes appear in assertions via claim mappings — see [`SamlOptions.DefaultClaimMappings` and `SamlServiceProvider.ClaimMappings`](/identityserver/saml/configuration.md#default-claim-mappings). The signing behavior is configured via the [`SamlSigningBehavior` enum](/identityserver/saml/configuration.md#samlsigningbehavior). +The Identity Provider signs the assertion with its private key. The Service Provider validates the signature before trusting any claims inside. + +In IdentityServer, you control what attributes appear in assertions via [claim mappings](/identityserver/saml/configuration.md#default-claim-mappings) and configure signing via [`SamlSigningBehavior`](/identityserver/saml/configuration.md#samlsigningbehavior). ## Identity Provider -The Identity Provider (IdP) is the system that authenticates users and issues assertions. It is the authority — the entity that knows who a user is and can prove it to other parties. +The Identity Provider (IdP) is the system that authenticates users and issues assertions. It is the authority: the entity that knows who a user is and can prove it to other parties. When a user needs access to a protected application, they authenticate at the IdP. The IdP verifies the user's identity using whatever mechanism is configured (password, multi-factor authentication, smart card), then constructs a signed assertion and delivers it to the requesting application. -**IdentityServer acts as the IdP** when you enable SAML 2.0 support via `AddSaml()`. It publishes its capabilities, endpoints, and certificates through a [metadata document](/identityserver/saml/endpoints.md#metadata-endpoint) that Service Providers import to configure trust. +**IdentityServer acts as the IdP** when you enable SAML 2.0 support via `AddSaml()`. It publishes its capabilities through a [metadata document](/identityserver/saml/endpoints.md#metadata-endpoint) that Service Providers import to configure trust. ## Service Provider The Service Provider (SP) is the application the user wants to access. Rather than managing credentials itself, it delegates authentication to the IdP and relies on the assertions it receives. -When an unauthenticated user arrives, the SP sends an `AuthnRequest` to the IdP. After the IdP authenticates the user and returns an assertion, the SP validates the signature, checks the conditions, extracts identity and attributes, and establishes a local session. The SP never handles the user's credentials — it trusts the IdP because the two parties have established a federation agreement backed by exchanged metadata and certificates. +When an unauthenticated user arrives, the SP sends an `AuthnRequest` to the IdP. After the IdP authenticates the user and returns an assertion, the SP validates the signature, checks the conditions, extracts identity and attributes, and establishes a local session. The SP never handles the user's credentials. It trusts the IdP because the two parties have established a federation agreement backed by exchanged metadata and certificates. + +```mermaid +sequenceDiagram + participant User + participant SP as Service Provider + participant IdP as Identity Provider + + User->>SP: Access protected resource + SP->>User: Redirect with AuthnRequest + User->>IdP: AuthnRequest (via browser) + IdP->>User: Login page + User->>IdP: Credentials + IdP->>User: SAML Response (assertion) + User->>SP: POST assertion to ACS URL + SP->>SP: Validate signature & conditions + SP->>User: Grant access (session created) +``` In IdentityServer, you register each SP using a `SamlServiceProvider` configuration object. This tells IdentityServer the SP's entity identifier, where to deliver assertions (the Assertion Consumer Service URL), and how to communicate. See the [Service Provider Store](/identityserver/saml/service-providers.md) and the [SamlServiceProvider model](/identityserver/saml/configuration.md#samlserviceprovider-model) for details. @@ -45,13 +64,13 @@ In IdentityServer, you register each SP using a `SamlServiceProvider` configurat SAML metadata is an XML document that describes an entity's capabilities: its endpoints, supported bindings, and the certificates it uses for signing and encryption. Both IdPs and SPs publish metadata documents. -Metadata makes federation scalable. Instead of manually exchanging certificates and endpoint URLs out-of-band, parties import each other's metadata and configure trust automatically. Large identity federations — such as InCommon (over 1,000 organizations) — rely on machine-readable metadata to coordinate trust across hundreds or thousands of participants. +Metadata makes federation scalable. Instead of manually exchanging certificates and endpoint URLs out-of-band, parties import each other's metadata and configure trust automatically. IdentityServer publishes its IdP metadata at `/saml/metadata`. Share this URL with each Service Provider during federation setup so they can automatically discover your signing certificates, NameID formats, and endpoint locations. See the [metadata endpoint](/identityserver/saml/endpoints.md#metadata-endpoint) for more details. ## Bindings -SAML bindings define how SAML messages physically travel over HTTP. The protocol payload (the XML message) is the same regardless of binding — the binding determines the transport mechanism. +SAML bindings define how SAML messages physically travel over HTTP. The protocol payload (the XML message) is the same regardless of binding; the binding determines the transport mechanism. IdentityServer supports two bindings: @@ -60,11 +79,26 @@ IdentityServer supports two bindings: The SAML specification also defines **HTTP-Artifact** binding, which sends a short reference token through the browser and resolves the full assertion via a back-channel SOAP call. IdentityServer does not currently support Artifact binding. -You configure the binding per SP via `AssertionConsumerServiceBinding` using the [`SamlBinding` enum](/identityserver/saml/configuration.md#samlbinding). +You configure the binding per SP via the `Binding` property on each [`IndexedEndpoint`](/identityserver/saml/configuration.md#indexedendpoint) in `AssertionConsumerServiceUrls`. This is the current API: + +```csharp +AssertionConsumerServiceUrls = new List +{ + new IndexedEndpoint + { + Location = new Uri("https://sp.example.com/saml/acs"), + Binding = SamlBinding.HttpPost, + Index = 0, + IsDefault = true + } +} +``` + +The [`SamlBinding` enum](/identityserver/saml/configuration.md#samlbinding) defines the available binding values. ## Profiles -SAML profiles are predefined recipes that combine assertions, protocol messages, and bindings into complete workflows for specific use cases. Following a profile is what makes SAML implementations interoperable — without adhering to a profile, a system can produce syntactically valid SAML messages that no other implementation will accept. +SAML profiles are predefined recipes that combine assertions, protocol messages, and bindings into complete workflows for specific use cases. Following a profile is what makes SAML implementations interoperable. Without adhering to a profile, a system can produce syntactically valid SAML messages that no other implementation will accept. The two profiles most relevant to IdentityServer are: @@ -79,11 +113,11 @@ The Name Identifier (NameID) is the value inside an assertion that identifies th The three most common formats are: -* **Persistent**: a stable, opaque identifier that remains the same for a given user-SP pair across all sessions. Use this when the SP needs to correlate the user over time — for example, to maintain account linking or preferences. Persistent identifiers do not reveal the user's real identity at the IdP. +* **Persistent**: a stable, opaque identifier that remains the same for a given user-SP pair across all sessions. Use this when the SP needs to correlate the user over time (for example, to maintain account linking or preferences). Persistent identifiers do not reveal the user's real identity at the IdP. * **Transient**: a session-scoped, one-time identifier that changes with every SSO session. Use this when the SP does not need to recognize the user across sessions (for example, anonymous access or attribute-only scenarios). Transient identifiers offer the best privacy protection. * **emailAddress**: the user's email address. Human-readable and easy to work with, but it exposes personally identifiable information (PII) and couples the identifier to a value that can change. -Mismatched NameID format expectations between an IdP and an SP are a common source of SSO failures. In IdentityServer, you configure the supported formats globally via `SamlOptions.SupportedNameIdFormats` and set a per-SP default via `SamlServiceProvider.DefaultNameIdFormat`. The claim used to resolve a persistent NameID value is set by `DefaultPersistentNameIdentifierClaimType`. See [SamlOptions](/identityserver/saml/configuration.md#samloptions) for the full configuration reference. +IdentityServer currently supports `email` and `unspecified` NameID formats out of the box. Persistent format support is planned for a future release. For custom NameID generation, implement [`ISamlNameIdGenerator`](/identityserver/saml/extensibility.md#isamlnameidgenerator). ## RelayState @@ -97,8 +131,26 @@ IdentityServer preserves RelayState automatically through the authentication flo SAML Single Logout (SLO) is a protocol for coordinating session termination across an entire federation. When a user logs out at one SP or at the IdP, the IdP sends `LogoutRequest` messages to every other SP where that user has an active session, then waits for each SP to confirm. +```mermaid +sequenceDiagram + participant User + participant SP_A as SP A (initiator) + participant IdP as Identity Provider + participant SP_B as SP B + participant SP_C as SP C + + User->>SP_A: Logout + SP_A->>IdP: LogoutRequest + IdP->>IdP: End user session + IdP->>SP_B: LogoutRequest (front-channel) + SP_B-->>IdP: LogoutResponse + IdP->>SP_C: LogoutRequest (front-channel) + SP_C-->>IdP: LogoutResponse + IdP-->>SP_A: LogoutResponse +``` + SLO is powerful in theory but complex in practice. Reliable SLO requires the IdP to track every active session across all SPs. Partial failures are common: an SP may be unreachable, slow to respond, or the user may close the browser before all notifications complete. These partial failures create ambiguous states where some SPs consider the session terminated and others do not. -For this reason, many deployments supplement SLO — or replace it entirely — with short session lifetimes and per-application logout as a simpler fallback. +For this reason, many deployments supplement SLO, or replace it entirely, with short session lifetimes and per-application logout as a simpler fallback. In IdentityServer, you configure SLO per SP by setting `SamlServiceProvider.SingleLogoutServiceUrl`. IdentityServer then sends front-channel logout notifications to all SPs with a configured SLO endpoint when a user's session ends. See the [logout endpoint](/identityserver/saml/endpoints.md#logout-endpoint) and [`ISamlLogoutNotificationService`](/identityserver/saml/extensibility.md#isamllogoutnotificationservice) for customization options. diff --git a/astro/src/content/docs/identityserver/saml/configuration.md b/astro/src/content/docs/identityserver/saml/configuration.md index 04ca1a16..0df860ba 100644 --- a/astro/src/content/docs/identityserver/saml/configuration.md +++ b/astro/src/content/docs/identityserver/saml/configuration.md @@ -1,6 +1,6 @@ --- title: "SAML Configuration" -description: Configuration options and models for the SAML 2.0 Identity Provider feature, including SamlOptions and SamlServiceProvider settings. +description: Configuration options and models for the SAML 2.0 Identity Provider feature, including SamlOptions, Saml2Options, and SamlServiceProvider settings. date: 2026-03-02 sidebar: label: Configuration @@ -21,14 +21,22 @@ builder.Services.AddIdentityServer() .AddSaml(); ``` -`AddSaml()` registers all SAML services and enables the five standard SAML endpoints. The -IdP-initiated SSO endpoint is **not** enabled by default and requires explicit opt-in (see -[Enabling IdP-Initiated SSO](#enabling-idp-initiated-sso) below). +`AddSaml()` registers all SAML services and six SAML endpoints, enabling five of them by default. The IdP-initiated SSO endpoint requires explicit opt-in (see [Enabling IdP-Initiated SSO](#enabling-idp-initiated-sso) below). It can be called with no arguments when all Service Provider configuration is managed via the admin API, or with an options callback to configure protocol-level settings via `Saml2Options`: + +```csharp +builder.Services.AddIdentityServer() + .AddSaml(saml2 => + { + saml2.EntityId = "https://idp.example.com/saml"; + saml2.Metadata.CacheDuration = TimeSpan.FromHours(1); + }); +``` ## SamlOptions -`SamlOptions` controls the global behavior of the SAML 2.0 Identity Provider. Access it via -`IdentityServerOptions.Saml`: +`SamlOptions` controls the global behavior and policy of the SAML 2.0 Identity Provider: how claims are mapped to SAML attributes, how assertions are signed, how NameIDs are resolved, and what tolerances apply to timestamps and request lifetimes. It is distinct from `Saml2Options`, which handles protocol plumbing (entity ID, endpoint paths, metadata generation). + +Access `SamlOptions` via `IdentityServerOptions.Saml` when calling `AddIdentityServer()`: ```csharp // Program.cs @@ -40,10 +48,12 @@ builder.Services.AddIdentityServer(options => }); ``` +Use `SamlOptions` when you need to set defaults that apply across all Service Providers (for example, a shared assertion lifetime, a common set of AuthnContext mappings, or a global signing policy). Individual SPs can override most of these defaults via their own `SamlServiceProvider` configuration. + Available options: * **`MetadataValidityDuration`** - If set, the metadata document includes a `validUntil` attribute. Defaults to 7 days. + IdentityServer-layer setting that, if set, causes the metadata document to include a `validUntil` attribute. Defaults to 7 days. For most deployments, prefer configuring `Saml2Options.Metadata.ExpiryDuration` via the `AddSaml()` callback instead, which operates at the protocol layer and defaults to 5 days. * **`WantAuthnRequestsSigned`** When `true`, the IdP requires all AuthnRequests to be signed. Defaults to `false`. @@ -51,16 +61,13 @@ Available options: * **`DefaultAttributeNameFormat`** Default SAML attribute name format URI for attributes in assertions. Defaults to `uri`. -* **`DefaultPersistentNameIdentifierClaimType`** - Claim type used to resolve a persistent NameID value. Defaults to `ClaimTypes.NameIdentifier`. - * **`DefaultClaimMappings`** - Maps OIDC claim types to SAML attribute names. See below. + Maps OIDC claim types to SAML attribute names. See [Default Claim Mappings](#default-claim-mappings) below. * **`SupportedNameIdFormats`** - Supported NameID formats for the IdP. Defaults to `[ Email, Persistent, Transient, Unspecified ]`. + Supported NameID formats advertised by the IdP. Defaults to `[ EmailAddress, Unspecified ]`. - The NameID format determines how the user is identified to the SP. **Persistent** identifiers are stable and opaque — suitable when the SP needs to correlate the same user across sessions (for example, account linking). **Transient** identifiers are session-scoped and change with each login — best for privacy-sensitive scenarios where the SP does not need a stable identity. **emailAddress** is human-readable but exposes PII and is coupled to a value that can change. Mismatched format expectations are a common source of SSO failures. See [Name Identifiers](/identityserver/saml/concepts.md#name-identifiers) for a full explanation. + The NameID format determines how the user is identified to the SP. **emailAddress** is human-readable but exposes PII and is coupled to a value that can change. **Unspecified** leaves the format to the IdP's discretion. Persistent and transient formats are planned for a future release. Mismatched format expectations are a common source of SSO failures. See [Name Identifiers](/identityserver/saml/concepts.md#name-identifiers) for a full explanation. * **`DefaultClockSkew`** Clock skew tolerance for validating SAML message timestamps. Defaults to 5 minutes. @@ -74,10 +81,19 @@ Available options: * **`MaxRelayStateLength`** Maximum length (in UTF-8 bytes) of the RelayState parameter. Defaults to 80. - RelayState is an opaque string that an SP includes in its `AuthnRequest` to preserve application state — typically the URL the user originally requested — across the SSO round-trip. IdentityServer echoes it back unchanged so the SP can redirect the user to the right page after authentication. The SAML specification recommends keeping RelayState short; this limit enforces that guidance. See [RelayState](/identityserver/saml/concepts.md#relaystate) for more context. + RelayState is an opaque string that an SP includes in its `AuthnRequest` to preserve application state (typically the URL the user originally requested) across the SSO round-trip. IdentityServer echoes it back unchanged so the SP can redirect the user to the right page after authentication. The SAML specification recommends keeping RelayState short; this limit enforces that guidance. See [RelayState](/identityserver/saml/concepts.md#relaystate) for more context. + +* **`DefaultAuthnContextMappings`** + Maps OIDC `acr`/`amr` values to SAML `AuthnContextClassRef` URIs. Used when an SP requests a specific AuthnContext and IdentityServer needs to translate the user's authentication method into the corresponding SAML URI. Type: `Dictionary`. Defaults to empty. Per-SP overrides are set via `SamlServiceProvider.AuthnContextMappings`. + +* **`DefaultAssertionLifetime`** + How long issued assertions are considered valid. Type: `TimeSpan`. Defaults to 5 minutes. Per-SP overrides are set via `SamlServiceProvider.AssertionLifetime`. + +* **`EmailNameIdClaimType`** + The claim type used to resolve an email-format NameID. Defaults to `ClaimTypes.Email`. Per-SP overrides are set via `SamlServiceProvider.EmailNameIdClaimType`. * **`UserInteraction`** - Configures SAML endpoint paths. See below. + Configures SAML endpoint paths. See [SamlUserInteractionOptions](#samluserinteractionoptions) below. ### Default Claim Mappings @@ -124,76 +140,138 @@ The full URL for each endpoint is formed by combining the base URL of the Identi the `Route` prefix and the individual path suffix. For example, the metadata endpoint is available at `https://your-idp.example.com/saml/metadata` by default. +## Saml2Options + +`Saml2Options` is the protocol-level configuration class for the SAML 2.0 IdP. While `SamlOptions` controls behavior and policy (claim mappings, assertion lifetime, signing defaults), `Saml2Options` controls the SAML protocol plumbing: the IdP's entity identity, which endpoint paths and HTTP bindings are active, and how the metadata document is generated and cached. + +It lives in the `Duende.IdentityServer.Saml.Configuration` namespace and is configured via the `AddSaml()` options callback: + +```csharp +builder.Services.AddIdentityServer() + .AddSaml(saml2 => + { + saml2.EntityId = "https://idp.example.com/saml"; + saml2.Endpoints.SingleSignOnServicePath = "/saml/sso"; + saml2.Metadata.CacheDuration = TimeSpan.FromHours(1); + }); +``` + +Use `Saml2Options` when you need to control the IdP's published identity (entity ID), the URL paths it listens on, or the shape of the metadata document it serves to Service Providers. Most deployments do not need to set `EntityId` explicitly; the default (`{host}/saml`) is suitable for standard configurations. + +Available options: + +* **`EntityId`** (`string?`) + The SAML entity ID of this IdP. If not set, IdentityServer derives it from the host URL combined with `EntityIdPath` (resulting in `{host}/saml` by default). Most deployments do not need to set this explicitly. Defaults to `null`. + +* **`EntityIdPath`** (`string`) + Path component appended to the OIDC issuer URL when `EntityId` is not explicitly set. Defaults to `/saml`. + +* **`Endpoints.SingleSignOnServicePath`** (`string`) + URL path for the SSO endpoint. Defaults to `/saml/signin`. + +* **`Endpoints.MetadataPath`** (`string`) + URL path for the metadata endpoint. Defaults to `/saml/metadata`. + +* **`Endpoints.SingleSignOnServiceBindings`** (`ICollection`) + HTTP bindings accepted by the SSO endpoint. Defaults to both HTTP-Redirect and HTTP-POST. + +* **`Metadata.Enabled`** (`bool`) + Whether the metadata endpoint is active. Defaults to `true`. + +* **`Metadata.CacheDuration`** (`TimeSpan`) + How long clients should cache the metadata document. Defaults to 1 hour. + +* **`Metadata.ExpiryDuration`** (`TimeSpan`) + Protocol-layer setting that controls how far in the future the metadata `validUntil` attribute is set. Defaults to **5 days**. This is the preferred way to configure metadata expiry. Set it via the `AddSaml()` callback on `Saml2Options`. It is distinct from `SamlOptions.MetadataValidityDuration` (the IdentityServer-layer property accessed via `IdentityServerOptions.Saml`), which defaults to 7 days. When both are configured, `Saml2Options.Metadata.ExpiryDuration` takes effect at the protocol level. + ## SamlServiceProvider Model -`SamlServiceProvider` represents a registered SAML 2.0 Service Provider configuration. +`SamlServiceProvider` represents a registered SAML 2.0 Service Provider. Each SP has its own entity ID, ACS endpoints, signing certificates, and claim configuration. SPs can be registered statically in code or managed dynamically via a custom store. + +Most properties on `SamlServiceProvider` are optional overrides of the global defaults set in `SamlOptions`. When a property is `null`, the corresponding `SamlOptions` default applies. This lets you configure sensible defaults once and only specify per-SP values where behavior needs to differ. Available options: -* **`EntityId`** - The SP's entity identifier URI, as declared in its SAML metadata. Required. +* **`EntityId`** (`string`) + The SP's entity identifier, as declared in its SAML metadata. Required. -* **`DisplayName`** +* **`DisplayName`** (`string`) Human-readable name shown in logs and consent screens. Required. -* **`Description`** +* **`Description`** (`string?`) Optional description. Defaults to `null`. -* **`Enabled`** +* **`Enabled`** (`bool`) When `false`, all SAML requests from this SP are rejected. Defaults to `true`. -* **`ClockSkew`** +* **`ClockSkew`** (`TimeSpan?`) Per-SP clock skew override. Uses `SamlOptions.DefaultClockSkew` when `null`. Defaults to `null`. -* **`RequestMaxAge`** +* **`RequestMaxAge`** (`TimeSpan?`) Per-SP request maximum age. Uses `SamlOptions.DefaultRequestMaxAge` when `null`. Defaults to `null`. -* **`AssertionConsumerServiceUrls`** - ACS URLs where SAML responses will be delivered. At least one is required. - -* **`AssertionConsumerServiceBinding`** - SAML binding for the ACS (`HttpPost` or `HttpRedirect`). - -* **`SingleLogoutServiceUrl`** - SP's Single Logout Service endpoint. Required for SLO support. Defaults to `null`. - -* **`RequireSignedAuthnRequests`** +* **`AssertionConsumerServiceUrls`** (`ICollection`) + ACS endpoints where SAML responses will be delivered. At least one is required. Each entry is an `IndexedEndpoint` that specifies the URL, binding, ordering index, and whether it is the default endpoint. See [IndexedEndpoint](#indexedendpoint) below. + + ```csharp + AssertionConsumerServiceUrls = new List + { + new IndexedEndpoint + { + Location = new Uri("https://sp.example.com/saml/acs"), + Binding = SamlBinding.HttpPost, + Index = 0, + IsDefault = true + } + } + ``` + +* **`SingleLogoutServiceUrl`** (`SamlEndpointType?`) + SP's Single Logout Service endpoint, expressed as a `SamlEndpointType` with a `Location` (Uri) and `Binding` (SamlBinding). Required for SLO support. Defaults to `null`. See [SamlEndpointType](#samlendpointtype) below. + +* **`RequireSignedAuthnRequests`** (`bool`) When `true`, unsigned AuthnRequests from this SP are rejected. Defaults to `false`. -* **`SigningCertificates`** +* **`SigningCertificates`** (`ICollection?`) Certificates used to verify SP-signed messages. Defaults to `null`. -* **`EncryptionCertificates`** - Certificates used to encrypt assertions for this SP. Defaults to `null`. +* **`AllowIdpInitiated`** (`bool`) + When `true`, IdP-initiated SSO is allowed for this SP. Defaults to `false`. -* **`EncryptAssertions`** - When `true`, assertions are encrypted using `EncryptionCertificates`. Defaults to `false`. +* **`ClaimMappings`** (`ReadOnlyDictionary`) + Per-SP claim-to-attribute mappings (internal claim name → SAML attribute URI) that override `SamlOptions.DefaultClaimMappings`. Defaults to `{}`. -* **`RequireConsent`** - When `true`, the user is always shown a consent screen. Defaults to `false`. +* **`DefaultNameIdFormat`** (`string`) + Default NameID format to use when the SP does not specify one. Defaults to `urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified`. -* **`AllowIdpInitiated`** - When `true`, IdP-initiated SSO is allowed for this SP. Defaults to `false`. +* **`SigningBehavior`** (`SamlSigningBehavior?`) + Per-SP signing behavior. Uses `SamlOptions.DefaultSigningBehavior` when `null`. Defaults to `null`. -* **`ClaimMappings`** - Per-SP claim-to-attribute mappings that override `SamlOptions.DefaultClaimMappings`. Defaults to `{}`. +* **`AssertionLifetime`** (`TimeSpan?`) + Per-SP override for how long issued assertions are valid. Uses `SamlOptions.DefaultAssertionLifetime` when `null`. Defaults to `null`. -* **`DefaultNameIdFormat`** - Default NameID format to use when the SP does not specify one. Defaults to `urn:...unspecified`. +* **`AllowedScopes`** (`ICollection`) + Scopes associated with this SP. Used to determine which identity resources (and their claim types) are available for inclusion in assertions. When empty, all mapped claims are included. Defaults to empty. -* **`DefaultPersistentNameIdentifierClaimType`** - Per-SP override for the claim type used to resolve a persistent NameID. Defaults to `null`. +* **`AuthnContextMappings`** (`Dictionary`) + Per-SP override for `acr`/`amr` → `AuthnContextClassRef` URI mappings. Overrides `SamlOptions.DefaultAuthnContextMappings` when set. Defaults to empty. -* **`SigningBehavior`** - Per-SP signing behavior. Uses `SamlOptions.DefaultSigningBehavior` when `null`. Defaults to `null`. +* **`RequestedClaimTypes`** (`List`) + Claim types this SP expects in assertions. Used to drive claim population for the SP. + +* **`EmailNameIdClaimType`** (`string?`) + Per-SP override for the claim used to resolve an email-format NameID. Uses `SamlOptions.EmailNameIdClaimType` when `null`. Defaults to `null`. + +* **`AllowedSignatureAlgorithms`** (`List?`) + Signature algorithms this SP accepts. When `null`, the IdP's default algorithm is used. Defaults to `null`. -## Enums +## Enums and Value Types ### SamlBinding SAML bindings define how messages travel over HTTP. HTTP-Redirect encodes the message into the URL query string, which works well for small messages such as `AuthnRequest` but is limited by URL length constraints. HTTP-POST encodes the message in a hidden HTML form field and submits it automatically, making it the right choice for larger payloads (such as assertions with many attributes) and for keeping message content out of server access logs. See [Bindings](/identityserver/saml/concepts.md#bindings) for a deeper explanation. -Defines the SAML protocol binding used for message transport: +`SamlBinding` is used in two places: on `IndexedEndpoint` (for each ACS endpoint in `AssertionConsumerServiceUrls`) and on `SamlEndpointType` (for `SingleLogoutServiceUrl`). | Value | Description | | -------------- | ------------------------------------------------------------------------------------- | @@ -208,15 +286,14 @@ Controls what elements are signed in SAML responses: | Value | Description | | --------------- | ------------------------------------------------------------------------------------- | -| `DoNotSign` | No signing. For testing only — do not use in production. | +| `DoNotSign` | No signing. For testing only. Do not use in production. | | `SignResponse` | Signs the entire SAML `` element. | | `SignAssertion` | Signs the `` element inside the response. **Recommended.** | | `SignBoth` | Signs both the `` and the ``. Maximum security, larger messages. | ### SamlEndpointType -`SamlEndpointType` is a class (not an enum) that represents a SAML endpoint with a location and -binding. Used for `SamlServiceProvider.SingleLogoutServiceUrl`: +`SamlEndpointType` is a class that pairs a URL location with a SAML binding. It is used specifically for `SamlServiceProvider.SingleLogoutServiceUrl` to describe where the SP's SLO service lives and which HTTP binding it accepts. ```csharp new SamlServiceProvider @@ -230,6 +307,46 @@ new SamlServiceProvider } ``` +Properties: + +* **`Location`** (`Uri`): The URL of the endpoint. +* **`Binding`** (`SamlBinding`): The HTTP binding the endpoint accepts. + +### IndexedEndpoint + +`IndexedEndpoint` represents a single Assertion Consumer Service (ACS) endpoint on a Service Provider. It extends the basic location-and-binding pair with an index (for ordering when multiple ACS endpoints are registered) and an optional default flag. + +`IndexedEndpoint` is used as the element type of `SamlServiceProvider.AssertionConsumerServiceUrls`. + +Properties: + +* **`Location`** (`Uri`): The ACS URL where SAML responses are delivered. +* **`Binding`** (`SamlBinding`): The HTTP binding the ACS endpoint accepts (`HttpPost` or `HttpRedirect`). +* **`Index`** (`int`): Integer index used to order multiple ACS endpoints. Lower values take precedence. +* **`IsDefault`** (`bool?`): When `true`, this endpoint is the default ACS. When multiple endpoints are registered, exactly one should be marked as default. + +Example with multiple ACS endpoints: + +```csharp +AssertionConsumerServiceUrls = new List +{ + new IndexedEndpoint + { + Location = new Uri("https://sp.example.com/saml/acs"), + Binding = SamlBinding.HttpPost, + Index = 0, + IsDefault = true + }, + new IndexedEndpoint + { + Location = new Uri("https://sp.example.com/saml/acs/redirect"), + Binding = SamlBinding.HttpRedirect, + Index = 1, + IsDefault = false + } +} +``` + ## Enabling IdP-Initiated SSO IdP-initiated SSO is disabled by default. To enable it, set the endpoint option and configure diff --git a/astro/src/content/docs/identityserver/saml/endpoints.md b/astro/src/content/docs/identityserver/saml/endpoints.md index 1589cf1b..fd40708a 100644 --- a/astro/src/content/docs/identityserver/saml/endpoints.md +++ b/astro/src/content/docs/identityserver/saml/endpoints.md @@ -61,7 +61,7 @@ Service Provider's Assertion Consumer Service (ACS) URL using the configured bin **Path**: `/saml/idp-initiated` **Methods**: GET, POST -**Enabled by default**: No — requires explicit opt-in +**Enabled by default**: No (requires explicit opt-in) Supports IdP-initiated SSO flows, where the IdP starts the authentication without receiving an `AuthnRequest` from the SP. The SP must have `AllowIdpInitiated = true` set in its @@ -101,7 +101,7 @@ Processes SAML `LogoutResponse` messages returned by Service Providers after the logout notification from IdentityServer. This endpoint completes the SAML SLO round-trip. :::note -SAML Single Logout is inherently complex: it requires coordinated session termination across every SP that participated in the user's session. Partial failures are common — an SP may be unreachable, slow to respond, or the user may close the browser before all notifications complete, leaving some SPs with an active session while others consider it terminated. Many deployments supplement SLO with short session lifetimes as a simpler fallback. See [Single Logout](/identityserver/saml/concepts.md#single-logout) for more background. +SAML Single Logout is inherently complex: the process requires coordinated session termination across every SP that participated in the user's session. Partial failures are common. An SP may be unreachable, slow to respond, or the user may close the browser before all notifications complete, leaving some SPs with an active session while others consider the session terminated. Many deployments supplement SLO with short session lifetimes as a simpler fallback. See [Single Logout](/identityserver/saml/concepts.md#single-logout) for more background. ::: ## Customizing Endpoint Paths @@ -122,4 +122,4 @@ builder.Services.AddIdentityServer(options => }); ``` -See [SAML Configuration](/identityserver/saml/configuration/) for full path option documentation. +See [SamlUserInteractionOptions](/identityserver/saml/configuration.md#samluserinteractionoptions) for full path option documentation. diff --git a/astro/src/content/docs/identityserver/saml/extensibility.md b/astro/src/content/docs/identityserver/saml/extensibility.md index d7d99ad3..770f212b 100644 --- a/astro/src/content/docs/identityserver/saml/extensibility.md +++ b/astro/src/content/docs/identityserver/saml/extensibility.md @@ -1,6 +1,6 @@ --- title: "SAML Extensibility" -description: Extensibility interfaces for customizing SAML 2.0 Identity Provider behavior, including claims mapping, interaction, and response generation. +description: Extensibility interfaces for customizing SAML 2.0 Identity Provider behavior, including NameID generation, SSO response generation, metadata, AuthnRequest validation, interaction, logout, and sign-in state storage. date: 2026-03-02 sidebar: label: Extensibility @@ -16,167 +16,474 @@ IdentityServer's SAML 2.0 Identity Provider feature exposes several extensibilit you can implement to customize SAML behavior. All interfaces are registered in the DI container and can be replaced with custom implementations. -## ISamlClaimsMapper +--- + +## ISamlInteractionService + + +TODO docs: `ISamlInteractionService` is being replaced. In the current implementation, SAML-specific request context is accessed by calling `GetAuthenticationContextAsync` on `IIdentityServerInteractionService` and pattern-matching the result to `SamlAuthenticationContext`. The concepts below still apply, but the access pattern is changing. + + +To access SAML-specific request details from your login UI, call `GetAuthenticationContextAsync` on `IIdentityServerInteractionService`. The returned context can be pattern-matched to `SamlAuthenticationContext` for SAML requests or `AuthorizationRequest` for OIDC requests. This is **not required for standard login flows** — your existing login pages work with SAML automatically because IdentityServer redirects to your login page with a `returnUrl` regardless of protocol. + +```csharp +public interface ISamlInteractionService +{ + Task GetAuthenticationRequestContextAsync( + CancellationToken ct = default); + + Task StoreRequestedAuthnContextResultAsync( + bool requestedAuthnContextRequirementsWereMet, + CancellationToken ct = default); +} +``` + +### When to Use + +Access SAML-specific context from your login UI pages when you need to: -Customizes how user claims are mapped to SAML attributes in the assertion. +* Display SAML-specific information about the requesting SP +* Check the SP's `RequestedAuthnContext` requirements and adjust your authentication flow accordingly (e.g., enforce MFA when the SP requests a specific `AuthnContext` class) +* Report back to IdentityServer whether the user's authentication met the SP's `RequestedAuthnContext` requirements + +For standard login, consent, and logout flows, no SAML-specific code is needed in your pages. + +--- + +## ISaml2SsoInteractionResponseGenerator + +`ISaml2SsoInteractionResponseGenerator` determines what interaction (login or error) +is required during a SAML sign-in flow. After an `AuthnRequest` is received and validated, +IdentityServer calls this interface to decide whether the user needs to be redirected to the login +page or whether the flow can proceed directly to assertion generation. + +The default implementation handles standard login flows. Override it when you need custom step-up +authentication logic or any other non-standard interaction decision. ```csharp -public interface ISamlClaimsMapper +public interface ISaml2SsoInteractionResponseGenerator { - Task> MapClaimsAsync(SamlClaimsMappingContext context); + Task ProcessInteractionAsync( + SamlServiceProvider sp, + AuthNRequest request, + CancellationToken ct = default); } ``` ### When to Use -Override this interface when the built-in claim mapping (configured via -`SamlOptions.DefaultClaimMappings` and `SamlServiceProvider.ClaimMappings`) is not flexible enough. -Registering a custom `ISamlClaimsMapper` **completely replaces** the default mapping logic. +Override this interface to customize the interaction flow for SAML sign-in requests. For example, +to implement custom step-up authentication logic. + +### Registration + +```csharp +// Program.cs +builder.Services.AddScoped(); +``` + +--- + +## ISamlLogoutNotificationService + +`ISamlLogoutNotificationService` builds the set of front-channel logout notifications that +IdentityServer sends to SAML Service Providers when a user logs out. When a logout is initiated, +IdentityServer calls this service to determine which SPs should be notified and what messages to +send them. + +The default implementation sends a SAML `LogoutRequest` to each SP that has a configured +`SingleLogoutServiceUrl`. Override it to customize which SPs receive notifications or to modify +the logout messages. -### Context +```csharp +public interface ISamlLogoutNotificationService +{ + Task> GetSamlFrontChannelLogoutsAsync( + LogoutNotificationContext context, + CancellationToken ct); +} +``` -`SamlClaimsMappingContext` provides: +### When to Use -* `UserClaims` — the user's claims to be mapped to SAML attributes -* `ServiceProvider` — the `SamlServiceProvider` that initiated the request +Override this interface to customize which Service Providers receive logout notifications, or to +modify the logout messages sent. ### Registration -Register via DI by replacing the default: +```csharp +// Program.cs +builder.Services.AddScoped(); +``` + +--- + +## ISamlNameIdGenerator + +`ISamlNameIdGenerator` is responsible for generating the SAML `NameID` value included in +assertions sent to Service Providers. The `NameID` identifies the subject of the assertion (typically +the authenticated user) in a format the SP understands. It is called during assertion generation, +after the user has authenticated and the requested `NameID` format has been resolved. + +The default implementation handles the most common formats: email address and +unspecified. Register a custom implementation to support additional `NameID` formats or to derive +the `NameID` value from non-standard claims. + +```csharp +public interface ISamlNameIdGenerator +{ + Task GenerateAsync(NameIdGenerationContext context, CancellationToken ct); +} + +public sealed class NameIdGenerationContext +{ + public required ClaimsPrincipal Subject { get; init; } + public required SamlServiceProvider ServiceProvider { get; init; } + public required string ResolvedFormat { get; init; } + public string? SPNameQualifier { get; init; } +} + +public sealed class NameIdGenerationResult +{ + public NameId? NameId { get; private init; } + public SamlError? Error { get; private init; } + public bool IsError => Error is not null; + public static NameIdGenerationResult Success(NameId nameId) => ...; + public static NameIdGenerationResult Failure(string statusCode, string subStatusCode, string message) => ...; +} +``` + +### When to Use + +Override `ISamlNameIdGenerator` when: + +* You need to support a custom `NameID` format not handled by the default implementation. +* The `NameID` value must be derived from a non-standard claim or computed from multiple claims. +* You need SP-specific `NameID` generation logic based on `context.ServiceProvider`. + +### Registration ```csharp // Program.cs -builder.Services.AddScoped(); +builder.Services.AddScoped(); ``` ### Example ```csharp -// MyClaimsMapper.cs -public class MyClaimsMapper : ISamlClaimsMapper +// MyNameIdGenerator.cs +public class MyNameIdGenerator : ISamlNameIdGenerator { - public Task> MapClaimsAsync(SamlClaimsMappingContext context) + public Task GenerateAsync( + NameIdGenerationContext context, + CancellationToken ct) { - var attributes = context.UserClaims - .Where(c => c.Type == "email") - .Select(c => new SamlAttribute - { - Name = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", - Values = new[] { c.Value } - }); + // Example: use a custom "employee_id" claim as the NameID value + var employeeId = context.Subject.FindFirst("employee_id")?.Value; + if (employeeId is null) + return Task.FromResult(NameIdGenerationResult.Failure( + StatusCodes.Responder, StatusCodes.UnknownPrincipal, + "Employee ID claim not found.")); + + var nameId = new NameId(employeeId, context.ResolvedFormat); + return Task.FromResult(NameIdGenerationResult.Success(nameId)); + } +} +``` + +--- + +## IIdpInitiatedSsoService + +`IIdpInitiatedSsoService` enables IdP-initiated SSO, a flow where the Identity Provider sends a +SAML assertion to a Service Provider without first receiving an `AuthnRequest`. This is commonly +used in application portal pages (e.g., a "My Apps" dashboard) where the user is already +authenticated and clicks a tile to launch an SP application. - return Task.FromResult(attributes); +The built-in endpoint `/saml/idp-initiated?spEntityId={entityId}` uses this service internally. +You can also inject `IIdpInitiatedSsoService` directly into your own Razor Pages or controllers +to generate and send the SAML response programmatically. Because this flow bypasses the normal +SP-initiated request, **the caller is responsible for anti-forgery protection** (e.g., ensuring +the request originates from a legitimate authenticated session). + +```csharp +public interface IIdpInitiatedSsoService +{ + Task CreateResponseAsync( + HttpContext httpContext, + string spEntityId, + string? relayState, + CancellationToken ct); + + Task CreateResponseAsync( + HttpContext httpContext, + string spEntityId, + CancellationToken ct); +} +``` + +### When to Use + +Use `IIdpInitiatedSsoService` when: + +* You are building a portal page where authenticated users can launch SP applications with a single + click, without the SP initiating the flow. +* You need to pass a `relayState` value to the SP (e.g., a deep-link URL within the SP application). +* You want to trigger IdP-initiated SSO from custom application code rather than the built-in + endpoint. + +### Registration + +`IIdpInitiatedSsoService` is registered by the SAML plugin and does not need to be replaced. +Inject it directly into your Razor Page or controller: + +```csharp +// MyAppsPage.cshtml.cs +public class MyAppsPageModel : PageModel +{ + private readonly IIdpInitiatedSsoService _ssoService; + + public MyAppsPageModel(IIdpInitiatedSsoService ssoService) + => _ssoService = ssoService; + + public async Task OnPostLaunchAsync(string spEntityId) + { + var result = await _ssoService.CreateResponseAsync(HttpContext, spEntityId, ct: HttpContext.RequestAborted); + // Handle result (e.g., write the auto-submit form to the response) + return result.ToActionResult(); } } ``` --- -## ISamlInteractionService +## ISaml2SsoResponseGenerator -Provides services for the login UI to communicate with IdentityServer during SAML authentication -flows. +`ISaml2SsoResponseGenerator` generates the SAML `` element sent back to the Service +Provider after a successful (or failed) authentication. It is called at the end of the sign-in +pipeline, after interaction is complete and the user's identity has been established. The response +includes the SAML assertion with the subject, attributes, and conditions the SP expects. + +The default implementation produces a standards-compliant signed response. Override this interface +when you need full control over the SAML response structure. For example, to add custom +attributes, change signing behavior, or embed additional assertion elements required by a specific +SP or federation. ```csharp -public interface ISamlInteractionService +public interface ISaml2SsoResponseGenerator { - Task GetAuthenticationRequestContextAsync( - CancellationToken ct = default); + Task CreateResponse( + ValidatedAuthnRequest validatedAuthnRequest, + CancellationToken ct); - Task StoreRequestedAuthnContextResultAsync( - bool requestedAuthnContextRequirementsWereMet, - CancellationToken ct = default); + Task CreateErrorResponse( + ValidatedAuthnRequest validatedAuthnRequest, + Saml2InteractionResponse interactionResponse, + CancellationToken ct); } ``` ### When to Use -Inject `ISamlInteractionService` into your login UI pages to: +Override `ISaml2SsoResponseGenerator` when: -* Retrieve the current SAML authentication request context (SP name, requested AuthnContext, etc.) -* Report back to IdentityServer whether the user's authentication met the SP's `RequestedAuthnContext` - requirements +* You need to add custom SAML attributes or assertion elements not supported by the default + implementation. +* You need to change how the response or assertion is signed or encrypted. +* You need SP-specific response customization based on `validatedAuthnRequest.ServiceProvider`. + +### Registration -If `StoreRequestedAuthnContextResultAsync` is called with `false`, IdentityServer will include a -SAML `NoAuthnContext` status code in the response, as per SAML Core spec section 3.3.2.2.1. +```csharp +// Program.cs +builder.Services.AddScoped(); +``` --- -## ISamlSigninInteractionResponseGenerator +## ISaml2MetadataResponseGenerator -Determines what interaction (login, consent, error) is required during a SAML sign-in flow. +`ISaml2MetadataResponseGenerator` generates the IdP metadata document served at the +`/saml/metadata` endpoint. SAML metadata describes the IdP's capabilities, endpoints, and signing +keys to Service Providers and federation operators. SPs typically fetch this document during +initial configuration to establish trust. + +The default implementation produces a standards-compliant metadata document from the configured +`Saml2Options` and signing keys. Override this interface to add custom metadata elements +such as organization information, contact details, additional key descriptors, or +federation-specific extensions required by specific SPs or federation operators. ```csharp -public interface ISamlSigninInteractionResponseGenerator +public interface ISaml2MetadataResponseGenerator { - Task ProcessInteractionAsync( - SamlServiceProvider sp, - AuthNRequest request, - CancellationToken ct = default); + Task GenerateMetadataAsync( + string issuer, + IEnumerable signingKeys, + Saml2Options options, + string baseUrl, + CancellationToken ct); } ``` ### When to Use -Override this interface to customize the interaction flow for SAML sign-in requests — for example, -to implement custom step-up authentication logic, or to enforce per-SP consent requirements. +Override `ISaml2MetadataResponseGenerator` when: -The default implementation (`DefaultSamlSigninInteractionResponseGenerator`) handles standard -login and consent flows. +* You need to include organization or contact information in the metadata document. +* A federation operator or SP requires custom metadata extensions. +* You need to advertise additional key descriptors or endpoint bindings. ### Registration ```csharp // Program.cs -builder.Services.AddScoped(); +builder.Services.AddScoped(); ``` --- -## ISamlLogoutNotificationService +## IAuthnRequestValidator + +`IAuthnRequestValidator` validates incoming SAML `AuthnRequest` messages from Service Providers. +It is called early in the sign-in pipeline, before any interaction begins. Validation ensures the +request is well-formed, the SP is registered, the signature is valid, and the requested ACS URL +is permitted. -Builds the front-channel logout notifications that IdentityServer sends to SAML Service Providers -when a user logs out. +The default implementation enforces signature requirements, checks SP registration, and validates +ACS URLs. Override this interface to add custom business rules on top of the default validation. +For example, restricting which SPs can request certain `AuthnContext` classes, enforcing IP-based +access controls, or applying time-of-day restrictions. ```csharp -public interface ISamlLogoutNotificationService +public interface IAuthnRequestValidator { - Task> GetSamlFrontChannelLogoutsAsync( - LogoutNotificationContext context, + Task ValidateAsync( + ValidatedAuthnRequest request, CancellationToken ct); } ``` ### When to Use -Override this interface to customize which Service Providers receive logout notifications, or to -modify the logout messages sent. The default implementation sends a SAML `LogoutRequest` to each -SP that has a configured `SingleLogoutServiceUrl`. +Override `IAuthnRequestValidator` when: + +* You need to enforce custom business rules on incoming `AuthnRequest` messages beyond what the default + implementation checks. +* You want to restrict which SPs can request specific `AuthnContext` classes. +* You need to apply IP-based, time-based, or other contextual access controls at the request + validation stage. ### Registration ```csharp // Program.cs -builder.Services.AddScoped(); +builder.Services.AddScoped(); +``` + +### Example + +```csharp +// MyAuthnRequestValidator.cs +public class MyAuthnRequestValidator : IAuthnRequestValidator +{ + private readonly IAuthnRequestValidator _default; + + public MyAuthnRequestValidator(IAuthnRequestValidator defaultValidator) + => _default = defaultValidator; + + public async Task ValidateAsync( + ValidatedAuthnRequest request, + CancellationToken ct) + { + // Run default validation first + var result = await _default.ValidateAsync(request, ct); + if (!result.IsError) + { + // Add custom rule: only allow SP "https://partner.example.com" during business hours + if (request.ServiceProvider.EntityId == "https://partner.example.com" + && DateTime.UtcNow.Hour is < 8 or > 18) + { + return AuthnRequestValidationResult.Failure("Access outside business hours is not permitted."); + } + } + return result; + } +} ``` --- -## ISamlFrontChannelLogout +## ISamlSigninStateStore + +`ISamlSigninStateStore` persists SAML sign-in request state between the initial SSO request and +the callback after the user has authenticated. Because SAML sign-in involves a redirect to the +login UI and back, the original request context (SP entity ID, ACS URL, relay state, etc.) must +be stored somewhere durable for the duration of the interaction. + +The default implementation stores state in memory (suitable for development and testing). +For production deployments, use the Entity Framework Core implementation that ships with +IdentityServer, or implement a custom store backed by your own persistence layer. -Represents a single front-channel logout notification to be sent to a Service Provider. This is -a data interface returned by `ISamlLogoutNotificationService`; you typically do not need to -implement it directly. +State is retained after a successful callback to allow browser retries (e.g., if the user +navigates back). TTL-based expiry is the primary cleanup mechanism; `RemoveSigninRequestStateAsync` +is called on explicit cleanup paths. ```csharp -public interface ISamlFrontChannelLogout +public interface ISamlSigninStateStore { - SamlBinding SamlBinding { get; } - Uri Destination { get; } - string EncodedContent { get; } - string? RelayState { get; } + Task StoreSigninRequestStateAsync(SamlAuthenticationState state, CancellationToken ct = default); + Task RetrieveSigninRequestStateAsync(StateId stateId, CancellationToken ct = default); + Task RemoveSigninRequestStateAsync(StateId stateId, CancellationToken ct = default); } ``` -Each instance represents a SAML `LogoutRequest` (or response) message encoded for delivery to a -specific SP via the specified binding and destination URL. +### When to Use + +Override `ISamlSigninStateStore` when: + +* You need a custom persistence mechanism beyond the built-in in-memory or EF Core implementations. +* You want to store sign-in state in a specific distributed cache (Redis, etc.) for your infrastructure. +* You need custom TTL or cleanup behavior for in-flight SSO requests. + +### Registration + +```csharp +// Program.cs +builder.Services.AddScoped(); +``` + +### Example + +```csharp +// MyDistributedSamlSigninStateStore.cs +public class MyDistributedSamlSigninStateStore : ISamlSigninStateStore +{ + private readonly IDistributedCache _cache; + + public MyDistributedSamlSigninStateStore(IDistributedCache cache) + => _cache = cache; + + public async Task StoreSigninRequestStateAsync( + SamlAuthenticationState state, + CancellationToken ct = default) + { + var stateId = StateId.New(); + var json = JsonSerializer.Serialize(state); + await _cache.SetStringAsync(stateId.Value, json, + new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15) }, + ct); + return stateId; + } + + public async Task RetrieveSigninRequestStateAsync( + StateId stateId, + CancellationToken ct = default) + { + var json = await _cache.GetStringAsync(stateId.Value, ct); + return json is null ? null : JsonSerializer.Deserialize(json); + } + + public Task RemoveSigninRequestStateAsync(StateId stateId, CancellationToken ct = default) + => _cache.RemoveAsync(stateId.Value, ct); +} +``` diff --git a/astro/src/content/docs/identityserver/saml/index.md b/astro/src/content/docs/identityserver/saml/index.md index d7fc17b5..60c8bb9f 100644 --- a/astro/src/content/docs/identityserver/saml/index.md +++ b/astro/src/content/docs/identityserver/saml/index.md @@ -33,11 +33,26 @@ interoperability with existing SAML-based systems. If you are new to SAML 2.0 or want a refresher on the protocol's core building blocks, see [SAML 2.0 Concepts](/identityserver/saml/concepts.md) for an overview of assertions, bindings, metadata, Name Identifiers, and other key concepts before diving into configuration. +## What's Included + +The SAML 2.0 IdP feature is a comprehensive implementation covering the full SP-initiated and IdP-initiated SSO flows, logout, and a range of extensibility points. Key capabilities include: + +- **SP-initiated SSO**: HTTP-Redirect and HTTP-POST bindings for authentication requests +- **IdP-initiated SSO**: opt-in support for portal or launcher pages that push assertions to SPs without a prior request +- **Single Logout (SLO)**: front-channel logout notifications to registered SPs +- **Assertion signing**: per-SP configuration of signing algorithms +- **NameID format support**: email and unspecified formats (persistent planned for a future release) +- **AuthnContext class mapping**: maps OIDC `acr`/`amr` values to SAML AuthnContext class URIs +- **Per-SP claim mappings**: transform and filter claims before they are included in assertions +- **Extensibility interfaces**: customize NameID generation, response generation, metadata, and more + ## Quick Setup +The following steps show the minimum configuration needed to get SAML 2.0 working. For a full reference of all options, see the pages in this section. + ### 1. Register SAML Services -Call `AddSaml()` on the IdentityServer builder: +Call `AddSaml()` on the IdentityServer builder to enable all SAML endpoints (IdP-initiated SSO requires explicit opt-in): ```csharp // Program.cs @@ -45,15 +60,11 @@ builder.Services.AddIdentityServer() .AddSaml(); ``` -This enables all SAML endpoints except IdP-initiated SSO (which requires explicit opt-in). - ### 2. Register Service Providers -Register your SAML Service Providers using the in-memory store (for development/testing) or a -custom `ISamlServiceProviderStore` implementation (for production): +Register Service Providers using the in-memory store for development, the EF Core store for production, or implement a custom `ISamlServiceProviderStore`: ```csharp -// Program.cs builder.Services.AddIdentityServer() .AddSaml() .AddInMemorySamlServiceProviders(new[] @@ -62,16 +73,29 @@ builder.Services.AddIdentityServer() { EntityId = "https://sp.example.com", DisplayName = "Example SP", - AssertionConsumerServiceUrls = new[] { new Uri("https://sp.example.com/acs") }, - AssertionConsumerServiceBinding = SamlBinding.HttpPost, + AssertionConsumerServiceUrls = new List + { + new IndexedEndpoint + { + Location = new Uri("https://sp.example.com/acs"), + Binding = SamlBinding.HttpPost, + Index = 0, + IsDefault = true + } + } } }); ``` -### 3. Configure Protocol Type (Optional) +For production, use the EF Core store from `Duende.IdentityServer.EntityFramework.Stores` to persist SP configuration in your database. See [Service Providers](/identityserver/saml/service-providers.md) for all storage options. + +## Login Page Compatibility + +Your existing IdentityServer login pages work with SAML without modification. When a SAML `AuthnRequest` arrives, IdentityServer processes it and redirects to your login page with a `returnUrl`, just as it does for OIDC authorization requests. Your login page authenticates the user and redirects back. The framework handles the rest, regardless of whether the original request was OIDC or SAML. + +No SAML-specific code is needed in your login, consent, or logout pages for standard flows. -SAML 2.0 uses the protocol type constant `IdentityServerConstants.ProtocolTypes.Saml2p` -(`"saml2p"`). This is used in logging, discovery, and extensibility hooks. +For advanced scenarios where your login UI needs access to SAML-specific request details (such as `RequestedAuthnContext` requirements), call `GetAuthenticationContextAsync` on `IIdentityServerInteractionService` and pattern-match on the result to access `SamlAuthenticationContext`. See [Extensibility](/identityserver/saml/extensibility.md) for details. ## Protocol Endpoints @@ -86,4 +110,4 @@ SAML 2.0 endpoints are registered under the `/saml` path prefix: | Logout | `/saml/logout` | | Logout Callback | `/saml/logout_callback` | -See [SAML Endpoints](/identityserver/saml/endpoints/) for full details. +See [SAML Endpoints](/identityserver/saml/endpoints.md) for full details. diff --git a/astro/src/content/docs/identityserver/saml/service-providers.md b/astro/src/content/docs/identityserver/saml/service-providers.md index a4763f50..6585710c 100644 --- a/astro/src/content/docs/identityserver/saml/service-providers.md +++ b/astro/src/content/docs/identityserver/saml/service-providers.md @@ -1,6 +1,6 @@ --- -title: "SAML Service Provider Store" -description: How to register and manage SAML 2.0 Service Providers using ISamlServiceProviderStore, including in-memory and custom implementations. +title: "SAML Service Provider Management" +description: "How to register and manage SAML 2.0 Service Providers using ISamlServiceProviderStore for read-only lookup." date: 2026-03-02 sidebar: label: Service Providers @@ -10,25 +10,35 @@ sidebar: Added in 8.0 (prerelease) IdentityServer needs to know which SAML 2.0 Service Providers (SPs) are allowed to request -authentication. This is managed through the `ISamlServiceProviderStore` interface. +authentication. The SAML plugin provides the `ISamlServiceProviderStore` interface: a read-only lookup called on every incoming SAML request to resolve SP configuration by entity ID. + +For simple deployments, you configure SPs at startup using the in-memory store. For production systems, you implement a custom store backed by a database or configuration service. ## ISamlServiceProviderStore +`ISamlServiceProviderStore` is the read-only lookup interface that IdentityServer calls on every incoming SAML request. When a SAML AuthnRequest arrives, IdentityServer extracts the `Issuer` entity ID and calls `FindByEntityIdAsync` to load the SP's configuration. If the method returns `null`, the request is rejected. + +This interface is used for read-only lookup during request processing. Your store implementation should be optimized for fast, concurrent reads (e.g., backed by a cache or an indexed database query). + +`GetAllSamlServiceProvidersAsync` is used for bulk operations such as metadata generation or cache warming. It returns all registered SPs as an async stream. + ```csharp public interface ISamlServiceProviderStore { Task FindByEntityIdAsync(string entityId, CancellationToken ct); + IAsyncEnumerable GetAllSamlServiceProvidersAsync(CancellationToken ct = default); } ``` -`FindByEntityIdAsync` looks up a Service Provider by its SAML entity identifier (the -`entityID` attribute from the SP's SAML metadata). Return `null` if the entity ID is not -recognized, which will cause IdentityServer to reject the SAML request. +`FindByEntityIdAsync` looks up a Service Provider by its SAML entity identifier (the `entityID` attribute from the SP's SAML metadata). Return `null` if the entity ID is not recognized, which will cause IdentityServer to reject the SAML request. + +`GetAllSamlServiceProvidersAsync` returns all registered SPs as an `IAsyncEnumerable`, allowing callers to stream results without loading all SPs into memory at once. ## In-Memory Store (Development / Testing) -For development and testing, use the in-memory store with a static list of `SamlServiceProvider` -objects: +The in-memory store is the simplest way to register SPs. It is configured at startup with a static list of `SamlServiceProvider` objects and is ideal for development, testing, and demos. Because it holds SPs in memory, it does not support the Admin API. Use a persistent store for runtime management. + +Register the in-memory store using the IdentityServer builder: ```csharp // Program.cs @@ -40,16 +50,44 @@ builder.Services.AddIdentityServer() { EntityId = "https://sp.example.com", DisplayName = "Example SP", - AssertionConsumerServiceUrls = new[] { new Uri("https://sp.example.com/acs") }, - AssertionConsumerServiceBinding = SamlBinding.HttpPost, + AssertionConsumerServiceUrls = new List + { + new IndexedEndpoint + { + Location = new Uri("https://sp.example.com/acs"), + Binding = SamlBinding.HttpPost, + Index = 0, + IsDefault = true + } + } } }); ``` -## Custom Store (Production) +## Entity Framework Core Store (Production) + +For production deployments, IdentityServer ships an EF Core-backed implementation of `ISamlServiceProviderStore` in the `Duende.IdentityServer.EntityFramework.Stores` package. This stores SP configuration in your database alongside other IdentityServer operational and configuration data. + +Register the EF Core store using the IdentityServer builder: + +```csharp +// Program.cs +builder.Services.AddIdentityServer() + .AddSaml() + .AddSamlConfigurationStore(options => + { + options.ConfigureDbContext = b => + b.UseSqlServer(connectionString); + }); +``` + +The EF Core store handles concurrent reads efficiently and integrates with EF Core migrations for schema management. + +## Custom Store + +For deployments that need a store not covered by the built-in in-memory or EF Core implementations, implement `ISamlServiceProviderStore` backed by your own data store (e.g., a NoSQL database or external configuration service). Register your implementation using `AddSamlServiceProviderStore()` on the IdentityServer builder. -For production deployments, implement `ISamlServiceProviderStore` backed by your data store, then -register it using `AddSamlServiceProviderStore()`: +Your implementation must handle concurrent reads efficiently. Consider adding a caching layer (e.g., `IMemoryCache`) in front of your database queries, since `FindByEntityIdAsync` is called on every SAML request. ```csharp // Program.cs @@ -81,18 +119,36 @@ public class MySamlServiceProviderStore : ISamlServiceProviderStore { EntityId = record.EntityId, DisplayName = record.DisplayName, - AssertionConsumerServiceUrls = record.AcsUrls.Select(u => new Uri(u)).ToList(), - AssertionConsumerServiceBinding = SamlBinding.HttpPost, + AssertionConsumerServiceUrls = record.AcsEndpoints.Select(e => new IndexedEndpoint + { + Location = new Uri(e.Url), + Binding = SamlBinding.HttpPost, + Index = e.Index, + IsDefault = e.IsDefault + }).ToList(), // ... map remaining properties }; } + + public async IAsyncEnumerable GetAllSamlServiceProvidersAsync( + [EnumeratorCancellation] CancellationToken ct = default) + { + await foreach (var record in _repository.GetAllAsync(ct)) + { + yield return new SamlServiceProvider + { + EntityId = record.EntityId, + DisplayName = record.DisplayName, + // ... map remaining properties + }; + } + } } ``` ## Full Configuration Example -The following example shows a fully configured `SamlServiceProvider` with signing, encryption, -and single logout: +The following example shows a fully configured `SamlServiceProvider` with signing, single logout, and claim mappings. This object can be used directly with the in-memory store or returned from a custom `ISamlServiceProviderStore` implementation. ```csharp new SamlServiceProvider @@ -102,8 +158,16 @@ new SamlServiceProvider Enabled = true, // Assertion Consumer Service - AssertionConsumerServiceUrls = new[] { new Uri("https://sp.example.com/acs") }, - AssertionConsumerServiceBinding = SamlBinding.HttpPost, + AssertionConsumerServiceUrls = new List + { + new IndexedEndpoint + { + Location = new Uri("https://sp.example.com/acs"), + Binding = SamlBinding.HttpPost, + Index = 0, + IsDefault = true + } + }, // Single Logout Service SingleLogoutServiceUrl = new SamlEndpointType @@ -117,19 +181,15 @@ new SamlServiceProvider RequireSignedAuthnRequests = true, SigningCertificates = new[] { myCertificate }, - // Encryption - EncryptAssertions = true, - EncryptionCertificates = new[] { spEncryptionCertificate }, - // NameID DefaultNameIdFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", // Claims - ClaimMappings = new Dictionary + ClaimMappings = new ReadOnlyDictionary(new Dictionary { - ["department"] = "businessUnit", - }, + ["department"] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department", + }), } ``` -See [SAML Configuration](/identityserver/saml/configuration/) for full property documentation. +See [SAML Configuration](/identityserver/saml/configuration.md) for full property documentation.