Blazor Server and CleanCode - ASP.NET Core Identity - c#

I am having trouble wiring up identity into Blazor server with ASP.NET Core identity. Specifically getting the correct logged in state in Blazor pages (while I am getting them from the Blazor pages).
I think it's related to some of the startup being initialized in another project - but not sure how to debug it or what the solution is to be able to get the logged in state correctly.
Reproduction steps and link to GH repo below as a POC.
Background
I'm porting over the clean-code project by JasonTaylor from Angular / ASP.NET Core to a Blazor server project with ASP.NET Core Identity.
Issue
The application runs up and I can browse the pages when I register I can see logged-in state in the identity-based default pages but in the Blazor pages that use the AuthorizeView (e.g. LoginDisplay.razor) it's not aware of being authorized.
Startup in the Blazor project:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddApplication();
services.AddInfrastructure(Configuration);
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddSingleton<ICurrentUserService, CurrentUserService>();
services.AddHttpContextAccessor();
services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>();
services.AddRazorPages();
services.AddServerSideBlazor();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
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.UseHealthChecks("/health");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
AddInfrastructure in another project references in startup:
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("CleanArchitectureDb"));
}
else
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
}
services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());
services.AddScoped<IDomainEventService, DomainEventService>();
services
.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<ApplicationUser>>();
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddTransient<IDateTime, DateTimeService>();
services.AddTransient<IIdentityService, IdentityService>();
services.AddTransient<ICsvFileBuilder, CsvFileBuilder>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddAuthorization(options =>
{
options.AddPolicy("CanPurge", policy => policy.RequireRole("Administrator"));
});
return services;
}
}
public class RevalidatingIdentityAuthenticationStateProvider<TUser>
: RevalidatingServerAuthenticationStateProvider where TUser : class
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IdentityOptions _options;
public RevalidatingIdentityAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> optionsAccessor)
: base(loggerFactory)
{
_scopeFactory = scopeFactory;
_options = optionsAccessor.Value;
}
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user == null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
Steps to reproduce
Register : https://localhost:44399/Identity/Account/Register
Browse to : https://localhost:44399/Identity/Account/Login - Notice username in header is populated from the ASP.Net Identity pages
Browse to : https://localhost:44399/ - Notice the Header is Register, Login, About (Based on https://github.com/davidshorter/CleanCodeBlazor/blob/Rework/src/Web/Shared/LoginDisplay.razor)
Pushed up my changes to GH if anyone fancies a
look : https://github.com/davidshorter/CleanCodeBlazor/tree/Rework

This was an issue with mixing IdentityServer and ASP.net identity.
By removing Microsoft.AspNetCore.ApiAuthorization.IdentityServer and the use of base class from ApiAuthorizationDbContext<ApplicationUser> back to IdentityDbContext<ApplicationUser> resolved this.

Related

ASP.Net Core Authentication SignalR and Cors

