@ -104,13 +104,6 @@ public class BaseRequestValidatorTests
@@ -104,13 +104,6 @@ public class BaseRequestValidatorTests
_ userAccountKeysQuery ) ;
}
private void SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( bool recoveryCodeSupportEnabled )
{
_f eatureService
. IsEnabled ( FeatureFlagKeys . RecoveryCodeSupportForSsoRequiredUsers )
. Returns ( recoveryCodeSupportEnabled ) ;
}
/ * Logic path
* ValidateAsync - > UpdateFailedAuthDetailsAsync - > _ mailService . SendFailedLoginAttemptsEmailAsync
* | - > BuildErrorResultAsync - > _ eventService . LogUserEventAsync
@ -118,16 +111,14 @@ public class BaseRequestValidatorTests
@@ -118,16 +111,14 @@ public class BaseRequestValidatorTests
* | - > SetErrorResult
* /
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_ContextNotValid_SelfHosted_ShouldBuildErrorResult_ShouldLogWarning (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
_ globalSettings . SelfHosted = true ;
_ sut . isValid = false ;
@ -144,16 +135,14 @@ public class BaseRequestValidatorTests
@@ -144,16 +135,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_DeviceNotValidated_ShouldLogError (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
// 1 -> to pass
@ -186,16 +175,14 @@ public class BaseRequestValidatorTests
@@ -186,16 +175,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_DeviceValidated_ShouldSucceed (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
// 1 -> to pass
@ -232,16 +219,14 @@ public class BaseRequestValidatorTests
@@ -232,16 +219,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_ValidatedAuthRequest_ConsumedOnSuccess (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
// 1 -> to pass
@ -297,16 +282,14 @@ public class BaseRequestValidatorTests
@@ -297,16 +282,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_ValidatedAuthRequest_NotConsumed_When2faRequired (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
// 1 -> to pass
@ -329,7 +312,8 @@ public class BaseRequestValidatorTests
@@ -329,7 +312,8 @@ public class BaseRequestValidatorTests
// 2 -> will result to false with no extra configuration
// 3 -> set two factor to be required
requestContext . User . TwoFactorProviders = "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}" ;
requestContext . User . TwoFactorProviders =
"{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}" ;
_ twoFactorAuthenticationValidator
. RequiresTwoFactorAsync ( requestContext . User , tokenRequest )
. Returns ( Task . FromResult ( new Tuple < bool , Organization > ( true , null ) ) ) ;
@ -339,7 +323,7 @@ public class BaseRequestValidatorTests
@@ -339,7 +323,7 @@ public class BaseRequestValidatorTests
. Returns ( Task . FromResult ( new Dictionary < string , object >
{
{ "TwoFactorProviders" , new [ ] { "0" , "1" } } ,
{ "TwoFactorProviders2" , new Dictionary < string , object > { { "Email" , null } } }
{ "TwoFactorProviders2" , new Dictionary < string , object > { { "Email" , null } } }
} ) ) ;
// Act
@ -356,16 +340,14 @@ public class BaseRequestValidatorTests
@@ -356,16 +340,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_TwoFactorTokenInvalid_ShouldSendFailedTwoFactorEmail (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
var user = requestContext . User ;
@ -400,16 +382,14 @@ public class BaseRequestValidatorTests
@@ -400,16 +382,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_TwoFactorRememberTokenExpired_ShouldNotSendFailedTwoFactorEmail (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
var user = requestContext . User ;
@ -455,21 +435,17 @@ public class BaseRequestValidatorTests
@@ -455,21 +435,17 @@ public class BaseRequestValidatorTests
// Test grantTypes that require SSO when a user is in an organization that requires it
[Theory]
[BitAutoData("password", true)]
[BitAutoData("password", false)]
[BitAutoData("webauthn", true)]
[BitAutoData("webauthn", false)]
[BitAutoData("refresh_token", true)]
[BitAutoData("refresh_token", false)]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_OrgSsoRequiredTrue_ShouldSetSsoResult (
string grantType ,
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
_ sut . isValid = true ;
@ -489,21 +465,17 @@ public class BaseRequestValidatorTests
@@ -489,21 +465,17 @@ public class BaseRequestValidatorTests
// Test grantTypes with RequireSsoPolicyRequirement when feature flag is enabled
[Theory]
[BitAutoData("password", true)]
[BitAutoData("password", false)]
[BitAutoData("webauthn", true)]
[BitAutoData("webauthn", false)]
[BitAutoData("refresh_token", true)]
[BitAutoData("refresh_token", false)]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_WithPolicyRequirementsEnabled_OrgSsoRequiredTrue_ShouldSetSsoResult (
string grantType ,
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
_f eatureService . IsEnabled ( FeatureFlagKeys . PolicyRequirements ) . Returns ( true ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
_ sut . isValid = true ;
@ -525,21 +497,17 @@ public class BaseRequestValidatorTests
@@ -525,21 +497,17 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData("password", true)]
[BitAutoData("password", false)]
[BitAutoData("webauthn", true)]
[BitAutoData("webauthn", false)]
[BitAutoData("refresh_token", true)]
[BitAutoData("refresh_token", false)]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_WithPolicyRequirementsEnabled_OrgSsoRequiredFalse_ShouldSucceed (
string grantType ,
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
_f eatureService . IsEnabled ( FeatureFlagKeys . PolicyRequirements ) . Returns ( true ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
_ sut . isValid = true ;
@ -574,21 +542,17 @@ public class BaseRequestValidatorTests
@@ -574,21 +542,17 @@ public class BaseRequestValidatorTests
// Test grantTypes where SSO would be required but the user is not in an
// organization that requires it
[Theory]
[BitAutoData("password", true)]
[BitAutoData("password", false)]
[BitAutoData("webauthn", true)]
[BitAutoData("webauthn", false)]
[BitAutoData("refresh_token", true)]
[BitAutoData("refresh_token", false)]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_OrgSsoRequiredFalse_ShouldSucceed (
string grantType ,
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
_ sut . isValid = true ;
@ -623,19 +587,17 @@ public class BaseRequestValidatorTests
@@ -623,19 +587,17 @@ public class BaseRequestValidatorTests
// Test the grantTypes where SSO is in progress or not relevant
[Theory]
[BitAutoData("authorization_code", true)]
[BitAutoData("authorization_code", false)]
[BitAutoData("client_credentials", true)]
[BitAutoData("client_credentials", false)]
[BitAutoData("authorization_code")]
[BitAutoData("client_credentials")]
[BitAutoData("client_credentials")]
public async Task ValidateAsync_GrantTypes_SsoRequiredFalse_ShouldSucceed (
string grantType ,
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
_ sut . isValid = true ;
@ -671,16 +633,14 @@ public class BaseRequestValidatorTests
@@ -671,16 +633,14 @@ public class BaseRequestValidatorTests
* ValidateAsync - > UserService . IsLegacyUser - > FailAuthForLegacyUserAsync
* /
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
var user = context . CustomValidatorRequestContext . User ;
user . Key = null ;
@ -705,16 +665,14 @@ public class BaseRequestValidatorTests
@@ -705,16 +665,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_CustomResponse_NoMasterPassword_ShouldSetUserDecryptionOptions (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
_ userDecryptionOptionsBuilder . ForUser ( Arg . Any < User > ( ) ) . Returns ( _ userDecryptionOptionsBuilder ) ;
_ userDecryptionOptionsBuilder . WithDevice ( Arg . Any < Device > ( ) ) . Returns ( _ userDecryptionOptionsBuilder ) ;
_ userDecryptionOptionsBuilder . WithSso ( Arg . Any < SsoConfig > ( ) ) . Returns ( _ userDecryptionOptionsBuilder ) ;
@ -755,19 +713,16 @@ public class BaseRequestValidatorTests
@@ -755,19 +713,16 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true, KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(false, KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(true, KdfType.Argon2id, 11, 128, 5)]
[BitAutoData(false, KdfType.Argon2id, 11, 128, 5)]
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
public async Task ValidateAsync_CustomResponse_MasterPassword_ShouldSetUserDecryptionOptions (
bool featureFlagValue ,
KdfType kdfType , int kdfIterations , int? kdfMemory , int? kdfParallelism ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
_ userDecryptionOptionsBuilder . ForUser ( Arg . Any < User > ( ) ) . Returns ( _ userDecryptionOptionsBuilder ) ;
_ userDecryptionOptionsBuilder . WithDevice ( Arg . Any < Device > ( ) ) . Returns ( _ userDecryptionOptionsBuilder ) ;
_ userDecryptionOptionsBuilder . WithSso ( Arg . Any < SsoConfig > ( ) ) . Returns ( _ userDecryptionOptionsBuilder ) ;
@ -826,16 +781,14 @@ public class BaseRequestValidatorTests
@@ -826,16 +781,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_CustomResponse_ShouldIncludeAccountKeys (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var mockAccountKeys = new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData (
@ -908,16 +861,14 @@ public class BaseRequestValidatorTests
@@ -908,16 +861,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_CustomResponse_AccountKeysQuery_SkippedWhenPrivateKeyIsNull (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
requestContext . User . PrivateKey = null ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
@ -938,16 +889,14 @@ public class BaseRequestValidatorTests
@@ -938,16 +889,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_CustomResponse_AccountKeysQuery_CalledWithCorrectUser (
bool featureFlagValue ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagValue ) ;
var expectedUser = requestContext . User ;
_ userAccountKeysQuery . Run ( Arg . Any < User > ( ) ) . Returns ( new UserAccountKeysData
@ -987,22 +936,20 @@ public class BaseRequestValidatorTests
@@ -987,22 +936,20 @@ public class BaseRequestValidatorTests
/// Tests the core PM-21153 feature: SSO-required users can use recovery codes to disable 2FA,
/// but must then authenticate via SSO with a descriptive message about the recovery.
/// This test validates:
/// 1. Validation order is changed (2FA before SSO) when recovery code is provided
/// 1. Validation order prioritizes 2FA before SSO when recovery code is provided
/// 2. Recovery code successfully validates and sets TwoFactorRecoveryRequested flag
/// 3. SSO validation then fails with recovery-specific message
/// 4. User is NOT logged in (must authenticate via IdP)
/// </summary>
[Theory]
[BitAutoData(true)] // Feature flag ON - new behavior
[BitAutoData(false)] // Feature flag OFF - should fail at SSO before 2FA recovery
[BitAutoData]
public async Task ValidateAsync_RecoveryCodeForSsoRequiredUser_BlocksWithDescriptiveMessage (
bool featureFlagEnabled ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagEnabled ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
var user = requestContext . User ;
@ -1015,8 +962,8 @@ public class BaseRequestValidatorTests
@@ -1015,8 +962,8 @@ public class BaseRequestValidatorTests
// 2. SSO is required (this user is in an org that requires SSO)
_ policyService . AnyPoliciesApplicableToUserAsync (
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
. Returns ( Task . FromResult ( true ) ) ;
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
. Returns ( Task . FromResult ( true ) ) ;
// 3. 2FA is required
_ twoFactorAuthenticationValidator
@ -1040,30 +987,16 @@ public class BaseRequestValidatorTests
@@ -1040,30 +987,16 @@ public class BaseRequestValidatorTests
var errorResponse = ( ErrorResponseModel ) context . GrantResult . CustomResponse [ "ErrorModel" ] ;
if ( featureFlagEnabled )
{
// NEW BEHAVIOR: Recovery succeeds, then SSO blocks with descriptive message
Assert . Equal (
"Two-factor recovery has been performed. SSO authentication is required." ,
errorResponse . Message ) ;
// Verify recovery was marked
Assert . True ( requestContext . TwoFactorRecoveryRequested ,
"TwoFactorRecoveryRequested flag should be set" ) ;
}
else
{
// LEGACY BEHAVIOR: SSO blocks BEFORE recovery can happen
Assert . Equal (
"SSO authentication is required." ,
errorResponse . Message ) ;
// Recovery succeeds, then SSO blocks with descriptive message
Assert . Equal (
"Two-factor recovery has been performed. SSO authentication is required." ,
errorResponse . Message ) ;
// Recovery never happened because SSO checked first
Assert . False ( requestContext . TwoFactorRecoveryRequested ,
"TwoFactorRecoveryRequested should be false (SSO blocked first)" ) ;
}
// Verify recovery was marked
Assert . True ( requestContext . TwoFactorRecoveryRequested ,
"TwoFactorRecoveryRequested flag should be set" ) ;
// In both cases: User is NOT logged in
// User is NOT logged in
await _ eventService . DidNotReceive ( ) . LogUserEventAsync ( user . Id , EventType . User_LoggedIn ) ;
}
@ -1078,16 +1011,14 @@ public class BaseRequestValidatorTests
@@ -1078,16 +1011,14 @@ public class BaseRequestValidatorTests
/// 4. NOT be logged in
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_InvalidRecoveryCodeForSsoRequiredUser_FailsAt2FA (
bool featureFlagEnabled ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagEnabled ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
var user = requestContext . User ;
@ -1096,8 +1027,8 @@ public class BaseRequestValidatorTests
@@ -1096,8 +1027,8 @@ public class BaseRequestValidatorTests
// 2. SSO is required
_ policyService . AnyPoliciesApplicableToUserAsync (
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
. Returns ( Task . FromResult ( true ) ) ;
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
. Returns ( Task . FromResult ( true ) ) ;
// 3. 2FA is required
_ twoFactorAuthenticationValidator
@ -1121,51 +1052,32 @@ public class BaseRequestValidatorTests
@@ -1121,51 +1052,32 @@ public class BaseRequestValidatorTests
var errorResponse = ( ErrorResponseModel ) context . GrantResult . CustomResponse [ "ErrorModel" ] ;
if ( featureFlagEnabled )
{
// NEW BEHAVIOR: 2FA is checked first (due to recovery code request), fails with 2FA error
Assert . Equal (
"Two-step token is invalid. Try again." ,
errorResponse . Message ) ;
// 2FA is checked first (due to recovery code request), fails with 2FA error
Assert . Equal (
"Two-step token is invalid. Try again." ,
errorResponse . Message ) ;
// Recovery was attempted but failed - flag should NOT be set
Assert . False ( requestContext . TwoFactorRecoveryRequested ,
"TwoFactorRecoveryRequested should be false (recovery failed)" ) ;
// Recovery was attempted but failed - flag should NOT be set
Assert . False ( requestContext . TwoFactorRecoveryRequested ,
"TwoFactorRecoveryRequested should be false (recovery failed)" ) ;
// Verify failed 2FA email was sent
await _ mailService . Received ( 1 ) . SendFailedTwoFactorAttemptEmailAsync (
user . Email ,
TwoFactorProviderType . RecoveryCode ,
Arg . Any < DateTime > ( ) ,
Arg . Any < string > ( ) ) ;
// Verify failed 2FA email was sent
await _ mailService . Received ( 1 ) . SendFailedTwoFactorAttemptEmailAsync (
user . Email ,
TwoFactorProviderType . RecoveryCode ,
Arg . Any < DateTime > ( ) ,
Arg . Any < string > ( ) ) ;
// Verify failed login event was logged
await _ eventService . Received ( 1 ) . LogUserEventAsync ( user . Id , EventType . User_FailedLogIn2fa ) ;
// Verify failed login event was logged
await _ eventService . Received ( 1 ) . LogUserEventAsync ( user . Id , EventType . User_FailedLogIn2fa ) ;
}
else
{
// LEGACY BEHAVIOR: SSO is checked first, blocks before 2FA
Assert . Equal (
"SSO authentication is required." ,
errorResponse . Message ) ;
// 2FA validation never happened
await _ mailService . DidNotReceive ( ) . SendFailedTwoFactorAttemptEmailAsync (
Arg . Any < string > ( ) ,
Arg . Any < TwoFactorProviderType > ( ) ,
Arg . Any < DateTime > ( ) ,
Arg . Any < string > ( ) ) ;
}
// In both cases: User is NOT logged in
// User is NOT logged in
await _ eventService . DidNotReceive ( ) . LogUserEventAsync ( user . Id , EventType . User_LoggedIn ) ;
// Verify user failed login count was updated (in new behavior path)
if ( featureFlagEnabled )
{
await _ userRepository . Received ( 1 ) . ReplaceAsync ( Arg . Is < User > ( u = >
u . Id = = user . Id & & u . FailedLoginCount > 0 ) ) ;
}
await _ userRepository . Received ( 1 ) . ReplaceAsync ( Arg . Is < User > ( u = >
u . Id = = user . Id & & u . FailedLoginCount > 0 ) ) ;
}
/// <summary>
@ -1179,16 +1091,14 @@ public class BaseRequestValidatorTests
@@ -1179,16 +1091,14 @@ public class BaseRequestValidatorTests
/// This is the "happy path" for recovery code usage.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RecoveryCodeForNonSsoUser_SuccessfulLogin (
bool featureFlagEnabled ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( featureFlagEnabled ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
var user = requestContext . User ;
@ -1197,8 +1107,8 @@ public class BaseRequestValidatorTests
@@ -1197,8 +1107,8 @@ public class BaseRequestValidatorTests
// 2. SSO is NOT required (this is a regular user, not in SSO org)
_ policyService . AnyPoliciesApplicableToUserAsync (
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
. Returns ( Task . FromResult ( false ) ) ;
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
. Returns ( Task . FromResult ( false ) ) ;
// 3. 2FA is required
_ twoFactorAuthenticationValidator
@ -1235,7 +1145,8 @@ public class BaseRequestValidatorTests
@@ -1235,7 +1145,8 @@ public class BaseRequestValidatorTests
await _ sut . ValidateAsync ( context ) ;
// Assert
Assert . False ( context . GrantResult . IsError , "Authentication should succeed for non-SSO user with valid recovery code" ) ;
Assert . False ( context . GrantResult . IsError ,
"Authentication should succeed for non-SSO user with valid recovery code" ) ;
// Verify user successfully logged in
await _ eventService . Received ( 1 ) . LogUserEventAsync ( user . Id , EventType . User_LoggedIn ) ;
@ -1244,19 +1155,9 @@ public class BaseRequestValidatorTests
@@ -1244,19 +1155,9 @@ public class BaseRequestValidatorTests
await _ userRepository . Received ( 1 ) . ReplaceAsync ( Arg . Is < User > ( u = >
u . Id = = user . Id & & u . FailedLoginCount = = 0 ) ) ;
if ( featureFlagEnabled )
{
// NEW BEHAVIOR: Recovery flag should be set for audit purposes
Assert . True ( requestContext . TwoFactorRecoveryRequested ,
"TwoFactorRecoveryRequested flag should be set for audit/logging" ) ;
}
else
{
// LEGACY BEHAVIOR: Recovery flag doesn't exist, but login still succeeds
// (SSO check happens before 2FA in legacy, but user is not SSO-required so both pass)
Assert . False ( requestContext . TwoFactorRecoveryRequested ,
"TwoFactorRecoveryRequested should be false in legacy mode" ) ;
}
// Recovery flag should be set for audit purposes
Assert . True ( requestContext . TwoFactorRecoveryRequested ,
"TwoFactorRecoveryRequested flag should be set for audit/logging" ) ;
}
/// <summary>
@ -1265,16 +1166,14 @@ public class BaseRequestValidatorTests
@@ -1265,16 +1166,14 @@ public class BaseRequestValidatorTests
/// is checked using the old PolicyService.AnyPoliciesApplicableToUserAsync approach.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Disabled_UsesLegacySsoValidation (
bool recoveryCodeFeatureEnabled ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( recoveryCodeFeatureEnabled ) ;
_f eatureService . IsEnabled ( FeatureFlagKeys . RedirectOnSsoRequired ) . Returns ( false ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
@ -1284,7 +1183,7 @@ public class BaseRequestValidatorTests
@@ -1284,7 +1183,7 @@ public class BaseRequestValidatorTests
// SSO is required via legacy path
_ policyService . AnyPoliciesApplicableToUserAsync (
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
. Returns ( Task . FromResult ( true ) ) ;
// Act
@ -1309,16 +1208,14 @@ public class BaseRequestValidatorTests
@@ -1309,16 +1208,14 @@ public class BaseRequestValidatorTests
/// instead of the legacy RequireSsoLoginAsync method.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_UsesNewSsoRequestValidator (
bool recoveryCodeFeatureEnabled ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( recoveryCodeFeatureEnabled ) ;
_f eatureService . IsEnabled ( FeatureFlagKeys . RedirectOnSsoRequired ) . Returns ( true ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
@ -1328,9 +1225,9 @@ public class BaseRequestValidatorTests
@@ -1328,9 +1225,9 @@ public class BaseRequestValidatorTests
// Configure SsoRequestValidator to indicate SSO is required
_ ssoRequestValidator . ValidateAsync (
Arg . Any < User > ( ) ,
Arg . Any < ValidatedTokenRequest > ( ) ,
Arg . Any < CustomValidatorRequestContext > ( ) )
Arg . Any < User > ( ) ,
Arg . Any < ValidatedTokenRequest > ( ) ,
Arg . Any < CustomValidatorRequestContext > ( ) )
. Returns ( Task . FromResult ( false ) ) ; // false = SSO required
// Set up the ValidationErrorResult that SsoRequestValidator would set
@ -1367,16 +1264,14 @@ public class BaseRequestValidatorTests
@@ -1367,16 +1264,14 @@ public class BaseRequestValidatorTests
/// authentication continues successfully through the new validation path.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_SsoNotRequired_SuccessfulLogin (
bool recoveryCodeFeatureEnabled ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( recoveryCodeFeatureEnabled ) ;
_f eatureService . IsEnabled ( FeatureFlagKeys . RedirectOnSsoRequired ) . Returns ( true ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
@ -1387,9 +1282,9 @@ public class BaseRequestValidatorTests
@@ -1387,9 +1282,9 @@ public class BaseRequestValidatorTests
// SsoRequestValidator returns true (SSO not required)
_ ssoRequestValidator . ValidateAsync (
Arg . Any < User > ( ) ,
Arg . Any < ValidatedTokenRequest > ( ) ,
Arg . Any < CustomValidatorRequestContext > ( ) )
Arg . Any < User > ( ) ,
Arg . Any < ValidatedTokenRequest > ( ) ,
Arg . Any < CustomValidatorRequestContext > ( ) )
. Returns ( Task . FromResult ( true ) ) ;
// No 2FA required
@ -1430,16 +1325,14 @@ public class BaseRequestValidatorTests
@@ -1430,16 +1325,14 @@ public class BaseRequestValidatorTests
/// (e.g., with organization identifier), that custom response is properly propagated to the result.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_PropagatesCustomResponse (
bool recoveryCodeFeatureEnabled ,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( recoveryCodeFeatureEnabled ) ;
_f eatureService . IsEnabled ( FeatureFlagKeys . RedirectOnSsoRequired ) . Returns ( true ) ;
_ sut . isValid = true ;
@ -1461,9 +1354,9 @@ public class BaseRequestValidatorTests
@@ -1461,9 +1354,9 @@ public class BaseRequestValidatorTests
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
_ ssoRequestValidator . ValidateAsync (
Arg . Any < User > ( ) ,
Arg . Any < ValidatedTokenRequest > ( ) ,
Arg . Any < CustomValidatorRequestContext > ( ) )
Arg . Any < User > ( ) ,
Arg . Any < ValidatedTokenRequest > ( ) ,
Arg . Any < CustomValidatorRequestContext > ( ) )
. Returns ( Task . FromResult ( false ) ) ;
// Act
@ -1473,7 +1366,8 @@ public class BaseRequestValidatorTests
@@ -1473,7 +1366,8 @@ public class BaseRequestValidatorTests
Assert . True ( context . GrantResult . IsError ) ;
Assert . NotNull ( context . GrantResult . CustomResponse ) ;
Assert . Contains ( "SsoOrganizationIdentifier" , context . CustomValidatorRequestContext . CustomResponse ) ;
Assert . Equal ( "test-org-identifier" , context . CustomValidatorRequestContext . CustomResponse [ "SsoOrganizationIdentifier" ] ) ;
Assert . Equal ( "test-org-identifier" ,
context . CustomValidatorRequestContext . CustomResponse [ "SsoOrganizationIdentifier" ] ) ;
}
/// <summary>
@ -1484,11 +1378,11 @@ public class BaseRequestValidatorTests
@@ -1484,11 +1378,11 @@ public class BaseRequestValidatorTests
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Disabled_RecoveryWithSso_LegacyMessage (
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( true ) ;
_f eatureService . IsEnabled ( FeatureFlagKeys . RedirectOnSsoRequired ) . Returns ( false ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
@ -1509,7 +1403,7 @@ public class BaseRequestValidatorTests
@@ -1509,7 +1403,7 @@ public class BaseRequestValidatorTests
// SSO is required (legacy check)
_ policyService . AnyPoliciesApplicableToUserAsync (
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
Arg . Any < Guid > ( ) , PolicyType . RequireSso , OrganizationUserStatusType . Confirmed )
. Returns ( Task . FromResult ( true ) ) ;
// Act
@ -1535,11 +1429,11 @@ public class BaseRequestValidatorTests
@@ -1535,11 +1429,11 @@ public class BaseRequestValidatorTests
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_RecoveryWithSso_NewValidatorMessage (
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest ,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext ,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext ,
GrantValidationResult grantResult )
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag ( true ) ;
_f eatureService . IsEnabled ( FeatureFlagKeys . RedirectOnSsoRequired ) . Returns ( true ) ;
var context = CreateContext ( tokenRequest , requestContext , grantResult ) ;
@ -1568,13 +1462,16 @@ public class BaseRequestValidatorTests
@@ -1568,13 +1462,16 @@ public class BaseRequestValidatorTests
} ;
requestContext . CustomResponse = new Dictionary < string , object >
{
{ "ErrorModel" , new ErrorResponseModel ( "Two-factor recovery has been performed. SSO authentication is required." ) }
{
"ErrorModel" ,
new ErrorResponseModel ( "Two-factor recovery has been performed. SSO authentication is required." )
}
} ;
_ ssoRequestValidator . ValidateAsync (
Arg . Any < User > ( ) ,
Arg . Any < ValidatedTokenRequest > ( ) ,
Arg . Any < CustomValidatorRequestContext > ( ) )
Arg . Any < User > ( ) ,
Arg . Any < ValidatedTokenRequest > ( ) ,
Arg . Any < CustomValidatorRequestContext > ( ) )
. Returns ( Task . FromResult ( false ) ) ;
// Act