Redirect Login to Controller Action - asp.net-core

Starting with the ASP.NET 5 Web App Template using Individual User Accounts I have managed to get external authentication working with Microsoft accounts. When users click Login they are redirected to ExternalLogin in AccountController like this
<form asp-controller="Account" asp-action="ExternalLogin" method="post" asp-route-returnurl="#ViewData["ReturnUrl"]" class="nav navbar-right">
<button type="submit" class="btn btn-null nav navbar-nav navbar-right" name="provider" value="Microsoft" title="Log in"><span class="fa fa-sign-in"/> Log In</button>
</form>
That gets them logged in using thier Microsoft account and all seems to work nicely. But how do I intercept direct attempts to access privileged actions [Authorize]
so that the user is redirected to ExternalLogin? Can a default action be set in Startup.cs?
EDIT 1 Attempting to follow the advice of #Yves I have created CustomAutorizationFilter in a Filters folder. It doesn't check for any conditions
public class CustomAutorizationFilter : IAuthorizationFilter
{
public void OnAuthorization(Microsoft.AspNet.Mvc.Filters.AuthorizationContext context)
{
//if (...) // Check you conditions here
//{
context.Result = new RedirectToActionResult("ExternalLogin", "Account", null);
//}
}
}
and have edited ConfigureServices as below
services.AddMvc(config =>
{
config.Filters.Add(typeof(Filters.CustomAutorizationFilter));
});
When I run the app locally it no longer goes to the Home page. It returns a blank http://localhost:52711/Account/ExternalLogin
Obviously there is much I do not understand.
Edit 2: Here is the signature of ExternalLogin
// POST: /Account/ExternalLogin
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public IActionResult ExternalLogin(string provider, string returnUrl = null)
This is how ExternalLogin comes out of the box in the ASP.Net 5 Web App Template.

You can register an IAuthorizationFilter or an IActionFilter implementations to accomplish this. In these filters you can check if the request is trying to access a privileged action, if the user is logged in or have enough permission to do it.
If you are using AutorizeAttribute, I suggest you to use AutorizationFilter.
If you go with your own custom attributes, then use ActionFilter.
Here is an example:
MVC calls IAuthorizationFilter.OnAuthorization method before every action execution.
public class CustomAuthorizationFilter : IAuthorizationFilter
{
public void OnAuthorization(Microsoft.AspNet.Mvc.Filters.AuthorizationContext context)
{
if (...) // Check you conditions here
{
context.Result = new RedirectToActionResult("ExternalLogin", "Account", null);
}
}
}
To register this filter, in Startup.cs edit your ConfigureServices method:
services.AddMvc(config =>
{
config.Filters.Add(typeof(CustomAuthorizationFilter ));
});
Or if you wan to use your own attributes you can use ActionFilter's OnActionExecuting method to check if everything is happening as you wish...

As I was unable to get CustomAuthorizationFilter working as suggested by #Yves I have resorted to a nasty hack. I have modified AccountController Login as below
// GET: /Account/Login
[HttpGet]
[AllowAnonymous]
public IActionResult Login(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return RedirectToAction(nameof(ExternalLogin), new { provider = "Microsoft", returnUrl = returnUrl });
//return View();
}
This seems to work but I'd appreciate any feedback or advice if there is a better way.

Related

ASP.NET Core 3.1 How can I redirect to the SignIn Action contained in the AccountController provided by MicrosoftIdentity

