Blazor - Cascade username to all components - authorization

I'm writing a Blazor server-side app with Windows Authentication enabled in the project.
I'm unable to use Role/Policy based authentication (I don't have access to change users roles/policies) and instead will be grabbing a set of usernames from a SQL database to check against the current user as to what parts of the NavMenu they can access.
I'm struggling to get the windows username available to all components and accessible from within the #code section though.
I've seen in the App.razor they use CascadingAuthenticationState component and an AuthoriseView component
I know you can use #context.User.Identity.Name to display the username but I'm not sure how to access the #context from within a #code section to get the username.
I've also tried this code and in the app which displays the username successfully:
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private string _authMessage;
private async Task LogUsername()
{
var authState = await authenticationStateTask;
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
_authMessage = $"{user.Identity.Name} is authenticated.";
}
else
{
_authMessage = "The user is NOT authenticated.";
}
}
But I don't like the idea of repeating something like this async code in every component.
My simple idea was to create an "AuthorisationService" class and register it as a singleton. This would get the set of usernames from SQL and the current user at the outset when the page is first loaded and the logic of checking can live in there.
I'd inject it into every component and for the NavMenu I can have if-statements as to who can access what making them visible or not .
If anyone can shed some light on how to get the windows username in this way (or a better way if one exists as I'm just learning and completely new to Blazor) that would be great!
Many Thanks
Nick

In order to cascade the UserName to all components you need to create a class to get the userName like this:
public class UserInfo
{
private readonly AuthenticationStateProvider authenticationStateProvider;
public UserInfo(AuthenticationStateProvider authenticationStateProvider)
{
this.authenticationStateProvider = authenticationStateProvider;
}
public async Task<string> GetUserName()
{
var authState = await authenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
return user?.Identity?.Name ?? "Pitza man";
}
}
Now you need to register this class in Startup
services.AddScoped<UserInfo>();
Then in your main layout you can add a cascading value
#inherits LayoutComponentBase
#inject UserInfo _user
<CascadingValue Value="_user.GetUserName()" Name="UserName">
#Body
</CascadingValue>
Then in your component can get this value:
#code {
[CascadingParameter(Name = "UserName")] public string UserName { get; set; }
}

