This quickstart will walk you through how to configure OpenIddict as a SAML Identity Provider (IdP) using the Rock Solid Knowledge SAML component.
This quickstart assumes a working knowledge of .NET, OpenIddict, and that you already have a working OpenIddict implementation. If you don't have one, this quickstart is based on the OpenIddict Velusia Sample, you can clone that to get started.
If you are using the Velusia Sample as a starting point, please ensure you are using a version of the sample from a commit no later than ea3cfc788c52c61fefb45a3be0a31de48c97c8af as our SAML component currently does not support .Net 8.
In the later steps, you'll need a Saml Service Provider to be able to configure a Client and test login, you can use any Saml 2.0 compliant Service Provider, if you don't have one the Rock Solid Knowledge Saml component can be used to create one, check out our quickstart, Acting as a ServiceProvider for more information.
A dotnet template is available here and a video walkthrough can be found here.
Configuring SAML for OpenIddict
License
Our SAML component requires a valid license. For a demo license, please sign up on our products page, or reach out to support@openiddictcomponents.com.
Install the Rsk.Saml.OpenIddict library
To configure OpenIddict as a SAML Identity Provider you'll need up to four packages.
The first Package is Rsk.Saml.OpenIddict
, this package contains all the services required and is not optional.
dotnet add package Rsk.Saml.OpenIddict
The next package is Rsk.Saml.OpenIddict.EntityFrameworkCore, this contains EntityFramework Core implementations of the store interfaces in the Rsk.Saml.OpenIddict package. As OpenIddict contains no In-Memory store implementations, this is required.
While OpenIddict also supports MongoDb, at this time we have no official support planned for MongoDb
dotnet add package Rsk.Saml.OpenIddict.EntityFrameworkCore
The next package is the Asp.Net Identity integration package. This package is optional, if you are not using Asp.Net Identity see Configuring OpenIddict Saml with a custom user store.
As the Velusia Server uses Asp.Net Identity with the built-in views you will need to install this package if you have started with that sample.
dotnet add package Rsk.Saml.AspNetCore.Identity
The final package is the Quartz.Net integration package. This package is only required if you wish to use the Quartz.Net scheduler to prune old SAML messages or SAML artifact data.
As the Velusia Server uses Quartz.Net, you will need to install this package if you have started with that sample.
dotnet add package Rsk.Saml.OpenIddict.Quartz
Configure OpenIddict
To configure OpenIddict as an SAML IdentityProvider you need access to the OpenIddictServerBuilder
object. This can be accessed in your AddServer
method.
services.AddOpenIddict()
.AddCore(options =>
{
// Emitted for brevity ...
})
.AddServer(options =>
{
// Enable the authorization, logout, token and userinfo endpoints.
options.SetAuthorizationEndpointUris("connect/authorize")
.SetLogoutEndpointUris("connect/logout")
.SetTokenEndpointUris("connect/token")
.SetUserinfoEndpointUris("connect/userinfo");
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles);
// Note: this sample only uses the authorization code flow but you can enable
// the other flows if you need to support implicit, password or client credentials.
options.AllowAuthorizationCodeFlow();
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableStatusCodePagesIntegration();
// Configure SAML for OpenIddict
options.AddSamlPlugin(builder =>
{
builder.UseSamlEntityFrameworkCore()
.AddSamlDbContexts(// Options builder emitted);
builder.ConfigureSamlOpenIddictServerOptions(serverOptions =>
{
serverOptions.HostOptions = new SamlHostUserInteractionOptions()
{
LoginUrl = "/Identity/Account/Login",
LogoutUrl = "/connect/logout",
};
serverOptions.IdpOptions = new SamlIdpOptions()
{
Licensee = "",
LicenseKey = "",
};
});
builder.PruneSamlMessages();
builder.AddSamlAspIdentity<ApplicationUser>();
});
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Emitted for brevity ...
});
In the AddServer
method, call the AddSamlPlugin
extension method. This will add all the services required for SAML to the DI container and give you access to the OpenIddictSamlBuilder
to continue the configuration.
The SAML component for OpenIddict requires three DbContexts registered, a SamlConfigurationDbContext
to store SAML ServiceProvider configuration, a AddSamlMessageDbContext
to store the messages and a SamlArtefactDbContext
to store SAML artifact messages.
To setup OpenIddict with EntityFramework Core, call the UseSamlEntityFrameworkCore
extension method, this will give you access to a OpenIddictSamlEntityFrameworkCoreBuilder
. Then either use the AddSamlConfigurationDbContext
, AddSamlMessageDbContext
and AddSamlArtefactDbContext
methods passing your DbContextOptionsBuilder
to add the DbContexts, or the AddSamlDbContexts
method to configure all three at once.
A properly configured DbContext is needed to generate and run migrations for the component, for more information on migrations with Saml for OpenIddict see our documentation.
If you are unfamiliar with how EntityFramework Core configures DbContexts check out Microsoft's documentation on the subject.
Next call ConfigureSamlOpenIddictServerOptions
and configure both the SamlHostUserInteractionOptions
and SamlIdpOptions
.
The SamlHostUserInteractionOptions
defines values about the OpenIddict Server the SAML component requires. Such as the LoginUrl
and LogoutUrl
which are the login and logout routes respectively. If you're using the Velusia sample with the built-in Asp.Identity views, set these to "/Identity/Account/Login
and "/Connect/Logout"
. There are some additional parameters but these can be left as the default for now. For a full list see.
The SamlIdPOptions
allows you to configure Saml specific options about the IdentityProvider. Whether or not the Identity Provider requires signed authentication requests can be configured here. Configure your Licensee and LicenseKey here, the rest of the options can be left as they are. For a full list of all the options see.
If you're using the Quartz integration package to clear expired Saml messages or artifact data, call either or both the PruneSamlArtifacts
or PruneSamlMessages
extension methods here. For more information on our Quartz integration see Saml OpenIddict Quartz integration
Finally add a call to UseOpenIddictSamlPlugin
to your IApplicationBuilder
to configure the required SAML middleware.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
else
{
app.UseStatusCodePagesWithReExecute("~/error");
//app.UseExceptionHandler("~/error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
//app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseOpenIddictSamlPlugin();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapDefaultControllerRoute();
endpoints.MapRazorPages();
});
}
Configuring a Service Provider
The OpenIddict Velusia sample contains a Hosted Service class Worker
which runs before application startup to perform migrations and seed the database with the required Client configuration. We'll be modifying the StartAsync
method to add our Saml ServiceProvider configuration.
public async Task StartAsync(CancellationToken cancellationToken)
{
await using var scope = _serviceProvider.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await context.Database.EnsureCreatedAsync();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("mvc") == null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC client application",
RedirectUris =
{
new Uri("https://localhost:44338/callback/login/local")
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44338/callback/logout/local")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
}
The StartAsync
method creates a Scope and resolves the Application DbContext to perform migrations and seed the database. Due to the way the Velusia sample and OpenIddict are architected, this context contains both the Asp.Net Identity and the OpenIddict entities so the Migrate command creates tables for both sets.
To follow this way of seeding the database you can do the same for both the OpenIddictSamlMessageDbContext
, SamlConfigurationDbContext
and the SamlArtifactDbContext
.
var samlOpenIddictMessageContext = scope.ServiceProvider.GetRequiredService<OpenIddictSamlMessageDbContext>();
await samlOpenIddictMessageContext.Database.EnsureCreatedAsync();
var samlConfigurationContext = scope.ServiceProvider.GetRequiredService<SamlConfigurationDbContext>();
await samlConfigurationContext.Database.EnsureCreatedAsync();
var samlArtifactContext = scope.ServiceProvider.GetRequiredService<SamlArtifactDbContext>();
await samlArtifactContext.Database.EnsureCreatedAsync();
Otherwise you can generate and perform migrations in your preferred way, see Microsoft's documentation on migrations in EntityFramework Core for more information.
You can now configure an OpenIddict Application (OAuth Client) and a Saml ServiceProvider. You can resolve the IOpenIddictApplicationManager
from the scope to create the Application object using an OpenIddictApplicationDescriptor
. The Application ClientId should match the EntityId of the Saml ServiceProvider. For a full list of ServiceProvider options see, Configuring a ServiceProvider
The Worker class already contains a reference to a ServiceProvider object due to importing the Microsoft.Extensions.DependencyInjection namespace, to find the correct Saml ServiceProvider object use the full namespace, Rsk.Saml.Models.ServiceProver
If you have configured the Velusia Sample Client as ServiceProvider you can copy the below settings. Otherwise set your ClientId and EntityId to the EntityId from SAML the metadata. The OpenIddict Application will also require at least one scope permission as SAML assertions require at least one assertion to be generated. The component will use the corresponding Application's scopes to control data access.
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync(clientId) == null)
{
var newClientDescriptor = new OpenIddictApplicationDescriptor()
{
ClientId = "https://localhost:44338",
Permissions = { Permissions.Scopes.Email }
};
await manager.CreateAsync(newClientDescriptor);
}
if (await samlConfigurationContext.ServiceProviders.SingleOrDefaultAsync(provider => provider.EntityId == clientId) == null)
{
var serviceProvider = new Rsk.Saml.Models.ServiceProvider()
{
EntityId = "https://localhost:44338",
AssertionConsumerServices = new List<Service>()
{
new Service(SamlConstants.BindingTypes.HttpPost, "https://localhost:44338/saml/acs"),
},
SingleLogoutServices = new List<Service>()
{
new Service(SamlConstants.BindingTypes.HttpRedirect, "https://localhost:44338/saml/slo")
},
RequireAuthenticationRequestsSigned = false
};
await samlConfigurationContext.ServiceProviders.AddAsync(serviceProvider.ToEntity());
await samlConfigurationContext.SaveChangesAsync();
}
Here RequireAuthenticationRequestsSigned
has been set to false to streamline the setup process, if you want to configure signed SAML requests you'll need to configure your ServiceProviders public signing key under the SigningCertificates
collection on the ServiceProvider.
Scope to Claim Mapping
OpenIddict has no internal concept of scope/claim mapping, in the Velusia sample IdentityProvider, inside the authorize endpoint passthrough method, Identity information is retrieved from the Asp.Identity User Manager before being passed onto a GetDestinations
method to decide which tokens the Claim needs to be inserted into.
private static IEnumerable<string> GetDestinations(Claim claim)
{
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (claim.Subject.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (claim.Subject.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (claim.Subject.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
The SAML component also requires this mapping information, to provide this you can either provide a IOpenIddictSamlScopeClaimMapper
implementation and add it to the DI container, or use the default OpenIddictSamlScopeClaimMapper
provided. The default implementation will check a Scopes Properties
Dictionary for a Claims
array. For more information, see our OpenIddict Scope Claim Mapping documentation
To use the default implementation, when you seed your Scope objects, use the AddClaimTypes
extension method on the OpenIddictScopeDescriptor
.
The component will throw a warning on startup if you are using the default scope mapper and any of the configured scopes don't have a claims collection in their properties
If you are planning to use the default OIDC scopes, you will need to seed a OpenIddict Scope with their corresponding claims types added to the properties.
var emailScopeDescriptor = new OpenIddictScopeDescriptor()
{
//...
};
emailScopeDescriptor.AddClaimTypes("email");
await scopeManager.CreateAsync(emailScopeDescriptor);
You should now have a correctly configured OpenIddict implementation with a SAML ServiceProvider. If the integration is not successful the component will display validation errors via logging. Check out our troubleshooting page or FAQ for common issues. If you are still having issues see our support page for next steps.