Blazor role based authentication - c#

There are a lot of resources online for using roles to authenticate users in a blazor application. (example : https://visualstudiomagazine.com/articles/2019/10/25/authorizing-users-in-blazor.aspx)
What's frustrating me is that none of them cover how to add users to specific role groups. If I wanted to say, authenticate everyone under a specific domain .. say all google logins with the address #example.ca
Does anyone know how to do this? Or even where to explicitly type out admin emails to add them to a specific Role group?

What's frustrating me is that none of them cover how to add users to
specific role groups.
This question has nothing to do with Blazor.
Here is how you can add a registering user to a role:
[Route("api/[controller]")]
[ApiController]
public class AccountsController : ControllerBase
{
private static UserModel LoggedOutUser = new UserModel { IsAuthenticated = false };
private readonly UserManager<IdentityUser> _userManager;
public AccountsController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
[HttpPost]
public async Task<IActionResult> Post([FromBody]RegisterModel model)
{
var newUser = new IdentityUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(newUser, model.Password);
if (!result.Succeeded)
{
var errors = result.Errors.Select(x => x.Description);
return BadRequest(new RegisterResult { Successful = false, Errors = errors });
}
// Add all new users to the User role
await _userManager.AddToRoleAsync(newUser, "User");
// Add new users whose email starts with 'admin' to the Admin role
if (newUser.Email.StartsWith("admin"))
{
await _userManager.AddToRoleAsync(newUser, "Admin");
}
return Ok(new RegisterResult { Successful = true });
}
}
}
See source and more here
If I wanted to say, authenticate everyone under a specific domain ..
say all google logins with the address #example.ca
Again, this question has nothing to do with Blazor. This is a candidate for using a policy-based authentication with the requirement you've mentioned above. See the docs how to implement this, and don't hesitate to ask for help if needed.
Hope this helps...

Related

Route user to correct area

When a user logs in. I'm looking for a way to route a user to the correct area based on their role in identity.
I've tried:
You have the default controller that you can redirect to that area return RedirectToAction("Index", "Dashboard", new { area = "Admin" });,
but I have multiple roles.
Add a rewrite option in the StartUp Configure method. Which works in the beginning, but if you have a link to another area - it doesn't work.
On the default controller I created a view. Added some razor #if (User.Identity.IsAuthenticated && User.IsInRole("Admin")) else if(). Then used <text>and <script> tags in between the if statements to call a function that would redirect the user. It didn't provide an error, just didn't work. I figured it wouldn't, but... trying to think outside the box
Thanks ahead for the ideas!
If you are using asp.net core identity,in controller, you could directly use var isInRole = User.IsInRole("Admin") to check whether current user has an Admin role.
Or use UserManager to get current user and all his roles:
private readonly UserManager<IdentityUser> _userManager;
public HomeController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
[Authorize(Roles = "Admin")]
public async Task<IActionResult> TestView()
{
var user = await _userManager.GetUserAsync(HttpContext.User);
var roles = await _userManager.GetRolesAsync(user);
var matchingvalues = roles.SingleOrDefault(stringToCheck => stringToCheck.Equals("Admin"));
if(matchingvalues != null)
{
return RedirectToAction("Index", "Dashboard", new { area = "Admin" });
}
return View();
}

ASP.Net Core MVC Identity - Add temporary (session) claim

