Skip to content

Commit 3097e7f

Browse files
[PM- 22675] Send password auth method (#6228)
* feat: add Passwordvalidation * fix: update strings to constants * fix: add customResponse for rust consumption * test: add tests for SendPasswordValidator. fix: update tests for SendAccessGrantValidator * feat: update send access constants.
1 parent 50b36bd commit 3097e7f

File tree

10 files changed

+647
-76
lines changed

10 files changed

+647
-76
lines changed

src/Core/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
using System.Runtime.CompilerServices;
22

33
[assembly: InternalsVisibleTo("Core.Test")]
4+
[assembly: InternalsVisibleTo("Identity.IntegrationTest")]

src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendGrantValidatorResultTypes.cs

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendPasswordValidatorResultTypes.cs

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Duende.IdentityServer.Validation;
2+
3+
namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
4+
5+
/// <summary>
6+
/// String constants for the Send Access user feature
7+
/// </summary>
8+
public static class SendAccessConstants
9+
{
10+
/// <summary>
11+
/// A catch all error type for send access related errors. Used mainly in the <see cref="GrantValidationResult.CustomResponse"/>
12+
/// </summary>
13+
public const string SendAccessError = "send_access_error_type";
14+
public static class TokenRequest
15+
{
16+
/// <summary>
17+
/// used to fetch Send from database.
18+
/// </summary>
19+
public const string SendId = "send_id";
20+
/// <summary>
21+
/// used to validate Send protected passwords
22+
/// </summary>
23+
public const string ClientB64HashedPassword = "password_hash_b64";
24+
/// <summary>
25+
/// email used to see if email is associated with the Send
26+
/// </summary>
27+
public const string Email = "email";
28+
/// <summary>
29+
/// Otp code sent to email associated with the Send
30+
/// </summary>
31+
public const string Otp = "otp";
32+
}
33+
34+
public static class GrantValidatorResults
35+
{
36+
/// <summary>
37+
/// The sendId is valid and the request is well formed.
38+
/// </summary>
39+
public const string ValidSendGuid = "valid_send_guid";
40+
/// <summary>
41+
/// The sendId is missing from the request.
42+
/// </summary>
43+
public const string MissingSendId = "send_id_required";
44+
/// <summary>
45+
/// The sendId is invalid, does not match a known send.
46+
/// </summary>
47+
public const string InvalidSendId = "send_id_invalid";
48+
}
49+
50+
public static class PasswordValidatorResults
51+
{
52+
/// <summary>
53+
/// The passwordHashB64 does not match the send's password hash.
54+
/// </summary>
55+
public const string RequestPasswordDoesNotMatch = "password_hash_b64_invalid";
56+
/// <summary>
57+
/// The passwordHashB64 is missing from the request.
58+
/// </summary>
59+
public const string RequestPasswordIsRequired = "password_hash_b64_required";
60+
}
61+
62+
public static class EmailOtpValidatorResults
63+
{
64+
/// <summary>
65+
/// Represents the error code indicating that an email address is required.
66+
/// </summary>
67+
public const string EmailRequired = "email_required";
68+
/// <summary>
69+
/// Represents the status indicating that both email and OTP are required, and the OTP has been sent.
70+
/// </summary>
71+
public const string EmailOtpSent = "email_and_otp_required_otp_sent";
72+
}
73+
}

src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
77
using Bit.Core.Utilities;
88
using Bit.Identity.IdentityServer.Enums;
9-
using Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums;
109
using Duende.IdentityServer.Models;
1110
using Duende.IdentityServer.Validation;
1211

@@ -20,11 +19,11 @@ public class SendAccessGrantValidator(
2019
{
2120
string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess;
2221

23-
private static readonly Dictionary<SendGrantValidatorResultTypes, string>
24-
_sendGrantValidatorErrors = new()
22+
private static readonly Dictionary<string, string>
23+
_sendGrantValidatorErrorDescriptions = new()
2524
{
26-
{ SendGrantValidatorResultTypes.MissingSendId, "send_id is required." },
27-
{ SendGrantValidatorResultTypes.InvalidSendId, "send_id is invalid." }
25+
{ SendAccessConstants.GrantValidatorResults.MissingSendId, $"{SendAccessConstants.TokenRequest.SendId} is required." },
26+
{ SendAccessConstants.GrantValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." }
2827
};
2928

3029

@@ -38,7 +37,7 @@ public async Task ValidateAsync(ExtensionGrantValidationContext context)
3837
}
3938

4039
var (sendIdGuid, result) = GetRequestSendId(context);
41-
if (result != SendGrantValidatorResultTypes.ValidSendGuid)
40+
if (result != SendAccessConstants.GrantValidatorResults.ValidSendGuid)
4241
{
4342
context.Result = BuildErrorResult(result);
4443
return;
@@ -55,7 +54,7 @@ public async Task ValidateAsync(ExtensionGrantValidationContext context)
5554
// We should only map to password or email + OTP protected.
5655
// If user submits password guess for a falsely protected send, then we will return invalid password.
5756
// If user submits email + OTP guess for a falsely protected send, then we will return email sent, do not actually send an email.
58-
context.Result = BuildErrorResult(SendGrantValidatorResultTypes.InvalidSendId);
57+
context.Result = BuildErrorResult(SendAccessConstants.GrantValidatorResults.InvalidSendId);
5958
return;
6059

6160
case NotAuthenticated:
@@ -64,7 +63,7 @@ public async Task ValidateAsync(ExtensionGrantValidationContext context)
6463
return;
6564

6665
case ResourcePassword rp:
67-
// TODO PM-22675: Validate if the password is correct.
66+
// Validate if the password is correct, or if we need to respond with a 400 stating a password has is required
6867
context.Result = _sendPasswordRequestValidator.ValidateSendPassword(context, rp, sendIdGuid);
6968
return;
7069
case EmailOtp eo:
@@ -84,15 +83,15 @@ public async Task ValidateAsync(ExtensionGrantValidationContext context)
8483
/// </summary>
8584
/// <param name="context">request context</param>
8685
/// <returns>a parsed sendId Guid and success result or a Guid.Empty and error type otherwise</returns>
87-
private static (Guid, SendGrantValidatorResultTypes) GetRequestSendId(ExtensionGrantValidationContext context)
86+
private static (Guid, string) GetRequestSendId(ExtensionGrantValidationContext context)
8887
{
8988
var request = context.Request.Raw;
90-
var sendId = request.Get("send_id");
89+
var sendId = request.Get(SendAccessConstants.TokenRequest.SendId);
9190

9291
// if the sendId is null then the request is the wrong shape and the request is invalid
9392
if (sendId == null)
9493
{
95-
return (Guid.Empty, SendGrantValidatorResultTypes.MissingSendId);
94+
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.MissingSendId);
9695
}
9796
// the send_id is not null so the request is the correct shape, so we will attempt to parse it
9897
try
@@ -102,13 +101,13 @@ private static (Guid, SendGrantValidatorResultTypes) GetRequestSendId(ExtensionG
102101
// Guid.Empty indicates an invalid send_id return invalid grant
103102
if (sendGuid == Guid.Empty)
104103
{
105-
return (Guid.Empty, SendGrantValidatorResultTypes.InvalidSendId);
104+
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId);
106105
}
107-
return (sendGuid, SendGrantValidatorResultTypes.ValidSendGuid);
106+
return (sendGuid, SendAccessConstants.GrantValidatorResults.ValidSendGuid);
108107
}
109108
catch
110109
{
111-
return (Guid.Empty, SendGrantValidatorResultTypes.InvalidSendId);
110+
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId);
112111
}
113112
}
114113

@@ -117,18 +116,26 @@ private static (Guid, SendGrantValidatorResultTypes) GetRequestSendId(ExtensionG
117116
/// </summary>
118117
/// <param name="error">The error type.</param>
119118
/// <returns>The error result.</returns>
120-
private static GrantValidationResult BuildErrorResult(SendGrantValidatorResultTypes error)
119+
private static GrantValidationResult BuildErrorResult(string error)
121120
{
122121
return error switch
123122
{
124123
// Request is the wrong shape
125-
SendGrantValidatorResultTypes.MissingSendId => new GrantValidationResult(
124+
SendAccessConstants.GrantValidatorResults.MissingSendId => new GrantValidationResult(
126125
TokenRequestErrors.InvalidRequest,
127-
errorDescription: _sendGrantValidatorErrors[SendGrantValidatorResultTypes.MissingSendId]),
126+
errorDescription: _sendGrantValidatorErrorDescriptions[SendAccessConstants.GrantValidatorResults.MissingSendId],
127+
new Dictionary<string, object>
128+
{
129+
{ SendAccessConstants.SendAccessError, SendAccessConstants.GrantValidatorResults.MissingSendId}
130+
}),
128131
// Request is correct shape but data is bad
129-
SendGrantValidatorResultTypes.InvalidSendId => new GrantValidationResult(
132+
SendAccessConstants.GrantValidatorResults.InvalidSendId => new GrantValidationResult(
130133
TokenRequestErrors.InvalidGrant,
131-
errorDescription: _sendGrantValidatorErrors[SendGrantValidatorResultTypes.InvalidSendId]),
134+
errorDescription: _sendGrantValidatorErrorDescriptions[SendAccessConstants.GrantValidatorResults.InvalidSendId],
135+
new Dictionary<string, object>
136+
{
137+
{ SendAccessConstants.SendAccessError, SendAccessConstants.GrantValidatorResults.InvalidSendId }
138+
}),
132139
// should never get here
133140
_ => new GrantValidationResult(TokenRequestErrors.InvalidRequest)
134141
};

src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using Bit.Core.KeyManagement.Sends;
44
using Bit.Core.Tools.Models.Data;
55
using Bit.Identity.IdentityServer.Enums;
6-
using Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums;
76
using Duende.IdentityServer.Models;
87
using Duende.IdentityServer.Validation;
98

@@ -16,31 +15,44 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher
1615
/// <summary>
1716
/// static object that contains the error messages for the SendPasswordRequestValidator.
1817
/// </summary>
19-
private static Dictionary<SendPasswordValidatorResultTypes, string> _sendPasswordValidatorErrors = new()
18+
private static readonly Dictionary<string, string> _sendPasswordValidatorErrorDescriptions = new()
2019
{
21-
{ SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch, "Request Password hash is invalid." }
20+
{ SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, $"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid." },
21+
{ SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, $"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required." }
2222
};
2323

2424
public GrantValidationResult ValidateSendPassword(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId)
2525
{
2626
var request = context.Request.Raw;
27-
var clientHashedPassword = request.Get("password_hash");
27+
var clientHashedPassword = request.Get(SendAccessConstants.TokenRequest.ClientB64HashedPassword);
2828

29-
if (string.IsNullOrEmpty(clientHashedPassword))
29+
// It is an invalid request _only_ if the passwordHashB64 is missing which indicated bad shape.
30+
if (clientHashedPassword == null)
3031
{
32+
// Request is the wrong shape and doesn't contain a passwordHashB64 field.
3133
return new GrantValidationResult(
3234
TokenRequestErrors.InvalidRequest,
33-
errorDescription: _sendPasswordValidatorErrors[SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch]);
35+
errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired],
36+
new Dictionary<string, object>
37+
{
38+
{ SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired }
39+
});
3440
}
3541

42+
// _sendPasswordHasher.PasswordHashMatches checks for an empty string so no need to do it before we make the call.
3643
var hashMatches = _sendPasswordHasher.PasswordHashMatches(
3744
resourcePassword.Hash, clientHashedPassword);
3845

3946
if (!hashMatches)
4047
{
48+
// Request is the correct shape but the passwordHashB64 doesn't match, hash could be empty.
4149
return new GrantValidationResult(
4250
TokenRequestErrors.InvalidGrant,
43-
errorDescription: _sendPasswordValidatorErrors[SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch]);
51+
errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch],
52+
new Dictionary<string, object>
53+
{
54+
{ SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch }
55+
});
4456
}
4557

