-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathAuthRequestService.cs
330 lines (288 loc) · 13.4 KB
/
AuthRequestService.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
using System.Diagnostics;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Exceptions;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
#nullable enable
namespace Bit.Core.Auth.Services.Implementations;
public class AuthRequestService : IAuthRequestService
{
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IUserRepository _userRepository;
private readonly IGlobalSettings _globalSettings;
private readonly IDeviceRepository _deviceRepository;
private readonly ICurrentContext _currentContext;
private readonly IPushNotificationService _pushNotificationService;
private readonly IEventService _eventService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IMailService _mailService;
private readonly IFeatureService _featureService;
private readonly ILogger<AuthRequestService> _logger;
public AuthRequestService(
IAuthRequestRepository authRequestRepository,
IUserRepository userRepository,
IGlobalSettings globalSettings,
IDeviceRepository deviceRepository,
ICurrentContext currentContext,
IPushNotificationService pushNotificationService,
IEventService eventService,
IOrganizationUserRepository organizationRepository,
IMailService mailService,
IFeatureService featureService,
ILogger<AuthRequestService> logger)
{
_authRequestRepository = authRequestRepository;
_userRepository = userRepository;
_globalSettings = globalSettings;
_deviceRepository = deviceRepository;
_currentContext = currentContext;
_pushNotificationService = pushNotificationService;
_eventService = eventService;
_organizationUserRepository = organizationRepository;
_mailService = mailService;
_featureService = featureService;
_logger = logger;
}
public async Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId)
{
var authRequest = await _authRequestRepository.GetByIdAsync(id);
if (authRequest == null || authRequest.UserId != userId)
{
return null;
}
return authRequest;
}
public async Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid id, string code)
{
var authRequest = await _authRequestRepository.GetByIdAsync(id);
if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code))
{
return null;
}
if (!IsAuthRequestValid(authRequest))
{
return null;
}
return authRequest;
}
/// <summary>
/// Validates and Creates an <see cref="AuthRequest" /> in the database, as well as pushes it through notifications services
/// </summary>
/// <remarks>
/// This method can only be called inside of an HTTP call because of it's reliance on <see cref="ICurrentContext" />
/// </remarks>
public async Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model)
{
if (!_currentContext.DeviceType.HasValue)
{
throw new BadRequestException("Device type not provided.");
}
var userNotFound = false;
var user = await _userRepository.GetByEmailAsync(model.Email);
if (user == null)
{
userNotFound = true;
}
else if (_globalSettings.PasswordlessAuth.KnownDevicesOnly)
{
var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);
if (devices == null || !devices.Any(d => d.Identifier == model.DeviceIdentifier))
{
userNotFound = true;
}
}
// Anonymous endpoints must not leak that a user exists or not
if (userNotFound)
{
throw new BadRequestException("User or known device not found.");
}
// AdminApproval requests require correlating the user and their organization
if (model.Type == AuthRequestType.AdminApproval)
{
// TODO: When single org policy is turned on we should query for only a single organization from the current user
// and create only an AuthRequest for that organization and return only that one
// This will send out the request to all organizations this user belongs to
var organizationUsers = await _organizationUserRepository.GetManyByUserAsync(_currentContext.UserId!.Value);
if (organizationUsers.Count == 0)
{
throw new BadRequestException("User does not belong to any organizations.");
}
Debug.Assert(user is not null, "user should have been validated to be non-null and thrown if it's not.");
// A user event will automatically create logs for each organization/provider this user belongs to.
await _eventService.LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);
AuthRequest? firstAuthRequest = null;
foreach (var organizationUser in organizationUsers)
{
var createdAuthRequest = await CreateAuthRequestAsync(model, user, organizationUser.OrganizationId);
firstAuthRequest ??= createdAuthRequest;
await NotifyAdminsOfDeviceApprovalRequestAsync(organizationUser, user);
}
// I know this won't be null because I have already validated that at least one organization exists
return firstAuthRequest!;
}
Debug.Assert(user is not null, "user should have been validated to be non-null and thrown if it's not.");
var authRequest = await CreateAuthRequestAsync(model, user, organizationId: null);
await _pushNotificationService.PushAuthRequestAsync(authRequest);
return authRequest;
}
private async Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model, User user, Guid? organizationId)
{
Debug.Assert(_currentContext.DeviceType.HasValue, "DeviceType should have already been validated to have a value.");
var authRequest = new AuthRequest
{
RequestDeviceIdentifier = model.DeviceIdentifier,
RequestDeviceType = _currentContext.DeviceType.Value,
RequestIpAddress = _currentContext.IpAddress,
AccessCode = model.AccessCode,
PublicKey = model.PublicKey,
UserId = user.Id,
Type = model.Type.GetValueOrDefault(),
OrganizationId = organizationId,
};
authRequest = await _authRequestRepository.CreateAsync(authRequest);
return authRequest;
}
public async Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid currentUserId, AuthRequestUpdateRequestModel model)
{
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
if (authRequest == null)
{
throw new NotFoundException();
}
// Once Approval/Disapproval has been set, this AuthRequest should not be updated again.
if (authRequest.Approved is not null)
{
throw new DuplicateAuthRequestException();
}
// Do type specific validation
switch (authRequest.Type)
{
case AuthRequestType.AdminApproval:
// AdminApproval has a different expiration time, by default is 7 days compared to
// non-AdminApproval ones having a default of 15 minutes.
if (IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.AdminRequestExpiration))
{
throw new NotFoundException();
}
break;
case AuthRequestType.AuthenticateAndUnlock:
case AuthRequestType.Unlock:
if (IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.UserRequestExpiration))
{
throw new NotFoundException();
}
if (authRequest.UserId != currentUserId)
{
throw new NotFoundException();
}
// Admin approval responses are not tied to a specific device, but these types are so we need to validate them
var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier, currentUserId);
if (device == null)
{
throw new BadRequestException("Invalid device.");
}
authRequest.ResponseDeviceId = device.Id;
break;
}
authRequest.ResponseDate = DateTime.UtcNow;
authRequest.Approved = model.RequestApproved;
if (model.RequestApproved)
{
authRequest.Key = model.Key;
authRequest.MasterPasswordHash = model.MasterPasswordHash;
}
await _authRequestRepository.ReplaceAsync(authRequest);
// We only want to send an approval notification if the request is approved (or null),
// to not leak that it was denied to the originating client if it was originated by a malicious actor.
if (authRequest.Approved ?? true)
{
if (authRequest.OrganizationId.HasValue)
{
var organizationUser = await _organizationUserRepository
.GetByOrganizationAsync(authRequest.OrganizationId.Value, authRequest.UserId);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_ApprovedAuthRequest);
}
// No matter what we want to push out the success notification
await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);
}
// If the request is rejected by an organization admin then we want to log an event of that action
else if (authRequest.Approved.HasValue && !authRequest.Approved.Value && authRequest.OrganizationId.HasValue)
{
var organizationUser = await _organizationUserRepository
.GetByOrganizationAsync(authRequest.OrganizationId.Value, authRequest.UserId);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_RejectedAuthRequest);
}
return authRequest;
}
private bool IsAuthRequestValid(AuthRequest authRequest)
{
return authRequest.Type switch
{
AuthRequestType.AuthenticateAndUnlock or AuthRequestType.Unlock
=> !IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.UserRequestExpiration),
AuthRequestType.AdminApproval => IsAdminApprovalAuthRequestValid(authRequest),
_ => false,
};
}
private bool IsAdminApprovalAuthRequestValid(AuthRequest authRequest)
{
Debug.Assert(authRequest.Type == AuthRequestType.AdminApproval, "This method should only be called on AdminApproval type");
// If an AdminApproval type has been approved it's expiration time is based on how long it's been since approved.
if (authRequest.Approved is true)
{
Debug.Assert(authRequest.ResponseDate.HasValue, "The response date should have been set when the request was updated.");
return !IsDateExpired(authRequest.ResponseDate.Value, _globalSettings.PasswordlessAuth.AfterAdminApprovalExpiration);
}
else
{
return !IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.AdminRequestExpiration);
}
}
private static bool IsDateExpired(DateTime savedDate, TimeSpan allowedLifetime)
{
return DateTime.UtcNow > savedDate.Add(allowedLifetime);
}
private async Task NotifyAdminsOfDeviceApprovalRequestAsync(OrganizationUser organizationUser, User user)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.DeviceApprovalRequestAdminNotifications))
{
_logger.LogWarning("Skipped sending device approval notification to admins - feature flag disabled");
return;
}
var adminEmails = await GetAdminAndAccountRecoveryEmailsAsync(organizationUser.OrganizationId);
await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(
adminEmails,
organizationUser.OrganizationId,
user.Email,
user.Name);
}
/// <summary>
/// Returns a list of emails for admins and custom users with the ManageResetPassword permission.
/// </summary>
/// <param name="organizationId">The organization to search within</param>
private async Task<List<string>> GetAdminAndAccountRecoveryEmailsAsync(Guid organizationId)
{
var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(
organizationId,
OrganizationUserType.Admin);
var customUsers = await _organizationUserRepository.GetManyDetailsByRoleAsync(
organizationId,
OrganizationUserType.Custom);
return admins.Select(a => a.Email)
.Concat(customUsers
.Where(a => a.GetPermissions().ManageResetPassword)
.Select(a => a.Email))
.Distinct()
.ToList();
}
}