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

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.

AspectRSK SAML 2.0 (SAML2P)Duende SAML
PackageRsk.Saml.DuendeIdentityServer (separate)Built into Duende.IdentityServer
LicenseSeparate RSK SAML licenseIncluded in Advanced/Custom; add-on for Standard
Registration.AddSamlPlugin().AddSaml()
Middleware.UseIdentityServerSamlPlugin() requiredNot needed
SSO Endpoint/saml/sso/Saml2/SSO (configurable)
SLO Endpoint/saml/slo/Saml2/SLO (configurable)
Metadata/saml/metadata/Saml2 (configurable)

Before migrating, ensure:

  1. Duende License with SAML support: see IdentityServer pricing page
  2. .NET 10 SDK: Duende v8+ requires .NET 10
  3. Test Environment: Set up a test environment before production migration

Duende IdentityServer v8+ requires .NET 10:

.csproj
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>

Duende IdentityServer includes all necessary SAML infrastructure. Make sure to use the latest version of IdentityServer, and remove references to RSK:

.csproj
<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! -->

Remove all RSK namespace references, and replace them with similar Duende namespaces:

*.cs
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;

Before (RSK):

Program.cs
builder.Services.AddIdentityServer()
.AddSamlPlugin(options =>
{
options.Licensee = "Your Licensee";
options.LicenseKey = "Your License Key";
options.WantAuthenticationRequestsSigned = false;
});

After (Duende):

Program.cs
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);
});

The RSK SAML middleware is no longer required, and can be removed.

Program.cs
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.

RSK SAMLDuende SAML
.AddSamlPlugin(options => { }).AddSaml(saml => { })
.UseIdentityServerSamlPlugin()Not needed
RSK (SamlIdpOptions)Duende (SamlOptions)
WantAuthenticationRequestsSignedWantAuthnRequestsSigned
LicenseeN/A (use IdentityServerOptions.LicenseKey)
LicenseKeyN/A (use IdentityServerOptions.LicenseKey)
DefaultAssertionLifetime (NEW)
Metadata.CacheDuration (NEW)
Metadata.ExpiryDuration (NEW)
RSK (ServiceProvider)Duende (SamlServiceProvider)
EntityIdEntityId
AssertionConsumerServicesAssertionConsumerServiceUrls
SingleLogoutServicesSingleLogoutServiceUrls
ClaimsMappingClaimMappings
SignAssertions (bool)SigningBehavior (enum)
EncryptAssertionsEncryptAssertions
RequireAuthenticationRequestsSignedRequireSignedAuthnRequests
AllowIdpInitiatedSsoAllowIdpInitiated
AllowedScopes (NEW, required)
Enabled (NEW)
DisplayName (NEW)
RSKDuende
Service(binding, url)IndexedEndpoint (for ACS) or SamlEndpointType (for SLO)
SamlConstants.BindingTypes.HttpPostSamlBinding.HttpPost
SamlConstants.BindingTypes.HttpRedirectSamlBinding.HttpRedirect

Duende uses two different endpoint types:

  • IndexedEndpoint: For ACS endpoints. Includes Index and IsDefault properties for SAML endpoint indexing.
  • SamlEndpointType: For SLO endpoints. A simpler type with just Location and Binding.

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: AllowedScopes must be set (see Breaking Changes)
  • Endpoint types changed: RSK used a single Service class; Duende uses IndexedEndpoint for ACS and SamlEndpointType for SLO
  • Signing behavior: Changed from boolean (SignAssertions) to enum (SigningBehavior)
  • New optional properties: DisplayName and Enabled for 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.

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
}
];
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"]
}
];

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 this
AllowedScopes = ["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 claimsInclude this scope
sub (subject identifier)openid
name, family_name, given_nameprofile
email, email_verifiedemail
Custom claimsYour custom identity resource name

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.

// ✅ CORRECT
new IndexedEndpoint
{
Binding = SamlBinding.HttpPost,
Location = "https://sp.example.com/Saml2/Acs",
Index = 0,
IsDefault = true
}
// ❌ WRONG - will fail runtime validation
new IndexedEndpoint
{
Binding = SamlBinding.HttpRedirect, // Not supported for ACS
Location = "https://sp.example.com/Saml2/Acs",
Index = 0,
IsDefault = true
}

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
}
RSK PathDuende Path
/saml/sso/Saml2/SSO
/saml/slo/Saml2/SLO
/saml/metadata/Saml2

These paths are configurable via SamlOptions.Endpoints. You have two options:

  1. Update external Service Providers to use the new Duende endpoint paths
  2. 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.SignAssertion

Available values: SignAssertion, SignResponse, SignBoth, DoNotSign. See SAML Configuration for details.

RSK SAML had its own license. Duende uses a single license key. See the licensing documentation for configuration options.

Program.cs
.AddSamlPlugin(options =>
{
options.Licensee = "...";
options.LicenseKey = "...";
})
builder.Services.AddIdentityServer(options =>
{
options.LicenseKey = "your-duende-license-key";
});

Watch for these subtle naming differences when translating RSK code to Duende:

  • ClaimsMappingClaimMappings (plural)
  • AllowIdpInitiatedSsoAllowIdpInitiated (shorter)
  • RequireAuthenticationRequestsSignedRequireSignedAuthnRequests (reordered)

After migration, verify:

  • Solution builds without errors
  • /Saml2 returns 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)

Verify the metadata endpoint returns valid SAML metadata:

Terminal
curl -k https://localhost:5001/Saml2

You 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.

The Service Provider’s EntityId doesn’t match what’s registered. Check:

  1. Case sensitivity - entity IDs are case-sensitive
  2. Trailing slashes - https://sp.example.comhttps://sp.example.com/
  3. The Service Provider is registered with Enabled = true

Add AllowedScopes to your Service Provider configuration:

AllowedScopes = ["openid", "profile", "email"]

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.

  1. Check ClaimMappings dictionary
  2. Ensure claims exist on the user
  3. Verify AllowedScopes includes the relevant identity resources
  1. Verify SingleLogoutServiceUrls is configured
  2. Ensure SLO binding is SamlBinding.HttpRedirect — if you configured HttpPost, SLO notifications will be silently skipped (check debug logs for “UnsupportedBinding”)
  3. Check that the Service Provider and IdP agree on the binding
  4. Ensure session management is properly configured