Skip to content

Commit

Permalink
Implement OIDC session management
Browse files Browse the repository at this point in the history
  • Loading branch information
GREsau committed Jun 10, 2024
1 parent 32ecdba commit a1e4248
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 2 deletions.
50 changes: 49 additions & 1 deletion samples/Contruum/Contruum.Server/Handlers.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -55,4 +61,46 @@ public ValueTask HandleAsync(HandleUserinfoRequestContext context)
return default;
}
}

public class AttachCheckSessionIframeEndpoint : IOpenIddictServerHandler<HandleConfigurationRequestContext>
{
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<ApplyAuthorizationResponseContext>
{
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;
}
}
}
49 changes: 49 additions & 0 deletions samples/Contruum/Contruum.Server/Pages/Connect/CheckSession.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@page
@{
Layout = null;
}

<!DOCTYPE html>
<title>OIDC Check Session</title>
<script>
const sessionCookieName = '@Contruum.Server.SessionIdExtensions.CookieName';
async function checkSession(origin, message) {
try {
if (!origin || !message) {
return 'error';
}
const [clientId, sessionState] = message.split(' ');
if (!clientId || !sessionState) {
return 'error';
}
const [clientHash, salt] = sessionState.split('.');
if (!clientHash || !salt) {
return 'error';
}
const sessionId = document.cookie.split('; ').find(c => c.startsWith(sessionCookieName + '='))?.split('=')?.[1];
if (!sessionId) {
return 'changed';
}
const utf8Bytes = new TextEncoder().encode(clientId + origin + sessionId + salt);
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8Bytes);
const hashBytes = new Uint8Array(hashBuffer);
const hashBase64 = btoa(Array.from(hashBytes, byte => String.fromCharCode(byte)).join(''));
const hashBase64Url = hashBase64.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
return clientHash === hashBase64Url ? 'unchanged' : 'changed';
} catch (e) {
console.error('OIDC Check Session error', e)
return 'error';
}
}
window.addEventListener('message', async function (e) {
const result = await checkSession(e.origin, e.data);
e.source.postMessage(result, e.origin);
}, false);
</script>
33 changes: 33 additions & 0 deletions samples/Contruum/Contruum.Server/SessionIdExtensions.cs
Original file line number Diff line number Diff line change
@@ -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];
}
}
17 changes: 17 additions & 0 deletions samples/Contruum/Contruum.Server/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System.Threading.Tasks;
using Contruum.Server.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -101,6 +112,12 @@ public void ConfigureServices(IServiceCollection services)
// Register the event handler responsible for populating userinfo responses.
options.AddEventHandler<HandleUserinfoRequestContext>(options =>
options.UseSingletonHandler<Handlers.PopulateUserinfo>());
options.AddEventHandler<HandleConfigurationRequestContext>(options =>
options.UseSingletonHandler<Handlers.AttachCheckSessionIframeEndpoint>());
options.AddEventHandler<ApplyAuthorizationResponseContext>(options =>
options.UseSingletonHandler<Handlers.AttachSessionState>());
})

.AddValidation(options =>
Expand Down
3 changes: 2 additions & 1 deletion samples/Contruum/Contruum.Server/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
"Introspection": "connect/introspect",
"Token": "connect/token",
"Userinfo": "connect/userinfo",
"Logout": "connect/endsession"
"Logout": "connect/endsession",
"CheckSession": "connect/checksession"
},

"Scopes": [
Expand Down

0 comments on commit a1e4248

Please sign in to comment.