1using System.IdentityModel.Tokens.Jwt;
2using System.Security.Claims;
3using System.Security.Cryptography;
4using System.Text;
5using Den.Application.Auth;
6using Den.Domain.Entities;
7using Den.Infrastructure.Persistence;
8
9using Microsoft.EntityFrameworkCore;
10using Microsoft.Extensions.Options;
11using Microsoft.IdentityModel.Tokens;
12
13namespace Den.Infrastructure.Auth;
14
15public class AuthService(
16 DenDbContext context,
17 IOptions<JwtSettings> jwtOptions
18) : IAuthService
19{
20 private readonly JwtSettings _jwtSettings = jwtOptions.Value;
21 public async Task<AuthResponse> SignupAsync(SignupRequest request)
22 {
23 if (await context.Users.AnyAsync(u => u.Email == request.Email))
24 {
25 throw new InvalidOperationException("email already in use");
26 }
27
28 if (await context.Users.AnyAsync(u => u.Username == request.Username))
29 {
30 throw new InvalidOperationException("username already taken");
31 }
32
33 var passwordHash = HashPassword(request.Password);
34
35 var user = new User
36 {
37 Id = Guid.NewGuid(),
38 Username = request.Username,
39 DisplayName = request.DisplayName,
40 Email = request.Email,
41 PasswordHash = passwordHash,
42 Role = UserRole.VIEWER
43 };
44
45 context.Users.Add(user);
46 await context.SaveChangesAsync();
47
48 var (accessToken, refreshToken, session) = await GenerateTokenPair(user);
49 return new AuthResponse(accessToken, refreshToken, session.Expiry);
50 }
51
52 public async Task<AuthResponse?> LoginAsync(LoginRequest request)
53 {
54 var user = await context.Users.FirstOrDefaultAsync(u => u.Username == request.Username);
55 if (user is null || !VerifyPassword(request.Password, user.PasswordHash))
56 {
57 return null;
58 }
59
60 var (accessToken, refreshToken, session) = await GenerateTokenPair(user);
61 return new AuthResponse(accessToken, refreshToken, session.Expiry);
62 }
63
64 public async Task<RefreshResponse?> RefreshAsync(RefreshRequest request)
65 {
66 var session = await context.Sessions
67 .Include(s => s.User)
68 .FirstOrDefaultAsync(s => s.RefreshTokenHash == HashRefreshToken(request.RefreshToken));
69 if (session is null)
70 {
71 return null;
72 }
73
74 if (session.Expiry <= DateTime.UtcNow)
75 {
76 return null;
77 }
78
79 var accessToken = await GenerateAccessToken(session.User);
80 return new RefreshResponse(accessToken);
81 }
82
83 private static string HashPassword(string password)
84 {
85 return BCrypt.Net.BCrypt.HashPassword(password);
86 }
87
88 private static bool VerifyPassword(string password, string hash)
89 {
90 return BCrypt.Net.BCrypt.Verify(password, hash);
91 }
92
93 private async Task<(string AccessToken, string RefreshToken, Session Session)> GenerateTokenPair(User user)
94 {
95 var tokenExpiry = DateTime.UtcNow.AddDays(7);
96 var refreshToken = GenerateRefreshToken();
97
98 var session = new Session {
99 Id = Guid.NewGuid(),
100 RefreshTokenHash = HashRefreshToken(refreshToken),
101 Expiry = tokenExpiry,
102 UserId = user.Id,
103 };
104 context.Sessions.Add(session);
105 await context.SaveChangesAsync();
106
107 return (await GenerateAccessToken(user), refreshToken, session);
108 }
109
110 private async Task<string> GenerateAccessToken(User user)
111 {
112 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret));
113 var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
114
115 var now = DateTime.UtcNow;
116 var accessToken = new JwtSecurityToken(
117 issuer: _jwtSettings.Issuer,
118 audience: _jwtSettings.Audience,
119 claims: [
120 new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
121 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
122 new Claim(ClaimTypes.Role, user.Role.ToString()),
123 ],
124 expires: now.AddMinutes(5),
125 signingCredentials: creds,
126 notBefore: now
127 );
128
129 return new JwtSecurityTokenHandler().WriteToken(accessToken);
130 }
131
132 private static string GenerateRefreshToken()
133 {
134 var randomBytes = new byte[32];
135 using var rng = RandomNumberGenerator.Create();
136 rng.GetBytes(randomBytes);
137 return Convert.ToBase64String(randomBytes);
138 }
139
140 public static string HashRefreshToken(string token)
141 {
142 var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
143 return Convert.ToBase64String(hashBytes);
144 }
145
146 public static bool VerifyRefreshToken(string a, string b)
147 {
148 return CryptographicOperations.FixedTimeEquals(
149 Encoding.UTF8.GetBytes(a),
150 Encoding.UTF8.GetBytes(b)
151 );
152 }
153}