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, IResourceOwnerPasswordValidator { private UserManager _userManager; private readonly ICurrentContext _currentContext; private readonly ICaptchaValidationService _captchaValidationService; private readonly IAuthRequestRepository _authRequestRepository; private readonly IDeviceValidator _deviceValidator; public ResourceOwnerPasswordValidator( UserManager userManager, IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, IMailService mailService, ILogger 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, }; await ValidateAsync(context, context.Request, validatorContext); } protected async override Task 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 claims, Dictionary 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 customResponse) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor required.", customResponse); } [Obsolete("Consider using SetGrantValidationErrorResult instead.")] protected override void SetSsoResult(ResourceOwnerPasswordValidationContext context, Dictionary customResponse) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Sso authentication required.", customResponse); } [Obsolete("Consider using SetGrantValidationErrorResult instead.")] protected override void SetErrorResult(ResourceOwnerPasswordValidationContext context, Dictionary 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; } }