mirror of
https://github.com/bitwarden/server
synced 2025-12-20 18:23:44 +00:00
pm-24210 (#6142)
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@@ -43,11 +44,31 @@ public class AuthRequest : ITableObject<Guid>
|
|||||||
|
|
||||||
public bool IsSpent()
|
public bool IsSpent()
|
||||||
{
|
{
|
||||||
return ResponseDate.HasValue || AuthenticationDate.HasValue || GetExpirationDate() < DateTime.UtcNow;
|
return ResponseDate.HasValue || AuthenticationDate.HasValue || IsExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsExpired()
|
||||||
|
{
|
||||||
|
// TODO: PM-24252 - consider using TimeProvider for better mocking in tests
|
||||||
|
return GetExpirationDate() < DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: PM-24252 - this probably belongs in a service.
|
||||||
|
public bool IsValidForAuthentication(Guid userId,
|
||||||
|
string password)
|
||||||
|
{
|
||||||
|
return ResponseDate.HasValue // it’s been responded to
|
||||||
|
&& Approved == true // it was approved
|
||||||
|
&& !IsExpired() // it's not expired
|
||||||
|
&& Type == AuthRequestType.AuthenticateAndUnlock // it’s an authN request
|
||||||
|
&& !AuthenticationDate.HasValue // it was not already used for authN
|
||||||
|
&& UserId == userId // it belongs to the user
|
||||||
|
&& CoreHelpers.FixedTimeEquals(AccessCode, password); // the access code matches the password
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime GetExpirationDate()
|
public DateTime GetExpirationDate()
|
||||||
{
|
{
|
||||||
|
// TODO: PM-24252 - this should reference PasswordlessAuthSettings.UserRequestExpiration
|
||||||
return CreationDate.AddMinutes(15);
|
return CreationDate.AddMinutes(15);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ public enum DeviceValidationResultType : byte
|
|||||||
InvalidUser = 1,
|
InvalidUser = 1,
|
||||||
InvalidNewDeviceOtp = 2,
|
InvalidNewDeviceOtp = 2,
|
||||||
NewDeviceVerificationRequired = 3,
|
NewDeviceVerificationRequired = 3,
|
||||||
NoDeviceInformationProvided = 4
|
NoDeviceInformationProvided = 4,
|
||||||
|
AuthRequestFlowUnknownDevice = 5,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ public class DeviceValidator(
|
|||||||
private readonly ILogger<DeviceValidator> _logger = logger;
|
private readonly ILogger<DeviceValidator> _logger = logger;
|
||||||
private readonly ITwoFactorEmailService _twoFactorEmailService = twoFactorEmailService;
|
private readonly ITwoFactorEmailService _twoFactorEmailService = twoFactorEmailService;
|
||||||
|
|
||||||
|
private const string PasswordGrantType = "password";
|
||||||
|
|
||||||
public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context)
|
public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context)
|
||||||
{
|
{
|
||||||
// Parse device from request and return early if no device information is provided
|
// Parse device from request and return early if no device information is provided
|
||||||
@@ -68,10 +70,14 @@ public class DeviceValidator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We have established that the device is unknown at this point; begin new device verification
|
// We have established that the device is unknown at this point; begin new device verification
|
||||||
if (request.GrantType == "password" &&
|
// for standard password grant type requests
|
||||||
request.Raw["AuthRequest"] == null &&
|
// Note: the auth request flow re-uses the resource owner password flow but new device verification
|
||||||
!context.TwoFactorRequired &&
|
// is not required for auth requests
|
||||||
!context.SsoRequired &&
|
var rawAuthRequestId = request.Raw["AuthRequest"]?.ToLowerInvariant();
|
||||||
|
var isAuthRequest = !string.IsNullOrEmpty(rawAuthRequestId);
|
||||||
|
if (request.GrantType == PasswordGrantType &&
|
||||||
|
!isAuthRequest &&
|
||||||
|
context is { TwoFactorRequired: false, SsoRequired: false } &&
|
||||||
_globalSettings.EnableNewDeviceVerification)
|
_globalSettings.EnableNewDeviceVerification)
|
||||||
{
|
{
|
||||||
var validationResult = await HandleNewDeviceVerificationAsync(context.User, request);
|
var validationResult = await HandleNewDeviceVerificationAsync(context.User, request);
|
||||||
@@ -87,6 +93,16 @@ public class DeviceValidator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Device still unknown, but if we are in an auth request flow, this is not valid
|
||||||
|
// as we only support auth request authN requests on known devices
|
||||||
|
if (request.GrantType == PasswordGrantType && isAuthRequest &&
|
||||||
|
context is { TwoFactorRequired: false, SsoRequired: false })
|
||||||
|
{
|
||||||
|
(context.ValidationErrorResult, context.CustomResponse) =
|
||||||
|
BuildDeviceErrorResult(DeviceValidationResultType.AuthRequestFlowUnknownDevice);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// At this point we have established either new device verification is not required or the NewDeviceOtp is valid,
|
// At this point we have established either new device verification is not required or the NewDeviceOtp is valid,
|
||||||
// so we save the device to the database and proceed with authentication
|
// so we save the device to the database and proceed with authentication
|
||||||
requestDevice.UserId = context.User.Id;
|
requestDevice.UserId = context.User.Id;
|
||||||
@@ -252,7 +268,7 @@ public class DeviceValidator(
|
|||||||
var customResponse = new Dictionary<string, object>();
|
var customResponse = new Dictionary<string, object>();
|
||||||
switch (errorType)
|
switch (errorType)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* The ErrorMessage is brittle and is used to control the flow in the clients. Do not change them without updating the client as well.
|
* The ErrorMessage is brittle and is used to control the flow in the clients. Do not change them without updating the client as well.
|
||||||
* There is a backwards compatibility issue as well: if you make a change on the clients then ensure that they are backwards
|
* There is a backwards compatibility issue as well: if you make a change on the clients then ensure that they are backwards
|
||||||
* compatible.
|
* compatible.
|
||||||
@@ -273,6 +289,10 @@ public class DeviceValidator(
|
|||||||
result.ErrorDescription = "No device information provided";
|
result.ErrorDescription = "No device information provided";
|
||||||
customResponse.Add("ErrorModel", new ErrorResponseModel("no device information provided"));
|
customResponse.Add("ErrorModel", new ErrorResponseModel("no device information provided"));
|
||||||
break;
|
break;
|
||||||
|
case DeviceValidationResultType.AuthRequestFlowUnknownDevice:
|
||||||
|
result.ErrorDescription = "Auth requests are not supported on unknown devices";
|
||||||
|
customResponse.Add("ErrorModel", new ErrorResponseModel("auth request flow unsupported on unknown device"));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return (result, customResponse);
|
return (result, customResponse);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Duende.IdentityServer.Models;
|
using Duende.IdentityServer.Models;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@@ -90,21 +89,30 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var authRequestId = context.Request.Raw["AuthRequest"]?.ToString()?.ToLowerInvariant();
|
var authRequestId = context.Request.Raw["AuthRequest"]?.ToLowerInvariant();
|
||||||
if (!string.IsNullOrWhiteSpace(authRequestId) && Guid.TryParse(authRequestId, out var authRequestGuid))
|
if (!string.IsNullOrEmpty(authRequestId))
|
||||||
{
|
{
|
||||||
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestGuid);
|
// only allow valid guids
|
||||||
if (authRequest != null)
|
if (!Guid.TryParse(authRequestId, out var authRequestGuid))
|
||||||
{
|
{
|
||||||
var requestAge = DateTime.UtcNow - authRequest.CreationDate;
|
return false;
|
||||||
if (requestAge < TimeSpan.FromHours(1) &&
|
|
||||||
CoreHelpers.FixedTimeEquals(authRequest.AccessCode, context.Password))
|
|
||||||
{
|
|
||||||
authRequest.AuthenticationDate = DateTime.UtcNow;
|
|
||||||
await _authRequestRepository.ReplaceAsync(authRequest);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestGuid);
|
||||||
|
|
||||||
|
if (authRequest == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth request is non-null so validate it
|
||||||
|
if (authRequest.IsValidForAuthentication(validatorContext.User.Id, context.Password))
|
||||||
|
{
|
||||||
|
authRequest.AuthenticationDate = DateTime.UtcNow;
|
||||||
|
await _authRequestRepository.ReplaceAsync(authRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,18 @@ namespace Bit.Identity.IntegrationTest.RequestValidation;
|
|||||||
|
|
||||||
public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplicationFactory>
|
public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplicationFactory>
|
||||||
{
|
{
|
||||||
private const string DefaultPassword = "master_password_hash";
|
private const string _defaultPassword = "master_password_hash";
|
||||||
private const string DefaultUsername = "test@email.qa";
|
private const string _defaultUsername = "test@email.qa";
|
||||||
private const string DefaultDeviceIdentifier = "test_identifier";
|
private const string _defaultDeviceIdentifier = "test_identifier";
|
||||||
|
private const DeviceType _defaultDeviceType = DeviceType.FirefoxBrowser;
|
||||||
|
private const string _defaultDeviceName = "firefox";
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAsync_Success()
|
public async Task ValidateAsync_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var localFactory = new IdentityApplicationFactory();
|
var localFactory = new IdentityApplicationFactory();
|
||||||
await EnsureUserCreatedAsync(localFactory);
|
await RegisterUserAsync(localFactory);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await localFactory.Server.PostAsync("/connect/token",
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
@@ -74,12 +76,12 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var localFactory = new IdentityApplicationFactory();
|
var localFactory = new IdentityApplicationFactory();
|
||||||
await EnsureUserCreatedAsync(localFactory);
|
await RegisterUserAsync(localFactory);
|
||||||
|
|
||||||
var userManager = localFactory.GetService<UserManager<User>>();
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
|
||||||
// Verify the User is not null to ensure the failure is due to bad password
|
// Verify the User is not null to ensure the failure is due to bad password
|
||||||
Assert.NotNull(await userManager.FindByEmailAsync(DefaultUsername));
|
Assert.NotNull(await userManager.FindByEmailAsync(_defaultUsername));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await localFactory.Server.PostAsync("/connect/token",
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
@@ -95,23 +97,142 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_NotNull_AgeLessThanOneHour_Success()
|
public async Task ValidateAsync_ValidateContextAsync_ValidAuthRequest_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var localFactory = new IdentityApplicationFactory();
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
|
||||||
// Ensure User
|
// Ensure User
|
||||||
await EnsureUserCreatedAsync(localFactory);
|
await RegisterUserAsync(localFactory);
|
||||||
var userManager = localFactory.GetService<UserManager<User>>();
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
var user = await userManager.FindByEmailAsync(DefaultUsername);
|
var user = await userManager.FindByEmailAsync(_defaultUsername);
|
||||||
Assert.NotNull(user);
|
Assert.NotNull(user);
|
||||||
|
|
||||||
// Connect Request to User and set CreationDate
|
// Ensure device is known b/c auth requests aren't allowed for unknown devices.
|
||||||
var authRequest = CreateAuthRequest(
|
await AddKnownDevice(localFactory, user.Id);
|
||||||
user.Id,
|
|
||||||
AuthRequestType.AuthenticateAndUnlock,
|
// Create valid auth request and tie it to the user
|
||||||
DateTime.UtcNow.AddMinutes(-30)
|
var authRequest = CreateAuthRequest(r =>
|
||||||
);
|
{
|
||||||
|
r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago
|
||||||
|
r.Approved = true; // approved
|
||||||
|
r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // not expired
|
||||||
|
r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request
|
||||||
|
r.AuthenticationDate = null; // not used for authN yet
|
||||||
|
r.UserId = user.Id; // connect request to user
|
||||||
|
r.AccessCode = _defaultPassword; // matches the password
|
||||||
|
});
|
||||||
|
|
||||||
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
|
var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
Assert.NotEmpty(expectedAuthRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "scope", "api offline_access" },
|
||||||
|
{ "client_id", "web" },
|
||||||
|
{ "deviceType", DeviceTypeAsString(_defaultDeviceType) },
|
||||||
|
{ "deviceIdentifier", _defaultDeviceIdentifier },
|
||||||
|
{ "deviceName", _defaultDeviceName },
|
||||||
|
{ "grant_type", "password" },
|
||||||
|
{ "username", _defaultUsername },
|
||||||
|
{ "password", _defaultPassword },
|
||||||
|
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||||
|
var root = body.RootElement;
|
||||||
|
|
||||||
|
var token = AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String).GetString();
|
||||||
|
Assert.NotNull(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateAsync_ValidateContextAsync_ValidAuthRequest_UnknownDevice_Failure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
|
||||||
|
// Ensure User
|
||||||
|
await RegisterUserAsync(localFactory);
|
||||||
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
var user = await userManager.FindByEmailAsync(_defaultUsername);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Create valid auth request and tie it to the user
|
||||||
|
var authRequest = CreateAuthRequest(r =>
|
||||||
|
{
|
||||||
|
r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago
|
||||||
|
r.Approved = true; // approved
|
||||||
|
r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // not expired
|
||||||
|
r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request
|
||||||
|
r.AuthenticationDate = null; // not used for authN yet
|
||||||
|
r.UserId = user.Id; // connect request to user
|
||||||
|
r.AccessCode = _defaultPassword; // matches the password
|
||||||
|
});
|
||||||
|
|
||||||
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
|
var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
Assert.NotEmpty(expectedAuthRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "scope", "api offline_access" },
|
||||||
|
{ "client_id", "web" },
|
||||||
|
{ "deviceType", DeviceTypeAsString(_defaultDeviceType) },
|
||||||
|
{ "deviceIdentifier", _defaultDeviceIdentifier },
|
||||||
|
{ "deviceName", _defaultDeviceName },
|
||||||
|
{ "grant_type", "password" },
|
||||||
|
{ "username", _defaultUsername },
|
||||||
|
{ "password", _defaultPassword },
|
||||||
|
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||||
|
var root = body.RootElement;
|
||||||
|
|
||||||
|
var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object);
|
||||||
|
var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString();
|
||||||
|
Assert.Equal("auth request flow unsupported on unknown device", errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateAsync_ValidateContextAsync_Expired_AuthRequest_Failure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
// Ensure User
|
||||||
|
await RegisterUserAsync(localFactory);
|
||||||
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
|
||||||
|
var user = await userManager.FindByEmailAsync(_defaultUsername);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Ensure device is known b/c auth requests aren't allowed for unknown devices.
|
||||||
|
await AddKnownDevice(localFactory, user.Id);
|
||||||
|
|
||||||
|
// Create AuthRequest
|
||||||
|
var authRequest = CreateAuthRequest(r =>
|
||||||
|
{
|
||||||
|
r.ResponseDate = DateTime.UtcNow.AddMinutes(-10); // 10 minutes ago
|
||||||
|
r.Approved = true; // approved
|
||||||
|
r.CreationDate = DateTime.UtcNow.AddMinutes(-16); // expired after 15 minutes
|
||||||
|
r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request
|
||||||
|
r.AuthenticationDate = null; // not used for authN yet
|
||||||
|
r.UserId = user.Id; // connect request to user
|
||||||
|
r.AccessCode = _defaultPassword; // matches the password
|
||||||
|
});
|
||||||
|
|
||||||
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
await authRequestRepository.CreateAsync(authRequest);
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
@@ -125,40 +246,51 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
{ "scope", "api offline_access" },
|
{ "scope", "api offline_access" },
|
||||||
{ "client_id", "web" },
|
{ "client_id", "web" },
|
||||||
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
{ "deviceIdentifier", DefaultDeviceIdentifier },
|
{ "deviceIdentifier", _defaultDeviceIdentifier },
|
||||||
{ "deviceName", "firefox" },
|
{ "deviceName", "firefox" },
|
||||||
{ "grant_type", "password" },
|
{ "grant_type", "password" },
|
||||||
{ "username", DefaultUsername },
|
{ "username", _defaultUsername },
|
||||||
{ "password", DefaultPassword },
|
{ "password", _defaultPassword },
|
||||||
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
|
||||||
var root = body.RootElement;
|
|
||||||
|
|
||||||
var token = AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String).GetString();
|
await AssertStandardError(context);
|
||||||
Assert.NotNull(token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_NotNull_AgeGreaterThanOneHour_Failure()
|
public async Task ValidateAsync_ValidateContextAsync_Unapproved_AuthRequest_Failure()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var localFactory = new IdentityApplicationFactory();
|
var localFactory = new IdentityApplicationFactory();
|
||||||
// Ensure User
|
// Ensure User
|
||||||
await EnsureUserCreatedAsync(localFactory);
|
await RegisterUserAsync(localFactory);
|
||||||
var userManager = localFactory.GetService<UserManager<User>>();
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
|
||||||
var user = await userManager.FindByEmailAsync(DefaultUsername);
|
var user = await userManager.FindByEmailAsync(_defaultUsername);
|
||||||
Assert.NotNull(user);
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Ensure device is known b/c auth requests aren't allowed for unknown devices.
|
||||||
|
await AddKnownDevice(localFactory, user.Id);
|
||||||
|
|
||||||
// Create AuthRequest
|
// Create AuthRequest
|
||||||
var authRequest = CreateAuthRequest(
|
var authRequest = CreateAuthRequest(r =>
|
||||||
user.Id,
|
{
|
||||||
AuthRequestType.AuthenticateAndUnlock,
|
r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago
|
||||||
DateTime.UtcNow.AddMinutes(-61)
|
r.Approved = false; // NOT approved
|
||||||
);
|
r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid
|
||||||
|
r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request
|
||||||
|
r.AuthenticationDate = null; // not used for authN yet
|
||||||
|
r.UserId = user.Id; // connect request to user
|
||||||
|
r.AccessCode = _defaultPassword; // matches the password
|
||||||
|
});
|
||||||
|
|
||||||
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
|
var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
Assert.NotEmpty(expectedAuthRequest);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = await localFactory.Server.PostAsync("/connect/token",
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
@@ -167,22 +299,137 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
{ "scope", "api offline_access" },
|
{ "scope", "api offline_access" },
|
||||||
{ "client_id", "web" },
|
{ "client_id", "web" },
|
||||||
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
{ "deviceIdentifier", DefaultDeviceIdentifier },
|
{ "deviceIdentifier", _defaultDeviceIdentifier },
|
||||||
{ "deviceName", "firefox" },
|
{ "deviceName", "firefox" },
|
||||||
{ "grant_type", "password" },
|
{ "grant_type", "password" },
|
||||||
{ "username", DefaultUsername },
|
{ "username", _defaultUsername },
|
||||||
{ "password", DefaultPassword },
|
{ "password", _defaultPassword },
|
||||||
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
||||||
/*
|
await AssertStandardError(context);
|
||||||
An improvement on the current failure flow would be to document which part of
|
}
|
||||||
the flow failed since all of the failures are basically the same.
|
|
||||||
This doesn't build confidence in the tests.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> InvalidAuthRequestTypes()
|
||||||
|
{
|
||||||
|
// yield the two enum values that should fail
|
||||||
|
yield return [AuthRequestType.Unlock];
|
||||||
|
yield return [AuthRequestType.AdminApproval];
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(InvalidAuthRequestTypes))]
|
||||||
|
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_Invalid_Type_Failure(AuthRequestType invalidType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
// Ensure User
|
||||||
|
await RegisterUserAsync(localFactory);
|
||||||
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
|
||||||
|
var user = await userManager.FindByEmailAsync(_defaultUsername);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Ensure device is known b/c auth requests aren't allowed for unknown devices.
|
||||||
|
await AddKnownDevice(localFactory, user.Id);
|
||||||
|
|
||||||
|
// Create AuthRequest
|
||||||
|
var authRequest = CreateAuthRequest(r =>
|
||||||
|
{
|
||||||
|
r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago
|
||||||
|
r.Approved = true; // approved
|
||||||
|
r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid
|
||||||
|
r.Type = invalidType; // invalid type for authN
|
||||||
|
r.AuthenticationDate = null; // not used for authN yet
|
||||||
|
r.UserId = user.Id; // connect request to user
|
||||||
|
r.AccessCode = _defaultPassword; // matches the password
|
||||||
|
});
|
||||||
|
|
||||||
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
|
var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
Assert.NotEmpty(expectedAuthRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "scope", "api offline_access" },
|
||||||
|
{ "client_id", "web" },
|
||||||
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
|
{ "deviceIdentifier", _defaultDeviceIdentifier },
|
||||||
|
{ "deviceName", "firefox" },
|
||||||
|
{ "grant_type", "password" },
|
||||||
|
{ "username", _defaultUsername },
|
||||||
|
{ "password", _defaultPassword },
|
||||||
|
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
await AssertStandardError(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_WrongUser_Failure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
|
||||||
|
// Ensure User 1 exists
|
||||||
|
await RegisterUserAsync(localFactory);
|
||||||
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
var user = await userManager.FindByEmailAsync(_defaultUsername);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Ensure User 2 exists so we can satisfy auth request foreign key constraint
|
||||||
|
var user2Username = "user2@email.com";
|
||||||
|
var user2Password = "user2_password";
|
||||||
|
await RegisterUserAsync(localFactory, user2Username, user2Password);
|
||||||
|
var user2 = await userManager.FindByEmailAsync(user2Username);
|
||||||
|
Assert.NotNull(user2);
|
||||||
|
|
||||||
|
// Ensure device is known b/c auth requests aren't allowed for unknown devices.
|
||||||
|
await AddKnownDevice(localFactory, user.Id);
|
||||||
|
|
||||||
|
// Create valid auth request for user 2
|
||||||
|
var authRequest = CreateAuthRequest(r =>
|
||||||
|
{
|
||||||
|
r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago
|
||||||
|
r.Approved = true; // approved
|
||||||
|
r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // not expired
|
||||||
|
r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request
|
||||||
|
r.AuthenticationDate = null; // not used for authN yet
|
||||||
|
r.UserId = user2.Id; // connect request to user2
|
||||||
|
r.AccessCode = _defaultPassword; // matches the password
|
||||||
|
});
|
||||||
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
|
var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user2.Id);
|
||||||
|
Assert.NotEmpty(expectedAuthRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "scope", "api offline_access" },
|
||||||
|
{ "client_id", "web" },
|
||||||
|
{ "deviceType", DeviceTypeAsString(_defaultDeviceType) },
|
||||||
|
{ "deviceIdentifier", _defaultDeviceIdentifier },
|
||||||
|
{ "deviceName", _defaultDeviceName },
|
||||||
|
{ "grant_type", "password" },
|
||||||
|
{ "username", _defaultUsername },
|
||||||
|
{ "password", _defaultPassword },
|
||||||
|
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||||
var root = body.RootElement;
|
var root = body.RootElement;
|
||||||
|
|
||||||
@@ -191,17 +438,180 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
|
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureUserCreatedAsync(IdentityApplicationFactory factory)
|
[Fact]
|
||||||
|
public async Task ValidateAsync_ValidateContextAsync_Unanswered_AuthRequest_Failure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
// Ensure User
|
||||||
|
await RegisterUserAsync(localFactory);
|
||||||
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
|
||||||
|
var user = await userManager.FindByEmailAsync(_defaultUsername);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Ensure device is known b/c auth requests aren't allowed for unknown devices.
|
||||||
|
await AddKnownDevice(localFactory, user.Id);
|
||||||
|
|
||||||
|
// Create AuthRequest
|
||||||
|
var authRequest = CreateAuthRequest(r =>
|
||||||
|
{
|
||||||
|
r.ResponseDate = null; // not answered
|
||||||
|
r.Approved = true; // approved
|
||||||
|
r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid
|
||||||
|
r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request
|
||||||
|
r.AuthenticationDate = null; // not used for authN yet
|
||||||
|
r.UserId = user.Id; // connect request to user
|
||||||
|
r.AccessCode = _defaultPassword; // matches the password
|
||||||
|
});
|
||||||
|
|
||||||
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
|
var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
Assert.NotEmpty(expectedAuthRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "scope", "api offline_access" },
|
||||||
|
{ "client_id", "web" },
|
||||||
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
|
{ "deviceIdentifier", _defaultDeviceIdentifier },
|
||||||
|
{ "deviceName", "firefox" },
|
||||||
|
{ "grant_type", "password" },
|
||||||
|
{ "username", _defaultUsername },
|
||||||
|
{ "password", _defaultPassword },
|
||||||
|
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
await AssertStandardError(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateAsync_ValidateContextAsync_WrongPassword_AuthRequest_Failure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
// Ensure User
|
||||||
|
await RegisterUserAsync(localFactory);
|
||||||
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
|
||||||
|
var user = await userManager.FindByEmailAsync(_defaultUsername);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Ensure device is known b/c auth requests aren't allowed for unknown devices.
|
||||||
|
await AddKnownDevice(localFactory, user.Id);
|
||||||
|
|
||||||
|
// Create AuthRequest
|
||||||
|
var authRequest = CreateAuthRequest(r =>
|
||||||
|
{
|
||||||
|
r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // answered
|
||||||
|
r.Approved = true; // approved
|
||||||
|
r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid
|
||||||
|
r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request
|
||||||
|
r.AuthenticationDate = null; // not used for authN yet
|
||||||
|
r.UserId = user.Id; // connect request to user
|
||||||
|
r.AccessCode = "WRONG_BAD_PASSWORD"; // does not match the password
|
||||||
|
});
|
||||||
|
|
||||||
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
|
var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
Assert.NotEmpty(expectedAuthRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "scope", "api offline_access" },
|
||||||
|
{ "client_id", "web" },
|
||||||
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
|
{ "deviceIdentifier", _defaultDeviceIdentifier },
|
||||||
|
{ "deviceName", "firefox" },
|
||||||
|
{ "grant_type", "password" },
|
||||||
|
{ "username", _defaultUsername },
|
||||||
|
{ "password", _defaultPassword },
|
||||||
|
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
await AssertStandardError(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateAsync_ValidateContextAsync_Spent_AuthRequest_Failure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var localFactory = new IdentityApplicationFactory();
|
||||||
|
// Ensure User
|
||||||
|
await RegisterUserAsync(localFactory);
|
||||||
|
var userManager = localFactory.GetService<UserManager<User>>();
|
||||||
|
|
||||||
|
var user = await userManager.FindByEmailAsync(_defaultUsername);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Ensure device is known b/c auth requests aren't allowed for unknown devices.
|
||||||
|
await AddKnownDevice(localFactory, user.Id);
|
||||||
|
|
||||||
|
// Create AuthRequest
|
||||||
|
var authRequest = CreateAuthRequest(r =>
|
||||||
|
{
|
||||||
|
r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // answered
|
||||||
|
r.Approved = true; // approved
|
||||||
|
r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid
|
||||||
|
r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request
|
||||||
|
r.AuthenticationDate = DateTime.UtcNow.AddMinutes(-2); // spent request - already has been used for authN
|
||||||
|
r.UserId = user.Id; // connect request to user
|
||||||
|
r.AccessCode = _defaultPassword; // does not match the password
|
||||||
|
});
|
||||||
|
|
||||||
|
var authRequestRepository = localFactory.GetService<IAuthRequestRepository>();
|
||||||
|
await authRequestRepository.CreateAsync(authRequest);
|
||||||
|
|
||||||
|
var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
Assert.NotEmpty(expectedAuthRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||||
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "scope", "api offline_access" },
|
||||||
|
{ "client_id", "web" },
|
||||||
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
|
{ "deviceIdentifier", _defaultDeviceIdentifier },
|
||||||
|
{ "deviceName", "firefox" },
|
||||||
|
{ "grant_type", "password" },
|
||||||
|
{ "username", _defaultUsername },
|
||||||
|
{ "password", _defaultPassword },
|
||||||
|
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await AssertStandardError(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private async Task RegisterUserAsync(
|
||||||
|
IdentityApplicationFactory factory,
|
||||||
|
string username = _defaultUsername,
|
||||||
|
string password = _defaultPassword
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Register user
|
|
||||||
await factory.RegisterNewIdentityFactoryUserAsync(
|
await factory.RegisterNewIdentityFactoryUserAsync(
|
||||||
new RegisterFinishRequestModel
|
new RegisterFinishRequestModel
|
||||||
{
|
{
|
||||||
Email = DefaultUsername,
|
Email = username,
|
||||||
MasterPasswordHash = DefaultPassword,
|
MasterPasswordHash = password,
|
||||||
Kdf = KdfType.PBKDF2_SHA256,
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||||
UserAsymmetricKeys = new KeysRequestModel()
|
UserAsymmetricKeys = new KeysRequestModel
|
||||||
{
|
{
|
||||||
PublicKey = "public_key",
|
PublicKey = "public_key",
|
||||||
EncryptedPrivateKey = "private_key"
|
EncryptedPrivateKey = "private_key"
|
||||||
@@ -218,11 +628,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
{ "scope", "api offline_access" },
|
{ "scope", "api offline_access" },
|
||||||
{ "client_id", "web" },
|
{ "client_id", "web" },
|
||||||
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||||
{ "deviceIdentifier", deviceId ?? DefaultDeviceIdentifier },
|
{ "deviceIdentifier", deviceId ?? _defaultDeviceIdentifier },
|
||||||
{ "deviceName", "firefox" },
|
{ "deviceName", "firefox" },
|
||||||
{ "grant_type", "password" },
|
{ "grant_type", "password" },
|
||||||
{ "username", username ?? DefaultUsername },
|
{ "username", username ?? _defaultUsername },
|
||||||
{ "password", password ?? DefaultPassword },
|
{ "password", password ?? _defaultPassword },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,8 +643,8 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
{ "scope", "api offline_access" },
|
{ "scope", "api offline_access" },
|
||||||
{ "client_id", "web" },
|
{ "client_id", "web" },
|
||||||
{ "grant_type", "password" },
|
{ "grant_type", "password" },
|
||||||
{ "username", DefaultUsername },
|
{ "username", _defaultUsername },
|
||||||
{ "password", DefaultPassword },
|
{ "password", _defaultPassword },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,24 +653,53 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
|||||||
return ((int)deviceType).ToString();
|
return ((int)deviceType).ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AuthRequest CreateAuthRequest(
|
|
||||||
Guid userId,
|
private AuthRequest CreateAuthRequest(Action<AuthRequest>? customize = null)
|
||||||
AuthRequestType authRequestType,
|
|
||||||
DateTime creationDate,
|
|
||||||
bool? approved = null,
|
|
||||||
DateTime? responseDate = null)
|
|
||||||
{
|
{
|
||||||
return new AuthRequest
|
var req = new AuthRequest
|
||||||
{
|
{
|
||||||
UserId = userId,
|
// required fields with defaults
|
||||||
Type = authRequestType,
|
UserId = Guid.NewGuid(),
|
||||||
Approved = approved,
|
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||||
RequestDeviceIdentifier = DefaultDeviceIdentifier,
|
RequestDeviceIdentifier = _defaultDeviceIdentifier,
|
||||||
RequestIpAddress = "1.1.1.1",
|
RequestIpAddress = "1.1.1.1",
|
||||||
AccessCode = DefaultPassword,
|
AccessCode = _defaultPassword,
|
||||||
PublicKey = "test_public_key",
|
PublicKey = "test_public_key",
|
||||||
CreationDate = creationDate,
|
CreationDate = DateTime.UtcNow,
|
||||||
ResponseDate = responseDate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// let the caller tweak whatever they need
|
||||||
|
customize?.Invoke(req);
|
||||||
|
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddKnownDevice(IdentityApplicationFactory factory, Guid userId)
|
||||||
|
{
|
||||||
|
var userDevice = new Device
|
||||||
|
{
|
||||||
|
Identifier = _defaultDeviceIdentifier,
|
||||||
|
Type = _defaultDeviceType,
|
||||||
|
Name = _defaultDeviceName,
|
||||||
|
UserId = userId,
|
||||||
|
};
|
||||||
|
var deviceRepository = factory.GetService<IDeviceRepository>();
|
||||||
|
await deviceRepository.CreateAsync(userDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AssertStandardError(HttpContext context)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
An improvement on the current failure flow would be to document which part of
|
||||||
|
the flow failed since all of the failures are basically the same.
|
||||||
|
This doesn't build confidence in the tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||||
|
var root = body.RootElement;
|
||||||
|
|
||||||
|
var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object);
|
||||||
|
var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString();
|
||||||
|
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,12 +325,11 @@ public class DeviceValidatorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void ValidateRequestDeviceAsync_IsAuthRequest_SavesDevice_ReturnsTrue(
|
public async void ValidateRequestDeviceAsync_IsAuthRequest_UnknownDevice_Errors(
|
||||||
CustomValidatorRequestContext context,
|
CustomValidatorRequestContext context,
|
||||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
context.KnownDevice = false;
|
|
||||||
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
ArrangeForHandleNewDeviceVerificationTest(context, request);
|
||||||
AddValidDeviceToRequest(request);
|
AddValidDeviceToRequest(request);
|
||||||
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
|
||||||
@@ -342,8 +341,11 @@ public class DeviceValidatorTests
|
|||||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await _deviceService.Received(1).SaveAsync(context.Device);
|
Assert.False(result);
|
||||||
Assert.True(result);
|
Assert.NotNull(context.CustomResponse["ErrorModel"]);
|
||||||
|
var expectedErrorMessage = "auth request flow unsupported on unknown device";
|
||||||
|
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
|
||||||
|
Assert.Equal(expectedErrorMessage, actualResponse.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
|
|||||||
Reference in New Issue
Block a user