diff --git a/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs b/src/AppwriteHelper/Authentication/AppwriteSignInCallbackHelper.cs index d24bf24..f0d50cd 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; @@ -11,7 +13,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) + /// + /// 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. + /// Cookie options containing security settings like ExpireTimeSpan. + /// An AppwriteSignInResult containing the principal, authentication properties, session, and user information. + public async Task CreateAppwriteCookieSignInAsync( + string userId, + string secret, + IOptionsMonitor? cookieOptions = null) { ArgumentException.ThrowIfNullOrEmpty(userId); ArgumentException.ThrowIfNullOrEmpty(secret); @@ -20,48 +32,63 @@ 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); 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); - var expires = cookieLifetime ?? TimeSpan.FromMinutes(15); + // Calculate session expiration time + if (!DateTimeOffset.TryParse(session.Expire?.ToString(), out var sessionExpireTime)) + throw new InvalidOperationException("Invalid session expiration date"); + + if (sessionExpireTime <= DateTime.UtcNow) + throw new InvalidOperationException("Session has already expired"); + + // Determine the cookie expiration time + DateTimeOffset cookieExpireTime; + if (cookieOptions?.CurrentValue?.ExpireTimeSpan.HasValue == true) + { + // Use ExpireTimeSpan from options, but cap it to session expiration + var configuredExpire = DateTimeOffset.UtcNow.Add(cookieOptions.CurrentValue.ExpireTimeSpan.Value); + cookieExpireTime = configuredExpire < sessionExpireTime + ? configuredExpire + : sessionExpireTime; + } + else + { + // Use session expiration time + cookieExpireTime = sessionExpireTime; + } + var authenticationProperties = new AuthenticationProperties { - IsPersistent = isPersistent, - ExpiresUtc = DateTimeOffset.UtcNow.Add(expires), - AllowRefresh = true + IsPersistent = true, + ExpiresUtc = cookieExpireTime, + AllowRefresh = false }; var appwriteSession = new AuthenticationToken { Name = AppwriteAuthenticationDefaults.AuthenticationTokenAppwriteSession, - Value = JsonSerializer.Serialize(session.ToMap()) + Value = session.Secret }; - 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 be7e05d..df9c528 100644 --- a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs +++ b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationEvents.cs @@ -1,22 +1,25 @@ 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 { 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) { @@ -33,60 +36,34 @@ public override Task RedirectToAccessDenied(RedirectContext IsSessionAcceptedByServerAsync(AppwriteCookieAuthenticationOptions options, string sessionSecret) + private async Task IsSessionAcceptedByServerAsync(AppwriteCookieAuthenticationOptions options, string sessionSecret) { try { var user = await CreateAccountClient(options, sessionSecret).Get(); return !string.IsNullOrWhiteSpace(user?.Id); } - catch + catch (Exception ex) { + _logger.LogWarning(ex, "Session not accepted by Appwrite"); return false; } } - private static async Task TryUpdateSessionAsync(Account account, string sessionId) + private bool TryGetSession(CookieValidatePrincipalContext context, out string session) { - try - { - await account.UpdateSession(sessionId); - return true; - } - catch - { - return false; - } - } + session = ""; - private static bool TryGetSession(CookieValidatePrincipalContext context, out Session session) - { - var json = context.Properties.GetTokenValue(AppwriteAuthenticationDefaults.AuthenticationTokenAppwriteSession); - if (string.IsNullOrEmpty(json)) + if (context?.Properties == null) { - session = null!; + _logger.LogWarning("Cookie validation context or properties is null"); return false; } - try - { - var deserializedSession = JsonSerializer.Deserialize(json); - if (deserializedSession == null || string.IsNullOrEmpty(deserializedSession.Secret)) - { - session = null!; - 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 + session = context.Properties.GetTokenValue(AppwriteAuthenticationDefaults.AuthenticationTokenAppwriteSession) ?? ""; + if (string.IsNullOrEmpty(session)) { - session = null!; + _logger.LogDebug("No Appwrite session token found in authentication properties"); 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 9ffdda3..513d641 100644 --- a/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs +++ b/src/AppwriteHelper/Authentication/Cookies/AppwriteCookieAuthenticationOptions.cs @@ -10,26 +10,20 @@ public AppwriteCookieAuthenticationOptions() Cookie.Name = AppwriteAuthenticationDefaults.AppwriteHelperCookieName; Cookie.HttpOnly = true; Cookie.SecurePolicy = CookieSecurePolicy.Always; - Cookie.SameSite = SameSiteMode.None; - SlidingExpiration = true; + Cookie.SameSite = SameSiteMode.Lax; + Cookie.Path = "/"; } public CookieBuilder Cookie { get; } = new CookieBuilder(); - public TimeSpan ExpireTimeSpan { get; set; } = TimeSpan.FromMinutes(15); - - 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); }); 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))