mirror of
https://github.com/bitwarden/server.git
synced 2025-05-15 00:19:11 -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:
parent
2918d46b62
commit
80e7a0afd6
@ -1,6 +0,0 @@
|
|||||||
namespace Bit.Core.Auth.Models.Api;
|
|
||||||
|
|
||||||
public interface ICaptchaProtectedModel
|
|
||||||
{
|
|
||||||
string CaptchaResponse { get; set; }
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
@ -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() { }
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -88,8 +88,6 @@ public interface IMailService
|
|||||||
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
|
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
|
||||||
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);
|
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);
|
||||||
Task SendOTPEmailAsync(string email, string token);
|
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 SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
||||||
Task SendUnclaimedOrganizationDomainEmailAsync(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);
|
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||||
|
@ -1137,40 +1137,6 @@ public class HandlebarsMailService : IMailService
|
|||||||
await _mailDeliveryService.SendEmailAsync(message);
|
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)
|
public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage("Domain not verified", adminEmails);
|
var message = CreateDefaultMessage("Domain not verified", adminEmails);
|
||||||
|
@ -268,16 +268,6 @@ public class NoopMailService : IMailService
|
|||||||
return Task.FromResult(0);
|
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)
|
public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
|
@ -45,7 +45,6 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public virtual bool EnableCloudCommunication { get; set; } = false;
|
public virtual bool EnableCloudCommunication { get; set; } = false;
|
||||||
public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days
|
public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days
|
||||||
public virtual string EventGridKey { get; set; }
|
public virtual string EventGridKey { get; set; }
|
||||||
public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings();
|
|
||||||
public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings();
|
public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings();
|
||||||
public virtual IBaseServiceUriSettings BaseServiceUri { get; set; }
|
public virtual IBaseServiceUriSettings BaseServiceUri { get; set; }
|
||||||
public virtual string DatabaseProvider { get; set; }
|
public virtual string DatabaseProvider { get; set; }
|
||||||
@ -629,16 +628,6 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public bool EnforceSsoPolicyForAllUsers { get; set; }
|
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 class StripeSettings
|
||||||
{
|
{
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
@ -5,7 +5,6 @@ using Bit.Core.Auth.Enums;
|
|||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Services;
|
|
||||||
using Bit.Core.Auth.UserFeatures.Registration;
|
using Bit.Core.Auth.UserFeatures.Registration;
|
||||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -37,7 +36,6 @@ public class AccountsController : Controller
|
|||||||
private readonly ILogger<AccountsController> _logger;
|
private readonly ILogger<AccountsController> _logger;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IRegisterUserCommand _registerUserCommand;
|
private readonly IRegisterUserCommand _registerUserCommand;
|
||||||
private readonly ICaptchaValidationService _captchaValidationService;
|
|
||||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||||
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
|
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
|
||||||
@ -85,7 +83,6 @@ public class AccountsController : Controller
|
|||||||
ILogger<AccountsController> logger,
|
ILogger<AccountsController> logger,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IRegisterUserCommand registerUserCommand,
|
IRegisterUserCommand registerUserCommand,
|
||||||
ICaptchaValidationService captchaValidationService,
|
|
||||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
|
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
|
||||||
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
|
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
|
||||||
@ -99,7 +96,6 @@ public class AccountsController : Controller
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_registerUserCommand = registerUserCommand;
|
_registerUserCommand = registerUserCommand;
|
||||||
_captchaValidationService = captchaValidationService;
|
|
||||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||||
_sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand;
|
_sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand;
|
||||||
@ -167,7 +163,7 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("register/finish")]
|
[HttpPost("register/finish")]
|
||||||
public async Task<RegisterResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
|
public async Task<RegisterFinishResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
|
||||||
{
|
{
|
||||||
var user = model.ToUser();
|
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)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
return new RegisterFinishResponseModel();
|
||||||
return new RegisterResponseModel(captchaBypassToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
|
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Core.Auth.Models.Business;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Entities;
|
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
|
|
||||||
namespace Bit.Identity.IdentityServer;
|
namespace Bit.Identity.IdentityServer;
|
||||||
@ -9,7 +8,7 @@ public class CustomValidatorRequestContext
|
|||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is the device that the user is using to authenticate. It can be either known or unknown.
|
/// 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.
|
/// The option to set it here saves a trip to the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Device Device { get; set; }
|
public Device Device { get; set; }
|
||||||
@ -39,5 +38,4 @@ public class CustomValidatorRequestContext
|
|||||||
/// This will be null if the authentication request is successful.
|
/// This will be null if the authentication request is successful.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, object> CustomResponse { get; set; }
|
public Dictionary<string, object> CustomResponse { get; set; }
|
||||||
public CaptchaResponse CaptchaResponse { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
private readonly IDeviceValidator _deviceValidator;
|
private readonly IDeviceValidator _deviceValidator;
|
||||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IMailService _mailService;
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
@ -49,7 +48,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IMailService mailService,
|
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@ -66,7 +64,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
_deviceValidator = deviceValidator;
|
_deviceValidator = deviceValidator;
|
||||||
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_mailService = mailService;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
CurrentContext = currentContext;
|
CurrentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
@ -81,23 +78,12 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||||
CustomValidatorRequestContext validatorContext)
|
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's master password hash is correct.
|
||||||
var isBot = validatorContext.CaptchaResponse?.IsBot ?? false;
|
|
||||||
var valid = await ValidateContextAsync(context, validatorContext);
|
var valid = await ValidateContextAsync(context, validatorContext);
|
||||||
var user = validatorContext.User;
|
var user = validatorContext.User;
|
||||||
if (!valid || isBot)
|
if (!valid)
|
||||||
{
|
{
|
||||||
if (isBot)
|
await UpdateFailedAuthDetailsAsync(user);
|
||||||
{
|
|
||||||
_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 BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
||||||
return;
|
return;
|
||||||
@ -167,7 +153,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice);
|
await UpdateFailedAuthDetailsAsync(user);
|
||||||
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -379,7 +365,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
await _userRepository.ReplaceAsync(user);
|
await _userRepository.ReplaceAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice)
|
private async Task UpdateFailedAuthDetailsAsync(User user)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
@ -390,32 +376,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
user.FailedLoginCount = ++user.FailedLoginCount;
|
user.FailedLoginCount = ++user.FailedLoginCount;
|
||||||
user.LastFailedLoginDate = user.RevisionDate = utcNow;
|
user.LastFailedLoginDate = user.RevisionDate = utcNow;
|
||||||
await _userRepository.ReplaceAsync(user);
|
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)
|
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user)
|
||||||
|
@ -35,7 +35,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IMailService mailService,
|
|
||||||
ILogger<CustomTokenRequestValidator> logger,
|
ILogger<CustomTokenRequestValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@ -53,7 +52,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
mailService,
|
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
|
@ -3,7 +3,6 @@ using Bit.Core;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.Services;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -21,7 +20,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
{
|
{
|
||||||
private UserManager<User> _userManager;
|
private UserManager<User> _userManager;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ICaptchaValidationService _captchaValidationService;
|
|
||||||
private readonly IAuthRequestRepository _authRequestRepository;
|
private readonly IAuthRequestRepository _authRequestRepository;
|
||||||
private readonly IDeviceValidator _deviceValidator;
|
private readonly IDeviceValidator _deviceValidator;
|
||||||
public ResourceOwnerPasswordValidator(
|
public ResourceOwnerPasswordValidator(
|
||||||
@ -31,11 +29,9 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IMailService mailService,
|
|
||||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ICaptchaValidationService captchaValidationService,
|
|
||||||
IAuthRequestRepository authRequestRepository,
|
IAuthRequestRepository authRequestRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
@ -50,7 +46,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
mailService,
|
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
@ -63,7 +58,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_captchaValidationService = captchaValidationService;
|
|
||||||
_authRequestRepository = authRequestRepository;
|
_authRequestRepository = authRequestRepository;
|
||||||
_deviceValidator = deviceValidator;
|
_deviceValidator = deviceValidator;
|
||||||
}
|
}
|
||||||
@ -88,37 +82,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
Device = knownDevice ?? requestDevice,
|
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);
|
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,
|
protected async override Task<bool> ValidateContextAsync(ResourceOwnerPasswordValidationContext context,
|
||||||
|
@ -35,7 +35,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IMailService mailService,
|
|
||||||
ILogger<CustomTokenRequestValidator> logger,
|
ILogger<CustomTokenRequestValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@ -54,7 +53,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
mailService,
|
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Models.Api;
|
|
||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -9,7 +8,7 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Identity.Models.Request.Accounts;
|
namespace Bit.Identity.Models.Request.Accounts;
|
||||||
|
|
||||||
public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel
|
public class RegisterRequestModel : IValidatableObject
|
||||||
{
|
{
|
||||||
[StringLength(50)]
|
[StringLength(50)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
@ -22,7 +21,6 @@ public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel
|
|||||||
public string MasterPasswordHash { get; set; }
|
public string MasterPasswordHash { get; set; }
|
||||||
[StringLength(50)]
|
[StringLength(50)]
|
||||||
public string MasterPasswordHint { get; set; }
|
public string MasterPasswordHint { get; set; }
|
||||||
public string CaptchaResponse { get; set; }
|
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
public KeysRequestModel Keys { get; set; }
|
public KeysRequestModel Keys { get; set; }
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
namespace Bit.Identity.Models.Response.Accounts;
|
|
||||||
public interface ICaptchaProtectedResponseModel
|
|
||||||
{
|
|
||||||
public string CaptchaBypassToken { get; set; }
|
|
||||||
}
|
|
@ -0,0 +1,10 @@
|
|||||||
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
|
namespace Bit.Identity.Models.Response.Accounts;
|
||||||
|
|
||||||
|
public class RegisterFinishResponseModel : ResponseModel
|
||||||
|
{
|
||||||
|
public RegisterFinishResponseModel()
|
||||||
|
: base("registerFinish")
|
||||||
|
{ }
|
||||||
|
}
|
@ -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; }
|
|
||||||
}
|
|
@ -17,9 +17,6 @@
|
|||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
},
|
|
||||||
"captcha": {
|
|
||||||
"maximumFailedLoginAttempts": 5
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
|
@ -14,9 +14,6 @@
|
|||||||
"internalVault": null,
|
"internalVault": null,
|
||||||
"internalSso": null,
|
"internalSso": null,
|
||||||
"internalScim": null
|
"internalScim": null
|
||||||
},
|
|
||||||
"captcha": {
|
|
||||||
"maximumFailedLoginAttempts": 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,14 +151,6 @@ public static class ServiceCollectionExtensions
|
|||||||
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<EmergencyAccessInviteTokenable>>>())
|
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 =>
|
services.AddSingleton<IDataProtectorTokenFactory<SsoTokenable>>(serviceProvider =>
|
||||||
new DataProtectorTokenFactory<SsoTokenable>(
|
new DataProtectorTokenFactory<SsoTokenable>(
|
||||||
SsoTokenable.ClearTextPrefix,
|
SsoTokenable.ClearTextPrefix,
|
||||||
@ -401,16 +393,6 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddSingleton<IReferenceEventService, AzureQueueReferenceEventService>();
|
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)
|
public static void AddOosServices(this IServiceCollection services)
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -67,7 +67,7 @@ public class SsoTokenableTests
|
|||||||
ExpirationDate = expectedDateTime
|
ExpirationDate = expectedDateTime
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = Tokenable.FromToken<HCaptchaTokenable>(token.ToToken());
|
var result = Tokenable.FromToken<SsoTokenable>(token.ToToken());
|
||||||
|
|
||||||
Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10));
|
Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10));
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ using System.Text;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Services;
|
|
||||||
using Bit.Core.Auth.UserFeatures.Registration;
|
using Bit.Core.Auth.UserFeatures.Registration;
|
||||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -38,7 +37,6 @@ public class AccountsControllerTests : IDisposable
|
|||||||
private readonly ILogger<AccountsController> _logger;
|
private readonly ILogger<AccountsController> _logger;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IRegisterUserCommand _registerUserCommand;
|
private readonly IRegisterUserCommand _registerUserCommand;
|
||||||
private readonly ICaptchaValidationService _captchaValidationService;
|
|
||||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||||
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
|
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
|
||||||
@ -54,7 +52,6 @@ public class AccountsControllerTests : IDisposable
|
|||||||
_logger = Substitute.For<ILogger<AccountsController>>();
|
_logger = Substitute.For<ILogger<AccountsController>>();
|
||||||
_userRepository = Substitute.For<IUserRepository>();
|
_userRepository = Substitute.For<IUserRepository>();
|
||||||
_registerUserCommand = Substitute.For<IRegisterUserCommand>();
|
_registerUserCommand = Substitute.For<IRegisterUserCommand>();
|
||||||
_captchaValidationService = Substitute.For<ICaptchaValidationService>();
|
|
||||||
_assertionOptionsDataProtector = Substitute.For<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>();
|
_assertionOptionsDataProtector = Substitute.For<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>();
|
||||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For<IGetWebAuthnLoginCredentialAssertionOptionsCommand>();
|
_getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For<IGetWebAuthnLoginCredentialAssertionOptionsCommand>();
|
||||||
_sendVerificationEmailForRegistrationCommand = Substitute.For<ISendVerificationEmailForRegistrationCommand>();
|
_sendVerificationEmailForRegistrationCommand = Substitute.For<ISendVerificationEmailForRegistrationCommand>();
|
||||||
@ -68,7 +65,6 @@ public class AccountsControllerTests : IDisposable
|
|||||||
_logger,
|
_logger,
|
||||||
_userRepository,
|
_userRepository,
|
||||||
_registerUserCommand,
|
_registerUserCommand,
|
||||||
_captchaValidationService,
|
|
||||||
_assertionOptionsDataProtector,
|
_assertionOptionsDataProtector,
|
||||||
_getWebAuthnLoginCredentialAssertionOptionsCommand,
|
_getWebAuthnLoginCredentialAssertionOptionsCommand,
|
||||||
_sendVerificationEmailForRegistrationCommand,
|
_sendVerificationEmailForRegistrationCommand,
|
||||||
|
@ -33,7 +33,6 @@ public class BaseRequestValidatorTests
|
|||||||
private readonly IDeviceValidator _deviceValidator;
|
private readonly IDeviceValidator _deviceValidator;
|
||||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IMailService _mailService;
|
|
||||||
private readonly ILogger<BaseRequestValidatorTests> _logger;
|
private readonly ILogger<BaseRequestValidatorTests> _logger;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
@ -54,7 +53,6 @@ public class BaseRequestValidatorTests
|
|||||||
_deviceValidator = Substitute.For<IDeviceValidator>();
|
_deviceValidator = Substitute.For<IDeviceValidator>();
|
||||||
_twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
|
_twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
|
||||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
_mailService = Substitute.For<IMailService>();
|
|
||||||
_logger = Substitute.For<ILogger<BaseRequestValidatorTests>>();
|
_logger = Substitute.For<ILogger<BaseRequestValidatorTests>>();
|
||||||
_currentContext = Substitute.For<ICurrentContext>();
|
_currentContext = Substitute.For<ICurrentContext>();
|
||||||
_globalSettings = Substitute.For<GlobalSettings>();
|
_globalSettings = Substitute.For<GlobalSettings>();
|
||||||
@ -72,7 +70,6 @@ public class BaseRequestValidatorTests
|
|||||||
_deviceValidator,
|
_deviceValidator,
|
||||||
_twoFactorAuthenticationValidator,
|
_twoFactorAuthenticationValidator,
|
||||||
_organizationUserRepository,
|
_organizationUserRepository,
|
||||||
_mailService,
|
|
||||||
_logger,
|
_logger,
|
||||||
_currentContext,
|
_currentContext,
|
||||||
_globalSettings,
|
_globalSettings,
|
||||||
@ -84,36 +81,6 @@ public class BaseRequestValidatorTests
|
|||||||
_policyRequirementQuery);
|
_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
|
/* Logic path
|
||||||
* ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
* ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
||||||
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||||
@ -128,8 +95,6 @@ public class BaseRequestValidatorTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
|
||||||
_globalSettings.Captcha.Returns(new GlobalSettings.CaptchaSettings());
|
|
||||||
_globalSettings.SelfHosted = true;
|
_globalSettings.SelfHosted = true;
|
||||||
_sut.isValid = false;
|
_sut.isValid = false;
|
||||||
|
|
||||||
@ -142,44 +107,6 @@ public class BaseRequestValidatorTests
|
|||||||
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
|
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]
|
[Theory, BitAutoData]
|
||||||
public async Task ValidateAsync_DeviceNotValidated_ShouldLogError(
|
public async Task ValidateAsync_DeviceNotValidated_ShouldLogError(
|
||||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
@ -189,7 +116,6 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
// 2 -> will result to false with no extra configuration
|
// 2 -> will result to false with no extra configuration
|
||||||
@ -226,7 +152,6 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
// 2 -> will result to false with no extra configuration
|
// 2 -> will result to false with no extra configuration
|
||||||
@ -263,7 +188,6 @@ public class BaseRequestValidatorTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
context.ValidatedTokenRequest.GrantType = grantType;
|
context.ValidatedTokenRequest.GrantType = grantType;
|
||||||
@ -294,7 +218,6 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
context.ValidatedTokenRequest.GrantType = grantType;
|
context.ValidatedTokenRequest.GrantType = grantType;
|
||||||
@ -326,7 +249,6 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
context.ValidatedTokenRequest.GrantType = grantType;
|
context.ValidatedTokenRequest.GrantType = grantType;
|
||||||
@ -363,7 +285,6 @@ public class BaseRequestValidatorTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
context.ValidatedTokenRequest.GrantType = grantType;
|
context.ValidatedTokenRequest.GrantType = grantType;
|
||||||
@ -401,7 +322,6 @@ public class BaseRequestValidatorTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
context.ValidatedTokenRequest.GrantType = grantType;
|
context.ValidatedTokenRequest.GrantType = grantType;
|
||||||
@ -439,7 +359,6 @@ public class BaseRequestValidatorTests
|
|||||||
var user = context.CustomValidatorRequestContext.User;
|
var user = context.CustomValidatorRequestContext.User;
|
||||||
user.Key = null;
|
user.Key = null;
|
||||||
|
|
||||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
|
||||||
context.ValidatedTokenRequest.ClientId = "Not Web";
|
context.ValidatedTokenRequest.ClientId = "Not Web";
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
_twoFactorAuthenticationValidator
|
_twoFactorAuthenticationValidator
|
||||||
|
@ -54,7 +54,6 @@ IBaseRequestValidatorTestWrapper
|
|||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IMailService mailService,
|
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@ -71,7 +70,6 @@ IBaseRequestValidatorTestWrapper
|
|||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
mailService,
|
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using AspNetCoreRateLimit;
|
using AspNetCoreRateLimit;
|
||||||
using Bit.Core.Auth.Services;
|
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Platform.Push.Internal;
|
using Bit.Core.Platform.Push.Internal;
|
||||||
@ -207,8 +206,6 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
|
|
||||||
Replace<IMailDeliveryService, NoopMailDeliveryService>(services);
|
Replace<IMailDeliveryService, NoopMailDeliveryService>(services);
|
||||||
|
|
||||||
Replace<ICaptchaValidationService, NoopCaptchaValidationService>(services);
|
|
||||||
|
|
||||||
// TODO: Install and use azurite in CI pipeline
|
// TODO: Install and use azurite in CI pipeline
|
||||||
Replace<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>(services);
|
Replace<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>(services);
|
||||||
|
|
||||||
|
@ -31,9 +31,6 @@ public class Configuration
|
|||||||
"Learn more: https://docs.docker.com/compose/compose-file/#ports")]
|
"Learn more: https://docs.docker.com/compose/compose-file/#ports")]
|
||||||
public string HttpsPort { get; set; } = "443";
|
public string HttpsPort { get; set; } = "443";
|
||||||
|
|
||||||
[Description("Configure Nginx for Captcha.")]
|
|
||||||
public bool Captcha { get; set; } = false;
|
|
||||||
|
|
||||||
[Description("Configure Nginx for SSL.")]
|
[Description("Configure Nginx for SSL.")]
|
||||||
public bool Ssl { get; set; } = true;
|
public bool Ssl { get; set; } = true;
|
||||||
|
|
||||||
|
@ -73,7 +73,6 @@ public class NginxConfigBuilder
|
|||||||
|
|
||||||
public TemplateModel(Context context)
|
public TemplateModel(Context context)
|
||||||
{
|
{
|
||||||
Captcha = context.Config.Captcha;
|
|
||||||
Ssl = context.Config.Ssl;
|
Ssl = context.Config.Ssl;
|
||||||
EnableKeyConnector = context.Config.EnableKeyConnector;
|
EnableKeyConnector = context.Config.EnableKeyConnector;
|
||||||
EnableScim = context.Config.EnableScim;
|
EnableScim = context.Config.EnableScim;
|
||||||
@ -127,7 +126,6 @@ public class NginxConfigBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Captcha { get; set; }
|
|
||||||
public bool Ssl { get; set; }
|
public bool Ssl { get; set; }
|
||||||
public bool EnableKeyConnector { get; set; }
|
public bool EnableKeyConnector { get; set; }
|
||||||
public bool EnableScim { get; set; }
|
public bool EnableScim { get; set; }
|
||||||
|
@ -100,16 +100,6 @@ server {
|
|||||||
proxy_pass http://web:5000/sso-connector.html;
|
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/ {
|
location /attachments/ {
|
||||||
proxy_pass http://attachments:5000/;
|
proxy_pass http://attachments:5000/;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user