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:
parent
e4d5d6d5c4
commit
9976699a4e
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Auth.PasswordValidation;
|
||||
|
||||
public static class PasswordValidationConstants
|
||||
{
|
||||
public const int PasswordHasherKdfIterations = 100000;
|
||||
}
|
@ -1,19 +1,24 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
// TODO: in real implementation, we would use ideally use Tools provided Send queries for the data we need
|
||||
// instead of directly injecting the send repository here.
|
||||
public class SendAccessGrantValidator(ISendRepository sendRepository) : IExtensionGrantValidator
|
||||
public class SendAccessGrantValidator(ISendRepository sendRepository, IPasswordHasher<User> passwordHasher) : IExtensionGrantValidator
|
||||
{
|
||||
public const string GrantType = "send_access";
|
||||
|
||||
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)
|
||||
{
|
||||
var request = context.Request.Raw;
|
||||
@ -22,40 +27,59 @@ public class SendAccessGrantValidator(ISendRepository sendRepository) : IExtensi
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
{
|
||||
// TODO: evaluate if adding send enumeration protection is required here as rate limiting is present already on the token endpoint
|
||||
// Yes, it does help with self hosted instances. We will
|
||||
// TODO: Add send enumeration protection here (primarily benefits self hosted instances).
|
||||
// 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");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// if send is anon, provide access token
|
||||
if (string.IsNullOrEmpty(send.Password))
|
||||
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);
|
||||
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
|
||||
// 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 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:
|
||||
// // shouldn’t ever hit this
|
||||
// throw new InvalidOperationException($"Unknown auth method: {method.GetType()}");
|
||||
// }
|
||||
|
||||
|
||||
|
||||
context.Result = new GrantValidationResult("send_access", GrantType);
|
||||
}
|
||||
|
||||
private GrantValidationResult BuildBaseSuccessResult(string sendId)
|
||||
@ -82,4 +135,15 @@ public class SendAccessGrantValidator(ISendRepository sendRepository) : IExtensi
|
||||
authenticationMethod: GrantType,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
@ -9,6 +11,8 @@ using Bit.SharedWeb.Utilities;
|
||||
using Duende.IdentityServer.ResponseHandling;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Bit.Identity.Utilities;
|
||||
|
||||
@ -57,6 +61,10 @@ public static class ServiceCollectionExtensions
|
||||
.AddExtensionGrantValidator<WebAuthnGrantValidator>()
|
||||
.AddExtensionGrantValidator<SendAccessGrantValidator>();
|
||||
|
||||
// ExtensionGrantValidator Dependencies
|
||||
services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
|
||||
services.Configure<PasswordHasherOptions>(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);
|
||||
|
||||
if (!globalSettings.SelfHosted)
|
||||
{
|
||||
// Only cloud instances should be able to handle installations
|
||||
|
@ -16,6 +16,7 @@ using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Auth.LoginFeatures;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.PasswordValidation;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Auth.Services.Implementations;
|
||||
@ -443,7 +444,7 @@ public static class ServiceCollectionExtensions
|
||||
this IServiceCollection services, GlobalSettings globalSettings)
|
||||
{
|
||||
services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>();
|
||||
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 100000);
|
||||
services.Configure<PasswordHasherOptions>(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);
|
||||
services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
|
||||
{
|
||||
options.TokenLifespan = TimeSpan.FromDays(30);
|
||||
|
Loading…
x
Reference in New Issue
Block a user