I had a use-case for getting info from the HttpContext related to JWT tokens.
Serverside can be confusing, because it is a black-box that works through signal R, but this works for me. You can modify for your own use-case.
Edit the Host.cshtml file to get the HttpContext data you want/need
Add a parameter for the data you need on the App.razor component
Pass the data from HttpContext as a parameter into the App.razor component on Host.cshtml page
Create a cascading value for the data in App.Razor
_Host.cshtml
<body>
#{
var token = await HttpContext.GetTokenAsync("access_token");
}
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" param-AccessToken="token" />
</app>
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
Reload
<a class="dismiss">๐Ÿ—™</a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
App.Razor
<CascadingValue Name="AccessToken" Value="AccessToken">
<CascadingAuthenticationState>
<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>
</CascadingAuthenticationState>
</CascadingValue>
#code{
[Parameter] public string AccessToken { get; set; }
}
You can read more about this (https://github.com/dotnet/aspnetcore/issues/18183)

Related

How can I check in the login whether the email exists in a database - razor page?

I want the site login form to check if the email is in the system or not, to return an error message.
I can not do this because I am new. I tried to do something I think that's the direction, I'd be happy if you could help me.
public class ConnectionAttribute : ValidationAttribute
{
private readonly OrderContext _context;
private readonly string _connectingMail;
public ConnectionAttribute(string connectingMail)
{
_connectingMail = connectingMail;
}
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
List<Customer> allCustomers = _context.Customer.ToList();
List<string> allMails = new List<string>();
foreach (var item in allCustomers)
{
allMails.Add(item.CustomerMail);
}
var file = value as string;
if (allMails.Contains(_connectingMail))
{
return new ValidationResult(GetErrorMessage());
}
return ValidationResult.Success;
}
public string GetErrorMessage()
{
return $".";
}
}
I think this code is correct, but I do not know what to call it after clicking connect .
What can you call him?
You could try to use Remote validation.
First, Remote validation in ASP.NET (Core) relies on Unobtrusive AJAX, so you will need to install that first. The easiest way to do this is via LibMan. Please check the wwwroot/lib folder whether you have installed them or not. If doesn't install it, refer the following steps:
Please right click on the lib folder in wwwroot, choose Add ยป Client-side Library, and then choose jsdelivr as the source, and type in jquery-ajax-unobtrusive.
Second, since your application is an Asp.net Core Razor application, in the PageModel, add Email property with PageRemote attribute and a OnPostCheckEmail method to check whether the email is exist or not:
public class RemoteValidateModel : PageModel
{
[PageRemote(ErrorMessage = "Email Address already exists", AdditionalFields = "__RequestVerificationToken",
HttpMethod = "post",PageHandler = "CheckEmail")]
[BindProperty]
public string Email { get; set; }
public void OnGet()
{
}
//this method is used to check whether the email is exist or not.
public JsonResult OnPostCheckEmail()
{
//query the database and get all existing Emails or directly check whether the email is exist in the database or not.
var existingEmails = new[] { "jane#test.com", "claire#test.com", "dave#test.com" };
var valid = !existingEmails.Contains(Email);
return new JsonResult(valid);
}
}
Code in the Razor Page:
#page
#model RazorSample.Pages.RemoteValidateModel
#{
}
<form method="post">
<input asp-for="Email" />
<span asp-validation-for="Email" class="text-danger"></span><br>
<input type="submit" />
</form>
#section scripts{
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<partial name="_ValidationScriptsPartial" />
<script src="~/lib/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.min.js"></script>
}
Then, the result as below:
More detail information, please check this tutorial:
Remote Validation in Razor Pages
Besides, if you want to use Remote validation in Asp.net Core MVC application, you can check [Remote] attribute.

How to get username of requesting user not app pool under windows auth

Using Blazor server app.
I have the app pool running as a domain account, but I need the user name doing the request.
When using this, I get the name of the app pool instead.
CallingUser = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split(new char[] { '\\' })[1];
Update
This post helped me
Get Current User in a Blazor component
Also, needed to yet enable Websockets in IIS. Didn't discover that until I published to a test server.
If you want to get the login user in the sever side. You should make the service as scope and then you could inject the AuthenticationStateProvider into service.
Then you could get the current login user.
Details, you could refer to below codes:
public class WeatherForecastService
{
private readonly AuthenticationStateProvider _authenticationStateProvider;
public WeatherForecastService(AuthenticationStateProvider authenticationStateProvider) {
_authenticationStateProvider = authenticationStateProvider;
}
public string GetCurrentUserName() {
var provider= _authenticationStateProvider.GetAuthenticationStateAsync();
return provider.Result.User.Identity.Name;
}
}
As far as I know, if you want to get the current login user, you could try to use AuthenticationStateProvider service.
The built-in AuthenticationStateProvider service obtains authentication state data from ASP.NET Core's HttpContext.User.
You could inject the AuthenticationStateProvider AuthenticationStateProvider and then use AuthenticationStateProvider.GetAuthenticationStateAsync to get the user state, at last ,you could use user.Identity.Name to get the current user name.
More details ,you could refer to below codes:
#page "/counter"
#using Microsoft.AspNetCore.Components.Authorization
#inject AuthenticationStateProvider AuthenticationStateProvider
<h1>Counter</h1>
<p>Current count: #currentCount</p>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
<hr />
<button class="btn btn-primary" #onclick="GetUserName">Click me</button>
<p>Current Login User = #Username</p>
#code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
private string Username = string.Empty;
private async Task GetUserName()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
Username = user.Identity.Name;
}
}
Result:

blazor scoped service initializing twice

