1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-25 13:24:50 -05:00

PM-20532 - WIP on SendAccessGrantValidator.cs

This commit is contained in:
Jared Snider 2025-05-23 16:40:36 -04:00
parent e4d5d6d5c4
commit 9976699a4e
No known key found for this signature in database
GPG Key ID: A149DDD612516286
4 changed files with 97 additions and 18 deletions

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Auth.PasswordValidation;
public static class PasswordValidationConstants
{
public const int PasswordHasherKdfIterations = 100000;
}

View File

@ -1,19 +1,24 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Core.Entities;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Duende.IdentityServer.Models; using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity;
namespace Bit.Identity.IdentityServer.RequestValidators; namespace Bit.Identity.IdentityServer.RequestValidators;
// TODO: in real implementation, we would use ideally use Tools provided Send queries for the data we need public class SendAccessGrantValidator(ISendRepository sendRepository, IPasswordHasher<User> passwordHasher) : IExtensionGrantValidator
// instead of directly injecting the send repository here.
public class SendAccessGrantValidator(ISendRepository sendRepository) : IExtensionGrantValidator
{ {
public const string GrantType = "send_access"; public const string GrantType = "send_access";
string IExtensionGrantValidator.GrantType => GrantType; string IExtensionGrantValidator.GrantType => GrantType;
private const string _invalidRequestMissingSendIdMessage = "Invalid request. send_id is required.";
private const string _invalidRequestPasswordRequiredMessage = "Invalid request. Password is required.";
private const string _invalidGrantPasswordInvalid = "Password invalid.";
// TODO: add email OTP validation error messages here.
public async Task ValidateAsync(ExtensionGrantValidationContext context) public async Task ValidateAsync(ExtensionGrantValidationContext context)
{ {
var request = context.Request.Raw; var request = context.Request.Raw;
@ -22,40 +27,59 @@ public class SendAccessGrantValidator(ISendRepository sendRepository) : IExtensi
if (string.IsNullOrEmpty(sendId)) if (string.IsNullOrEmpty(sendId))
{ {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Invalid request. send_id is required."); context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, errorDescription: _invalidRequestMissingSendIdMessage);
return; return;
} }
if (!Guid.TryParse(sendId, out var sendIdGuid)) if (!Guid.TryParse(sendId, out var sendIdGuid))
{ {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Invalid request. send_id is required."); context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, errorDescription: _invalidRequestMissingSendIdMessage);
return; return;
} }
// TODO: replace repository look up & following logic with use of SendAuthenticationQuery.GetAuthenticationMethod from Tools
// See below for example of consumption of SendAuthQuery.GetAuthenticationMethod(sendId)
// Look up send by id // Look up send by id
var send = await sendRepository.GetByIdAsync(sendIdGuid); var send = await sendRepository.GetByIdAsync(sendIdGuid);
// SendAuthQuery from Tools will return authN type + if a send doesn't exist, then we will add enumeration protection
// Only will map to password or email + OTP protected. If user submits password guess for a falsely protected send, then
// return invalid password.
if (send == null) if (send == null)
{ {
// TODO: evaluate if adding send enumeration protection is required here as rate limiting is present already on the token endpoint // TODO: Add send enumeration protection here (primarily benefits self hosted instances).
// Yes, it does help with self hosted instances. We will // We should only map to password or email + OTP protected. If user submits password guess for a
// falsely protected send, then we will return invalid password.
// TODO: we should re-use _invalidGrantPasswordInvalid or similar error message here.
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Invalid request"); context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Invalid request");
return; return;
} }
if (!string.IsNullOrEmpty(send.Password))
// if send is anon, provide access token
if (string.IsNullOrEmpty(send.Password))
{ {
// context.Result = new GrantValidationResult(subject: sendId); // Send is password protected so we need to validate the password.
var password = request.Get("password");
if (string.IsNullOrEmpty(password))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, errorDescription: _invalidRequestPasswordRequiredMessage);
return;
}
var passwordValid = ValidateSendPassword(send.Password, password);
if (!passwordValid)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, errorDescription: _invalidGrantPasswordInvalid);
return;
}
// password is valid, so we can issue an access token.
context.Result = BuildBaseSuccessResult(sendId); context.Result = BuildBaseSuccessResult(sendId);
return; return;
} }
// if send is anon, provide access token
context.Result = BuildBaseSuccessResult(sendId);
// Email + OTP - if we generate OTP here, we could run into rate limiting issues with re-hitting this endpoint // Email + OTP - if we generate OTP here, we could run into rate limiting issues with re-hitting this endpoint
// We will generate & validate OTP here. // We will generate & validate OTP here.
@ -64,8 +88,37 @@ public class SendAccessGrantValidator(ISendRepository sendRepository) : IExtensi
// if send is password protected, check if password is provided and validate if so // if send is password protected, check if password is provided and validate if so
// if send is email + OTP protected, check if email and OTP are provided and validate if so // if send is email + OTP protected, check if email and OTP are provided and validate if so
// TODO: Example Consumption of SendAuthQuery.GetAuthenticationMethod(sendId) to replace above logic.
// var method = await sendAuthQuery.GetAuthenticationMethod(sendId);
//
// switch (method)
// {
// case NeverAuthenticate:
// // null send scenario.
// HandleNullSend(); // this is where we add send enumeration protection
// break;
//
// case NotAuthenticated:
// // automatically issue access token
// break;
//
// case ResourcePassword rp:
// ValidatePassword(rp.Hash);
// break;
//
// case EmailOtp eo:
// We will either send the OTP here or validate it.
// SendOtpToEmails(eo.Emails) or ValidateOtp(eo.Emails);
// break;
//
// default:
// // shouldnt ever hit this
// throw new InvalidOperationException($"Unknown auth method: {method.GetType()}");
// }
context.Result = new GrantValidationResult("send_access", GrantType);
} }
private GrantValidationResult BuildBaseSuccessResult(string sendId) private GrantValidationResult BuildBaseSuccessResult(string sendId)
@ -82,4 +135,15 @@ public class SendAccessGrantValidator(ISendRepository sendRepository) : IExtensi
authenticationMethod: GrantType, authenticationMethod: GrantType,
claims: claims); claims: claims);
} }
private bool ValidateSendPassword(string sendPassword, string userSubmittedPassword)
{
if (string.IsNullOrWhiteSpace(sendPassword) || string.IsNullOrWhiteSpace(userSubmittedPassword))
{
return false;
}
var passwordResult = passwordHasher.VerifyHashedPassword(new User(), sendPassword, userSubmittedPassword);
return passwordResult is PasswordVerificationResult.Success or PasswordVerificationResult.SuccessRehashNeeded;
}
} }

