I'm making a CRM project. I need to restrict access to some pages to clients and workers. I'm using CookieAuthentication and Authorize attribute and for some reason it's not working.
After registration of claims and cookies for user I'm trying to access this page "Master/Index" or "MasterController/Index" not sure which one is right to redirect but anyway instead of page I see this:
If Master as ControllerRoute
If MasterController as ControllerRoute
I'm 100% sure that user is not only Authorized but even has it's role because debagger shows it in any case:
Step After If Statement
Step After If Statement
And my MasterController is:
public class MasterController : Controller
{
[Authorize]
public IActionResult Index()
{
return View();
}
}
That's how I register user after his form sending on HttpPost page:
private async Task RegisterNewUser(LoginModel login, string r)
{
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, login.Login),
new Claim(ClaimTypes.Role, r)
};
ClaimsIdentity claimsIdentity = new(claims, "Cookies");
await ControllerContext.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
}
And just to show you that I added auth in my Program.cs:
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Verification/Auth";
options.LogoutPath = "/Verification/Logout";
options.AccessDeniedPath = "/";
});
...
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
Btw, if I comment [Authorize] than everything works fine but that's not what I need.
The ordering of your middleware is incorrect. You need to place UseAuthentication() before UseAuthorization().
With it the way you have it, every time it hits the authorization middleware, it realizes the user is not authenticated, so redirects.
It never gets past that, as it will only get to the authentication middleware once it successfully passes through the authorization middleware. Hence you have an infinite loop resulting in your browser deciding it has had too many redirects.
See here for details.
Related
We are using .NET Core 3.1 and Google Authentication. This is the code that we have currently:
Startup.cs:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddGoogle(googleOptions =>
{
googleOptions.ClientId = "CLIENT_ID"
googleOptions.ClientSecret = "CLIENT_SECRET"
})
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Error/403";
});
AccountController.cs:
public class AccountController : BaseController
{
[AllowAnonymous]
public IActionResult SignInGoogle()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(SignInReturn))
}, GoogleDefaults.AuthenticationScheme);
}
[AllowAnonymous]
public IActionResult SignInReturn()
{
// Do stuff with the user here. Their information is in the User
// property of the controller.
return Ok();
}
}
When users visit /Account/SignInGoogle, they are redirected to Google sign in page. Once they log in successfully, they are redirected back to /Account/SignInReturn. If I place a breakpoint there, I can see that claims are set inside User property.
However, we don't want the User property to be automatically set. We also don't want that the user is considered as logged-in once SignInReturn is called. We would just like to receive information about the user (name, surname, email) and then proceed with our custom claims handling logic. Is it possible?
Google auth uses the OAuth2 protocol. The Google Authentication package just wraps OAuth in an AuthenticationBuilder setup. By using any OAUth2 library you can authenticate outside of the AspNetCore AuthenticationBuilder and retrieve the JWT.
See also: What is the best OAuth2 C# library?
You can access the tokens by handling the OnCreatingTicket event:
googleOptions.Events.OnCreatingTicket = (context) =>
{
string accessToken = context.AccessToken;
string refreshToken = context.RefreshToken;
// do stuff with them
return Task.CompletedTask;
}
Note that you don't get the refresh token unless you specify googleOptions.AccessType = "offline"; and even then you only get them when you first consent (you can trigger reconsent if you require the refresh token).
Or you can follow the approach set out by Microsoft, which basically saves the tokens in a cookie. You can read about that in the documentation here.
I am trying to develop a project in .NET Core 3.1. I am trying to implement cookie based authentication in my project. My login function is:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(UserLoginModel userModel)
{
if (!ModelState.IsValid)
{
return View(userModel);
}
if (userModel.Email == "admin#test.com" && userModel.Password == "123")
{
var identity = new ClaimsIdentity(IdentityConstants.ApplicationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "User Id"));
identity.AddClaim(new Claim(ClaimTypes.Name, "User Name"));
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, principal);
return RedirectToAction(nameof(HomeController.Index), "Home");
}
else
{
ModelState.AddModelError("", "Invalid UserName or Password");
return View();
}
}
To implement cookie based authentication, I put the below code in my ConfigureService method of Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "_auth";
options.Cookie.HttpOnly = true;
options.LoginPath = new PathString("/account/login");
options.LogoutPath = new PathString("/account/logout");
options.AccessDeniedPath = new PathString("/account/login");
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.SlidingExpiration = false;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Latest);
}
And the configure method of Startup class is:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
But the problem is each time I try to login, the below exception occur in below code of login action method.
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme,
principal)
The exception occured is given below:
InvalidOperationException: No sign-in authentication handler is
registered for the scheme 'Identity.Application'. The registered
sign-in schemes are: Cookies. Did you forget to call
AddAuthentication().AddCookies("Identity.Application",...)?
Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext
context, string scheme, ClaimsPrincipal principal,
AuthenticationProperties properties)
_01_AuthenticationDemo.Controllers.AccountController.Login(UserLoginModel userModel) in AccountController.cs
+
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme,
principal);
Can anyone give me suggestion to solve the problem.
No sign-in authentication handler is registered for the scheme 'Identity.Application'. The registered sign-in schemes are: Cookies.
Please sepcify it with CookieAuthenticationDefaults.AuthenticationScheme, like below.
if (userModel.Email == "admin#test.com" && userModel.Password == "123")
{
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "User Id"));
identity.AddClaim(new Claim(ClaimTypes.Name, "User Name"));
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return RedirectToAction(nameof(HomeController.Index), "Home");
}
For more information, please check: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1#create-an-authentication-cookie
Test Result
I'll leave this comment here just in case it helps anyone.
I spent 2 days trying to get cookies authentication and then just a basic session authentication login page working on my .NET Core 3.1 site.
It turned out the problem was that my advertising platform (Ezoic) was causing the issue. They basically use some sort of proxy system whereby requests go to their server then their server makes the request to my server (a reverse proxy?). Anyway, if you're using a CDN or ad server or something similar then this could be a problem.
Another clue in my case was that my site worked fine on localhost but not on the production server.
To resolve the issue the ad company said I could add an X-Forwarded-For header to my site but I've not been able to get this to work, despite following the instructions about adding the appropriate lines to Startup.cs.
Summary
This is my first try with OAuth2 and External Login Mechanisms.
I'm creating a WebApp that will expose API features through a user-friendly UI.
In order to make API calls, I need to receive an access token from QBO that grants access to resources.
So, my WebApp has an external login option which I use to authenticate against QBO, and then authorize my app.
Everything works fine until...
Services Configuration
Based on a tutorial for GitHub authentication, I came up with this.
services.AddAuthentication(o => {
o.DefaultAuthenticateScheme = IdentityConstants.ExternalScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
o.DefaultChallengeScheme = IdentityConstants.ExternalScheme;
})
.AddOAuth("qbo", "qbo", o => {
o.CallbackPath = new PathString("/signin-qbo");
o.ClientId = Configuration["ecm.qbo.client-id"];
o.ClientSecret = Configuration["ecm.qbo.client-secret"];
o.SaveTokens = true;
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.Scope.Add("com.intuit.quickbooks.accounting");
o.AuthorizationEndpoint = Configuration["ecm.qbo.authorization-endpoint"];
o.TokenEndpoint = Configuration["ecm.qbo.token-endpoint"];
o.UserInformationEndpoint = Configuration["ecm.qbo.user-info-endpoint"];
o.Events.OnCreatingTicket = async context => {
var companyId = context.Request.Query["realmid"].FirstOrDefault() ?? throw new ArgumentNullException("realmId");
var accessToken = context.AccessToken;
var refreshToken = context.RefreshToken;
Configuration["ecm.qbo.access-token"] = accessToken;
Configuration["ecm.qbo.refresh-token"] = refreshToken;
Configuration["ecm.qbo.realm-id"] = companyId;
context.Backchannel.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
context.Backchannel.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await context.Backchannel.GetStringAsync(context.Options.UserInformationEndpoint);
var result = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
var user = (ClaimsIdentity)context.Principal.Identity;
user.AddClaims(new Claim[] {
new Claim("access_token", accessToken),
new Claim("refresh_token", refreshToken),
new Claim(ClaimTypes.GivenName, result["givenName"]),
new Claim(ClaimTypes.Surname, result["familyName"]),
new Claim(ClaimTypes.Email, result["email"]),
new Claim(ClaimTypes.Name, result["givenName"]+" "+result["familyName"])
});
};
});
This works. I can add my claims based on user information, the context.Principal.Identity indicates that it's authenticated.
For some reasons, it seems to try and redirect to `/Identity/Account/Login?returnUrl=%2F. Why is that?
Login page redirection
Here, I don't get why I get the redirection and this confuses me a lot. So, I added the AccountController just to try and shut it up.
namespace ecm.backoffice.Controllers {
[Authorize]
[Route("[controller]/[action]")]
public class AccountController : Controller {
[AllowAnonymous]
[HttpGet]
public IActionResult Login(string returnUrl = "/") {
return Challenge(new AuthenticationProperties { RedirectUri = returnUrl });
}
[Authorize]
[HttpGet]
public async Task<IActionResult> Logout(string returnUrl = "/") {
await Request.HttpContext.SignOutAsync("qbo");
return Redirect(returnUrl);
}
}
}
And this creates more confusion than it solves, actually. I'm lost here...
Apply Migrations
This Individual User Authentication WebApp seems to use Identity which looks like it creates a lot of behind the scene mechanisms. I first tried to register to my app, and had to "Apply Migrations", which I totally get, since the data model wasn't initialized.
So, I clicked the Apply Migrations button. I though I was okay with this...
Entity Framework Core
I am aware that the app is using Entity Framework Core for its persistence mechanism, hence the registration process, etc. And to configure it, I needed to add these lines to the services configs.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
It looks like it just won't work at all.
Thoughts
At this point, I think that the message says it Entity Framework just can't load the user information from its underlying data store. I totally understand that, and I don't want to register this user. I just want to take for granted that if QBO authenticated the user, it's fine by me and I grant open bar access to the WebApp features, even if the user ain't registered.
How to tell that to my WebApp?
Related Q/A I read prior to ask
Prevent redirect to /Account/Login in asp.net core 2.2
ASP.NET Core (2.1) Web API: Identity and external login provider
asp.net core 2.2 redirects to login after successful sign in
External Login Authentication in Asp.net core 2.1
And many more...
I have the following useful load in a token generated with JWT
{
"sub": "flamelsoft#gmail.com",
"jti": "0bca1034-f3ce-4f72-bd91-65c1a61924c4",
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator",
"exp": 1509480891,
"iss": "http://localhost:40528",
"aud": "http://localhost:40528"
}
with this code
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DBContextSCM>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection"), b =>
b.MigrationsAssembly("FlamelsoftSCM")));
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<DBContextSCM>()
.AddDefaultTokenProviders();
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddAuthentication()
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
};
});
services.AddMvc();
}
AccountController.cs
[HttpPost]
[Authorize(Roles="Administrator")]
public async Task<IActionResult> Register([FromBody]RegisterModel model)
{
try
{
var user = new User { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var role = await _roleManager.FindByIdAsync(model.Role);
result = await _userManager.AddToRoleAsync(user, role.Name);
if (result.Succeeded)
return View(model);
}
return BadRequest($"Error: Could not create user");
}
catch (Exception ex)
{
return BadRequest($"Error: {ex.Message}");
}
}
user.service.ts
export class UserService {
constructor(private http: Http, private config: AppConfig, private currentUser: User) { }
create(user: User) {
return this.http.post(this.config.apiUrl + 'Account/Register', user, this.jwt());
}
private jwt() {
const userJson = localStorage.getItem('currentUser');
this.currentUser = userJson !== null ? JSON.parse(userJson) : new User();
if (this.currentUser && this.currentUser.token) {
let headers = new Headers({ 'Authorization': 'Bearer ' + this.currentUser.token });
return new RequestOptions({ headers: headers });
}
}}
The problem is that the validation of the role does not work, the request arrives at the controller and returns a code 200 in the header, but never enters the class.
When I remove the [Authorize (Roles = "Administrator")] it enters correctly my code.
Is there something badly defined? Or what would be the alternative to define an authorization through roles.
TL;DR
As mentioned in the comments of the original question, changing:
[HttpPost]
[Authorize(Roles = "Administrator")]
public async Task<IActionResult> Register([FromBody]RegisterModel model)
{
// Code
}
to
[HttpPost]
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Administrator")]
public async Task<IActionResult> Register([FromBody]RegisterModel model)
{
// Code
}
resolved the issue.
Bearer is the default authentication scheme name when using JWT bearer authentication in ASP.NET Core.
But why do we need to specify the AuthenticationSchemes property on the [Authorize] attribute?
It's because configuring authentication schemes doesn't mean they will run on each HTTP request. If a specific action is accessible to anonymous users, why bother extract user information from a cookie or a token? MVC is smart about this and will only run authentication handlers when it's needed, that is, during requests that are somehow protected.
In our case, MVC discovers the [Authorize] attribute, hence knows it has to run authentication and authorization to determine if the request is authorized or not. The trick lies in the fact that it will only run the authentication schemes handlers which have been specified. Here, we had none, so no authentication was performed, which meant authorization failed since the request was considered anonymous.
Adding the authentication scheme to the attribute instructed MVC to run that handler, which extracted user information from the token in the HTTP request, which lead to the Administrator role being discovered, and the request was allowed.
As a side note, there's another way to achieve this, without resorting to using the AuthenticationSchemes property of the [Authorize] attribute.
Imagine that your application only has one authentication scheme configured, it would be a pain to have to specify that AuthenticationSchemes property on every [Authorize] attribute.
With ASP.NET Core, you can configure a default authentication scheme. Doing so implies that the associated handler will be run for each HTTP request, regardless of whether the resource is protected or not.
Setting this up is done in two parts:
public class Startup
{
public void ConfiguresServices(IServiceCollection services)
{
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme /* this sets the default authentication scheme */)
.AddJwtBearer(options =>
{
// Configure options here
});
}
public void Configure(IApplicationBuilder app)
{
// This inserts the middleware that will execute the
// default authentication scheme handler on every request
app.UseAuthentication();
app.UseMvc();
}
}
Doing this means that by the time MVC evaluates whether the request is authorized or not, authentication will have taken place already, so not specifying any value for the AuthenticationSchemes property of the [Authorize] attribute won't be a problem.
The authorization part of the process will still run and check against the authenticated user whether they're part of the Administrator group or not.
I know this question already has an answer, but something important is left out here. You need to make sure you're actually setting the claims for the logged in user. In my case, I'm using JWT Authentication, so this step is very important:
var claims = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, user.UserName) });
var roles = await _userManager.GetRolesAsync(user);
if (roles.Count > 0)
{
foreach (var role in roles) { claims.AddClaim(new Claim(ClaimTypes.Role, role)); }
}
var token = new JwtSecurityToken(
issuer: _configuration["JWT:Issuer"],
audience: _configuration["JWT:Audience"],
expires: DateTime.UtcNow.AddMinutes(15),
signingCredentials: signingCredentials,
claims: claims.Claims);
I was banging my head trying to figure out why HttpContext.User didn't include what I expected trying to narrow down the [Authroization(Roles="Admin")] issue. Turns out, if you're using JWT Auth you need to remember to set the Claims[] to the identity. Maybe this is done automatically in other dotnet ways, but jwt seems to require you to set that manually.
After I set the claims for the user, the [Authorize(Roles = "Whatever")] worked as expected.
I'm currently working on a project that I don't use Identity.
The things is that this project should have a remember me option that allow user to automatically reconnect into the web site.
My problem is that I can't find any complete tutoriel to create a cookie without Identity.
If somebody have a good sample of code or tutoial :)
Thanks
In my project, I use AngularJS for Frontend and .Net Core API for Backend.
So, I don't need to configure pages for AccessDeniedPath, LoginPath and so on.
Here's what I do:
Configure the cookie in the startup class:
public void Configure(IApplicationBuilder app) {
//...
CookieAuthenticationOptions options = new CookieAuthenticationOptions();
options.AuthenticationScheme = "MyCookie";
options.AutomaticAuthenticate = true;
options.CookieName = "MyCookie";
app.UseCookieAuthentication(options);
//...
}
The login is like this:
[HttpPost, Route("Login")]
public IActionResult LogIn([FromBody]LoginModel login) {
//...
var identity = new ClaimsIdentity("MyCookie");
//add the login as the name of the user
identity.AddClaim(new Claim(ClaimTypes.Name, login.Login));
//add a list of roles
foreach (Role r in someList.Roles) {
identity.AddClaim(new Claim(ClaimTypes.Role, r.Name));
}
var principal = new ClaimsPrincipal(identity);
HttpContext.Authentication.SignInAsync("MyCookie", principal).Wait();
return Ok();
}
The logout is like this:
[HttpPost, Route("Logout")]
public async Task<IActionResult> LogOut() {
await HttpContext.Authentication.SignOutAsync("MyCookie");
return Ok();
}
Then you can use it like this:
[HttpPost]
[Authorize(Roles = "Role1,Role2,Role3")]
public IActionResult Post() {
//...
string userName = this.User.Identity.Name;
//...
}
*See that the method is authorized only for "Role1, Role2 and Role3". And see how to get the user name.
I think you are asking how to make a persistent cookie when the user logs in with a "Remember Me?" checkbox selected.
All the answers are on the right path - you'll ultimately invoke HttpContext.Authentication.SignInAsync, but the cookie middleware issues a session cookie by default. You'll need to pass along authentication properties as a third parameter to make the cookie persistent, for example:
HttpContext.Authentication.SignInAsync(
Options.Cookies.ApplicationCookieAuthenticationScheme,
userPrincipal,
new AuthenticationProperties { IsPersistent = isPersistent });
There is a pretty good article on this here: Using Cookie Middleware without ASP.NET Core Identity.
Basically what you do is set up the cookie handling middleware as if you were going to identify the user, but then you just create a ClaimsPrincipal object without asking the user to login. You pass that object to the SigninAsync method and it creates the cookie for you. If you follow this article you should be fine.