I'm trying to learn asp.net core, more specifically blazor server. From the documentation, it appears a service registered as scoped will be created once per connection. My user service constructor is running twice on the first load of the page in the browser, and twice again on each refresh of the page.
I believe these are the applicable parts of the code necessary to help me determine why this is occurring. My question is how to make it create one instance of the user service for each client connection? I'm getting the correct output on screen but don't prefer it to run twice.
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHttpContextAccessor();
services.AddDbContext<AWMOPSContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("AWMOPSContext")),
ServiceLifetime.Transient);
services.AddScoped<UserService>();
}
public class UserService
{
public Associate Associate { get; set; }
public UserService(AWMOPSContext context, IHttpContextAccessor httpContextAccessor)
{
var username = httpContextAccessor.HttpContext.User.Identity.Name.Substring(7);
Associate = context.Associates.Where(a => a.LogonName == username).FirstOrDefault();
Debug.WriteLine($"Hello {Associate.PreferredName} {Associate.LastName}");
}
}
#page "/"
#inject AWMWP.Services.UserService user;
<h1>Welcome #user.Associate.PreferredName #user.Associate.LastName</h1>
It is called twice, as you are using pre-rendering. Go to _Host.cshtml and change render-mode="ServerPrerendered" to render-mode="Server", and it would be called only once:
<app>
<component type="typeof(App)" render-mode="Server" />
</app>
Reference:
https://learn.microsoft.com/en-us/aspnet/core/blazor/lifecycle?view=aspnetcore-3.1#stateful-reconnection-after-prerendering

Get Current User in a Blazor component