4658
return BuildSendPasswordSuccessResult(sendId);

test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Bit.Identity.IdentityServer.Enums;
99
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
1010
using Bit.IntegrationTestCommon.Factories;
11+
using Duende.IdentityModel;
1112
using Duende.IdentityServer.Validation;
1213
using NSubstitute;
1314
using Xunit;
@@ -96,17 +97,17 @@ public async Task SendAccessGrant_MissingSendId_ReturnsInvalidRequest()
9697
}).CreateClient();
9798

9899
var requestBody = new FormUrlEncodedContent([
99-
new KeyValuePair<string, string>("grant_type", CustomGrantTypes.SendAccess),
100-
new KeyValuePair<string, string>("client_id", BitwardenClient.Send)
100+
new KeyValuePair<string, string>(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
101+
new KeyValuePair<string, string>(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send)
101102
]);
102103

103104
// Act
104105
var response = await client.PostAsync("/connect/token", requestBody);
105106

106107
// Assert
107108
var content = await response.Content.ReadAsStringAsync();
108-
Assert.Contains("invalid_request", content);
109-
Assert.Contains("send_id is required", content);
109+
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
110+
Assert.Contains($"{SendAccessConstants.TokenRequest.SendId} is required", content);
110111
}
111112

112113
[Fact]
@@ -245,16 +246,16 @@ private static FormUrlEncodedContent CreateTokenRequestBody(
245246
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
246247
var parameters = new List<KeyValuePair<string, string>>
247248
{
248-
new("grant_type", CustomGrantTypes.SendAccess),
249-
new("client_id", BitwardenClient.Send ),
250-
new("scope", ApiScopes.ApiSendAccess),
249+
new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
250+
new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ),
251+
new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
251252
new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()),
252-
new("send_id", sendIdBase64)
253+
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64)
253254
};
254255

255256
if (!string.IsNullOrEmpty(password))
256257
{
257-
parameters.Add(new("password_hash", password));
258+
parameters.Add(new(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password));
258259
}
259260

260261
if (!string.IsNullOrEmpty(emailOtp) && !string.IsNullOrEmpty(sendEmail))

0 commit comments

Comments
 (0)