mirror of
https://github.com/bitwarden/server.git
synced 2026-02-03 23:53:19 +08:00
* feat(BaseRequestValidator): Add global setting for new device verification. Refactor BaseRequestValidator enabling better self-documenting code and better single responsibility principle for validators. Updated DeviceValidator to handle new device verification, behind a feature flag. Moved IDeviceValidator interface to separate file. Updated CustomRequestValidator to act as the conduit by which *Validators communicate authentication context between themselves and the RequestValidators. Adding new test for DeviceValidator class. Updated tests for BaseRequestValidator as some functionality was moved to the DeviceValidator class.
230 lines
8.9 KiB
C#
230 lines
8.9 KiB
C#
using System.Security.Claims;
|
|
using Bit.Core;
|
|
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;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Settings;
|
|
using Bit.Core.Utilities;
|
|
using Duende.IdentityServer.Models;
|
|
using Duende.IdentityServer.Validation;
|
|
using Microsoft.AspNetCore.Identity;
|
|
|
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
|
|
|
public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>,
|
|
IResourceOwnerPasswordValidator
|
|
{
|
|
private UserManager<User> _userManager;
|
|
private readonly ICurrentContext _currentContext;
|
|
private readonly ICaptchaValidationService _captchaValidationService;
|
|
private readonly IAuthRequestRepository _authRequestRepository;
|
|
private readonly IDeviceValidator _deviceValidator;
|
|
public ResourceOwnerPasswordValidator(
|
|
UserManager<User> userManager,
|
|
IUserService userService,
|
|
IEventService eventService,
|
|
IDeviceValidator deviceValidator,
|
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
IMailService mailService,
|
|
ILogger<ResourceOwnerPasswordValidator> logger,
|
|
ICurrentContext currentContext,
|
|
GlobalSettings globalSettings,
|
|
ICaptchaValidationService captchaValidationService,
|
|
IAuthRequestRepository authRequestRepository,
|
|
IUserRepository userRepository,
|
|
IPolicyService policyService,
|
|
IFeatureService featureService,
|
|
ISsoConfigRepository ssoConfigRepository,
|
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
|
: base(
|
|
userManager,
|
|
userService,
|
|
eventService,
|
|
deviceValidator,
|
|
twoFactorAuthenticationValidator,
|
|
organizationUserRepository,
|
|
mailService,
|
|
logger,
|
|
currentContext,
|
|
globalSettings,
|
|
userRepository,
|
|
policyService,
|
|
featureService,
|
|
ssoConfigRepository,
|
|
userDecryptionOptionsBuilder)
|
|
{
|
|
_userManager = userManager;
|
|
_currentContext = currentContext;
|
|
_captchaValidationService = captchaValidationService;
|
|
_authRequestRepository = authRequestRepository;
|
|
_deviceValidator = deviceValidator;
|
|
}
|
|
|
|
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
|
|
{
|
|
if (!AuthEmailHeaderIsValid(context))
|
|
{
|
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant,
|
|
"Auth-Email header invalid.");
|
|
return;
|
|
}
|
|
|
|
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
|
|
// We want to keep this device around incase the device is new for the user
|
|
var requestDevice = DeviceValidator.GetDeviceFromRequest(context.Request);
|
|
var knownDevice = await _deviceValidator.GetKnownDeviceAsync(user, requestDevice);
|
|
var validatorContext = new CustomValidatorRequestContext
|
|
{
|
|
User = user,
|
|
KnownDevice = knownDevice != null,
|
|
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,
|
|
CustomValidatorRequestContext validatorContext)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(context.UserName) || validatorContext.User == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var authRequestId = context.Request.Raw["AuthRequest"]?.ToString()?.ToLowerInvariant();
|
|
if (!string.IsNullOrWhiteSpace(authRequestId) && Guid.TryParse(authRequestId, out var authRequestGuid))
|
|
{
|
|
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestGuid);
|
|
if (authRequest != null)
|
|
{
|
|
var requestAge = DateTime.UtcNow - authRequest.CreationDate;
|
|
if (requestAge < TimeSpan.FromHours(1) &&
|
|
CoreHelpers.FixedTimeEquals(authRequest.AccessCode, context.Password))
|
|
{
|
|
authRequest.AuthenticationDate = DateTime.UtcNow;
|
|
await _authRequestRepository.ReplaceAsync(authRequest);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
|
|
List<Claim> claims, Dictionary<string, object> customResponse)
|
|
{
|
|
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
|
|
identityProvider: Constants.IdentityProvider,
|
|
claims: claims.Count > 0 ? claims : null,
|
|
customResponse: customResponse);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
[Obsolete("Consider using SetGrantValidationErrorResult instead.")]
|
|
protected override void SetTwoFactorResult(ResourceOwnerPasswordValidationContext context,
|
|
Dictionary<string, object> customResponse)
|
|
{
|
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor required.",
|
|
customResponse);
|
|
}
|
|
|
|
[Obsolete("Consider using SetGrantValidationErrorResult instead.")]
|
|
protected override void SetSsoResult(ResourceOwnerPasswordValidationContext context,
|
|
Dictionary<string, object> customResponse)
|
|
{
|
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Sso authentication required.",
|
|
customResponse);
|
|
}
|
|
|
|
[Obsolete("Consider using SetGrantValidationErrorResult instead.")]
|
|
protected override void SetErrorResult(ResourceOwnerPasswordValidationContext context,
|
|
Dictionary<string, object> customResponse)
|
|
{
|
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse);
|
|
}
|
|
|
|
protected override void SetValidationErrorResult(
|
|
ResourceOwnerPasswordValidationContext context, CustomValidatorRequestContext requestContext)
|
|
{
|
|
context.Result = new GrantValidationResult
|
|
{
|
|
Error = requestContext.ValidationErrorResult.Error,
|
|
ErrorDescription = requestContext.ValidationErrorResult.ErrorDescription,
|
|
IsError = true,
|
|
CustomResponse = requestContext.CustomResponse
|
|
};
|
|
}
|
|
|
|
protected override ClaimsPrincipal GetSubject(ResourceOwnerPasswordValidationContext context)
|
|
{
|
|
return context.Result.Subject;
|
|
}
|
|
|
|
private bool AuthEmailHeaderIsValid(ResourceOwnerPasswordValidationContext context)
|
|
{
|
|
if (_currentContext.HttpContext.Request.Headers.TryGetValue("Auth-Email", out var authEmailHeader))
|
|
{
|
|
try
|
|
{
|
|
var authEmailDecoded = CoreHelpers.Base64UrlDecodeString(authEmailHeader);
|
|
if (authEmailDecoded != context.UserName)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception e) when (e is InvalidOperationException || e is FormatException)
|
|
{
|
|
// Invalid B64 encoding
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|