Files
Swarm/DysonNetwork.Pass/Features/Auth/Services/AuthenticationService.cs

196 lines
6.7 KiB
C#

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using DysonNetwork.Common.Models;
using DysonNetwork.Pass.Data;
using DysonNetwork.Pass.Features.Auth.Interfaces;
using DysonNetwork.Pass.Features.Auth.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using NodaTime;
namespace DysonNetwork.Pass.Features.Auth.Services;
public class AuthenticationService : IAuthenticationService
{
private readonly PassDatabase _db;
private readonly IConfiguration _configuration;
private readonly ISessionService _sessionService;
private readonly IOidcService _oidcService;
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthenticationService(
PassDatabase db,
IConfiguration configuration,
ISessionService sessionService,
IOidcService oidcService,
IHttpContextAccessor httpContextAccessor)
{
_db = db;
_configuration = configuration;
_sessionService = sessionService;
_oidcService = oidcService;
_httpContextAccessor = httpContextAccessor;
}
public async Task<AuthResult> AuthenticateAsync(string username, string password)
{
// First try to find by username (Name in the Account model)
var account = await _db.Accounts
.Include(a => a.Profile) // Include Profile for email lookup
.FirstOrDefaultAsync(a => a.Name == username);
// If not found by username, try to find by email in the Profile
if (account == null)
{
account = await _db.Accounts
.Include(a => a.Profile)
.FirstOrDefaultAsync(a => a.Profile != null && a.Profile.Email == username);
}
if (account == null || !await VerifyPasswordAsync(account, password))
{
return new AuthResult { Success = false, Error = "Invalid username/email or password" };
}
return await CreateAuthResult(account);
}
private async Task<bool> VerifyPasswordAsync(Account account, string password)
{
// Find password auth factor for the account
var passwordFactor = await _db.AccountAuthFactors
.FirstOrDefaultAsync(f => f.AccountId == account.Id && f.FactorType == AccountAuthFactorType.Password);
if (passwordFactor == null)
return false;
return BCrypt.Net.BCrypt.Verify(password, passwordFactor.Secret);
}
public async Task<AuthResult> AuthenticateWithOidcAsync(string provider, string code, string state)
{
return await _oidcService.AuthenticateAsync(provider, code, state);
}
public async Task<AuthResult> RefreshTokenAsync(string refreshToken)
{
var session = await _db.AuthSessions
.FirstOrDefaultAsync(s => s.RefreshToken == refreshToken && !s.IsRevoked);
if (session == null || session.RefreshTokenExpiryTime <= SystemClock.Instance.GetCurrentInstant())
{
return new AuthResult { Success = false, Error = "Invalid or expired refresh token" };
}
var account = await _db.Accounts.FindAsync(session.AccountId);
if (account == null)
{
return new AuthResult { Success = false, Error = "Account not found" };
}
// Invalidate the old session
await _sessionService.InvalidateSessionAsync(session.Id);
// Create a new session
return await CreateAuthResult(account);
}
public async Task<bool> ValidateTokenAsync(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]!);
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero,
ValidIssuer = _configuration["Jwt:Issuer"]
}, out _);
return true;
}
catch
{
return false;
}
}
public async Task LogoutAsync(Guid sessionId)
{
await _sessionService.InvalidateSessionAsync(sessionId);
}
public async Task<bool> ValidateSessionAsync(Guid sessionId)
{
return await _sessionService.ValidateSessionAsync(sessionId);
}
public async Task<AuthSession> GetSessionAsync(Guid sessionId)
{
var session = await _sessionService.GetSessionAsync(sessionId);
if (session == null)
throw new Exception("Session not found");
return session;
}
private async Task<AuthResult> CreateAuthResult(Account account)
{
var ipAddress = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
var userAgent = _httpContextAccessor.HttpContext?.Request.Headers.UserAgent.ToString() ?? string.Empty;
var session = await _sessionService.CreateSessionAsync(account.Id, ipAddress, userAgent);
var token = GenerateJwtToken(account, session.Id);
return new AuthResult
{
Success = true,
AccessToken = token,
RefreshToken = session.RefreshToken,
Session = session
};
}
private string GenerateJwtToken(Account account, Guid sessionId)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]!);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, account.Id.ToString()),
new Claim(ClaimTypes.Name, account.Username),
new Claim("session_id", sessionId.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature
),
Issuer = _configuration["Jwt:Issuer"],
Audience = _configuration["Jwt:Audience"]
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
private bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
using var hmac = new HMACSHA512(storedSalt);
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
return computedHash.SequenceEqual(storedHash);
}
}