196 lines
6.7 KiB
C#
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);
|
|
}
|
|
}
|