mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Add directions for configuring cert for SAML sample
This commit is contained in:
parent
481eb372e6
commit
e178273d94
5 changed files with 115 additions and 5 deletions
1
identity-server/clients/src/MvcSaml/.gitignore
vendored
Normal file
1
identity-server/clients/src/MvcSaml/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
saml-sp.pfx
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Sustainsys.Saml2;
|
||||
using Sustainsys.Saml2.AspNetCore2;
|
||||
|
|
@ -11,6 +12,12 @@ namespace MvcSaml;
|
|||
|
||||
internal static class HostingExtensions
|
||||
{
|
||||
// The SP certificate is used to sign AuthnRequests and LogoutRequests sent to the IdP.
|
||||
// Generate it with the commands in README.md, then restart both this app and the IdP host.
|
||||
// Without the certificate, AuthnRequest signing and SP-initiated single logout are unavailable.
|
||||
private const string SpCertificatePath = "saml-sp.pfx";
|
||||
private const string SpCertificatePassword = "changeit";
|
||||
|
||||
public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
// The IdentityServer base URL is injected by Aspire at runtime via the "is-host" environment variable.
|
||||
|
|
@ -19,6 +26,8 @@ internal static class HostingExtensions
|
|||
|
||||
builder.Services.AddControllersWithViews();
|
||||
|
||||
var spCert = LoadSpCertificate();
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
|
|
@ -37,9 +46,19 @@ internal static class HostingExtensions
|
|||
// Best practice: require the IdP to sign assertions.
|
||||
options.SPOptions.WantAssertionsSigned = true;
|
||||
|
||||
// Best practice: the SP does not sign AuthnRequests for this sample (no SP cert needed).
|
||||
// Set to Always and add a ServiceCertificate to enable signed AuthnRequests.
|
||||
options.SPOptions.AuthenticateRequestSigningBehavior = SigningBehavior.Never;
|
||||
if (spCert != null)
|
||||
{
|
||||
// Best practice: sign AuthnRequests and LogoutRequests with the SP's certificate.
|
||||
// The IdP validates the signature using the public key registered in SamlServiceProviders.
|
||||
options.SPOptions.ServiceCertificates.Add(spCert);
|
||||
options.SPOptions.AuthenticateRequestSigningBehavior = SigningBehavior.Always;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No certificate available — AuthnRequest signing and SP-initiated SLO are unavailable.
|
||||
// See README.md for instructions on generating saml-sp.pfx.
|
||||
options.SPOptions.AuthenticateRequestSigningBehavior = SigningBehavior.Never;
|
||||
}
|
||||
|
||||
// Load the IdP configuration from the metadata endpoint published by IdentityServer.
|
||||
// This automatically picks up signing certificates, endpoints, and capabilities.
|
||||
|
|
@ -72,4 +91,16 @@ internal static class HostingExtensions
|
|||
|
||||
return app;
|
||||
}
|
||||
|
||||
// Returns null if the certificate file has not been generated yet.
|
||||
// See README.md for generation instructions.
|
||||
private static X509Certificate2 LoadSpCertificate()
|
||||
{
|
||||
if (!File.Exists(SpCertificatePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return X509CertificateLoader.LoadPkcs12FromFile(SpCertificatePath, SpCertificatePassword);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@
|
|||
<UserSecretsId>3a8b2c1d-4e5f-6a7b-8c9d-0e1f2a3b4c5d</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="saml-sp.pfx" Condition="Exists('saml-sp.pfx')">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
|
|
|
|||
39
identity-server/clients/src/MvcSaml/README.md
Normal file
39
identity-server/clients/src/MvcSaml/README.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# MvcSaml
|
||||
|
||||
This client demonstrates SAML 2.0 single sign-on and single logout against IdentityServer.
|
||||
|
||||
## SP Certificate
|
||||
|
||||
The SP certificate is required for two SAML best practices:
|
||||
|
||||
- **Signed AuthnRequests** — the SP signs every authentication request it sends to the IdP, proving the request originated from this SP and has not been tampered with.
|
||||
- **SP-initiated Single Logout (SLO)** — the SP signs logout requests sent to the IdP. The IdP always requires signed logout requests.
|
||||
|
||||
Without the certificate, the SSO login flow still works, but AuthnRequest signing is disabled and SP-initiated single logout will fail.
|
||||
|
||||
### Generating the certificate
|
||||
|
||||
Create a self-signed certificate with `openssl`:
|
||||
|
||||
```sh
|
||||
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes -subj "/CN=MvcSaml SP"
|
||||
openssl pkcs12 -export -out saml-sp.pfx -inkey key.pem -in cert.pem -passout pass:changeit
|
||||
rm key.pem cert.pem
|
||||
```
|
||||
|
||||
Place `saml-sp.pfx` in this project directory (`clients/src/MvcSaml/`). The file is excluded from source control.
|
||||
|
||||
After generating the certificate, **restart both this app and the IdentityServer host** so both sides pick up the new public key.
|
||||
|
||||
### Why both sides need to restart
|
||||
|
||||
The MvcSaml SP reads the certificate at startup to configure request signing. The IdentityServer host also reads the same certificate file at startup to register the SP's public key for signature validation. Both must be restarted whenever the certificate is regenerated.
|
||||
|
||||
## Without the certificate
|
||||
|
||||
| Feature | Without certificate | With certificate |
|
||||
|---|---|---|
|
||||
| SSO (login) | Works | Works |
|
||||
| AuthnRequest signing | Disabled | Enabled (always signed) |
|
||||
| SP-initiated Single Logout | Fails (unsigned logout request rejected by IdP) | Works |
|
||||
| IdP-initiated Single Logout | Works (IdP signs its own requests) | Works |
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Duende.IdentityServer.Models;
|
||||
|
||||
namespace Duende.IdentityServer.Hosts.Shared.Configuration;
|
||||
|
||||
public static class SamlServiceProviders
|
||||
{
|
||||
// Path to the MvcSaml SP certificate, relative to the host's working directory.
|
||||
// Must match the file generated by following README.md in the MvcSaml project.
|
||||
private const string SpCertificatePath = "../../clients/src/MvcSaml/saml-sp.pfx";
|
||||
private const string SpCertificatePassword = "changeit";
|
||||
|
||||
public static IEnumerable<SamlServiceProvider> Get() =>
|
||||
[
|
||||
new SamlServiceProvider
|
||||
|
|
@ -16,10 +22,37 @@ public static class SamlServiceProviders
|
|||
Enabled = true,
|
||||
// ACS URL follows the Sustainsys.Saml2 convention: <base>/Saml2/Acs
|
||||
AssertionConsumerServiceUrls = [new Uri("https://localhost:44350/Saml2/Acs")],
|
||||
// SLO URL follows the Sustainsys.Saml2 convention: <base>/Saml2/Logout
|
||||
SingleLogoutServiceUrl = new SamlEndpointType
|
||||
{
|
||||
Location = new Uri("https://localhost:44350/Saml2/Logout"),
|
||||
Binding = SamlBinding.HttpRedirect
|
||||
},
|
||||
// Sign the assertion (not the response envelope) — the Sustainsys default expectation
|
||||
SigningBehavior = SamlSigningBehavior.SignAssertion,
|
||||
// No RequireSignedAuthnRequests — keeps the sample self-contained without distributing an SP cert
|
||||
// No EncryptAssertions — plain HTTP is fine for local development
|
||||
// When the SP certificate is present, require signed AuthnRequests and register the
|
||||
// SP's public key so the IdP can validate the signatures.
|
||||
RequireSignedAuthnRequests = SpCertificateExists(),
|
||||
SigningCertificates = LoadSpSigningCertificates(),
|
||||
}
|
||||
];
|
||||
|
||||
private static bool SpCertificateExists() => File.Exists(SpCertificatePath);
|
||||
|
||||
private static ICollection<X509Certificate2> LoadSpSigningCertificates()
|
||||
{
|
||||
if (!SpCertificateExists())
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// For ease during development, we load the public key directly from the SP's certificate file.
|
||||
// In a deployed application, you would want to distribute the public key through PKI or
|
||||
// another secure channel rather than reading the SP's private key material here.
|
||||
#pragma warning disable SYSLIB0057
|
||||
// Only obsolete in .NET 9+; keeping the older API while we support .NET 8.
|
||||
var cert = new X509Certificate2(SpCertificatePath, SpCertificatePassword);
|
||||
#pragma warning restore SYSLIB0057
|
||||
return [cert];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue