wip session auth

This commit is contained in:
john 2025-05-29 00:39:40 +02:00
parent aa4394fd21
commit 7b6c155a73
23 changed files with 321 additions and 90 deletions

View file

@ -1,8 +1,10 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json;
using Femto.Api.Sessions; using Femto.Api.Sessions;
using Femto.Common; using Femto.Common;
using Femto.Modules.Auth.Application; using Femto.Modules.Auth.Application;
using Femto.Modules.Auth.Application.Dto;
using Femto.Modules.Auth.Application.Interface.ValidateSession; using Femto.Modules.Auth.Application.Interface.ValidateSession;
using Femto.Modules.Auth.Errors; using Femto.Modules.Auth.Errors;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
@ -25,29 +27,42 @@ internal class SessionAuthenticationHandler(
if (string.IsNullOrWhiteSpace(sessionId)) if (string.IsNullOrWhiteSpace(sessionId))
return AuthenticateResult.NoResult(); return AuthenticateResult.NoResult();
var userJson = this.Request.Cookies["user"];
if (string.IsNullOrWhiteSpace(userJson))
return AuthenticateResult.Fail("Invalid user");
var user = JsonSerializer.Deserialize<UserInfo>(userJson);
if (user is null)
return AuthenticateResult.Fail("Invalid user");
var rememberMe = this.Request.Cookies["rememberme"];
try try
{ {
var result = await authModule.Command(new ValidateSessionCommand(sessionId)); var result = await authModule.Command(
new ValidateSessionCommand(sessionId, user, rememberMe)
);
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(ClaimTypes.Name, result.User.Username), new(ClaimTypes.Name, user.Username),
new("sub", result.User.Id.ToString()), new("sub", user.Id.ToString()),
new("user_id", result.User.Id.ToString()), new("user_id", user.Id.ToString()),
}; };
claims.AddRange( claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role.ToString())));
result.User.Roles.Select(role => new Claim(ClaimTypes.Role, role.ToString()))
);
var identity = new ClaimsIdentity(claims, this.Scheme.Name); var identity = new ClaimsIdentity(claims, this.Scheme.Name);
var principal = new ClaimsPrincipal(identity); var principal = new ClaimsPrincipal(identity);
this.Context.SetSession(result.Session, result.User, Logger); this.Context.SetSession(result.SessionDto, user, Logger);
currentUserContext.CurrentUser = new CurrentUser( currentUserContext.CurrentUser = new CurrentUser(
result.User.Id, user.Id,
result.User.Username, user.Username,
result.Session.SessionId result.SessionDto.SessionId,
rememberMe
); );
return AuthenticateResult.Success( return AuthenticateResult.Success(

View file

@ -29,7 +29,7 @@ public class AuthController(
{ {
var result = await authModule.Command(new LoginCommand(request.Username, request.Password)); var result = await authModule.Command(new LoginCommand(request.Username, request.Password));
HttpContext.SetSession(result.Session, result.User, logger); HttpContext.SetSession(result.SessionDto, result.User, logger);
return new LoginResponse( return new LoginResponse(
result.User.Id, result.User.Id,
@ -45,7 +45,7 @@ public class AuthController(
new RegisterCommand(request.Username, request.Password, request.SignupCode) new RegisterCommand(request.Username, request.Password, request.SignupCode)
); );
HttpContext.SetSession(result.Session, result.User, logger); HttpContext.SetSession(result.SessionDto, result.User, logger);
return new RegisterResponse( return new RegisterResponse(
result.User.Id, result.User.Id,
@ -57,7 +57,13 @@ public class AuthController(
[HttpDelete("session")] [HttpDelete("session")]
public async Task<ActionResult> DeleteSession() public async Task<ActionResult> DeleteSession()
{ {
var currentUser = currentUserContext.CurrentUser;
if (currentUser != null)
await authModule.Command(new DeauthenticateCommand(currentUser.Id, currentUser.SessionId, currentUser.RememberMeToken));
HttpContext.DeleteSession(); HttpContext.DeleteSession();
return Ok(new { }); return Ok(new { });
} }
@ -73,7 +79,7 @@ public class AuthController(
try try
{ {
var result = await authModule.Command( var result = await authModule.Command(
new RefreshUserSessionCommand(userId, currentUser), new RefreshUserCommand(userId, currentUser),
cancellationToken cancellationToken
); );

View file

@ -8,14 +8,14 @@ namespace Femto.Api.Sessions;
internal static class HttpContextSessionExtensions internal static class HttpContextSessionExtensions
{ {
public static void SetSession(this HttpContext httpContext, Session session, UserInfo user, ILogger logger) public static void SetSession(this HttpContext httpContext, SessionDto sessionDto, UserInfo user, ILogger logger)
{ {
var cookieSettings = httpContext.RequestServices.GetService<IOptions<CookieSettings>>(); var cookieSettings = httpContext.RequestServices.GetService<IOptions<CookieSettings>>();
var secure = cookieSettings?.Value.Secure ?? true; var secure = cookieSettings?.Value.Secure ?? true;
var sameSite = cookieSettings?.Value.SameSite ?? SameSiteMode.Strict; var sameSite = cookieSettings?.Value.SameSite ?? SameSiteMode.Strict;
var domain = cookieSettings?.Value.Domain; var domain = cookieSettings?.Value.Domain;
var expires = session.Expires; var expires = sessionDto.Expires;
logger.LogInformation( logger.LogInformation(
"cookie settings: Secure={Secure}, SameSite={SameSite}, domain={Domain}, Expires={Expires}", "cookie settings: Secure={Secure}, SameSite={SameSite}, domain={Domain}, Expires={Expires}",
@ -27,7 +27,7 @@ internal static class HttpContextSessionExtensions
httpContext.Response.Cookies.Append( httpContext.Response.Cookies.Append(
"session", "session",
session.SessionId, sessionDto.SessionId,
new CookieOptions new CookieOptions
{ {
IsEssential = true, IsEssential = true,
@ -55,7 +55,7 @@ internal static class HttpContextSessionExtensions
IsEssential = true, IsEssential = true,
Secure = secure, Secure = secure,
SameSite = sameSite, SameSite = sameSite,
Expires = session.Expires, Expires = sessionDto.Expires,
} }
); );
} }

View file

@ -5,4 +5,4 @@ public interface ICurrentUserContext
CurrentUser? CurrentUser { get; } CurrentUser? CurrentUser { get; }
} }
public record CurrentUser(Guid Id, string Username, string SessionId); public record CurrentUser(Guid Id, string Username, string SessionId, string? RememberMeToken);

View file

@ -0,0 +1,27 @@
# Remember me
We want to implement long lived sessions
we will do this with a remember me cookie
this should be implemented as so:
logging or registering and including a "rememberMe" flag with the request will generate a new remember me token, which can be stored as a cookie .
the remember me token should live until:
* the user changes password anywhere
* the user logs out on that device
* the user logs in with an expired session, in which case the remember me token will be used to refresh the session, and then it will be swapped out for a new one
that means we need to implement three spots:
- [ ] login
- [ ] register
- [ ] validate session
we will implement it as described [here](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence)
we will only check the remember me token in "validate session".
"refresh session" is only called with valid sessions so we do not need to check it here, as the session should already have been validated

View file

@ -0,0 +1,16 @@
# Strong vs weak sessions
a **strong** session is one that should have the power to do account level admin tasks like change password
a **weak** session has strictly fewer privileges than a strong session
## where to get a strong session
a strong session is created when a user provides a username and a password. a session remains strong until it is refreshed, at which point it becomes weak.
## where to get a weak session
A weak session is any session that has not been directly created by user credentials, i.e.:
* short-term session refresh
* long-term session refresh

View file

@ -1,3 +1,3 @@
namespace Femto.Modules.Auth.Application.Dto; namespace Femto.Modules.Auth.Application.Dto;
public record LoginResult(Session Session, UserInfo User); public record LoginResult(SessionDto SessionDto, UserInfo User);

View file

@ -1,3 +1,3 @@
namespace Femto.Modules.Auth.Application.Dto; namespace Femto.Modules.Auth.Application.Dto;
public record RefreshUserSessionResult(Session Session, UserInfo User); public record RefreshUserSessionResult(SessionDto SessionDto, UserInfo User);

View file

@ -1,3 +1,3 @@
namespace Femto.Modules.Auth.Application.Dto; namespace Femto.Modules.Auth.Application.Dto;
public record RegisterResult(Session Session, UserInfo User); public record RegisterResult(SessionDto SessionDto, UserInfo User);

View file

@ -2,9 +2,16 @@ using Femto.Modules.Auth.Models;
namespace Femto.Modules.Auth.Application.Dto; namespace Femto.Modules.Auth.Application.Dto;
public record Session(string SessionId, DateTimeOffset Expires) public record SessionDto(
string SessionId,
DateTimeOffset Expires,
bool Weak,
string? RememberMe = null
)
{ {
internal Session(UserSession session) : this(session.Id, session.Expires) internal SessionDto(Session session)
{ : this(session.Id, session.Expires, !session.IsStronglyAuthenticated) { }
}
internal SessionDto(Session session, string? rememberMe)
: this(session.Id, session.Expires, !session.IsStronglyAuthenticated, rememberMe) { }
} }

View file

@ -1,3 +1,3 @@
namespace Femto.Modules.Auth.Application.Dto; namespace Femto.Modules.Auth.Application.Dto;
public record ValidateSessionResult(Session Session, UserInfo User); public record ValidateSessionResult(SessionDto SessionDto);

View file

@ -0,0 +1,6 @@
using Femto.Common.Domain;
namespace Femto.Modules.Auth.Application.Interface.Deauthenticate;
public record DeauthenticateCommand(Guid UserId, string SessionId, string? RememberMeToken) : ICommand;

View file

@ -0,0 +1,12 @@
using Femto.Common.Domain;
using Femto.Modules.Auth.Data;
namespace Femto.Modules.Auth.Application.Interface.Deauthenticate;
internal class DeauthenticateCommandHandler(AuthContext context) : ICommandHandler<DeauthenticateCommand>
{
public async Task Handle(DeauthenticateCommand request, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}

View file

@ -1,6 +1,7 @@
using Femto.Common.Domain; using Femto.Common.Domain;
using Femto.Modules.Auth.Application.Dto; using Femto.Modules.Auth.Application.Dto;
using Femto.Modules.Auth.Data; using Femto.Modules.Auth.Data;
using Femto.Modules.Auth.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Femto.Modules.Auth.Application.Interface.Login; namespace Femto.Modules.Auth.Application.Interface.Login;
@ -21,8 +22,10 @@ internal class LoginCommandHandler(AuthContext context)
if (!user.HasPassword(request.Password)) if (!user.HasPassword(request.Password))
throw new DomainError("invalid credentials"); throw new DomainError("invalid credentials");
var session = user.StartNewSession(); var session = Session.Strong(user.Id);
return new(new Session(session.Id, session.Expires), new UserInfo(user)); await context.AddAsync(session, cancellationToken);
return new(new SessionDto(session), new UserInfo(user));
} }
} }

View file

@ -4,4 +4,4 @@ using Femto.Modules.Auth.Application.Dto;
namespace Femto.Modules.Auth.Application.Interface.RefreshUserSession; namespace Femto.Modules.Auth.Application.Interface.RefreshUserSession;
public record RefreshUserSessionCommand(Guid ForUser, CurrentUser CurrentUser) : ICommand<RefreshUserSessionResult>; public record RefreshUserCommand(Guid ForUser, CurrentUser CurrentUser) : ICommand<RefreshUserSessionResult>;

View file

@ -2,15 +2,17 @@ using Femto.Common.Domain;
using Femto.Common.Infrastructure.DbConnection; using Femto.Common.Infrastructure.DbConnection;
using Femto.Modules.Auth.Application.Dto; using Femto.Modules.Auth.Application.Dto;
using Femto.Modules.Auth.Data; using Femto.Modules.Auth.Data;
using Femto.Modules.Auth.Errors;
using Femto.Modules.Auth.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Femto.Modules.Auth.Application.Interface.RefreshUserSession; namespace Femto.Modules.Auth.Application.Interface.RefreshUserSession;
internal class RefreshUserSessionCommandHandler(AuthContext context) internal class RefreshUserSessionCommandHandler(AuthContext context)
: ICommandHandler<RefreshUserSessionCommand, RefreshUserSessionResult> : ICommandHandler<RefreshUserCommand, RefreshUserSessionResult>
{ {
public async Task<RefreshUserSessionResult> Handle( public async Task<RefreshUserSessionResult> Handle(
RefreshUserSessionCommand request, RefreshUserCommand request,
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
@ -25,8 +27,20 @@ internal class RefreshUserSessionCommandHandler(AuthContext context)
if (user is null) if (user is null)
throw new DomainError("invalid request"); throw new DomainError("invalid request");
var session = user.PossiblyRefreshSession(request.CurrentUser.SessionId); var session = await context.Sessions.SingleOrDefaultAsync(
s => s.Id == request.CurrentUser.SessionId && s.Expires > DateTimeOffset.UtcNow,
cancellationToken
);
return new(new Session(session), new UserInfo(user)); if (session is null)
throw new InvalidSessionError();
if (session.ShouldRefresh)
{
session = Session.Weak(user.Id);
await context.AddAsync(session, cancellationToken);
}
return new(new SessionDto(session), new UserInfo(user));
} }
} }

View file

@ -6,13 +6,19 @@ using Microsoft.EntityFrameworkCore;
namespace Femto.Modules.Auth.Application.Interface.Register; namespace Femto.Modules.Auth.Application.Interface.Register;
internal class RegisterCommandHandler(AuthContext context) : ICommandHandler<RegisterCommand, RegisterResult> internal class RegisterCommandHandler(AuthContext context)
: ICommandHandler<RegisterCommand, RegisterResult>
{ {
public async Task<RegisterResult> Handle(RegisterCommand request, CancellationToken cancellationToken) public async Task<RegisterResult> Handle(
RegisterCommand request,
CancellationToken cancellationToken
)
{ {
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
var code = await context.SignupCodes
.Where(c => c.Code == request.SignupCode) var code = await context
.SignupCodes.Where(c => c.Code == request.SignupCode)
.Where(c => c.ExpiresAt == null || c.ExpiresAt > now) .Where(c => c.ExpiresAt == null || c.ExpiresAt > now)
.Where(c => c.RedeemingUserId == null) .Where(c => c.RedeemingUserId == null)
.SingleOrDefaultAsync(cancellationToken); .SingleOrDefaultAsync(cancellationToken);
@ -22,14 +28,16 @@ internal class RegisterCommandHandler(AuthContext context) : ICommandHandler<Reg
var user = new UserIdentity(request.Username); var user = new UserIdentity(request.Username);
await context.AddAsync(user, cancellationToken);
user.SetPassword(request.Password); user.SetPassword(request.Password);
var session = user.StartNewSession(); var session = Session.Strong(user.Id);
await context.AddAsync(user, cancellationToken); await context.AddAsync(session, cancellationToken);
code.Redeem(user.Id); code.Redeem(user.Id);
return new(new Session(session.Id, session.Expires), new UserInfo(user)); return new(new SessionDto(session), new UserInfo(user));
} }
} }

View file

@ -7,4 +7,4 @@ namespace Femto.Modules.Auth.Application.Interface.ValidateSession;
/// Validate an existing session, and then return either the current session, or a new one in case the expiry is further in the future /// Validate an existing session, and then return either the current session, or a new one in case the expiry is further in the future
/// </summary> /// </summary>
/// <param name="SessionId"></param> /// <param name="SessionId"></param>
public record ValidateSessionCommand(string SessionId) : ICommand<ValidateSessionResult>; public record ValidateSessionCommand(string SessionId, UserInfo User, string? RememberMe) : ICommand<ValidateSessionResult>;

View file

@ -1,7 +1,9 @@
using Femto.Common.Domain; using Femto.Common.Domain;
using Femto.Common.Infrastructure.DbConnection;
using Femto.Modules.Auth.Application.Dto; using Femto.Modules.Auth.Application.Dto;
using Femto.Modules.Auth.Data; using Femto.Modules.Auth.Data;
using Femto.Modules.Auth.Errors; using Femto.Modules.Auth.Errors;
using Femto.Modules.Auth.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Femto.Modules.Auth.Application.Interface.ValidateSession; namespace Femto.Modules.Auth.Application.Interface.ValidateSession;
@ -13,22 +15,97 @@ internal class ValidateSessionCommandHandler(AuthContext context)
ValidateSessionCommand request, ValidateSessionCommand request,
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{
try
{
return new ValidateSessionResult(await DoSessionValidation(request, cancellationToken));
}
finally
{
await context.SaveChangesAsync(cancellationToken);
}
}
private async Task<SessionDto> DoSessionValidation(
ValidateSessionCommand request,
CancellationToken cancellationToken
)
{ {
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
var user = await context.Users.SingleOrDefaultAsync( var session = await context.Sessions.SingleOrDefaultAsync(
u => u.Sessions.Any(s => s.Id == request.SessionId && s.Expires > now), s => s.Id == request.SessionId,
cancellationToken cancellationToken
); );
if (user is null) var rememberMe = request.RememberMe;
if (session is null)
{
(session, rememberMe) = await this.TryAuthenticateWithRememberMeToken(
request.User,
request.RememberMe,
cancellationToken
);
}
if (session.UserId != request.User.Id)
{
context.Remove(session);
throw new InvalidSessionError();
}
if (session.Expires < now)
{
context.Remove(session);
throw new InvalidSessionError();
}
if (session.ShouldRefresh)
{
context.Remove(session);
session = Session.Weak(session.UserId);
await context.AddAsync(session, cancellationToken);
}
return new SessionDto(session, rememberMe);
}
private async Task<(Session, string)> TryAuthenticateWithRememberMeToken(
UserInfo user,
string? rememberMeToken,
CancellationToken cancellationToken
)
{
if (rememberMeToken is null)
throw new InvalidSessionError(); throw new InvalidSessionError();
var session = user.PossiblyRefreshSession(request.SessionId); var parts = rememberMeToken.Split('.');
if (parts.Length != 2)
throw new InvalidSessionError();
return new ValidateSessionResult( var selector = parts[0];
new Session(session.Id, session.Expires), var verifier = parts[1];
new UserInfo(user)
var longTermSession = await context.LongTermSessions.SingleOrDefaultAsync(
s => s.Selector == selector,
cancellationToken
); );
if (longTermSession is null)
throw new InvalidSessionError();
context.Remove(longTermSession);
if (!longTermSession.Validate(verifier))
throw new InvalidSessionError();
var session = Session.Weak(user.Id);
await context.AddAsync(session, cancellationToken);
(longTermSession, rememberMeToken) = LongTermSession.Create(user.Id);
await context.AddAsync(longTermSession, cancellationToken);
return (session, rememberMeToken);
} }
} }

View file

@ -7,7 +7,9 @@ namespace Femto.Modules.Auth.Data;
internal class AuthContext(DbContextOptions<AuthContext> options) : DbContext(options), IOutboxContext internal class AuthContext(DbContextOptions<AuthContext> options) : DbContext(options), IOutboxContext
{ {
public virtual DbSet<UserIdentity> Users { get; set; } public virtual DbSet<UserIdentity> Users { get; set; }
public virtual DbSet<Session> Sessions { get; set; }
public virtual DbSet<SignupCode> SignupCodes { get; set; } public virtual DbSet<SignupCode> SignupCodes { get; set; }
public virtual DbSet<LongTermSession> LongTermSessions { get; set; }
public virtual DbSet<OutboxEntry> Outbox { get; set; } public virtual DbSet<OutboxEntry> Outbox { get; set; }
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)

View file

@ -0,0 +1,51 @@
using System.Text;
using static System.Security.Cryptography.RandomNumberGenerator;
namespace Femto.Modules.Auth.Models;
public class LongTermSession
{
private static TimeSpan TokenTimeout { get; } = TimeSpan.FromDays(90);
public int Id { get; private set; }
public string Selector { get; private set; }
public byte[] HashedVerifier { get; private set; }
public DateTimeOffset Expires { get; private set; }
public Guid UserId { get; private set; }
private LongTermSession() {}
public static (LongTermSession, string) Create(Guid userId)
{
var selector = GetString("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 12);
var verifier = GetString("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 32);
using var sha256 = System.Security.Cryptography.SHA256.Create();
var longTermSession = new LongTermSession
{
Selector = selector,
HashedVerifier = sha256.ComputeHash(Encoding.UTF8.GetBytes(verifier)),
UserId = userId,
Expires = DateTimeOffset.UtcNow + TokenTimeout
};
var rememberMeToken = $"{selector}.{verifier}";
return (longTermSession, rememberMeToken);
}
public bool Validate(string verifier)
{
if (this.Expires < DateTimeOffset.UtcNow)
return false;
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hashedVerifier = sha256.ComputeHash(Encoding.UTF8.GetBytes(verifier));
return hashedVerifier.SequenceEqual(this.HashedVerifier);
}
}

View file

@ -15,7 +15,7 @@ internal class UserIdentity : Entity
public Password? Password { get; private set; } public Password? Password { get; private set; }
public ICollection<UserSession> Sessions { get; private set; } = []; public ICollection<Session> Sessions { get; private set; } = [];
public ICollection<UserRole> Roles { get; private set; } = []; public ICollection<UserRole> Roles { get; private set; } = [];
@ -31,12 +31,6 @@ internal class UserIdentity : Entity
this.AddDomainEvent(new UserWasCreatedEvent(this)); this.AddDomainEvent(new UserWasCreatedEvent(this));
} }
public UserIdentity WithPassword(string password)
{
this.SetPassword(password);
return this;
}
public void SetPassword(string password) public void SetPassword(string password)
{ {
this.Password = new Password(password); this.Password = new Password(password);
@ -51,25 +45,6 @@ internal class UserIdentity : Entity
return this.Password.Check(requestPassword); return this.Password.Check(requestPassword);
} }
public UserSession PossiblyRefreshSession(string sessionId)
{
var session = this.Sessions.Single(s => s.Id == sessionId);
if (session.ExpiresSoon)
return this.StartNewSession();
return session;
}
public UserSession StartNewSession()
{
var session = UserSession.Create();
this.Sessions.Add(session);
return session;
}
} }
public class SetPasswordError(string message, Exception inner) : DomainError(message, inner); public class SetPasswordError(string message, Exception inner) : DomainError(message, inner);

View file

@ -1,21 +1,33 @@
using static System.Security.Cryptography.RandomNumberGenerator;
namespace Femto.Modules.Auth.Models; namespace Femto.Modules.Auth.Models;
internal class UserSession internal class Session
{ {
private static TimeSpan SessionTimeout { get; } = TimeSpan.FromMinutes(30); private static TimeSpan SessionTimeout { get; } = TimeSpan.FromMinutes(30);
private static TimeSpan ExpiryBuffer { get; } = TimeSpan.FromMinutes(5); private static TimeSpan ExpiryBuffer { get; } = TimeSpan.FromMinutes(5);
public string Id { get; private set; } public string Id { get; private set; }
public Guid UserId { get; private set; }
public DateTimeOffset Expires { get; private set; } public DateTimeOffset Expires { get; private set; }
public bool ExpiresSoon => Expires < DateTimeOffset.UtcNow + ExpiryBuffer; public bool ExpiresSoon => Expires < DateTimeOffset.UtcNow + ExpiryBuffer;
private UserSession() {} // true if this session was created with remember me token
// otherwise false
// required to be true to do things like change password etc.
public bool IsStronglyAuthenticated { get; private set; }
public bool ShouldRefresh => this.Expires < DateTimeOffset.UtcNow + ExpiryBuffer;
public static UserSession Create() private Session() { }
public static Session Strong(Guid userId) => new(userId, true);
public static Session Weak(Guid userId) => new(userId, false);
private Session(Guid userId, bool isStrong)
{ {
return new() this.Id = Convert.ToBase64String(GetBytes(32));
{ this.UserId = userId;
Id = Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)), this.Expires = DateTimeOffset.UtcNow + SessionTimeout;
Expires = DateTimeOffset.UtcNow + SessionTimeout this.IsStronglyAuthenticated = isStrong;
};
} }
} }