Skip to content

Commit 3485f44

Browse files
authored
Initial SAML Drop (#2375)
* Initial import of SAML code * Added missing SAML-relevant tests * Fixed compilation errors after rebasing XUnit V3 changes * Fixed subtle issues introduced in initial code import * Reworked test to use Kestrel based test hosts to allow cross-server communication * Eliminated odd value types to remain more consistent with code base * Do not enable SAML by default, but provide mechnism to opt-in * Base SAML test client * Add directions for configuring cert for SAML sample * Include encrypting assertions in SAML sample * Apply CT alias to SAML code * Dotnet format * Updated necessary SAML code after rebasing cooperative cancellation support * Updated SAML code to participate in cooperative cancellation * Fix name of SAML sign in state cookie
1 parent e556d5b commit 3485f44

File tree

202 files changed

+20924
-34
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

202 files changed

+20924
-34
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
<PackageVersion Include="SimpleFeedReader" Version="2.0.4" />
9999
<PackageVersion Include="Spectre.Console.Cli" Version="0.53.0" />
100100
<PackageVersion Include="Spectre.Console.Json" Version="0.53.0" />
101+
<PackageVersion Include="Sustainsys.Saml2.AspNetCore2" Version="2.11.0" />
101102
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
102103
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
103104
<PackageVersion Include="System.Text.Json" Version="10.0.0" />

identity-server/aspire/AppHosts/All/All.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<ProjectReference Include="..\..\..\clients\src\MvcHybridBackChannel\MvcHybridBackChannel.csproj" />
4848
<ProjectReference Include="..\..\..\clients\src\MvcJarJwt\MvcJarJwt.csproj" />
4949
<ProjectReference Include="..\..\..\clients\src\MvcJarUriJwt\MvcJarUriJwt.csproj" />
50+
<ProjectReference Include="..\..\..\clients\src\MvcSaml\MvcSaml.csproj" />
5051
<ProjectReference Include="..\..\..\clients\src\Web\Web.csproj" />
5152
<ProjectReference Include="..\..\..\clients\src\WindowsConsoleSystemBrowser\WindowsConsoleSystemBrowser.csproj" />
5253
<ProjectReference Include="..\..\..\hosts\AspNetIdentity10\Host.AspNetIdentity10.csproj" />

identity-server/aspire/AppHosts/All/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ void ConfigureWebClients()
118118
RegisterClientIfEnabled<Projects.MvcHybridBackChannel>("mvc-hybrid-backchannel");
119119
RegisterClientIfEnabled<Projects.MvcJarJwt>("mvc-jar-jwt");
120120
RegisterClientIfEnabled<Projects.MvcJarUriJwt>("mvc-jar-uri-jwt");
121+
RegisterClientIfEnabled<Projects.MvcSaml>("mvc-saml");
121122
RegisterClientIfEnabled<Projects.Web>("web");
122123
RegisterTemplateIfEnabled<Projects.IdentityServerTemplate>("template-is", 7001);
123124
RegisterTemplateIfEnabled<Projects.IdentityServerEmpty>("template-is-empty", 7002);

identity-server/aspire/AppHosts/All/appsettings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"MvcHybridBackChannel": true,
4343
"MvcJarJwt": true,
4444
"MvcJarUriJwt": true,
45+
"MvcSaml": true,
4546
"Web": true,
4647
"WindowsConsoleSystemBrowser": false
4748
},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
saml-sp.pfx
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Authentication;
5+
using Microsoft.AspNetCore.Authentication.Cookies;
6+
using Microsoft.AspNetCore.Authorization;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Sustainsys.Saml2.AspNetCore2;
9+
10+
namespace MvcSaml.Controllers;
11+
12+
public class HomeController : Controller
13+
{
14+
[AllowAnonymous]
15+
public IActionResult Index() => View();
16+
17+
public IActionResult Secure() => View();
18+
19+
public IActionResult Logout() => SignOut(
20+
new AuthenticationProperties { RedirectUri = "/" },
21+
Saml2Defaults.Scheme,
22+
CookieAuthenticationDefaults.AuthenticationScheme);
23+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using System.Security.Cryptography.X509Certificates;
5+
using Microsoft.AspNetCore.Authentication.Cookies;
6+
using Sustainsys.Saml2;
7+
using Sustainsys.Saml2.AspNetCore2;
8+
using Sustainsys.Saml2.Configuration;
9+
using Sustainsys.Saml2.Metadata;
10+
11+
namespace MvcSaml;
12+
13+
internal static class HostingExtensions
14+
{
15+
// The SP certificate is used to sign AuthnRequests and LogoutRequests sent to the IdP.
16+
// Generate it with the commands in README.md, then restart both this app and the IdP host.
17+
// Without the certificate, AuthnRequest signing and SP-initiated single logout are unavailable.
18+
private const string SpCertificatePath = "saml-sp.pfx";
19+
private const string SpCertificatePassword = "changeit";
20+
21+
public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
22+
{
23+
// The IdentityServer base URL is injected by Aspire at runtime via the "is-host" environment variable.
24+
var idpBaseUrl = builder.Configuration["is-host"]
25+
?? throw new InvalidOperationException("is-host configuration is required");
26+
27+
builder.Services.AddControllersWithViews();
28+
29+
var spCert = LoadSpCertificate();
30+
31+
builder.Services.AddAuthentication(options =>
32+
{
33+
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
34+
options.DefaultChallengeScheme = Saml2Defaults.Scheme;
35+
})
36+
.AddCookie(options =>
37+
{
38+
options.Cookie.Name = "mvcsaml";
39+
})
40+
.AddSaml2(options =>
41+
{
42+
// SP entity ID — must match the EntityId registered in the IdP's SamlServiceProviders config.
43+
// By convention, Sustainsys uses <base-url>/Saml2 as the entity ID.
44+
options.SPOptions.EntityId = new EntityId("https://localhost:44350/Saml2");
45+
46+
// Best practice: require the IdP to sign assertions.
47+
options.SPOptions.WantAssertionsSigned = true;
48+
49+
if (spCert != null)
50+
{
51+
// Best practice: sign AuthnRequests and LogoutRequests with the SP's certificate.
52+
// The IdP validates the signature using the public key registered in SamlServiceProviders.
53+
options.SPOptions.ServiceCertificates.Add(spCert);
54+
options.SPOptions.AuthenticateRequestSigningBehavior = SigningBehavior.Always;
55+
}
56+
else
57+
{
58+
// No certificate available — AuthnRequest signing and SP-initiated SLO are unavailable.
59+
// See README.md for instructions on generating saml-sp.pfx.
60+
options.SPOptions.AuthenticateRequestSigningBehavior = SigningBehavior.Never;
61+
}
62+
63+
// Load the IdP configuration from the metadata endpoint published by IdentityServer.
64+
// This automatically picks up signing certificates, endpoints, and capabilities.
65+
options.IdentityProviders.Add(
66+
new IdentityProvider(new EntityId(idpBaseUrl), options.SPOptions)
67+
{
68+
MetadataLocation = $"{idpBaseUrl}/saml/metadata",
69+
LoadMetadata = true
70+
});
71+
});
72+
73+
builder.Services.AddAuthorization();
74+
75+
return builder.Build();
76+
}
77+
78+
public static WebApplication ConfigurePipeline(this WebApplication app)
79+
{
80+
app.UseDeveloperExceptionPage();
81+
app.UseHttpsRedirection();
82+
app.UseStaticFiles();
83+
84+
app.UseRouting();
85+
86+
app.UseAuthentication();
87+
app.UseAuthorization();
88+
89+
app.MapDefaultControllerRoute()
90+
.RequireAuthorization();
91+
92+
return app;
93+
}
94+
95+
// Returns null if the certificate file has not been generated yet.
96+
// See README.md for generation instructions.
97+
private static X509Certificate2 LoadSpCertificate()
98+
{
99+
if (!File.Exists(SpCertificatePath))
100+
{
101+
return null;
102+
}
103+
104+
return X509CertificateLoader.LoadPkcs12FromFile(SpCertificatePath, SpCertificatePassword);
105+
}
106+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<UserSecretsId>3a8b2c1d-4e5f-6a7b-8c9d-0e1f2a3b4c5d</UserSecretsId>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<None Update="saml-sp.pfx" Condition="Exists('saml-sp.pfx')">
9+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
10+
</None>
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" />
15+
<PackageReference Include="Serilog.AspNetCore" />
16+
17+
<PackageReference Include="OpenTelemetry" />
18+
<PackageReference Include="OpenTelemetry.Exporter.Console" />
19+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
20+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
21+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
22+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
23+
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" />
24+
25+
<ProjectReference Include="..\..\..\aspire\ServiceDefaults\ServiceDefaults.csproj" />
26+
<ProjectReference Include="..\Constants\Constants.csproj" />
27+
</ItemGroup>
28+
29+
</Project>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using MvcSaml;
5+
using Serilog;
6+
using Serilog.Events;
7+
8+
Log.Logger = new LoggerConfiguration()
9+
.MinimumLevel.Information()
10+
.MinimumLevel.Override("MvcSaml", LogEventLevel.Debug)
11+
.Enrich.FromLogContext()
12+
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}")
13+
.CreateLogger();
14+
15+
try
16+
{
17+
var builder = WebApplication
18+
.CreateBuilder(args);
19+
20+
builder
21+
.AddServiceDefaults();
22+
23+
builder
24+
.ConfigureServices()
25+
.ConfigurePipeline()
26+
.Run();
27+
}
28+
catch (Exception ex)
29+
{
30+
Log.Fatal(ex, messageTemplate: "Unhandled exception");
31+
}
32+
finally
33+
{
34+
Log.Information(messageTemplate: "Shut down complete");
35+
Log.CloseAndFlush();
36+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"profiles": {
3+
"Host": {
4+
"commandName": "Project",
5+
"launchBrowser": true,
6+
"applicationUrl": "https://localhost:44350",
7+
"environmentVariables": {
8+
"ASPNETCORE_ENVIRONMENT": "Development"
9+
}
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)