1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-11 22:52:15 -05:00

chore(captcha): [PM-15162] Remove captcha enforcement and issuing of bypass token

* Remove captcha enforcement and issuing/verification of bypass token

* Removed more captcha logic.

* Removed logic to enforce failed login attempts

* Linting.

* Fixed order of initialization.

* Fixed merge conflicts

* Renamed registration finish response for clarity

* Remove unnecessary mailService references.
This commit is contained in:
Todd Martin 2025-05-09 10:44:38 -04:00 committed by GitHub
parent 2918d46b62
commit 80e7a0afd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 22 additions and 740 deletions

View File

@ -1,6 +0,0 @@
namespace Bit.Core.Auth.Models.Api;
public interface ICaptchaProtectedModel
{
string CaptchaResponse { get; set; }
}

View File

@ -1,9 +0,0 @@
namespace Bit.Core.Auth.Models.Business;
public class CaptchaResponse
{
public bool Success { get; set; }
public bool MaybeBot { get; set; }
public bool IsBot { get; set; }
public double Score { get; set; }
}

View File

@ -1,43 +0,0 @@
using System.Text.Json.Serialization;
using Bit.Core.Entities;
using Bit.Core.Tokens;
namespace Bit.Core.Auth.Models.Business.Tokenables;
public class HCaptchaTokenable : ExpiringTokenable
{
private const double _tokenLifetimeInHours = (double)5 / 60; // 5 minutes
public const string ClearTextPrefix = "BWCaptchaBypass_";
public const string DataProtectorPurpose = "CaptchaServiceDataProtector";
public const string TokenIdentifier = "CaptchaBypassToken";
public string Identifier { get; set; } = TokenIdentifier;
public Guid Id { get; set; }
public string Email { get; set; }
[JsonConstructor]
public HCaptchaTokenable()
{
ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours);
}
public HCaptchaTokenable(User user) : this()
{
Id = user?.Id ?? default;
Email = user?.Email;
}
public bool TokenIsValid(User user)
{
if (Id == default || Email == default || user == null)
{
return false;
}
return Id == user.Id &&
Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);
}
// Validates deserialized
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email);
}

View File

@ -1,15 +0,0 @@
using Bit.Core.Auth.Models.Business;
using Bit.Core.Context;
using Bit.Core.Entities;
namespace Bit.Core.Auth.Services;
public interface ICaptchaValidationService
{
string SiteKey { get; }
string SiteKeyResponseKeyName { get; }
bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null);
Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress,
User user = null);
string GenerateCaptchaBypassToken(User user);
}

View File

