From e5b41752cba1462b454665020e8886bca6f299bb Mon Sep 17 00:00:00 2001 From: Branko Zachemsky Date: Thu, 8 Jan 2026 06:32:14 +0100 Subject: [PATCH 1/3] Added login delegate in IdentityComponentsEndpointRouteBuilderExtensions --- ...omponentsEndpointRouteBuilderExtensions.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs b/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs index 2ba34f456..2fb019539 100644 --- a/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs +++ b/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -20,14 +20,17 @@ namespace Microsoft.AspNetCore.Routing { public static class IdentityComponentsEndpointRouteBuilderExtensions { + public delegate Task LoginHandler(string username, string group, IList roles); + // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project. - public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints) + public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints, LoginHandler? loginHandler = null) { ArgumentNullException.ThrowIfNull(endpoints); endpoints.MapPost("/Login", async ( HttpContext context, - [FromServices] SignInManager signInManager) => + [FromServices] SignInManager signInManager, + [FromServices] UserManager userManager) => { try { @@ -42,7 +45,17 @@ public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpoin var result = await signInManager.PasswordSignInAsync(username, password, false, lockoutOnFailure: false); if (result.Succeeded) + { + // Get user details for the login handler + var user = await userManager.FindByNameAsync(username); + if (user != null && loginHandler != null) + { + var roles = await userManager.GetRolesAsync(user); + await loginHandler(username, user.Group, roles); + } + return TypedResults.LocalRedirect(string.IsNullOrEmpty(returnUrl) ? "/" : !returnUrl.StartsWith("/") ? "/" + returnUrl : returnUrl.StartsWith("//") ? "/" + returnUrl.TrimStart('/') : returnUrl); + } else // Redirect back to login with error return TypedResults.LocalRedirect($"/Security/Login?error=invalid&returnUrl={Uri.EscapeDataString(returnUrl ?? "/")}"); } @@ -110,6 +123,13 @@ public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpoin // Sign in the user await signInManager.SignInAsync(user, isPersistent: false); + // Invoke the login handler + if (loginHandler != null) + { + var roles = await userManager.GetRolesAsync(user); + await loginHandler(user.UserName!, user.Group, roles); + } + if (!string.IsNullOrEmpty(returnUrl)) { string[] split = returnUrl.Split('?'); From 040cff2a1e954f37c43290cbd35bccb01d0b6d53 Mon Sep 17 00:00:00 2001 From: Branko Zachemsky Date: Thu, 8 Jan 2026 06:45:42 +0100 Subject: [PATCH 2/3] add client identification in logindelegate --- ...omponentsEndpointRouteBuilderExtensions.cs | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs b/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs index 2fb019539..d6efbf4f6 100644 --- a/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs +++ b/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -18,9 +18,18 @@ namespace Microsoft.AspNetCore.Routing { + public class ClientIdentification + { + public string IpAddress { get; set; } = string.Empty; + public string UserAgent { get; set; } = string.Empty; + public string Host { get; set; } = string.Empty; + public string Protocol { get; set; } = string.Empty; + public Dictionary AdditionalHeaders { get; set; } = new(); + } + public static class IdentityComponentsEndpointRouteBuilderExtensions { - public delegate Task LoginHandler(string username, string group, IList roles); + public delegate Task LoginHandler(string username, string group, IList roles, ClientIdentification clientInfo); // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project. public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints, LoginHandler? loginHandler = null) @@ -51,7 +60,8 @@ public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpoin if (user != null && loginHandler != null) { var roles = await userManager.GetRolesAsync(user); - await loginHandler(username, user.Group, roles); + var clientInfo = GetClientIdentification(context); + await loginHandler(username, user.Group, roles, clientInfo); } return TypedResults.LocalRedirect(string.IsNullOrEmpty(returnUrl) ? "/" : !returnUrl.StartsWith("/") ? "/" + returnUrl : returnUrl.StartsWith("//") ? "/" + returnUrl.TrimStart('/') : returnUrl); @@ -127,7 +137,8 @@ public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpoin if (loginHandler != null) { var roles = await userManager.GetRolesAsync(user); - await loginHandler(user.UserName!, user.Group, roles); + var clientInfo = GetClientIdentification(context); + await loginHandler(user.UserName!, user.Group, roles, clientInfo); } if (!string.IsNullOrEmpty(returnUrl)) @@ -152,5 +163,50 @@ public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpoin return endpoints; } + + private static ClientIdentification GetClientIdentification(HttpContext context) + { + var clientInfo = new ClientIdentification + { + IpAddress = GetClientIpAddress(context), + UserAgent = context.Request.Headers["User-Agent"].FirstOrDefault() ?? "Unknown", + Host = context.Request.Host.ToString(), + Protocol = context.Request.Protocol + }; + + // Add additional headers that might be useful for client identification + var headersToCapture = new[] { "Referer", "Accept-Language", "X-Requested-With", "Origin" }; + foreach (var header in headersToCapture) + { + var value = context.Request.Headers[header].FirstOrDefault(); + if (!string.IsNullOrEmpty(value)) + { + clientInfo.AdditionalHeaders[header] = value; + } + } + + return clientInfo; + } + + private static string GetClientIpAddress(HttpContext context) + { + // Try to get IP from X-Forwarded-For header (for reverse proxy scenarios) + var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + if (!string.IsNullOrEmpty(forwardedFor)) + { + // X-Forwarded-For can contain multiple IPs, take the first one + var ips = forwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (ips.Length > 0) + return ips[0]; + } + + // Try X-Real-IP header + var realIp = context.Request.Headers["X-Real-IP"].FirstOrDefault(); + if (!string.IsNullOrEmpty(realIp)) + return realIp; + + // Fall back to RemoteIpAddress + return context.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; + } } } From f4139eafda817f3743b67a1474058e6c1ebdd7da Mon Sep 17 00:00:00 2001 From: Branko Zachemsky Date: Thu, 8 Jan 2026 06:54:39 +0100 Subject: [PATCH 3/3] add logout handler in IdentityComponentsEndpointRouteBuilderExtensions --- ...omponentsEndpointRouteBuilderExtensions.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs b/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs index d6efbf4f6..c71adadb2 100644 --- a/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs +++ b/src/Security/src/AXOpen.Security/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -30,9 +30,10 @@ public class ClientIdentification public static class IdentityComponentsEndpointRouteBuilderExtensions { public delegate Task LoginHandler(string username, string group, IList roles, ClientIdentification clientInfo); + public delegate Task LogoutHandler(string username); // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project. - public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints, LoginHandler? loginHandler = null) + public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints, LoginHandler? loginHandler = null, LogoutHandler? logoutHandler = null) { ArgumentNullException.ThrowIfNull(endpoints); @@ -83,6 +84,14 @@ public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpoin { var formCollection = await context.Request.ReadFormAsync(); var returnUrl = formCollection["ReturnUrl"].ToString(); + + // Get username before signing out + string? username = context.User.Identity?.Name; + + if (!string.IsNullOrEmpty(username) && logoutHandler != null) + { + await logoutHandler(username); + } await signInManager.SignOutAsync(); return TypedResults.LocalRedirect(string.IsNullOrEmpty(returnUrl) ? "/" : !returnUrl.StartsWith("/") ? "/" + returnUrl : returnUrl.StartsWith("//") ? "/" + returnUrl.TrimStart('/') : returnUrl); @@ -120,12 +129,22 @@ public static IEndpointRouteBuilder MapAdditionalIdentityEndpoints(this IEndpoin if (TokenHasher.VerifyToken(externalAuthId, currentUser.ExternalAuthId)) { // Same user - log out + if (logoutHandler != null) + { + var clientInfo = GetClientIdentification(context); + await logoutHandler(currentUser.UserName!); + } await signInManager.SignOutAsync(); return TypedResults.LocalRedirect(string.IsNullOrEmpty(returnUrl) ? "/" : !returnUrl.StartsWith("/") ? "/" + returnUrl : returnUrl.StartsWith("//") ? "/" + returnUrl.TrimStart('/') : returnUrl); } else { // Different user - log out current and log in new + if (logoutHandler != null) + { + var clientInfo = GetClientIdentification(context); + await logoutHandler(currentUser.UserName!); + } await signInManager.SignOutAsync(); } }