From c7603e71a53d841abb0194a3b1d5f1fb9b3b5344 Mon Sep 17 00:00:00 2001 From: Jared Snider Date: Thu, 15 May 2025 22:39:19 -0400 Subject: [PATCH] PM-20532 - SendAccessGrantValidator - WIP --- .../SendAccessGrantValidator.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccessGrantValidator.cs diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccessGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccessGrantValidator.cs new file mode 100644 index 0000000000..f2d7d5af08 --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccessGrantValidator.cs @@ -0,0 +1,85 @@ +using System.Security.Claims; +using Bit.Core.Identity; +using Bit.Core.Tools.Repositories; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; + +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 const string GrantType = "send_access"; + + string IExtensionGrantValidator.GrantType => GrantType; + + public async Task ValidateAsync(ExtensionGrantValidationContext context) + { + var request = context.Request.Raw; + + var sendId = request.Get("send_id"); + + if (string.IsNullOrEmpty(sendId)) + { + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Invalid request. send_id is required."); + return; + } + + if (!Guid.TryParse(sendId, out var sendIdGuid)) + { + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Invalid request. send_id is required."); + return; + } + + // 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 + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Invalid request"); + return; + } + + + // if send is anon, provide access token + if (string.IsNullOrEmpty(send.Password)) + { + // context.Result = new GrantValidationResult(subject: sendId); + context.Result = BuildBaseSuccessResult(sendId); + return; + } + + + // 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. + + + // 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 + + + context.Result = new GrantValidationResult("send_access", GrantType); + } + + private GrantValidationResult BuildBaseSuccessResult(string sendId) + { + var claims = new List + { + // TODO: Add email claim when issuing access token for email + OTP send + new Claim(Claims.SendId, sendId), + new Claim(Claims.Type, IdentityClientType.Send.ToString()) + }; + + return new GrantValidationResult( + subject: sendId, + authenticationMethod: GrantType, + claims: claims); + } +}