diff --git a/OliverBooth/Data/Web/Configuration/SessionConfiguration.cs b/OliverBooth/Data/Web/Configuration/SessionConfiguration.cs index 2b47a27..b41c566 100644 --- a/OliverBooth/Data/Web/Configuration/SessionConfiguration.cs +++ b/OliverBooth/Data/Web/Configuration/SessionConfiguration.cs @@ -17,6 +17,7 @@ internal sealed class SessionConfiguration : IEntityTypeConfiguration builder.Property(e => e.Updated).IsRequired(); builder.Property(e => e.LastAccessed).IsRequired(); builder.Property(e => e.Expires).IsRequired(); + builder.Property(e => e.UserAgent).HasMaxLength(255).IsRequired(); builder.Property(e => e.UserId); builder.Property(e => e.IpAddress).HasConversion().IsRequired(); builder.Property(e => e.RequiresTotp).IsRequired(); diff --git a/OliverBooth/Data/Web/ISession.cs b/OliverBooth/Data/Web/ISession.cs index 88f7b9c..d711057 100644 --- a/OliverBooth/Data/Web/ISession.cs +++ b/OliverBooth/Data/Web/ISession.cs @@ -49,9 +49,15 @@ public interface ISession /// The update timestamp. DateTimeOffset Updated { get; } + /// + /// Gets the user agent string associated with this session. + /// + /// The user agent string. + string UserAgent { get; } + /// /// Gets the user ID associated with the session. /// /// The user ID. Guid UserId { get; } -} \ No newline at end of file +} diff --git a/OliverBooth/Data/Web/Session.cs b/OliverBooth/Data/Web/Session.cs index 8d42b3c..b46624f 100644 --- a/OliverBooth/Data/Web/Session.cs +++ b/OliverBooth/Data/Web/Session.cs @@ -25,6 +25,9 @@ internal sealed class Session : ISession /// public DateTimeOffset Updated { get; set; } + /// + public string UserAgent { get; set; } = string.Empty; + /// public Guid UserId { get; set; } } diff --git a/OliverBooth/Services/SessionService.cs b/OliverBooth/Services/SessionService.cs index 9ee415c..14908ed 100644 --- a/OliverBooth/Services/SessionService.cs +++ b/OliverBooth/Services/SessionService.cs @@ -55,7 +55,8 @@ internal sealed class SessionService : BackgroundService, ISessionService Updated = now, LastAccessed = now, Expires = now + TimeSpan.FromDays(1), - RequiresTotp = !string.IsNullOrWhiteSpace(user.Totp) + RequiresTotp = !string.IsNullOrWhiteSpace(user.Totp), + UserAgent = request.Headers.UserAgent.ToString() }; EntityEntry entry = context.Sessions.Add(session); context.SaveChanges(); @@ -110,7 +111,7 @@ internal sealed class SessionService : BackgroundService, ISessionService public bool TryGetCurrentUser(HttpRequest request, HttpResponse response, [NotNullWhen(true)] out IUser? user) { user = null; - + if (!TryGetSession(request, out ISession? session)) { _logger.LogDebug("Session not found; redirecting"); @@ -203,16 +204,29 @@ internal sealed class SessionService : BackgroundService, ISessionService if (!remoteIpAddress.TryWriteBytes(remoteAddressBytes, out _) || !session.IpAddress.TryWriteBytes(sessionAddressBytes, out _)) { + _logger.LogWarning("Failed to write bytes for session {Id}", session.Id); return false; } if (!remoteAddressBytes.SequenceEqual(sessionAddressBytes)) { + _logger.LogInformation("Session {Id} has IP mismatch (wanted {Expected}, got {Actual})", session.Id, + session.IpAddress, remoteIpAddress); + return false; + } + + var userAgent = request.Headers.UserAgent.ToString(); + if (session.UserAgent != userAgent) + { + _logger.LogInformation("Session {Id} has user agent mismatch (wanted {Expected}, got {Actual})", session.Id, + session.UserAgent, userAgent); return false; } if (!_userService.TryGetUser(session.UserId, out _)) { + _logger.LogWarning("User {Id} not found for session {Session} (client {Ip})", session.UserId, session.Id, + remoteIpAddress); return false; }