From 32ecdba5ab00513e2c6cba0947a0fcdff165e5be Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Mon, 10 Jun 2024 15:57:57 +0100 Subject: [PATCH 1/4] Implement RP-initiated logout and enable hybrid flow --- .../Pages/Connect/EndSession.cshtml | 19 ++++++++++++ .../Pages/Connect/EndSession.cshtml.cs | 30 +++++++++++++++++++ samples/Contruum/Contruum.Server/Startup.cs | 9 ++++-- .../Contruum/Contruum.Server/appsettings.json | 7 ++++- 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 samples/Contruum/Contruum.Server/Pages/Connect/EndSession.cshtml create mode 100644 samples/Contruum/Contruum.Server/Pages/Connect/EndSession.cshtml.cs diff --git a/samples/Contruum/Contruum.Server/Pages/Connect/EndSession.cshtml b/samples/Contruum/Contruum.Server/Pages/Connect/EndSession.cshtml new file mode 100644 index 000000000..817884ca1 --- /dev/null +++ b/samples/Contruum/Contruum.Server/Pages/Connect/EndSession.cshtml @@ -0,0 +1,19 @@ +@page +@model Contruum.Server.Pages.Connect.EndSessionModel +@using Microsoft.Extensions.Primitives + +
+

Log out

+

Are you sure you want to sign out?

