Den is the private cloud vault for your reminders, calendars and to-dos.
at main 153 lines 4.9 kB view raw
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}