@ -1,132 +0,0 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Bit.Core.Auth.Models.Business;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Auth.Services;
public class HCaptchaValidationService : ICaptchaValidationService
{
private readonly ILogger<HCaptchaValidationService> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly GlobalSettings _globalSettings;
private readonly IDataProtectorTokenFactory<HCaptchaTokenable> _tokenizer;
public HCaptchaValidationService(
ILogger<HCaptchaValidationService> logger,
IHttpClientFactory httpClientFactory,
IDataProtectorTokenFactory<HCaptchaTokenable> tokenizer,
GlobalSettings globalSettings)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_globalSettings = globalSettings;
_tokenizer = tokenizer;
}
public string SiteKeyResponseKeyName => "HCaptcha_SiteKey";
public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey;
public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user));
public async Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
User user = null)
{
var response = new CaptchaResponse { Success = false };
if (string.IsNullOrWhiteSpace(captchaResponse))
{
return response;
}
if (user != null && ValidateCaptchaBypassToken(captchaResponse, user))
{
response.Success = true;
return response;
}
var httpClient = _httpClientFactory.CreateClient("HCaptchaValidationService");
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://hcaptcha.com/siteverify"),
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "response", captchaResponse.TrimStart("hcaptcha|".ToCharArray()) },
{ "secret", _globalSettings.Captcha.HCaptchaSecretKey },
{ "sitekey", SiteKey },
{ "remoteip", clientIpAddress }
})
};
HttpResponseMessage responseMessage;
try
{
responseMessage = await httpClient.SendAsync(requestMessage);
}
catch (Exception e)
{
_logger.LogError(11389, e, "Unable to verify with HCaptcha.");
return response;
}
if (!responseMessage.IsSuccessStatusCode)
{
return response;
}
using var hcaptchaResponse = await responseMessage.Content.ReadFromJsonAsync<HCaptchaResponse>();
response.Success = hcaptchaResponse.Success;
var score = hcaptchaResponse.Score.GetValueOrDefault();
response.MaybeBot = score >= _globalSettings.Captcha.MaybeBotScoreThreshold;
response.IsBot = score >= _globalSettings.Captcha.IsBotScoreThreshold;
response.Score = score;
return response;
}
public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null)
{
if (user == null)
{
return currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired;
}
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
var failedLoginCount = user?.FailedLoginCount ?? 0;
var requireOnCloud = !_globalSettings.SelfHosted && !user.EmailVerified &&
user.CreationDate < DateTime.UtcNow.AddHours(-24);
return currentContext.IsBot ||
_globalSettings.Captcha.ForceCaptchaRequired ||
requireOnCloud ||
failedLoginCeiling > 0 && failedLoginCount >= failedLoginCeiling;
}
private static bool TokenIsValidApiKey(string bypassToken, User user) =>
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;
private bool TokenIsValidCaptchaBypassToken(string encryptedToken, User user)
{
return _tokenizer.TryUnprotect(encryptedToken, out var data) &&
data.Valid && data.TokenIsValid(user);
}
private bool ValidateCaptchaBypassToken(string bypassToken, User user) =>
TokenIsValidApiKey(bypassToken, user) || TokenIsValidCaptchaBypassToken(bypassToken, user);
public class HCaptchaResponse : IDisposable
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("score")]
public double? Score { get; set; }
[JsonPropertyName("score_reason")]
public List<string> ScoreReason { get; set; }
public void Dispose() { }
}
}

View File

@ -1,18 +0,0 @@
using Bit.Core.Auth.Models.Business;
using Bit.Core.Context;
using Bit.Core.Entities;
namespace Bit.Core.Auth.Services;
public class NoopCaptchaValidationService : ICaptchaValidationService
{
public string SiteKeyResponseKeyName => null;
public string SiteKey => null;
public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) => false;
public string GenerateCaptchaBypassToken(User user) => "";
public Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
User user = null)
{
return Task.FromResult(new CaptchaResponse { Success = true });
}
}

View File

@ -1,36 +0,0 @@
using Bit.Core.Auth.Models.Api;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Auth.Utilities;
public class CaptchaProtectedAttribute : ActionFilterAttribute
{
public string ModelParameterName { get; set; } = "model";
public override void OnActionExecuting(ActionExecutingContext context)
{
var currentContext = context.HttpContext.RequestServices.GetRequiredService<ICurrentContext>();
var captchaValidationService = context.HttpContext.RequestServices.GetRequiredService<ICaptchaValidationService>();
if (captchaValidationService.RequireCaptchaValidation(currentContext, null))
{
var captchaResponse = (context.ActionArguments[ModelParameterName] as ICaptchaProtectedModel)?.CaptchaResponse;
if (string.IsNullOrWhiteSpace(captchaResponse))
{
throw new BadRequestException(captchaValidationService.SiteKeyResponseKeyName, captchaValidationService.SiteKey);
}
var captchaValidationResponse = captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse,
currentContext.IpAddress, null).GetAwaiter().GetResult();
if (!captchaValidationResponse.Success || captchaValidationResponse.IsBot)
{
throw new BadRequestException("Captcha is invalid. Please refresh and try again");
}
}
}
}

View File

