MVC5 EF6 How to add confirmation screen with additional authentication before submitting data - authentication

Developing a new MVC5 project. I have my scaffolding in place for CRUD functionality but there is a requirement that when data is inserted or updated, an e-signature is required. Before data can be submitted to the database the user must be presented with a page asking them to enter their username and password again to confirm the data. If the username and password entered is valid and the username matches the currently signed in user, then the original data entered can be saved to its table (for example Member) and the e-signature information is saved to a separate table (ESignature). I'd appreciate any help on the best way to go about this - a view model combining Member and ESignature, or a reuse of the LoginViewModel from the Account controller to check the authentication, or an alternative approach? I need something that I can use across half a dozen controllers where e-signatures are required.

Alright maybe my approach is not the best but I will attempt.
My solution would be to create a CustomAttribute: AuthorizeAttribute and decorate all the actions which require Esignature. In your CustomAttribute implementation you will redirect to a controller action exactly similar to Login but with slight modification.
public class CustomAuthorize : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
var url = filterContext.HttpContext.Request.Url;
var query = url.Query;
if (query.Contains("g="))
{
var code = query.Split(new String[] { "g=" }, StringSplitOptions.None);
//You can create time sensistive token and validate it.
}
else
{
//Redirect User to a particular page
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "controller", "Account" },
{ "action", "elogin" },
{ "redirectUrl", url.AbsolutePath}
}
);
}
}
}
Then decorate for example Index() method with it.
[CustomAuthorize]
public ActionResult Index()
{
return View();
}
At first when you hit the Index() method then inside OnAuthorization method of CustomAuthorizeAttribute the else loop gets executed and re-directs you to a elogin method inside AccountController. This method is similar to the Login HttpGet method. While specifying the RedirectToResult I am specifying the redirectUrl path of the current page so when you successfully validate a user inside the elogin method then with the help of redirectUrl we can come back.
[AllowAnonymous]
public ActionResult ELogin(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View("Login");
}
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ELogin(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindAsync(model.UserName, model.Password);
if (user != null)
{
await SignInAsync(user, model.RememberMe);
var url = String.Format("{0}/?g={1}", returnUrl, "HashCode");
return RedirectToLocal(url);
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
The only difference in the HttpPost ELogin method is that before doing RedirectToLocal I append /g=HasCode. Note: Here you can append your own logic to create a time sensitive hash. When we get redirected to our home page then we can inspect inside our OnAuthorization Method if the url contains g=HashCode then don't redirect to Login Page.
This would be very basic idea on how you can approach to force users to re-sign in whenever they hit specific controllers. You will have to do additional security checks and be careful in what you are exposing via url.

Related

In ASP.NET Core razor Login page, how to redisplay the Login page in the POST handler?

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 Identity and click Add
Select Account\Login and click the drop-down arrow to select the ApplicationDbContext Data context class.
In Controllers/HomeController.cs, add an [Authorize] attribute to the Index method:
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.cs and add some code to the OnPostAsync() 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 the Index page. It does not, as I would have expected, redisplay the login page with the model error displayed.
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();
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.

User re verification page .net core

A page that asks the already signed in user to confirm their password one more time for security purposes on certain actions. Once confirmed it will go back to whatever request(action)they made in the first place. Should I use an user API for this? How can I achieve something like this?
Public IActionResult IndexMethod()
{
//process request only if user was verified using that verification page.
//It can take in parameters such as tokens if needed
}
In my opinion, if you want to confirm their password one more time for security purposes on certain actions. I suggest you could try to use action filter instead of directly going to the action and you could store the previous url into session.
More details, you could refer to below test demo:
1.Enable session:
Add below codes into Startup.cs's ConfigureServices method:
services.AddSession();
Add below codes into Configure method:
app.UseSession();
2.Create a filter:
public class ConfirmActionFilter : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
}
public override void OnActionExecuting(ActionExecutingContext context)
{
//We will store the user is comfirmed into session and check it at the filter
if (String.IsNullOrEmpty(context.HttpContext.Session.GetString("checked")))
{
//store the path into session route .
context.HttpContext.Session.SetString("route", context.HttpContext.Request.Path);
//redirect to the confrim controller action
context.Result = new RedirectToActionResult("Index", "Confirm", context.HttpContext.Request.RouteValues);
}
}
}
3.Add confirm controller:
public class ConfirmController : Controller
{
public IActionResult Index()
{
//You could get the path
HttpContext.Session.SetString("checked","true");
return View();
}
public IActionResult Checked() {
// redirect to the path user has accessed.
var re = HttpContext.Session.GetString("route");
return new RedirectResult(re);
}
}
filter usage:
[ConfirmActionFilter]
public class HomeController : Controller
Result:
If the user access firstly, you will find it will go to the confirm method.

IdentityServer4 client_id specific login pages

