From 42ec04605dde0aa6603b5fd3e45b2f8020d7a13f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:07:39 +0000 Subject: [PATCH 1/6] Initial plan From 881728c110053712199a724395a65a9a67485029 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:11:05 +0000 Subject: [PATCH 2/6] Fix critical cookie authentication security flaws Co-authored-by: neptrio <33280179+neptrio@users.noreply.github.com> --- .../AppwriteSignInCallbackHelper.cs | 25 ++++++++-- .../AppwriteCookieAuthenticationEvents.cs | 49 +++++++++++++------ .../AppwriteCookieAuthenticationOptions.cs | 5 +- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs b/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs index d24bf24..add3da7 100644 --- a/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs +++ b/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs @@ -1,7 +1,9 @@ using Appwrite.Models; using AppwriteHelper.Authentication.AppwriteServer; +using AppwriteHelper.Authentication.Cookies; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using System.Security.Claims; using System.Text.Json; @@ -12,6 +14,17 @@ public class AppwriteSignInCallbackHelper([FromKeyedServices(Constants.APPWRITE_ private readonly IAppwriteClientFactory _appwriteClientFactory = appwriteClientFactory; public async Task CreateSignInAsync(string userId, string secret, TimeSpan? cookieLifetime = null, bool isPersistent = true, string? authenticationType = null) + { + return await CreateSignInAsync(userId, secret, cookieLifetime, isPersistent, authenticationType, null); + } + + public async Task CreateSignInAsync( + string userId, + string secret, + TimeSpan? cookieLifetime = null, + bool isPersistent = true, + string? authenticationType = null, + IOptionsMonitor? cookieOptions = null) { ArgumentException.ThrowIfNullOrEmpty(userId); ArgumentException.ThrowIfNullOrEmpty(secret); @@ -20,8 +33,8 @@ public async Task CreateSignInAsync(string userId, string var serverAccount = new Appwrite.Services.Account(serverClient); var session = await serverAccount.CreateSession(userId, secret); - if (session == null) - throw new InvalidOperationException("Invalid session"); + if (session == null || string.IsNullOrEmpty(session.Secret)) + throw new InvalidOperationException("Invalid session or session secret"); var userClient = _appwriteClientFactory.CreateUserClientFromSession(session.Secret); var userAccount = new Appwrite.Services.Account(userClient); @@ -47,7 +60,13 @@ public async Task CreateSignInAsync(string userId, string var identity = new ClaimsIdentity(claims, authenticationType ?? AppwriteAuthenticationDefaults.CookieAuthenticationScheme); var principal = new ClaimsPrincipal(identity); - var expires = cookieLifetime ?? TimeSpan.FromMinutes(15); + // Enforce maximum cookie lifetime for security + var defaultExpireTime = TimeSpan.FromMinutes(15); + var maxExpireTime = cookieOptions?.CurrentValue?.MaximumExpireTimeSpan ?? TimeSpan.FromHours(24); + + var requestedExpireTime = cookieLifetime ?? defaultExpireTime; + var expires = requestedExpireTime > maxExpireTime ? maxExpireTime : requestedExpireTime; + var authenticationProperties = new AuthenticationProperties { IsPersistent = isPersistent, diff --git a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs index e7114ff..17b2e51 100644 --- a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs +++ b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.IdentityModel.Tokens.Jwt; using System.Text.Json; @@ -13,9 +14,15 @@ namespace AppwriteHelper.Authentication.Cookies public sealed class AppwriteCookieAuthenticationEvents : CookieAuthenticationEvents { private readonly IOptionsMonitor _options; + private readonly ILogger _logger; - public AppwriteCookieAuthenticationEvents(IOptionsMonitor options) - => _options = options; + public AppwriteCookieAuthenticationEvents( + IOptionsMonitor options, + ILogger logger) + { + _options = options; + _logger = logger; + } public override Task RedirectToLogin(RedirectContext context) { @@ -55,8 +62,10 @@ public override async Task ValidatePrincipal(CookieValidatePrincipalContext cont // Optional: additional online revoked session check if (options.CheckForRevokedSessions) { - if (!await IsSessionRevokedAsync(options, session.Secret)) + var isSessionValid = await IsSessionRevokedAsync(options, session.Secret); + if (!isSessionValid) { + _logger.LogWarning("Session has been revoked"); await RejectAsync(context); return; } @@ -91,39 +100,51 @@ private static Account CreateAccountClient(AppwriteCookieAuthenticationOptions o return new Account(client); } - private static async Task IsSessionRevokedAsync(AppwriteCookieAuthenticationOptions options, string sessionSecret) + private async Task IsSessionRevokedAsync(AppwriteCookieAuthenticationOptions options, string sessionSecret) { try { var account = CreateAccountClient(options, sessionSecret); var user = await account.Get(); - return user == null; + // Return false if session is revoked (user is null), true if session is still valid + return user != null; } - catch + catch (Exception ex) { - return true; + _logger.LogWarning(ex, "Failed to validate session with Appwrite. Session will be rejected for security."); + // Fail secure - treat exception as revoked session + return false; } } - private static async Task TryUpdateSessionAsync(Account account, string sessionId) + private async Task TryUpdateSessionAsync(Account account, string sessionId) { try { await account.UpdateSession(sessionId); return true; } - catch + catch (Exception ex) { + _logger.LogWarning(ex, "Failed to update session {SessionId} with Appwrite", sessionId); return false; } } - private static bool TryGetSession(CookieValidatePrincipalContext context, out Session session) + private bool TryGetSession(CookieValidatePrincipalContext context, out Session session) { + session = null!; + + if (context?.Properties == null) + { + _logger.LogWarning("Cookie validation context or properties is null"); + return false; + } + var json = context.Properties.GetTokenValue(AppwriteAuthenticationDefaults.AuthenticationTokenAppwriteSession); if (string.IsNullOrEmpty(json)) { - session = null!; + _logger.LogDebug("No Appwrite session token found in authentication properties"); return false; } @@ -132,16 +153,16 @@ private static bool TryGetSession(CookieValidatePrincipalContext context, out Se var deserializedSession = JsonSerializer.Deserialize(json); if (deserializedSession == null || string.IsNullOrEmpty(deserializedSession.Secret)) { - session = null!; + _logger.LogWarning("Deserialized session is null or has empty secret"); return false; } session = deserializedSession; return true; } - catch + catch (JsonException ex) { - session = null!; + _logger.LogWarning(ex, "Failed to deserialize session from authentication token"); return false; } } diff --git a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs index 9ffdda3..69a0fd4 100644 --- a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs +++ b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs @@ -10,7 +10,8 @@ public AppwriteCookieAuthenticationOptions() Cookie.Name = AppwriteAuthenticationDefaults.AppwriteHelperCookieName; Cookie.HttpOnly = true; Cookie.SecurePolicy = CookieSecurePolicy.Always; - Cookie.SameSite = SameSiteMode.None; + Cookie.SameSite = SameSiteMode.Lax; + Cookie.Path = "/"; SlidingExpiration = true; } @@ -18,6 +19,8 @@ public AppwriteCookieAuthenticationOptions() public TimeSpan ExpireTimeSpan { get; set; } = TimeSpan.FromMinutes(15); + public TimeSpan MaximumExpireTimeSpan { get; set; } = TimeSpan.FromHours(24); + public bool SlidingExpiration { get; set; } /// From b8c6deb6538a64a541a41169c97baec312c33369 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:13:36 +0000 Subject: [PATCH 3/6] Address code review feedback - rename method and improve documentation Co-authored-by: neptrio <33280179+neptrio@users.noreply.github.com> --- .../AppwriteSignInCallbackHelper.cs | 19 +++++++++++++++++++ .../AppwriteCookieAuthenticationEvents.cs | 16 ++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs b/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs index add3da7..bde38fd 100644 --- a/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs +++ b/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs @@ -13,11 +13,30 @@ public class AppwriteSignInCallbackHelper([FromKeyedServices(Constants.APPWRITE_ { private readonly IAppwriteClientFactory _appwriteClientFactory = appwriteClientFactory; + /// + /// Creates a sign-in result for the specified user with default cookie options. + /// + /// The user ID to sign in. + /// The authentication secret. + /// The cookie lifetime. If null, defaults to 15 minutes. Maximum is enforced by cookie options. + /// Whether the cookie should be persistent. + /// The authentication type. If null, defaults to cookie authentication scheme. + /// An AppwriteSignInResult containing the principal, authentication properties, session, and user information. public async Task CreateSignInAsync(string userId, string secret, TimeSpan? cookieLifetime = null, bool isPersistent = true, string? authenticationType = null) { return await CreateSignInAsync(userId, secret, cookieLifetime, isPersistent, authenticationType, null); } + /// + /// Creates a sign-in result for the specified user with specific cookie options for maximum lifetime enforcement. + /// + /// The user ID to sign in. + /// The authentication secret. + /// The cookie lifetime. If null, defaults to 15 minutes. Maximum is enforced by cookie options. + /// Whether the cookie should be persistent. + /// The authentication type. If null, defaults to cookie authentication scheme. + /// Cookie options containing security settings like MaximumExpireTimeSpan. If null, uses default max of 24 hours. + /// An AppwriteSignInResult containing the principal, authentication properties, session, and user information. public async Task CreateSignInAsync( string userId, string secret, diff --git a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs index 17b2e51..43f21c7 100644 --- a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs +++ b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs @@ -59,13 +59,13 @@ public override async Task ValidatePrincipal(CookieValidatePrincipalContext cont } } - // Optional: additional online revoked session check + // Optional: additional online session validity check if (options.CheckForRevokedSessions) { - var isSessionValid = await IsSessionRevokedAsync(options, session.Secret); + var isSessionValid = await IsSessionValidAsync(options, session.Secret); if (!isSessionValid) { - _logger.LogWarning("Session has been revoked"); + _logger.LogWarning("Session has been revoked or is no longer valid"); await RejectAsync(context); return; } @@ -100,19 +100,19 @@ private static Account CreateAccountClient(AppwriteCookieAuthenticationOptions o return new Account(client); } - private async Task IsSessionRevokedAsync(AppwriteCookieAuthenticationOptions options, string sessionSecret) + private async Task IsSessionValidAsync(AppwriteCookieAuthenticationOptions options, string sessionSecret) { try { var account = CreateAccountClient(options, sessionSecret); var user = await account.Get(); - // Return false if session is revoked (user is null), true if session is still valid + // Return true if session is still valid, false if revoked return user != null; } catch (Exception ex) { _logger.LogWarning(ex, "Failed to validate session with Appwrite. Session will be rejected for security."); - // Fail secure - treat exception as revoked session + // Fail secure - treat exception as invalid session return false; } } @@ -131,9 +131,9 @@ private async Task TryUpdateSessionAsync(Account account, string sessionId } } - private bool TryGetSession(CookieValidatePrincipalContext context, out Session session) + private bool TryGetSession(CookieValidatePrincipalContext context, out Session? session) { - session = null!; + session = null; if (context?.Properties == null) { From e03b74605a31f27e8f2ead3e811dac580fcfd87d Mon Sep 17 00:00:00 2001 From: Kevin Meyer Date: Mon, 9 Feb 2026 09:08:39 +0100 Subject: [PATCH 4/6] match appwrite behaviour more closely and removed overhead and extras. --- .../AppwriteSignInCallbackHelper.cs | 89 +++++++++--------- .../AppwriteCookieAuthenticationEvents.cs | 92 ++----------------- .../AppwriteCookieAuthenticationOptions.cs | 13 +-- .../AuthenticationBuilderExtensions.cs | 2 +- 4 files changed, 54 insertions(+), 142 deletions(-) diff --git a/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs b/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs index bde38fd..aee0b4d 100644 --- a/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs +++ b/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs @@ -14,35 +14,15 @@ public class AppwriteSignInCallbackHelper([FromKeyedServices(Constants.APPWRITE_ private readonly IAppwriteClientFactory _appwriteClientFactory = appwriteClientFactory; /// - /// Creates a sign-in result for the specified user with default cookie options. + /// Creates a sign-in result for the specified user with specific cookie options for lifetime enforcement. /// /// The user ID to sign in. /// The authentication secret. - /// The cookie lifetime. If null, defaults to 15 minutes. Maximum is enforced by cookie options. - /// Whether the cookie should be persistent. - /// The authentication type. If null, defaults to cookie authentication scheme. + /// Cookie options containing security settings like ExpireTimeSpan. /// An AppwriteSignInResult containing the principal, authentication properties, session, and user information. - public async Task CreateSignInAsync(string userId, string secret, TimeSpan? cookieLifetime = null, bool isPersistent = true, string? authenticationType = null) - { - return await CreateSignInAsync(userId, secret, cookieLifetime, isPersistent, authenticationType, null); - } - - /// - /// Creates a sign-in result for the specified user with specific cookie options for maximum lifetime enforcement. - /// - /// The user ID to sign in. - /// The authentication secret. - /// The cookie lifetime. If null, defaults to 15 minutes. Maximum is enforced by cookie options. - /// Whether the cookie should be persistent. - /// The authentication type. If null, defaults to cookie authentication scheme. - /// Cookie options containing security settings like MaximumExpireTimeSpan. If null, uses default max of 24 hours. - /// An AppwriteSignInResult containing the principal, authentication properties, session, and user information. - public async Task CreateSignInAsync( - string userId, - string secret, - TimeSpan? cookieLifetime = null, - bool isPersistent = true, - string? authenticationType = null, + public async Task CreateAppwriteCookieSignInAsync( + string userId, + string secret, IOptionsMonitor? cookieOptions = null) { ArgumentException.ThrowIfNullOrEmpty(userId); @@ -59,38 +39,51 @@ public async Task CreateSignInAsync( var userAccount = new Appwrite.Services.Account(userClient); var user = await userAccount.Get(); + if (user == null) + throw new InvalidOperationException("User not given"); var claims = new List { - new(ClaimTypes.Name, user?.Name ?? string.Empty), - new(ClaimTypes.Email, user?.Email ?? string.Empty), - new(ClaimTypes.NameIdentifier, user?.Id ?? string.Empty), + new(ClaimTypes.Name, user.Name ?? string.Empty), + new(ClaimTypes.Email, user.Email ?? string.Empty), + new(ClaimTypes.NameIdentifier, user.Id ?? string.Empty), }; - //if (user?.Prefs.Data != null) - //{ - // foreach (var p in user.Prefs.Data) - // { - // if (!string.IsNullOrEmpty(p.Key)) - // claims.Add(new Claim(AppwriteClaimTypes.Pref(p.Key), p.Value.ToString())); - // } - //} - - var identity = new ClaimsIdentity(claims, authenticationType ?? AppwriteAuthenticationDefaults.CookieAuthenticationScheme); + var identity = new ClaimsIdentity(claims, AppwriteAuthenticationDefaults.CookieAuthenticationScheme); var principal = new ClaimsPrincipal(identity); - // Enforce maximum cookie lifetime for security - var defaultExpireTime = TimeSpan.FromMinutes(15); - var maxExpireTime = cookieOptions?.CurrentValue?.MaximumExpireTimeSpan ?? TimeSpan.FromHours(24); - - var requestedExpireTime = cookieLifetime ?? defaultExpireTime; - var expires = requestedExpireTime > maxExpireTime ? maxExpireTime : requestedExpireTime; + // Calculate session expiration time + if (!long.TryParse(session.Expire?.ToString(), out var expireUnixTimestamp)) + throw new InvalidOperationException("Invalid session expiration timestamp"); + + var sessionExpireTime = DateTimeOffset.FromUnixTimeSeconds(expireUnixTimestamp); + var sessionExpireTimeSpan = sessionExpireTime - DateTimeOffset.UtcNow; + + // Ensure session expiration is positive + if (sessionExpireTimeSpan <= TimeSpan.Zero) + throw new InvalidOperationException("Session has already expired"); + + // Determine the cookie expiration time + TimeSpan cookieExpireTime; + if (cookieOptions?.CurrentValue?.ExpireTimeSpan.HasValue == true) + { + // Use ExpireTimeSpan from options, but cap it to session expiration + var configuredExpire = cookieOptions.CurrentValue.ExpireTimeSpan.Value; + cookieExpireTime = configuredExpire > sessionExpireTimeSpan + ? sessionExpireTimeSpan + : configuredExpire; + } + else + { + // Use session expiration time + cookieExpireTime = sessionExpireTimeSpan; + } var authenticationProperties = new AuthenticationProperties { - IsPersistent = isPersistent, - ExpiresUtc = DateTimeOffset.UtcNow.Add(expires), - AllowRefresh = true + IsPersistent = true, + ExpiresUtc = DateTimeOffset.UtcNow.Add(cookieExpireTime), + AllowRefresh = false }; var appwriteSession = new AuthenticationToken @@ -99,7 +92,7 @@ public async Task CreateSignInAsync( Value = JsonSerializer.Serialize(session.ToMap()) }; - authenticationProperties.StoreTokens(new List { appwriteSession }); + authenticationProperties.StoreTokens([appwriteSession]); return new AppwriteSignInResult(principal, authenticationProperties, session, user); } diff --git a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs index 181e0c9..831d580 100644 --- a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs +++ b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs @@ -1,14 +1,10 @@ using Appwrite; -using Appwrite.Models; using Appwrite.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.IdentityModel.Tokens.Jwt; -using System.Text.Json; namespace AppwriteHelper.Authentication.Cookies { @@ -40,60 +36,26 @@ public override Task RedirectToAccessDenied(RedirectContext IsSessionAcceptedByServerAsync(AppwriteCookieAuthenticationOptions options, string sessionSecret) + private async Task IsSessionAcceptedByServerAsync(AppwriteCookieAuthenticationOptions options, string sessionSecret) { try { @@ -116,27 +78,14 @@ private static async Task IsSessionAcceptedByServerAsync(AppwriteCookieAut } catch (Exception ex) { + _logger.LogWarning(ex, "Session not accepted by Appwrite"); return false; } } - private async Task TryUpdateSessionAsync(Account account, string sessionId) - { - try - { - await account.UpdateSession(sessionId); - return true; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to update session {SessionId} with Appwrite", sessionId); - return false; - } - } - - private bool TryGetSession(CookieValidatePrincipalContext context, out Session? session) + private bool TryGetSession(CookieValidatePrincipalContext context, out string session) { - session = null; + session = ""; if (context?.Properties == null) { @@ -144,35 +93,14 @@ private bool TryGetSession(CookieValidatePrincipalContext context, out Session? return false; } - var json = context.Properties.GetTokenValue(AppwriteAuthenticationDefaults.AuthenticationTokenAppwriteSession); - if (string.IsNullOrEmpty(json)) + session = context.Properties.GetTokenValue(AppwriteAuthenticationDefaults.AuthenticationTokenAppwriteSession) ?? ""; + if (string.IsNullOrEmpty(session)) { _logger.LogDebug("No Appwrite session token found in authentication properties"); return false; } - try - { - var deserializedSession = JsonSerializer.Deserialize(json); - if (deserializedSession == null || string.IsNullOrEmpty(deserializedSession.Secret)) - { - _logger.LogWarning("Deserialized session is null or has empty secret"); - return false; - } - - session = deserializedSession; - - //check at least for secret and id - if (String.IsNullOrEmpty(session.Secret) || String.IsNullOrEmpty(session.Id)) - return false; - - return true; - } - catch (JsonException ex) - { - _logger.LogWarning(ex, "Failed to deserialize session from authentication token"); - return false; - } + return true; } private static async Task RejectAsync(CookieValidatePrincipalContext context) diff --git a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs index 69a0fd4..513d641 100644 --- a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs +++ b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs @@ -12,27 +12,18 @@ public AppwriteCookieAuthenticationOptions() Cookie.SecurePolicy = CookieSecurePolicy.Always; Cookie.SameSite = SameSiteMode.Lax; Cookie.Path = "/"; - SlidingExpiration = true; } public CookieBuilder Cookie { get; } = new CookieBuilder(); - public TimeSpan ExpireTimeSpan { get; set; } = TimeSpan.FromMinutes(15); - - public TimeSpan MaximumExpireTimeSpan { get; set; } = TimeSpan.FromHours(24); - - public bool SlidingExpiration { get; set; } + public TimeSpan? ExpireTimeSpan { get; set; } /// /// If enabled, the middleware checks if the session is still valid by calling the Appwrite account endpoint. + /// This should only be enabled, if you call endpoints not using the Appwrite Client that would fail if the session is beeing revoked. /// public bool CheckForRevokedSessions { get; set; } = false; - /// - /// If enabled, the middleware extends the Appwrite session when the cookie is renewed. - /// - public bool ExtendSessionOnRenewal { get; set; } = false; - public string AppwriteEndpoint { get; set; } = string.Empty; public string AppwriteProject { get; set; } = string.Empty; } diff --git a/src/AppwriteHelper/AuthenticationBuilderExtensions.cs b/src/AppwriteHelper/AuthenticationBuilderExtensions.cs index 003d7ac..f4adcb0 100644 --- a/src/AppwriteHelper/AuthenticationBuilderExtensions.cs +++ b/src/AppwriteHelper/AuthenticationBuilderExtensions.cs @@ -39,7 +39,7 @@ public static AuthenticationBuilder AddAppwriteCookieAuthentication(this Authent builder.Services.AddScoped(); builder.AddCookie(cookieScheme, options => - { + { options.EventsType = typeof(AppwriteCookieAuthenticationEvents); }); From 0a542d8331eb3f21335b8b67b2718818b6b9cbb2 Mon Sep 17 00:00:00 2001 From: Kevin Meyer Date: Mon, 9 Feb 2026 09:14:30 +0100 Subject: [PATCH 5/6] check for expire --- .../Cookies/AppwriteCookieAuthenticationEvents.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs index 831d580..df9c528 100644 --- a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs +++ b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs @@ -36,6 +36,14 @@ public override Task RedirectToAccessDenied(RedirectContext Date: Mon, 9 Feb 2026 11:11:37 +0100 Subject: [PATCH 6/6] --- .../AppwriteSignInCallbackHelper.cs | 26 ++++++++----------- .../AppwriteUserClientCollectionMiddelware.cs | 2 +- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs b/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs index aee0b4d..f0d50cd 100644 --- a/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs +++ b/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs @@ -53,43 +53,39 @@ public async Task CreateAppwriteCookieSignInAsync( var principal = new ClaimsPrincipal(identity); // Calculate session expiration time - if (!long.TryParse(session.Expire?.ToString(), out var expireUnixTimestamp)) - throw new InvalidOperationException("Invalid session expiration timestamp"); + if (!DateTimeOffset.TryParse(session.Expire?.ToString(), out var sessionExpireTime)) + throw new InvalidOperationException("Invalid session expiration date"); - var sessionExpireTime = DateTimeOffset.FromUnixTimeSeconds(expireUnixTimestamp); - var sessionExpireTimeSpan = sessionExpireTime - DateTimeOffset.UtcNow; - - // Ensure session expiration is positive - if (sessionExpireTimeSpan <= TimeSpan.Zero) + if (sessionExpireTime <= DateTime.UtcNow) throw new InvalidOperationException("Session has already expired"); // Determine the cookie expiration time - TimeSpan cookieExpireTime; + DateTimeOffset cookieExpireTime; if (cookieOptions?.CurrentValue?.ExpireTimeSpan.HasValue == true) { // Use ExpireTimeSpan from options, but cap it to session expiration - var configuredExpire = cookieOptions.CurrentValue.ExpireTimeSpan.Value; - cookieExpireTime = configuredExpire > sessionExpireTimeSpan - ? sessionExpireTimeSpan - : configuredExpire; + var configuredExpire = DateTimeOffset.UtcNow.Add(cookieOptions.CurrentValue.ExpireTimeSpan.Value); + cookieExpireTime = configuredExpire < sessionExpireTime + ? configuredExpire + : sessionExpireTime; } else { // Use session expiration time - cookieExpireTime = sessionExpireTimeSpan; + cookieExpireTime = sessionExpireTime; } var authenticationProperties = new AuthenticationProperties { IsPersistent = true, - ExpiresUtc = DateTimeOffset.UtcNow.Add(cookieExpireTime), + ExpiresUtc = cookieExpireTime, AllowRefresh = false }; var appwriteSession = new AuthenticationToken { Name = AppwriteAuthenticationDefaults.AuthenticationTokenAppwriteSession, - Value = JsonSerializer.Serialize(session.ToMap()) + Value = session.Secret }; authenticationProperties.StoreTokens([appwriteSession]); diff --git a/src/AppwriteHelper/Middelwares/AppwriteUserClientCollectionMiddelware.cs b/src/AppwriteHelper/Middelwares/AppwriteUserClientCollectionMiddelware.cs index e8c8123..c7a39ce 100644 --- a/src/AppwriteHelper/Middelwares/AppwriteUserClientCollectionMiddelware.cs +++ b/src/AppwriteHelper/Middelwares/AppwriteUserClientCollectionMiddelware.cs @@ -18,7 +18,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) var session = authenticationProperties?.GetTokenValue(AppwriteAuthenticationDefaults.AuthenticationTokenAppwriteSession); if (authenticateResultFeature?.AuthenticateResult?.Succeeded == true) - { + { if (_client != null) { if (!string.IsNullOrEmpty(session))