-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Auth/pm 17129/login with 2fa recovery code #5383
base: main
Are you sure you want to change the base?
Changes from 15 commits
8d92120
f94f20b
2716dca
8454e6f
020ce0f
b966da7
e0b95b5
7bddf6d
e9a0894
9bca4c4
f69c5e9
9602ed2
8035127
22e6469
831f111
3fc2c62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,4 +10,5 @@ public enum TwoFactorProviderType : byte | |
Remember = 5, | ||
OrganizationDuo = 6, | ||
WebAuthn = 7, | ||
RecoveryCode = 8, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
๏ปฟusing Bit.Core.Entities; | ||
using Microsoft.AspNetCore.Identity; | ||
|
||
namespace Bit.Core.Auth.Identity.TokenProviders; | ||
|
||
/// <summary> | ||
/// We are repurposing the TwoFactorTokenProvider workflow to handle Recovery Codes | ||
/// being sent in because it is the easiest way identified to handle allowing a user | ||
/// to successfully go through the login process while providing their Recovery Code. | ||
/// | ||
/// Originally, submitting your recovery code would land you on the login screen, | ||
/// but with the approach of using a TwoFactorTokenProvider we don't have to embed | ||
/// logic jankily in other parts of the recovery code process to get them logged in, | ||
/// we can treat Recovery Codes as just another 2FA method. This means that some of | ||
/// the functionality will not be needed in the same way it's needed for other 2FA | ||
/// methods. | ||
/// </summary> | ||
public class RecoveryCodeTokenProvider : IUserTwoFactorTokenProvider<User> | ||
{ | ||
/// <summary> | ||
/// Hijack the can generate two factor token to repurpose it to check | ||
/// if the user has a two factor recovery code on their account. | ||
/// </summary> | ||
public virtual Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user) | ||
{ | ||
return Task.FromResult(!string.IsNullOrEmpty(user.TwoFactorRecoveryCode)); | ||
} | ||
|
||
/// <summary> | ||
/// This function shouldn't get called because we are not using recovery codes | ||
/// the typical 2FA flow. | ||
/// </summary> | ||
public virtual async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user) | ||
Check warning on line 33 in src/Core/Auth/Identity/TokenProviders/RecoveryCodeTokenProvider.cs
|
||
{ | ||
throw new Exception("This should not have been called."); | ||
} | ||
|
||
public Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user) | ||
{ | ||
var processedToken = token.Replace(" ", string.Empty).ToLower(); | ||
return Task.FromResult(string.Equals(processedToken, user.TwoFactorRecoveryCode)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,6 @@ public interface IUserService | |
Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash); | ||
Task SendMasterPasswordHintAsync(string email); | ||
Task SendTwoFactorEmailAsync(User user); | ||
Task<bool> VerifyTwoFactorEmailAsync(User user, string token); | ||
Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user); | ||
Task<bool> DeleteWebAuthnKeyAsync(User user, int id); | ||
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); | ||
|
@@ -41,8 +40,6 @@ Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string new | |
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash); | ||
Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true); | ||
Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type); | ||
Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); | ||
Task<string> GenerateUserTokenAsync(User user, string tokenProvider, string purpose); | ||
Task<IdentityResult> DeleteAsync(User user); | ||
Task<IdentityResult> DeleteAsync(User user, string token); | ||
Task SendDeleteConfirmationAsync(string email); | ||
|
@@ -55,9 +52,7 @@ Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken, | |
Task CancelPremiumAsync(User user, bool? endOfPeriod = null); | ||
Task ReinstatePremiumAsync(User user); | ||
Task EnablePremiumAsync(Guid userId, DateTime? expirationDate); | ||
Task EnablePremiumAsync(User user, DateTime? expirationDate); | ||
Task DisablePremiumAsync(Guid userId, DateTime? expirationDate); | ||
Task DisablePremiumAsync(User user, DateTime? expirationDate); | ||
Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate); | ||
Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null, | ||
int? version = null); | ||
|
@@ -91,6 +86,17 @@ Task<IdentityResult> UpdatePasswordHash(User user, string newPassword, | |
|
||
void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true); | ||
|
||
[Obsolete("To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.")] | ||
Task<bool> RecoverTwoFactorAsync(string email, string secret, string recoveryCode); | ||
|
||
/// <summary> | ||
/// This method removes the two factor providers on a user and regenerates | ||
/// a new recovery code. Also removes policies on a user for when they lose | ||
/// their 2fa status, they need to comply with their organizations policies. | ||
/// </summary> | ||
/// <param name="user">The user to refresh the 2FA and Recovery Code on.</param> | ||
Task RefreshUser2FaAndRecoveryCodeAsync(User user); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ก : I think this might be a good candidate for a command since it spans multiple domains: User modification and Organization modification. |
||
|
||
/// <summary> | ||
/// Returns true if the user is a legacy user. Legacy users use their master key as their encryption key. | ||
/// We force these users to the web to migrate their encryption scheme. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,7 +77,7 @@ public BaseRequestValidator( | |
protected async Task ValidateAsync(T context, ValidatedTokenRequest request, | ||
CustomValidatorRequestContext validatorContext) | ||
{ | ||
// 1. we need to check if the user is a bot and if their master password hash is correct | ||
// 1. We need to check if the user is a bot and if their master password hash is correct. | ||
var isBot = validatorContext.CaptchaResponse?.IsBot ?? false; | ||
var valid = await ValidateContextAsync(context, validatorContext); | ||
var user = validatorContext.User; | ||
|
@@ -99,7 +99,7 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, | |
return; | ||
} | ||
|
||
// 2. Does this user belong to an organization that requires SSO | ||
// 2. Decide if this user belong to an organization that requires SSO. | ||
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType); | ||
if (validatorContext.SsoRequired) | ||
{ | ||
|
@@ -111,17 +111,21 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, | |
return; | ||
} | ||
|
||
// 3. Check if 2FA is required | ||
(validatorContext.TwoFactorRequired, var twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request); | ||
// This flag is used to determine if the user wants a rememberMe token sent when authentication is successful | ||
// 3. Check if 2FA is required. | ||
(validatorContext.TwoFactorRequired, var twoFactorOrganization) = | ||
await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request); | ||
|
||
// This flag is used to determine if the user wants a rememberMe token sent when | ||
// authentication is successful. | ||
var returnRememberMeToken = false; | ||
if (validatorContext.TwoFactorRequired) | ||
{ | ||
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString(); | ||
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); | ||
var twoFactorToken = request.Raw["TwoFactorToken"]; | ||
var twoFactorProvider = request.Raw["TwoFactorProvider"]; | ||
var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && | ||
!string.IsNullOrWhiteSpace(twoFactorProvider); | ||
// response for 2FA required and not provided state | ||
|
||
// 3a. Response for 2FA required and not provided state. | ||
if (!validTwoFactorRequest || | ||
!Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) | ||
{ | ||
|
@@ -133,26 +137,27 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, | |
return; | ||
} | ||
|
||
// Include Master Password Policy in 2FA response | ||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); | ||
// Include Master Password Policy in 2FA response. | ||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); | ||
SetTwoFactorResult(context, resultDict); | ||
return; | ||
} | ||
|
||
var twoFactorTokenValid = await _twoFactorAuthenticationValidator | ||
.VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); | ||
var twoFactorTokenValid = | ||
await _twoFactorAuthenticationValidator | ||
.VerifyTwoFactorAsync(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); | ||
|
||
// response for 2FA required but request is not valid or remember token expired state | ||
// 3b. Response for 2FA required but request is not valid or remember token expired state. | ||
if (!twoFactorTokenValid) | ||
{ | ||
// The remember me token has expired | ||
// The remember me token has expired. | ||
if (twoFactorProviderType == TwoFactorProviderType.Remember) | ||
{ | ||
var resultDict = await _twoFactorAuthenticationValidator | ||
.BuildTwoFactorResultAsync(user, twoFactorOrganization); | ||
|
||
// Include Master Password Policy in 2FA response | ||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); | ||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); | ||
SetTwoFactorResult(context, resultDict); | ||
} | ||
else | ||
|
@@ -163,17 +168,30 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, | |
return; | ||
} | ||
|
||
// When the two factor authentication is successful, we can check if the user wants a rememberMe token | ||
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; | ||
if (twoFactorRemember // Check if the user wants a rememberMe token | ||
&& twoFactorTokenValid // Make sure two factor authentication was successful | ||
&& twoFactorProviderType != TwoFactorProviderType.Remember) // if the two factor auth was rememberMe do not send another token | ||
// 3c. When the 2FA authentication is successful, we can check if the user wants a | ||
// rememberMe token. | ||
var twoFactorRemember = request.Raw["TwoFactorRemember"] == "1"; | ||
if (twoFactorRemember // Check if the user wants a rememberMe token. | ||
&& twoFactorProviderType != TwoFactorProviderType.Remember) // if the 2FA auth was rememberMe do not send another token. | ||
{ | ||
returnRememberMeToken = true; | ||
} | ||
|
||
// 3d. If the user is logging in using a RecoveryCode type to login | ||
// as their second factor we know we are in the flow where we need | ||
// to disable their other 2FAs just like as if they were using | ||
// their recovery code because they no longer have access to their | ||
// 2FA device. | ||
if (FeatureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ : I think I would like this to live in the I think it should live in the |
||
{ | ||
if (twoFactorProviderType == TwoFactorProviderType.RecoveryCode) | ||
{ | ||
await _userService.RefreshUser2FaAndRecoveryCodeAsync(user); | ||
} | ||
} | ||
} | ||
|
||
// 4. Check if the user is logging in from a new device | ||
// 4. Check if the user is logging in from a new device. | ||
var deviceValid = await _deviceValidator.ValidateRequestDeviceAsync(request, validatorContext); | ||
if (!deviceValid) | ||
{ | ||
|
@@ -182,7 +200,7 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, | |
return; | ||
} | ||
|
||
// 5. Force legacy users to the web for migration | ||
// 5. Force legacy users to the web for migration. | ||
if (UserService.IsLegacyUser(user) && request.ClientId != "web") | ||
{ | ||
await FailAuthForLegacyUserAsync(user, context); | ||
|
@@ -223,7 +241,7 @@ protected async Task BuildSuccessResultAsync(User user, T context, Device device | |
customResponse.Add("Key", user.Key); | ||
} | ||
|
||
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); | ||
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); | ||
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); | ||
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); | ||
customResponse.Add("Kdf", (byte)user.Kdf); | ||
|
@@ -402,7 +420,7 @@ private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user) | |
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling; | ||
} | ||
|
||
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicy(User user) | ||
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user) | ||
{ | ||
// Check current context/cache to see if user is in any organizations, avoids extra DB call if not | ||
var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ญ : I don't think we need this whole Token Provider implementation since we aren't really generating a token. We just use the
TwoFactorProviderType
to control how we validate the user.