I have two login methods, OpenIDConnect and Cookie based Authentication.
I provide a button for the user to choose, and I have this code that then handles the request.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Identity.Web.UI;
[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
public async Task<IActionResult> LogIn(LogInModel model, string actionType)
{
if (actionType.Equals("AzureLogin")) {
return RedirectToAction("SignIn", "AccountController", new { Area = "MicrosoftIdentity" });
}
// default Cookie Auth follows.
}
When I redirect I am getting:
Status Code: 404; Not Found.
How can I invoke the provided SignIn Action. .NET Docs mention this, but I would prefer not
to use the asp-controller property.
<li class="nav-item">
<a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
</li>
The name of the controller should be Account and not AccountController.
e.g.
return RedirectToAction("SignIn", "Account", new { Area = "MicrosoftIdentity" });

Getting error "ERR_TOO_MANY_REDIRECTS" in asp.net core

I am trying to create custom login logout where whout loged in user can not access home view. and after loged in user can redirect to home view. If user trying to access home view so he has to be redirect to login page.
here is my code...
LoginPartial View
#using Microsoft.AspNetCore.Identity
#inject SignInManager<IdentityUser> SignInManager
#inject UserManager<IdentityUser> UserManager
#using Microsoft.AspNetCore.Http;
#{
var userId = Context.Session.GetString("username");
if (userId == null)
{
Context.Response.Redirect("/Login");
}
else
{
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link text-light mt-2">Hello #Context.Session.GetString("username")!</a>
</li>
<li class="nav-item">
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="#Url.Action("Index", "Home", new { area = "" })">
<button type="submit" class="nav-link btn btn-link text-white-50">Logout</button>
</form>
</li>
</ul>
}
}
here is my Login controller
public class LoginController : Controller
{
private readonly ApplicationDbContext _db;
public LoginController(ApplicationDbContext context)
{
_db = context;
}
public IActionResult Index()
{
return View("Login");
}
[HttpPost]
public IActionResult LoginProcess(string username, string password)
{
var userId = _db.logins.Where(p=>p.UserName==username && p.Password==password && p.ExpiryDate> DateTime.Now).Select(p=>p.Id).FirstOrDefault();
if (userId>0)
{
HttpContext.Session.SetString("username", username);
return Redirect("~/Reception/Home");
}
else
{
ViewBag.error = "Invalid Login..!";
return View("Login");
}
}
[HttpGet]
public IActionResult Logout()
{
HttpContext.Session.Remove("username");
return RedirectToAction("Index");
}
}
}
User can not open Home view without login.
Without running the code, it looks on first pass like you're setting the user on an infinite loop. Your view checks for a username and redirects to the "/login" endpoint on failure, which subsequently returns the View, which checks again, and so on. Eventually, the browser hits the brakes on you.
From what you're presenting, it looks like you're trying to roll your own login mechanism rather than take advantage of what ASP NET Core can offer to help deal with some of this automatically. Take a look at Simple authorization in ASP.NET Core
I would suggest ..create a base controller. another controllers should inherit from base controller.You could check whether user is logged-in or not in base controller and if you found user is not logged in then you can redirect user to login page.
The way you have checked user login at login view , it is not recommended.
public class BaseController : Controller
{
// Check here user credentials or whether user is logged-in or not
}
public class LoginController : BaseController
{
public IActionResult home()
{
return View("home");
}
}
SO whenever any user wants to access any page, your application will always check user authentication in this way.

How to redirect after Azure AD authentication to different controller action in ASP Net Core MVC

