Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 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
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);
}
Comment on lines +290 to +294
}

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
Loading