@ -1,31 +0,0 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Additional security has been placed on your Bitwarden account.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Account:</b> {{AffectedEmail}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">IP Address:</b> {{IpAddress}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
If this was you, you can remove the captcha requirement by successfully logging in.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -1,13 +0,0 @@
{{#>BasicTextLayout}}
Additional security has been placed on your Bitwarden account.
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
Account: {{AffectedEmail}}
Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
IP Address: {{IpAddress}}
If this was you, you can remove the captcha requirement by successfully logging in.
If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection.
{{/BasicTextLayout}}

View File

@ -1,31 +0,0 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Additional security has been placed on your Bitwarden account.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Account:</b> {{AffectedEmail}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">IP Address:</b> {{IpAddress}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a <a target="_blank" clicktracking=off href="https://bitwarden.com/help/two-step-recovery-code/" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">recovery code</a>.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
If this was not you, you should <a target="_blank" clicktracking=off href="https://bitwarden.com/help/master-password/#change-master-password" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">change your master password</a> immediately. You can view our tips for selecting a secure master password <a target="_blank" clicktracking=off href="https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">here</a>.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -1,13 +0,0 @@
{{#>BasicTextLayout}}
Additional security has been placed on your Bitwarden account.
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
Account: {{AffectedEmail}}
Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
IP Address: {{IpAddress}}
If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a recovery code (https://bitwarden.com/help/two-step-recovery-code/).
If this was not you, you should change your master password (https://bitwarden.com/help/master-password/#change-master-password) immediately. You can view our tips for selecting a secure master password here (https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/).
{{/BasicTextLayout}}

View File

@ -88,8 +88,6 @@ public interface IMailService
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);
Task SendOTPEmailAsync(string email, string token);
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);

View File

@ -1137,40 +1137,6 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
var message = CreateDefaultMessage("Failed login attempts detected", email);
var model = new FailedAuthAttemptsModel()
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip,
AffectedEmail = email
};
await AddMessageContentAsync(message, "Auth.FailedLoginAttempts", model);
message.Category = "FailedLoginAttempts";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
var message = CreateDefaultMessage("Failed login attempts detected", email);
var model = new FailedAuthAttemptsModel()
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip,
AffectedEmail = email
};
await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempts", model);
message.Category = "FailedTwoFactorAttempts";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
{
var message = CreateDefaultMessage("Domain not verified", adminEmails);

View File

@ -268,16 +268,6 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
public Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
return Task.FromResult(0);
}
public Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
return Task.FromResult(0);
}
public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
{
return Task.FromResult(0);

View File

@ -45,7 +45,6 @@ public class GlobalSettings : IGlobalSettings
public virtual bool EnableCloudCommunication { get; set; } = false;
public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days
public virtual string EventGridKey { get; set; }
public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings();
public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings();
public virtual IBaseServiceUriSettings BaseServiceUri { get; set; }
public virtual string DatabaseProvider { get; set; }
@ -629,16 +628,6 @@ public class GlobalSettings : IGlobalSettings
public bool EnforceSsoPolicyForAllUsers { get; set; }
}
public class CaptchaSettings
{
public bool ForceCaptchaRequired { get; set; } = false;
public string HCaptchaSecretKey { get; set; }
public string HCaptchaSiteKey { get; set; }
public int MaximumFailedLoginAttempts { get; set; }
public double MaybeBotScoreThreshold { get; set; } = double.MaxValue;
public double IsBotScoreThreshold { get; set; } = double.MaxValue;
}
public class StripeSettings
{
public string ApiKey { get; set; }

View File

@ -5,7 +5,6 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Api.Response.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.Registration;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
using Bit.Core.Context;
@ -37,7 +36,6 @@ public class AccountsController : Controller
private readonly ILogger<AccountsController> _logger;
private readonly IUserRepository _userRepository;
private readonly IRegisterUserCommand _registerUserCommand;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
@ -85,7 +83,6 @@ public class AccountsController : Controller
ILogger<AccountsController> logger,
IUserRepository userRepository,
IRegisterUserCommand registerUserCommand,
ICaptchaValidationService captchaValidationService,
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
@ -99,7 +96,6 @@ public class AccountsController : Controller
_logger = logger;
_userRepository = userRepository;
_registerUserCommand = registerUserCommand;
_captchaValidationService = captchaValidationService;
_assertionOptionsDataProtector = assertionOptionsDataProtector;
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
_sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand;
@ -167,7 +163,7 @@ public class AccountsController : Controller
}
[HttpPost("register/finish")]
public async Task<RegisterResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
public async Task<RegisterFinishResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
{
var user = model.ToUser();
@ -208,12 +204,11 @@ public class AccountsController : Controller
}
}
private RegisterResponseModel ProcessRegistrationResult(IdentityResult result, User user)
private RegisterFinishResponseModel ProcessRegistrationResult(IdentityResult result, User user)
{
if (result.Succeeded)
{
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
return new RegisterResponseModel(captchaBypassToken);
return new RegisterFinishResponseModel();
}
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))

View File

@ -1,5 +1,4 @@
using Bit.Core.Auth.Models.Business;
using Bit.Core.Entities;
using Bit.Core.Entities;
using Duende.IdentityServer.Validation;
namespace Bit.Identity.IdentityServer;
@ -9,7 +8,7 @@ public class CustomValidatorRequestContext
public User User { get; set; }
/// <summary>
/// This is the device that the user is using to authenticate. It can be either known or unknown.
/// We set it here since the ResourceOwnerPasswordValidator needs the device to know if CAPTCHA is required.
/// We set it here since the ResourceOwnerPasswordValidator needs the device to do device validation.
/// The option to set it here saves a trip to the database.
/// </summary>
public Device Device { get; set; }
@ -39,5 +38,4 @@ public class CustomValidatorRequestContext
/// This will be null if the authentication request is successful.
/// </summary>
public Dictionary<string, object> CustomResponse { get; set; }
public CaptchaResponse CaptchaResponse { get; set; }
}

