Skip to content

Commit 6f58315

Browse files
Add PreSignInCheck to PasskeySignInCoreAsync and add unit tests (#65316)
1 parent ce2a97f commit 6f58315

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

src/Identity/Core/src/SignInManager.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,12 @@ private async Task<SignInResult> PasskeySignInCoreAsync(string credentialJson)
666666
return SignInResult.Failed;
667667
}
668668

669+
var error = await PreSignInCheck(assertionResult.User);
670+
if (error != null)
671+
{
672+
return error;
673+
}
674+
669675
// After a successful assertion, we need to update the passkey so that it has the latest
670676
// sign count and authenticator data.
671677
var setPasskeyResult = await UserManager.AddOrUpdatePasskeyAsync(assertionResult.User, assertionResult.Passkey);

src/Identity/test/Identity.Test/SignInManagerTest.cs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)