I have setup my ASP Net Core 2.0 project to authenticate with Azure AD (using the standard Azure AD Identity Authentication template in VS2017 which uses OIDC). Everything is working fine and the app returns to the base url (/) and runs the HomeController.Index action after authentication is successful.
However I now want to redirect to a different controller action (AccountController.CheckSignIn) after authentication so that I can check if the user already exists in my local database table and if not (ie it's a new user) create a local user record and then redirect to HomeController.Index action.
I could put this check in the HomeController.Index action itself but I want to avoid this check from running every time the user clicks on Home button.
Here are some code snippets which may help give clarity...
AAD settings in appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "<my-domain>.onmicrosoft.com",
"TenantId": "<my-tennant-id>",
"ClientId": "<my-client-id>",
"CallbackPath": "/signin-oidc" // I don't know where this goes but it doesn't exist anywhere in my app and authentication fails if i change it
}
I added a new action to my AccountController.CheckSignIn to handle this requirement but I cannot find a way to call it after authentication.
public class AccountController : Controller
{
// I want to call this action after authentication is successful
// GET: /Account/CheckSignIn
[HttpGet]
public IActionResult CheckSignIn()
{
var provider = OpenIdConnectDefaults.AuthenticationScheme;
var key = User.FindFirstValue(ClaimTypes.NameIdentifier);
var info = new ExternalLoginInfo(User, provider, key, User.Identity.Name);
if (info == null)
{
return BadRequest("Something went wrong");
}
var user = new ApplicationUser { UserName = User.Identity.Name };
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
return BadRequest("Something else went wrong");
}
}
return RedirectToAction(nameof(HomeController.Index), "Home");
}
// This action only gets called when user clicks on Sign In link but not when user first navigates to site
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/Account/CheckSignIn" }, OpenIdConnectDefaults.AuthenticationScheme);
}
}
I have found a way to make it work by using a redirect as follows...
Inside Startup
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=SignIn}/{id?}");
});
Inside AccountController
// GET: /Account/CheckSignIn
[HttpGet]
[Authorize]
public IActionResult CheckSignIn()
{
//add code here to check if AzureAD identity exists in user table in local database
//if not then insert new user record into local user table
return RedirectToAction(nameof(HomeController.Index), "Home");
}
//
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/Account/CheckSignIn" }, OpenIdConnectDefaults.AuthenticationScheme);
}
Inside AzureAdServiceCollectionExtensions (.net core 2.0)
private static Task RedirectToIdentityProvider(RedirectContext context)
{
if (context.Request.Path != new PathString("/"))
{
context.Properties.RedirectUri = new PathString("/Account/CheckSignIn");
}
return Task.FromResult(0);
}
The default behavior is: user will be redirected to the original page. For example, user is not authenticated and access Index page, after authenticated, he will be redirected to Index page; user is not authenticated and access Contact page, after authenticated, he will be redirected to Contact page.
As a workaround, you can modify the default website route to redirect user to specific controller/action:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=CheckSignIn}/{id?}"
);
});
After your custom logic, you could redirect user to your truly default page(Home/Index).
I want to check if the user exists in my local database, not only when Sign in is selected, but also when any other link to my website is clicked which requires authentication.
After a lot of trial and error I found a solution. Not sure if it is the best solution, but it works.
Basically I use the Authorize attribute with a policy [Authorize(Policy = "HasUserId")] as described in Claims-based authorization in ASP.NET Core.
Now when the policy is not met, you can reroute to the register action.
A – very simplified – version of the AccountController would look like this (I use a LogOn action instead of SignIn to prevent conflicts with the AzureADB2C AccountController):
public class AccountController : Controller
{
public IActionResult AccessDenied([FromQuery] string returnUrl)
{
if (User.Identity.IsAuthenticated)
return RedirectToAction(nameof(Register), new { returnUrl });
return new ActionResult<string>($"Access denied: {returnUrl}").Result;
}
public IActionResult LogOn()
{
// TODO: set redirectUrl to the view you want to show when a registerd user is logged on.
var redirectUrl = Url.Action("Test");
return Challenge(
new AuthenticationProperties { RedirectUri = redirectUrl },
AzureADB2CDefaults.AuthenticationScheme);
}
// User must be authorized to register, but does not have to meet the policy:
[Authorize]
public string Register([FromQuery] string returnUrl)
{
// TODO Register user in local database and after successful registration redirect to returnUrl.
return $"This is the Account:Register action method. returnUrl={returnUrl}";
}
// Example of how to use the Authorize attribute with a policy.
// This action will only be executed whe the user is logged on AND registered.
[Authorize(Policy = "HasUserId")]
public string Test()
{
return "This is the Account:Test action method...";
}
}
In Startup.cs, in the ConfigureServices method, set the AccessDeniedPath:
services.Configure<CookieAuthenticationOptions>(AzureADB2CDefaults.CookieScheme,
options => options.AccessDeniedPath = new PathString("/Account/AccessDenied/"));
A quick-and-dirty way to implement the HasUserId policy is to add the UserId from your local database as a claim in the OnSigningIn event of the CookieAuthenticationOptions and then use RequireClaim to check for the UserId claim. But because I need my data context (with a scoped lifetime) I used an AuthorizationRequirement with an AuthorizationHandler (see Authorization Requirements):
The AuthorizationRequirement is in this case just an empty marker class:
using Microsoft.AspNetCore.Authorization;
namespace YourAppName.Authorization
{
public class HasUserIdAuthorizationRequirement : IAuthorizationRequirement
{
}
}
Implementation of the AuthorizationHandler:
public class HasUserIdAuthorizationHandler : AuthorizationHandler<HasUserIdAuthorizationRequirement>
{
// Warning: To make sure the Azure objectidentifier is present,
// make sure to select in your Sign-up or sign-in policy (user flow)
// in the Return claims section: User's Object ID.
private const string ClaimTypeAzureObjectId = "http://schemas.microsoft.com/identity/claims/objectidentifier";
private readonly IUserService _userService;
public HasUserIdAuthorizationHandler(IUserService userService)
{
_userService = userService;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, HasUserIdAuthorizationRequirement requirement)
{
// Load User Id from database:
var azureObjectId = context.User?.FindFirst(ClaimTypeAzureObjectId)?.Value;
var userId = await _userService.GetUserIdForAzureUser(azureObjectId);
if (userId == 0)
return;
context.Succeed(requirement);
}
}
_userService.GetUserIdForAzureUser searches for an existing UserId in the database, connected to the azureObjectId and returns 0 when not found or when azureObjectId is null.
In Startup.cs, in the ConfigureServices method, add the Authorization policy and the AuthorizationHandler:
services.AddAuthorization(options => options.AddPolicy("HasUserId",
policy => policy.Requirements.Add(new HasUserIdAuthorizationRequirement())));
// AddScoped used for the HasUserIdAuthorizationHandler, because it uses the
// data context with a scoped lifetime.
services.AddScoped<IAuthorizationHandler, HasUserIdAuthorizationHandler>();
// My custom service to access user data from the database:
services.AddScoped<IUserService, UserService>();
And finally, in _LoginPartial.cshtml change the SignIn action from:
<a class="nav-link text-dark" asp-area="AzureADB2C" asp-controller="Account" asp-action="SignIn">Sign in</a>
To:
<a class="nav-link text-dark" asp-controller="Account" asp-action="LogOn">Sign in</a>
Now, when the user is not logged on and clicks Sign in, or any link to an action or controller decorated with [Authorize(Policy="HasUserId")], he will first be rerouted to the AD B2C logon page. Then, after logon, when the user is already registered, he will be rerouted to the selected link. When not registered, he will be rerouted to the Account/Register action.
Remark: If using policies does not fit well for your solution, take a look at https://stackoverflow.com/a/41348219.