View File

@ -29,7 +29,6 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly IDeviceValidator _deviceValidator;
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IMailService _mailService;
private readonly ILogger _logger;
private readonly GlobalSettings _globalSettings;
private readonly IUserRepository _userRepository;
@ -49,7 +48,6 @@ public abstract class BaseRequestValidator<T> where T : class
IDeviceValidator deviceValidator,
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger logger,
ICurrentContext currentContext,
GlobalSettings globalSettings,
@ -66,7 +64,6 @@ public abstract class BaseRequestValidator<T> where T : class
_deviceValidator = deviceValidator;
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
_organizationUserRepository = organizationUserRepository;
_mailService = mailService;
_logger = logger;
CurrentContext = currentContext;
_globalSettings = globalSettings;
@ -81,23 +78,12 @@ public abstract class BaseRequestValidator<T> where T : class
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.
var isBot = validatorContext.CaptchaResponse?.IsBot ?? false;
// 1. We need to check if the user's master password hash is correct.
var valid = await ValidateContextAsync(context, validatorContext);
var user = validatorContext.User;
if (!valid || isBot)
if (!valid)
{
if (isBot)
{
_logger.LogInformation(Constants.BypassFiltersEventId,
"Login attempt for {UserName} detected as a captcha bot with score {CaptchaScore}.",
request.UserName, validatorContext.CaptchaResponse.Score);
}
if (!valid)
{
await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice);
}
await UpdateFailedAuthDetailsAsync(user);
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
return;
@ -167,7 +153,7 @@ public abstract class BaseRequestValidator<T> where T : class
}
else
{
await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice);
await UpdateFailedAuthDetailsAsync(user);
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
}
return;
@ -379,7 +365,7 @@ public abstract class BaseRequestValidator<T> where T : class
await _userRepository.ReplaceAsync(user);
}
private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice)
private async Task UpdateFailedAuthDetailsAsync(User user)
{
if (user == null)
{
@ -390,32 +376,6 @@ public abstract class BaseRequestValidator<T> where T : class
user.FailedLoginCount = ++user.FailedLoginCount;
user.LastFailedLoginDate = user.RevisionDate = utcNow;
await _userRepository.ReplaceAsync(user);
if (ValidateFailedAuthEmailConditions(unknownDevice, user))
{
if (twoFactorInvalid)
{
await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
}
else
{
await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
}
}
}
/// <summary>
/// checks to see if a user is trying to log into a new device
/// and has reached the maximum number of failed login attempts.
/// </summary>
/// <param name="unknownDevice">boolean</param>
/// <param name="user">current user</param>
/// <returns></returns>
private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
{
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
var failedLoginCount = user?.FailedLoginCount ?? 0;
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
}
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user)

