2024-10-10 17:26:17 -07:00
|
|
|
|
using System.Security.Claims;
|
2022-09-27 18:30:37 +02:00
|
|
|
|
using Bit.Core;
|
2023-11-23 07:07:37 +10:00
|
|
|
|
using Bit.Core.AdminConsole.Enums;
|
2025-04-23 15:43:36 +01:00
|
|
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
2023-11-23 07:07:37 +10:00
|
|
|
|
using Bit.Core.AdminConsole.Services;
|
2023-08-17 16:03:06 -04:00
|
|
|
|
using Bit.Core.Auth.Entities;
|
2023-04-14 13:25:56 -04:00
|
|
|
|
using Bit.Core.Auth.Enums;
|
2023-06-26 20:17:39 -04:00
|
|
|
|
using Bit.Core.Auth.Models.Api.Response;
|
|
|
|
|
|
using Bit.Core.Auth.Repositories;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
using Bit.Core.Context;
|
2022-01-11 10:40:51 +01:00
|
|
|
|
using Bit.Core.Entities;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
using Bit.Core.Enums;
|
|
|
|
|
|
using Bit.Core.Identity;
|
|
|
|
|
|
using Bit.Core.Models.Api;
|
2023-04-17 07:35:47 -07:00
|
|
|
|
using Bit.Core.Models.Api.Response;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
using Bit.Core.Repositories;
|
2021-02-22 15:35:16 -06:00
|
|
|
|
using Bit.Core.Services;
|
|
|
|
|
|
using Bit.Core.Settings;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
using Bit.Core.Utilities;
|
2023-11-20 16:32:23 -05:00
|
|
|
|
using Duende.IdentityServer.Validation;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
using Microsoft.AspNetCore.Identity;
|
|
|
|
|
|
|
2024-10-24 10:41:25 -07:00
|
|
|
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
2022-08-29 16:06:55 -04:00
|
|
|
|
|
2020-07-16 08:01:39 -04:00
|
|
|
|
public abstract class BaseRequestValidator<T> where T : class
|
|
|
|
|
|
{
|
|
|
|
|
|
private UserManager<User> _userManager;
|
|
|
|
|
|
private readonly IEventService _eventService;
|
2024-10-10 17:26:17 -07:00
|
|
|
|
private readonly IDeviceValidator _deviceValidator;
|
2024-10-24 10:41:25 -07:00
|
|
|
|
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
|
|
|
|
|
private readonly IMailService _mailService;
|
2023-01-13 15:02:53 +01:00
|
|
|
|
private readonly ILogger _logger;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
private readonly GlobalSettings _globalSettings;
|
2023-05-12 08:22:19 +01:00
|
|
|
|
private readonly IUserRepository _userRepository;
|
2022-12-12 15:51:41 -05:00
|
|
|
|
|
2023-06-19 10:16:15 -04:00
|
|
|
|
protected ICurrentContext CurrentContext { get; }
|
|
|
|
|
|
protected IPolicyService PolicyService { get; }
|
2023-06-26 20:17:39 -04:00
|
|
|
|
protected IFeatureService FeatureService { get; }
|
|
|
|
|
|
protected ISsoConfigRepository SsoConfigRepository { get; }
|
2023-11-20 15:55:31 +01:00
|
|
|
|
protected IUserService _userService { get; }
|
|
|
|
|
|
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
2025-04-23 15:43:36 +01:00
|
|
|
|
protected IPolicyRequirementQuery PolicyRequirementQuery { get; }
|
2023-06-19 10:16:15 -04:00
|
|
|
|
|
2020-07-16 08:01:39 -04:00
|
|
|
|
public BaseRequestValidator(
|
|
|
|
|
|
UserManager<User> userManager,
|
|
|
|
|
|
IUserService userService,
|
|
|
|
|
|
IEventService eventService,
|
2024-10-10 17:26:17 -07:00
|
|
|
|
IDeviceValidator deviceValidator,
|
2024-10-24 10:41:25 -07:00
|
|
|
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
2020-07-16 08:01:39 -04:00
|
|
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
|
|
|
|
IMailService mailService,
|
2023-01-13 15:02:53 +01:00
|
|
|
|
ILogger logger,
|
2020-07-16 08:01:39 -04:00
|
|
|
|
ICurrentContext currentContext,
|
|
|
|
|
|
GlobalSettings globalSettings,
|
2023-04-17 07:35:47 -07:00
|
|
|
|
IUserRepository userRepository,
|
2023-05-04 15:12:03 -04:00
|
|
|
|
IPolicyService policyService,
|
2023-06-26 20:17:39 -04:00
|
|
|
|
IFeatureService featureService,
|
2023-09-09 14:35:08 -07:00
|
|
|
|
ISsoConfigRepository ssoConfigRepository,
|
2025-04-23 15:43:36 +01:00
|
|
|
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
|
|
|
|
|
IPolicyRequirementQuery policyRequirementQuery)
|
2022-08-29 14:53:16 -04:00
|
|
|
|
{
|
2020-07-16 08:01:39 -04:00
|
|
|
|
_userManager = userManager;
|
|
|
|
|
|
_userService = userService;
|
|
|
|
|
|
_eventService = eventService;
|
2024-10-10 17:26:17 -07:00
|
|
|
|
_deviceValidator = deviceValidator;
|
2024-10-24 10:41:25 -07:00
|
|
|
|
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
_organizationUserRepository = organizationUserRepository;
|
|
|
|
|
|
_mailService = mailService;
|
|
|
|
|
|
_logger = logger;
|
2023-06-19 10:16:15 -04:00
|
|
|
|
CurrentContext = currentContext;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
_globalSettings = globalSettings;
|
2023-06-19 10:16:15 -04:00
|
|
|
|
PolicyService = policyService;
|
2022-03-02 15:45:00 -06:00
|
|
|
|
_userRepository = userRepository;
|
2023-06-26 20:17:39 -04:00
|
|
|
|
FeatureService = featureService;
|
|
|
|
|
|
SsoConfigRepository = ssoConfigRepository;
|
2023-11-20 15:55:31 +01:00
|
|
|
|
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
2025-04-23 15:43:36 +01:00
|
|
|
|
PolicyRequirementQuery = policyRequirementQuery;
|
2022-08-29 14:53:16 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-12-01 16:42:41 -05:00
|
|
|
|
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
|
|
|
|
|
CustomValidatorRequestContext validatorContext)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2025-02-13 15:51:36 -05:00
|
|
|
|
// 1. We need to check if the user is a bot and if their master password hash is correct.
|
2024-09-05 11:17:15 -07:00
|
|
|
|
var isBot = validatorContext.CaptchaResponse?.IsBot ?? false;
|
2022-04-01 17:08:47 -03:00
|
|
|
|
var valid = await ValidateContextAsync(context, validatorContext);
|
|
|
|
|
|
var user = validatorContext.User;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
if (!valid || isBot)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2024-12-12 09:08:11 -08:00
|
|
|
|
if (isBot)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation(Constants.BypassFiltersEventId,
|
|
|
|
|
|
"Login attempt for {UserName} detected as a captcha bot with score {CaptchaScore}.",
|
|
|
|
|
|
request.UserName, validatorContext.CaptchaResponse.Score);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!valid)
|
|
|
|
|
|
{
|
|
|
|
|
|
await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-12-01 16:42:41 -05:00
|
|
|
|
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
2022-08-29 16:06:55 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
// 2. Decide if this user belongs to an organization that requires SSO.
|
2024-12-12 09:08:11 -08:00
|
|
|
|
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
|
|
|
|
|
|
if (validatorContext.SsoRequired)
|
|
|
|
|
|
{
|
|
|
|
|
|
SetSsoResult(context,
|
|
|
|
|
|
new Dictionary<string, object>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-10-24 10:41:25 -07:00
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
// 3. Check if 2FA is required.
|
|
|
|
|
|
(validatorContext.TwoFactorRequired, var twoFactorOrganization) =
|
|
|
|
|
|
await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request);
|
|
|
|
|
|
|
|
|
|
|
|
// This flag is used to determine if the user wants a rememberMe token sent when
|
|
|
|
|
|
// authentication is successful.
|
2024-12-12 09:08:11 -08:00
|
|
|
|
var returnRememberMeToken = false;
|
2025-02-13 15:51:36 -05:00
|
|
|
|
|
2024-12-12 09:08:11 -08:00
|
|
|
|
if (validatorContext.TwoFactorRequired)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2025-02-13 15:51:36 -05:00
|
|
|
|
var twoFactorToken = request.Raw["TwoFactorToken"];
|
|
|
|
|
|
var twoFactorProvider = request.Raw["TwoFactorProvider"];
|
2024-12-12 09:08:11 -08:00
|
|
|
|
var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
|
|
|
|
|
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
2025-02-13 15:51:36 -05:00
|
|
|
|
|
|
|
|
|
|
// 3a. Response for 2FA required and not provided state.
|
2024-10-24 10:41:25 -07:00
|
|
|
|
if (!validTwoFactorRequest ||
|
|
|
|
|
|
!Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
|
2022-05-09 12:25:13 -04:00
|
|
|
|
{
|
2024-10-24 10:41:25 -07:00
|
|
|
|
var resultDict = await _twoFactorAuthenticationValidator
|
|
|
|
|
|
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
|
|
|
|
|
if (resultDict == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
// Include Master Password Policy in 2FA response.
|
|
|
|
|
|
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
|
2024-10-24 10:41:25 -07:00
|
|
|
|
SetTwoFactorResult(context, resultDict);
|
2020-07-16 08:01:39 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
var twoFactorTokenValid =
|
|
|
|
|
|
await _twoFactorAuthenticationValidator
|
|
|
|
|
|
.VerifyTwoFactorAsync(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken);
|
2024-10-24 10:41:25 -07:00
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
// 3b. Response for 2FA required but request is not valid or remember token expired state.
|
2024-12-12 09:08:11 -08:00
|
|
|
|
if (!twoFactorTokenValid)
|
2023-09-09 14:35:08 -07:00
|
|
|
|
{
|
2025-02-13 15:51:36 -05:00
|
|
|
|
// The remember me token has expired.
|
2024-12-12 09:08:11 -08:00
|
|
|
|
if (twoFactorProviderType == TwoFactorProviderType.Remember)
|
2023-09-12 13:39:14 -07:00
|
|
|
|
{
|
2024-10-24 10:41:25 -07:00
|
|
|
|
var resultDict = await _twoFactorAuthenticationValidator
|
|
|
|
|
|
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
|
|
|
|
|
|
|
|
|
|
|
// Include Master Password Policy in 2FA response
|
2025-02-13 15:51:36 -05:00
|
|
|
|
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
|
2024-10-24 10:41:25 -07:00
|
|
|
|
SetTwoFactorResult(context, resultDict);
|
2023-09-12 13:39:14 -07:00
|
|
|
|
}
|
2024-12-12 09:08:11 -08:00
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice);
|
|
|
|
|
|
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
|
|
|
|
|
}
|
2022-08-29 16:06:55 -04:00
|
|
|
|
return;
|
2022-08-29 15:53:48 -04:00
|
|
|
|
}
|
2024-12-12 09:08:11 -08:00
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
// 3c. When the 2FA authentication is successful, we can check if the user wants a
|
|
|
|
|
|
// rememberMe token.
|
|
|
|
|
|
var twoFactorRemember = request.Raw["TwoFactorRemember"] == "1";
|
|
|
|
|
|
// Check if the user wants a rememberMe token.
|
|
|
|
|
|
if (twoFactorRemember
|
|
|
|
|
|
// if the 2FA auth was rememberMe do not send another token.
|
|
|
|
|
|
&& twoFactorProviderType != TwoFactorProviderType.Remember)
|
2024-12-12 09:08:11 -08:00
|
|
|
|
{
|
|
|
|
|
|
returnRememberMeToken = true;
|
|
|
|
|
|
}
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
2024-12-12 09:08:11 -08:00
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
// 4. Check if the user is logging in from a new device.
|
2024-12-12 09:08:11 -08:00
|
|
|
|
var deviceValid = await _deviceValidator.ValidateRequestDeviceAsync(request, validatorContext);
|
|
|
|
|
|
if (!deviceValid)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2024-12-12 09:08:11 -08:00
|
|
|
|
SetValidationErrorResult(context, validatorContext);
|
|
|
|
|
|
await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn);
|
|
|
|
|
|
return;
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
// 5. Force legacy users to the web for migration.
|
2025-01-10 11:55:40 +01:00
|
|
|
|
if (UserService.IsLegacyUser(user) && request.ClientId != "web")
|
2024-06-03 09:19:56 -04:00
|
|
|
|
{
|
2025-01-10 11:55:40 +01:00
|
|
|
|
await FailAuthForLegacyUserAsync(user, context);
|
|
|
|
|
|
return;
|
2024-06-03 09:19:56 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-12 09:08:11 -08:00
|
|
|
|
await BuildSuccessResultAsync(user, context, validatorContext.Device, returnRememberMeToken);
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
2024-06-03 09:19:56 -04:00
|
|
|
|
protected async Task FailAuthForLegacyUserAsync(User user, T context)
|
|
|
|
|
|
{
|
|
|
|
|
|
await BuildErrorResultAsync(
|
|
|
|
|
|
$"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}",
|
|
|
|
|
|
false, context, user);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-05-09 12:25:13 -04:00
|
|
|
|
protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
|
|
|
|
|
protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2020-07-16 08:01:39 -04:00
|
|
|
|
await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn);
|
2022-08-29 16:06:55 -04:00
|
|
|
|
|
2020-07-16 08:01:39 -04:00
|
|
|
|
var claims = new List<Claim>();
|
2022-08-29 16:06:55 -04:00
|
|
|
|
|
2020-07-16 08:01:39 -04:00
|
|
|
|
if (device != null)
|
|
|
|
|
|
{
|
2023-01-13 15:02:53 +01:00
|
|
|
|
claims.Add(new Claim(Claims.Device, device.Identifier));
|
[PM-10600] Push notification creation to affected clients (#4923)
* PM-10600: Notification push notification
* PM-10600: Sending to specific client types for relay push notifications
* PM-10600: Sending to specific client types for other clients
* PM-10600: Send push notification on notification creation
* PM-10600: Explicit group names
* PM-10600: Id typos
* PM-10600: Revert global push notifications
* PM-10600: Added DeviceType claim
* PM-10600: Sent to organization typo
* PM-10600: UT coverage
* PM-10600: Small refactor, UTs coverage
* PM-10600: UTs coverage
* PM-10600: Startup fix
* PM-10600: Test fix
* PM-10600: Required attribute, organization group for push notification fix
* PM-10600: UT coverage
* PM-10600: Fix Mobile devices not registering to organization push notifications
We only register devices for organization push notifications when the organization is being created. This does not work, since we have a use case (Notification Center) of delivering notifications to all users of organization. This fixes it, by adding the organization id tag when device registers for push notifications.
* PM-10600: Unit Test coverage for NotificationHubPushRegistrationService
Fixed IFeatureService substitute mocking for Android tests.
Added user part of organization test with organizationId tags expectation.
* PM-10600: Unit Tests fix to NotificationHubPushRegistrationService after merge conflict
* PM-10600: Organization push notifications not sending to mobile device from self-hosted.
Self-hosted instance uses relay to register the mobile device against Bitwarden Cloud Api. Only the self-hosted server knows client's organization membership, which means it needs to pass in the organization id's information to the relay. Similarly, for Bitwarden Cloud, the organizaton id will come directly from the server.
* PM-10600: Fix self-hosted organization notification not being received by mobile device.
When mobile device registers on self-hosted through the relay, every single id, like user id, device id and now organization id needs to be prefixed with the installation id. This have been missing in the PushController that handles this for organization id.
* PM-10600: Broken NotificationsController integration test
Device type is now part of JWT access token, so the notification center results in the integration test are now scoped to client type web and all.
* PM-10600: Merge conflicts fix
* merge conflict fix
2025-02-12 16:46:30 +01:00
|
|
|
|
claims.Add(new Claim(Claims.DeviceType, device.Type.ToString()));
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
|
|
|
|
|
var customResponse = new Dictionary<string, object>();
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2020-07-16 08:01:39 -04:00
|
|
|
|
customResponse.Add("PrivateKey", user.PrivateKey);
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(user.Key))
|
|
|
|
|
|
{
|
|
|
|
|
|
customResponse.Add("Key", user.Key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
|
2020-07-16 08:01:39 -04:00
|
|
|
|
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset);
|
|
|
|
|
|
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
|
|
|
|
|
|
customResponse.Add("Kdf", (byte)user.Kdf);
|
|
|
|
|
|
customResponse.Add("KdfIterations", user.KdfIterations);
|
2023-01-25 13:56:54 +01:00
|
|
|
|
customResponse.Add("KdfMemory", user.KdfMemory);
|
|
|
|
|
|
customResponse.Add("KdfParallelism", user.KdfParallelism);
|
2023-08-17 16:03:06 -04:00
|
|
|
|
customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context)));
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
|
|
|
|
|
if (sendRememberToken)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2020-07-16 08:01:39 -04:00
|
|
|
|
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
|
|
|
|
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
|
|
|
|
|
|
customResponse.Add("TwoFactorToken", token);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-09-10 16:51:46 -05:00
|
|
|
|
await ResetFailedAuthDetailsAsync(user);
|
2020-07-16 08:01:39 -04:00
|
|
|
|
await SetSuccessResult(context, user, claims, customResponse);
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
2024-12-12 09:08:11 -08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// This does two things, it sets the error result for the current ValidatorContext _and_ it logs error.
|
|
|
|
|
|
/// These two things should be seperated to maintain single concerns.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="message">Error message for the error result</param>
|
|
|
|
|
|
/// <param name="twoFactorRequest">bool that controls how the error is logged</param>
|
|
|
|
|
|
/// <param name="context">used to set the error result in the current validator</param>
|
|
|
|
|
|
/// <param name="user">used to associate the failed login with a user</param>
|
|
|
|
|
|
/// <returns>void</returns>
|
|
|
|
|
|
[Obsolete("Consider using SetValidationErrorResult to set the validation result, and LogFailedLoginEvent " +
|
|
|
|
|
|
"to log the failure.")]
|
2020-10-26 11:56:16 -05:00
|
|
|
|
protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2020-10-26 11:56:16 -05:00
|
|
|
|
if (user != null)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2020-07-16 08:01:39 -04:00
|
|
|
|
await _eventService.LogUserEventAsync(user.Id,
|
2022-08-29 14:53:16 -04:00
|
|
|
|
twoFactorRequest ? EventType.User_FailedLogIn2fa : EventType.User_FailedLogIn);
|
|
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
2021-11-09 16:37:32 +01:00
|
|
|
|
if (_globalSettings.SelfHosted)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2021-11-09 16:37:32 +01:00
|
|
|
|
_logger.LogWarning(Constants.BypassFiltersEventId,
|
|
|
|
|
|
string.Format("Failed login attempt{0}{1}", twoFactorRequest ? ", 2FA invalid." : ".",
|
2023-06-19 10:16:15 -04:00
|
|
|
|
$" {CurrentContext.IpAddress}"));
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
|
|
|
|
|
await Task.Delay(2000); // Delay for brute force.
|
|
|
|
|
|
SetErrorResult(context,
|
2023-05-04 15:12:03 -04:00
|
|
|
|
new Dictionary<string, object> { { "ErrorModel", new ErrorResponseModel(message) } });
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
2024-12-12 09:08:11 -08:00
|
|
|
|
protected async Task LogFailedLoginEvent(User user, EventType eventType)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (user != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _eventService.LogUserEventAsync(user.Id, eventType);
|
|
|
|
|
|
}
|
2022-08-29 14:53:16 -04:00
|
|
|
|
|
2024-12-12 09:08:11 -08:00
|
|
|
|
if (_globalSettings.SelfHosted)
|
|
|
|
|
|
{
|
|
|
|
|
|
string formattedMessage;
|
|
|
|
|
|
switch (eventType)
|
|
|
|
|
|
{
|
|
|
|
|
|
case EventType.User_FailedLogIn:
|
|
|
|
|
|
formattedMessage = string.Format("Failed login attempt. {0}", $" {CurrentContext.IpAddress}");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case EventType.User_FailedLogIn2fa:
|
|
|
|
|
|
formattedMessage = string.Format("Failed login attempt, 2FA invalid.{0}", $" {CurrentContext.IpAddress}");
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
formattedMessage = "Failed login attempt.";
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
_logger.LogWarning(Constants.BypassFiltersEventId, formattedMessage);
|
|
|
|
|
|
}
|
|
|
|
|
|
await Task.Delay(2000); // Delay for brute force.
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Obsolete("Consider using SetValidationErrorResult instead.")]
|
|
|
|
|
|
protected abstract void SetTwoFactorResult(T context, Dictionary<string, object> customResponse);
|
|
|
|
|
|
[Obsolete("Consider using SetValidationErrorResult instead.")]
|
2020-07-16 08:01:39 -04:00
|
|
|
|
protected abstract void SetSsoResult(T context, Dictionary<string, object> customResponse);
|
2024-12-12 09:08:11 -08:00
|
|
|
|
[Obsolete("Consider using SetValidationErrorResult instead.")]
|
|
|
|
|
|
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
|
2022-08-29 14:53:16 -04:00
|
|
|
|
|
2024-12-12 09:08:11 -08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// This consumes the ValidationErrorResult property in the CustomValidatorRequestContext and sets
|
|
|
|
|
|
/// it appropriately in the response object for the token and grant validators.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="context">The current grant or token context</param>
|
|
|
|
|
|
/// <param name="requestContext">The modified request context containing material used to build the response object</param>
|
|
|
|
|
|
protected abstract void SetValidationErrorResult(T context, CustomValidatorRequestContext requestContext);
|
2022-03-02 15:45:00 -06:00
|
|
|
|
protected abstract Task SetSuccessResult(T context, User user, List<Claim> claims,
|
2020-07-16 08:01:39 -04:00
|
|
|
|
Dictionary<string, object> customResponse);
|
2022-08-29 16:06:55 -04:00
|
|
|
|
|
2023-06-26 20:17:39 -04:00
|
|
|
|
protected abstract ClaimsPrincipal GetSubject(T context);
|
2022-08-29 16:06:55 -04:00
|
|
|
|
|
2024-10-24 10:41:25 -07:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are
|
|
|
|
|
|
/// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement.
|
2024-12-12 09:08:11 -08:00
|
|
|
|
/// If the GrantType is authorization_code or client_credentials we know the user is trying to login
|
|
|
|
|
|
/// using the SSO flow so they are allowed to continue.
|
2024-10-24 10:41:25 -07:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="user">user trying to login</param>
|
|
|
|
|
|
/// <param name="grantType">magic string identifying the grant type requested</param>
|
2024-12-12 09:08:11 -08:00
|
|
|
|
/// <returns>true if sso required; false if not required or already in process</returns>
|
|
|
|
|
|
private async Task<bool> RequireSsoLoginAsync(User user, string grantType)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2021-05-10 11:13:37 -05:00
|
|
|
|
if (grantType == "authorization_code" || grantType == "client_credentials")
|
2020-10-26 11:56:16 -05:00
|
|
|
|
{
|
2024-12-12 09:08:11 -08:00
|
|
|
|
// Already using SSO to authenticate, or logging-in via api key to skip SSO requirement
|
|
|
|
|
|
// allow to authenticate successfully
|
|
|
|
|
|
return false;
|
2022-08-29 15:53:48 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-17 16:03:06 -04:00
|
|
|
|
// Check if user belongs to any organization with an active SSO policy
|
2025-04-23 15:43:36 +01:00
|
|
|
|
var ssoRequired = FeatureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
|
|
|
|
|
? (await PolicyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(user.Id))
|
|
|
|
|
|
.SsoRequired
|
|
|
|
|
|
: await PolicyService.AnyPoliciesApplicableToUserAsync(
|
|
|
|
|
|
user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
|
|
|
|
|
if (ssoRequired)
|
2022-08-29 15:53:48 -04:00
|
|
|
|
{
|
2024-12-12 09:08:11 -08:00
|
|
|
|
return true;
|
2022-08-29 14:53:16 -04:00
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
2024-12-12 09:08:11 -08:00
|
|
|
|
// Default - SSO is not required
|
|
|
|
|
|
return false;
|
2022-08-29 15:53:48 -04:00
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
|
2022-03-02 15:45:00 -06:00
|
|
|
|
private async Task ResetFailedAuthDetailsAsync(User user)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2022-03-02 15:45:00 -06:00
|
|
|
|
// Early escape if db hit not necessary
|
|
|
|
|
|
if (user == null || user.FailedLoginCount == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
2022-08-29 14:53:16 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-03-02 15:45:00 -06:00
|
|
|
|
user.FailedLoginCount = 0;
|
|
|
|
|
|
user.RevisionDate = DateTime.UtcNow;
|
|
|
|
|
|
await _userRepository.ReplaceAsync(user);
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-03-02 15:45:00 -06:00
|
|
|
|
private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2022-03-09 12:07:06 -06:00
|
|
|
|
if (user == null)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
2022-03-02 15:45:00 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var utcNow = DateTime.UtcNow;
|
|
|
|
|
|
user.FailedLoginCount = ++user.FailedLoginCount;
|
|
|
|
|
|
user.LastFailedLoginDate = user.RevisionDate = utcNow;
|
|
|
|
|
|
await _userRepository.ReplaceAsync(user);
|
2022-08-29 16:06:55 -04:00
|
|
|
|
|
2022-03-02 15:45:00 -06:00
|
|
|
|
if (ValidateFailedAuthEmailConditions(unknownDevice, user))
|
|
|
|
|
|
{
|
2022-03-09 12:07:06 -06:00
|
|
|
|
if (twoFactorInvalid)
|
|
|
|
|
|
{
|
2023-06-19 10:16:15 -04:00
|
|
|
|
await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
|
2022-03-09 12:07:06 -06:00
|
|
|
|
}
|
2022-03-02 15:45:00 -06:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2023-06-19 10:16:15 -04:00
|
|
|
|
await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
|
2022-03-02 15:45:00 -06:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-08-29 16:06:55 -04:00
|
|
|
|
}
|
2022-05-09 12:25:13 -04:00
|
|
|
|
|
2024-09-05 11:17:15 -07:00
|
|
|
|
/// <summary>
|
2024-10-24 10:41:25 -07:00
|
|
|
|
/// checks to see if a user is trying to log into a new device
|
2024-09-05 11:17:15 -07:00
|
|
|
|
/// and has reached the maximum number of failed login attempts.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="unknownDevice">boolean</param>
|
|
|
|
|
|
/// <param name="user">current user</param>
|
|
|
|
|
|
/// <returns></returns>
|
2022-05-09 12:25:13 -04:00
|
|
|
|
private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
|
|
|
|
|
|
{
|
|
|
|
|
|
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
|
|
|
|
|
|
var failedLoginCount = user?.FailedLoginCount ?? 0;
|
|
|
|
|
|
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
|
2020-07-16 08:01:39 -04:00
|
|
|
|
}
|
2023-04-17 07:35:47 -07:00
|
|
|
|
|
2025-02-13 15:51:36 -05:00
|
|
|
|
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user)
|
2023-04-17 07:35:47 -07:00
|
|
|
|
{
|
|
|
|
|
|
// Check current context/cache to see if user is in any organizations, avoids extra DB call if not
|
2023-06-19 10:16:15 -04:00
|
|
|
|
var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
|
2023-04-17 07:35:47 -07:00
|
|
|
|
.ToList();
|
|
|
|
|
|
|
2024-12-12 09:08:11 -08:00
|
|
|
|
if (orgs.Count == 0)
|
2023-04-17 07:35:47 -07:00
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-06-19 10:16:15 -04:00
|
|
|
|
return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user));
|
2023-04-17 07:35:47 -07:00
|
|
|
|
}
|
2023-06-26 20:17:39 -04:00
|
|
|
|
|
|
|
|
|
|
#nullable enable
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents
|
|
|
|
|
|
/// </summary>
|
2023-08-17 16:03:06 -04:00
|
|
|
|
private async Task<UserDecryptionOptions> CreateUserDecryptionOptionsAsync(User user, Device device, ClaimsPrincipal subject)
|
2023-06-26 20:17:39 -04:00
|
|
|
|
{
|
2023-11-20 15:55:31 +01:00
|
|
|
|
var ssoConfig = await GetSsoConfigurationDataAsync(subject);
|
|
|
|
|
|
return await UserDecryptionOptionsBuilder
|
|
|
|
|
|
.ForUser(user)
|
|
|
|
|
|
.WithDevice(device)
|
|
|
|
|
|
.WithSso(ssoConfig)
|
|
|
|
|
|
.BuildAsync();
|
2023-06-26 20:17:39 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-17 16:03:06 -04:00
|
|
|
|
private async Task<SsoConfig?> GetSsoConfigurationDataAsync(ClaimsPrincipal subject)
|
2023-06-26 20:17:39 -04:00
|
|
|
|
{
|
|
|
|
|
|
var organizationClaim = subject?.FindFirstValue("organizationId");
|
|
|
|
|
|
|
|
|
|
|
|
if (organizationClaim == null || !Guid.TryParse(organizationClaim, out var organizationId))
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var ssoConfig = await SsoConfigRepository.GetByOrganizationIdAsync(organizationId);
|
|
|
|
|
|
if (ssoConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-17 16:03:06 -04:00
|
|
|
|
return ssoConfig;
|
2023-06-26 20:17:39 -04:00
|
|
|
|
}
|
2020-07-16 08:01:39 -04:00
|
|
|
|
}
|