From ae9aa9ebec4de42d2c0ab8c68f1b2bccd7b1c5f8 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Fri, 13 Apr 2018 16:46:00 -0700 Subject: [PATCH] Add AllowAnonymous for resetting password navigation flow (#1744) Addresses #1736 and aspnet/templating#448 --- .../ForgotPasswordConfirmation.cshtml.cs | 2 + .../Identity/Pages/Account/Lockout.cshtml.cs | 2 + .../Pages/Account/ResetPassword.cshtml.cs | 2 + .../ResetPasswordConfirmation.cshtml.cs | 2 + .../Microsoft.AspNetCore.Identity.UI.csproj | 4 +- ...ctionalTestsServiceCollectionExtensions.cs | 3 + test/Identity.FunctionalTests/LoginTests.cs | 55 +++++++++++++++++++ .../Pages/Account/Login.cs | 12 ++++ test/Identity.FunctionalTests/UserStories.cs | 9 +++ 9 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs index 77bdaa352c..c88d1d1fd5 100644 --- a/src/UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs @@ -1,10 +1,12 @@ // 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 Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal { + [AllowAnonymous] public class ForgotPasswordConfirmation : PageModel { public void OnGet() diff --git a/src/UI/Areas/Identity/Pages/Account/Lockout.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Lockout.cshtml.cs index 33a13a0ced..d48564c5ad 100644 --- a/src/UI/Areas/Identity/Pages/Account/Lockout.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Lockout.cshtml.cs @@ -1,10 +1,12 @@ // 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 Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal { + [AllowAnonymous] public class LockoutModel : PageModel { public void OnGet() diff --git a/src/UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs index 8b2f9c00dc..990eb81524 100644 --- a/src/UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs @@ -4,11 +4,13 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal { + [AllowAnonymous] [IdentityDefaultUI(typeof(ResetPasswordModel<>))] public abstract class ResetPasswordModel : PageModel { diff --git a/src/UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs index 37b30f963b..ef48687003 100644 --- a/src/UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs @@ -1,10 +1,12 @@ // 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 Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal { + [AllowAnonymous] public class ResetPasswordConfirmationModel : PageModel { public void OnGet() diff --git a/src/UI/Microsoft.AspNetCore.Identity.UI.csproj b/src/UI/Microsoft.AspNetCore.Identity.UI.csproj index bf7063da43..2795f6c76d 100644 --- a/src/UI/Microsoft.AspNetCore.Identity.UI.csproj +++ b/src/UI/Microsoft.AspNetCore.Identity.UI.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core Identity UI is the default Razor Pages built-in UI for the ASP.NET Core Identity framework. @@ -16,7 +16,7 @@ - + diff --git a/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs b/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs index 6c4be2e45c..93ecf771b6 100644 --- a/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs +++ b/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs @@ -48,5 +48,8 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests config.Filters.Add(new AuthorizeFilter(policy)); }) .Services; + + public static IServiceCollection SetupMaxFailedAccessAttempts(this IServiceCollection services) => + services.Configure(o => o.Lockout.MaxFailedAccessAttempts = 0); } } diff --git a/test/Identity.FunctionalTests/LoginTests.cs b/test/Identity.FunctionalTests/LoginTests.cs index 89c6ce3581..e8c31c8eda 100644 --- a/test/Identity.FunctionalTests/LoginTests.cs +++ b/test/Identity.FunctionalTests/LoginTests.cs @@ -228,5 +228,60 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests await UserStories.ResetPasswordAsync(resetPasswordClient, email, userName, newPassword); await UserStories.LoginExistingUserAsync(newClient, userName, newPassword); } + + [Fact] + public async Task CanResetPassword_WithGlobalAuthorizeFilter() + { + // Arrange + var emailSender = new ContosoEmailSender(); + void ConfigureTestServices(IServiceCollection services) => + services.SetupGlobalAuthorizeFilter().SetupTestEmailSender(emailSender); + + var server = ServerFactory.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices)); + + var client = server.CreateClient(); + var resetPasswordClient = server.CreateClient(); + var newClient = server.CreateClient(); + + var userName = $"{Guid.NewGuid()}@example.com"; + var password = $"!Test.Password1$"; + var newPassword = $"!New.Password1$"; + + await UserStories.RegisterNewUserAsync(client, userName, password); + var registrationEmail = Assert.Single(emailSender.SentEmails); + await UserStories.ConfirmEmailAsync(registrationEmail, client); + + // Act & Assert + await UserStories.ForgotPasswordAsync(resetPasswordClient, userName); + Assert.Equal(2, emailSender.SentEmails.Count); + var email = emailSender.SentEmails[1]; + await UserStories.ResetPasswordAsync(resetPasswordClient, email, userName, newPassword); + await UserStories.LoginExistingUserAsync(newClient, userName, newPassword); + } + + [Fact] + public async Task UserLockedOut_AfterMaxFailedAccessAttempts_WithGlobalAuthorizeFilter() + { + // Arrange + var emailSender = new ContosoEmailSender(); + void ConfigureTestServices(IServiceCollection services) => + services.SetupGlobalAuthorizeFilter().SetupMaxFailedAccessAttempts().SetupTestEmailSender(emailSender); + + var server = ServerFactory.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices)); + + var client = server.CreateClient(); + var newClient = server.CreateClient(); + + var userName = $"{Guid.NewGuid()}@example.com"; + var password = $"!Test.Password1$"; + var wrongPassword = $"!Wrong.Password1$"; + + await UserStories.RegisterNewUserAsync(client, userName, password); + var registrationEmail = Assert.Single(emailSender.SentEmails); + await UserStories.ConfirmEmailAsync(registrationEmail, client); + + // Act & Assert + await UserStories.LockoutExistingUserAsync(newClient, userName, wrongPassword); + } } } diff --git a/test/Identity.FunctionalTests/Pages/Account/Login.cs b/test/Identity.FunctionalTests/Pages/Account/Login.cs index b7e9bdc752..2206c278a5 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Login.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Login.cs @@ -64,6 +64,18 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account Context.WithAuthenticatedUser()); } + public async Task LockoutUserAsync(string userName, string password) + { + var loginAttempt = await SendLoginForm(userName, password); + + var lockedOut = ResponseAssert.IsRedirect(loginAttempt); + Assert.Equal("/Identity/Account/Lockout", lockedOut.ToString()); + + var lockedOutResponse = await Client.GetAsync(lockedOut); + var lockout = await ResponseAssert.IsHtmlDocumentAsync(lockedOutResponse); + return new DefaultUIPage(Client, lockout, Context); + } + private async Task SendLoginForm(string userName, string password) { return await Client.SendAsync(_loginForm, new Dictionary() diff --git a/test/Identity.FunctionalTests/UserStories.cs b/test/Identity.FunctionalTests/UserStories.cs index 4bded7e564..c0ff1666d2 100644 --- a/test/Identity.FunctionalTests/UserStories.cs +++ b/test/Identity.FunctionalTests/UserStories.cs @@ -36,6 +36,15 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests return await login.LoginValidUserAsync(userName, password); } + internal static async Task LockoutExistingUserAsync(HttpClient client, string userName, string password) + { + var index = await Index.CreateAsync(client); + + var login = await index.ClickLoginLinkAsync(); + + return await login.LockoutUserAsync(userName, password); + } + internal static async Task RegisterNewUserWithSocialLoginAsync(HttpClient client, string userName, string email) { var index = await Index.CreateAsync(client, new DefaultUIContext().WithSocialLoginEnabled());