View File

@ -35,7 +35,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IDeviceValidator deviceValidator,
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger<CustomTokenRequestValidator> logger,
ICurrentContext currentContext,
GlobalSettings globalSettings,
@ -53,7 +52,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
deviceValidator,
twoFactorAuthenticationValidator,
organizationUserRepository,
mailService,
logger,
currentContext,
globalSettings,

View File

@ -3,7 +3,6 @@ using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Repositories;
@ -21,7 +20,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
{
private UserManager<User> _userManager;
private readonly ICurrentContext _currentContext;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IDeviceValidator _deviceValidator;
public ResourceOwnerPasswordValidator(
@ -31,11 +29,9 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IDeviceValidator deviceValidator,
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger<ResourceOwnerPasswordValidator> logger,
ICurrentContext currentContext,
GlobalSettings globalSettings,
ICaptchaValidationService captchaValidationService,
IAuthRequestRepository authRequestRepository,
IUserRepository userRepository,
IPolicyService policyService,
@ -50,7 +46,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
deviceValidator,
twoFactorAuthenticationValidator,
organizationUserRepository,
mailService,
logger,
currentContext,
globalSettings,
@ -63,7 +58,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
{
_userManager = userManager;
_currentContext = currentContext;
_captchaValidationService = captchaValidationService;
_authRequestRepository = authRequestRepository;
_deviceValidator = deviceValidator;
}
@ -88,37 +82,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
Device = knownDevice ?? requestDevice,
};
string bypassToken = null;
if (!validatorContext.KnownDevice &&
_captchaValidationService.RequireCaptchaValidation(_currentContext, user))
{
var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString();
if (string.IsNullOrWhiteSpace(captchaResponse))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.",
new Dictionary<string, object>
{
{ _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey },
});
return;
}
validatorContext.CaptchaResponse = await _captchaValidationService.ValidateCaptchaResponseAsync(
captchaResponse, _currentContext.IpAddress, user);
if (!validatorContext.CaptchaResponse.Success)
{
await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null);
return;
}
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
}
await ValidateAsync(context, context.Request, validatorContext);
if (context.Result.CustomResponse != null && bypassToken != null)
{
context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken;
}
}
protected async override Task<bool> ValidateContextAsync(ResourceOwnerPasswordValidationContext context,

View File

@ -35,7 +35,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IDeviceValidator deviceValidator,
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger<CustomTokenRequestValidator> logger,
ICurrentContext currentContext,
GlobalSettings globalSettings,
@ -54,7 +53,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
deviceValidator,
twoFactorAuthenticationValidator,
organizationUserRepository,
mailService,
logger,
currentContext,
globalSettings,

View File

@ -1,7 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Core;
using Bit.Core.Auth.Models.Api;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -9,7 +8,7 @@ using Bit.Core.Utilities;
namespace Bit.Identity.Models.Request.Accounts;
public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel
public class RegisterRequestModel : IValidatableObject
{
[StringLength(50)]
public string Name { get; set; }
@ -22,7 +21,6 @@ public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel
public string MasterPasswordHash { get; set; }
[StringLength(50)]
public string MasterPasswordHint { get; set; }
public string CaptchaResponse { get; set; }
public string Key { get; set; }
public KeysRequestModel Keys { get; set; }
public string Token { get; set; }

View File

@ -1,5 +0,0 @@
namespace Bit.Identity.Models.Response.Accounts;
public interface ICaptchaProtectedResponseModel
{
public string CaptchaBypassToken { get; set; }
}

View File

@ -0,0 +1,10 @@
using Bit.Core.Models.Api;
namespace Bit.Identity.Models.Response.Accounts;
public class RegisterFinishResponseModel : ResponseModel
{
public RegisterFinishResponseModel()
: base("registerFinish")
{ }
}

View File

@ -1,14 +0,0 @@
using Bit.Core.Models.Api;
namespace Bit.Identity.Models.Response.Accounts;
public class RegisterResponseModel : ResponseModel, ICaptchaProtectedResponseModel
{
public RegisterResponseModel(string captchaBypassToken)
: base("register")
{
CaptchaBypassToken = captchaBypassToken;
}
public string CaptchaBypassToken { get; set; }
}

View File

@ -17,9 +17,6 @@
},
"braintree": {
"production": true
},
"captcha": {
"maximumFailedLoginAttempts": 5
}
},
"Logging": {

View File

@ -14,9 +14,6 @@
"internalVault": null,
"internalSso": null,
"internalScim": null
},
"captcha": {
"maximumFailedLoginAttempts": 0
}
}
}

