From duende-skills
Configures Duende IdentityServer as a SAML 2.0 IdP: SP registration, SSO/SLO flows, claim mappings, extensibility, and production deployment.
How this skill is triggered — by the user, by Claude, or both
Slash command
/duende-skills:identityserver-samlThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Setting up IdentityServer as a SAML 2.0 Identity Provider (IdP)
SamlServiceProvider modelClaimMappings or extensibility interfacesISamlServiceProviderStore)SignAssertion is the default and most interoperable signing behaviorDocs: https://docs.duendesoftware.com/identityserver/saml
builder.Services.AddIdentityServer()
.AddInMemoryClients(Config.Clients)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddSaml()
.AddInMemorySamlServiceProviders(Config.SamlServiceProviders);
Update the login page to call DenyAuthenticationAsync for SAML cancellation support (when user cancels login during a SAML flow).
| Endpoint | Path | Purpose |
|---|---|---|
| Metadata | /Saml2 | IdP metadata (certificates, endpoints, NameID formats) |
| Sign-in | /Saml2/SSO | Receives AuthnRequest (GET/POST) |
| Sign-in Callback | /Saml2/SSO/Callback | Builds SAML Response after authentication |
| Logout | /Saml2/SLO | Handles LogoutRequest/LogoutResponse |
| Logout Callback | /Saml2/SLO/Callback | Completes SLO round-trip |
Paths are customizable via SamlOptions.Endpoints.
new SamlServiceProvider
{
// Required
EntityId = "https://sp.example.com",
DisplayName = "Example SP",
// ACS endpoints (HTTP-POST only, indexed)
AssertionConsumerServiceUrls =
[
new IndexedEndpoint
{
Location = "https://sp.example.com/acs",
Binding = SamlBinding.HttpPost,
Index = 0,
IsDefault = true
}
],
// Single Logout (HTTP-Redirect only)
SingleLogoutServiceUrls =
[
new SamlEndpointType
{
Location = "https://sp.example.com/saml/slo",
Binding = SamlBinding.HttpRedirect
}
],
// Security
SigningBehavior = SamlSigningBehavior.SignAssertion,
RequireSignedAuthnRequests = true,
Certificates =
[
new ServiceProviderCertificate
{
Certificate = spCert,
Use = KeyUse.Signing
}
],
// Claims (identity resources the SP can access)
AllowedScopes = ["openid", "profile", "email"],
RequestedClaimTypes = ["email", "name"], // optional narrowing
ClaimMappings = new Dictionary<string, string>
{
["email"] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
["name"] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
},
// NameID
DefaultNameIdFormat = SamlNameIdFormat.EmailAddress,
// IdP-Initiated SSO (opt-in)
AllowIdpInitiated = false
}
AllowedScopes (identity resources) → filters available claim types
↓
RequestedClaimTypes (optional narrowing) → selects specific claims
↓
ClaimMappings (OIDC claim name → SAML attribute URI) → output as <saml:Attribute>
Use SamlOptions.DefaultClaimMappings for global defaults; per-SP ClaimMappings override them.
builder.Services.AddIdentityServer(options =>
{
options.Saml.EntityId = "https://idp.example.com/Saml2"; // default: {host}/Saml2
options.Saml.WantAuthnRequestsSigned = true; // default: true
options.Saml.RequireSignedLogoutResponses = true; // default: true
options.Saml.DefaultSigningBehavior = SamlSigningBehavior.SignAssertion;
options.Saml.DefaultClockSkew = TimeSpan.FromMinutes(5);
options.Saml.DefaultRequestMaxAge = TimeSpan.FromMinutes(5);
options.Saml.DefaultAssertionLifetime = TimeSpan.FromMinutes(5);
options.Saml.SupportedNameIdFormats = [SamlNameIdFormat.EmailAddress, SamlNameIdFormat.Unspecified];
options.Saml.MaxRelayStateLength = 80; // SAML spec requirement
// Global claim mappings
options.Saml.DefaultClaimMappings = new Dictionary<string, string>
{
["name"] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
["email"] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
["role"] = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
};
// AuthnContext mappings (acr/amr → SAML AuthnContext URIs)
options.Saml.DefaultAuthnContextMappings = new Dictionary<string, string>
{
["pwd"] = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
};
});
options.Saml.Metadata.CacheDuration = TimeSpan.FromHours(12);
options.Saml.Metadata.ExpiryDuration = TimeSpan.FromDays(5);
.AddInMemorySamlServiceProviders(new[]
{
new SamlServiceProvider { EntityId = "...", /* ... */ }
});
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlServer(connectionString);
})
Run EF migrations: dotnet ef migrations add Update_DuendeIdentityServer_v8_0
.AddSamlServiceProviderStore<MySamlSpStore>()
public class MySamlSpStore : ISamlServiceProviderStore
{
public Task<SamlServiceProvider?> FindByEntityIdAsync(
string entityId, CancellationToken ct)
{ /* lookup from your backend */ }
public IAsyncEnumerable<SamlServiceProvider> GetAllSamlServiceProvidersAsync(
CancellationToken ct)
{ /* stream all SPs */ }
}
// Add HybridCache layer to any custom store
.AddSamlServiceProviderStoreCache<MySamlSpStore>()
All 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.
SLO uses front-channel logout via iframes (not redirect chains):
/Saml2/SLOKey points:
ISamlLogoutSessionStore for distributed deployments (tracks which SPs have active sessions)| Interface | Purpose |
|---|---|
ISamlNameIdGenerator | Custom NameID value derivation (e.g., from employee_id claim) |
ISamlSigningService | HSM/Key Vault signing certificate integration |
ISaml2MetadataResponseGenerator | Custom metadata extensions (org info, federation) |
ISaml2IssuerNameService | Multi-tenant: dynamic entity ID per tenant |
ISaml2SsoInteractionResponseGenerator | Custom step-up auth logic during SSO |
ISaml2SsoResponseGenerator | Custom SAML Response generation |
ISamlLogoutNotificationService | Selective SLO targeting (choose which SPs get notified) |
ISamlLogoutSessionStore | Distributed SLO state (Redis, EF Core) |
ISaml2FrontChannelLogoutRequestBuilder | Custom logout request structure |
ISamlResourceResolver | Dynamic scope filtering per SP |
IIdpInitiatedSsoService | Portal "My Apps" dashboard for IdP-initiated flows |
IAuthnRequestValidator | Custom SP access rules, IP/time-based controls |
ILogoutRequestValidator | Custom SLO authorization rules |
ISamlSigninStateStore | Distributed sign-in state (for multi-node deployments) |
ISamlServiceProviderConfigurationValidator | Custom SP config validation rules |
public class EmployeeNameIdGenerator : ISamlNameIdGenerator
{
public Task<NameIdGenerationResult> GenerateAsync(
NameIdGenerationContext context, CancellationToken ct)
{
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."));
return Task.FromResult(NameIdGenerationResult.Success(
new NameId(employeeId, context.ResolvedFormat)));
}
}
Inject IIdentityServerInteractionService and call GetSamlAuthenticationContextAsync(returnUrl) to access SamlAuthenticationContext (requesting SP, required AuthnContext) for customizing login flows per SP.
IdentityServer can consume SAML assertions from external IdPs via federation. Add a SAML authentication handler (e.g., Sustainsys.Saml2 or ITfoxtec.Identity.Saml2) and configure it as an external provider in IdentityServer's login UI — same pattern as any external authentication scheme.
❌ Enabling AllowIdpInitiated on all SPs — only enable where explicitly required (less secure)
❌ Using DoNotSign outside of local testing
❌ Using in-memory SP stores in production
❌ Omitting AllowedScopes — SP gets no claims in the assertion
❌ Configuring ACS URLs with HTTP-Redirect binding (only HTTP-POST is supported)
AddSaml() requires Advanced or Custom Edition license.ISamlSigninStateStore and ISamlLogoutSessionStore (e.g., EF Core, Redis). Without them, SSO/SLO state is lost across nodes.AllowedScopes doesn't include a resource containing a claim type, that claim won't reach ClaimMappings.identityserver-configuration — IdentityServer host configuration and optionsidentityserver-stores — Persistent store patterns (EF Core, custom stores)identity-security-hardening — Key rotation, HTTPS enforcementidentityserver-ui-flows — Login/logout UI flows that SAML integrates withidentityserver-upgrade-v7-to-v8 — Migration guide including SAML EF migrationsnpx claudepluginhub duendesoftware/duende-skills --plugin duende-skillsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.