Rock Solid Knowledge SAML to Duende IdentityServer SAML
This guide walks through migrating from Rock Solid Knowledge (RSK) SAML (Rsk.Saml.DuendeIdentityServer) to Duende IdentityServer with built-in SAML support.
What Changed
Section titled “What Changed”| Aspect | RSK SAML 2.0 (SAML2P) | Duende SAML |
|---|---|---|
| Package | Rsk.Saml.DuendeIdentityServer (separate) | Built into Duende.IdentityServer |
| License | Separate RSK SAML license | Included in Advanced/Custom; add-on for Standard |
| Registration | .AddSamlPlugin() | .AddSaml() |
| Middleware | .UseIdentityServerSamlPlugin() required | Not needed |
| SSO Endpoint | /saml/sso | /Saml2/SSO (configurable) |
| SLO Endpoint | /saml/slo | /Saml2/SLO (configurable) |
| Metadata | /saml/metadata | /Saml2 (configurable) |
Migration Guide
Section titled “Migration Guide”Prerequisites
Section titled “Prerequisites”Before migrating, ensure:
- Duende License with SAML support: see IdentityServer pricing page
- .NET 10 SDK: Duende v8+ requires .NET 10
- Test Environment: Set up a test environment before production migration
Step 1: Update Target Framework
Section titled “Step 1: Update Target Framework”Duende IdentityServer v8+ requires .NET 10:
<TargetFramework>net8.0</TargetFramework><TargetFramework>net10.0</TargetFramework>Step 2: Replace NuGet Packages
Section titled “Step 2: Replace NuGet Packages”Duende IdentityServer includes all necessary SAML infrastructure. Make sure to use the latest version of IdentityServer, and remove references to RSK:
<PackageReference Include="Duende.IdentityServer" Version="7.4.3" /><PackageReference Include="Rsk.Saml.DuendeIdentityServer" Version="11.0.0" /><PackageReference Include="Duende.IdentityServer" Version="8.0.*" /><!-- No separate SAML package needed! -->Step 3: Update Namespaces
Section titled “Step 3: Update Namespaces”Remove all RSK namespace references, and replace them with similar Duende namespaces:
using Rsk.Saml;using Rsk.Saml.Configuration;using Rsk.Saml.Models;using Rsk.Saml.Services;using Duende.IdentityServer;using Duende.IdentityServer.Models;using Duende.IdentityServer.Saml;using Duende.IdentityServer.Saml.Models;Step 4: Update Service Registration
Section titled “Step 4: Update Service Registration”Before (RSK):
builder.Services.AddIdentityServer() .AddSamlPlugin(options => { options.Licensee = "Your Licensee"; options.LicenseKey = "Your License Key"; options.WantAuthenticationRequestsSigned = false; });After (Duende):
builder.Services.AddIdentityServer() .AddSaml(saml => { saml.WantAuthnRequestsSigned = false; saml.DefaultSigningBehavior = SamlSigningBehavior.SignAssertion; saml.DefaultClockSkew = TimeSpan.FromMinutes(5); saml.DefaultAssertionLifetime = TimeSpan.FromMinutes(5);
// Metadata configuration saml.Metadata.CacheDuration = TimeSpan.FromHours(12); saml.Metadata.ExpiryDuration = TimeSpan.FromDays(5); });Step 5: Remove RSK SAML Middleware
Section titled “Step 5: Remove RSK SAML Middleware”The RSK SAML middleware is no longer required, and can be removed.
app.UseIdentityServer() .UseIdentityServerSamlPlugin();app.UseIdentityServer();Forgetting to remove .UseIdentityServerSamlPlugin() will cause a compilation error.
Step 6: Migrate Service Provider Configuration
Section titled “Step 6: Migrate Service Provider Configuration”This is the most complex part. See Service Provider Configuration Changes for the full before/after comparison.
Step 7: Migrate Data (when using Entity Framework stores)
Section titled “Step 7: Migrate Data (when using Entity Framework stores)”If you are using Duende IdentityServer’s Entity Framework stores for configuration and operational data, make sure to apply required database migrations. The v7.4 to v8.0 upgrade guide documents the updated database schema and migration aspects.
API Mapping Reference
Section titled “API Mapping Reference”Extension Methods
Section titled “Extension Methods”| RSK SAML | Duende SAML |
|---|---|
.AddSamlPlugin(options => { }) | .AddSaml(saml => { }) |
.UseIdentityServerSamlPlugin() | Not needed |
SAML Options
Section titled “SAML Options”RSK (SamlIdpOptions) | Duende (SamlOptions) |
|---|---|
WantAuthenticationRequestsSigned | WantAuthnRequestsSigned |
Licensee | N/A (use IdentityServerOptions.LicenseKey) |
LicenseKey | N/A (use IdentityServerOptions.LicenseKey) |
| — | DefaultAssertionLifetime (NEW) |
| — | Metadata.CacheDuration (NEW) |
| — | Metadata.ExpiryDuration (NEW) |
Service Provider Model
Section titled “Service Provider Model”RSK (ServiceProvider) | Duende (SamlServiceProvider) |
|---|---|
EntityId | EntityId |
AssertionConsumerServices | AssertionConsumerServiceUrls |
SingleLogoutServices | SingleLogoutServiceUrls |
ClaimsMapping | ClaimMappings |
SignAssertions (bool) | SigningBehavior (enum) |
EncryptAssertions | EncryptAssertions |
RequireAuthenticationRequestsSigned | RequireSignedAuthnRequests |
AllowIdpInitiatedSso | AllowIdpInitiated |
| — | AllowedScopes (NEW, required) |
| — | Enabled (NEW) |
| — | DisplayName (NEW) |
Endpoint Classes
Section titled “Endpoint Classes”| RSK | Duende |
|---|---|
Service(binding, url) | IndexedEndpoint (for ACS) or SamlEndpointType (for SLO) |
SamlConstants.BindingTypes.HttpPost | SamlBinding.HttpPost |
SamlConstants.BindingTypes.HttpRedirect | SamlBinding.HttpRedirect |
Duende uses two different endpoint types:
IndexedEndpoint: For ACS endpoints. IncludesIndexandIsDefaultproperties for SAML endpoint indexing.SamlEndpointType: For SLO endpoints. A simpler type with justLocationandBinding.
Service Provider Configuration
Section titled “Service Provider Configuration”Each SAML Service Provider registered with your IdP must be migrated from the RSK ServiceProvider model to Duende’s SamlServiceProvider model. The overall structure is similar, but there are key differences:
- New required property:
AllowedScopesmust be set (see Breaking Changes) - Endpoint types changed: RSK used a single
Serviceclass; Duende usesIndexedEndpointfor ACS andSamlEndpointTypefor SLO - Signing behavior: Changed from boolean (
SignAssertions) to enum (SigningBehavior) - New optional properties:
DisplayNameandEnabledfor better SP management
See the Service Provider Model mapping table for a complete property-by-property comparison. The examples below show a full before/after comparison.
RSK Service Provider (Before)
Section titled “RSK Service Provider (Before)”using Rsk.Saml;using Rsk.Saml.Models;
public static IEnumerable<ServiceProvider> ServiceProviders =>[ new Rsk.Saml.Models.ServiceProvider { EntityId = "https://sp.example.com/saml",
AssertionConsumerServices = [ new Service(SamlConstants.BindingTypes.HttpPost, "https://sp.example.com/Saml2/Acs") ],
SingleLogoutServices = [ new Service(SamlConstants.BindingTypes.HttpPost, "https://sp.example.com/Saml2/Logout") ],
ClaimsMapping = new Dictionary<string, string> { [ClaimTypes.Name] = ClaimTypes.Name, [ClaimTypes.Email] = ClaimTypes.Email, },
SignAssertions = true, EncryptAssertions = false, RequireAuthenticationRequestsSigned = false, AllowIdpInitiatedSso = true }];Duende Service Provider (After)
Section titled “Duende Service Provider (After)”using Duende.IdentityServer.Models;using Duende.IdentityServer.Saml;using Duende.IdentityServer.Saml.Models;
public static IEnumerable<SamlServiceProvider> SamlServiceProviders =>[ new SamlServiceProvider { EntityId = "https://sp.example.com/saml", DisplayName = "Example Service Provider", Enabled = true,
AssertionConsumerServiceUrls = [ new IndexedEndpoint { Location = "https://sp.example.com/Saml2/Acs", Binding = SamlBinding.HttpPost, Index = 0, IsDefault = true } ],
SingleLogoutServiceUrls = [ new SamlEndpointType { Location = "https://sp.example.com/Saml2/Logout", Binding = SamlBinding.HttpRedirect // Note: Only HttpRedirect is supported for SLO } ],
ClaimMappings = new Dictionary<string, string> { [ClaimTypes.Name] = ClaimTypes.Name, [ClaimTypes.Email] = ClaimTypes.Email, },
SigningBehavior = SamlSigningBehavior.SignAssertion, EncryptAssertions = false, RequireSignedAuthnRequests = false, AllowIdpInitiated = true,
// NEW: Required for Duende AllowedScopes = ["openid", "profile", "email"] }];Breaking Changes
Section titled “Breaking Changes”AllowedScopes is Required
Section titled “AllowedScopes is Required”Duende requires AllowedScopes to be set on each SAML Service Provider (SP). Without it, the Service Provider will fail runtime validation.
// REQUIRED - will fail validation without thisAllowedScopes = ["openid", "profile", "email"]AllowedScopes determines which claims can be included in SAML assertions. Include the identity resource names that contain the claim types your Service Provider needs:
| If SP needs these claims | Include this scope |
|---|---|
sub (subject identifier) | openid |
name, family_name, given_name | profile |
email, email_verified | email |
| Custom claims | Your custom identity resource name |
ACS Binding Must Be HttpPost
Section titled “ACS Binding Must Be HttpPost”All AssertionConsumerServiceUrls must use SamlBinding.HttpPost. HTTP-Redirect is not supported for SAML Response delivery because SAML responses containing signed assertions are typically too large for URL-based transport.
Duende IdentityServer enforces this at runtime via DefaultSamlServiceProviderConfigurationValidator. If you configure an ACS endpoint with any other binding, the Service Provider will fail validation and be rejected with an error like:
Assertion Consumer Service at index 0 uses an unsupported binding ‘HttpRedirect’. Only HTTP-POST is supported for SAML Response delivery.
// ✅ CORRECTnew IndexedEndpoint{ Binding = SamlBinding.HttpPost, Location = "https://sp.example.com/Saml2/Acs", Index = 0, IsDefault = true}
// ❌ WRONG - will fail runtime validationnew IndexedEndpoint{ Binding = SamlBinding.HttpRedirect, // Not supported for ACS Location = "https://sp.example.com/Saml2/Acs", Index = 0, IsDefault = true}SLO Only Supports HttpRedirect
Section titled “SLO Only Supports HttpRedirect”Single Logout (SLO) endpoints only support SamlBinding.HttpRedirect. Unlike ACS binding validation, this is not checked at configuration time. If you configure HttpPost for SLO, the configuration will load successfully, but SLO notifications will be silently skipped for that Service Provider at runtime.
This happens because the SLO notification service specifically looks for an HttpRedirect endpoint. If none is found, it logs a debug message (“UnsupportedBinding”) and skips the Service Provider without throwing an error.
If your RSK configuration used HttpPost for SLO, change it to HttpRedirect:
new Service(SamlConstants.BindingTypes.HttpPost, "https://sp.example.com/Saml2/Logout")new SamlEndpointType{ Location = "https://sp.example.com/Saml2/Logout", Binding = SamlBinding.HttpRedirect}Endpoint URL Paths Changed
Section titled “Endpoint URL Paths Changed”| RSK Path | Duende Path |
|---|---|
/saml/sso | /Saml2/SSO |
/saml/slo | /Saml2/SLO |
/saml/metadata | /Saml2 |
These paths are configurable via SamlOptions.Endpoints. You have two options:
- Update external Service Providers to use the new Duende endpoint paths
- Configure Duende to use the legacy RSK paths — this is useful when you have external SPs that you don’t control or cannot easily update
If not all integrations are under your control, configuring IdentityServer to use the legacy paths may be the simpler approach.
Signing Behavior Changed from Bool to Enum
Section titled “Signing Behavior Changed from Bool to Enum”SignAssertions = true,SignResponse = false,SigningBehavior = SamlSigningBehavior.SignAssertionAvailable values: SignAssertion, SignResponse, SignBoth, DoNotSign. See SAML Configuration for details.
License Configuration Changed
Section titled “License Configuration Changed”RSK SAML had its own license. Duende uses a single license key. See the licensing documentation for configuration options.
.AddSamlPlugin(options =>{ options.Licensee = "..."; options.LicenseKey = "...";})builder.Services.AddIdentityServer(options =>{ options.LicenseKey = "your-duende-license-key";});Property Naming Differences
Section titled “Property Naming Differences”Watch for these subtle naming differences when translating RSK code to Duende:
ClaimsMapping→ClaimMappings(plural)AllowIdpInitiatedSso→AllowIdpInitiated(shorter)RequireAuthenticationRequestsSigned→RequireSignedAuthnRequests(reordered)
Testing Checklist
Section titled “Testing Checklist”After migration, verify:
- Solution builds without errors
-
/Saml2returns valid SAML metadata XML - Metadata contains correct entity ID
- Metadata contains correct ACS and SLO endpoints
- Service Provider-initiated SSO flow works (SP → IdP → authenticate → SP)
- Claims are correctly mapped in SAML assertions
- Single Logout works (if configured)
- IdP-initiated SSO works (if enabled)
- Existing SAML integrations with live Service Providers still work
- No license warnings in logs (valid SAML-enabled license active)
Quick Metadata Test
Section titled “Quick Metadata Test”Verify the metadata endpoint returns valid SAML metadata:
curl -k https://localhost:5001/Saml2You should receive an XML document starting with <EntityDescriptor> containing your IdP’s entity ID, signing certificates, and endpoint locations. If you’ve configured custom endpoint paths, adjust the URL accordingly.
Troubleshooting
Section titled “Troubleshooting””Service Provider not found” Error
Section titled “”Service Provider not found” Error”The Service Provider’s EntityId doesn’t match what’s registered. Check:
- Case sensitivity - entity IDs are case-sensitive
- Trailing slashes -
https://sp.example.com≠https://sp.example.com/ - The Service Provider is registered with
Enabled = true
”AllowedScopes is required” Error
Section titled “”AllowedScopes is required” Error”Add AllowedScopes to your Service Provider configuration:
AllowedScopes = ["openid", "profile", "email"]“Unsupported binding” Error for ACS
Section titled ““Unsupported binding” Error for ACS”If you see an error like:
Assertion Consumer Service at index 0 uses an unsupported binding ‘HttpRedirect’. Only HTTP-POST is supported for SAML Response delivery.
Change your ACS endpoint binding to SamlBinding.HttpPost. HTTP-Redirect cannot be used for ACS because SAML responses with signed assertions exceed URL length limits.
Missing Claims in Assertion
Section titled “Missing Claims in Assertion”- Check
ClaimMappingsdictionary - Ensure claims exist on the user
- Verify
AllowedScopesincludes the relevant identity resources
SSO Works but SLO Fails
Section titled “SSO Works but SLO Fails”- Verify
SingleLogoutServiceUrlsis configured - Ensure SLO binding is
SamlBinding.HttpRedirect— if you configuredHttpPost, SLO notifications will be silently skipped (check debug logs for “UnsupportedBinding”) - Check that the Service Provider and IdP agree on the binding
- Ensure session management is properly configured