View File

@ -1,4 +1,6 @@
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.PasswordValidation;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.IdentityServer; using Bit.Core.IdentityServer;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -9,6 +11,8 @@ using Bit.SharedWeb.Utilities;
using Duende.IdentityServer.ResponseHandling; using Duende.IdentityServer.ResponseHandling;
using Duende.IdentityServer.Services; using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores; using Duende.IdentityServer.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Bit.Identity.Utilities; namespace Bit.Identity.Utilities;
@ -57,6 +61,10 @@ public static class ServiceCollectionExtensions
.AddExtensionGrantValidator<WebAuthnGrantValidator>() .AddExtensionGrantValidator<WebAuthnGrantValidator>()
.AddExtensionGrantValidator<SendAccessGrantValidator>(); .AddExtensionGrantValidator<SendAccessGrantValidator>();
// ExtensionGrantValidator Dependencies
services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
services.Configure<PasswordHasherOptions>(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);
if (!globalSettings.SelfHosted) if (!globalSettings.SelfHosted)
{ {
// Only cloud instances should be able to handle installations // Only cloud instances should be able to handle installations

View File

@ -16,6 +16,7 @@ using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.IdentityServer; using Bit.Core.Auth.IdentityServer;
using Bit.Core.Auth.LoginFeatures; using Bit.Core.Auth.LoginFeatures;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.PasswordValidation;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services; using Bit.Core.Auth.Services;
using Bit.Core.Auth.Services.Implementations; using Bit.Core.Auth.Services.Implementations;
@ -443,7 +444,7 @@ public static class ServiceCollectionExtensions
this IServiceCollection services, GlobalSettings globalSettings) this IServiceCollection services, GlobalSettings globalSettings)
{ {
services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>(); services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>();
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 100000); services.Configure<PasswordHasherOptions>(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);
services.Configure<TwoFactorRememberTokenProviderOptions>(options => services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
{ {
options.TokenLifespan = TimeSpan.FromDays(30); options.TokenLifespan = TimeSpan.FromDays(30);