I'm starting a new site with Blazor and Windows Authentication and need to identify the current user viewing the page/component.
For a Razor Page, the current user name can be accessed with Context.User.Identity.Name, but that doesn't seem to work in a Blazor component. I've tried injecting HttpContext into the component but the Context is null at runtime.
As a bonus, I will eventually want to incorporate this into Startup.cs so I only need to get the username once and can leverage a corporate user class (with EF Core) for my applications. Answers tailored to that use case would also be appreciated.
There are three possibilities for getting the user in a component (a page is a component):
Inject IHttpContextAccessor and from it access HttpContext and then User; need to register IHttpContextAccessor in Startup.ConfigureServices, normally using AddHttpContextAccessor. Edit: according to the Microsoft docs you must not do this for security reasons.
Inject an AuthenticationStateProvider property, call GetAuthenticationStateAsync and get a User from it
Wrap your component in a <CascadingAuthenticationState> component, declare a Task<AuthenticationState> property and call it to get the User (similar to #2)
See more here: https://learn.microsoft.com/en-us/aspnet/core/security/blazor.
For me the solution mentioned in the first answer 2. option worked perfect:
I am using Blazor server side on .Net Core 5.0 .
I injected
#inject AuthenticationStateProvider GetAuthenticationStateAsync
in my Blazor page and added the following in the code section:
protected async override Task OnInitializedAsync()
{
var authstate = await GetAuthenticationStateAsync.GetAuthenticationStateAsync();
var user = authstate.User;
var name = user.Identity.Name;
}
In my startup.cs, I have the following lines:
services.AddScoped<ApiAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(p =>
p.GetRequiredService<ApiAuthenticationStateProvider>());
For blazor wasm in net 5.0 and above. Here is how I did,
Wrap your <App> component inside <CascadingAuthenticationState> as shown below,
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(Program).Assembly">
<Found Context="routeData">
...
</Found>
<NotFound>
...
</NotFound>
</Router>
</CascadingAuthenticationState>
Then add Task<AuthenticationState> CascadingParameter inside any component as shown below,
public class AppRootBase : ComponentBase
{
[CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }
}
Now you can access logged in user Identity and Claims inside component as shown below,
protected override async Task OnInitializedAsync()
{
var authState = await authenticationStateTask;
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
Console.WriteLine($"{user.Identity.Name} is authenticated.");
}
}
Here is the reference from Microsoft docs.
I've now been able to get it to work with a general class, as well as a component.
To get access to the HttpContext User; in ConfigureServices, in Startup.cs add
services.AddHttpContextAccessor();
I have a CorporateUserService class for my CorporateUser class. The service class gets a DbContext through constructor injection.
I then created a new CurrentCorporateUserService that inherits from the CorporateUserService. It accepts a DbContext and an IHttpContextAccessor through constructor injection
public class CurrentCorporateUserService : CorporateUserService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CurrentCorporateUserService(IHttpContextAccessor httpContextAccessor,
MyDbContext context) : base(context)
{
_httpContextAccessor = httpContextAccessor;
}
...
The base service class has a method GetUserByUsername(string username). The Current service class adds an additional method
public CorporateUser GetCurrentUser()
{
return base.GetUserByUsername(_httpContextAccessor.HttpContext.User.Identity.Name.Substring(8));
}
The Current service class is registered in Startup.cs
services.AddScoped<CurrentCorporateUserService>();
Once that is done, I can use the CurrentCorporateUserService in a component with directive injection.
[Inject]
private CurrentCorporateUserService CurrentCorporateUserService { get; set; } =
default!;
or in any class, with constructor injection.
public MyDbContext(DbContextOptions<MyDbContext> options,
CurrentCorporateUserService CurrentCorporateUserService)
: base(options)
{
_currentUser = CurrentCorporateUserService.GetCurrentUser();
}
Making it a project wide service means all my developers do not have to concern themselves with how to get the Current User, they just need to inject the service into their class.
For example, using it in MyDbContext makes the current user available to every save event. In the code below, any class that inherits the BaseReport class will automatically have the report metadata updated when the record is saved.
public override Int32 SaveChanges()
{
var entries = ChangeTracker.Entries().Where(e => e.Entity is BaseReport
&& (e.State == EntityState.Added || e.State == EntityState.Modified));
foreach (var entityEntry in entries)
{
((BaseReport)entityEntry.Entity).ModifiedDate = DateTime.Now;
((BaseReport)entityEntry.Entity).ModifiedByUser = _currentUser.Username;
}
return base.SaveChanges();
}
I've had a similar requirement and have been using:
var authstate = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authstate.User;
var name = user.Identity.Name;
I already had an AuthenticationStateProvider in my startup.cs and added it to the constructor of my custom class.
If you create a new project and choose Blazor with Windows Authentication you get a file called Shared\LoginDisplay.razor with the following content:
<AuthorizeView>
Hello, #context.User.Identity.Name!
</AuthorizeView>
Using the <AuthorizeView> we can access #context.User.Identity.Name without any modifications on any page.
In your App.razor, make sure the element encapsulated inside a CascadingAuthenticationState element. This is what is generated by default if you create your Blazor project with authentication support.
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(Program).Assembly" PreferExactMatches="#true">
<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>
</CascadingAuthenticationState>
In your component you can use the AuthenticationStateProvider to access the current user like in the following sample:
#page "/"
#layout MainLayout
#inject AuthenticationStateProvider AuthenticationStateProvider
#inject SignInManager<IdentityUser> SignInManager
#code
{
override protected async Task OnInitializedAsync()
{
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (SignInManager.IsSignedIn(authenticationState.User))
{
//Do something...
}
}
}
The below solution works only if you are running under IIS or IIS Express on Windows. If running under kestrel using 'dotnet run', please follow steps here, https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth?view=aspnetcore-3.0&tabs=visual-studio#kestrel
[startup.cs]
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
}
[index.razor]
#page "/"
#using Microsoft.AspNetCore.Http
#inject IHttpContextAccessor httpContextAccessor
<h1>#UserName</h1>
#code {
public string UserName;
protected override async Task OnInitializedAsync()
{
UserName = httpContextAccessor.HttpContext.User.Identity.Name;
}
}
I also had this problem. The best solution I found was to inject both UserManager and AuthenticationStateProvider and then I made these extension functions:
public static async Task<CustomIdentityUser> GetUserFromClaimAsync(this
ClaimsPrincipal claimsPrincipal,
UserManager<CustomIdentityUser> userManager)
{
var id = userManager.GetUserId(claimsPrincipal);
return await userManager.FindByIdAsync(id);
}
public static async Task<CustomIdentityUser> GetCurrentUserAsync(this AuthenticationStateProvider provider, UserManager<CustomIdentityUser> UM)
{
return await (await provider.GetAuthenticationStateAsync()).User.GetUserFromClaimAsync(UM);
}
public static async Task<string> GetCurrentUserIdAsync(this AuthenticationStateProvider provider, UserManager<CustomIdentityUser> UM)
{
return UM.GetUserId((await provider.GetAuthenticationStateAsync()).User);
}
This was a painful journey for me chasing a moving target. In my case I only needed the user name for my Blazor component used in a Razor page. My solution required the following:
In the Index.cshtml.cs I added two properties and constructor
public IHttpContextAccessor HttpContextAccessor { get; set; }
public string UserName { get; set; }
public TestModel(IHttpContextAccessor httpContextAccessor)
{
HttpContextAccessor = httpContextAccessor;
if (HttpContextAccessor != null) UserName = HttpContextAccessor.HttpContext.User.Identity.Name;
}
Then in the Index.cshtml where I add the component I called it as follows:
<component type="typeof(MyApp.Components.FileMain)" param-UserName="Model.UserName" render-mode="ServerPrerendered" />
In my component I use a code behind file (FileMain.razor.cs using public class FileMainBase : ComponentBase) have the code:
[Parameter]
public string UserName { get; set; } = default!;
and then as a proof of concept I added to the FileMain.razor page
<div class="form-group-sm">
<label class="control-label">User: </label>
#if (UserName != null)
{
<span>#UserName</span>
}
</div>
You should add needed claims to the User after login.
For example I show the FullName on top of site (AuthLinks component) instead of email.
[HttpPost]
public async Task<IActionResult> Login(LoginVM model)
{
var user = await _userManager.FindByNameAsync(model.Email);
if (user == null || !await _userManager.CheckPasswordAsync(user, model.Password))
return Unauthorized("Email or password is wrong.");
var signingCredentials = GetSigningCredentials();
var claims = GetClaims(user);
var tokenOptions = GenerateTokenOptions(signingCredentials, claims);
var token = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
return Ok(new LoginDto { Token = token, FullName = user.FirstName + " " + user.LastName });
}
#code {
private LoginVM loginVM = new();
[Inject]
public AuthenticationStateProvider _authStateProvider { get; set; }
private async Task SubmitForm()
{
var response = await _http.PostAsJsonAsync("api/accounts/login", loginVM);
var content = await response.Content.ReadAsStringAsync();
var loginDto = JsonSerializer.Deserialize<LoginDto>(content);
await _localStorage.SetItemAsync("authToken", loginDto.Token);
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", loginDto.Token);
(_authStateProvider as AuthStateProvider).NotifyLogin(loginDto.FullName);
_navigationManager.NavigateTo("/");
}
}
public void NotifyLogin(string fullName)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("FullName", fullName) }, "jwtAuthType"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
<AuthorizeView>
<Authorized>
#context.User.FindFirst("FullName")?.Value
<button class="btn btn-outline-danger mx-4" #onclick="LogOut">LogOut</button>
</Authorized>
<NotAuthorized>
Login
Register
</NotAuthorized>
</AuthorizeView>
GitHub project link:
https://github.com/mammadkoma/Attendance
This worked for me:
Replace:
#context.User.Identity.Name
With:
#context.User.Claims.Where(x => x.Type=="name").Select(x => x.Value).FirstOrDefault()
UPDATE - This answer does not work. Rather than deleting it, I've let it here as information. Please consider the other answers for the question instead.
In ConfigureServices, in Startup.cs, add
services.AddHttpContextAccessor();
In your component class [Note: I use code-behind with null types enabled]
[Inject]
private IHttpContextAccessor HttpContextAccessor { get; set; } = default!;
private string username = default!;
In your component code (code behind), in protected override void OnInitialized()
username = HttpContextAccessor.HttpContext.User.Identity.Name;
username can now be used throughout the component just like any other variable.
However, see my other answer in this question to add get the current user name from a service usable in any class.
This is what works for me on a single page
Add to Startup.cs
services.AddHttpContextAccessor();
On the Razor Component page
#using Microsoft.AspNetCore.Http
#inject IHttpContextAccessor httpContextAccessor
<div>#GetCurrentUser()</div>
#code{
protected string GetCurrentUser()
{
return httpContextAccessor.HttpContext.User.Identity.Name;
}
}

