From 641dfe3b629dfb4acd484d24d21d2040e1aeb31b Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Fri, 16 Mar 2018 10:05:13 -0700 Subject: [PATCH] Use RefreshSignInAsync (#1655) Addresses #1616 --- src/Identity/IdentityConstants.cs | 2 +- .../Account/Manage/ChangePassword.cshtml | 2 +- .../Account/Manage/ChangePassword.cshtml.cs | 2 +- .../Account/Manage/ExternalLogins.cshtml | 6 +- .../Account/Manage/ExternalLogins.cshtml.cs | 2 +- .../Account/Manage/ResetAuthenticator.cshtml | 4 +- .../Manage/ResetAuthenticator.cshtml.cs | 2 +- .../Pages/Account/Manage/SetPassword.cshtml | 2 +- .../Account/Manage/SetPassword.cshtml.cs | 2 +- .../Pages/Account/Manage/_ManageNav.cshtml | 2 +- ...ctionalTestsServiceCollectionExtensions.cs | 11 ++ .../ManagementTests.cs | 136 ++++++++++++++++++ .../Pages/Account/Manage/ChangePassword.cs | 33 +++++ .../Pages/Account/Manage/Index.cs | 37 ++++- .../Pages/Account/Manage/LinkExternalLogin.cs | 39 +++++ .../Account/Manage/ManageExternalLogin.cs | 37 +++++ .../Account/Manage/RemoveExternalLogin.cs | 32 +++++ .../Account/Manage/ResetAuthenticator.cs | 33 +++++ .../Pages/Account/Manage/SetPassword.cs | 32 +++++ .../Account/Manage/TwoFactorAuthentication.cs | 13 ++ test/Identity.FunctionalTests/Pages/Index.cs | 11 ++ test/Identity.FunctionalTests/UserStories.cs | 41 ++++++ .../Pages/Contoso/Login.cshtml | 2 +- 23 files changed, 468 insertions(+), 15 deletions(-) create mode 100644 test/Identity.FunctionalTests/Pages/Account/Manage/ChangePassword.cs create mode 100644 test/Identity.FunctionalTests/Pages/Account/Manage/LinkExternalLogin.cs create mode 100644 test/Identity.FunctionalTests/Pages/Account/Manage/ManageExternalLogin.cs create mode 100644 test/Identity.FunctionalTests/Pages/Account/Manage/RemoveExternalLogin.cs create mode 100644 test/Identity.FunctionalTests/Pages/Account/Manage/ResetAuthenticator.cs create mode 100644 test/Identity.FunctionalTests/Pages/Account/Manage/SetPassword.cs diff --git a/src/Identity/IdentityConstants.cs b/src/Identity/IdentityConstants.cs index 0843a33bd4..e01715dfbc 100644 --- a/src/Identity/IdentityConstants.cs +++ b/src/Identity/IdentityConstants.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Identity { /// - /// Represents all the options you can use to configure the cookies middleware uesd by the identity system. + /// Represents all the options you can use to configure the cookies middleware used by the identity system. /// public class IdentityConstants { diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml b/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml index c35de0c622..878f3afdde 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml +++ b/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml @@ -8,7 +8,7 @@ @Html.Partial("_StatusMessage", Model.StatusMessage)
-
+
diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs index c44118ae9c..32569fdc0c 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs @@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal return Page(); } - await _signInManager.SignInAsync(user, isPersistent: false); + await _signInManager.RefreshSignInAsync(user); _logger.LogInformation("User changed their password successfully."); StatusMessage = "Your password has been changed."; diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml b/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml index ccb66a6a49..b5592871e6 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml +++ b/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml @@ -17,7 +17,7 @@ @if (Model.ShowRemoveButton) { - +
@@ -39,12 +39,12 @@ {

Add another service to log in.


- +

@foreach (var provider in Model.OtherLogins) { - + }

diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs index b03f2f9dcc..256f62335c 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs @@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal throw new InvalidOperationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'."); } - await _signInManager.SignInAsync(user, isPersistent: false); + await _signInManager.RefreshSignInAsync(user); StatusMessage = "The external login was removed."; return RedirectToPage(); } diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml b/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml index 3b999d42c8..334754cfa4 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml +++ b/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml @@ -18,7 +18,7 @@

- - + +
\ No newline at end of file diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs index f8fb0da2cf..b0d977b4e6 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal await _userManager.ResetAuthenticatorKeyAsync(user); _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); - await _signInManager.SignInAsync(user, isPersistent: false); + await _signInManager.RefreshSignInAsync(user); StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key."; return RedirectToPage("./EnableAuthenticator"); diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml b/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml index feeb3c30e4..02d35209f3 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml +++ b/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml @@ -13,7 +13,7 @@

-
+
diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs index 6478d5685b..a2092c7be5 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal return Page(); } - await _signInManager.SignInAsync(user, isPersistent: false); + await _signInManager.RefreshSignInAsync(user); StatusMessage = "Your password has been set."; return RedirectToPage(); diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml b/src/UI/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml index f759d6310b..6e42d16d67 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml +++ b/src/UI/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml @@ -3,7 +3,7 @@
  • Password
  • @if ((bool)ViewData["ManageNav.HasExternalLogins"]) { -
  • External logins
  • +
  • External logins
  • }
  • Two-factor authentication
  • Personal data
  • diff --git a/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs b/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs index dd87f0088e..2ea424972e 100644 --- a/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs +++ b/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs @@ -1,7 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Security.Claims; +using System.Threading.Tasks; using Identity.DefaultUI.WebSite; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.UI.Services; @@ -25,6 +29,13 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests public static IServiceCollection SetupTestEmailSender(this IServiceCollection services, IEmailSender sender) => services.AddSingleton(sender); + public static IServiceCollection SetupGetUserClaimsPrincipal(this IServiceCollection services, Action captureUser, string schemeName) => + services.Configure(schemeName, o => o.Events.OnSigningIn = context => + { + captureUser(context.Principal); + return Task.CompletedTask; + }); + public static IServiceCollection SetupEmailRequired(this IServiceCollection services) => services.Configure(o => o.SignIn.RequireConfirmedEmail = true); diff --git a/test/Identity.FunctionalTests/ManagementTests.cs b/test/Identity.FunctionalTests/ManagementTests.cs index 2bf1a8b32d..2089048afe 100644 --- a/test/Identity.FunctionalTests/ManagementTests.cs +++ b/test/Identity.FunctionalTests/ManagementTests.cs @@ -2,8 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using Identity.DefaultUI.WebSite; +using Microsoft.AspNetCore.TestHost; using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests @@ -53,6 +57,124 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests await UserStories.ConfirmEmailAsync(email, client); } + [Fact] + public async Task CanChangePassword() + { + // Arrange + var principals = new List(); + var server = ServerFactory.CreateServer(builder => + builder.ConfigureTestServices(s => s.SetupGetUserClaimsPrincipal(user => principals.Add(user), IdentityConstants.ApplicationScheme))); + + var client = ServerFactory.CreateDefaultClient(server); + var newClient = ServerFactory.CreateDefaultClient(server); + + var userName = $"{Guid.NewGuid()}@example.com"; + var password = "!Test.Password1"; + + var index = await UserStories.RegisterNewUserAsync(client, userName, password); + + // Act 1 + var changedPassword = await UserStories.ChangePasswordAsync(index, "!Test.Password1", "!Test.Password2"); + + // Assert 1 + // RefreshSignIn generates a new security stamp claim + AssertClaimsNotEqual(principals[0], principals[1], "AspNet.Identity.SecurityStamp"); + + // Act 2 + await UserStories.LoginExistingUserAsync(newClient, userName, "!Test.Password2"); + + // Assert 2 + // Signing in again with a different client uses the same security stamp claim + AssertClaimsEqual(principals[1], principals[2], "AspNet.Identity.SecurityStamp"); + } + + [Fact] + public async Task CanSetPasswordWithExternalLogin() + { + // Arrange + var principals = new List(); + var server = ServerFactory.CreateServer(builder => + builder.ConfigureTestServices(s => s.SetupTestThirdPartyLogin() + .SetupGetUserClaimsPrincipal(user => principals.Add(user), IdentityConstants.ApplicationScheme))); + + var client = ServerFactory.CreateDefaultClient(server); + var newClient = ServerFactory.CreateDefaultClient(server); + + var guid = Guid.NewGuid(); + var userName = $"{guid}"; + var email = $"{guid}@example.com"; + + // Act 1 + var index = await UserStories.RegisterNewUserWithSocialLoginAsync(client, userName, email); + index = await UserStories.LoginWithSocialLoginAsync(newClient, userName); + + // Assert 1 + Assert.NotNull(principals[1].Identities.Single().Claims.Single(c => c.Type == ClaimTypes.AuthenticationMethod).Value); + + // Act 2 + await UserStories.SetPasswordAsync(index, "!Test.Password2"); + + // Assert 2 + // RefreshSignIn uses the same AuthenticationMethod claim value + AssertClaimsEqual(principals[1], principals[2], ClaimTypes.AuthenticationMethod); + + // Act & Assert 3 + // Can log in with the password set above + await UserStories.LoginExistingUserAsync(ServerFactory.CreateDefaultClient(server), email, "!Test.Password2"); + } + + [Fact] + public async Task CanRemoveExternalLogin() + { + // Arrange + var principals = new List(); + var server = ServerFactory.CreateServer(builder => + builder.ConfigureTestServices(s => s.SetupTestThirdPartyLogin() + .SetupGetUserClaimsPrincipal(user => principals.Add(user), IdentityConstants.ApplicationScheme))); + + var client = ServerFactory.CreateDefaultClient(server); + + var guid = Guid.NewGuid(); + var userName = $"{guid}"; + var email = $"{guid}@example.com"; + + // Act + var index = await UserStories.RegisterNewUserAsync(client, email, "!TestPassword1"); + var linkLogin = await UserStories.LinkExternalLoginAsync(index, email); + await UserStories.RemoveExternalLoginAsync(linkLogin, email); + + // RefreshSignIn generates a new security stamp claim + AssertClaimsNotEqual(principals[0], principals[1], "AspNet.Identity.SecurityStamp"); + } + + [Fact] + public async Task CanResetAuthenticator() + { + // Arrange + var principals = new List(); + var server = ServerFactory.CreateServer(builder => + builder.ConfigureTestServices(s => s.SetupTestThirdPartyLogin() + .SetupGetUserClaimsPrincipal(user => principals.Add(user), IdentityConstants.ApplicationScheme))); + + var client = ServerFactory.CreateDefaultClient(server); + var newClient = ServerFactory.CreateDefaultClient(server); + + var userName = $"{Guid.NewGuid()}@example.com"; + var password = $"!Test.Password1$"; + + // Act + var loggedIn = await UserStories.RegisterNewUserAsync(client, userName, password); + var showRecoveryCodes = await UserStories.EnableTwoFactorAuthentication(loggedIn); + var twoFactorKey = showRecoveryCodes.Context.AuthenticatorKey; + + // Use a new client to simulate a new browser session. + var index = await UserStories.LoginExistingUser2FaAsync(newClient, userName, password, twoFactorKey); + await UserStories.ResetAuthenticator(index); + + // RefreshSignIn generates a new security stamp claim + AssertClaimsNotEqual(principals[1], principals[2], "AspNet.Identity.SecurityStamp"); + } + [Fact] public async Task CanDownloadPersonalData() { @@ -89,5 +211,19 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests // Act & Assert await UserStories.DeleteUser(index, password); } + + private void AssertClaimsEqual(ClaimsPrincipal expectedPrincipal, ClaimsPrincipal actualPrincipal, string claimType) + { + var expectedPrincipalClaim = expectedPrincipal.Identities.Single().Claims.Single(c => c.Type == claimType).Value; + var actualPrincipalClaim = actualPrincipal.Identities.Single().Claims.Single(c => c.Type == claimType).Value; + Assert.Equal(expectedPrincipalClaim, actualPrincipalClaim); + } + + private void AssertClaimsNotEqual(ClaimsPrincipal expectedPrincipal, ClaimsPrincipal actualPrincipal, string claimType) + { + var expectedPrincipalClaim = expectedPrincipal.Identities.Single().Claims.Single(c => c.Type == claimType).Value; + var actualPrincipalClaim = actualPrincipal.Identities.Single().Claims.Single(c => c.Type == claimType).Value; + Assert.NotEqual(expectedPrincipalClaim, actualPrincipalClaim); + } } } diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/ChangePassword.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/ChangePassword.cs new file mode 100644 index 0000000000..126f9f6529 --- /dev/null +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/ChangePassword.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage +{ + public class ChangePassword : DefaultUIPage + { + private readonly IHtmlFormElement _changePasswordForm; + + public ChangePassword(HttpClient client, IHtmlDocument changePassword, DefaultUIContext context) + : base(client, changePassword, context) + { + _changePasswordForm = HtmlAssert.HasForm("#change-password-form", changePassword); + } + + public async Task ChangePasswordAsync(string oldPassword, string newPassword) + { + await Client.SendAsync(_changePasswordForm, new Dictionary + { + ["Input_OldPassword"] = oldPassword, + ["Input_NewPassword"] = newPassword, + ["Input_ConfirmPassword"] = newPassword + }); + + return this; + } + } +} diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs index 329f3d4abe..255339e6dc 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Net.Http; using System.Threading.Tasks; using AngleSharp.Dom.Html; @@ -14,6 +13,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage private readonly IHtmlAnchorElement _profileLink; private readonly IHtmlAnchorElement _changePasswordLink; private readonly IHtmlAnchorElement _twoFactorLink; + private readonly IHtmlAnchorElement _externalLoginLink; private readonly IHtmlAnchorElement _personalDataLink; private readonly IHtmlFormElement _updateProfileForm; private readonly IHtmlElement _confirmEmailButton; @@ -27,6 +27,10 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage _profileLink = HtmlAssert.HasLink("#profile", manage); _changePasswordLink = HtmlAssert.HasLink("#change-password", manage); _twoFactorLink = HtmlAssert.HasLink("#two-factor", manage); + if (Context.ContosoLoginEnabled) + { + _externalLoginLink = HtmlAssert.HasLink("#external-login", manage); + } _personalDataLink = HtmlAssert.HasLink("#personal-data", manage); _updateProfileForm = HtmlAssert.HasForm("#profile-form", manage); if (!Context.EmailConfirmed) @@ -43,6 +47,14 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage return new TwoFactorAuthentication(Client, twoFactor, Context); } + public async Task ClickTwoFactorEnabledLinkAsync() + { + var goToTwoFactor = await Client.GetAsync(_twoFactorLink.Href); + var twoFactor = await ResponseAssert.IsHtmlDocumentAsync(goToTwoFactor); + Context.TwoFactorEnabled = true; + return new TwoFactorAuthentication(Client, twoFactor, Context); + } + internal async Task SendConfirmationEmailAsync() { Assert.False(Context.EmailConfirmed); @@ -55,6 +67,22 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage return new Index(Client, manage, Context); } + public async Task ClickChangePasswordLinkAsync() + { + var goToChangePassword = await Client.GetAsync(_changePasswordLink.Href); + var changePasswordDocument = await ResponseAssert.IsHtmlDocumentAsync(goToChangePassword); + return new ChangePassword(Client, changePasswordDocument, Context); + } + + public async Task ClickChangePasswordLinkExternalLoginAsync() + { + var response = await Client.GetAsync(_changePasswordLink.Href); + var goToSetPassword = ResponseAssert.IsRedirect(response); + var setPasswordResponse = await Client.GetAsync(goToSetPassword); + var setPasswordDocument = await ResponseAssert.IsHtmlDocumentAsync(setPasswordResponse); + return new SetPassword(Client, setPasswordDocument, Context); + } + public async Task ClickPersonalDataLinkAsync() { var goToPersonalData = await Client.GetAsync(_personalDataLink.Href); @@ -62,5 +90,12 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage return new PersonalData(Client, personalData, Context); } + public async Task ClickLinkLoginAsync() + { + var goToExternalLogin = await Client.GetAsync(_externalLoginLink.Href); + var externalLoginDocument = await ResponseAssert.IsHtmlDocumentAsync(goToExternalLogin); + + return new LinkExternalLogin(Client, externalLoginDocument, Context); + } } } diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/LinkExternalLogin.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/LinkExternalLogin.cs new file mode 100644 index 0000000000..f601b75def --- /dev/null +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/LinkExternalLogin.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage +{ + public class LinkExternalLogin : DefaultUIPage + { + private readonly IHtmlFormElement _linkLoginForm; + private readonly IHtmlElement _linkLoginButton; + + public LinkExternalLogin(HttpClient client, IHtmlDocument externalLoginsDocument, DefaultUIContext context) + : base(client, externalLoginsDocument, context) + { + _linkLoginForm = HtmlAssert.HasForm("#link-login-form", externalLoginsDocument); + _linkLoginButton = HtmlAssert.HasElement("#link-login-button", externalLoginsDocument); + } + + public async Task LinkExternalLoginAsync(string loginEmail) + { + // Click on the button to link external login to current user account + var linkExternalLogin = await Client.SendAsync(_linkLoginForm, _linkLoginButton); + var goToLinkExternalLogin = ResponseAssert.IsRedirect(linkExternalLogin); + var externalLoginResponse = await Client.GetAsync(goToLinkExternalLogin); + var externalLoginDocument = await ResponseAssert.IsHtmlDocumentAsync(externalLoginResponse); + + // Redirected to manage page for external login with a remove button + return new ManageExternalLogin(Client, externalLoginDocument, Context); + } + + public RemoveExternalLogin ClickRemoveLoginAsync(IHtmlDocument linkedExternalLoginDocument) + { + return new RemoveExternalLogin(Client, linkedExternalLoginDocument, Context); + } + } +} \ No newline at end of file diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/ManageExternalLogin.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/ManageExternalLogin.cs new file mode 100644 index 0000000000..f3904abef9 --- /dev/null +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/ManageExternalLogin.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage +{ + public class ManageExternalLogin : DefaultUIPage + { + private readonly IHtmlFormElement _externalLoginForm; + + public ManageExternalLogin(HttpClient client, IHtmlDocument externalLoginDocument, DefaultUIContext context) + : base(client, externalLoginDocument, context) + { + _externalLoginForm = HtmlAssert.HasForm("#external-login", externalLoginDocument); + } + + public async Task ManageExternalLoginAsync(string loginEmail) + { + var linkedExternalLogin = await Client.SendAsync(_externalLoginForm, new Dictionary + { + ["Input_Login"] = loginEmail + }); + + var goToLinkedExternalLogin = ResponseAssert.IsRedirect(linkedExternalLogin); + var externalLoginResponse = await Client.GetAsync(goToLinkedExternalLogin); + var goToManageExternalLogin = ResponseAssert.IsRedirect(externalLoginResponse); + var manageExternalLoginResponse = await Client.GetAsync(goToManageExternalLogin); + + var manageExternalLoginDocument = await ResponseAssert.IsHtmlDocumentAsync(manageExternalLoginResponse); + return new RemoveExternalLogin(Client, manageExternalLoginDocument, Context); + } + } +} diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/RemoveExternalLogin.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/RemoveExternalLogin.cs new file mode 100644 index 0000000000..b3022d0998 --- /dev/null +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/RemoveExternalLogin.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage +{ + public class RemoveExternalLogin : DefaultUIPage + { + private readonly IHtmlFormElement _removeLoginForm; + + public RemoveExternalLogin(HttpClient client, IHtmlDocument externalLogin, DefaultUIContext context) + : base(client, externalLogin, context) + { + _removeLoginForm = HtmlAssert.HasForm("#remove-login", externalLogin); + } + + public async Task RemoveLoginAsync(string loginProvider, string providerKey) + { + await Client.SendAsync(_removeLoginForm, new Dictionary + { + ["login_LoginProvider"] = loginProvider, + ["login_ProviderKey"] = providerKey + }); + + return this; + } + } +} \ No newline at end of file diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/ResetAuthenticator.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/ResetAuthenticator.cs new file mode 100644 index 0000000000..40bb6caadc --- /dev/null +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/ResetAuthenticator.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage +{ + public class ResetAuthenticator : DefaultUIPage + { + private readonly IHtmlFormElement _resetAuthenticatorForm; + private readonly IHtmlElement _resetAuthenticatorButton; + + public ResetAuthenticator( + HttpClient client, + IHtmlDocument resetAuthenticator, + DefaultUIContext context) + : base(client, resetAuthenticator, context) + { + Assert.True(Context.UserAuthenticated); + _resetAuthenticatorForm = HtmlAssert.HasForm("#reset-authenticator-form", resetAuthenticator); + _resetAuthenticatorButton = HtmlAssert.HasElement("#reset-authenticator-button", resetAuthenticator); + } + + internal async Task ResetAuthenticatorAsync() + { + await Client.SendAsync(_resetAuthenticatorForm, _resetAuthenticatorButton); + return this; + } + } +} \ No newline at end of file diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/SetPassword.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/SetPassword.cs new file mode 100644 index 0000000000..4917bcf14b --- /dev/null +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/SetPassword.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage +{ + public class SetPassword : DefaultUIPage + { + private readonly IHtmlFormElement _setPasswordForm; + + public SetPassword(HttpClient client, IHtmlDocument setPassword, DefaultUIContext context) + : base(client, setPassword, context) + { + _setPasswordForm = HtmlAssert.HasForm("#set-password-form", setPassword); + } + + public async Task SetPasswordAsync(string newPassword) + { + await Client.SendAsync(_setPasswordForm, new Dictionary + { + ["Input_NewPassword"] = newPassword, + ["Input_ConfirmPassword"] = newPassword + }); + + return this; + } + } +} diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/TwoFactorAuthentication.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/TwoFactorAuthentication.cs index 1faec56c1b..333f564b9b 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Manage/TwoFactorAuthentication.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/TwoFactorAuthentication.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage public class TwoFactorAuthentication : DefaultUIPage { private readonly IHtmlAnchorElement _enableAuthenticatorLink; + private readonly IHtmlAnchorElement _resetAuthenticatorLink; public TwoFactorAuthentication(HttpClient client, IHtmlDocument twoFactor, DefaultUIContext context) : base(client, twoFactor, context) @@ -19,6 +20,10 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage { _enableAuthenticatorLink = HtmlAssert.HasLink("#enable-authenticator", twoFactor); } + else + { + _resetAuthenticatorLink = HtmlAssert.HasLink("#reset-authenticator", twoFactor); + } } internal async Task ClickEnableAuthenticatorLinkAsync() @@ -30,5 +35,13 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage return new EnableAuthenticator(Client, enableAuthenticator, Context); } + + internal async Task ClickResetAuthenticatorLinkAsync() + { + var goToResetAuthenticator = await Client.GetAsync(_resetAuthenticatorLink.Href); + var resetAuthenticator = await ResponseAssert.IsHtmlDocumentAsync(goToResetAuthenticator); + + return new ResetAuthenticator(Client, resetAuthenticator, Context); + } } } \ No newline at end of file diff --git a/test/Identity.FunctionalTests/Pages/Index.cs b/test/Identity.FunctionalTests/Pages/Index.cs index 71fdd9e7fa..6d5a0626db 100644 --- a/test/Identity.FunctionalTests/Pages/Index.cs +++ b/test/Identity.FunctionalTests/Pages/Index.cs @@ -70,5 +70,16 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests return new Account.Manage.Index(Client, manage, Context); } + + internal async Task ClickManageLinkWithExternalLoginAsync() + { + Assert.True(Context.UserAuthenticated); + + var goToManage = await Client.GetAsync(_manageLink.Href); + var manage = await ResponseAssert.IsHtmlDocumentAsync(goToManage); + Context.ContosoLoginEnabled = true; + + return new Account.Manage.Index(Client, manage, Context); + } } } diff --git a/test/Identity.FunctionalTests/UserStories.cs b/test/Identity.FunctionalTests/UserStories.cs index 768efcd011..74b6bdec55 100644 --- a/test/Identity.FunctionalTests/UserStories.cs +++ b/test/Identity.FunctionalTests/UserStories.cs @@ -87,6 +87,14 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests return await enableAuthenticator.SendValidCodeAsync(); } + internal static async Task ResetAuthenticator(Index index) + { + var manage = await index.ClickManageLinkAsync(); + var twoFactor = await manage.ClickTwoFactorEnabledLinkAsync(); + var resetAuthenticator = await twoFactor.ClickResetAuthenticatorLinkAsync(); + return await resetAuthenticator.ResetAuthenticatorAsync(); + } + internal static async Task LoginExistingUserRecoveryCodeAsync( HttpClient client, string userName, @@ -136,6 +144,39 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests return await resetPassword.SendNewPasswordAsync(email, newPassword); } + internal static async Task ChangePasswordAsync(Index index, string oldPassword, string newPassword) + { + var manage = await index.ClickManageLinkAsync(); + var changePassword = await manage.ClickChangePasswordLinkAsync(); + + return await changePassword.ChangePasswordAsync(oldPassword, newPassword); + } + + internal static async Task SetPasswordAsync(Index index, string newPassword) + { + var manage = await index.ClickManageLinkAsync(); + var setPassword = await manage.ClickChangePasswordLinkExternalLoginAsync(); + + return await setPassword.SetPasswordAsync(newPassword); + } + + internal static async Task LinkExternalLoginAsync(Index index, string loginEmail) + { + var manage = await index.ClickManageLinkWithExternalLoginAsync(); + var linkLogin = await manage.ClickLinkLoginAsync(); + + return await linkLogin.LinkExternalLoginAsync(loginEmail); + } + + internal static async Task RemoveExternalLoginAsync(ManageExternalLogin manageExternalLogin, string loginEmail) + { + // Provide an email to link an external account to + var removeLogin = await manageExternalLogin.ManageExternalLoginAsync(loginEmail); + + // Remove external login + return await removeLogin.RemoveLoginAsync("Contoso", "Contoso"); + } + internal static async Task DeleteUser(Index index, string password) { var manage = await index.ClickManageLinkAsync(); diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml b/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml index cb6572a8c6..91d36c62b0 100644 --- a/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml +++ b/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml @@ -9,7 +9,7 @@
    - +