View File

@ -151,14 +151,6 @@ public static class ServiceCollectionExtensions
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<EmergencyAccessInviteTokenable>>>())
);
services.AddSingleton<IDataProtectorTokenFactory<HCaptchaTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<HCaptchaTokenable>(
HCaptchaTokenable.ClearTextPrefix,
HCaptchaTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<HCaptchaTokenable>>>())
);
services.AddSingleton<IDataProtectorTokenFactory<SsoTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<SsoTokenable>(
SsoTokenable.ClearTextPrefix,
@ -401,16 +393,6 @@ public static class ServiceCollectionExtensions
{
services.AddSingleton<IReferenceEventService, AzureQueueReferenceEventService>();
}
if (CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSecretKey) &&
CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSiteKey))
{
services.AddSingleton<ICaptchaValidationService, HCaptchaValidationService>();
}
else
{
services.AddSingleton<ICaptchaValidationService, NoopCaptchaValidationService>();
}
}
public static void AddOosServices(this IServiceCollection services)

View File

@ -1,87 +0,0 @@
using AutoFixture.Xunit2;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Entities;
using Bit.Core.Tokens;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Auth.Models.Business.Tokenables;
public class HCaptchaTokenableTests
{
[Fact]
public void CanHandleNullUser()
{
var token = new HCaptchaTokenable(null);
Assert.Equal(default, token.Id);
Assert.Equal(default, token.Email);
}
[Fact]
public void TokenWithNullUserIsInvalid()
{
var token = new HCaptchaTokenable(null)
{
ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1)
};
Assert.False(token.Valid);
}
[Theory, BitAutoData]
public void TokenValidityCheckNullUserIdIsInvalid(User user)
{
var token = new HCaptchaTokenable(user)
{
ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1)
};
Assert.False(token.TokenIsValid(null));
}
[Theory, AutoData]
public void CanUpdateExpirationToNonStandard(User user)
{
var token = new HCaptchaTokenable(user)
{
ExpirationDate = DateTime.MinValue
};
Assert.Equal(DateTime.MinValue, token.ExpirationDate, TimeSpan.FromMilliseconds(10));
}
[Theory, AutoData]
public void SetsDataFromUser(User user)
{
var token = new HCaptchaTokenable(user);
Assert.Equal(user.Id, token.Id);
Assert.Equal(user.Email, token.Email);
}
[Theory, AutoData]
public void SerializationSetsCorrectDateTime(User user)
{
var expectedDateTime = DateTime.UtcNow.AddHours(-5);
var token = new HCaptchaTokenable(user)
{
ExpirationDate = expectedDateTime
};
var result = Tokenable.FromToken<HCaptchaTokenable>(token.ToToken());
Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10));
}
[Theory, AutoData]
public void IsInvalidIfIdentifierIsWrong(User user)
{
var token = new HCaptchaTokenable(user)
{
Identifier = "not correct"
};
Assert.False(token.Valid);
}
}

