profileShare

rasmusjy / splitapp-backend-microservices

Read-only snapshot

No repository description.

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