Why is my Unauthorzied request not getting redirected to AccessDenied URL in ASP.Net MVC Core?

I have my AccountAdmin Controller where I manage Users in Identity.
So I have the Authorize Attribute like this at the top of the controller:
[Authorize(Roles = "Admin")]
public class AccountAdminController : Controller
The whole system is working great. If I log in as a user with the Admin role I can get to the page. And if I log in as a user without the Admin role I cannot get to the page. But my problem is that instead of being redirected to the "Account/AccessDenied" Page, I just get the "/AccountAdmin/Index" URL where I am denied the content and it just gives me the "Status Code: 403; Forbidden" message from:
app.UseStatusCodePages();
in my startup.
In StartUp.ConfigureServices I have:
services.AddIdentity<AppUser, IdentityRole>(options =>
{
options.Password.RequiredLength = 4;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireDigit = false;
options.User.AllowedUserNameCharacters = null;
}).AddEntityFrameworkStores<ApplicationDbContext>();
In StartupConfigure I have:
app.UseIdentity();
I know I haven't posted a lot of code here but it's all pretty straight forward stuff.
The caveaot is I am using Windows Authentication. I show the user login like DomainName\UserName in the UpperRight corner.
And then I made kind of an Impersonation Sign in Page where we can Sign in with TestRole1, TestRole2, etc.
The AccountController looks like this:
public class AccountController : Controller
{
private SignInManager<AppUser> _signInManager;
private UserManager<AppUser> _userManager;
public AccountController(SignInManager<AppUser> signInManager,
UserManager<AppUser> userManager)
{
_signInManager = signInManager;
_userManager = userManager;
}
public IActionResult Login()
{
return View(_userManager.Users.OrderBy(u => u.UserName));
}
[HttpPost]
public async Task<IActionResult> Login(string userName, bool persistant)
{
await _signInManager.SignInAsync(await _userManager.FindByNameAsync(userName), persistant);
return RedirectToAction("Index", "Home");
}
public async Task<IActionResult> LogOff()
{
await _signInManager.SignOutAsync();
return RedirectToAction("Login", "Account");
}
It's all working pretty well as far as Authentication and Authorization goes.
Accept I found that until I click my SignIn my real windows account won't match up with the roles I assigned to myself. It has to go through the SignInManager at:
[HttpPost]
public async Task<IActionResult> Login(string userName, bool persistant)
{
await _signInManager.SignInAsync(await _userManager.FindByNameAsync(userName), persistant);
return RedirectToAction("Index", "Home");
}
How can I get this AccessDenied redirect working?
Update 1:
I tried to make a filter like this:
public class MyAuthorizationFilter : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.Response.StatusCode == 403)
{
context.HttpContext.Response.Redirect("/Access/Denied");
}
}
}
But now when I try to replace the standard Authorize Attribute with this:
[MyAuthorizationFilter(Roles = "Admin")]
it doesn't know what "Roles" is.
It says: "The type or namespace Roles could not be found. Are you missing an assembly or namespace?"
Well it took me a while to work out how I want to do this. But here it is.
I didn't want this to go through error handling because it is not an unhandled error.
It is handled with app.UseStatusCodePages(); in StartUp.Configure.
So on a line below that in StartUp.Configure I add:
// 400 - 599
app.UseStatusCodePagesWithRedirects("~/Account/Status/{0}");
This is a place I can redirect to handle what I want to do with specific statuses but leave a default for what I don't specify.
The "Account/Status" action in the Account controller looks like this.
public IActionResult Status()
{
string statusCode = HttpContext.Request.Path;
statusCode = statusCode.Substring(statusCode.LastIndexOf('/') + 1);
ViewBag.StatusCode = statusCode;
return View(ViewBag);
}
and the view looks like this:
<div class="container-fluid">
<div class="panel panel-danger">
<div class="panel-header">
<h3>StatusCode:
#switch ((string)ViewBag.StatusCode)
{
case "403":
#:Access Denied
<div style="margin-top:5px"><a asp-controller="Account" asp-action="Login" class="btn btn-primary">Login as a different user</a></div>
break;
default:
#ViewBag.StatusCode
break;
}
</h3>
</div>
</div>
</div>
I just wanted this to stay out of the way of my error handling when I get to plugging that in. That is something different.
There must be a simple setting somewhere. But none of these solutions I see like AuthenticatonChallenge = true and so forth are working for me.
I think this is pretty tidy for now.

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

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.