-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAccountRecoveryAdministrationService.cs
More file actions
121 lines (108 loc) · 5.32 KB
/
Copy pathAccountRecoveryAdministrationService.cs
File metadata and controls
121 lines (108 loc) · 5.32 KB
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
using System.Diagnostics.CodeAnalysis;
namespace Ashlar.Identity.Features.Administration;
internal sealed class AccountRecoveryAdministrationService(
IUserAdministrationReader userAdministrationReader,
IRememberedMfaDeviceRepository rememberedMfaDeviceRepository,
TimeProvider? timeProvider = null)
: IAccountRecoveryAdministrationService
{
internal const string LastPrimarySignInMethodWarningCode = "last_primary_sign_in_method";
private readonly IUserAdministrationReader _userAdministrationReader = userAdministrationReader ?? throw new ArgumentNullException(nameof(userAdministrationReader));
private readonly IRememberedMfaDeviceRepository _rememberedMfaDeviceRepository = rememberedMfaDeviceRepository ?? throw new ArgumentNullException(nameof(rememberedMfaDeviceRepository));
private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System;
public async Task<Result<AccountRecoveryOptions>> GetAccountRecoveryOptionsAsync(
AccountRecoveryOptionsRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
if (!TryValidateRequest(request, out var validationFailure))
{
return validationFailure;
}
var detailResult = await _userAdministrationReader.GetUserDetailAsync(
request.Actor!,
new UserAdministrationDetailRequest(
request.UserId,
request.Tenant,
request.IncludeAllTenants,
request.RecentSecurityEventWindow),
cancellationToken);
if (!detailResult.TryGetValue(out var detail))
{
return Result.Failure<AccountRecoveryOptions>(detailResult.GetFailure());
}
var rememberedMfaDeviceCount = await _rememberedMfaDeviceRepository.CountForUserAsync(
request.UserId,
request.IncludeAllTenants ? null : request.Tenant,
activeOnly: true,
_timeProvider.GetUtcNow(),
cancellationToken);
var posture = detail.SecurityPosture;
var activeCredentials = posture.CredentialInventory
.Where(credential => credential.Status == CredentialStatus.Active)
.ToArray();
var primaryCredentials = posture.PrimaryCredentials
.Where(credential => credential.Status == CredentialStatus.Active && credential.IsAvailable)
.ToArray();
var revocableProviderOptions = activeCredentials
.Where(credential => credential.IsRevocable)
.GroupBy(credential => credential.Provider)
.Select(group => CreateProviderOption(group, primaryCredentials))
.OrderBy(option => option.Provider.Type.Value, StringComparer.Ordinal)
.ThenBy(option => option.Provider.Name, StringComparer.Ordinal)
.ToArray();
var warnings = revocableProviderOptions
.Where(option => option.WouldRemoveLastPrimarySignInMethod)
.Select(option => new AccountRecoveryWarning(
LastPrimarySignInMethodWarningCode,
"Revoking this provider may remove the last primary sign-in method.",
option.Provider))
.ToArray();
return Result.Success(new AccountRecoveryOptions(
detail,
new AccountRecoveryActionOptions(
WouldResetMfa: rememberedMfaDeviceCount > 0 || activeCredentials.Any(credential => credential.IsResettable),
WouldRevokeSessions: posture.ActiveSessionCount > 0,
revocableProviderOptions,
warnings)));
}
private static AccountRecoveryProviderOption CreateProviderOption(
IGrouping<AuthenticationProviderKey, CredentialPostureItem> group,
CredentialPostureItem[] primaryCredentials)
{
var credentials = group.ToArray();
var primaryCredentialCount = credentials.Count(credential => credential.IsPrimaryCredential);
return new AccountRecoveryProviderOption(
group.Key,
SelectDisplayName(credentials, group.Key),
credentials.Length,
primaryCredentialCount,
credentials.Count(credential => credential.IsAdditionalVerificationFactor),
primaryCredentialCount > 0
&& primaryCredentials.Length > 0
&& primaryCredentials.All(credential => credential.Provider == group.Key));
}
private static string SelectDisplayName(IEnumerable<CredentialPostureItem> credentials, AuthenticationProviderKey provider)
{
var displayName = credentials
.Select(credential => credential.DisplayName)
.Where(displayName => !string.IsNullOrWhiteSpace(displayName))
.Order(StringComparer.Ordinal)
.FirstOrDefault();
return displayName ?? provider.Name;
}
private static bool TryValidateRequest(AccountRecoveryOptionsRequest request, [NotNullWhen(false)] out Result<AccountRecoveryOptions>? failure)
{
try
{
AccountRecoveryOptionsRequest.ThrowIfInvalid(request);
failure = null;
return true;
}
catch (ArgumentException exception)
{
failure = Result.Failure<AccountRecoveryOptions>(AshlarFailureCodes.ValidationError, exception.Message);
return false;
}
}
}