2024-02-20 20:39:52 +00:00
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
using System.Net;
|
2024-02-24 15:04:03 +00:00
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2024-02-20 20:39:52 +00:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
|
|
using OliverBooth.Data.Blog;
|
|
|
|
using OliverBooth.Data.Web;
|
2024-02-24 15:27:03 +00:00
|
|
|
using ISession = OliverBooth.Data.Web.ISession;
|
2024-02-20 20:39:52 +00:00
|
|
|
|
|
|
|
namespace OliverBooth.Services;
|
|
|
|
|
|
|
|
internal sealed class SessionService : ISessionService
|
|
|
|
{
|
|
|
|
private readonly ILogger<SessionService> _logger;
|
2024-02-24 14:52:43 +00:00
|
|
|
private readonly IUserService _userService;
|
2024-02-24 15:27:03 +00:00
|
|
|
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
2024-02-20 20:39:52 +00:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="SessionService" /> class.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="logger">The logger.</param>
|
|
|
|
/// <param name="userService">The user service.</param>
|
|
|
|
/// <param name="blogContextFactory">The <see cref="BlogContext" /> factory.</param>
|
|
|
|
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
|
|
|
|
public SessionService(ILogger<SessionService> logger,
|
2024-02-24 14:52:43 +00:00
|
|
|
IUserService userService,
|
2024-02-20 20:39:52 +00:00
|
|
|
IDbContextFactory<BlogContext> blogContextFactory,
|
|
|
|
IDbContextFactory<WebContext> webContextFactory)
|
|
|
|
{
|
|
|
|
_logger = logger;
|
|
|
|
_userService = userService;
|
2024-02-24 15:27:03 +00:00
|
|
|
_webContextFactory = webContextFactory;
|
2024-02-20 20:39:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public ISession CreateSession(HttpRequest request, IUser user)
|
|
|
|
{
|
2024-02-25 14:11:33 +00:00
|
|
|
if (request is null)
|
|
|
|
{
|
|
|
|
throw new ArgumentNullException(nameof(request));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (user is null)
|
|
|
|
{
|
|
|
|
throw new ArgumentNullException(nameof(user));
|
|
|
|
}
|
2024-02-20 20:39:52 +00:00
|
|
|
|
2024-02-24 15:27:03 +00:00
|
|
|
using WebContext context = _webContextFactory.CreateDbContext();
|
2024-02-20 20:39:52 +00:00
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
var session = new Session
|
|
|
|
{
|
|
|
|
UserId = user.Id,
|
|
|
|
IpAddress = request.HttpContext.Connection.RemoteIpAddress!,
|
|
|
|
Created = now,
|
|
|
|
Updated = now,
|
|
|
|
LastAccessed = now,
|
|
|
|
Expires = now + TimeSpan.FromDays(1),
|
|
|
|
RequiresTotp = !string.IsNullOrWhiteSpace(user.Totp)
|
|
|
|
};
|
|
|
|
EntityEntry<Session> entry = context.Sessions.Add(session);
|
|
|
|
context.SaveChanges();
|
|
|
|
return entry.Entity;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public void DeleteSession(ISession session)
|
|
|
|
{
|
2024-02-24 15:27:03 +00:00
|
|
|
using WebContext context = _webContextFactory.CreateDbContext();
|
2024-02-20 20:39:52 +00:00
|
|
|
context.Sessions.Remove((Session)session);
|
|
|
|
context.SaveChanges();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2024-02-24 15:37:39 +00:00
|
|
|
public void SaveSessionCookie(HttpResponse response, ISession session)
|
|
|
|
{
|
2024-02-25 14:11:33 +00:00
|
|
|
if (response is null)
|
|
|
|
{
|
|
|
|
throw new ArgumentNullException(nameof(response));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (session is null)
|
|
|
|
{
|
|
|
|
throw new ArgumentNullException(nameof(session));
|
|
|
|
}
|
2024-02-24 15:37:39 +00:00
|
|
|
|
|
|
|
Span<byte> buffer = stackalloc byte[16];
|
2024-02-25 14:11:33 +00:00
|
|
|
if (!session.Id.TryWriteBytes(buffer))
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
2024-02-24 15:37:39 +00:00
|
|
|
|
|
|
|
IPAddress? remoteIpAddress = response.HttpContext.Connection.RemoteIpAddress;
|
|
|
|
_logger.LogDebug("Writing cookie 'sid' to HTTP response for {RemoteAddr}", remoteIpAddress);
|
|
|
|
response.Cookies.Append("sid", Convert.ToBase64String(buffer));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2024-02-20 20:39:52 +00:00
|
|
|
public bool TryGetSession(Guid sessionId, [NotNullWhen(true)] out ISession? session)
|
|
|
|
{
|
2024-02-24 15:27:03 +00:00
|
|
|
using WebContext context = _webContextFactory.CreateDbContext();
|
2024-02-20 20:39:52 +00:00
|
|
|
session = context.Sessions.FirstOrDefault(s => s.Id == sessionId);
|
|
|
|
return session is not null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2024-02-24 15:04:03 +00:00
|
|
|
public bool TryGetSession(HttpRequest request, [NotNullWhen(true)] out ISession? session)
|
2024-02-20 20:39:52 +00:00
|
|
|
{
|
2024-02-25 14:11:33 +00:00
|
|
|
if (request is null)
|
|
|
|
{
|
|
|
|
throw new ArgumentNullException(nameof(request));
|
|
|
|
}
|
2024-02-20 20:39:52 +00:00
|
|
|
|
|
|
|
session = null;
|
|
|
|
IPAddress? remoteIpAddress = request.HttpContext.Connection.RemoteIpAddress;
|
2024-02-25 14:11:33 +00:00
|
|
|
if (remoteIpAddress is null)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2024-02-20 20:39:52 +00:00
|
|
|
|
|
|
|
if (!request.Cookies.TryGetValue("sid", out string? sessionIdCookie))
|
2024-02-25 14:11:33 +00:00
|
|
|
{
|
2024-02-20 20:39:52 +00:00
|
|
|
return false;
|
2024-02-25 14:11:33 +00:00
|
|
|
}
|
2024-02-20 20:39:52 +00:00
|
|
|
|
|
|
|
Span<byte> bytes = stackalloc byte[16];
|
|
|
|
if (!Convert.TryFromBase64Chars(sessionIdCookie, bytes, out int bytesWritten) || bytesWritten < 16)
|
2024-02-25 14:11:33 +00:00
|
|
|
{
|
2024-02-20 20:39:52 +00:00
|
|
|
return false;
|
2024-02-25 14:11:33 +00:00
|
|
|
}
|
2024-02-20 20:39:52 +00:00
|
|
|
|
|
|
|
var sessionId = new Guid(bytes);
|
2024-02-24 15:04:03 +00:00
|
|
|
return TryGetSession(sessionId, out session);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public bool ValidateSession(HttpRequest request, ISession session)
|
|
|
|
{
|
2024-02-25 14:11:33 +00:00
|
|
|
if (request is null)
|
|
|
|
{
|
|
|
|
throw new ArgumentNullException(nameof(request));
|
|
|
|
}
|
2024-02-20 20:39:52 +00:00
|
|
|
|
2024-02-25 14:11:33 +00:00
|
|
|
if (session is null)
|
|
|
|
{
|
|
|
|
throw new ArgumentNullException(nameof(session));
|
|
|
|
}
|
2024-02-24 15:04:03 +00:00
|
|
|
|
2024-02-25 14:11:33 +00:00
|
|
|
IPAddress? remoteIpAddress = request.HttpContext.Connection.RemoteIpAddress;
|
|
|
|
if (remoteIpAddress is null)
|
2024-02-20 20:39:52 +00:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-02-25 14:12:04 +00:00
|
|
|
if (session.Expires <= DateTimeOffset.UtcNow)
|
|
|
|
{
|
|
|
|
_logger.LogInformation("Session {Id} has expired (client {Ip})", session.Id, remoteIpAddress);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-02-20 20:39:52 +00:00
|
|
|
Span<byte> remoteAddressBytes = stackalloc byte[16];
|
|
|
|
Span<byte> sessionAddressBytes = stackalloc byte[16];
|
|
|
|
if (!remoteIpAddress.TryWriteBytes(remoteAddressBytes, out _) ||
|
|
|
|
!session.IpAddress.TryWriteBytes(sessionAddressBytes, out _))
|
|
|
|
return false;
|
|
|
|
|
2024-02-24 15:04:03 +00:00
|
|
|
if (!remoteAddressBytes.SequenceEqual(sessionAddressBytes))
|
2024-02-20 20:39:52 +00:00
|
|
|
return false;
|
|
|
|
|
2024-02-24 15:04:03 +00:00
|
|
|
if (_userService.TryGetUser(session.UserId, out _))
|
2024-02-20 20:39:52 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|