View File

@ -67,7 +67,7 @@ public class SsoTokenableTests
ExpirationDate = expectedDateTime
};
var result = Tokenable.FromToken<HCaptchaTokenable>(token.ToToken());
var result = Tokenable.FromToken<SsoTokenable>(token.ToToken());
Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10));
}

View File

@ -3,7 +3,6 @@ using System.Text;
using Bit.Core;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.Registration;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
using Bit.Core.Context;
@ -38,7 +37,6 @@ public class AccountsControllerTests : IDisposable
private readonly ILogger<AccountsController> _logger;
private readonly IUserRepository _userRepository;
private readonly IRegisterUserCommand _registerUserCommand;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
@ -54,7 +52,6 @@ public class AccountsControllerTests : IDisposable
_logger = Substitute.For<ILogger<AccountsController>>();
_userRepository = Substitute.For<IUserRepository>();
_registerUserCommand = Substitute.For<IRegisterUserCommand>();
_captchaValidationService = Substitute.For<ICaptchaValidationService>();
_assertionOptionsDataProtector = Substitute.For<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>();
_getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For<IGetWebAuthnLoginCredentialAssertionOptionsCommand>();
_sendVerificationEmailForRegistrationCommand = Substitute.For<ISendVerificationEmailForRegistrationCommand>();
@ -68,7 +65,6 @@ public class AccountsControllerTests : IDisposable
_logger,
_userRepository,
_registerUserCommand,
_captchaValidationService,
_assertionOptionsDataProtector,
_getWebAuthnLoginCredentialAssertionOptionsCommand,
_sendVerificationEmailForRegistrationCommand,

View File

