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;
}
}
Related
I'm building a Blazor app to see how I can persist user data after a B2C AD login.
I want to persist claim data to sql database (ef 6 core) when the user logs in to the app.
I'm trying to capture a Tenant for the user for use in filtering on the app.
I is custom middleware a good way to go with this?
This is a Blazor Server Side app
I have something like this for testing.
public class PersistUserChangesMiddleware
{
private readonly RequestDelegate _next;
public PersistUserChangesMiddleware(RequestDelegate next)
{
_next = next;
}
[Authorize]
public Task Invoke(HttpContext httpContext, MyContext context)
{
try
{
var user = httpContext.User;
var claims = user.Claims;
var tenant = claims?.FirstOrDefault(c => c.Type.Equals("extension_CompanyId", StringComparison.OrdinalIgnoreCase));
if(tenant != null)
{
context.Tenants.Add(new Models.Tenant()
{
TenantName = tenant.Value
});
context.SaveChanges();
}
}
catch (Exception)
{
throw;
}
return _next(httpContext);
}
}
}
I'm not getting the user back from this call in the middleware. Do I need to do it a different way for Blazor? I set [Authorize] but still no user.
I can't see which AuthenticationStateProvider is configured, but it's likely to be ServerAuthenticationStateProvider.
Create a custom AuthenticationStateProvider which is essentially a pass through provider that just grabs the ClaimsPrincipal user and does whatever you want with it. (Let me know if you're using a different provider).
public class MyAuthenticationStateProvider : ServerAuthenticationStateProvider
{
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var authstate = await base.GetAuthenticationStateAsync();
if (authstate.User is not null)
{
ClaimsPrincipal user = authstate.User;
// do stuff with the ClaimsPrincipal
}
return authstate;
}
}
And then register it in Program:
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// sequence is crucial - Must be after AddServerSideBlazor
builder.Services.AddScoped<AuthenticationStateProvider, MyAuthenticationStateProvider>();
builder.Services.AddSingleton<WeatherForecastService>();
Test it with a break point on the first line of GetAuthenticationStateAsync.
I have web API with custom policies and authorization handlers.
I wanted to reuse authorization handlers but HttpContext is null when attribute is used on signalr's hub.
For example this is my controller.
[Authorize]
public sealed class ChatsController : ControllerBase
{
[HttpPost("{chatId}/messages/send")]
[Authorize(Policy = PolicyNames.ChatParticipant)]
public Task SendMessage() => Task.CompletedTask;
}
And this my my authorization handler. I can extract "chatId" from HttpContext and then use my custom logic to authorize user.
internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement)
{
if(_httpContextAccessor.HttpContext != null)
{
// Logic
}
return Task.CompletedTask;
}
}
However this won't work with Azure SignalR because I don't have access to HttpContext. I know that I can provide custom IUserIdProvider but I have no idea how to access "chatId" from "Join" method in my custom authorization handler.
[Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
[Authorize(Policy = PolicyNames.ChatParticipant)]
public async Task Join(Guid chatId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
}
Is it possible to reuse my authorization handlers?
I would like to avoid copypasting my code.
One solution is to extract my authorization code to separate services but then I have to manually call those from my hubs and abandon [Authorize] way.
Your chat is a resource, and you want to use resource based authorization. In this case declarative authorization with an attribute is not enough, because chat id is known at runtime only. So you have to use imperative authorization with IAuthorizationService.
Now in your hub:
[Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
private readonly IAuthorizationService authService;
public ChatHub(IAuthorizationService authService)
{
this.authService = authService;
}
public async Task Join(Guid chatId)
{
// Get claims principal from authorized hub context
var user = this.Context.User;
// Get chat from DB or wherever you store it, or optionally just pass the ID to the authorization service
var chat = myDb.GetChatById(chatId);
var validationResult = await this.authService.AuthorizeAsync(user, chat, PolicyNames.ChatParticipant);
if (validationResult.Succeeded)
{
await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
}
}
}
Your authorization handler should look different, because it needs the chat resource in its signature to do this kind of evaluation:
internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, Chat>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, Chat chat)
{
// You have both user and chat now
var user = context.User;
if (this.IsMyUserAuthorizedToUseThisChat(user, chat))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
Edit: there is actually another option I didn't know about
You can make use of HubInvocationContext that SignalR Hub provides for authorized methods. This can be automatically injected into your AuthorizationHandler, which should look like this:
public class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, HubInvocationContext hubContext)
{
var chatId = Guid.Parse((string)hubContext.HubMethodArguments[0]);
}
}
Hub method will be decorated normally with [Authorize(Policy = PolicyNames.ChatParticipant)]
You still will have two authorization handlers, AuthorizationHandler<ChatParticipantRequirement> and AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext>, no way around it. As for code dublication, you can however just get the Chat ID in the handler, either from HttpContext or HubInvocationContext, and than pass it to you custom written MyAuthorizer that you could inject into both handlers:
public class MyAuthorizer : IMyAuthorizer
{
public bool CanUserChat(Guid userId, Guid chatId);
}
I have a blazor server web application and a .NET Core worker process, these both use a common class for data access (generic unit of work / generic repository).
In the database I would like to log the user names that are inserting or editing records. To do this I want to inject a ClaimsPrincipal to the shared UoW and Repo classes).
So, I would like to be able to extract the current ClaimsPrincipal in a transient service via dependency injection.
For the worker I can inject a ClaimsPrincipal via the following code;
public static IServiceCollection CreateWorkerClaimsPrincipal(this IServiceCollection services, string workerName)
{
Claim workerNameClaim = new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", workerName);
ClaimsIdentity identity = new ClaimsIdentity(
new System.Security.Claims.Claim[] { workerNameClaim },
"My-Worker-Authentication-Type",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"role");
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
services.AddTransient<ClaimsPrincipal>(s => principal);
return services;
}
This is working and meets my needs.
For the blazor server web application I need to do something similar.
I believe that the correct way to extract the ClaimsPrincipal is via the AuthenticationStateProvider, however this needs a call to an async method GetAuthenticationStateAsync.
NOTE: I cannot user IHttpContextAccessor as this doesn't work with Azure App Service.
I want something like;
public void ConfigureServices(IServiceCollection services)
{
/// ...
services.AddTransient<ClaimsPrincipal>(); // I think I need to do something here?
/// ...
}
So when I request a ClaimsPrincipal via dependency injection I want to return the user from;
var authState = await AUthenticationStateProvider.GetAuthenticationStateAsync();
return authState.User;
Is this possible?
As is often the way, by working this through into a simple example for a SO post I have found a workable (I think) solution from https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-5.0#implement-a-custom-authenticationstateprovider
NOTE: I'm still not 100% sure if the async init pattern will always resolve the AuthenticationState before the Repository property is called, but its hanging together so far... Just beware of this if you choose to use this code.
I have changed the approach, and instead of trying to resolve ClaimsPrincipal via DI (because AuthenticationStateProvider is not available for a worker process), I have created a custom AuthenticationStateProvider in the worker.
public class WorkerAuthStateProvider : AuthenticationStateProvider
{
private readonly string _workerName;
public WorkerAuthStateProvider(string workerName)
{
_workerName = workerName;
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity(new[] {
new Claim(ClaimTypes.Name, _workerName),
}, "My-Worker-Authentication-Type");
ClaimsPrincipal user = new ClaimsPrincipal(identity);
return Task.FromResult(new AuthenticationState(user));
}
}
and then register this in configureServices to resolve for instances of AuthenticationStateProvider in the worker program.cs file (also passing a custom worker process name, so I can use this on all my workers);
services.AddScoped<AuthenticationStateProvider, WorkerAuthStateProvider>(serviceProvider =>
{
return new WorkerAuthStateProvider(Constants.Logging.RoleNames.MYWORKERNAME);
});
The AuthenticationStateProvider already works in the blazor web apps so this allows me to resolve this correctly, in the constructor for my GenericUnitOfWork pattern for data access on both Web and Workers, for example;
private TDbContext _dbContext;
private readonly ILogger<TEntity> _logger;
private GenericRepository<TEntity, TDbContext> _repository;
private ClaimsPrincipal _user;
private readonly AuthenticationStateProvider _authenticationStateProvider;
public GenericUnitOfWork(TDbContext context, ILogger<TEntity> logger, AuthenticationStateProvider authenticationStateProvider)
{
_dbContext = context;
_logger = logger;
_authenticationStateProvider = authenticationStateProvider;
UserInit = InitUserAsync();
}
/// <summary>
/// Async initialisation pattern from https://blog.stephencleary.com/2013/01/async-oop-2-constructors.html
/// </summary>
public Task UserInit { get; private set; }
private async Task InitUserAsync()
{
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
_user = authState.User;
}
public IGenericRepository<TEntity, TDbContext> Repository
{
get
{
if (_repository == null)
{
// when accessing the repository, we are expecting to pass the current application claims principal
// however the ClaimsPrincipal is resolved using an Async method from the AuthenticationStateProvider.
// In the event that the Async method has not yet completed we need to throw an exception so we can determine
// if a further async code fix is required.
if (_user == null)
{
throw new InvalidOperationException("Async ClaimsPrincipal has not been loaded from the AuthenticationStateProvider");
}
_repository = new GenericRepository<TEntity, TDbContext>(_dbContext, _logger, _user);
}
return _repository;
}
}
Something I have struggled with is understanding the lifecycle of the Page Model class for my Razor Pages usages. I'm trying to think about how and when to deal with common data I pass to my business logic like the userId that is running the request. So many times I need to save this information with the results of the action.
So where is the best place to handle something over and over like geting User details that might be in the persistence model and not in the context of the page model's User from the HTTPContext?
I should mention I am using authorize tags with cookie backed authenication to a webservice.
For accessing Reuqest from other layers except the Razor Page, you could try IHttpContextAccessor.
For general way to handling user details from request, you could create a service like below:
public interface IUserService
{
IdentityUser GetUser();
}
public class UserService:IUserService
{
private readonly ApplicationDbContext _context;
private readonly HttpContext _httpContext;
public UserService(ApplicationDbContext context
, IHttpContextAccessor httpContextAccessor)
{
_context = context;
_httpContext = httpContextAccessor.HttpContext;
}
public IdentityUser GetUser()
{
StringValues userId = "";
if (_httpContext.Request.Headers.TryGetValue("userId", out userId))
{
var user = _context.Users.FirstOrDefault(u => u.Id == userId);
return user;
}
else
{
return null;
}
}
}
And then register like
services.AddScoped<IUserService, UserService>();
Then, you could resolve IUserService from DI to use them when you want to access user info.
public class IndexModel : PageModel
{
private readonly IUserService _userService;
public IndexModel(IUserService userService)
{
_userService = userService;
}
public void OnGet()
{
var user = _userService.GetUser();
}
}
I have my AccountAdmin Controller where I manage Users in Identity.
So I have the Authorize Attribute like this at the top of the controller:
[Authorize(Roles = "Admin")]
public class AccountAdminController : Controller
The whole system is working great. If I log in as a user with the Admin role I can get to the page. And if I log in as a user without the Admin role I cannot get to the page. But my problem is that instead of being redirected to the "Account/AccessDenied" Page, I just get the "/AccountAdmin/Index" URL where I am denied the content and it just gives me the "Status Code: 403; Forbidden" message from:
app.UseStatusCodePages();
in my startup.
In StartUp.ConfigureServices I have:
services.AddIdentity<AppUser, IdentityRole>(options =>
{
options.Password.RequiredLength = 4;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireDigit = false;
options.User.AllowedUserNameCharacters = null;
}).AddEntityFrameworkStores<ApplicationDbContext>();
In StartupConfigure I have:
app.UseIdentity();
I know I haven't posted a lot of code here but it's all pretty straight forward stuff.
The caveaot is I am using Windows Authentication. I show the user login like DomainName\UserName in the UpperRight corner.
And then I made kind of an Impersonation Sign in Page where we can Sign in with TestRole1, TestRole2, etc.
The AccountController looks like this:
public class AccountController : Controller
{
private SignInManager<AppUser> _signInManager;
private UserManager<AppUser> _userManager;
public AccountController(SignInManager<AppUser> signInManager,
UserManager<AppUser> userManager)
{
_signInManager = signInManager;
_userManager = userManager;
}
public IActionResult Login()
{
return View(_userManager.Users.OrderBy(u => u.UserName));
}
[HttpPost]
public async Task<IActionResult> Login(string userName, bool persistant)
{
await _signInManager.SignInAsync(await _userManager.FindByNameAsync(userName), persistant);
return RedirectToAction("Index", "Home");
}
public async Task<IActionResult> LogOff()
{
await _signInManager.SignOutAsync();
return RedirectToAction("Login", "Account");
}
It's all working pretty well as far as Authentication and Authorization goes.
Accept I found that until I click my SignIn my real windows account won't match up with the roles I assigned to myself. It has to go through the SignInManager at:
[HttpPost]
public async Task<IActionResult> Login(string userName, bool persistant)
{
await _signInManager.SignInAsync(await _userManager.FindByNameAsync(userName), persistant);
return RedirectToAction("Index", "Home");
}
How can I get this AccessDenied redirect working?
Update 1:
I tried to make a filter like this:
public class MyAuthorizationFilter : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.Response.StatusCode == 403)
{
context.HttpContext.Response.Redirect("/Access/Denied");
}
}
}
But now when I try to replace the standard Authorize Attribute with this:
[MyAuthorizationFilter(Roles = "Admin")]
it doesn't know what "Roles" is.
It says: "The type or namespace Roles could not be found. Are you missing an assembly or namespace?"
Well it took me a while to work out how I want to do this. But here it is.
I didn't want this to go through error handling because it is not an unhandled error.
It is handled with app.UseStatusCodePages(); in StartUp.Configure.
So on a line below that in StartUp.Configure I add:
// 400 - 599
app.UseStatusCodePagesWithRedirects("~/Account/Status/{0}");
This is a place I can redirect to handle what I want to do with specific statuses but leave a default for what I don't specify.
The "Account/Status" action in the Account controller looks like this.
public IActionResult Status()
{
string statusCode = HttpContext.Request.Path;
statusCode = statusCode.Substring(statusCode.LastIndexOf('/') + 1);
ViewBag.StatusCode = statusCode;
return View(ViewBag);
}
and the view looks like this:
<div class="container-fluid">
<div class="panel panel-danger">
<div class="panel-header">
<h3>StatusCode:
#switch ((string)ViewBag.StatusCode)
{
case "403":
#:Access Denied
<div style="margin-top:5px"><a asp-controller="Account" asp-action="Login" class="btn btn-primary">Login as a different user</a></div>
break;
default:
#ViewBag.StatusCode
break;
}
</h3>
</div>
</div>
</div>
I just wanted this to stay out of the way of my error handling when I get to plugging that in. That is something different.
There must be a simple setting somewhere. But none of these solutions I see like AuthenticatonChallenge = true and so forth are working for me.
I think this is pretty tidy for now.