Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8577a1a
OIDC sessions: OidcLogin configuration in core, OidcSession entity, b…
KrzysztofPajak Oct 11, 2026
f8c34f3
OIDC sessions: refuse provider tokens of a subject issued before a re…
KrzysztofPajak Oct 11, 2026
dd877a4
OIDC sessions: server-side session record, end by provider session or…
KrzysztofPajak Oct 11, 2026
75267d2
OIDC sessions: idle and maximum lifetime, provider revalidation of ba…
KrzysztofPajak Oct 11, 2026
954d538
OIDC sessions: revalidate from the stored record, keep newer tokens, …
KrzysztofPajak Oct 11, 2026
5386742
OIDC sessions: the authentication cookie references the server-side s…
KrzysztofPajak Oct 12, 2026
b79ac22
OIDC sessions: cleanup task, indexes and 2.5 migration
KrzysztofPajak Oct 12, 2026
e5eb201
OIDC sessions: keep the session claim when the same customer is signe…
KrzysztofPajak Oct 12, 2026
a83a73e
OIDC sessions: sign out even when the session record cannot be ended
KrzysztofPajak Oct 12, 2026
dc352db
LiteDB: UpdateOneAsync with no matching document updates nothing
KrzysztofPajak Oct 12, 2026
ac2e6d2
Docs: list the OpenID Connect session cleanup task in the scheduled t…
KrzysztofPajak Oct 12, 2026
2551909
OIDC sessions: confirm every provider session at the identity provide…
KrzysztofPajak Oct 12, 2026
90e7919
OIDC sessions: an ended session does not keep its refresh token
KrzysztofPajak Oct 12, 2026
f6aa28d
OIDC sessions: refresh lease shared by every instance, abandoned sess…
KrzysztofPajak Oct 13, 2026
d0eeff9
OIDC sessions: a new provider sign-in of the same customer ends the s…
KrzysztofPajak Oct 13, 2026
f912be9
OpenID Connect login: start a server-side session with the refresh to…
KrzysztofPajak Oct 12, 2026
41dbec5
OpenID Connect login: an unreadable sid claim never fails the sign-in…
KrzysztofPajak Oct 12, 2026
ca01b2c
OpenID Connect login: confirm back-office sessions by exchanging the …
KrzysztofPajak Oct 12, 2026
3ca37a5
OpenID Connect login: validate back-channel logout tokens
KrzysztofPajak Oct 12, 2026
c86d7f0
OpenID Connect login: bound the logout token replay cache and name th…
KrzysztofPajak Oct 12, 2026
f4f10d4
OpenID Connect login: back-channel logout endpoint
KrzysztofPajak Oct 12, 2026
6b049de
OpenID Connect login: release the logout token when ending sessions f…
KrzysztofPajak Oct 12, 2026
bc1a56b
OpenID Connect login: the token endpoint client does not follow redir…
KrzysztofPajak Oct 12, 2026
b0e04bc
OpenID Connect login: refresh the provider metadata when a logout tok…
KrzysztofPajak Oct 12, 2026
8f428e2
OpenID Connect login: connecting an account and signing in again as t…
KrzysztofPajak Oct 12, 2026
ee0a746
OpenID Connect login: the incomplete setup message addresses customer…
KrzysztofPajak Oct 12, 2026
bf818d3
OpenID Connect login: the token refresher confirms every provider ses…
KrzysztofPajak Oct 12, 2026
8b0ff4f
OpenID Connect login: back-channel logout creates no store or work co…
KrzysztofPajak Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ai/skills/scheduled-task.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public class ScheduleTask : BaseEntity
| `GenerateSitemapXmlTask` | `"Generate sitemap XML file"` | 10080 min | false |
| `EndAuctionsTask` | `"End of the auctions"` | 60 min | false |
| `CancelOrderScheduledTask` | `"Cancel unpaid and pending orders"` | 1440 min | false |
| `DeleteOidcSessionsScheduleTask` | `"Delete OpenID Connect sessions"` | 1440 min | true |

## Expected Output
Produce one of these results:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Grand.Infrastructure.Configuration;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;

namespace Grand.Business.Authentication.Services;
Expand All @@ -26,20 +27,26 @@ public class CookieAuthenticationService : IGrandAuthenticationService
/// <param name="groupService">Group service</param>
/// <param name="httpContextAccessor">HTTP context accessor</param>
/// <param name="securityConfig">SecurityConfig</param>
/// <param name="oidcSessionService">Server-side sessions of provider sign-ins</param>
/// <param name="logger">Logger</param>
public CookieAuthenticationService(
CustomerSettings customerSettings,
ICustomerService customerService,
IGroupService groupService,
IHttpContextAccessor httpContextAccessor,
ICookieOptionsFactory cookieOptionsFactory,
SecurityConfig securityConfig)
SecurityConfig securityConfig,
IOidcSessionService oidcSessionService,
ILogger<CookieAuthenticationService> logger)
{
_customerSettings = customerSettings;
_customerService = customerService;
_groupService = groupService;
_httpContextAccessor = httpContextAccessor;
_cookieOptionsFactory = cookieOptionsFactory;
_securityConfig = securityConfig;
_oidcSessionService = oidcSessionService;
_logger = logger;
}

