@@ -486,6 +486,168 @@ public async Task CanPasskeySignIn()
486486 ] ) ) ;
487487 }
488488
489+ [ Theory ]
490+ [ InlineData ( true ) ]
491+ [ InlineData ( false ) ]
492+ public async Task CanRequireConfirmedEmailForPasskeySignIn ( bool confirmed )
493+ {
494+ // Setup
495+ var user = new PocoUser { UserName = "Foo" } ;
496+ var passkey = new UserPasskeyInfo ( null , null , default , 0 , null , false , false , false , null , null ) ;
497+ var assertionResult = PasskeyAssertionResult . Success ( passkey , user ) ;
498+ var passkeyHandler = new Mock < IPasskeyHandler < PocoUser > > ( ) ;
499+ passkeyHandler
500+ . Setup ( h => h . MakeRequestOptionsAsync ( user , It . IsAny < HttpContext > ( ) ) )
501+ . Returns ( Task . FromResult ( new PasskeyRequestOptionsResult
502+ {
503+ AssertionState = "<some-assertion-state>" ,
504+ RequestOptionsJson = "<some-options-json>" ,
505+ } ) ) ;
506+ passkeyHandler
507+ . Setup ( h => h . PerformAssertionAsync ( It . IsAny < PasskeyAssertionContext > ( ) ) )
508+ . Returns ( Task . FromResult ( assertionResult ) ) ;
509+ var manager = SetupUserManager ( user , passkeyHandler : passkeyHandler . Object ) ;
510+ manager . Setup ( m => m . IsEmailConfirmedAsync ( user ) ) . ReturnsAsync ( confirmed ) . Verifiable ( ) ;
511+ var context = new DefaultHttpContext ( ) ;
512+ var auth = MockAuth ( context ) ;
513+ if ( confirmed )
514+ {
515+ manager
516+ . Setup ( m => m . AddOrUpdatePasskeyAsync ( user , passkey ) )
517+ . Returns ( Task . FromResult ( IdentityResult . Success ) )
518+ . Verifiable ( ) ;
519+ SetupSignIn ( context , auth , user . Id , isPersistent : false , loginProvider : null ) ;
520+ }
521+ SetupPasskeyAuth ( context , auth ) ;
522+
523+ var identityOptions = new IdentityOptions ( ) ;
524+ identityOptions . SignIn . RequireConfirmedEmail = true ;
525+ var logger = new TestLogger < SignInManager < PocoUser > > ( ) ;
526+ var helper = SetupSignInManager ( manager . Object , context , logger , identityOptions ) ;
527+
528+ // Act
529+ await helper . MakePasskeyRequestOptionsAsync ( user ) ;
530+ var signInResult = await helper . PasskeySignInAsync ( credentialJson : "<some-passkey>" ) ;
531+
532+ // Assert
533+ Assert . Equal ( confirmed , signInResult . Succeeded ) ;
534+ Assert . NotEqual ( confirmed , signInResult . IsNotAllowed ) ;
535+
536+ var message = $ "User cannot sign in without a confirmed email.";
537+ if ( ! confirmed )
538+ {
539+ Assert . Contains ( message , logger . LogMessages ) ;
540+ }
541+ else
542+ {
543+ Assert . DoesNotContain ( message , logger . LogMessages ) ;
544+ }
545+
546+ manager . Verify ( ) ;
547+ auth . Verify ( ) ;
548+ }
549+
550+ [ Theory ]
551+ [ InlineData ( true ) ]
552+ [ InlineData ( false ) ]
553+ public async Task CanRequireConfirmedPhoneNumberForPasskeySignIn ( bool confirmed )
554+ {
555+ // Setup
556+ var user = new PocoUser { UserName = "Foo" } ;
557+ var passkey = new UserPasskeyInfo ( null , null , default , 0 , null , false , false , false , null , null ) ;
558+ var assertionResult = PasskeyAssertionResult . Success ( passkey , user ) ;
559+ var passkeyHandler = new Mock < IPasskeyHandler < PocoUser > > ( ) ;
560+ passkeyHandler
561+ . Setup ( h => h . MakeRequestOptionsAsync ( user , It . IsAny < HttpContext > ( ) ) )
562+ . Returns ( Task . FromResult ( new PasskeyRequestOptionsResult
563+ {
564+ AssertionState = "<some-assertion-state>" ,
565+ RequestOptionsJson = "<some-options-json>" ,
566+ } ) ) ;
567+ passkeyHandler
568+ . Setup ( h => h . PerformAssertionAsync ( It . IsAny < PasskeyAssertionContext > ( ) ) )
569+ . Returns ( Task . FromResult ( assertionResult ) ) ;
570+ var manager = SetupUserManager ( user , passkeyHandler : passkeyHandler . Object ) ;
571+ manager . Setup ( m => m . IsPhoneNumberConfirmedAsync ( user ) ) . ReturnsAsync ( confirmed ) . Verifiable ( ) ;
572+ var context = new DefaultHttpContext ( ) ;
573+ var auth = MockAuth ( context ) ;
574+ if ( confirmed )
575+ {
576+ manager
577+ . Setup ( m => m . AddOrUpdatePasskeyAsync ( user , passkey ) )
578+ . Returns ( Task . FromResult ( IdentityResult . Success ) )
579+ . Verifiable ( ) ;
580+ SetupSignIn ( context , auth , user . Id , isPersistent : false , loginProvider : null ) ;
581+ }
582+ SetupPasskeyAuth ( context , auth ) ;
583+
584+ var identityOptions = new IdentityOptions ( ) ;
585+ identityOptions . SignIn . RequireConfirmedPhoneNumber = true ;
586+ var logger = new TestLogger < SignInManager < PocoUser > > ( ) ;
587+ var helper = SetupSignInManager ( manager . Object , context , logger , identityOptions ) ;
588+
589+ // Act
590+ await helper . MakePasskeyRequestOptionsAsync ( user ) ;
591+ var signInResult = await helper . PasskeySignInAsync ( credentialJson : "<some-passkey>" ) ;
592+
593+ // Assert
594+ Assert . Equal ( confirmed , signInResult . Succeeded ) ;
595+ Assert . NotEqual ( confirmed , signInResult . IsNotAllowed ) ;
596+
597+ var message = $ "User cannot sign in without a confirmed phone number.";
598+ if ( ! confirmed )
599+ {
600+ Assert . Contains ( message , logger . LogMessages ) ;
601+ }
602+ else
603+ {
604+ Assert . DoesNotContain ( message , logger . LogMessages ) ;
605+ }
606+
607+ manager . Verify ( ) ;
608+ auth . Verify ( ) ;
609+ }
610+
611+ [ Fact ]
612+ public async Task PasskeySignInReturnsLockedOutWhenLockedOut ( )
613+ {
614+ // Setup
615+ var user = new PocoUser { UserName = "Foo" } ;
616+ var passkey = new UserPasskeyInfo ( null , null , default , 0 , null , false , false , false , null , null ) ;
617+ var assertionResult = PasskeyAssertionResult . Success ( passkey , user ) ;
618+ var passkeyHandler = new Mock < IPasskeyHandler < PocoUser > > ( ) ;
619+ passkeyHandler
620+ . Setup ( h => h . MakeRequestOptionsAsync ( user , It . IsAny < HttpContext > ( ) ) )
621+ . Returns ( Task . FromResult ( new PasskeyRequestOptionsResult
622+ {
623+ AssertionState = "<some-assertion-state>" ,
624+ RequestOptionsJson = "<some-options-json>" ,
625+ } ) ) ;
626+ passkeyHandler
627+ . Setup ( h => h . PerformAssertionAsync ( It . IsAny < PasskeyAssertionContext > ( ) ) )
628+ . Returns ( Task . FromResult ( assertionResult ) ) ;
629+ var manager = SetupUserManager ( user , passkeyHandler : passkeyHandler . Object ) ;
630+ manager . Setup ( m => m . SupportsUserLockout ) . Returns ( true ) . Verifiable ( ) ;
631+ manager . Setup ( m => m . IsLockedOutAsync ( user ) ) . ReturnsAsync ( true ) . Verifiable ( ) ;
632+ var context = new DefaultHttpContext ( ) ;
633+ var auth = MockAuth ( context ) ;
634+ SetupPasskeyAuth ( context , auth ) ;
635+
636+ var logger = new TestLogger < SignInManager < PocoUser > > ( ) ;
637+ var helper = SetupSignInManager ( manager . Object , context , logger ) ;
638+
639+ // Act
640+ await helper . MakePasskeyRequestOptionsAsync ( user ) ;
641+ var signInResult = await helper . PasskeySignInAsync ( credentialJson : "<some-passkey>" ) ;
642+
643+ // Assert
644+ Assert . False ( signInResult . Succeeded ) ;
645+ Assert . True ( signInResult . IsLockedOut ) ;
646+ Assert . Contains ( $ "User is currently locked out.", logger . LogMessages ) ;
647+ manager . Verify ( ) ;
648+ auth . Verify ( ) ;
649+ }
650+
489651 private static void SetupPasskeyAuth ( HttpContext context , Mock < IAuthenticationService > auth )
490652 {
491653 // Calling AuthenticateAsync will return a failure result
0 commit comments