From 13cae6cbbe41638e8ccd5dfc69b71ead42197fb8 Mon Sep 17 00:00:00 2001 From: Charley Wu Date: Sun, 14 Apr 2024 18:31:54 +0800 Subject: [PATCH] Simplify single logout --- .../Controllers/AccountController.cs | 15 ++- samples/AspNetCoreReactSample/Program.cs | 52 +++-------- .../Pages/Account/Logout.cshtml.cs | 17 +++- samples/AspNetCoreSample/Program.cs | 67 ++++---------- .../Pages/Account/Logout.cshtml.cs | 17 +++- samples/BlazorSample/Program.cs | 52 +++-------- .../Controllers/AccountController.cs | 16 +++- samples/OwinSample/Startup.cs | 92 ++++++++++--------- 8 files changed, 142 insertions(+), 186 deletions(-) diff --git a/samples/AspNetCoreReactSample/Controllers/AccountController.cs b/samples/AspNetCoreReactSample/Controllers/AccountController.cs index be5fb720..564d2229 100644 --- a/samples/AspNetCoreReactSample/Controllers/AccountController.cs +++ b/samples/AspNetCoreReactSample/Controllers/AccountController.cs @@ -48,9 +48,20 @@ public IActionResult Login(string? scheme) [AllowAnonymous] [HttpGet("/account/logout")] - public IActionResult Logout() + public async Task Logout(string? redirectUrl) { - return SignOut(); + if (string.IsNullOrWhiteSpace(redirectUrl)) + { + redirectUrl = "/"; + } + + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + await HttpContext.SignOutAsync(properties); + var authScheme = User.Claims.FirstOrDefault(x => string.Equals(x.Type, "auth_scheme"))?.Value; + if (!string.IsNullOrWhiteSpace(authScheme)) + { + await HttpContext.SignOutAsync(authScheme, properties); + } } } diff --git a/samples/AspNetCoreReactSample/Program.cs b/samples/AspNetCoreReactSample/Program.cs index 835475a4..90579f97 100644 --- a/samples/AspNetCoreReactSample/Program.cs +++ b/samples/AspNetCoreReactSample/Program.cs @@ -1,7 +1,6 @@ using System.Security.Claims; using GSS.Authentication.CAS; using GSS.Authentication.CAS.AspNetCore; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -16,45 +15,7 @@ options.FallbackPolicy = options.DefaultPolicy; }); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) - .AddCookie(options => - { - options.Events.OnSigningOut = async context => - { - var authService = context.HttpContext.RequestServices.GetRequiredService(); - var result = await authService.AuthenticateAsync(context.HttpContext, null); - string? authScheme = null; - if (result.Properties != null || result.Properties!.Items.TryGetValue(".AuthScheme", out authScheme) && - string.IsNullOrWhiteSpace(authScheme)) - { - if (string.Equals(authScheme, CasDefaults.AuthenticationType)) - { - options.CookieManager.DeleteCookie(context.HttpContext, options.Cookie.Name!, - context.CookieOptions); - // redirecting to the identity provider to sign out - await context.HttpContext.SignOutAsync(authScheme); - return; - } - - if (string.Equals(authScheme, OpenIdConnectDefaults.AuthenticationScheme) && - builder.Configuration.GetValue("OIDC:SaveTokens", false)) - { - options.CookieManager.DeleteCookie(context.HttpContext, options.Cookie.Name!, - context.CookieOptions); - // redirecting to the identity provider to sign out - await context.HttpContext.SignOutAsync(authScheme); - return; - } - } - - await context.Options.Events.RedirectToLogout(new RedirectContext( - context.HttpContext, - context.Scheme, - context.Options, - context.Properties, - "/" - )); - }; - }) + .AddCookie() .AddCAS(options => { options.CasServerUrlBase = builder.Configuration["CAS:ServerUrlBase"]!; @@ -65,6 +26,7 @@ await context.Options.Events.RedirectToLogout(new RedirectContext options.Scope.Add(s)); } - + options.Events.OnTokenValidated = context => + { + if (context.Principal?.Identity is ClaimsIdentity claimIdentity) + { + claimIdentity.AddClaim(new Claim("auth_scheme", OpenIdConnectDefaults.AuthenticationScheme)); + } + return Task.CompletedTask; + }; options.TokenValidationParameters.NameClaimType = builder.Configuration.GetValue("OIDC:NameClaimType", "name"); }); diff --git a/samples/AspNetCoreSample/Pages/Account/Logout.cshtml.cs b/samples/AspNetCoreSample/Pages/Account/Logout.cshtml.cs index 88ac8ad2..6f78d20a 100644 --- a/samples/AspNetCoreSample/Pages/Account/Logout.cshtml.cs +++ b/samples/AspNetCoreSample/Pages/Account/Logout.cshtml.cs @@ -1,12 +1,23 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc.RazorPages; namespace AspNetCoreSample.Pages.Account; public class LogoutModel : PageModel { - public IActionResult OnGet() + public async Task OnGet(string? redirectUrl) { - return SignOut(); + if (string.IsNullOrWhiteSpace(redirectUrl)) + { + redirectUrl = "/"; + } + + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + await HttpContext.SignOutAsync(properties); + var authScheme = User.Claims.FirstOrDefault(x => string.Equals(x.Type, "auth_scheme"))?.Value; + if (!string.IsNullOrWhiteSpace(authScheme)) + { + await HttpContext.SignOutAsync(authScheme, properties); + } } } \ No newline at end of file diff --git a/samples/AspNetCoreSample/Program.cs b/samples/AspNetCoreSample/Program.cs index c6258907..12ac9f3f 100644 --- a/samples/AspNetCoreSample/Program.cs +++ b/samples/AspNetCoreSample/Program.cs @@ -2,7 +2,6 @@ using System.Security.Cryptography.X509Certificates; using GSS.Authentication.CAS; using GSS.Authentication.CAS.AspNetCore; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -34,53 +33,7 @@ options.FallbackPolicy = options.DefaultPolicy; }); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) - .AddCookie(options => - { - options.Events.OnSigningOut = async context => - { - var authService = context.HttpContext.RequestServices.GetRequiredService(); - var result = await authService.AuthenticateAsync(context.HttpContext, null); - string? authScheme = null; - if (result.Properties != null || result.Properties!.Items.TryGetValue(".AuthScheme", out authScheme) && - string.IsNullOrWhiteSpace(authScheme)) - { - if (string.Equals(authScheme, CasDefaults.AuthenticationType)) - { - options.CookieManager.DeleteCookie(context.HttpContext, options.Cookie.Name!, - context.CookieOptions); - // redirecting to the identity provider to sign out - await context.HttpContext.SignOutAsync(authScheme); - return; - } - - if (string.Equals(authScheme, OpenIdConnectDefaults.AuthenticationScheme) && - builder.Configuration.GetValue("OIDC:SaveTokens", false)) - { - options.CookieManager.DeleteCookie(context.HttpContext, options.Cookie.Name!, - context.CookieOptions); - // redirecting to the identity provider to sign out - await context.HttpContext.SignOutAsync(authScheme); - return; - } - } - - var saml2SessionIndex = context.HttpContext.User.FindFirst(Saml2ClaimTypes.SessionIndex); - if (saml2SessionIndex != null) - { - // redirecting to the identity provider to sign out - await context.HttpContext.SignOutAsync(Saml2Defaults.Scheme); - return; - } - - await context.Options.Events.RedirectToLogout(new RedirectContext( - context.HttpContext, - context.Scheme, - context.Options, - context.Properties, - "/" - )); - }; - }) + .AddCookie() .AddCAS(options => { options.CasServerUrlBase = builder.Configuration["CAS:ServerUrlBase"]!; @@ -92,6 +45,7 @@ await context.Options.Events.RedirectToLogout(new RedirectContext options.Scope.Add(s)); } - options.TokenValidationParameters.NameClaimType = builder.Configuration.GetValue("OIDC:NameClaimType", "name"); + options.Events.OnTokenValidated = context => + { + if (context.Principal?.Identity is ClaimsIdentity claimIdentity) + { + claimIdentity.AddClaim(new Claim("auth_scheme", OpenIdConnectDefaults.AuthenticationScheme)); + } + return Task.CompletedTask; + }; options.Events.OnRemoteFailure = context => { var failure = context.Failure; @@ -182,6 +144,13 @@ await context.Options.Events.RedirectToLogout(new RedirectContext + { + if (result.Principal?.Identity is ClaimsIdentity claimIdentity) + { + claimIdentity.AddClaim(new Claim("auth_scheme", Saml2Defaults.Scheme)); + } + }; options.Notifications.MetadataCreated = (metadata, _) => { var ssoDescriptor = metadata.RoleDescriptors.OfType().First(); diff --git a/samples/BlazorSample/Pages/Account/Logout.cshtml.cs b/samples/BlazorSample/Pages/Account/Logout.cshtml.cs index 4c2081a0..f29ecdbf 100644 --- a/samples/BlazorSample/Pages/Account/Logout.cshtml.cs +++ b/samples/BlazorSample/Pages/Account/Logout.cshtml.cs @@ -1,12 +1,23 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc.RazorPages; namespace BlazorSample.Pages.Account; public class Logout : PageModel { - public IActionResult OnGet() + public async Task OnGet(string? redirectUrl) { - return SignOut(); + if (string.IsNullOrWhiteSpace(redirectUrl)) + { + redirectUrl = "/"; + } + + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + await HttpContext.SignOutAsync(properties); + var authScheme = User.Claims.FirstOrDefault(x => string.Equals(x.Type, "auth_scheme"))?.Value; + if (!string.IsNullOrWhiteSpace(authScheme)) + { + await HttpContext.SignOutAsync(authScheme, properties); + } } } \ No newline at end of file diff --git a/samples/BlazorSample/Program.cs b/samples/BlazorSample/Program.cs index 3ef0eb34..cea8a449 100644 --- a/samples/BlazorSample/Program.cs +++ b/samples/BlazorSample/Program.cs @@ -2,7 +2,6 @@ using BlazorSample.Components; using GSS.Authentication.CAS; using GSS.Authentication.CAS.AspNetCore; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -19,45 +18,7 @@ options.FallbackPolicy = options.DefaultPolicy; }); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) - .AddCookie(options => - { - options.Events.OnSigningOut = async context => - { - var authService = context.HttpContext.RequestServices.GetRequiredService(); - var result = await authService.AuthenticateAsync(context.HttpContext, null); - string? authScheme = null; - if (result.Properties != null || result.Properties!.Items.TryGetValue(".AuthScheme", out authScheme) && - string.IsNullOrWhiteSpace(authScheme)) - { - if (string.Equals(authScheme, CasDefaults.AuthenticationType)) - { - options.CookieManager.DeleteCookie(context.HttpContext, options.Cookie.Name!, - context.CookieOptions); - // redirecting to the identity provider to sign out - await context.HttpContext.SignOutAsync(authScheme); - return; - } - - if (string.Equals(authScheme, OpenIdConnectDefaults.AuthenticationScheme) && - builder.Configuration.GetValue("OIDC:SaveTokens", false)) - { - options.CookieManager.DeleteCookie(context.HttpContext, options.Cookie.Name!, - context.CookieOptions); - // redirecting to the identity provider to sign out - await context.HttpContext.SignOutAsync(authScheme); - return; - } - } - - await context.Options.Events.RedirectToLogout(new RedirectContext( - context.HttpContext, - context.Scheme, - context.Options, - context.Properties, - "/" - )); - }; - }) + .AddCookie() .AddCAS(options => { options.CasServerUrlBase = builder.Configuration["CAS:ServerUrlBase"]!; @@ -68,6 +29,7 @@ await context.Options.Events.RedirectToLogout(new RedirectContext options.Scope.Add(s)); } - + options.Events.OnTokenValidated = context => + { + if (context.Principal?.Identity is ClaimsIdentity claimIdentity) + { + claimIdentity.AddClaim(new Claim("auth_scheme", OpenIdConnectDefaults.AuthenticationScheme)); + } + return Task.CompletedTask; + }; options.TokenValidationParameters.NameClaimType = builder.Configuration.GetValue("OIDC:NameClaimType", "name"); }); diff --git a/samples/OwinSample/Controllers/AccountController.cs b/samples/OwinSample/Controllers/AccountController.cs index ddd169a9..85a7af1d 100644 --- a/samples/OwinSample/Controllers/AccountController.cs +++ b/samples/OwinSample/Controllers/AccountController.cs @@ -1,6 +1,7 @@ using System.Web; using System.Web.Mvc; using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; namespace OwinSample.Controllers { @@ -22,9 +23,20 @@ public ActionResult Login(string scheme) // GET /Account/Logout [HttpGet] - public void Logout() + public void Logout(string redirectUrl) { - Request.GetOwinContext().Authentication.SignOut(); + if (string.IsNullOrWhiteSpace(redirectUrl)) + { + redirectUrl = "/"; + } + var owinContext = Request.GetOwinContext(); + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + owinContext.Authentication.SignOut(properties, CookieAuthenticationDefaults.AuthenticationType); + var authScheme = owinContext.Authentication.User.FindFirst("auth_scheme")?.Value; + if (!string.IsNullOrWhiteSpace(authScheme)) + { + owinContext.Authentication.SignOut(properties, authScheme); + } } } } \ No newline at end of file diff --git a/samples/OwinSample/Startup.cs b/samples/OwinSample/Startup.cs index e75019cb..4c98df0a 100644 --- a/samples/OwinSample/Startup.cs +++ b/samples/OwinSample/Startup.cs @@ -2,10 +2,10 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using System.Web; using System.Web.Helpers; using System.Web.Mvc; using System.Web.Routing; +using GSS.Authentication.CAS; using GSS.Authentication.CAS.Owin; using GSS.Authentication.CAS.Security; using GSS.Authentication.CAS.Validation; @@ -86,33 +86,7 @@ public void Configuration(IAppBuilder app) LogoutPath = CookieAuthenticationDefaults.LogoutPath, // https://github.com/aspnet/AspNetKatana/wiki/System.Web-response-cookie-integration-issues CookieManager = new SystemWebCookieManager(), - SessionStore = singleLogout ? resolver.GetRequiredService() : null, - Provider = new CookieAuthenticationProvider - { - OnResponseSignOut = context => - { - var redirectContext = new CookieApplyRedirectContext - ( - context.OwinContext, - context.Options, - "/" - ); - if (configuration.GetValue("CAS:SingleSignOut", false)) - { - context.Options.CookieManager.DeleteCookie(context.OwinContext, context.Options.CookieName, - context.CookieOptions); - // Single Sign-Out - var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); - var serviceUrl = urlHelper.Action("Index", "Home", null, context.Request.Scheme); - var redirectUri = new UriBuilder(configuration["CAS:ServerUrlBase"]!); - redirectUri.Path += "/logout"; - redirectUri.Query = $"service={Uri.EscapeDataString(serviceUrl)}"; - redirectContext.RedirectUri = redirectUri.Uri.AbsoluteUri; - } - - context.Options.Provider.ApplyRedirect(redirectContext); - } - } + SessionStore = singleLogout ? resolver.GetRequiredService() : null }); app.UseCasAuthentication(options => @@ -143,6 +117,7 @@ public void Configuration(IAppBuilder app) if (assertion == null) return Task.CompletedTask; // Map claims from assertion + context.Identity.AddClaim(new Claim("auth_scheme", CasDefaults.AuthenticationType)); context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, assertion.PrincipalName)); if (assertion.Attributes.TryGetValue("display_name", out var displayName) && !string.IsNullOrWhiteSpace(displayName)) @@ -174,6 +149,7 @@ public void Configuration(IAppBuilder app) ClientSecret = configuration["OIDC:ClientSecret"], Authority = configuration["OIDC:Authority"], RequireHttpsMetadata = !env.Equals("Development", StringComparison.OrdinalIgnoreCase), + // required for single logout SaveTokens = configuration.GetValue("OIDC:SaveTokens", false), ResponseType = OpenIdConnectResponseType.Code, // https://github.com/aspnet/AspNetKatana/issues/348 @@ -184,31 +160,46 @@ public void Configuration(IAppBuilder app) CookieManager = new SystemWebCookieManager(), Notifications = new OpenIdConnectAuthenticationNotifications { - RedirectToIdentityProvider = notification => + SecurityTokenValidated = notification => { - // generate the redirect_uri parameter automatically - if (string.IsNullOrWhiteSpace(notification.Options.RedirectUri)) - { - notification.ProtocolMessage.RedirectUri = - notification.Request.Scheme + Uri.SchemeDelimiter + - notification.Request.Host + notification.Request.PathBase + - notification.Options.CallbackPath; - } - + notification.AuthenticationTicket.Identity.AddClaim(new Claim("auth_scheme", + OpenIdConnectAuthenticationDefaults.AuthenticationType)); return Task.CompletedTask; }, - AuthorizationCodeReceived = notification => + RedirectToIdentityProvider = async notification => { - // generate the redirect_uri parameter automatically - if (string.IsNullOrWhiteSpace(notification.Options.RedirectUri)) + // fix redirect_uri + if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication && + string.IsNullOrWhiteSpace(notification.Options.RedirectUri)) { - notification.TokenEndpointRequest.RedirectUri = + notification.ProtocolMessage.RedirectUri = notification.Request.Scheme + Uri.SchemeDelimiter + notification.Request.Host + notification.Request.PathBase + notification.Options.CallbackPath; } - return Task.CompletedTask; + if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout) + { + // fix post_logout_redirect_uri + if (!Uri.IsWellFormedUriString(notification.ProtocolMessage.PostLogoutRedirectUri, + UriKind.Absolute)) + { + notification.ProtocolMessage.PostLogoutRedirectUri = notification.Request.Scheme + + Uri.SchemeDelimiter + + notification.Request.Host + notification.Request.PathBase + + notification.ProtocolMessage.PostLogoutRedirectUri; + } + + // fix id_token_hint + if (string.IsNullOrWhiteSpace(notification.ProtocolMessage.IdTokenHint)) + { + var result = + await notification.OwinContext.Authentication.AuthenticateAsync( + CookieAuthenticationDefaults.AuthenticationType); + var idToken = result.Properties.Dictionary[OpenIdConnectParameterNames.IdToken]; + notification.ProtocolMessage.IdTokenHint = idToken; + } + } } } }); @@ -218,14 +209,25 @@ public void Configuration(IAppBuilder app) private static Saml2AuthenticationOptions CreateSaml2Options(IConfiguration configuration) { - var spOptions = new SPOptions { EntityId = new EntityId(configuration["SAML2:SP:EntityId"]), AuthenticateRequestSigningBehavior = SigningBehavior.Never }; - spOptions.TokenValidationParametersTemplate.NameClaimType = ClaimTypes.NameIdentifier; + var spOptions = new SPOptions + { + EntityId = new EntityId(configuration["SAML2:SP:EntityId"]), + AuthenticateRequestSigningBehavior = SigningBehavior.Never, + TokenValidationParametersTemplate = { NameClaimType = ClaimTypes.NameIdentifier } + }; var options = new Saml2AuthenticationOptions(false) { SPOptions = spOptions }; var idp = new IdentityProvider(new EntityId(configuration["SAML2:IdP:EntityId"]), spOptions) { MetadataLocation = configuration["SAML2:IdP:MetadataLocation"] }; options.IdentityProviders.Add(idp); + options.Notifications.AcsCommandResultCreated = (result, _) => + { + if (result.Principal.Identity is ClaimsIdentity identity) + { + identity.AddClaim(new Claim("auth_scheme", "Saml2")); + } + }; options.Notifications.MetadataCreated = (metadata, _) => { var ssoDescriptor = metadata.RoleDescriptors.OfType().First();