mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-22678] Send email otp authentication method (#6255)
feat(auth): email OTP validation, and generalize authentication interface - Generalized send authentication method interface - Made validate method async - Added email mail support for Handlebars - Modified email templates to match future implementation fix(auth): update constants, naming conventions, and error handling - Renamed constants for clarity - Updated claims naming convention - Fixed error message generation - Added customResponse for Rust consumption test(auth): add and fix tests for validators and email - Added tests for SendEmailOtpRequestValidator - Updated tests for SendAccessGrantValidator chore: apply dotnet formatting
This commit is contained in:
@@ -12,7 +12,7 @@ public class SendAccessClaimsPrincipalExtensionsTests
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var claims = new[] { new Claim(Claims.SendId, guid.ToString()) };
|
||||
var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, guid.ToString()) };
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
@@ -30,19 +30,19 @@ public class SendAccessClaimsPrincipalExtensionsTests
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => principal.GetSendId());
|
||||
Assert.Equal("Send ID claim not found.", ex.Message);
|
||||
Assert.Equal("send_id claim not found.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSendId_ThrowsInvalidOperationException_WhenClaimValueIsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var claims = new[] { new Claim(Claims.SendId, "not-a-guid") };
|
||||
var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, "not-a-guid") };
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => principal.GetSendId());
|
||||
Assert.Equal("Invalid Send ID claim value.", ex.Message);
|
||||
Assert.Equal("Invalid send_id claim value.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -247,11 +247,18 @@ public class HandlebarsMailServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
// Remove this test when we add actual tests. It only proves that
|
||||
// we've properly constructed the system under test.
|
||||
[Fact]
|
||||
public void ServiceExists()
|
||||
public async Task SendSendEmailOtpEmailAsync_SendsEmail()
|
||||
{
|
||||
Assert.NotNull(_sut);
|
||||
// Arrange
|
||||
var email = "test@example.com";
|
||||
var token = "aToken";
|
||||
var subject = string.Format("Your Bitwarden Send verification code is {0}", token);
|
||||
|
||||
// Act
|
||||
await _sut.SendSendEmailOtpEmailAsync(email, token, subject);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any<MailMessage>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,8 +213,8 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock password validator to return success
|
||||
var passwordValidator = Substitute.For<ISendPasswordRequestValidator>();
|
||||
passwordValidator.ValidateSendPassword(
|
||||
var passwordValidator = Substitute.For<ISendAuthenticationMethodValidator<ResourcePassword>>();
|
||||
passwordValidator.ValidateRequestAsync(
|
||||
Arg.Any<ExtensionGrantValidationContext>(),
|
||||
Arg.Any<ResourcePassword>(),
|
||||
Arg.Any<Guid>())
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Duende.IdentityModel;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation;
|
||||
|
||||
public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
private readonly IdentityApplicationFactory _factory;
|
||||
|
||||
public SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_MissingEmail_ReturnsInvalidRequest()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(["test@example.com"]));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId); // No email
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
Assert.Contains("email is required", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_EmailWithoutOtp_SendsOtpEmail()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
var generatedToken = "123456";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp([email]));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(generatedToken);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
// Mock mail service
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
Assert.Contains("email otp sent", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_ValidOtp_ReturnsAccessToken()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
var otp = "123456";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to validate successfully
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.ValidateTokenAsync(otp, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(true);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: otp);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
Assert.True(response.IsSuccessStatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenResponse.AccessToken, content);
|
||||
Assert.Contains(OidcConstants.TokenResponse.BearerTokenType, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_InvalidOtp_ReturnsInvalidGrant()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
var invalidOtp = "wrong123";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to validate as false
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.ValidateTokenAsync(invalidOtp, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(false);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: invalidOtp);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||
Assert.Contains("email otp is invalid", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_OtpGenerationFails_ReturnsInvalidRequest()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to fail generation
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns((string)null);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
}
|
||||
|
||||
private static FormUrlEncodedContent CreateTokenRequestBody(Guid sendId,
|
||||
string sendEmail = null, string emailOtp = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
var parameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||
new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ),
|
||||
new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
|
||||
new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()),
|
||||
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(sendEmail))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(
|
||||
SendAccessConstants.TokenRequest.Email, sendEmail));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(emailOtp))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(
|
||||
SendAccessConstants.TokenRequest.Otp, emailOtp));
|
||||
}
|
||||
|
||||
return new FormUrlEncodedContent(parameters);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ using Duende.IdentityServer.Validation;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer;
|
||||
namespace Bit.Identity.Test.IdentityServer.SendAccess;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SendAccessGrantValidatorTests
|
||||
@@ -167,7 +167,7 @@ public class SendAccessGrantValidatorTests
|
||||
// get the claims from the subject
|
||||
var claims = subject.Claims.ToList();
|
||||
Assert.NotEmpty(claims);
|
||||
Assert.Contains(claims, c => c.Type == Claims.SendId && c.Value == sendId.ToString());
|
||||
Assert.Contains(claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString());
|
||||
Assert.Contains(claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString());
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ public class SendAccessGrantValidatorTests
|
||||
.GetAuthenticationMethod(sendId)
|
||||
.Returns(resourcePassword);
|
||||
|
||||
sutProvider.GetDependency<ISendPasswordRequestValidator>()
|
||||
.ValidateSendPassword(context, resourcePassword, sendId)
|
||||
sutProvider.GetDependency<ISendAuthenticationMethodValidator<ResourcePassword>>()
|
||||
.ValidateRequestAsync(context, resourcePassword, sendId)
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
@@ -198,15 +198,16 @@ public class SendAccessGrantValidatorTests
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, context.Result);
|
||||
sutProvider.GetDependency<ISendPasswordRequestValidator>()
|
||||
await sutProvider.GetDependency<ISendAuthenticationMethodValidator<ResourcePassword>>()
|
||||
.Received(1)
|
||||
.ValidateSendPassword(context, resourcePassword, sendId);
|
||||
.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EmailOtpMethod_NotImplemented_ThrowsError(
|
||||
public async Task ValidateAsync_EmailOtpMethod_CallsEmailOtp(
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
SutProvider<SendAccessGrantValidator> sutProvider,
|
||||
GrantValidationResult expectedResult,
|
||||
Guid sendId,
|
||||
EmailOtp emailOtp)
|
||||
{
|
||||
@@ -216,15 +217,22 @@ public class SendAccessGrantValidatorTests
|
||||
sendId,
|
||||
tokenRequest);
|
||||
|
||||
|
||||
sutProvider.GetDependency<ISendAuthenticationQuery>()
|
||||
.GetAuthenticationMethod(sendId)
|
||||
.Returns(emailOtp);
|
||||
|
||||
sutProvider.GetDependency<ISendAuthenticationMethodValidator<EmailOtp>>()
|
||||
.ValidateRequestAsync(context, emailOtp, sendId)
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
// Currently the EmailOtp case doesn't set a result, so it should be null
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.ValidateAsync(context));
|
||||
Assert.Equal(expectedResult, context.Result);
|
||||
await sutProvider.GetDependency<ISendAuthenticationMethodValidator<EmailOtp>>()
|
||||
.Received(1)
|
||||
.ValidateRequestAsync(context, emailOtp, sendId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -256,7 +264,7 @@ public class SendAccessGrantValidatorTests
|
||||
public void GrantType_ReturnsCorrectType()
|
||||
{
|
||||
// Arrange & Act
|
||||
var validator = new SendAccessGrantValidator(null!, null!, null!);
|
||||
var validator = new SendAccessGrantValidator(null!, null!, null!, null!);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType);
|
||||
@@ -0,0 +1,310 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer.SendAccess;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SendEmailOtpRequestValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_MissingEmail_ReturnsInvalidRequest(
|
||||
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
EmailOtp emailOtp,
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
||||
Assert.Equal("email is required.", result.ErrorDescription);
|
||||
|
||||
// Verify no OTP generation or email sending occurred
|
||||
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.DidNotReceive()
|
||||
.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendSendEmailOtpEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_EmailNotInList_ReturnsInvalidRequest(
|
||||
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
EmailOtp emailOtp,
|
||||
string email,
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
|
||||
var emailOTP = new EmailOtp(["user@test.dev"]);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
Assert.Equal("email is invalid.", result.ErrorDescription);
|
||||
|
||||
// Verify no OTP generation or email sending occurred
|
||||
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.DidNotReceive()
|
||||
.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendSendEmailOtpEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_EmailWithoutOtp_GeneratesAndSendsOtp(
|
||||
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
EmailOtp emailOtp,
|
||||
Guid sendId,
|
||||
string email,
|
||||
string generatedToken)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);
|
||||
|
||||
sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.GenerateTokenAsync(
|
||||
SendAccessConstants.OtpToken.TokenProviderName,
|
||||
SendAccessConstants.OtpToken.Purpose,
|
||||
expectedUniqueId)
|
||||
.Returns(generatedToken);
|
||||
|
||||
emailOtp = emailOtp with { Emails = [email] };
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
||||
Assert.Equal("email otp sent.", result.ErrorDescription);
|
||||
|
||||
// Verify OTP generation
|
||||
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.Received(1)
|
||||
.GenerateTokenAsync(
|
||||
SendAccessConstants.OtpToken.TokenProviderName,
|
||||
SendAccessConstants.OtpToken.Purpose,
|
||||
expectedUniqueId);
|
||||
|
||||
// Verify email sending
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendSendEmailOtpEmailAsync(email, generatedToken, Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_OtpGenerationFails_ReturnsGenerationFailedError(
|
||||
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
EmailOtp emailOtp,
|
||||
Guid sendId,
|
||||
string email)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
emailOtp = emailOtp with { Emails = [email] };
|
||||
|
||||
sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns((string)null); // Generation fails
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
||||
|
||||
// Verify no email was sent
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendSendEmailOtpEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_ValidOtp_ReturnsSuccess(
|
||||
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
EmailOtp emailOtp,
|
||||
Guid sendId,
|
||||
string email,
|
||||
string otp)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, otp);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
emailOtp = emailOtp with { Emails = [email] };
|
||||
|
||||
var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);
|
||||
|
||||
sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.ValidateTokenAsync(
|
||||
otp,
|
||||
SendAccessConstants.OtpToken.TokenProviderName,
|
||||
SendAccessConstants.OtpToken.Purpose,
|
||||
expectedUniqueId)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsError);
|
||||
var sub = result.Subject;
|
||||
Assert.Equal(sendId.ToString(), sub.Claims.First(c => c.Type == Claims.SendAccessClaims.SendId).Value);
|
||||
|
||||
// Verify claims
|
||||
Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString());
|
||||
Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.Email && c.Value == email);
|
||||
Assert.Contains(sub.Claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString());
|
||||
|
||||
// Verify OTP validation was called
|
||||
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.Received(1)
|
||||
.ValidateTokenAsync(otp, SendAccessConstants.OtpToken.TokenProviderName, SendAccessConstants.OtpToken.Purpose, expectedUniqueId);
|
||||
|
||||
// Verify no email was sent (validation only)
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendSendEmailOtpEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateRequestAsync_InvalidOtp_ReturnsInvalidGrant(
|
||||
SutProvider<SendEmailOtpRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
EmailOtp emailOtp,
|
||||
Guid sendId,
|
||||
string email,
|
||||
string invalidOtp)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, invalidOtp);
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
emailOtp = emailOtp with { Emails = [email] };
|
||||
|
||||
var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);
|
||||
|
||||
sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.ValidateTokenAsync(invalidOtp,
|
||||
SendAccessConstants.OtpToken.TokenProviderName,
|
||||
SendAccessConstants.OtpToken.Purpose,
|
||||
expectedUniqueId)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
Assert.Equal("email otp is invalid.", result.ErrorDescription);
|
||||
|
||||
// Verify OTP validation was attempted
|
||||
await sutProvider.GetDependency<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>()
|
||||
.Received(1)
|
||||
.ValidateTokenAsync(invalidOtp,
|
||||
SendAccessConstants.OtpToken.TokenProviderName,
|
||||
SendAccessConstants.OtpToken.Purpose,
|
||||
expectedUniqueId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValidParameters_CreatesInstance()
|
||||
{
|
||||
// Arrange
|
||||
var otpTokenProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
|
||||
// Act
|
||||
var validator = new SendEmailOtpRequestValidator(otpTokenProvider, mailService);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(validator);
|
||||
}
|
||||
|
||||
private static NameValueCollection CreateValidatedTokenRequest(
|
||||
Guid sendId,
|
||||
string sendEmail = null,
|
||||
string otpCode = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
|
||||
var rawRequestParameters = new NameValueCollection
|
||||
{
|
||||
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
|
||||
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
|
||||
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
|
||||
{ "device_type", ((int)DeviceType.FirefoxBrowser).ToString() },
|
||||
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
|
||||
};
|
||||
|
||||
if (sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail);
|
||||
}
|
||||
|
||||
if (otpCode != null && sendEmail != null)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode);
|
||||
}
|
||||
|
||||
return rawRequestParameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core.Auth.UserFeatures.SendAccess;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.KeyManagement.Sends;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer.SendAccess;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SendPasswordRequestValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateSendPassword_MissingPasswordHash_ReturnsInvalidRequest(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error);
|
||||
Assert.Equal($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required.", result.ErrorDescription);
|
||||
|
||||
// Verify password hasher was not called
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.DidNotReceive()
|
||||
.PasswordHashMatches(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateSendPassword_PasswordHashMismatch_ReturnsInvalidGrant(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
Guid sendId,
|
||||
string clientPasswordHash)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.PasswordHashMatches(resourcePassword.Hash, clientPasswordHash)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
Assert.Equal($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid.", result.ErrorDescription);
|
||||
|
||||
// Verify password hasher was called with correct parameters
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.Received(1)
|
||||
.PasswordHashMatches(resourcePassword.Hash, clientPasswordHash);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateSendPassword_PasswordHashMatches_ReturnsSuccess(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
Guid sendId,
|
||||
string clientPasswordHash)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.PasswordHashMatches(resourcePassword.Hash, clientPasswordHash)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsError);
|
||||
|
||||
var sub = result.Subject;
|
||||
Assert.Equal(sendId, sub.GetSendId());
|
||||
|
||||
// Verify claims
|
||||
Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString());
|
||||
Assert.Contains(sub.Claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString());
|
||||
|
||||
// Verify password hasher was called
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.Received(1)
|
||||
.PasswordHashMatches(resourcePassword.Hash, clientPasswordHash);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateSendPassword_EmptyPasswordHash_CallsPasswordHasher(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, string.Empty);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.PasswordHashMatches(resourcePassword.Hash, string.Empty)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
|
||||
// Verify password hasher was called with empty string
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.Received(1)
|
||||
.PasswordHashMatches(resourcePassword.Hash, string.Empty);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateSendPassword_WhitespacePasswordHash_CallsPasswordHasher(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
var whitespacePassword = " ";
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, whitespacePassword);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.PasswordHashMatches(resourcePassword.Hash, whitespacePassword)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
|
||||
// Verify password hasher was called with whitespace string
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.Received(1)
|
||||
.PasswordHashMatches(resourcePassword.Hash, whitespacePassword);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateSendPassword_MultiplePasswordHashParameters_ReturnsInvalidGrant(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
Guid sendId)
|
||||
{
|
||||
// Arrange
|
||||
var firstPassword = "first-password";
|
||||
var secondPassword = "second-password";
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, firstPassword, secondPassword);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.PasswordHashMatches(resourcePassword.Hash, firstPassword)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error);
|
||||
|
||||
// Verify password hasher was called with first value
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.Received(1)
|
||||
.PasswordHashMatches(resourcePassword.Hash, $"{firstPassword},{secondPassword}");
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateSendPassword_SuccessResult_ContainsCorrectClaims(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
Guid sendId,
|
||||
string clientPasswordHash)
|
||||
{
|
||||
// Arrange
|
||||
tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash);
|
||||
|
||||
var context = new ExtensionGrantValidationContext
|
||||
{
|
||||
Request = tokenRequest
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISendPasswordHasher>()
|
||||
.PasswordHashMatches(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsError);
|
||||
var sub = result.Subject;
|
||||
|
||||
var sendIdClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.SendAccessClaims.SendId);
|
||||
Assert.NotNull(sendIdClaim);
|
||||
Assert.Equal(sendId.ToString(), sendIdClaim.Value);
|
||||
|
||||
var typeClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.Type);
|
||||
Assert.NotNull(typeClaim);
|
||||
Assert.Equal(IdentityClientType.Send.ToString(), typeClaim.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValidParameters_CreatesInstance()
|
||||
{
|
||||
// Arrange
|
||||
var sendPasswordHasher = Substitute.For<ISendPasswordHasher>();
|
||||
|
||||
// Act
|
||||
var validator = new SendPasswordRequestValidator(sendPasswordHasher);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(validator);
|
||||
}
|
||||
|
||||
private static NameValueCollection CreateValidatedTokenRequest(
|
||||
Guid sendId,
|
||||
params string[] passwordHash)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
|
||||
var rawRequestParameters = new NameValueCollection
|
||||
{
|
||||
{ OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess },
|
||||
{ OidcConstants.TokenRequest.ClientId, BitwardenClient.Send },
|
||||
{ OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess },
|
||||
{ "device_type", ((int)DeviceType.FirefoxBrowser).ToString() },
|
||||
{ SendAccessConstants.TokenRequest.SendId, sendIdBase64 }
|
||||
};
|
||||
|
||||
if (passwordHash != null && passwordHash.Length > 0)
|
||||
{
|
||||
foreach (var hash in passwordHash)
|
||||
{
|
||||
rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, hash);
|
||||
}
|
||||
}
|
||||
|
||||
return rawRequestParameters;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace Bit.Identity.Test.IdentityServer;
|
||||
public class SendPasswordRequestValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void ValidateSendPassword_MissingPasswordHash_ReturnsInvalidRequest(
|
||||
public async Task ValidateSendPassword_MissingPasswordHash_ReturnsInvalidRequest(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
@@ -36,7 +36,7 @@ public class SendPasswordRequestValidatorTests
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
@@ -50,7 +50,7 @@ public class SendPasswordRequestValidatorTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ValidateSendPassword_PasswordHashMismatch_ReturnsInvalidGrant(
|
||||
public async Task ValidateSendPassword_PasswordHashMismatch_ReturnsInvalidGrant(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
@@ -70,7 +70,7 @@ public class SendPasswordRequestValidatorTests
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
@@ -84,7 +84,7 @@ public class SendPasswordRequestValidatorTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ValidateSendPassword_PasswordHashMatches_ReturnsSuccess(
|
||||
public async Task ValidateSendPassword_PasswordHashMatches_ReturnsSuccess(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
@@ -104,7 +104,7 @@ public class SendPasswordRequestValidatorTests
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsError);
|
||||
@@ -113,7 +113,7 @@ public class SendPasswordRequestValidatorTests
|
||||
Assert.Equal(sendId, sub.GetSendId());
|
||||
|
||||
// Verify claims
|
||||
Assert.Contains(sub.Claims, c => c.Type == Claims.SendId && c.Value == sendId.ToString());
|
||||
Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString());
|
||||
Assert.Contains(sub.Claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString());
|
||||
|
||||
// Verify password hasher was called
|
||||
@@ -123,7 +123,7 @@ public class SendPasswordRequestValidatorTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ValidateSendPassword_EmptyPasswordHash_CallsPasswordHasher(
|
||||
public async Task ValidateSendPassword_EmptyPasswordHash_CallsPasswordHasher(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
@@ -142,7 +142,7 @@ public class SendPasswordRequestValidatorTests
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
@@ -155,7 +155,7 @@ public class SendPasswordRequestValidatorTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ValidateSendPassword_WhitespacePasswordHash_CallsPasswordHasher(
|
||||
public async Task ValidateSendPassword_WhitespacePasswordHash_CallsPasswordHasher(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
@@ -175,7 +175,7 @@ public class SendPasswordRequestValidatorTests
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
@@ -187,7 +187,7 @@ public class SendPasswordRequestValidatorTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ValidateSendPassword_MultiplePasswordHashParameters_ReturnsInvalidGrant(
|
||||
public async Task ValidateSendPassword_MultiplePasswordHashParameters_ReturnsInvalidGrant(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
@@ -208,7 +208,7 @@ public class SendPasswordRequestValidatorTests
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
@@ -221,7 +221,7 @@ public class SendPasswordRequestValidatorTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ValidateSendPassword_SuccessResult_ContainsCorrectClaims(
|
||||
public async Task ValidateSendPassword_SuccessResult_ContainsCorrectClaims(
|
||||
SutProvider<SendPasswordRequestValidator> sutProvider,
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
ResourcePassword resourcePassword,
|
||||
@@ -241,13 +241,13 @@ public class SendPasswordRequestValidatorTests
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId);
|
||||
var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsError);
|
||||
var sub = result.Subject;
|
||||
|
||||
var sendIdClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.SendId);
|
||||
var sendIdClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.SendAccessClaims.SendId);
|
||||
Assert.NotNull(sendIdClaim);
|
||||
Assert.Equal(sendId.ToString(), sendIdClaim.Value);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user