I'm trying to have different login pages based on the client_id.
Use case :
My default login page is a classic username/password type login, but for a specific client_id, the login page asks for 3 different infos that are found one a piece of paper that he received in the mail (sent by a third party).
Once i have these 3 infos, i can validate and find the associated user.
Technicals : So far, i have made it so that once IdentityServer4 redirects /connect/authorize to it's default login route (/account/login), i then redirect to my second login based on the client_id. It works but it is not elegant at all (feels hackish).
I'm sure there is a better way to achieve this, probably thru a middleware that would redirect from connect/authorize to my second login page, directly ?
Any ideas / tips ?
On the very initial Login call to IdentityServer, you call:
/// <summary>
/// Show login page
/// </summary>
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
// build a model so we know what to show on the login page
var vm = await accountService.BuildLoginViewModelAsync(returnUrl);
// some more code here
return View(vm);
}
In the called accountService.BuildLoginViewModelAsync, you have var context = await interaction.GetAuthorizationContextAsync(returnUrl); and in this context you have the clientId. You can extend the LoginViewModel class to include some custom property (of your own) and based on this property, in the AccountController, to return a different view. Then all you need is in the Views folder to create your specific view.
By this way, you can have as many views as you want.
Instead of creating hard coded separate views I simply use the appsettings.json file and specify different client configurations for each clientId. This way I can easily edit the file in the future any time there is a new client without having to re-deploy.
Then within the AccountController Login method I set the title and image of the current LoginViewModel (you have to add Title and Image to the LoginViewModel class) by matching the current clientid to a matching object within the appsettings.json file. Then set the ViewBag.Title and ViewBag.Image right before returning the view.
For information on how to wire-up the appsettings see my answer on this SO article
in the BuildLoginViewModelAsync(string returnUrl) method within the AccountController I do the following:
if (context?.ClientId != null)
{
try
{
_customClients.Value.ForEach(x => {
if (x.Name == context.ClientId)
{
title = x.Title;
image = x.Image;
}
});
}
catch (Exception){}
...
}
Here is my login method within the AccountController:
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
// build a model so we know what to show on the login page
var vm = await BuildLoginViewModelAsync(returnUrl);
if (vm.IsExternalLoginOnly)
{
// we only have one option for logging in and it's an external provider
return RedirectToAction("Challenge", "External", new { provider = vm.ExternalLoginScheme, returnUrl });
}
ViewBag.Title = vm.Title;
ViewBag.Image = vm.Image;
return View(vm);
}
Then in the _Layout.cshtml I use this;
#using IdentityServer4.Extensions
#{
string name = null;
string title = "SomeDefaultTitle";
string image = "~/somedefaulticon.png";
if (!true.Equals(ViewData["signed-out"]))
{
name = Context.User?.GetDisplayName();
}
try
{
title = ViewBag.Title;
image = ViewBag.Image;
}
catch (Exception)
{
}
}
later in the razor I use #title or #image wherever I need either.

Validate model after adding updating model

I am working on a web api on .net core. I am trying to validate/revalidate a model after adding additional data into the model (namely after adding the currently logged in user's username).
Let's say this is my controller's action
[Authorize]
[HttpPost]
public void Update([FromBody]UpdateUser User)
And here is the code I used for my authentication Scheme:
string token = Context.Request.Query["token"];
if (token == null) return AuthenticateResult.Fail("No JWT token provided");
try
{
var principal = LoginControl.Validate(token);
return AuthenticateResult.Success(new AuthenticationTicket(principal, SchemeName));
}
catch (Exception)
{
return AuthenticateResult.Fail("Failed to validate token");
}
Basically I am trying to achieve this flow:
Post request -> Authorize-> add the user ID into the model -> validate the model.
So after authorized, I need to first add User.UserName = CurrentUserName and only the model needs to be validated, after which I can use the ModelState object on the newly updated model.
As of now I am trying the following:
[HttpPost]
public async Task Update([FromBody]UpdateUser User)
{
User.UserName = "hello";
bool valid = await TryUpdateModelAsync(User);
valid = TryValidateModel(User);
}
Right now at both instances valid is false and the ModelState shows that UserName is required. The only validation I had added in UpdateUser is adding [Required] in the model.
I got it working by clearing the model before calling TryValidateModel.
[HttpPost]
public async Task Update(UpdateUser User)
{
User.UserName = "hello";
ModelState.Clear();
TryValidateModel(User);
// ModelState is now reset
}

Intercepting an encrypted login token in a request

I am working on an MVC site that has some pages that need authentication and others that don't. This is determined using the Authorize and AllowAnonymous attributes in a pretty standard way. If they try to access something restricted they get redirected to the login page.
I'm now wanting to add the functionality to automatically log them in using an encrypted token passed in the querystring (the link will be in emails sent out). So the workflow I want now is that if a request goes to a page that is restricted and there is a login token in the querystring I want it to use that token to log in. If it logs in successfully then I want it to run the original page requested with the new logged in context. If it fails to log in then it will redirect to a custom error page.
My question is where would I need to insert this logic into the site?
I have seen some suggestions on subclassing the Authorize attribute and overriding some of the methods but I'm not 100% sure how to go about this (eg what I would override and what I'd do in those overridden methods.
I've also had a look at putting the logic at a controller level but I am led to understand that the authorize attribute would redirect it away from the controller before any code in the controller itself was run.
It would be better to write a custom authorization attribute that will entirely replace the default functionality and check for the query string parameter and if present, decrypt it and authenticate the user. If you are using FormsAuthentication that would be to call the FormsAuthentication.SetAuthCookie method. Something along the lines of:
public class TokenAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
string token = filterContext.HttpContext.Request["token"];
IPrincipal user = this.GetUserFromToken(token);
if (user == null)
{
this.HandleUnAuthorizedRequest(filterContext);
}
else
{
FormsAuthentication.SetAuthCookie(user.Identity.Name, false);
filterContext.HttpContext.User = user;
}
}
private IPrincipal GetUserFromToken(string token)
{
// Here you could put your custom logic to decrypt the token and
// extract the associated user from it
throw new NotImplementedException();
}
private void HandleUnAuthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new ViewResult
{
ViewName = "~/Views/Shared/CustomError.cshtml",
};
}
}
and then you could decorate your action with this attribute:
[TokenAuthorize]
public ActionResult ProcessEmail(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}