#endregion
Expand All @@ -63,6 +70,8 @@ public CookieAuthenticationService(
private readonly ICookieOptionsFactory _cookieOptionsFactory;

private readonly SecurityConfig _securityConfig;
private readonly IOidcSessionService _oidcSessionService;
private readonly ILogger<CookieAuthenticationService> _logger;

#endregion

Expand All @@ -77,6 +86,44 @@ public virtual async Task SignIn(Customer customer, bool isPersistent)
{
ArgumentNullException.ThrowIfNull(customer);

//a re-sign-in of the same customer (password, e-mail or username change) keeps the server-side
//session of a provider sign-in, so idle/max lifetime, revalidation and back-channel logout still apply
await SignIn(customer, isPersistent, await CurrentOidcSessionIdOf(customer));
}

private async Task<string> CurrentOidcSessionIdOf(Customer customer)
{
if (_httpContextAccessor.HttpContext == null || string.IsNullOrEmpty(customer.Id))
return null;

var authenticateResult = await _httpContextAccessor.HttpContext.AuthenticateAsync(
GrandCookieAuthenticationDefaults.AuthenticationScheme);
if (authenticateResult?.Succeeded != true)
return null;

var customerId = authenticateResult.Principal?.FindFirst(claim =>
claim.Type == CustomerIdClaimType &&
claim.Issuer.Equals(_securityConfig.CookieClaimsIssuer, StringComparison.InvariantCultureIgnoreCase))?.Value;

return customerId == customer.Id ? OidcSessionId(authenticateResult.Principal) : null;
}

/// <summary>
/// Sign in with a session started at the identity provider
/// </summary>
/// <param name="customer">Customer</param>
/// <param name="isPersistent">Whether the authentication session is persisted across multiple requests</param>
/// <param name="oidcSessionId">Id of the OidcSession; null or empty = an ordinary sign-in</param>
public virtual async Task SignIn(Customer customer, bool isPersistent, string oidcSessionId)
{
ArgumentNullException.ThrowIfNull(customer);

//the same customer signing in through the provider again: the cookie that referenced the previous session is
//replaced, so that session would never be used or ended again
if (!string.IsNullOrEmpty(oidcSessionId) &&
await CurrentOidcSessionIdOf(customer) is { Length: > 0 } previous && previous != oidcSessionId)
await EndOidcSession(previous);

//create claims for customer's username and email
var claims = new List<Claim>();

Expand Down Expand Up @@ -109,6 +156,11 @@ public virtual async Task SignIn(Customer customer, bool isPersistent)
_securityConfig.CookieClaimsIssuer));
}

//the server-side session of a provider sign-in: ending it ends this cookie
if (!string.IsNullOrEmpty(oidcSessionId))
claims.Add(new Claim(OidcSessionDefaults.SessionIdClaimType, oidcSessionId, ClaimValueTypes.String,
_securityConfig.CookieClaimsIssuer));

//create principal for the present scheme of authentication
var userIdentity = new ClaimsIdentity(claims, GrandCookieAuthenticationDefaults.AuthenticationScheme);
var userPrincipal = new ClaimsPrincipal(userIdentity);
Expand Down Expand Up @@ -137,6 +189,12 @@ public virtual async Task SignOut()
//and then sign out customer from the present scheme of authentication
if (_httpContextAccessor.HttpContext != null)
{
var authenticateResult = await _httpContextAccessor.HttpContext.AuthenticateAsync(
GrandCookieAuthenticationDefaults.AuthenticationScheme);
var oidcSessionId = authenticateResult?.Succeeded == true ? OidcSessionId(authenticateResult.Principal) : null;
if (!string.IsNullOrEmpty(oidcSessionId))
await EndOidcSession(oidcSessionId);

await _httpContextAccessor.HttpContext.SignOutAsync(GrandCookieAuthenticationDefaults
.AuthenticationScheme);
//sign out also from other schema
Expand Down Expand Up @@ -215,8 +273,32 @@ private async Task<bool> IsValidCustomer(Customer customer, ClaimsPrincipal prin
if (!customer.Active || customer.Deleted || !await _groupService.IsRegistered(customer))
return false;

//a provider sign-in: the server-side session decides (ended, idle, refused by the provider)
var oidcSessionId = OidcSessionId(principal);
if (!string.IsNullOrEmpty(oidcSessionId) && !await _oidcSessionService.Validate(oidcSessionId, customer))
return false;

return true;
}

private async Task EndOidcSession(string sessionId)
{
try
{
await _oidcSessionService.End(sessionId, OidcSessionEndReason.SignOut);
}
catch (Exception ex)
{
//the cookie that referenced it is replaced anyway; the record goes with the cleanup task
_logger.LogWarning(ex, "Could not end the OpenID Connect session {SessionId}", sessionId);
}
}

private string OidcSessionId(ClaimsPrincipal principal) =>
principal?.FindFirst(claim =>
claim.Type == OidcSessionDefaults.SessionIdClaimType &&
claim.Issuer.Equals(_securityConfig.CookieClaimsIssuer, StringComparison.InvariantCultureIgnoreCase))?.Value;

/// <summary>
/// Get customer cookie
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Grand.Business.Core.Events.Customers;
using Grand.Business.Core.Extensions;
using Grand.Business.Core.Interfaces.Authentication;
using Grand.Business.Core.Interfaces.Common.Configuration;
using Grand.Business.Core.Interfaces.Common.Directory;
Expand Down Expand Up @@ -451,7 +452,6 @@ private async Task<bool> HasTwoFactor(Customer customer, string storeId) =>
private async Task<bool> RequiresPassword(Customer customer, string storeId) =>
await IsBackOfficeAccount(customer) || await HasTwoFactor(customer, storeId);

private async Task<bool> IsBackOfficeAccount(Customer customer) =>
!string.IsNullOrEmpty(customer.VendorId) || !string.IsNullOrEmpty(customer.StaffStoreId) ||
await _permissionService.Authorize(StandardPermission.ManageAccessAdminPanel, customer);
private Task<bool> IsBackOfficeAccount(Customer customer) =>
_permissionService.IsBackOfficeAccount(customer);
}
Loading