Skip to content
Introducing the next era of Duende IdentityServer. Read our CEO’s announcement

SAML Service Provider Management

IdentityServer needs to know which SAML 2.0 Service Providers (SPs) are allowed to request 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 more advanced setups where configuration changes more frequently, you implement a store backed by a database or configuration service. The Duende.IdentityServer.EntityFramework.Stores package contains an implementation of a database store.

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 cache warming. It returns all registered SPs as an async stream.

ISamlServiceProviderStore.cs
public interface ISamlServiceProviderStore
{
Task<SamlServiceProvider?> FindByEntityIdAsync(string entityId, CancellationToken ct);
IAsyncEnumerable<SamlServiceProvider> 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.

GetAllSamlServiceProvidersAsync returns all registered SPs as an IAsyncEnumerable<SamlServiceProvider>, allowing callers to stream results without loading all SPs into memory at once.

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 smaller deployments. For a working example, see the SAML 2.0 Basic sample.

Register the in-memory store using the IdentityServer builder:

Program.cs
builder.Services.AddIdentityServer()
.AddSaml()
.AddInMemorySamlServiceProviders(
[
new()
{
EntityId = "https://sp.example.com",
DisplayName = "Example SP",
AssertionConsumerServiceUrls =
[
new()
{
Location = "https://sp.example.com/acs",
Binding = SamlBinding.HttpPost,
Index = 0,
IsDefault = true
}
]
}
]
);

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:

Program.cs
builder.Services.AddIdentityServer()
.AddSaml()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlServer(connectionString);
});

The EF Core store handles concurrent reads efficiently and integrates with EF Core migrations for schema management.

For deployments that need a store not covered by the built-in options, implement ISamlServiceProviderStore backed by your own data store (e.g., a NoSQL database or external configuration service). Register your implementation using AddSamlServiceProviderStore<T>() on the IdentityServer builder.

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.

Program.cs
builder.Services.AddIdentityServer()
.AddSaml()
.AddSamlServiceProviderStore<MySamlServiceProviderStore>();
MySamlServiceProviderStore.cs
public class MySamlServiceProviderStore : ISamlServiceProviderStore
{
private readonly IServiceProviderRepository _repository;
public MySamlServiceProviderStore(IServiceProviderRepository repository)
{
_repository = repository;
}
public async Task<SamlServiceProvider?> FindByEntityIdAsync(
string entityId,
CancellationToken ct)
{
var record = await _repository.FindByEntityIdAsync(entityId, ct);
if (record is null)
return null;
return new SamlServiceProvider
{
EntityId = record.EntityId,
DisplayName = record.DisplayName,
AssertionConsumerServiceUrls = record.AcsEndpoints.Select(e => new IndexedEndpoint
{
Location = e.Url,
Binding = SamlBinding.HttpPost,
Index = e.Index,
IsDefault = e.IsDefault
}).ToList(),
// ... map remaining properties
};
}
public async IAsyncEnumerable<SamlServiceProvider> GetAllSamlServiceProvidersAsync(
[EnumeratorCancellation] CancellationToken ct)
{
await foreach (var record in _repository.GetAllAsync(ct))
{
yield return new SamlServiceProvider
{
EntityId = record.EntityId,
DisplayName = record.DisplayName,
// ... map remaining properties
};
}
}
}

When you call AddSamlServiceProviderStore<T>(), IdentityServer automatically wraps your store with a ValidatingSamlServiceProviderStore<T> that runs configuration checks on every SP loaded from the store.

The default validator (DefaultSamlServiceProviderConfigurationValidator) checks the following:

  • EntityId is required.
  • At least one Assertion Consumer Service URL is required.
  • All ACS URLs must use SamlBinding.HttpPost. HTTP Redirect is not supported for SAML Response delivery.
  • At least one AllowedScopes entry is required.
  • AssertionLifetime must be positive (if set).
  • ClockSkew must be non-negative (if set).
  • RequestMaxAge must be positive (if set).

If an SP fails validation, it’s treated as if it doesn’t exist: the store returns null and an InvalidSamlServiceProviderConfigurationEvent is raised. This means a misconfigured SP is silently rejected at runtime rather than causing an unhandled exception.

You can replace the default validator with your own implementation. See Extensibility: ISamlServiceProviderConfigurationValidator for details.

For custom stores, you can add a caching layer by calling AddSamlServiceProviderStoreCache<T>() instead of AddSamlServiceProviderStore<T>(). This wraps your store with CachingSamlServiceProviderStore<T>, which uses HybridCache to cache SP lookups.

Validation still runs: the caching layer wraps the validating store, so only valid SPs are cached. Invalid SPs are rejected before they reach the cache.

The cache duration is controlled by IdentityServerOptions.Caching.SamlServiceProviderStoreExpiration (default: 15 minutes).

To register your store with caching:

Program.cs
builder.Services.AddIdentityServer()
.AddSaml()
.AddSamlServiceProviderStoreCache<MySamlServiceProviderStore>();

To configure the cache expiration:

Program.cs
builder.Services
.AddIdentityServer(options =>
{
options.Caching.SamlServiceProviderStoreExpiration = TimeSpan.FromMinutes(30);
})
.AddSaml()
.AddSamlServiceProviderStoreCache<MySamlServiceProviderStore>();

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.

new SamlServiceProvider
{
EntityId = "https://sp.example.com",
DisplayName = "Example SP",
Enabled = true,
// Assertion Consumer Service
AssertionConsumerServiceUrls =
[
new()
{
Location = "https://sp.example.com/acs",
Binding = SamlBinding.HttpPost,
Index = 0,
IsDefault = true
}
],
// Single Logout Service
SingleLogoutServiceUrls =
[
new()
{
Location = "https://sp.example.com/saml/slo",
Binding = SamlBinding.HttpRedirect,
}
],
// Signing
SigningBehavior = SamlSigningBehavior.SignAssertion,
RequireSignedAuthnRequests = true, // bool?: null falls back to global SamlOptions.WantAuthnRequestsSigned
Certificates =
[
new()
{
Certificate = myCertificate,
Use = KeyUse.Signing
}
],
// NameID
DefaultNameIdFormat = SamlConstants.NameIdentifierFormats.Unspecified,
// Claims
ClaimMappings = new Dictionary<string, string>
{
["department"] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department",
},
}

Each entry in Certificates is a ServiceProviderCertificate, which uses the KeyUse enum to annotate whether the certificate is used for signing, encryption, or both.

See SAML Configuration for full property documentation.