From 626cb76e340036e8cc215c3dc788325e70a7eef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=ADsli=20Konr=C3=A1=C3=B0=20Bj=C3=B6rnsson?= Date: Wed, 23 Aug 2023 15:03:35 +0000 Subject: [PATCH] Adds sp-initiated endpoints --- src/AspNetCore.IdpSample/Startup.cs | 6 +++++ src/AspNetCore.SpSample/Startup.cs | 8 ++++++ .../Saml2pAuthenticationHandler.cs | 13 +--------- .../Builder/ApplicationBuilderExtensions.cs | 16 ++++++++++++ .../Builder/EndpointRouteBuilderExtensions.cs | 19 ++++++++++++-- .../Sp/FinishSsoEndpointMiddleware.cs | 26 +++++++++++++++++-- .../Models/Context/ValidateTokenContext.cs | 2 +- .../Models/Results/FinishSsoResult.cs | 12 +++++++-- .../Options/Saml2pOptions.cs | 5 ++++ 9 files changed, 88 insertions(+), 19 deletions(-) diff --git a/src/AspNetCore.IdpSample/Startup.cs b/src/AspNetCore.IdpSample/Startup.cs index 428add6..7f414d4 100644 --- a/src/AspNetCore.IdpSample/Startup.cs +++ b/src/AspNetCore.IdpSample/Startup.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; using Solid.Identity.Protocols.Saml2p; using Solid.Identity.Protocols.Saml2p.Abstractions; @@ -26,6 +27,11 @@ public class Startup { private static readonly string SigningCertificateBase64 = "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"; + static Startup() + { + IdentityModelEventSource.ShowPII = true; + } + public Startup(IConfiguration configuration) { Configuration = configuration; diff --git a/src/AspNetCore.SpSample/Startup.cs b/src/AspNetCore.SpSample/Startup.cs index d8a1604..575bcf0 100644 --- a/src/AspNetCore.SpSample/Startup.cs +++ b/src/AspNetCore.SpSample/Startup.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; using Solid.Identity.Protocols.Saml2p; using Solid.Identity.Protocols.Saml2p.Authentication; @@ -26,6 +27,10 @@ namespace AspNetCore.SpSample { public class Startup { + static Startup() + { + IdentityModelEventSource.ShowPII = true; + } public static readonly string SigningCertificateBase64 = "MIIDPDCCAiSgAwIBAgIQNl7j8AGK7J1B4E/BX+vSLzANBgkqhkiG9w0BAQUFADAgMR4wHAYDVQQDDBV0b2tlbi5zaWduYXR1cmUudGVzdHMwHhcNMjAwOTIzMTU1NTIwWhcNMzAwOTI1MTU1NTIwWjAgMR4wHAYDVQQDDBV0b2tlbi5zaWduYXR1cmUudGVzdHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+zSiOz1UGpHEUgd23nZ63TmX7EnbgRBqeJjcujZdyjCJvV5u/uIbSR3LBNlbS8Rv4uqYWRxotUaTSCdif/jryzHUwPORU2lt7XbeIGEK9aNv9LcxpuEu1sUo/7Ei34uJtMdoZh6cvlzGoGMcTxapQBxrmQyE6LOHkni/sA8zI+mKHbPrRUyeiL38A54Dnc4wAWWy8euQtu9bJge0qcnT0ezp41A1z/BQ6yRKioQ9jHiOgIKnBDdAhWTFPKH4Roq4lIMt8PpIy5F2VYP5rz95obFExnSwvd+8XHaHP5rjZ7yLhhSD9yZtYzLf9nw3ea6KgAAHBbg2iFJIswb1opzJlAgMBAAGjcjBwMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwIAYDVR0RBBkwF4IVdG9rZW4uc2lnbmF0dXJlLnRlc3RzMB0GA1UdDgQWBBQa8UJxJKZCMuFEbsUqLJtj1TMJODANBgkqhkiG9w0BAQUFAAOCAQEAguPDY/RnMhipwJS+6gsthlQ1lY55KMCkxEcyAJjz5pZpgfd4oG0gfmP7S2V1bxQ8hLAdYMRyF4yfUnog7YwCwecSIG0aADaksHWbQU+k51rk4d1VetZnwmRfktzs560dmprQKL9rseYZQhFbYYXe8yyFwe3fPgOJhZkIgq7eUzQRO6kXOEwRxxYmWE3XhiiALLGUA9Yb6yyLg3sQ4Myequk+W4Fxw3n9j0jCRTjye+JlycwLM+ST4Z5lFuZVLHWZqqreUYcRvYpJ9lIq7C5b/bQnJQ873rSF6jjx17E+/YrQFpJbjSJrl8cSx3QephdUWUC2Op4n051O91tM32Lrpg=="; public Startup(IConfiguration configuration) { @@ -48,6 +53,8 @@ public void ConfigureServices(IServiceCollection services) .AddSaml2p(options => { options.DefaultIssuer = "https://localhost:5003/saml"; + options.StartPath = "/saml2p/start"; + options.FinishPath = "/saml2p/finish"; options.AddIdentityProvider("https://localhost:5001/saml", idp => { idp.BaseUrl = new Uri("https://localhost:5001"); @@ -106,6 +113,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseCookiePolicy(); app.UseEndpoints(endpoints => { + endpoints.MapSaml2pServiceProvider("/sso"); endpoints.MapRazorPages(); }); } diff --git a/src/Solid.Identity.Protocols.Saml2p/Authentication/Saml2pAuthenticationHandler.cs b/src/Solid.Identity.Protocols.Saml2p/Authentication/Saml2pAuthenticationHandler.cs index 2ec4997..1dda296 100644 --- a/src/Solid.Identity.Protocols.Saml2p/Authentication/Saml2pAuthenticationHandler.cs +++ b/src/Solid.Identity.Protocols.Saml2p/Authentication/Saml2pAuthenticationHandler.cs @@ -47,18 +47,7 @@ protected override async Task HandleRemoteAuthenticateAsync if (!result.IsSuccessful) throw new SamlResponseException(result.PartnerId, result.Status, result.SubStatus); - var properties = new AuthenticationProperties - { - IssuedUtc = result.SecurityToken.ValidFrom, - ExpiresUtc = result.SecurityToken.ValidTo - }; - var token = new AuthenticationToken - { - Name = "saml2", - Value = result.Token - }; - properties.StoreTokens(new []{ token }); - var ticket = new AuthenticationTicket(result.Subject, properties, Scheme.Name); + var ticket = new AuthenticationTicket(result.Subject, result.Properties, Scheme.Name); return HandleRequestResult.Success(ticket); } catch(Exception ex) diff --git a/src/Solid.Identity.Protocols.Saml2p/Builder/ApplicationBuilderExtensions.cs b/src/Solid.Identity.Protocols.Saml2p/Builder/ApplicationBuilderExtensions.cs index acee1a8..9c81307 100644 --- a/src/Solid.Identity.Protocols.Saml2p/Builder/ApplicationBuilderExtensions.cs +++ b/src/Solid.Identity.Protocols.Saml2p/Builder/ApplicationBuilderExtensions.cs @@ -36,6 +36,22 @@ public static IApplicationBuilder UseSaml2pIdentityProvider(this IApplicationBui ; } + /// + /// Maps the SP endpoints to . + /// + /// The to map the endpoints to. + /// The base path to map the endpoints to. + /// The instance so that additional calls can be chained. + public static IApplicationBuilder UseSaml2pServiceProvider(this IApplicationBuilder builder, PathString path) + { + var options = builder.ApplicationServices.GetRequiredService>().Value; + + return builder + .Map(path.Add(options.StartPath), b => b.UseStartSsoEndpoint(path)) + .Map(path.Add(options.FinishPath), b => b.UseFinishSsoEndpoint(path)) + ; + } + internal static IApplicationBuilder UseStartSsoEndpoint(this IApplicationBuilder builder, PathString path) => builder.UsePathBase(path).UseMiddleware(); diff --git a/src/Solid.Identity.Protocols.Saml2p/Builder/EndpointRouteBuilderExtensions.cs b/src/Solid.Identity.Protocols.Saml2p/Builder/EndpointRouteBuilderExtensions.cs index cf4887a..e6b9980 100644 --- a/src/Solid.Identity.Protocols.Saml2p/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Solid.Identity.Protocols.Saml2p/Builder/EndpointRouteBuilderExtensions.cs @@ -27,8 +27,23 @@ public static IEndpointRouteBuilder MapSaml2pIdentityProvider(this IEndpointRout var options = builder.ServiceProvider.GetRequiredService>().Value; builder.Map(path.Add(options.AcceptPath), builder.CreateApplicationBuilder().UseAcceptSsoEndpoint(path).Build()).WithMetadata(new HttpMethodMetadata(new[] { HttpMethods.Get, HttpMethods.Post })); - builder.Map(path.Add(options.InitiatePath), builder.CreateApplicationBuilder().UseInitiateSsoEndpoint(path).Build()).WithMetadata(new HttpMethodMetadata(new[] { HttpMethods.Get, HttpMethods.Post })); ; - builder.Map(path.Add(options.CompletePath), builder.CreateApplicationBuilder().UseCompleteSsoEndpoint(path).Build()).WithMetadata(new HttpMethodMetadata(new[] { HttpMethods.Get, HttpMethods.Post })); ; + builder.Map(path.Add(options.InitiatePath), builder.CreateApplicationBuilder().UseInitiateSsoEndpoint(path).Build()).WithMetadata(new HttpMethodMetadata(new[] { HttpMethods.Get, HttpMethods.Post })); + builder.Map(path.Add(options.CompletePath), builder.CreateApplicationBuilder().UseCompleteSsoEndpoint(path).Build()).WithMetadata(new HttpMethodMetadata(new[] { HttpMethods.Get, HttpMethods.Post })); + return builder; + } + + /// + /// Maps the SP endpoints to . + /// + /// The to map the endpoints to. + /// The base path to map the endpoints to. + /// The instance so that additional calls can be chained. + public static IEndpointRouteBuilder MapSaml2pServiceProvider(this IEndpointRouteBuilder builder, PathString path) + { + var options = builder.ServiceProvider.GetRequiredService>().Value; + + builder.Map(path.Add(options.StartPath), builder.CreateApplicationBuilder().UseStartSsoEndpoint(path).Build()).WithMetadata(new HttpMethodMetadata(new[] { HttpMethods.Get, HttpMethods.Post })); + builder.Map(path.Add(options.FinishPath), builder.CreateApplicationBuilder().UseFinishSsoEndpoint(path).Build()).WithMetadata(new HttpMethodMetadata(new[] { HttpMethods.Get, HttpMethods.Post })); return builder; } } diff --git a/src/Solid.Identity.Protocols.Saml2p/Middleware/Sp/FinishSsoEndpointMiddleware.cs b/src/Solid.Identity.Protocols.Saml2p/Middleware/Sp/FinishSsoEndpointMiddleware.cs index da94a87..fce78c0 100644 --- a/src/Solid.Identity.Protocols.Saml2p/Middleware/Sp/FinishSsoEndpointMiddleware.cs +++ b/src/Solid.Identity.Protocols.Saml2p/Middleware/Sp/FinishSsoEndpointMiddleware.cs @@ -62,7 +62,15 @@ public override async Task InvokeAsync(HttpContext context) { try { - _ = await FinishSsoAsync(context); + var result = await FinishSsoAsync(context); + if (!result.IsSuccessful) + { + context.Response.StatusCode = 401; + return; + } + + await context.SignInAsync(result.Subject, result.Properties); + context.Response.Redirect(Options.DefaultRedirectPath); } catch(InvalidOperationException) { @@ -158,7 +166,21 @@ public async Task FinishSsoAsync(HttpContext context) context.User = validateContext.Subject; - return FinishSsoResult.Success(partner.Id, validateContext.Response.XmlSecurityToken, validateContext.SecurityToken, validateContext.Subject); + var properties = new AuthenticationProperties(); + if (validateContext.Subject != null) + { + properties.IssuedUtc = validateContext.SecurityToken!.ValidFrom; + properties.ExpiresUtc = validateContext.SecurityToken!.ValidTo; + + var authn = new AuthenticationToken + { + Name = "saml2", + Value = validateContext.Response.XmlSecurityToken + }; + properties.StoreTokens(new[] { authn }); + } + + return FinishSsoResult.Success(partner.Id, validateContext.Response.XmlSecurityToken, validateContext.SecurityToken, validateContext.Subject, properties); } } } diff --git a/src/Solid.Identity.Protocols.Saml2p/Models/Context/ValidateTokenContext.cs b/src/Solid.Identity.Protocols.Saml2p/Models/Context/ValidateTokenContext.cs index 5248538..6b66532 100644 --- a/src/Solid.Identity.Protocols.Saml2p/Models/Context/ValidateTokenContext.cs +++ b/src/Solid.Identity.Protocols.Saml2p/Models/Context/ValidateTokenContext.cs @@ -41,7 +41,7 @@ public class ValidateTokenContext public TokenValidationParameters TokenValidationParameters { get; internal set; } /// - /// The used to validatethe incoming security token. + /// The used to validate the incoming security token. /// public Saml2SecurityTokenHandler Handler { get; internal set; } diff --git a/src/Solid.Identity.Protocols.Saml2p/Models/Results/FinishSsoResult.cs b/src/Solid.Identity.Protocols.Saml2p/Models/Results/FinishSsoResult.cs index 82ebec3..5c2ca48 100644 --- a/src/Solid.Identity.Protocols.Saml2p/Models/Results/FinishSsoResult.cs +++ b/src/Solid.Identity.Protocols.Saml2p/Models/Results/FinishSsoResult.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Security.Claims; using System.Text; +using Microsoft.AspNetCore.Authentication; namespace Solid.Identity.Protocols.Saml2p.Models.Results { @@ -21,8 +22,9 @@ private FinishSsoResult() { } /// The XML representation of the received Saml2 token. /// The from a . /// The that was created from . + /// The instance used when signing in. /// A success result. - public static FinishSsoResult Success(string partnerId, string token, Saml2SecurityToken securityToken, ClaimsPrincipal subject) + public static FinishSsoResult Success(string partnerId, string token, Saml2SecurityToken securityToken, ClaimsPrincipal subject, AuthenticationProperties properties) { return new FinishSsoResult { @@ -30,7 +32,8 @@ public static FinishSsoResult Success(string partnerId, string token, Saml2Secur PartnerId = partnerId, Token = token, SecurityToken = securityToken, - Subject = subject + Subject = subject, + Properties = properties }; } @@ -81,6 +84,11 @@ public static FinishSsoResult Fail(string partnerId, Uri status, Uri subStatus) /// The that was created from . /// public ClaimsPrincipal Subject { get; private set; } + + /// + /// The instance used when signing in. + /// + public AuthenticationProperties Properties { get; private set; } /// /// The status of the SSO response. diff --git a/src/Solid.Identity.Protocols.Saml2p/Options/Saml2pOptions.cs b/src/Solid.Identity.Protocols.Saml2p/Options/Saml2pOptions.cs index 7b72656..da93252 100644 --- a/src/Solid.Identity.Protocols.Saml2p/Options/Saml2pOptions.cs +++ b/src/Solid.Identity.Protocols.Saml2p/Options/Saml2pOptions.cs @@ -65,6 +65,11 @@ public class Saml2pOptions /// public PathString FinishPath { get; set; } = "/finish"; + /// + /// The relative path used by default after SSO has finished (SP flow). + /// + public PathString DefaultRedirectPath { get; set; } = "/"; + /// /// Events object that contains delegates to be run during SSO (IDP flow). ///