profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
IdentityService.cs 9,243 bytes
1 using System.IdentityModel.Tokens.Jwt;
2 using System.Security.Claims;
3 using App.Domain.Contracts;
4 using App.Domain.Identity;
5 using Base.Helpers;
6 using Microsoft.AspNetCore.Identity;
7 using Microsoft.Extensions.Configuration;
8
9 namespace App.BLL.Services.Identity;
10
11 public class IdentityService : IIdentityService
12 {
13 private readonly IAppUnitOfWork _uow;
14 private readonly UserManager<AppUser> _userManager;
15 private readonly IConfiguration _configuration;
16
17 public IdentityService(
18 IAppUnitOfWork uow,
19 UserManager<AppUser> userManager,
20 IConfiguration configuration)
21 {
22 _uow = uow;
23 _userManager = userManager;
24 _configuration = configuration;
25 }
26
27 public async Task<IdentityServiceResult> RegisterAsync(RegisterRequest request)
28 {
29 var expiresInSeconds = ResolveExpiresInSeconds(request.ExpiresInSeconds);
30
31 var existing = await _userManager.FindByEmailAsync(request.Email);
32 if (existing != null)
33 {
34 return IdentityServiceResult.Fail(
35 $"User with email {request.Email} is already registered",
36 IdentityServiceErrorKind.BadRequest);
37 }
38
39 var refreshToken = new AppRefreshToken();
40 var appUser = new AppUser
41 {
42 Email = request.Email,
43 UserName = request.Email,
44 FirstName = request.FirstName,
45 LastName = request.LastName,
46 RefreshTokens = new List<AppRefreshToken> { refreshToken }
47 };
48 refreshToken.AppUser = appUser;
49
50 var createResult = await _userManager.CreateAsync(appUser, request.Password);
51 if (!createResult.Succeeded)
52 {
53 return IdentityServiceResult.Fail(
54 createResult.Errors.First().Description,
55 IdentityServiceErrorKind.BadRequest);
56 }
57
58 await _userManager.AddToRoleAsync(appUser, "user");
59
60 var claimsResult = await _userManager.AddClaimsAsync(appUser, new List<Claim>
61 {
62 new(ClaimTypes.GivenName, appUser.FirstName),
63 new(ClaimTypes.Surname, appUser.LastName)
64 });
65 if (!claimsResult.Succeeded)
66 {
67 return IdentityServiceResult.Fail(
68 claimsResult.Errors.First().Description,
69 IdentityServiceErrorKind.BadRequest);
70 }
71
72 var reloaded = await _userManager.FindByEmailAsync(appUser.Email);
73 if (reloaded == null)
74 {
75 return IdentityServiceResult.Fail(
76 $"User with email {request.Email} is not found after registration",
77 IdentityServiceErrorKind.BadRequest);
78 }
79
80 var jwt = await GenerateJwtAsync(reloaded, expiresInSeconds);
81
82 return IdentityServiceResult.Ok(new IdentityJwtPayload
83 {
84 Jwt = jwt,
85 RefreshToken = refreshToken.RefreshToken,
86 FirstName = reloaded.FirstName,
87 LastName = reloaded.LastName
88 });
89 }
90
91 public async Task<IdentityServiceResult> LoginAsync(LoginRequest request)
92 {
93 var expiresInSeconds = ResolveExpiresInSeconds(request.ExpiresInSeconds);
94
95 var appUser = await _userManager.FindByEmailAsync(request.Email);
96 if (appUser == null)
97 {
98 return IdentityServiceResult.Fail("User/Password problem", IdentityServiceErrorKind.NotFound);
99 }
100
101 var passwordOk = await _userManager.CheckPasswordAsync(appUser, request.Password);
102 if (!passwordOk)
103 {
104 return IdentityServiceResult.Fail("User/Password problem", IdentityServiceErrorKind.NotFound);
105 }
106
107 await _uow.RefreshTokens.RemoveExpiredForUserAsync(appUser.Id);
108
109 var refreshToken = new AppRefreshToken
110 {
111 AppUserId = appUser.Id
112 };
113 _uow.RefreshTokens.Add(refreshToken);
114 await _uow.SaveChangesAsync();
115
116 var jwt = await GenerateJwtAsync(appUser, expiresInSeconds);
117
118 return IdentityServiceResult.Ok(new IdentityJwtPayload
119 {
120 Jwt = jwt,
121 RefreshToken = refreshToken.RefreshToken,
122 FirstName = appUser.FirstName,
123 LastName = appUser.LastName
124 });
125 }
126
127 public async Task<IdentityServiceResult> RefreshTokenAsync(RefreshRequest request)
128 {
129 var expiresInSeconds = ResolveExpiresInSeconds(request.ExpiresInSeconds);
130
131 JwtSecurityToken? jwt;
132 try
133 {
134 jwt = new JwtSecurityTokenHandler().ReadJwtToken(request.Jwt);
135 }
136 catch (Exception)
137 {
138 return IdentityServiceResult.Fail("No token", IdentityServiceErrorKind.BadRequest);
139 }
140
141 if (jwt == null)
142 {
143 return IdentityServiceResult.Fail("No token", IdentityServiceErrorKind.BadRequest);
144 }
145
146 if (!IdentityHelpers.ValidateJWT(
147 request.Jwt,
148 _configuration.GetValue<string>("JWT:Key")!,
149 _configuration.GetValue<string>("JWT:Issuer")!,
150 _configuration.GetValue<string>("JWT:Audience")!))
151 {
152 return IdentityServiceResult.Fail("JWT validation fail", IdentityServiceErrorKind.BadRequest);
153 }
154
155 var userEmail = jwt.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value;
156 if (userEmail == null)
157 {
158 return IdentityServiceResult.Fail("No email in jwt", IdentityServiceErrorKind.BadRequest);
159 }
160
161 var appUser = await _userManager.FindByEmailAsync(userEmail);
162 if (appUser == null)
163 {
164 return IdentityServiceResult.Fail($"User with email {userEmail} not found", IdentityServiceErrorKind.NotFound);
165 }
166
167 var matchingTokens = (await _uow.RefreshTokens
168 .GetUserActiveTokensAsync(appUser.Id, request.RefreshToken)).ToList();
169
170 if (matchingTokens.Count == 0)
171 {
172 return IdentityServiceResult.Fail(
173 "RefreshTokens collection is null or empty - 0",
174 IdentityServiceErrorKind.NotFound);
175 }
176
177 if (matchingTokens.Count != 1)
178 {
179 return IdentityServiceResult.Fail(
180 "More than one valid refresh token found",
181 IdentityServiceErrorKind.NotFound);
182 }
183
184 var refreshToken = matchingTokens.First();
185 if (refreshToken.RefreshToken == request.RefreshToken)
186 {
187 refreshToken.PreviousRefreshToken = refreshToken.RefreshToken;
188 refreshToken.PreviousExpirationDT = DateTime.UtcNow.AddMinutes(1);
189 refreshToken.RefreshToken = Guid.NewGuid().ToString();
190 refreshToken.ExpirationDT = DateTime.UtcNow.AddDays(7);
191 _uow.RefreshTokens.Update(refreshToken);
192 await _uow.SaveChangesAsync();
193 }
194
195 var newJwt = await GenerateJwtAsync(appUser, expiresInSeconds);
196
197 return IdentityServiceResult.Ok(new IdentityJwtPayload
198 {
199 Jwt = newJwt,
200 RefreshToken = refreshToken.RefreshToken,
201 FirstName = appUser.FirstName,
202 LastName = appUser.LastName
203 });
204 }
205
206 public async Task<IdentityServiceResult> LogoutAsync(LogoutRequest request)
207 {
208 var appUser = await _uow.Users.GetByIdAsync(request.UserId);
209 if (appUser == null)
210 {
211 return IdentityServiceResult.Fail("User/Password problem", IdentityServiceErrorKind.NotFound);
212 }
213
214 var tokens = (await _uow.RefreshTokens
215 .GetUserTokensByValueAsync(request.UserId, request.RefreshToken)).ToList();
216
217 foreach (var token in tokens)
218 {
219 _uow.RefreshTokens.Remove(token);
220 }
221
222 var deleteCount = await _uow.SaveChangesAsync();
223 return IdentityServiceResult.Logout(deleteCount);
224 }
225
226 private int ResolveExpiresInSeconds(int requested)
227 {
228 if (requested <= 0) requested = int.MaxValue;
229 var configured = _configuration.GetValue<int>("JWT:ExpiresInSeconds");
230 return requested < configured ? requested : configured;
231 }
232
233 private async Task<string> GenerateJwtAsync(AppUser user, int expiresInSeconds)
234 {
235 var claims = new List<Claim>
236 {
237 new(ClaimTypes.NameIdentifier, user.Id.ToString()),
238 new(ClaimTypes.Email, user.Email ?? ""),
239 new(ClaimTypes.Name, user.UserName ?? user.Email ?? ""),
240 new(ClaimTypes.GivenName, user.FirstName),
241 new(ClaimTypes.Surname, user.LastName)
242 };
243
244 var userClaims = await _userManager.GetClaimsAsync(user);
245 foreach (var c in userClaims)
246 {
247 if (!claims.Any(existing => existing.Type == c.Type && existing.Value == c.Value))
248 {
249 claims.Add(c);
250 }
251 }
252
253 var roles = await _userManager.GetRolesAsync(user);
254 foreach (var role in roles)
255 {
256 claims.Add(new Claim(ClaimTypes.Role, role));
257 }
258
259 return IdentityHelpers.GenerateJwt(
260 claims,
261 _configuration.GetValue<string>("JWT:Key")!,
262 _configuration.GetValue<string>("JWT:Issuer")!,
263 _configuration.GetValue<string>("JWT:Audience")!,
264 expiresInSeconds);
265 }
266 }
267