I've modified the logic in the standard ASP.NET Core Login page POST routine (Areas/Identity/Pages/Account/Login.cshtml.cs). After the user has logged in successfully, I have additional logic that may deny the login attempt. If that additional logic denies the login attempt, I want to redisplay the login page with an appropriate message displayed on the page.
My problem is that, unlike an MVC controller, where calling return View() in a POST action redisplays the view, calling return Page() in a Razor page apparently redirects to "/" (the default page in the website).
I have two questions:
- In a Razor page POST routine, how do I redisplay the Razor page?
- What does
return Page()actually do in a POST routine?
Here is code to reproduce the behavior I see happening:
- In VS 2022, create a new ASP.NET Core Web App (Model-View-Controller) project.
- Framework: .NET 6.0
- Authentication Type: Individual Accounts
- In the Package Manager window, type: update-database
- Run the application. Create a new account and verify that you can log in.
- Right-click the project in Solution Explorer and select Add>New Scaffolded Item
- Select
Identityand clickAdd - Select
Account\Loginand click the drop-down arrow to select theApplicationDbContextData context class.
- Select
- In Controllers/HomeController.cs, add an
[Authorize]attribute to theIndexmethod:
using Microsoft.AspNetCore.Authorization; // Added this line
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using WebApplication3.Models;
namespace WebApplication3.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
[Authorize] // Added this line
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
Run the project. The
[Authorize]attribute forces you to log in before it redirects to the Index page.Now, open
Areas/Identity/Pages/Account/Login.cshtml.csand add some code to theOnPostAsync()routine that emulates the case where an error is detected after the user has successfully authenticated.
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (ModelState.IsValid) {
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded) {
// User is logged in, but we want to deny the login for some reason
ModelState.AddModelError("", "Some login error"); // <--------------
return Page(); // <--------------
// <snip>
}
// If we got this far, something failed, redisplay form
return Page();
}
- Run the app. The new
return Page()statement, which occurs after the user is authenticated, causes the app to happily display theIndexpage. It does not, as I would have expected, redisplay the login page with the model error displayed.
CodePudding user response:
Just add a SignOutAsync when you want to return to the login page after a successful call to SignInAsync
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded) {
// User is logged in, but we want to deny the login for some reason
await _signInManager.SignOutAsync();
ModelState.AddModelError("", "Some login error");
return Page();
}
TL,DR I suppose that we should look at the expected flow when the application starts.
The startup logic tries to reach the Index page, but this page has the [Authorize] attribute, so it cannot be displayed without a logged on user whatever roles we have in place.
Logically, the code flow needs to take a detour to the Login page. Here, when it receives the POST message, it looks for the credentials given and start the Identity method that leads to a logged in or not logged in user.
Exiting from the login page with a logged in user means that we have resolved the requirements for the [Authorize] attribute, else...
Now the code above logs in the user successfully and then starts some internal logic that should result in a return to the login page. But the code exits with the user still logged in, thus the Identity engine thinks that everything is ok and goes back the Index page that has caused the start of the identification process.
So we need to add the missing SignOutAsync before calling return Page();
CodePudding user response:
You said,
My problem is that, unlike an MVC controller, where calling return View() in a POST action redisplays the view, calling return Page() in a Razor page apparently redirects to "/" (the default page in the website).
I would suggest you add and check your additional logic before code executes if (ModelState.IsValid).
Example:
Login.cshtml.cs
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
string str = "invalid";
ViewData["Message"] = "";
if (str=="invalid")
{
ViewData["Message"] = "Something is invalid";
}
else
{
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
}
// If we got this far, something failed, redisplay form
return Page();
}
Then you could display the appropriate message(which we set in the 'if' condition in the example above) on the same login page like below.
Login.cshml
<p style="color:red; font-size:larger">
@ViewData["Message"]
</p>
Output:
If everything is fine then it will log in to the site.
Further, you could modify the logic as per your own requirements.