In my current setup I am using a SPA (ReactJS) running on a different machine than my ASP.net Core Server. The whole client-server communication runs through SignalR except authentication. For security I want to use cookie-based authentication. As user-management I am using ASP.net.Core.Identity.
Setting up CORS works and now I'm implementing a login/logout mechanism based on simple HTTP-Request.
Here the workflow:
Send SignIn-Request to server via simple HTTP. (Works)
After successfully signin set cookie on client. (Works)
Client starts SignalR hub-connections (using the provided cookie). (Works)
Client sends data to server via HubConnetion. (Works)
Server sends data based on Claimspricipal. (Fails)
After the successfull authentication the client receives the cookie and uses it when negotiating with the signalR-hubs. When calling other controller-endpoints I have access to the ClaimsPrincipal which authenticated previously.
Now to my problem: when accessing the HubCallerContext the ClaimsPrincipal is not set (identity nor claims are set). Do I need to register the ClaimsPrincipal on this context as well or will it be handled by ASP.net? Other examples about authentication are assumuming that caller and hub are running on the same server. Did I missed or missunderstood something?
Here my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<SignInManager<ApplicationUser>, ApplicationSignInManager>();
services.AddScoped<IAuthorizationHandler, MyRequirementHandler>();
services.AddCors(
options => options.AddPolicy("CorsPolicy",
builder =>
{
builder
.SetIsOriginAllowed(origin =>
{
if (string.IsNullOrEmpty(origin)) return false;
if (origin.ToLower().StartsWith("http://localhost")) return true;
return false;
})
.AllowAnyHeader()
.AllowCredentials();
})
);
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
services.AddAuthorization(options =>
{
options.AddPolicy("PolicyName",
policy => { policy.Requirements.Add(new MyRequirement()); });
});
services.ConfigureApplicationCookie(config =>
{
config.Cookie.HttpOnly = false;
config.Cookie.Name = "my-fav-cookie";
config.ExpireTimeSpan = TimeSpan.FromDays(14);
config.Cookie.SameSite = SameSiteMode.None;
config.Cookie.SecurePolicy = CookieSecurePolicy.Always;
config.SlidingExpiration = false;
});
services.AddSignalR(opt => { opt.EnableDetailedErrors = true; })
.AddMessagePackProtocol(option => { option.SerializerOptions.WithResolver(StandardResolver.Instance); })
.AddNewtonsoftJsonProtocol(option =>
{
option.PayloadSerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
option.PayloadSerializerSettings.DateParseHandling = DateParseHandling.DateTime;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerManager logger,
ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("CorsPolicy");
app.UseRouting();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
...some Hubendpoints...
});
}
Here the MyRequirement.cs:
public class MyRequirement : IAuthorizationRequirement
{
}
And here the MyRequirementHandler.cs:
public class
MyRequirementHandler: AuthorizationHandler<MyRequirement,
HubInvocationContext>
{
private readonly IAuthorizationHelper _authorizationHelper;
private readonly UserManager<ApplicationUser> _userManager;
public MyRequirementHandler(
IAuthorizationHelper authorizationHelper,
UserManager<ApplicationUser> userManager)
{
_authorizationHelper = authorizationHelper;
_userManager = userManager;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
MyRequirement requirement,
HubInvocationContext resource)
{
var user = context.User;
...
}
}
And the Hub.cs:
public class Hub: Hub<ISystemClient>
{
private readonly ISystemService _systemService;
public Hub(ISystemService systemService)
{
_systemService = systemService;
}
[Authorize("PolicyName")]
public async Task DoSthFancy()
{
var user = Context.User;
var fancyStruff = await _systemService.GetFancyStuff(user);
await Clients.Caller.SendFancyStuff(fancyStuff);
}
}
Thanks for your help!
EDIT (04-11-2022)
I edited/fixed the code-snippet of the Startup.cs due to bad copy-paste. Thanks for the hint.
After implementing an example step by step I figured out that ASP sets the ClaimsPrincipal inside the HubContext only when you are challeging the hub-endpoint or the hub itself by adding a [Authorization] attribute. So what was missing was a implementation of an AuthenticationHandler, a registration in the configuration and setting the authorization-attribute.
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public CustomAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager) : base(options, logger, encoder, clock)
{
_userManager = userManager;
_signInManager = signInManager;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Context.Request.Cookies.TryGetValue("my-fav-cookie", out var cookie))
{
var user = Context.Request.HttpContext.User;
var ticket = new AuthenticationTicket(user, new(), CustomCookieScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
return Task.FromResult(AuthenticateResult.Fail("my-fav-cookie"));
}
}
In the Startup.cs you need to register the Authentication:
services.AddAuthentication().AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(
"MyCustomScheme", _ => { });
and in the Hub:
namespace My.HubProject {
[Authorize(AuthenticationScheme="MyCustomScheme")]
public class MyHub : Hub {
...
}
}
Hope it will help others!

Configure IdentityServerJwt Authentication with Authorization

