-
-
Notifications
You must be signed in to change notification settings - Fork 13
/
UserApiServices.cs
342 lines (294 loc) · 13.5 KB
/
UserApiServices.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BackendFramework.Helper;
using BackendFramework.Interfaces;
using BackendFramework.Models;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson;
using MongoDB.Driver;
namespace BackendFramework.Services
{
/// <summary> All application logic for <see cref="User"/>s </summary>
public class UserService : IUserService
{
private readonly IUserContext _userDatabase;
private readonly IUserRoleService _userRole;
public UserService(IUserContext collectionSettings, IUserRoleService userRole)
{
_userDatabase = collectionSettings;
_userRole = userRole;
}
/// <summary> Password hashing and validation. </summary>
private static class PasswordHash
{
private const int SaltLength = 16;
/// <summary> Use SHA256 length. </summary>
private const int HashLength = 256 / 8;
/// <summary> Hash iterations to slow down brute force password cracking. </summary>
/// It's important that this value is not too low, or password cracking is made easier.
/// Value selected from default Django 3.1 iteration count (appropriate as of August 2020).
/// https://docs.djangoproject.com/en/dev/releases/3.1/#django-contrib-auth
private const int HashIterations = 216000;
/// <summary>
/// Hash a password with a generated salt and return the combined bytes suitable for storage.
/// </summary>
public static byte[] HashPassword(string password)
{
var salt = CreateSalt();
// Hash the password along with the salt
var hash = HashPassword(password, salt);
// Combine salt and hashed password for storage
var hashBytes = new byte[SaltLength + HashLength];
Array.Copy(salt, 0, hashBytes, 0, SaltLength);
Array.Copy(hash, 0, hashBytes, SaltLength, HashLength);
return hashBytes;
}
/// <summary>
/// Validate that a user-supplied password matches a previously hashed password.
/// </summary>
/// <param name="storedHash"> Stored password hash for a user. </param>
/// <param name="password"> The password that a user supplied to be validated. </param>
public static bool ValidatePassword(byte[] storedHash, string password)
{
// Get the salt from the first part of stored value.
var salt = new byte[SaltLength];
Array.Copy(storedHash, 0, salt, 0, SaltLength);
// Compute the hash on the password the user entered.
var computedHash = HashPassword(password, salt);
// Check if the password given to us matches the hash we have stored (after the salt).
for (var i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i + SaltLength])
{
return false;
}
}
return true;
}
/// <summary> Hash a password and salt using PBKDF2. </summary>
private static byte[] HashPassword(string password, byte[] salt)
{
// SHA256 is the recommended PBKDF2 hash algorithm.
using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, HashIterations, HashAlgorithmName.SHA256);
return pbkdf2.GetBytes(HashLength);
}
/// <summary> Create cryptographically-secure randomized salt. </summary>
private static byte[] CreateSalt()
{
byte[] salt;
using var crypto = new RNGCryptoServiceProvider();
crypto.GetBytes(salt = new byte[SaltLength]);
return salt;
}
}
/// <summary> Confirms login credentials are valid. </summary>
/// <returns> User when credentials are correct, null otherwise. </returns>
public async Task<User> Authenticate(string username, string password)
{
// Fetch the stored user.
var userList = await _userDatabase.Users.FindAsync(x =>
x.Username.ToLowerInvariant() == username.ToLowerInvariant());
var foundUser = userList.FirstOrDefault();
// Return null if user with specified username not found.
if (foundUser == null)
{
return null;
}
// Extract the bytes from encoded password.
var hashedPassword = Convert.FromBase64String(foundUser.Password);
// If authentication is successful, generate jwt token.
return PasswordHash.ValidatePassword(hashedPassword, password) ? await MakeJwt(foundUser) : null;
}
public async Task<User> MakeJwt(User user)
{
const int tokenExpirationMinutes = 60 * 4;
var tokenHandler = new JwtSecurityTokenHandler();
var secretKey = Environment.GetEnvironmentVariable("COMBINE_JWT_SECRET_KEY");
var key = Encoding.ASCII.GetBytes(secretKey);
// Fetch the projects Id and the roles for each Id
var projectPermissionMap = new List<ProjectPermissions>();
foreach (var (projectRoleKey, projectRoleValue) in user.ProjectRoles)
{
// Convert each userRoleId to its respective role and add to the mapping
var permissions = _userRole.GetUserRole(projectRoleKey, projectRoleValue).Result.Permissions;
var validEntry = new ProjectPermissions(projectRoleKey, permissions);
projectPermissionMap.Add(validEntry);
}
var claimString = projectPermissionMap.ToJson();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("UserId", user.Id),
new Claim("UserRoleInfo", claimString)
}),
Expires = DateTime.UtcNow.AddMinutes(tokenExpirationMinutes),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
// Sanitize user to remove password, avatar path, and old token
// Then add updated token.
Sanitize(user);
user.Token = tokenHandler.WriteToken(token);
if (await Update(user.Id, user) != ResultOfUpdate.Updated)
{
return null;
}
return user;
}
/// <summary> Finds all <see cref="User"/>s </summary>
public async Task<List<User>> GetAllUsers()
{
var users = await _userDatabase.Users.Find(_ => true).ToListAsync();
users.ForEach(c => Sanitize(c));
return (users);
}
/// <summary> Removes all <see cref="User"/>s </summary>
/// <returns> A bool: success of operation </returns>
public async Task<bool> DeleteAllUsers()
{
var deleted = await _userDatabase.Users.DeleteManyAsync(_ => true);
return deleted.DeletedCount != 0;
}
/// <summary> Finds <see cref="User"/> with specified userId </summary>
public async Task<User> GetUser(string userId)
{
var filterDef = new FilterDefinitionBuilder<User>();
var filter = filterDef.Eq(x => x.Id, userId);
var userList = await _userDatabase.Users.FindAsync(filter);
var user = userList.FirstOrDefault();
Sanitize(user);
return user;
}
/// <summary> Finds <see cref="User"/> with specified userId and returns avatar filepath </summary>
public async Task<string> GetUserAvatar(string userId)
{
var filterDef = new FilterDefinitionBuilder<User>();
var filter = filterDef.Eq(x => x.Id, userId);
var userList = await _userDatabase.Users.FindAsync(filter);
var user = userList.FirstOrDefault();
return string.IsNullOrEmpty(user?.Avatar) ? null : user.Avatar;
}
/// <summary> Finds <see cref="User"/> with specified userId and changes it's password </summary>
public async Task<ResultOfUpdate> ChangePassword(string userId, string password)
{
var hash = PasswordHash.HashPassword(password);
var filter = Builders<User>.Filter.Eq(x => x.Id, userId);
var updateDef = Builders<User>.Update
.Set(x => x.Password, Convert.ToBase64String(hash));
var updateResult = await _userDatabase.Users.UpdateOneAsync(filter, updateDef);
if (!updateResult.IsAcknowledged)
{
return ResultOfUpdate.NotFound;
}
else if (updateResult.ModifiedCount > 0)
{
return ResultOfUpdate.Updated;
}
else
{
return ResultOfUpdate.NoChange;
}
}
/// <summary> Adds a <see cref="User"/> </summary>
/// <returns> The <see cref="User"/> created, or null if the user could not be created. </returns>
public async Task<User> Create(User user)
{
// Check if collection is not empty
var users = await GetAllUsers();
// Check to see if username or email address is taken
if (users.Count != 0 && _userDatabase.Users.Find(
x => (x.Username.ToLowerInvariant() == user.Username.ToLowerInvariant() ||
x.Email.ToLowerInvariant() == user.Email.ToLowerInvariant())).ToList().Count > 0)
{
return null;
}
var hash = PasswordHash.HashPassword(user.Password);
// Replace password with encoded, hashed password.
user.Password = Convert.ToBase64String(hash);
await _userDatabase.Users.InsertOneAsync(user);
Sanitize(user);
return user;
}
/// <summary> Removes <see cref="User"/> with specified userId </summary>
/// <returns> A bool: success of operation </returns>
public async Task<bool> Delete(string userId)
{
var deleted = await _userDatabase.Users.DeleteOneAsync(x => x.Id == userId);
return deleted.DeletedCount > 0;
}
/// <summary> Removes avatar path, password, and token from <see cref="User"/> </summary>
public void Sanitize(User user)
{
// .Avatar or .Token set to "" or null will not be updated in the database
user.Avatar = null;
user.Password = null;
user.Token = null;
}
/// <summary> Updates <see cref="User"/> with specified userId </summary>
/// <returns> A <see cref="ResultOfUpdate"/> enum: success of operation </returns>
public async Task<ResultOfUpdate> Update(string userId, User user, bool updateIsAdmin = false)
{
var filter = Builders<User>.Filter.Eq(x => x.Id, userId);
// Note: Nulls out values not in update body
var updateDef = Builders<User>.Update
.Set(x => x.HasAvatar, user.HasAvatar)
.Set(x => x.Name, user.Name)
.Set(x => x.Email, user.Email)
.Set(x => x.Phone, user.Phone)
.Set(x => x.OtherConnectionField, user.OtherConnectionField)
.Set(x => x.WorkedProjects, user.WorkedProjects)
.Set(x => x.ProjectRoles, user.ProjectRoles)
.Set(x => x.Agreement, user.Agreement)
.Set(x => x.Username, user.Username)
.Set(x => x.UILang, user.UILang);
// If .Avatar or .Token has been set to null or "",
// this prevents it from being erased in the database
if (!string.IsNullOrEmpty(user.Avatar))
{
updateDef = updateDef.Set(x => x.Avatar, user.Avatar);
}
if (!string.IsNullOrEmpty(user.Token))
{
updateDef = updateDef.Set(x => x.Token, user.Token);
}
// Do not allow updating admin privileges unless explicitly allowed
// (e.g. admin creation CLI).
// This prevents a user from modifying this field and privilege escalating.
if (updateIsAdmin)
{
updateDef = updateDef.Set(x => x.IsAdmin, user.IsAdmin);
}
var updateResult = await _userDatabase.Users.UpdateOneAsync(filter, updateDef);
if (!updateResult.IsAcknowledged)
{
return ResultOfUpdate.NotFound;
}
else if (updateResult.ModifiedCount > 0)
{
return ResultOfUpdate.Updated;
}
else
{
return ResultOfUpdate.NoChange;
}
}
}
public class ProjectPermissions
{
public ProjectPermissions(string projectId, List<int> permissions)
{
ProjectId = projectId;
Permissions = permissions;
}
public string ProjectId { get; set; }
public List<int> Permissions { get; set; }
}
}