Reputation: 4899
I have an ASP.NET Core MVC app with several controllers(returning just views). The app already uses ASP.NET Core Identity for authentication and authorization. However, there are a couple of places where I need to invoke a controller action endpoint directly as an API using an HTTP Client. It appears that this requires API authentication that Identity does not seem to support out of the box.
I need to invoke these controller actions from a Razor component embedded into my MVC view using the component tag helper (render mode is ServerPrerendered
).
Now, I could have directly accessed my database from my Razor component by injecting the DB service into my component like I have done for other Razor components. However, I cannot do that with this one Razor component because it requires access to the UserManager
service of Identity and as per the documentation:
SignInManager<TUser>
andUserManager<TUser>
aren't supported in Razor components. Blazor Server apps use ASP.NET Core Identity.
Therefore, it seems like I might have to invoke a controller action from my Razor component by injecting IHttpClientFactory
.
However, in my entire app so far, I only need to invoke controller actions for two operations:
You may ask why do I need a Razor component for user account creation. Creating an account is a rather complex step(multiple steps) involving client interactivity like adding items dynamically to a list. Razor components have made it so easy for me. I'd have had to use JavaScript/jQuery if I were doing it in a .cshtml
view.
I don't think it's worth creating an entire API project just to help me with these two operations(although I do not mind doing so). I could add these actions to my existing accounts controller or create a new API controller in the same project. Nevertheless, I still have to secure these endpoints.
How can I add API authentication and authorization to these two controller actions (create user and edit user) without disturbing what the existing ASP.NET Core Identity setup is doing?
I still want the same Identity authentication/authorization for the rest of the controllers in my app but API authentication/authorization for these Create
and Edit
user actions.
Or since my Razor component is embedded in an MVC view, is there a way to invoke the controller action through my component just as an MVC view does by sending cookies with the request?
Either way, my goal is to secure the two controller action endpoints.
This is how Identity is currently configured in Startup.cs
:
services.AddDefaultIdentity<AppUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
This is a simplified version of how my Create
controller action would look like:
[Authorize(Roles = "Administrator")]
public async Task<IActionResult> Create(UserViewModel model)
{
if (ModelState.IsValid)
{
AppUser user = model.ToAppUser(); // Method on view model does the mapping
user.Id = Guid.NewGuid().ToString();
IdentityResult createUserResult = await _userManager.CreateAsync(user, model.Password);
if (createUserResult.Succeeded)
{
await userManager.AddToRoleAsync(user, role.Name);
return Ok();
}
foreach(var error in createUserResult.Errors)
{
ModelState.AddModelError("", error.Description);
}
}
}
This is how I would invoke it from the Razor component:
CreateUser.razor
:
@inject IHttpClientFactory HttpClientFactory
<EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
...
</EditForm>
@code {
private UserViewModel _model = new();
public async Task HandleValidSubmit()
{
var modelAsJson = new StringContent(
JsonSerializer.Serialize(_model),
Encoding.UTF8,
"application/json");
using var httpResponse =
await _httpClient.PostAsync("/api/Accounts/Create", modelAsJson);
//...
}
}
Embedding the component in an MVC view using <component>
:
Create.cshtml
...
<component type="typeof(CreateUser)" render-mode="ServerPrerendered" />
After some research, I've seen JWT as the most common way used for API authentication. From the instructions here, this is how it was added in Startup.cs
:
services.AddAuthentication( auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => {..})
Here, when the default authentication scheme is set to JWT, will this override my default Identity setup? How do I use both the schemes together?
Upvotes: 1
Views: 4551
Reputation: 1040
Since you are using an MVC controller you could use cookie authentications instead of bearer token (JWT) which are mostly meant for api controllers (even though it is possible to use either).
For cookies you would use [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
and for jwt [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
above your controller (for using you'll need
Microsoft.AspNetCore.Authentication.Cookies
).
The scheme is nothing more then a string that matches the string defined in your Startup.cs for example:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.AccessDeniedPath = "/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromHours(9);
options.Cookie = new CookieBuilder()
{
Name = "cookie",
SameSite = SameSiteMode.Strict,
};
options.LoginPath = "/Login";
....
}
But, I did not understood, from the question, how is the authentication handled, i.e. who issues the token or cookie. Form the link you shared that would be the "Authenticating Users using an Web API Endpoint" part. Or maybe you would go with Identity Server, or a third option.
p.s. Authentication/Authorization are hard features - Keep calm and code on :)
Upvotes: 1
Reputation: 10929
There is a way to support multiple authentication schemes in asp.net core.
it looks like this:
Startup class:
public void ConfigureServices(IServiceCollection services)
{
serviced.AddDefaultIdentity<AppUser>()
.AddRoles<IdentityUser>()
.AddEntityFrameworkStores<AppDbContext>();
services.AddAuthentication()
.AddCookie(options => {
options.LoginPath = "/Account/Unathorized/"
options.AddAccessDeniedPath = "/Account/Forbidden"
});
}
Controller:
[Authorize(AuthenticationSchemes = YourCustomeScheme)]
public class AuthController: controller
You can add and use as much as schemes as you want.
Upvotes: 4