In my blazor webassembly project i am trying to load poilicies from webapi, so that admin can set policies(permissions) for each role.
The problem is that the Authorization policy validation on the first page is done even before getting the response from httpclient request as it is async call, and i get an error the policy does not exists.
Is there any way that i can execute httpclient request synchronously OR check if the policy exists and wait till policy initialization is complete.
JSON Object:
[
{"policy": "Administration","roles": ["admin"]},
{"policy": "FinanceTransaction","roles": ["admin,accountant"]},
.....
]
Policy Initialization:
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
var baseAddress = builder.Configuration["apiUrl"];
builder.Services.AddHttpClient("Anonymous", client => client.BaseAddress = new Uri(baseAddress));
builder.Services.AddHttpClient("Protected", client => client.BaseAddress = new Uri(baseAddress));
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("Protected"));
builder.Services.AddAuthorizationCore(async options =>
{
var httpClientFactory = builder.Build().Services.GetRequiredService<IHttpClientFactory>();
var http = httpClientFactory.CreateClient("Anonymous");
var tasks = await http.GetFromJsonAsync<IEnumerable<PolicyRoles>>($"AppPolicy");
foreach (var task in tasks)
{
AppLoadStatus.PolicyLoaded = true;
OnPolicyLoaded.Invoke();
if (task.Roles.Any())
options.AddPolicy(task.Policy, policy => { policy.RequireRole(task.Roles); });
}
});
await builder.Build().RunAsync();
}
}
Razor Page:
#page "/Items/"
#attribute [Authorize(Policy = "FinanceTransaction")]
<div class="col col-lg-9">
#if (items == null)
{
<LoadingSpinner LoadFailed="#loadFailed" />
}
else
{
.....
.....
}
<div>
If i try to load the above page directly i get the error:
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: The AuthorizationPolicy named: 'FinanceTransaction' was not found
But if i wait for policy initialization and navigate to other pages everything would work fine.
Can any one help me to resolve the issue. Or is there any other better approach for this
app.razor:
#if (!AppLoadStatus.PolicyLoaded)
{
<LoadingSpinner />
}
else
{
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)">
<NotAuthorized>
#if (!context.User.Identity.IsAuthenticated)
{
<RedirectToLogin />
}
else
{
<AccessDenied />
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="#typeof(MainLayout)">
<RedirectToHome />
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
}
#code{
protected override void OnInitialized()
{
ServiceExtensions.OnPolicyLoaded += () => { StateHasChanged(); };
}
}
Related
I have implemented a custom validation attribute to check column uniqueness. I want to check if the provided value already exists in the database or not.
Here is my code:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class UniqueAttribute : ValidationAttribute
{
public UniqueAttribute()
{
}
public override bool RequiresValidationContext => true;
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
return ValidationResult.Success;
}
}
the validationContext in IsValid method always returns null. How it can be fixed?
Startup.cs : ConfigureServices method
public void ConfigureServices(IServiceCollection services)
{
services.AddApiVersioning(o =>
{
o.ReportApiVersions = true;
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddScoped<IClaimsTransformation, ClaimsTransformation>();
RegisterRepository(services);
RegisterServices(services);
RegisterAutoMapper(services);
services.AddControllersWithViews()
.AddJsonOptions(opts => opts.JsonSerializerOptions.PropertyNamingPolicy = null);
services.AddRazorPages();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
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.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
Model class :
public class Category
{
[Unique]
public string Name { get; set; }
public string Description { get; set; }
}
Below is an example to check if the provided value already exists in the database or not, you can refer to it.
UniqueAttribute:
public class UniqueAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value,ValidationContext validationContext)
{
var context = (MvcMovieContext)validationContext.GetService(typeof(MvcMovieContext));//change the MvcMovieContext to your DbContext
if (!context.Movie.Any(a => a.Company == value.ToString()))
{
return ValidationResult.Success;
}
return new ValidationResult("Company exists");
}
}
Movie:
public class Movie
{
[Unique]
public string Company { get; set; }
}
Create view:
#model MvcMovie.Models.Movie
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Company" class="control-label"></label>
<input asp-for="Company" class="form-control" />
<span asp-validation-for="Company" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
result:
I've got a problem which I quite don't understand yet (used doc: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-6.0) , if I try to login, it works on the Static Web App (SWA) and locally.
but when I try to logout,
locally it works and on the SWA it doesn't.
Anyone has an idea what the problem is?
Screenshots for reference:
Portal app registration:
appsettings.json
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/TENANT-ID",
"ClientId": "CLIENT-ID",
"ValidateAuthority": true
}
}
staticwebapp.config.json
{
"networking": {
"allowedIpRanges": [
"IP/MASK"
]
}
}
Program.cs
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddSingleton<IProductService<ProductA,InsuredPerson>, ProductAService>();
builder.Services.AddSingleton<IProductService<ProductB,InsuredPerson>, ProductBService>();
builder.Services.AddSingleton<IDownloadService,DownloadService>();
builder.Services.AddSingleton<IUploadService, UploadService>();
builder.Services.AddAutoMapper(new[] { typeof(ServiceMappingProfile)});
builder.Services.AddHttpClient();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddLocalization();
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes
.Add("https://graph.microsoft.com/User.Read");
options.ProviderOptions.LoginMode = "redirect";
});
var host = builder.Build();
CultureInfo culture;
var js = host.Services.GetRequiredService<IJSRuntime>();
var result = await js.InvokeAsync<string>("blazorCulture.get");
if (result != null)
{
culture = new CultureInfo(result);
}
else
{
culture = new CultureInfo("en-US");
await js.InvokeVoidAsync("blazorCulture.set", "en-US");
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
await host.RunAsync();
}
}
App.razor
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)">
<NotAuthorized>
#if (context.User.Identity?.IsAuthenticated != true)
{
<RedirectToLogin />
}
else
{
<p role="alert">You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="#routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="#typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
Please check these points while configuring:
• Install the NuGet package Microsoft.Authentication.WebAssembly.Msal to authenticate your application using NuGet Manager.
• While Enabling the authentication by injecting the [Authorize] attribute to the Razor pages (Counter.razor and FetchData.razor).Also add reference Microsoft.AspNetCore.Authorization
#attribute [Authorize]
#using Microsoft.AspNetCore.Authorization
In LoginDisplay.razor , check the below code for login and logout
#using Microsoft.AspNetCore.Components.Authorization
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
#inject NavigationManager Navigation
#inject SignOutSessionStateManager SignOutManager
<AuthorizeView>
<Authorized>
Hello, #context.User.Identity.Name!
<button class="nav-link btn btn-link" #onclick="BeginLogout">
Log out
</button>
</Authorized>
<NotAuthorized>
Log in
</NotAuthorized>
</AuthorizeView>
#code {
private async Task BeginLogout(MouseEventArgs args)
{
await SignOutManager.SetSignOutState();
Navigation.NavigateTo("authentication/logout");
}
}
Pages/Authentication.razor
#page "/authentication/{action}"
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="#Action" />
#code {
[Parameter]
public string Action { get; set; }
}
Reference : Using Azure Active Directory to Secure a Blazor WebAssembly Standalone App (code-maze.com)
Workarounds that you may try:
Try 1:
Clear all the cache , try login and logout.
Try 2:
If all these are set properly and still facing the issue , Try with setting Redirect type to Web instead of SPA.
For example:
Try 3:
Include "post_logout_redirect_uri": "https://localhost:5001/authentication/logout-callback", In the json file of app where the redirect uri is mentioned
And give that url in logout url in portal in the app's registration screen:
select Authentication in the menu.
In the Logout URL section, set it to https:// localhost:5001/authentication/logout-callback
I want to log off the user immediately after the accessdenied view displayed so the session token is also gets deleted.
Can someone help me to find a way to do this?
public async Task<IActionResult> AccessDenied()
{
bool haspermission = await _signInService.SignInCurrentUserAsync();
if (haspermission)
{
return Redirect(redirectUrl);
}
return View();
}
public IActionResult LogOut()
{
return SignOut("Cookies", "oidc");
}
cshtml
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#{
Layout = null;
}
<br />
<br />
<br />
<center>
<h3>Access Denied</h3>
<h4>You do not have sufficient rights to access.</h4>
</center>
Request the LogOut action after page loaded.
<script>
window.onload = function () {
$.get('/Controller/Action')
}
</script>
My Goal Create a simple login form in order to authenticate and authorize the user in my Blazor Server-side app with Cookie Authentication.
My Issue Everything works... The EditForm passes the values to my Controller. The Controller validates the usercredentials. Then runs HttpContext.SignInAsync(claims) and returns Ok().
But the Cookie is not passed and the user is not Authenticate either.
What I have done
1. The EditForm, passes the userinputs to the DisplayLoginModel() on a ValidSubmit.
<div class="row">
<div class="col-8">
<EditForm Model="#userLogin" OnValidSubmit="OnValidLogin">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-4 row">
<p class="col-sm-4 font-weight-bold">Email</p>
<div class="col-sm-8">
<InputText #bind-Value="userLogin.Email" class="form-control" />
</div>
</div>
<div class="mb-4 row">
<p class="col-sm-4 font-weight-bold">Password</p>
<div class="col-sm-8">
<InputText #bind-Value="userLogin.Password" class="form-control" />
</div>
</div>
<button class="btn btn-outline-primary col-sm-4" type="submit"><strong>Login EditForm</strong></button>
</EditForm>
</div>
</div>
2. The OnValidLogin Sends a request to the Form Controller
public DisplayLoginModel userLogin = new DisplayLoginModel();
private async Task OnValidLogin()
{
var requestMessage = new HttpRequestMessage()
{
Method = new HttpMethod("POST"),
RequestUri = new Uri("https://localhost:44370/Form"),
Content = JsonContent.Create(userLogin)
};
var client = httpfac.CreateClient();
var response = await client.SendAsync(requestMessage);
}
3. The Controller gets the user credentials from the displayloginModel and validets Ok().
[HttpPost]
public async Task<ActionResult> Post(DisplayLoginModel _userLogin)
{
if (_userLogin.Email == "this#Email.com")
{
ClaimsIdentity claimsIdentity = new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.Role, "ActiveUser")
}, "auth");
ClaimsPrincipal claims = new ClaimsPrincipal(claimsIdentity);
await HttpContext.SignInAsync(claims);
return Ok();
}
else
{
return BadRequest();
}
}
4. Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.AddHttpContextAccessor();
services.AddAuthentication("Cookies").AddCookie(options =>
{
options.SlidingExpiration = true;
});
services.AddRazorPages();
services.AddServerSideBlazor();
}
And
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
Why is the Controller not signing the user in, what am I missing?
An image of the solution structure is seen right below:
Because Blazor Server uses websockets (Blazor circuits) to render the UI while on the controller, await HttpContext.SignInAsync(claims); returns a cookie header so the browser can be authorized on next page load. This means you are actually being authenticated server-side but not from the client-side as you have not reloaded the page context to update the cookies. There is a reason why the default Identity login uses MVC architecture for the authentication process. :)
My suggestion, switch to API-based authentication/authorization or use a traditional login in flow.
I have set up the necessities to create an example to build off of. Here's my code.
StartUp File
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddSingleton<DoggoDataServices>();
services.AddSingleton<AddDoggoServices>();
services.AddSingleton<EventServices>();
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
App.razor
<Router AppAssembly="#typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)" />
</Found>
<NotFound>
<CascadingAuthenticationState>
<LayoutView Layout="#typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</CascadingAuthenticationState>
</NotFound>
</Router>
customAuth
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "john.smith#gmail.com"),
}, "apiauth_type");
var user = new ClaimsPrincipal(identity);
return Task.FromResult(new AuthenticationState(user));
}
}
}
index
<AuthorizeView>
<Authorized>
<p>Welcome, #context.User.Identity.Name</p>
</Authorized>
<NotAuthorized>
<p>Not Logged In</p>
</NotAuthorized>
</AuthorizeView>
Given the code, the index page only shows "not Logged in". Am I missing something so simple that I am overlooking it? I am new to blazor.
You forgot to add CascadingAuthenticationState in app.razor
<CascadingAuthenticationState>
<UserSession>
<Router AppAssembly="#typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="#typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</UserSession>
</CascadingAuthenticationState>
You can use the default template. It has an example that works.
You should call
NotifyAuthenticationStateChanged
When you use a Custom Provider, you should notify it when a user is Authenticated.
Example:
You may add this method to your Custom provider:
public void MarkUserAsAuthenticated(Users u)
{
// add your claims here. This is just an example.
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, u.UserName)
});
var user = new ClaimsPrincipal(identity);
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
}
And please note, that you should create and call similar method when a user is logs out:
public void LogoutUser()
{
// reset identities and other related info (localstorage data if you have, etc).
var identity = new ClaimsIdentity();
var user = new ClaimsPrincipal(identity);
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
}