1
0
mirror of https://github.com/bitwarden/server synced 2026-01-02 08:33:48 +00:00

feat(auth-validator): [PM-22975] Client Version Validator - initial implementation

This commit is contained in:
Patrick Pimentel
2025-11-17 15:46:02 -05:00
parent d1fecc2a0f
commit 1c4fd6ca24
20 changed files with 769 additions and 31 deletions

View File

@@ -40,6 +40,7 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly IUserRepository _userRepository;
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IMailService _mailService;
private readonly IClientVersionValidator _clientVersionValidator;
protected ICurrentContext CurrentContext { get; }
protected IPolicyService PolicyService { get; }
@@ -68,7 +69,8 @@ public abstract class BaseRequestValidator<T> where T : class
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository,
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery
IUserAccountKeysQuery userAccountKeysQuery,
IClientVersionValidator clientVersionValidator
)
{
_userManager = userManager;
@@ -89,6 +91,7 @@ public abstract class BaseRequestValidator<T> where T : class
_authRequestRepository = authRequestRepository;
_mailService = mailService;
_accountKeysQuery = userAccountKeysQuery;
_clientVersionValidator = clientVersionValidator;
}
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
@@ -259,6 +262,7 @@ public abstract class BaseRequestValidator<T> where T : class
return
[
() => ValidateMasterPasswordAsync(context, validatorContext),
() => ValidateClientVersionAsync(context, validatorContext),
() => ValidateTwoFactorAsync(context, request, validatorContext),
() => ValidateSsoAsync(context, request, validatorContext),
() => ValidateNewDeviceAsync(context, request, validatorContext),
@@ -272,6 +276,7 @@ public abstract class BaseRequestValidator<T> where T : class
return
[
() => ValidateMasterPasswordAsync(context, validatorContext),
() => ValidateClientVersionAsync(context, validatorContext),
() => ValidateSsoAsync(context, request, validatorContext),
() => ValidateTwoFactorAsync(context, request, validatorContext),
() => ValidateNewDeviceAsync(context, request, validatorContext),
@@ -323,6 +328,24 @@ public abstract class BaseRequestValidator<T> where T : class
return true;
}
/// <summary>
/// Validates whether the client version is compatible for the user attempting to authenticate.
/// New authentications only; refresh/device grants are handled elsewhere.
/// </summary>
/// <returns>true if the scheme successfully passed validation, otherwise false.</returns>
private async Task<bool> ValidateClientVersionAsync(T context, CustomValidatorRequestContext validatorContext)
{
var ok = await _clientVersionValidator.ValidateAsync(validatorContext.User, validatorContext);
if (ok)
{
return true;
}
SetValidationErrorResult(context, validatorContext);
await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn);
return false;
}
/// <summary>
/// Validates the user's Master Password hash.
/// </summary>

View File

@@ -0,0 +1,55 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Models.Api;
using Duende.IdentityServer.Validation;
namespace Bit.Identity.IdentityServer.RequestValidators;
public interface IClientVersionValidator
{
Task<bool> ValidateAsync(User user, CustomValidatorRequestContext requestContext);
}
public class ClientVersionValidator(ICurrentContext currentContext,
IGetMinimumClientVersionForUserQuery getMinimumClientVersionForUserQuery)
: IClientVersionValidator
{
private static readonly string UpgradeMessage = "Please update your app to continue using Bitwarden";
public async Task<bool> ValidateAsync(User? user, CustomValidatorRequestContext requestContext)
{
if (user == null)
{
return true;
}
var clientVersion = currentContext.ClientVersion;
var minVersion = await getMinimumClientVersionForUserQuery.Run(user);
// Fail-open if headers are missing or no restriction
if (minVersion == null)
{
return true;
}
if (clientVersion < minVersion)
{
requestContext.ValidationErrorResult = new ValidationResult
{
Error = "invalid_grant",
ErrorDescription = UpgradeMessage,
IsError = true
};
requestContext.CustomResponse = new Dictionary<string, object>
{
{ "ErrorModel", new ErrorResponseModel(UpgradeMessage) }
};
return false;
}
return true;
}
}

View File

@@ -49,7 +49,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository,
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery)
IUserAccountKeysQuery userAccountKeysQuery,
IClientVersionValidator clientVersionValidator)
: base(
userManager,
userService,
@@ -68,7 +69,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
policyRequirementQuery,
authRequestRepository,
mailService,
userAccountKeysQuery)
userAccountKeysQuery,
clientVersionValidator)
{
_userManager = userManager;
_updateInstallationCommand = updateInstallationCommand;

View File

@@ -43,7 +43,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IPolicyRequirementQuery policyRequirementQuery,
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery)
IUserAccountKeysQuery userAccountKeysQuery,
IClientVersionValidator clientVersionValidator)
: base(
userManager,
userService,
@@ -62,7 +63,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
policyRequirementQuery,
authRequestRepository,
mailService,
userAccountKeysQuery)
userAccountKeysQuery,
clientVersionValidator)
{
_userManager = userManager;
_currentContext = currentContext;

View File

@@ -52,7 +52,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository,
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery)
IUserAccountKeysQuery userAccountKeysQuery,
IClientVersionValidator clientVersionValidator)
: base(
userManager,
userService,
@@ -71,7 +72,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
policyRequirementQuery,
authRequestRepository,
mailService,
userAccountKeysQuery)
userAccountKeysQuery,
clientVersionValidator)
{
_assertionOptionsDataProtector = assertionOptionsDataProtector;
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;

View File

@@ -25,6 +25,7 @@ public static class ServiceCollectionExtensions
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
services.AddTransient<IDeviceValidator, DeviceValidator>();
services.AddTransient<IClientVersionValidator, ClientVersionValidator>();
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();
services.AddTransient<ISendAuthenticationMethodValidator<ResourcePassword>, SendPasswordRequestValidator>();