profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

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