Blazor - Securing using ADFS with local DB repository: how/when to hook into SQL

I have a Blazer Server app which now uses authentication from a local ADFS server. Having identified the user, I now need to load their permissions. We don't think this can be provided via claims from the ADFS server, so want to configure this in the DB, but need to understand how/when to get this information.
Regarding the hook into ADFS, my code is as follows (any suggestions on improvement most welcome)
App.razor
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)">
<NotAuthorized>
<h1>Sorry</h1>
<p>You're not authorized to reach this page.</p>
<p>You may need to log in as a different user.</p>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="#typeof(MainLayout)">
<h1>Sorry</h1>
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
appsettings.Development.json
{
"DetailedErrors": "true",
"ConnectionStrings": {
"MyDB": "Data Source=x.x.x.x;Initial Catalog=xxxxx;user id=me;password=sshhh;Persist Security Info=False;"
},
"Ida": {
"ADFSMetadata": "https://adfs.ourServer.com/FederationMetadata/2007-06/FederationMetadata.xml",
"Wtrealm": "https://localhost:44323/"
}
}
Startup.cs (only showing security related code)
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.WsFederation;
public class Startup
{
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
.....
app.UseAuthentication();
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Frame-Options", "DENY");
var user = context.User;
if (user?.Identities.HasNoItems(identity => identity.IsAuthenticated) ?? true)
{
await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme).ConfigureAwait(false);
}
if (next != null)
{
await next().ConfigureAwait(false);
}
});
....
}
...
public void ConfigureServices(IServiceCollection services)
{
var wtrealm = this.Configuration.GetSection("Ida:Wtrealm").Value;
var metadataAddress = this.Configuration.GetSection("Ida:ADFSMetadata").Value;
services
.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = wtrealm ;
options.MetadataAddress = metadataAddress;
options.UseTokenLifetime = false;
})
.AddCookie();
....
}
}
Any suggestions regarding the above code? When the user enters our site (any page), they automatically get pushed to the ADFS server to authenticate. Seems okay, but prevents the user from logging out....
So, from ADFS we get several claims that identify the user, e.g. their UPNname. My thought is to go to the DB and load all the roles/permissions/rights that this user has.
Where in my code should I put such code
The DB is currently used by another application that uses the older "membership" tables. I want to use something a bit more up-to-date, the identity model? I can't risk breaking security for the other application. Should I store security in a new DB?
Any pointers would be most welcome...assume I'm a novice at this.
The usual way to do this is to write a custom attribute provider for ADFS.
Then you can get the roles you want from the DB and they are added to the claims.
After reading a lot around this area, I discovered a very clearly presented PluralSight course that solved it for me:
ASP.NET Core 2 Authentication Playbook: Chris Klug
Specifically, see the chapter:
Doing Claims Transformation
For details, watch the course. In summary:
Create the Entity and Context for pulling your data from the DB.
Register these in the Startup in the normal manner.
Create a ProfileService (that has the logic to read the data from the
Context). I also included a MemoryCache object so I could locally
store the info.
Then create a Claims Transformation class (implementing IClaimsTransformation) and register that as a service.
Add these into your Blazor page's code-behind file as:
[Inject]
protected IClaimsTransformation ClaimsTransformation { get; set; } = null!;
[CascadingParameter]
protected Task AuthenticationStateTask { get; set; }
Consume the data thus:
AuthenticationState authState = await this.AuthenticationStateTask;
ClaimsPrincipal user = authState.User;
ClaimsPrincipal x = await this.ClaimsTransformation.TransformAsync(user);