@ -33,7 +33,6 @@ public class BaseRequestValidatorTests
private readonly IDeviceValidator _deviceValidator;
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IMailService _mailService;
private readonly ILogger<BaseRequestValidatorTests> _logger;
private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
@ -54,7 +53,6 @@ public class BaseRequestValidatorTests
_deviceValidator = Substitute.For<IDeviceValidator>();
_twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_mailService = Substitute.For<IMailService>();
_logger = Substitute.For<ILogger<BaseRequestValidatorTests>>();
_currentContext = Substitute.For<ICurrentContext>();
_globalSettings = Substitute.For<GlobalSettings>();
@ -72,7 +70,6 @@ public class BaseRequestValidatorTests
_deviceValidator,
_twoFactorAuthenticationValidator,
_organizationUserRepository,
_mailService,
_logger,
_currentContext,
_globalSettings,
@ -84,36 +81,6 @@ public class BaseRequestValidatorTests
_policyRequirementQuery);
}
/* Logic path
* ValidateAsync -> _Logger.LogInformation
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
* |-> SetErrorResult
*/
[Theory, BitAutoData]
public async Task ValidateAsync_IsBot_UserNotNull_ShouldBuildErrorResult_ShouldLogFailedLoginEvent(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = true;
_sut.isValid = true;
// Act
await _sut.ValidateAsync(context);
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
// Assert
await _eventService.Received(1)
.LogUserEventAsync(context.CustomValidatorRequestContext.User.Id,
EventType.User_FailedLogIn);
Assert.True(context.GrantResult.IsError);
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
}
/* Logic path
* ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
@ -128,8 +95,6 @@ public class BaseRequestValidatorTests
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_globalSettings.Captcha.Returns(new GlobalSettings.CaptchaSettings());
_globalSettings.SelfHosted = true;
_sut.isValid = false;
@ -142,44 +107,6 @@ public class BaseRequestValidatorTests
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
}
/* Logic path
* ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
* |-> SetErrorResult
*/
[Theory, BitAutoData]
public async Task ValidateAsync_ContextNotValid_MaxAttemptLogin_ShouldSendEmail(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
// This needs to be n-1 of the max failed login attempts
context.CustomValidatorRequestContext.User.FailedLoginCount = 2;
context.CustomValidatorRequestContext.KnownDevice = false;
_globalSettings.Captcha.Returns(
new GlobalSettings.CaptchaSettings
{
MaximumFailedLoginAttempts = 3
});
_sut.isValid = false;
// Act
await _sut.ValidateAsync(context);
// Assert
await _mailService.Received(1)
.SendFailedLoginAttemptsEmailAsync(
Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
Assert.True(context.GrantResult.IsError);
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
}
[Theory, BitAutoData]
public async Task ValidateAsync_DeviceNotValidated_ShouldLogError(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
@ -189,7 +116,6 @@ public class BaseRequestValidatorTests
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
// 1 -> to pass
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
// 2 -> will result to false with no extra configuration
@ -226,7 +152,6 @@ public class BaseRequestValidatorTests
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
// 1 -> to pass
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
// 2 -> will result to false with no extra configuration
@ -263,7 +188,6 @@ public class BaseRequestValidatorTests
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = grantType;
@ -294,7 +218,6 @@ public class BaseRequestValidatorTests
// Arrange
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = grantType;
@ -326,7 +249,6 @@ public class BaseRequestValidatorTests
// Arrange
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = grantType;
@ -363,7 +285,6 @@ public class BaseRequestValidatorTests
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = grantType;
@ -401,7 +322,6 @@ public class BaseRequestValidatorTests
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = grantType;
@ -439,7 +359,6 @@ public class BaseRequestValidatorTests
var user = context.CustomValidatorRequestContext.User;
user.Key = null;
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
context.ValidatedTokenRequest.ClientId = "Not Web";
_sut.isValid = true;
_twoFactorAuthenticationValidator

View File

@ -54,7 +54,6 @@ IBaseRequestValidatorTestWrapper
IDeviceValidator deviceValidator,
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger logger,
ICurrentContext currentContext,
GlobalSettings globalSettings,
@ -71,7 +70,6 @@ IBaseRequestValidatorTestWrapper
deviceValidator,
twoFactorAuthenticationValidator,
organizationUserRepository,
mailService,
logger,
currentContext,
globalSettings,

View File

@ -1,5 +1,4 @@
using AspNetCoreRateLimit;
using Bit.Core.Auth.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
@ -207,8 +206,6 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
Replace<IMailDeliveryService, NoopMailDeliveryService>(services);
Replace<ICaptchaValidationService, NoopCaptchaValidationService>(services);
// TODO: Install and use azurite in CI pipeline
Replace<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>(services);

View File

@ -31,9 +31,6 @@ public class Configuration
"Learn more: https://docs.docker.com/compose/compose-file/#ports")]
public string HttpsPort { get; set; } = "443";
[Description("Configure Nginx for Captcha.")]
public bool Captcha { get; set; } = false;
[Description("Configure Nginx for SSL.")]
public bool Ssl { get; set; } = true;

View File

@ -73,7 +73,6 @@ public class NginxConfigBuilder
public TemplateModel(Context context)
{
Captcha = context.Config.Captcha;
Ssl = context.Config.Ssl;
EnableKeyConnector = context.Config.EnableKeyConnector;
EnableScim = context.Config.EnableScim;
@ -127,7 +126,6 @@ public class NginxConfigBuilder
}
}
public bool Captcha { get; set; }
public bool Ssl { get; set; }
public bool EnableKeyConnector { get; set; }
public bool EnableScim { get; set; }

View File

@ -100,16 +100,6 @@ server {
proxy_pass http://web:5000/sso-connector.html;
}
{{#if Captcha}}
location = /captcha-connector.html {
proxy_pass http://web:5000/captcha-connector.html;
}
location = /captcha-mobile-connector.html {
proxy_pass http://web:5000/captcha-mobile-connector.html;
}
{{/if}}
location /attachments/ {
proxy_pass http://attachments:5000/;
}