For starters, this question was also asked here (Temporary Session Based Claims in ASP.NET Core Identity) and here (How to add claim to user dynamically?), but neither question received an answer so I am trying to revive it...
I am creating a multi-tenant web app. A user logs in, and they can see data related to their own company (but not the data of other companies who use the app). Because of franchise groups with multiple storefronts, etc, it is not uncommon for a single user to require access to several different companies, but in this case, they must choose a single company when logging in.
Nearly all data queries require a company ID as a parameter, so I need a convenient way of checking which company the user is currently logged into. If they log out and log into a different company, I need to see a different company ID. I would like to store it as an identity claim that I can see for the duration of the session, but I don't necessarily want to store it to the database since it may change with every login.
Here is my Login action (based on the standard ASP.Net Core MVC template):
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
// BEGIN CUSTOM CODE - Check which companies a user can access
IEnumerable<int> allCompanies = _myDbService.GetUserCompanies(model.Email);
if (allCompanies.Count() == 1)
{
// then they can only access one company - log them into it automatically.
// I need easy access to its ID with the User since it is used with almost every db query.
int companyID = allCompanies[0];
((ClaimsIdentity)User.Identity).AddClaim(new Claim("CompanyID", companyID.ToString())); // this shows as null on future controller actions.
Debug.WriteLine("Set breakpoint here to examine User"); // I see 1 claim (my custom claim) in the debugger, but it is not in the database yet.
// future controller actions show me 3 claims (nameidentifier, name, and security stamp) but User.Claims.First(c => c.Type == "CompanyID").Value throws error.
return RedirectToLocal(returnUrl);
}
else
{
// the user has access to several companies - make them choose one to complete login process
RedirectToAction("ChooseCompany", new { companyList = allCompanies });
}
// END CUSTOM CODE
}
// REMAINING CODE OMITTED FOR BREVITY
}
// If we got this far, something failed, redisplay form
return View(model);
}
So my questions are: why doesn't the custom claim "stick" so that it can be seen in future controller actions? Why isn't it saved to the database if that is the standard behavior? Is there a way to keep this value around for the duration of the session without using AddSession()? If I implement this as a claim, am I making a round trip to the database every time I access the value?
For anyone else with this issue, here's where I am so far... To permanently store the claim in the database and automatically load it at login, you must use the following and the change will not take effect until the next login:
await userManager.AddClaimAsync(user, new Claim("your-claim", "your-value"));
Using the following will only store the claim for the current session, so I was on the right track with that:
((ClaimsIdentity)User.Identity).AddClaim(new Claim("your-claim", "your-value"));
The only way I have found to get the claim to "stick" between controller actions is to override UserClaimsPrincipalFactory so that ASP.Net is using your custom code as part of the login process. The problem here is that you only have access to the ApplicationUser in the relevant method, and you can't really pass in any custom parameters AFAIK. So I first modified by ApplicationUser class like this:
public class ApplicationUser : IdentityUser
{
public long ActiveDealershipID { get; set; }
}
(You must apply the EF migration). Now I create a class that derives from UserClaimsPrincipalFactory like this:
public class MyClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
public MyClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> options) : base(userManager, roleManager, options)
{
}
public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
long dealershipID = user.ActiveDealershipID;
((ClaimsIdentity)principal.Identity).AddClaim(new Claim("DealershipID", dealershipID.ToString()));
return principal;
}
}
(You must register the service in startup.cs ConfigureServices):
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, MyClaimsPrincipalFactory>();
And then in my Login action, I set the new user property from the model data BEFORE the sign-in so that it will be available to the UserClaimsPrincipalFactory when it is setting the claims:
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
ApplicationUser user = await _userManager.FindByEmailAsync(model.Email);
user.ActiveDealershipID = model.ChosenDealership
await _userManager.UpdateAsync(user);
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
...
I am still open to better suggestions or other ideas, but this seems to be working for me.

Refresh user cookie ticket in ASP.Net Core Identity

