1
0
mirror of https://github.com/bitwarden/mobile synced 2026-01-08 11:33:31 +00:00

Feature/use hcaptcha if bot (#1476)

* Add captcha to login models and methods

* Add captcha web auth to login

* Extract captcha to abstract base class

* Add Captcha to register

* Null out captcha token after each successful challenge

* Cancel > close
This commit is contained in:
Matt Gibson
2021-08-04 15:47:23 -04:00
committed by GitHub
parent 9042b1009e
commit 2f2fa8a25b
18 changed files with 322 additions and 42 deletions

View File

@@ -30,7 +30,7 @@ namespace Bit.Core.Abstractions
Task<CipherResponse> PostCipherAsync(CipherRequest request);
Task<CipherResponse> PostCipherCreateAsync(CipherCreateRequest request);
Task<FolderResponse> PostFolderAsync(FolderRequest request);
Task<Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>> PostIdentityTokenAsync(TokenRequest request);
Task<IdentityResponse> PostIdentityTokenAsync(TokenRequest request);
Task PostPasswordHintAsync(PasswordHintRequest request);
Task SetPasswordAsync(SetPasswordRequest request);
Task<PreloginResponse> PostPreloginAsync(PreloginRequest request);

View File

@@ -21,7 +21,7 @@ namespace Bit.Core.Abstractions
bool AuthingWithSso();
bool AuthingWithPassword();
List<TwoFactorProvider> GetSupportedTwoFactorProviders();
Task<AuthResult> LogInAsync(string email, string masterPassword);
Task<AuthResult> LogInAsync(string email, string masterPassword, string captchaToken);
Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl);
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);

View File

@@ -6,6 +6,8 @@ namespace Bit.Core.Models.Domain
public class AuthResult
{
public bool TwoFactor { get; set; }
public bool CaptchaNeeded => !string.IsNullOrWhiteSpace(CaptchaSiteKey);
public string CaptchaSiteKey { get; set; }
public bool ResetMasterPassword { get; set; }
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
}

View File

@@ -15,5 +15,6 @@ namespace Bit.Core.Models.Request
public Guid? OrganizationUserId { get; set; }
public KdfType? Kdf { get; set; }
public int? KdfIterations { get; set; }
public string CaptchaResponse { get; set; }
}
}

View File

@@ -16,10 +16,11 @@ namespace Bit.Core.Models.Request
public string Token { get; set; }
public TwoFactorProviderType? Provider { get; set; }
public bool? Remember { get; set; }
public string CaptchaToken { get; set; }
public DeviceRequest Device { get; set; }
public TokenRequest(string[] credentials, string[] codes, TwoFactorProviderType? provider, string token,
bool? remember, DeviceRequest device = null)
bool? remember, string captchaToken, DeviceRequest device = null)
{
if (credentials != null && credentials.Length > 1)
{
@@ -36,6 +37,7 @@ namespace Bit.Core.Models.Request
Provider = provider;
Remember = remember;
Device = device;
CaptchaToken = captchaToken;
}
public Dictionary<string, string> ToIdentityToken(string clientId)
@@ -77,6 +79,11 @@ namespace Bit.Core.Models.Request
obj.Add("twoFactorProvider", ((int)Provider.Value).ToString());
obj.Add("twoFactorRemember", Remember.GetValueOrDefault() ? "1" : "0");
}
if (CaptchaToken != null)
{
obj.Add("captchaResponse", CaptchaToken);
}
return obj;
}

View File

@@ -30,6 +30,10 @@ namespace Bit.Core.Models.Response
var model = errorModel.ToObject<ErrorModel>();
Message = model.Message;
ValidationErrors = model.ValidationErrors;
CaptchaSiteKey = ValidationErrors.ContainsKey("HCaptcha_SiteKey") ?
ValidationErrors["HCaptcha_SiteKey"]?.FirstOrDefault() :
null;
CaptchaRequired = !string.IsNullOrWhiteSpace(CaptchaSiteKey);
}
else
{
@@ -44,6 +48,8 @@ namespace Bit.Core.Models.Response
public string Message { get; set; }
public Dictionary<string, List<string>> ValidationErrors { get; set; }
public HttpStatusCode StatusCode { get; set; }
public string CaptchaSiteKey { get; set; }
public bool CaptchaRequired { get; set; } = false;
public string GetSingleMessage()
{

View File

@@ -0,0 +1,13 @@
using Bit.Core.Enums;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Bit.Core.Models.Response
{
public class IdentityCaptchaResponse
{
[JsonProperty("HCaptcha_SiteKey")]
public string SiteKey { get; set; }
}
}

View File

@@ -0,0 +1,50 @@
using System.Net;
using Newtonsoft.Json.Linq;
namespace Bit.Core.Models.Response
{
public class IdentityResponse
{
public IdentityTokenResponse TokenResponse { get; }
public IdentityTwoFactorResponse TwoFactorResponse { get; }
public IdentityCaptchaResponse CaptchaResponse { get; }
public bool TwoFactorNeeded => TwoFactorResponse != null;
public bool FailedToParse { get; }
public IdentityResponse(HttpStatusCode httpStatusCode, JObject responseJObject)
{
var parsed = false;
if (responseJObject != null)
{
if (IsSuccessStatusCode(httpStatusCode))
{
TokenResponse = responseJObject.ToObject<IdentityTokenResponse>();
parsed = true;
}
else if (httpStatusCode == HttpStatusCode.BadRequest)
{
if (JObjectHasProperty(responseJObject, "TwoFactorProviders2"))
{
TwoFactorResponse = responseJObject.ToObject<IdentityTwoFactorResponse>();
parsed = true;
}
else if (JObjectHasProperty(responseJObject, "HCaptcha_SiteKey"))
{
CaptchaResponse = responseJObject.ToObject<IdentityCaptchaResponse>();
parsed = true;
}
}
}
FailedToParse = !parsed;
}
private bool IsSuccessStatusCode(HttpStatusCode httpStatusCode) =>
(int)httpStatusCode >= 200 && (int)httpStatusCode < 300;
private bool JObjectHasProperty(JObject jObject, string propertyName) =>
jObject.ContainsKey(propertyName) &&
jObject[propertyName] != null &&
(jObject[propertyName].HasValues || jObject[propertyName].Value<string>() != null);
}
}

View File

@@ -1,4 +1,5 @@
using Bit.Core.Enums;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Bit.Core.Models.Response
@@ -7,5 +8,7 @@ namespace Bit.Core.Models.Response
{
public List<TwoFactorProviderType> TwoFactorProviders { get; set; }
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders2 { get; set; }
[JsonProperty("CaptchaBypassToken")]
public string CaptchaToken { get; set; }
}
}

View File

@@ -80,8 +80,7 @@ namespace Bit.Core.Services
#region Auth APIs
public async Task<Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>> PostIdentityTokenAsync(
TokenRequest request)
public async Task<IdentityResponse> PostIdentityTokenAsync(TokenRequest request)
{
var requestMessage = new HttpRequestMessage
{
@@ -109,23 +108,14 @@ namespace Bit.Core.Services
responseJObject = JObject.Parse(responseJsonString);
}
if (responseJObject != null)
var identityResponse = new IdentityResponse(response.StatusCode, responseJObject);
if (identityResponse.FailedToParse)
{
if (response.IsSuccessStatusCode)
{
return new Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>(
responseJObject.ToObject<IdentityTokenResponse>(), null);
}
else if (response.StatusCode == HttpStatusCode.BadRequest &&
responseJObject.ContainsKey("TwoFactorProviders2") &&
responseJObject["TwoFactorProviders2"] != null &&
responseJObject["TwoFactorProviders2"].HasValues)
{
return new Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>(
null, responseJObject.ToObject<IdentityTwoFactorResponse>());
}
throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true));
}
throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true));
return identityResponse;
}
public async Task RefreshIdentityTokenAsync()

