Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ public override async Task<IActionResult> OnGetCallbackAsync(string? returnUrl =
}

// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
// bypassTwoFactor is false so that accounts with two-factor authentication enabled are
// still challenged for a second factor, consistent with the password sign-in path.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: false);
if (result.Succeeded)
{
if (_logger.IsEnabled(LogLevel.Information))
Expand All @@ -147,6 +149,10 @@ public override async Task<IActionResult> OnGetCallbackAsync(string? returnUrl =
}
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl });
}
if (result.IsLockedOut)
{
return RedirectToPage("./Lockout");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ public override async Task<IActionResult> OnGetCallbackAsync(string? returnUrl =
}

// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
// bypassTwoFactor is false so that accounts with two-factor authentication enabled are
// still challenged for a second factor, consistent with the password sign-in path.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: false);
if (result.Succeeded)
{
if (_logger.IsEnabled(LogLevel.Information))
Expand All @@ -147,6 +149,10 @@ public override async Task<IActionResult> OnGetCallbackAsync(string? returnUrl =
}
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl });
}
if (result.IsLockedOut)
{
return RedirectToPage("./Lockout");
Expand Down
31 changes: 31 additions & 0 deletions src/Identity/test/Identity.FunctionalTests/LoginTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,37 @@ void ConfigureTestServices(IServiceCollection services) => services
await UserStories.LoginExistingUserAsync(newClient, userName, password);
}

[Fact]
public async Task ExternalLoginIsChallengedForTwoFactorWhenAccountHasTwoFactorEnabled()
{
// Arrange
void ConfigureTestServices(IServiceCollection services) =>
services.SetupTestThirdPartyLogin();

var server = ServerFactory.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices));

var client = server.CreateClient();
var newClient = server.CreateClient();

var guid = Guid.NewGuid();
var userName = $"{guid}";
var email = $"{guid}@example.com";

var index = await UserStories.RegisterNewUserWithSocialLoginAsync(client, userName, email);
var manage = await index.ClickManageLinkWithExternalLoginAsync();
var twoFactor = await manage.ClickTwoFactorLinkAsync();
var enableAuthenticator = await twoFactor.ClickEnableAuthenticatorLinkAsync();
var showRecoveryCodes = await enableAuthenticator.SendValidCodeAsync();

var twoFactorKey = showRecoveryCodes.Context.AuthenticatorKey;

// Act & Assert
// Use a new client to simulate a new browser session. Signing in again via the same
// external provider must now be challenged for the second factor instead of completing
// immediately, matching the behavior of the password sign-in path.
await UserStories.LoginWithSocialLogin2FaAsync(newClient, userName, twoFactorKey);
}

[Fact]
public async Task CanLoginWithASocialLoginProvider()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,26 @@ public async Task<Index> SendExistingUserNameAsync(string userName)
return new Index(Client, externalLogin, Context.WithAuthenticatedUser());
}

// Used when the existing external-login account has two-factor authentication enabled:
// the external sign-in must be challenged for a second factor instead of completing immediately.
public async Task<Account.LoginWith2fa> SendExistingUserNameWith2FaAsync(string userName)
{
var contosoResponse = await Client.SendAsync(_loginForm, new Dictionary<string, string>
{
["Input_Login"] = userName
});

var goToExternalLogin = ResponseAssert.IsRedirect(contosoResponse);
var externalLogInResponse = await Client.GetAsync(goToExternalLogin);

var goToLoginWith2fa = ResponseAssert.IsRedirect(externalLogInResponse);
Assert.StartsWith(Account.LoginWith2fa.Path, goToLoginWith2fa.ToString());
var loginWith2faResponse = await Client.GetAsync(goToLoginWith2fa);
var loginWith2fa = await ResponseAssert.IsHtmlDocumentAsync(loginWith2faResponse);

return new Account.LoginWith2fa(Client, loginWith2fa, Context);
}

private async Task<IHtmlDocument> SendLoginForm(string userName)
{
var contosoResponse = await Client.SendAsync(_loginForm, new Dictionary<string, string>
Expand Down
19 changes: 19 additions & 0 deletions src/Identity/test/Identity.FunctionalTests/UserStories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ internal static async Task<Index> LoginWithSocialLoginAsync(HttpClient client, s
return await contosoLogin.SendExistingUserNameAsync(userName);
}

// Covers the case where an existing external-login account has two-factor authentication
// enabled: the sign-in must route through the two-factor page before completing.
internal static async Task<Index> LoginWithSocialLogin2FaAsync(HttpClient client, string userName, string twoFactorKey)
{
var index = await Index.CreateAsync(
client,
new DefaultUIContext()
.WithSocialLoginEnabled()
.WithExistingUser());

var login = await index.ClickLoginLinkAsync();

var contosoLogin = await login.ClickLoginWithContosoLinkAsync();

var login2Fa = await contosoLogin.SendExistingUserNameWith2FaAsync(userName);

return await login2Fa.Send2FACodeAsync(twoFactorKey);
}

internal static async Task<Index> LoginExistingUser2FaAsync(HttpClient client, string userName, string password, string twoFactorKey)
{
var index = await Index.CreateAsync(client);
Expand Down
Loading