In a controller in an ASP.NET Core web application I want to refresh the user and claims in the cookie ticket stored on the client.
The client is authenticated and authorized, ASP.NET Core Identity stores this Information in the cookie ticket - now in some Controller actions I want to refresh the data in the cookie.
The SignInManager has a function to refresh RefreshSignInAsync, but it does not accept HttpContext.User as parameter.
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
// todo: update the Client Cookie
await _signInManager.RefreshSignInAsync(User); // wrong type
}
How do I refresh the cookie?
public static class HttpContextExtensions
{
public static async Task RefreshLoginAsync(this HttpContext context)
{
if (context.User == null)
return;
// The example uses base class, IdentityUser, yours may be called
// ApplicationUser if you have added any extra fields to the model
var userManager = context.RequestServices
.GetRequiredService<UserManager<IdentityUser>>();
var signInManager = context.RequestServices
.GetRequiredService<SignInManager<IdentityUser>>();
IdentityUser user = await userManager.GetUserAsync(context.User);
if(signInManager.IsSignedIn(context.User))
{
await signInManager.RefreshSignInAsync(user);
}
}
}
Then use it in your controller
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
await HttpContext.RefreshLoginAsync();
}
Or abstract it in an action filter
public class RefreshLoginAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
await context.HttpContext.RefreshLoginAsync();
await next();
}
}
Then use it like this in your controller
[HttpPost("[action]")]
[Authorize]
[RefreshLogin] // or simpler [Authorize, RefreshLogin]
public async Task<IActionResult> Validate()
{
// your normal controller code
}
This is also possible if the user is already logged out (their access token has expired but their refresh token is still valid).
Important note: the following only works if you have "Do you want to remember your user's devices?" set to "No" in the cognito config. If anyone knows how to get it to work with it on, please let me know.
We use the following flow (js client app connecting to .NET Core API):
User signs in using username/password (CognitoSignInManager<CognitoUser>.PasswordSignInAsync)
The client receives the token, userID, and refreshToken and stores them in localStorage.
When the original token expires (1 hour), the client gets a 401 error from the API.
The client calls another API endpoint with the userID and refreshToken which then in turn calls the code below on our user service.
If the refresh result is successful, we return the new token (AuthenticationResult.IdToken).
The client the repeats the call that originally errored in a 401 with the new token.
Here is the code we added to the User Service:
public async Task<UserLoginResult> SignInRefreshAsync(string uid, string refreshToken)
{
try
{
var result = await _cognitoIdentityProvider.InitiateAuthAsync(
new InitiateAuthRequest
{
AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH,
ClientId = _pool.ClientID,
AuthParameters = new Dictionary<string, string>
{
{ "REFRESH_TOKEN", refreshToken },
{ "SECRET_HASH", HmacSHA256(uid + _pool.ClientID, _options.UserPoolClientSecret) }
}
});
if (!result.HttpStatusCode.Successful() || string.IsNullOrEmpty(result.AuthenticationResult?.IdToken))
return new UserLoginResult(UserLoginStatus.Failed);
return new UserLoginResult(UserLoginStatus.Success, uid, null, null, result.AuthenticationResult.IdToken, null);
}
catch
{
return new UserLoginResult(UserLoginStatus.Failed);
}
}
private static string HmacSHA256(string data, string key)
{
using (var sha = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(key)))
{
var result = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(data));
return Convert.ToBase64String(result);
}
}
IAmazonCognitoIdentityProvider _cognitoIdentityProvider is resolved from DI.
AWSCognitoClientOptions _options = configuration.GetAWSCognitoClientOptions(); and IConfiguration configuration is also resolved from DI.
UserLoginResult is our class to hold the token and refresh token. Obviously, adjust accordingly.
Please note that setting SECRET_HASH may not be required based on your config is Cognito.

ASP.Net Can't Login/Register successfully ( works locally but not on Azure )