View File

@@ -92,6 +92,7 @@ namespace Bit.Core.Services
}
public string Email { get; set; }
public string CaptchaToken { get; set; }
public string MasterPasswordHash { get; set; }
public string LocalMasterPasswordHash { get; set; }
public string Code { get; set; }
@@ -119,13 +120,14 @@ namespace Bit.Core.Services
TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc");
}
public async Task<AuthResult> LogInAsync(string email, string masterPassword)
public async Task<AuthResult> LogInAsync(string email, string masterPassword, string captchaToken)
{
SelectedTwoFactorProviderType = null;
var key = await MakePreloginKeyAsync(masterPassword, email);
var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key);
var localHashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization);
return await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, null, null, null);
return await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, null, null,
null, captchaToken);
}
public async Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl)
@@ -138,7 +140,7 @@ namespace Bit.Core.Services
bool? remember = null)
{
return LogInHelperAsync(Email, MasterPasswordHash, LocalMasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key,
twoFactorProvider, twoFactorToken, remember);
twoFactorProvider, twoFactorToken, remember, CaptchaToken);
}
public async Task<AuthResult> LogInCompleteAsync(string email, string masterPassword,
@@ -271,7 +273,8 @@ namespace Bit.Core.Services
private async Task<AuthResult> LogInHelperAsync(string email, string hashedPassword, string localHashedPassword,
string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key,
TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null)
TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null,
string captchaToken = null)
{
var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email);
var appId = await _appIdService.GetAppIdAsync();
@@ -300,25 +303,30 @@ namespace Bit.Core.Services
if (twoFactorToken != null && twoFactorProvider != null)
{
request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember,
deviceRequest);
captchaToken, deviceRequest);
}
else if (storedTwoFactorToken != null)
{
request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember,
storedTwoFactorToken, false, deviceRequest);
storedTwoFactorToken, false, captchaToken, deviceRequest);
}
else
{
request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, deviceRequest);
request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, captchaToken, deviceRequest);
}
var response = await _apiService.PostIdentityTokenAsync(request);
ClearState();
var result = new AuthResult { TwoFactor = response.Item2 != null };
var result = new AuthResult { TwoFactor = response.TwoFactorNeeded, CaptchaSiteKey = response.CaptchaResponse?.SiteKey };
if (result.CaptchaNeeded)
{
return result;
}
if (result.TwoFactor)
{
// Two factor required.
var twoFactorResponse = response.Item2;
Email = email;
MasterPasswordHash = hashedPassword;
LocalMasterPasswordHash = localHashedPassword;
@@ -326,12 +334,13 @@ namespace Bit.Core.Services
CodeVerifier = codeVerifier;
SsoRedirectUrl = redirectUrl;
_key = _setCryptoKeys ? key : null;
TwoFactorProvidersData = twoFactorResponse.TwoFactorProviders2;
result.TwoFactorProviders = twoFactorResponse.TwoFactorProviders2;
TwoFactorProvidersData = response.TwoFactorResponse.TwoFactorProviders2;
result.TwoFactorProviders = response.TwoFactorResponse.TwoFactorProviders2;
CaptchaToken = response.TwoFactorResponse.CaptchaToken;
return result;
}
var tokenResponse = response.Item1;
var tokenResponse = response.TokenResponse;
result.ResetMasterPassword = tokenResponse.ResetMasterPassword;
if (tokenResponse.TwoFactorToken != null)
{
@@ -374,6 +383,7 @@ namespace Bit.Core.Services
{
_key = null;
Email = null;
CaptchaToken = null;
MasterPasswordHash = null;
Code = null;
CodeVerifier = null;