+ +
+ @* Flow the request parameters so they can be received by the LogoutPost action: *@ + @foreach (var parameter in Request.HasFormContentType ? + (IEnumerable>)Request.Form : Request.Query) + { + + } + + + +
diff --git a/samples/Contruum/Contruum.Server/Pages/Connect/EndSession.cshtml.cs b/samples/Contruum/Contruum.Server/Pages/Connect/EndSession.cshtml.cs new file mode 100644 index 000000000..f95b0da1f --- /dev/null +++ b/samples/Contruum/Contruum.Server/Pages/Connect/EndSession.cshtml.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using OpenIddict.Server.AspNetCore; + +namespace Contruum.Server.Pages.Connect; + +public class EndSessionModel : PageModel +{ + public IActionResult OnGet() + { + return Page(); + } + + public async Task OnPostAsync() + { + await HttpContext.SignOutAsync(); + + // Returning a SignOutResult will ask OpenIddict to redirect the user agent + // to the post_logout_redirect_uri specified by the client application or to + // the RedirectUri specified in the authentication properties if none was set. + return SignOut( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties + { + RedirectUri = "/" + }); + } +} diff --git a/samples/Contruum/Contruum.Server/Startup.cs b/samples/Contruum/Contruum.Server/Startup.cs index c326e4f11..5a23830c5 100644 --- a/samples/Contruum/Contruum.Server/Startup.cs +++ b/samples/Contruum/Contruum.Server/Startup.cs @@ -69,11 +69,13 @@ public void ConfigureServices(IServiceCollection services) options.SetAuthorizationEndpointUris(Configuration["OpenIddict:Endpoints:Authorization"]!) .SetTokenEndpointUris(Configuration["OpenIddict:Endpoints:Token"]!) .SetIntrospectionEndpointUris(Configuration["OpenIddict:Endpoints:Introspection"]!) - .SetUserinfoEndpointUris(Configuration["OpenIddict:Endpoints:Userinfo"]!); + .SetUserinfoEndpointUris(Configuration["OpenIddict:Endpoints:Userinfo"]!) + .SetLogoutEndpointUris(Configuration["OpenIddict:Endpoints:Logout"]!); - // Enable the authorization code, implicit and the refresh token flows. + // Enable the authorization code, implicit, hybrid and the refresh token flows. options.AllowAuthorizationCodeFlow() .AllowImplicitFlow() + .AllowHybridFlow() .AllowRefreshTokenFlow(); // Expose all the supported claims in the discovery document. @@ -93,7 +95,8 @@ public void ConfigureServices(IServiceCollection services) // so that token requests are automatically handled by OpenIddict. options.UseAspNetCore() .EnableAuthorizationEndpointPassthrough() - .EnableAuthorizationRequestCaching(); + .EnableAuthorizationRequestCaching() + .EnableLogoutEndpointPassthrough(); // Register the event handler responsible for populating userinfo responses. options.AddEventHandler(options => diff --git a/samples/Contruum/Contruum.Server/appsettings.json b/samples/Contruum/Contruum.Server/appsettings.json index a1c472300..4c3a6e4d8 100644 --- a/samples/Contruum/Contruum.Server/appsettings.json +++ b/samples/Contruum/Contruum.Server/appsettings.json @@ -39,10 +39,12 @@ "ClientId": "oidc_certification_app_1", "ClientSecret": "secret_secret_secret", "RedirectUris": [ "https://www.certification.openid.net/test/a/d6e0d2a6-003e-4721-8b67-a24380468aa8/callback" ], + "PostLogoutRedirectUris": [ "https://www.certification.openid.net/test/a/d6e0d2a6-003e-4721-8b67-a24380468aa8/post_logout_redirect" ], "Type": "confidential", "Permissions": [ "ept:authorization", "ept:token", + "ept:logout", "gt:authorization_code", "gt:implicit", "gt:refresh_token", @@ -64,10 +66,12 @@ "ClientId": "oidc_certification_app_2", "ClientSecret": "secret_secret_secret", "RedirectUris": [ "https://www.certification.openid.net/test/a/d6e0d2a6-003e-4721-8b67-a24380468aa8/callback" ], + "PostLogoutRedirectUris": [ "https://www.certification.openid.net/test/a/d6e0d2a6-003e-4721-8b67-a24380468aa8/post_logout_redirect" ], "Type": "confidential", "Permissions": [ "ept:authorization", "ept:token", + "ept:logout", "gt:authorization_code", "gt:implicit", "gt:refresh_token", @@ -90,7 +94,8 @@ "Authorization": "connect/authorize", "Introspection": "connect/introspect", "Token": "connect/token", - "Userinfo": "connect/userinfo" + "Userinfo": "connect/userinfo", + "Logout": "connect/endsession" }, "Scopes": [ From a1e4248e64b495dc881ef84cef38f6a68bc6c772 Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Mon, 10 Jun 2024 16:02:26 +0100 Subject: [PATCH 2/4] Implement OIDC session management --- samples/Contruum/Contruum.Server/Handlers.cs | 50 ++++++++++++++++++- .../Pages/Connect/CheckSession.cshtml | 49 ++++++++++++++++++ .../Contruum.Server/SessionIdExtensions.cs | 33 ++++++++++++ samples/Contruum/Contruum.Server/Startup.cs | 17 +++++++ .../Contruum/Contruum.Server/appsettings.json | 3 +- 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 samples/Contruum/Contruum.Server/Pages/Connect/CheckSession.cshtml create mode 100644 samples/Contruum/Contruum.Server/SessionIdExtensions.cs diff --git a/samples/Contruum/Contruum.Server/Handlers.cs b/samples/Contruum/Contruum.Server/Handlers.cs index 9122e1ec4..ba95ce9d3 100644 --- a/samples/Contruum/Contruum.Server/Handlers.cs +++ b/samples/Contruum/Contruum.Server/Handlers.cs @@ -1,6 +1,12 @@ -using System.Globalization; +using System; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Configuration; using OpenIddict.Abstractions; using OpenIddict.Server; using static OpenIddict.Abstractions.OpenIddictConstants; @@ -55,4 +61,46 @@ public ValueTask HandleAsync(HandleUserinfoRequestContext context) return default; } } + + public class AttachCheckSessionIframeEndpoint : IOpenIddictServerHandler + { + private readonly IConfiguration _configuration; + + public AttachCheckSessionIframeEndpoint(IConfiguration configuration) + { + _configuration = configuration; + } + + public ValueTask HandleAsync(HandleConfigurationRequestContext context) + { + var baseUri = context.BaseUri ?? throw new InvalidOperationException("Missing BaseUri"); + var relativePath = _configuration["OpenIddict:Endpoints:CheckSession"]!; + + context.Metadata["check_session_iframe"] = baseUri.ToString().TrimEnd('/') + "/" + relativePath.TrimStart('/'); + + return default; + } + } + + public class AttachSessionState : IOpenIddictServerHandler + { + public ValueTask HandleAsync(ApplyAuthorizationResponseContext context) + { + if (context.Request?.ClientId is string clientId + && Uri.TryCreate(context.Request.RedirectUri, UriKind.Absolute, out var redirectUri) + && context.Transaction.GetHttpRequest()?.GetSessionId() is string sessionId) + { + var origin = redirectUri.GetLeftPart(UriPartial.Authority); + var salt = RandomNumberGenerator.GetHexString(8); + + var utf8Bytes = Encoding.UTF8.GetBytes(clientId + origin + sessionId + salt); + var hashBytes = SHA256.HashData(utf8Bytes); + var hashBase64Url = Base64UrlTextEncoder.Encode(hashBytes); + + context.Response.SetParameter("session_state", hashBase64Url + "." + salt); + } + + return default; + } + } } diff --git a/samples/Contruum/Contruum.Server/Pages/Connect/CheckSession.cshtml b/samples/Contruum/Contruum.Server/Pages/Connect/CheckSession.cshtml new file mode 100644 index 000000000..421287020 --- /dev/null +++ b/samples/Contruum/Contruum.Server/Pages/Connect/CheckSession.cshtml @@ -0,0 +1,49 @@ +@page +@{ + Layout = null; +} + + +OIDC Check Session + diff --git a/samples/Contruum/Contruum.Server/SessionIdExtensions.cs b/samples/Contruum/Contruum.Server/SessionIdExtensions.cs new file mode 100644 index 000000000..10b27bd82 --- /dev/null +++ b/samples/Contruum/Contruum.Server/SessionIdExtensions.cs @@ -0,0 +1,33 @@ +using System.Security.Cryptography; +using Microsoft.AspNetCore.Http; + +namespace Contruum.Server; + +public static class SessionIdExtensions +{ + public const string CookieName = "oidc_session"; + + public static void IssueSessionCookie(this HttpResponse response) + { + var sessionId = RandomNumberGenerator.GetHexString(32); + response.Cookies.Append( + CookieName, + sessionId, + new() + { + SameSite = SameSiteMode.None, + Secure = true, + HttpOnly = false, + }); + } + + public static void DeleteSessionCookie(this HttpResponse response) + { + response.Cookies.Delete(CookieName); + } + + public static string? GetSessionId(this HttpRequest request) + { + return request.Cookies[CookieName]; + } +} diff --git a/samples/Contruum/Contruum.Server/Startup.cs b/samples/Contruum/Contruum.Server/Startup.cs index 5a23830c5..23b999497 100644 --- a/samples/Contruum/Contruum.Server/Startup.cs +++ b/samples/Contruum/Contruum.Server/Startup.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; using Contruum.Server.Models; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; @@ -37,6 +38,16 @@ public void ConfigureServices(IServiceCollection services) options.AccessDeniedPath = "/connect/signin"; options.LoginPath = "/connect/signin"; options.LogoutPath = "/connect/signout"; + options.Events.OnSignedIn = (context) => + { + context.Response.IssueSessionCookie(); + return Task.CompletedTask; + }; + options.Events.OnSigningOut = (context) => + { + context.Response.DeleteSessionCookie(); + return Task.CompletedTask; + }; }); // OpenIddict offers native integration with Quartz.NET to perform scheduled tasks @@ -101,6 +112,12 @@ public void ConfigureServices(IServiceCollection services) // Register the event handler responsible for populating userinfo responses. options.AddEventHandler(options => options.UseSingletonHandler()); + + options.AddEventHandler(options => + options.UseSingletonHandler()); + + options.AddEventHandler(options => + options.UseSingletonHandler()); }) .AddValidation(options => diff --git a/samples/Contruum/Contruum.Server/appsettings.json b/samples/Contruum/Contruum.Server/appsettings.json index 4c3a6e4d8..db3039274 100644 --- a/samples/Contruum/Contruum.Server/appsettings.json +++ b/samples/Contruum/Contruum.Server/appsettings.json @@ -95,7 +95,8 @@ "Introspection": "connect/introspect", "Token": "connect/token", "Userinfo": "connect/userinfo", - "Logout": "connect/endsession" + "Logout": "connect/endsession", + "CheckSession": "connect/checksession" }, "Scopes": [ From 206a97adb294107f9fd18005a44aa714733b36d2 Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Mon, 10 Jun 2024 18:04:03 +0100 Subject: [PATCH 3/4] Revert "Implement OIDC session management" This reverts commit a1e4248e64b495dc881ef84cef38f6a68bc6c772. --- samples/Contruum/Contruum.Server/Handlers.cs | 50 +------------------ .../Pages/Connect/CheckSession.cshtml | 49 ------------------ .../Contruum.Server/SessionIdExtensions.cs | 33 ------------ samples/Contruum/Contruum.Server/Startup.cs | 17 ------- .../Contruum/Contruum.Server/appsettings.json | 3 +- 5 files changed, 2 insertions(+), 150 deletions(-) delete mode 100644 samples/Contruum/Contruum.Server/Pages/Connect/CheckSession.cshtml delete mode 100644 samples/Contruum/Contruum.Server/SessionIdExtensions.cs diff --git a/samples/Contruum/Contruum.Server/Handlers.cs b/samples/Contruum/Contruum.Server/Handlers.cs index ba95ce9d3..9122e1ec4 100644 --- a/samples/Contruum/Contruum.Server/Handlers.cs +++ b/samples/Contruum/Contruum.Server/Handlers.cs @@ -1,12 +1,6 @@ -using System; -using System.Globalization; -using System.Security.Cryptography; -using System.Text; +using System.Globalization; using System.Text.Json; using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Configuration; using OpenIddict.Abstractions; using OpenIddict.Server; using static OpenIddict.Abstractions.OpenIddictConstants; @@ -61,46 +55,4 @@ public ValueTask HandleAsync(HandleUserinfoRequestContext context) return default; } } - - public class AttachCheckSessionIframeEndpoint : IOpenIddictServerHandler - { - private readonly IConfiguration _configuration; - - public AttachCheckSessionIframeEndpoint(IConfiguration configuration) - { - _configuration = configuration; - } - - public ValueTask HandleAsync(HandleConfigurationRequestContext context) - { - var baseUri = context.BaseUri ?? throw new InvalidOperationException("Missing BaseUri"); - var relativePath = _configuration["OpenIddict:Endpoints:CheckSession"]!; - - context.Metadata["check_session_iframe"] = baseUri.ToString().TrimEnd('/') + "/" + relativePath.TrimStart('/'); - - return default; - } - } - - public class AttachSessionState : IOpenIddictServerHandler - { - public ValueTask HandleAsync(ApplyAuthorizationResponseContext context) - { - if (context.Request?.ClientId is string clientId - && Uri.TryCreate(context.Request.RedirectUri, UriKind.Absolute, out var redirectUri) - && context.Transaction.GetHttpRequest()?.GetSessionId() is string sessionId) - { - var origin = redirectUri.GetLeftPart(UriPartial.Authority); - var salt = RandomNumberGenerator.GetHexString(8); - - var utf8Bytes = Encoding.UTF8.GetBytes(clientId + origin + sessionId + salt); - var hashBytes = SHA256.HashData(utf8Bytes); - var hashBase64Url = Base64UrlTextEncoder.Encode(hashBytes); - - context.Response.SetParameter("session_state", hashBase64Url + "." + salt); - } - - return default; - } - } } diff --git a/samples/Contruum/Contruum.Server/Pages/Connect/CheckSession.cshtml b/samples/Contruum/Contruum.Server/Pages/Connect/CheckSession.cshtml deleted file mode 100644 index 421287020..000000000 --- a/samples/Contruum/Contruum.Server/Pages/Connect/CheckSession.cshtml +++ /dev/null @@ -1,49 +0,0 @@ -@page -@{ - Layout = null; -} - - -OIDC Check Session - diff --git a/samples/Contruum/Contruum.Server/SessionIdExtensions.cs b/samples/Contruum/Contruum.Server/SessionIdExtensions.cs deleted file mode 100644 index 10b27bd82..000000000 --- a/samples/Contruum/Contruum.Server/SessionIdExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Security.Cryptography; -using Microsoft.AspNetCore.Http; - -namespace Contruum.Server; - -public static class SessionIdExtensions -{ - public const string CookieName = "oidc_session"; - - public static void IssueSessionCookie(this HttpResponse response) - { - var sessionId = RandomNumberGenerator.GetHexString(32); - response.Cookies.Append( - CookieName, - sessionId, - new() - { - SameSite = SameSiteMode.None, - Secure = true, - HttpOnly = false, - }); - } - - public static void DeleteSessionCookie(this HttpResponse response) - { - response.Cookies.Delete(CookieName); - } - - public static string? GetSessionId(this HttpRequest request) - { - return request.Cookies[CookieName]; - } -} diff --git a/samples/Contruum/Contruum.Server/Startup.cs b/samples/Contruum/Contruum.Server/Startup.cs index 23b999497..5a23830c5 100644 --- a/samples/Contruum/Contruum.Server/Startup.cs +++ b/samples/Contruum/Contruum.Server/Startup.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Threading.Tasks; using Contruum.Server.Models; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; @@ -38,16 +37,6 @@ public void ConfigureServices(IServiceCollection services) options.AccessDeniedPath = "/connect/signin"; options.LoginPath = "/connect/signin"; options.LogoutPath = "/connect/signout"; - options.Events.OnSignedIn = (context) => - { - context.Response.IssueSessionCookie(); - return Task.CompletedTask; - }; - options.Events.OnSigningOut = (context) => - { - context.Response.DeleteSessionCookie(); - return Task.CompletedTask; - }; }); // OpenIddict offers native integration with Quartz.NET to perform scheduled tasks @@ -112,12 +101,6 @@ public void ConfigureServices(IServiceCollection services) // Register the event handler responsible for populating userinfo responses. options.AddEventHandler(options => options.UseSingletonHandler()); - - options.AddEventHandler(options => - options.UseSingletonHandler()); - - options.AddEventHandler(options => - options.UseSingletonHandler()); }) .AddValidation(options => diff --git a/samples/Contruum/Contruum.Server/appsettings.json b/samples/Contruum/Contruum.Server/appsettings.json index db3039274..4c3a6e4d8 100644 --- a/samples/Contruum/Contruum.Server/appsettings.json +++ b/samples/Contruum/Contruum.Server/appsettings.json @@ -95,8 +95,7 @@ "Introspection": "connect/introspect", "Token": "connect/token", "Userinfo": "connect/userinfo", - "Logout": "connect/endsession", - "CheckSession": "connect/checksession" + "Logout": "connect/endsession" }, "Scopes": [ From 903bfe1e15132e74f019e1c80822cc8e98b194ad Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Fri, 28 Jun 2024 15:10:56 +0100 Subject: [PATCH 4/4] Add link to OIDC Session Management sample --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5b80448b1..2fda55702 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ This repository contains samples demonstrating **how to use [OpenIddict](https:/ - **[Angular and Blazor samples](https://github.com/damienbod/AspNetCoreOpeniddict)** by [Damien Bowden](https://github.com/damienbod) - **[Experimental MAUI (WinUI-only) sample](https://github.com/kevinchalet/openiddict-core/tree/maui_winui_sample/sandbox/OpenIddict.Sandbox.Maui.Client)** by [Kévin Chalet](https://github.com/kevinchalet) + + - **[OIDC Session Management sample](https://github.com/GREsau/openiddict-session-management-sample)** by [Graham Esau](https://github.com/GREsau) ## Certification