I have an Blazor webassembly application which uses IdentityServerJwt AddAuthentication and is working with Hangfire. I am trying to configure Hangfire to allow only users who are admins authorization based on the article here but I am getting an No authentication handler is registered for the scheme 'Bearer' error. What should I add as an AuthenticationSchemes. JwtBearerDefaults.AuthenticationScheme` does not work.
What am I missing?
public partial class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
string handfirepolicyname="HangfirePolicyName";
public void ConfigureServices(IServiceCollection services)
{
...Code removed for brevity
services.AddAuthentication().AddIdentityServerJwt();
services.AddAuthorization(options =>
{
options.AddPolicy(handfirepolicyname, builder =>
{ builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser();
builder.RequireRole("admin");
});
});
var hangfireConnectionstring = "SomeHangfireDatabaseConnectionString";
var mySqlStorageOptions = new MySqlStorageOptions();
var mySqlStorage = new MySqlStorage(hangfireConnectionstring, mySqlStorageOptions);
services.AddHangfire(config => config.UseStorage(mySqlStorage));
services.AddHangfireServer();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ApplicationIdentityDbContext identityDbContext)
{
...Code removed for brevity
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
//UseAuthentication, UseAuthorization should be before UseHangfireDashboard
app.UseHangfireDashboard();
app.UseEndpoints(endpoints =>
{
endpoints.MapHangfireDashboard("/hangfire", new DashboardOptions()
{
Authorization = new List<IDashboardAuthorizationFilter> { }
}).RequireAuthorization(handfirepolicyname);
});
}
Error:
No authentication handler is registered for the scheme 'Bearer'. The registered schemes are: Identity.Application, Identity.External, Identity.TwoFactorRememberMe, Identity.TwoFactorUserId, idsrv, idsrv.external, IdentityServerJwt, IdentityServerJwtBearer. Did you forget to call AddAuthentication().Add[SomeAuthHandler]("Bearer",...)?
by adding
[Authorize(AuthenticationSchemes = "Bearer")]
public class TestController : ControllerBase{}
on top of the controller

issue with post(create a record) webapi in blazor server app

I create a blazor server app project and I am using built in webapi framework
I am checking a create record webapi in postman but it give a 204 content means my webapi run but it return 204 content see below image
//webapi test in postman but it return 204 content
blazor server app
EmpsController.cs
namespace CrudBlazorServerApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class EmpsController : ControllerBase
{
private readonly sqldbcontext _context;
public EmpsController(sqldbcontext context)
{
_context = context;
}
// GET: api/Emps
[HttpGet]
public async Task<ActionResult<IEnumerable<Emp>>> Getemps()
{
return await _context.emps.ToListAsync();
}
// GET: api/Emps/5
[HttpGet("{id}")]
public async Task<ActionResult<Emp>> GetEmp(int id)
{
var emp = await _context.emps.FindAsync(id);
if (emp == null)
{
return NotFound();
}
return emp;
}
[HttpPost]
public async Task<ActionResult<Emp>> PostEmp(Emp emp) //here I am facing issue record is not created
{
_context.emps.Add(emp);
await _context.SaveChangesAsync();
return CreatedAtAction("GetEmp", new { id = emp.empid }, emp);
}
Startup.cs
namespace CrudBlazorServerApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers()
.AddJsonOptions(options => options.JsonSerializerOptions.IgnoreNullValues = true);
services.AddDbContext<sqldbcontext>(options => options.UseSqlServer(Configuration.GetConnectionString("sqlserverconnn")));
services.AddRazorPages();
services.AddServerSideBlazor();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
var context = serviceScope.ServiceProvider.GetRequiredService<sqldbcontext>();
context.Database.EnsureCreated();
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
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.UseCors(policy =>
policy.WithOrigins("https://localhost:44399") // client address
.AllowAnyMethod()
.WithHeaders(HeaderNames.ContentType));
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
}
why it give 204 no content?
.net core project version : netcoreapp3.1
what I am trying:
I comment this line
await _context.SaveChangesAsync();
but record not created
please help
which place need to correction?

How to send cookies to browser on login using ASP.NET Core Identity?

I am building authentication on my asp.net core application using the identity. I followed 2 different tutorials but in both cases, my app doesn't send cookies to the browser. I reckon I am missing something important but I don't know what. Could you point me in the right direction? Here is the relevant code:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContextPool<IdentityDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext>();
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
public class HomeController : Controller
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly UserManager<IdentityUser> _userManager;
public HomeController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_signInManager = signInManager;
_userManager = userManager;
}
public async Task<IActionResult> SignUp()
{
await _userManager.CreateAsync(new IdentityUser("my_name"), "Secret123:");
return Ok("Signed up");
}
public async Task<IActionResult> SignIn()
{
var result = await _signInManager.PasswordSignInAsync("my_name", "Secret123:", true, false);
if(!result.Succeeded)
return Ok("Sign in failed");
return Ok("Signed in");
}
}
Edit: I created a fresh project and pasted these 2 classes, installed the same NuGet packages there, and the cookies worked. I have a problem somewhere else in the old project. I still have no clue where. I would appreciate if you comment where you think it might be.

Razor Session Data - HttpContextAccessor is null

I am a MVC5 newbie, creating a test web Site to look at MVC5 and Razor
I have a very simple site after user logs on and I need to change menus via _Layout from "Login" to add "Logout" "Account".
Note: The site is has its own authentication, I will look at single login later.
I am really struggling with managing session data in MVC5, not sure which is the best approach. I have tried TEMP DATA , but although I peak I have found that after user has been redirected between a couple of pages the data is lost. So looked at good old cookie, but since GDPR I can tell there is a lot less default support for cookies out of the box.
Anyway in the Startup I believe I am doing all the right things
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set session timeout value
options.IdleTimeout = TimeSpan.FromSeconds(30);
options.Cookie.HttpOnly = true;
});
}
services.AddMvc();
services.AddCaching();
services.AddSession();
services.AddHttpContextAccessor();
dependency inject it in , but in the Post when I attempt to call the SetString , the "private readonly IHttpContextAccessor _httpContextAccessor;" is null
Oddly the _Layout is not throwing same null exception
#using Microsoft.AspNetCore.Http
#inject IHttpContextAccessor HttpContextAccessor
#{
string UserId = HttpContextAccessor.HttpContext.Session.GetString("UserId");
}
After working on ASP.Net , the simple approach to handling Session , beginning to question whether I have missed something as it seems a lot of work in MVC5. So should I be using a different approach in MVC5
Just want to add this this for anyone else , probably not the cleanest code
With help form Sanjay now got a crude site up and running
Startup class , example code
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddHttpContextAccessor();
services.AddRazorPages();
services.AddRazorPages();
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(30);
});
}
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.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
}
Code for .csHtml
#using Microsoft.AspNetCore.Http;
#inject IHttpContextAccessor HttpContextAccessor
#{
if (Context.Session.GetString("UserRole") != null)
{
code for .cs
private readonly IHttpContextAccessor HttpContextAccessor;
public LoginModel(ISiteUserService siteUserService,
IHttpContextAccessor httpContextAccessor)
{
this.HttpContextAccessor = httpContextAccessor;
}
public IActionResult OnGet()
{
if(HttpContextAccessor.HttpContext.Session.GetString("UserRole")!= null)
{
Set
options.CheckConsentNeeded = context => false;
this to true i.e
options.CheckConsentNeeded = context => true;
and your session will not be null anymore.

Categories

Resources