Edit2 : What i found is that it logins correctly and give the cookie of the user ( .AspNet.ApplicationCookie ) but when it is there ( when running on the deployed version of the WebApp, not locally) nothing will load, so the eternal load is simply a redirect to the home page, i don't know what it could be..
At first i tought it was the Database connection that was wrong or something... but i just realised by login in the admin panel locally that the accounts i was trying to register using the deployed Web App were registering in the DB successfully, even though after doing the registration you are stuck in an eternal loading screen..
I don't know what could be wrong since it is 100% functional locally , but somehow it seems like you can't sign in/login when it is deployed in Azure.
Edit : Somehow this morning i've tried logging in and it gave me a Runtime Error :
Server Error in '/' Application.
Runtime Error
Description: An exception occurred while processing your request. Additionally, another exception occurred while executing the custom
error page for the first exception. The request has been terminated
Now i've been asked the more info :
When you test locally, make certain that the application is not
creating any .mdf files in within your project's App_Data folder on
the filesystem. If it is, this is a sure indication as to why it works
locally but not when deployed to azure. Technically, you can have
separate connection strings and thus separate databases for your
application and your application services.
When i test locally, no file is made in the App_Data folder as it uses the same database while deploying as when running it locally -> can register new accounts locally and on the deploy
Now for this :
Please update your question with your web.config file. Be sure to
include the membership and roleManager sections under
. The exception message indicates that the connection
string name you are using for application services (meaning
membership, role management, etc) is referencing a database that lives
in your App_Data directory. Be sure to omit any usernames and
passwords from your when you post the web.config.
In my Web.config i do not have any roleManager section nor membership(i don't use membership but i do use roleManager?)
as per requested here is some important code with the registration :
This is what happens when someone register ( AccountController )
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
user.Subscribed = false;
user.StartSubscription = new DateTime?();
user.EndSubscription = null;
user.paypalProfileID = null;
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
// string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
// var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
// await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking here");
return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
and this is the ApplicationUser identity :
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
public string paypalProfileID { get; set; }
public bool? Subscribed { get; set; }
public DateTime? StartSubscription { get; set; }
public DateTime? EndSubscription { get; set; }
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
&#$&$##&##$U##$$##$####$##$*(!$(#()##($!#(#$(#$
It was the NAV the whole time :
#if (User.IsInRole("admin"))
{
<li>#Html.ActionLink("Admin", "AdminPage", "Admin")</li>
}
doesn't seem to work deployed without having
<system.webServer>
<modules>
<remove name="RoleManager" />
</modules>
</system.webServer>
in the Web.Config
...

How to get current user for all actions in a controller?

This is a two-parter
Question 1 (the real question)
I have a DashboardController that is not tied to a model. A User must be logged in before they can access the dashboard. How can I run a check to see if a user is authenticated before every action is executed, and redirect them to the login view if not? I think OnActionExecuted is what I want, but I am not sure what the exact implementation should be. Am I on the right track here?
public class DashboardController : Controller
{
private ApplicationContext db = new ApplicationContext();
//
// GET: /Admin/
public ActionResult Index()
{
var categories = db.Categories.ToList();
return View(categories);
}
public ActionResult Product(int id)
{
var product = db.Products.Find(id);
return View(product);
}
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
if(Session["current_user"] == null)
{
// This (obviously) doesn't work - what should go here?
return RedirectToAction("Create", "Session");
}
base.OnActionExecuted(filterContext);
}
}
Question 2
If the user IS logged in, what is the right way to make the user accessible in all of these views? I have been told ViewBag is generally a bad idea - what should I use?
I can authorize controllers and actions by follow this link:
It's in brazilian portuguese originally, but the link below is translated to english.
https://translate.google.com.br/translate?sl=pt&tl=en&js=y&prev=_t&hl=pt-BR&ie=UTF-8&u=http%3A%2F%2Fdevbrasil.net%2Fprofiles%2Fblogs%2Fautentica-o-e-permiss-es-de-usu-rios-em-asp-net-mvc-4&edit-text=&act=url
You can get the logged user in views by
#HttpContext.Current.User.Identity.Name
PS: Sorry my bad english
Use [Authorize] atrribute.
For example:
[AcceptVerbs(HttpVerbs.Get)]
[Authorize]
public ActionResult add()
{
}
Then in the web.config
<authentication mode="Forms">
<forms name="my_cookie_name" loginUrl="~/login" defaultUrl="~/" timeout="2880"/>
</authentication>
If the user is not authenticated, it will get redirected automatically to the login page.
If you want something simple to control the identity of your users check the highest rated answer here: ASP.NET MVC - Set custom IIdentity or IPrincipal. It's a brilliant example. I use somthing similar in all my projects.
In my login action:
var user = _userService.system_login(systemlogin_model_post.email, systemlogin_model_post.password); // my user model
//... doing all sorts of validations
// once everyone is happy I create a cookie
Response.Cookies.Add(UserCookie.GetCookie(user));
Than using the code from the link above I create cookie:
public static class UserCookie
{
public static HttpCookie GetCookie(User user)
{
CustomPrincipalSerializeModel serializeModel = new CustomPrincipalSerializeModel { user_id = user.UserId, username = user.Username, roles = user.Roles ,session_token = GUIDGenerator.ToAlphaNumerical() };
JavaScriptSerializer serializer = new JavaScriptSerializer();
string userData = serializer.Serialize(serializeModel);
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1,
user.UserId.ToString(),
DateTime.Now,
DateTime.Now.AddMinutes(30),
false,
userData);
string encTicket = FormsAuthentication.Encrypt(authTicket);
return new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
}
}
When [Authorize] is fired this code takes care of it:
Global.asax
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
JavaScriptSerializer serializer = new JavaScriptSerializer();
CustomPrincipalSerializeModel serializeModel = serializer.Deserialize<CustomPrincipalSerializeModel>(authTicket.UserData);
CustomPrincipal newUser = new CustomPrincipal(authTicket.Name);
newUser.user_id = serializeModel.user_id;
newUser.username = serializeModel.username;
newUser.roles = serializeModel.roles;
newUser.form_token = serializeModel.form_token;
HttpContext.Current.User = newUser;
}
}
1) Authorize attribute of ASP.NET MVC is totally focused on your first problem. You may even go for customization but not suggested for most of scenarios.
2) To assign the currently logged in user and visible in all the views, you may bind the user name using ViewBag/ViewModel property to Layout(_Layout.cshtml) so that it appears on top of every page where the layout is used.
Note: If you want to perform any pre action-invoke logic, then OnActionExecuting filter is the correct place, before entering that action method.
Precisely, you have to create a class and that class inherits the Controller class.
public class MyAuthentication : Controller
{
public MyAuthentication()
{
isAuthenticated();
}
private void isAuthenticated()
{
// do your authentication
//if user authenticated keep user details within a cookie, so that
// when the next request, program logic will search for cookie,
//if it is found then assume user was authenticated else redirect to login page.
}
}
And then inherits this MyAuthentication class in your project for all controllers
public class DashboardController : MyAuthentication
{
public ActionResult Index()
{
var categories = db.Categories.ToList();
return View(categories);
}
// rest of the codes
}
So that the authentication remains in single place. You can inherit this where ever you want.
If you need current user anywhere in the controller/action then better way is to set the user data when you perform authorization.
In your authorization filter , you can use
System.Web.HttpContext.Current.Items["userdata"]=userDataObject;
For authentication , this article can help.
http://www.dotnet-tricks.com/Tutorial/mvc/G54G220114-Custom-Authentication-and-Authorization-in-ASP.NET-MVC.html
First Put the Form Authentication Cookie when the user is logged in. like
[HttpPost]
public ActionResult Login(Acccount obj){
// check whether the user login is valid or not
if(UseIsValid){
FormsAuthentication.SetAuthCookie(obj.username, obj.RememberMe);
return redirectToAction("Index","DashBoard");
}
return View(obj);
}
*
and Use [Authorize] attribute. like*
[Authorize]
public class DashboardController : Controller
{
private ApplicationContext db = new ApplicationContext();
public ActionResult Index()
{
var categories = db.Categories.ToList();
return View(categories);
}
}

Categories