1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05:00

feat : matching request email to session email (#5541)

* feat : matching request email to session email

* feat : implement AuthRequestHeaderValidator

* fix : matching table definitions between migrator and sql project.

* fix : fixing tests
This commit is contained in:
Ike 2025-03-21 18:18:17 -04:00 committed by GitHub
parent 6d4d7c7968
commit 85b299ccfc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 73 additions and 44 deletions

View File

@ -0,0 +1,39 @@
using Bit.Core.Context;
using Bit.Core.Utilities;
namespace Bit.Identity.IdentityServer.RequestValidators;
public class AuthRequestHeaderValidator : IAuthRequestHeaderValidator
{
private readonly ICurrentContext _currentContext;
public AuthRequestHeaderValidator(ICurrentContext currentContext)
{
_currentContext = currentContext;
}
public bool ValidateAuthEmailHeader(string userEmail)
{
if (_currentContext.HttpContext.Request.Headers.TryGetValue("Auth-Email", out var authEmailHeader))
{
try
{
var authEmailDecoded = CoreHelpers.Base64UrlDecodeString(authEmailHeader);
if (authEmailDecoded != userEmail)
{
return false;
}
}
catch (Exception e) when (e is InvalidOperationException || e is FormatException)
{
// Invalid B64 encoding
return false;
}
}
else
{
return false;
}
return true;
}
}

View File

@ -0,0 +1,12 @@
namespace Bit.Identity.IdentityServer.RequestValidators;
public interface IAuthRequestHeaderValidator
{
/// <summary>
/// This method matches the Email in the header the input email. Implementation depends on
/// GrantValidator.
/// </summary>
/// <param name="userEmail">email fetched by grantValidator</param>
/// <returns>true if the emails match false otherwise</returns>
bool ValidateAuthEmailHeader(string userEmail);
}

View File

@ -19,6 +19,7 @@ public class OpaqueKeyExchangeGrantValidator : BaseRequestValidator<ExtensionGra
public const string GrantType = "opaque-ke";
private readonly IOpaqueKeyExchangeService _opaqueKeyExchangeService;
private readonly IFeatureService _featureService;
private readonly IAuthRequestHeaderValidator _authRequestHeaderValidator;
public OpaqueKeyExchangeGrantValidator(
UserManager<User> userManager,
@ -28,15 +29,16 @@ public class OpaqueKeyExchangeGrantValidator : BaseRequestValidator<ExtensionGra
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger<CustomTokenRequestValidator> logger,
ILogger<OpaqueKeyExchangeGrantValidator> logger,
ICurrentContext currentContext,
GlobalSettings globalSettings,
ISsoConfigRepository ssoConfigRepository,
IUserRepository userRepository,
IPolicyService policyService,
IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IOpaqueKeyExchangeService opaqueKeyExchangeService)
IOpaqueKeyExchangeService opaqueKeyExchangeService,
IAuthRequestHeaderValidator authRequestHeaderValidator)
: base(
userManager,
userService,
@ -56,6 +58,7 @@ public class OpaqueKeyExchangeGrantValidator : BaseRequestValidator<ExtensionGra
{
_opaqueKeyExchangeService = opaqueKeyExchangeService;
_featureService = featureService;
_authRequestHeaderValidator = authRequestHeaderValidator;
}
string IExtensionGrantValidator.GrantType => "opaque-ke";
@ -81,10 +84,12 @@ public class OpaqueKeyExchangeGrantValidator : BaseRequestValidator<ExtensionGra
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
// TODO: we need to validate that the email sent up is the same one pulled from the session
// TODO: discuss with Ike if pulling over existing AuthEmailHeaderIsValid logic from
// ResourceOwnerPasswordValidator is best or if we should should refactor in some way.
if (_authRequestHeaderValidator.ValidateAuthEmailHeader(user.Email))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant,
"Auth-Email header invalid.");
return;
}
await ValidateAsync(context, context.Request, new CustomValidatorRequestContext { User = user });
}

View File

@ -23,6 +23,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IDeviceValidator _deviceValidator;
private readonly IAuthRequestHeaderValidator _authRequestHeaderValidator;
public ResourceOwnerPasswordValidator(
UserManager<User> userManager,
IUserService userService,
@ -40,7 +41,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IPolicyService policyService,
IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IAuthRequestHeaderValidator authRequestHeaderValidator)
: base(
userManager,
userService,
@ -63,11 +65,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
_captchaValidationService = captchaValidationService;
_authRequestRepository = authRequestRepository;
_deviceValidator = deviceValidator;
_authRequestHeaderValidator = authRequestHeaderValidator;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
if (!AuthEmailHeaderIsValid(context))
if (!_authRequestHeaderValidator.ValidateAuthEmailHeader(context.UserName))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant,
"Auth-Email header invalid.");
@ -200,30 +203,4 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
{
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;
}
}

View File

@ -23,6 +23,7 @@ public static class ServiceCollectionExtensions
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
services.AddTransient<IDeviceValidator, DeviceValidator>();
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
services.AddTransient<IAuthRequestHeaderValidator, AuthRequestHeaderValidator>();
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
var identityServerBuilder = services

View File

@ -110,7 +110,7 @@ public static class ServiceCollectionExtensions
public static void AddBaseServices(this IServiceCollection services, IGlobalSettings globalSettings)
{
services.AddScoped<ICipherService, CipherService>();
services.AddSingleton<IOpaqueKeyExchangeService, OpaqueKeyExchangeService>();
services.AddScoped<IOpaqueKeyExchangeService, OpaqueKeyExchangeService>();
services.AddUserServices(globalSettings);
services.AddTrialInitiationServices();
services.AddOrganizationServices(globalSettings);

View File

@ -8,7 +8,7 @@
[EncryptedPrivateKey] VARCHAR(MAX) NOT NULL,
[EncryptedUserKey] VARCHAR(MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_OpaqueKeyExchangeCredential] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [PK_OpaqueKeyExchangeCredential] PRIMARY KEY CLUSTERED ([UserId]),
CONSTRAINT [FK_OpaqueKeyExchangeCredential_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
);

View File

@ -43,7 +43,8 @@
"password",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:openid:params:grant-type:ciba",
"webauthn"
"webauthn",
"opaque-ke"
],
"response_types_supported": [
"code",

View File

@ -1,8 +1,3 @@
IF OBJECT_ID('[dbo].[OpaqueKeyExchangeCredential]') IS NULL
BEGIN
CREATE TABLE [dbo].[OpaqueKeyExchangeCredential]
@ -28,7 +23,6 @@ BEGIN
END
GO
CREATE OR ALTER PROCEDURE [dbo].[OpaqueKeyExchangeCredential_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,