I hope you guys can assist me, I am unable to get the role based authorization to work on first page load in my application.
I am writing a server side blazor app with custom authorization handler where I add Role claims to the User Identity object and this works fine.
In my razor page, I set the [Authorize] attribute which shows the page if the user is authenticated which is expected.
But if I add a role to the attribute like [Authorize(Roles = "User")] it fails on page refresh, clicking another menu item in the app then seems to apply the roles correctly and then it works.
In the App.razor file, I put a break point and inspected the context.User.Identity to find out that the claims had not yet been added yet.
<NotAuthorized>
#if (context.User.Identity.IsAuthenticated)
{
<p>You do not have sufficient rights to view this page...</p>
}
else
{
<NotLoggedIn></NotLoggedIn>
}
</NotAuthorized>
My custom authorization handler is only adding the role claims after this.
I have changed the order of the request pipeline in the startup.cs file around a it looks like this
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
The code in ConfigureServices is as follows:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme, "Bearer");
defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
defaultAuthorizationPolicyBuilder.AddRequirements(new AppSettingsAuthRequirement(Configuration));
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
Thanks for you time!
Related
I have an ASP.NET Core MVC application that uses JWT for validation
I add the authentication in the startup class, using our token secret in our appsettings file to validate the token.
services.Configure<ApplicationSettings>(Configuration.GetSection("AppSettings"));
var key = System.Text.Encoding.UTF8
.GetBytes(Configuration.GetSection("AppSettings:Token").Value);
services.AddAuthentication(x => {
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x => {
x.RequireHttpsMetadata = false;
x.SaveToken = false;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
};
});
And add the authorization middleware
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("MyPolicy");
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Now when a user tries to login the following controller method is run, using the same token secret to generate the token.
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] UserForLoginDto userForLoginDto)
{
var user = await _userManager.FindByNameAsync(userForLoginDto.Username);
var result = await _signInManager
.CheckPasswordSignInAsync(user, userForLoginDto.Password, false);
if (result.Succeeded)
{
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("UserID",user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(1),
SigningCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8
.GetBytes(appSettings.Token)), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(securityToken);
return Ok(new { token });
}
return Unauthorized();
}
So when the user logs in, a token is generated and send back to the client.
At this point I would expect that I could just add [Authorize] attribute to a controller method, and then the MVC framework will look for a valid token in the http headers. So I create a test controller method
[HttpGet]
[Authorize]
public IActionResult Get()
{
return Ok("Test");
}
And send a request that corresponds to the test controller method with the Authorization header set to Bearer <Token> yet I still get a 401 unauthorized.
Can anyone explain why this might happen? Please tell me if you need additional information.
I think it's the matter of using your middleware:
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
Could try it in the following way:
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
So first, we use authenticate the user - where the middleware reads the token and inject the identity to http context
Here is the setup, I have an auth server that issues tokens to an angular website. I have a controller inside the AuthServer that needs to use the [Authorize] system to only allow JWT tokens that are valid. When I check the User variable in my controller it is always null but when I check the HttpRequestHeaders on the controller I see the token being sent.
I also have an Api server that I implemented using the JWT tokens and [Authorize] system very easily.
Another layer, I am running both Api and Auth servers in docker containers.
My entire Startup.cs file from AuthServer:
var connectionString = Configuration.GetConnectionString("Default");
if (_env.IsDevelopment())
{
try
{
using (AppIdentityDbContext identityDb =
new AppIdentityDbContextFactory(connectionString).Create())
{
int Pendings = identityDb.Database.GetPendingMigrations().Count();
identityDb.Database.Migrate();
}
using (PersistedGrantDbContext persistGrantDb =
new PersistedGrantDbContextFactory(connectionString).Create())
{
int Pendings = persistGrantDb.Database.GetPendingMigrations().Count();
persistGrantDb.Database.Migrate();
}
}
catch (Exception)
{
}
}
services.AddControllersWithViews();
services.AddDbContextPool<AppIdentityDbContext>(options => options.UseSqlServer(connectionString));
services
.AddIdentity<AppUser, IdentityRole>(config=> {
config.User.RequireUniqueEmail = true;
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer().AddDeveloperSigningCredential()
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("Default"));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = (int)TimeSpan.FromDays(1).TotalSeconds; // interval in seconds
})
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<AppUser>()
.AddProfileService<AppUserProfileService>()
.AddJwtBearerClientAuthentication();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme,
jwtOptions =>
{
// jwt bearer options
jwtOptions.Authority = _env.IsDevelopment() ? "https://localhost:5001" : "";
jwtOptions.RequireHttpsMetadata = _env.IsDevelopment() ? false : true;
jwtOptions.Audience = "resourceapi";
jwtOptions.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateAudience = false,
ValidateIssuer = _env.IsDevelopment() ? false : true,
ValidateActor = false,
ValidateIssuerSigningKey = false
};
},
referenceOptions =>
{
// oauth2 introspection options
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()));
services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));
services.AddSingleton<IEmailSender, SmtpSender>();
Configure Section:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
//app.UseHsts();
app.UseHttpsRedirection();
var forwardOptions = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
RequireHeaderSymmetry = false
};
forwardOptions.KnownNetworks.Clear();
forwardOptions.KnownProxies.Clear();
// ref: https://github.com/aspnet/Docs/issues/2384
app.UseForwardedHeaders(forwardOptions);
}
app.UseCors("AllowAll");
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Check for user inside of AccountController: Controller
var u = User;
var _user = await _userManager.GetUserAsync(u);
var e = this._httpContextAccessor;
I have a Web API .NET Core 3.0 Service. It gets a header that contains a JWT with claims.
I added the following to ConfigureServices in Startup.cs to map my JWT to the .NET Core Authentication system:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(configureOptions =>
{
configureOptions.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
context.Token = context.HttpContext.Request.Headers["X-JWT-Assertion"];
return Task.CompletedTask;
}
};
});
I also added app.UseAuthentication(); to Configure in Startup.cs.
I then fire up my service and call an HTTP GET operation on it. When I do, I can see that the context.Token is set to my JWT. If I take that JWT over to https://JWT.io it shows that it has many claims.
But a break point in the GET operation shows that User.Claims is empty. What ever is needed to connect the JWT to the User is not happening.
Here are variations that I have tried:
Add [Authorize] above my controller:
Result: 401 Error: Unauthorized
Add [Authorize(JwtBearerDefaults.AuthenticationScheme)] above my controller
Result: The AuthorizationPolicy named: 'Bearer' was not found.
Add services.AddAuthorization() in ConfigureServices and [Authorize] above my controller
Result: 401 Error: Unauthorized
Add [Authorize(JwtBearerDefaults.AuthenticationScheme)] above my controller and code below to ConfigureServices :
services.AddAuthorization(auth =>
{
auth.AddPolicy(JwtBearerDefaults.AuthenticationScheme,
new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
});
Result: 401 Error: Unauthorized
To be clear, I don't want to do any Authorization, but I read that adding it may be needed to map the claims to the user.
What do I need to do to get the User property (that is part of the Controller base class) to be populated with my claims?
I suspect there is something wrong in your configuration. In the ConfigureServices method it should be as follows:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
Then in the Configure method it should be as follows:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...................
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization(); // These two must be before `UseEndpoints` and after `UseRouting`
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
After working on my project for a while, I released the HttpContext.User.IsAuthenticated() returns False after login and I need to know where I should look for the mistake I made that cause this problem.
This is the Login, OnPost method.
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = _userManager.Users.FirstOrDefault(u => u.StudentNumber == Input.StudentNumber.ToString());
if (!(user is null) && await _userManager.CheckPasswordAsync(user, Input.Password))
await _signInManager.SignInAsync(user, Input.RememberMe);
var isUserAuthenticated = HttpContext.User.IsAuthenticated();
return Redirect(returnUrl);
}
// If we got this far, something failed, redisplay form
return Page();
}
The ConfigureServices method.
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(option=>option.Password.RequireNonAlphanumeric=false)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddMvc(options => options.EnableEndpointRouting = false)
.AddNewtonsoftJson();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
The Configure method.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseIdentityServer();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
SignInManager.SignInAsync() only creates the cookie for the given user. This method would not set HttpContext.User.
But in the next request which has the cookie you can access HttpContext.User after AuthenticationMiddleware and HttpContext.User.IsAuthenticated() should be true.
AuthenticationMiddleware always try to authenticate user with the default scheme and since you have AddIdentityServer after AddDefaultIdentity, identity server is becoming your default scheme, but when you call SignInManager.SignInAsync the Identity scheme is triggered.
To sum up, with this configuration your AuthenticationMiddleware always tries to authenticate request for IdentityServer and if you want other scheme for you apis you should use [Authorize(AuthenticationSchemes = "Identity.Application")].
P.S. Identity.Application is authenticatio scheme for ASP.NET Identity
I have an Asp.net Core 2.2 MVC application that authenticates using an IdentityServer4 server.
It is configured as you can see on the bottom, with really short times for quick testing.
The desired behavior is:
Login (suppose without the "remember me checked")
Do things...
Wait until the session expires
On the next navigation click redirect on the login page for a new interactive sign-in
I supposed I must work on cookies and session server side, but my first doubt is that I have to work more with id_token.
Anyway the current behavior is:
Login without the "remember me checked"
Wait until the session expires
Click on a dummy page and I see that the session is empty (as expected) -> The login is available on the top menu
So I click on login -> No login page showed -> a new session server side is available and in the browser there is a new value of ".AspNetCore.Cookies" but the same for ".AspNetCore.Identity.Application" and "idsrv.session".
If I logout, the cookie client side is correctly removed, so at the next login shows the expected credential form.
What I'm doing wrong?
Is it correct to try to get a new interactive sign-in checking the cookie expiration?
Do I have to follow another way working on the ids (id_token) objects?
CODE
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfiguration>(Configuration);
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromSeconds(30);
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = Configuration.GetValue<string>("IdentitySettings:Authority");
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.SaveTokens = true;
options.Events.OnTicketReceived = async (context) =>
{
context.Properties.ExpiresUtc = DateTime.UtcNow.AddSeconds(30);
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment() || env.IsStaging())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseAuthentication();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
EDIT
The logout is done as following
public async void OnPost()
{
await HttpContext.SignOutAsync("Cookies");
await HttpContext.SignOutAsync("oidc",
new AuthenticationProperties
{
RedirectUri = "http://localhost:5002"
});
}