I started an application in Blazor .net 3.1, and I'm having a problem. I will want to add a user with an admin role (root) when starting the application. I am using EF. Adding the user works, but adding roles throws me an exception.
System.AggregateException : 'No service for type 'Microsoft.AspNetCore.Identity.RoleManager'1[Microsoft.AspNEtCore.Identity.IdentityRole]' has been registered.ontainer is destroyed)'
I have tried different solutions, like ASP.NET Core Identity Add custom user roles on application startup, old post but I still have the same exception, on SQLite, SQL Server,...
I created a static class and in the Startup.cs I call this method.
public static class RolesData
{
private static readonly string[] Roles = new string[] { "Admin", "Manager", "Member" };
public static async Task SeedRoles(IServiceProvider serviceProvider)
{
using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var roleManager = serviceScope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
foreach (var role in Roles)
{
if (!await roleManager.RoleExistsAsync(role))
{
await roleManager.CreateAsync(new IdentityRole(role));
}
}
}
}
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
RolesData.SeedRoles(app.ApplicationServices).Wait();
}
If you have any suggestions I'm interested, and also if you know of a site that explains authentication with Identity, I want to understand!
Thank you for your help
By the error it appears you have not configured the Identity server to expose Roles.
For example in Startup.cs
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>() // <------
.AddEntityFrameworkStores<ApplicationDbContext>();
I have a standard template with roles seeded here
It goes further to show how to enable the use of the Authorize attribute:
#attribute [Authorize(Roles = "Administrator")]
and AuthorizeView :
<AuthorizeView Roles="Administrator">
Only Administrators can see this.<br />
</AuthorizeView>
<AuthorizeView Roles="Moderator">
Only Moderators can see this.<br />
</AuthorizeView>
<AuthorizeView Roles="Moderator,Administrator">
Administrators and Moderators can see this.
</AuthorizeView>
The changes I made to a standard project to enable Roles and make them visible to Blazor WebAssembly can be seen in this commit
Microsoft has a good guide about this:
https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-identity-server?view=aspnetcore-6.0&tabs=visual-studio#name-and-role-claim-with-api-authorization
In the Client app, create a custom user factory. Identity Server sends multiple roles as a JSON array in a single role claim. A single role is sent as a string value in the claim. The factory creates an individual role claim for each of the user's roles.
CustomUserFactory.cs:
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
public class CustomUserFactory
: AccountClaimsPrincipalFactory<RemoteUserAccount>
{
public CustomUserFactory(IAccessTokenProviderAccessor accessor)
: base(accessor)
{
}
public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
if (user.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)user.Identity;
var roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();
if (roleClaims.Any())
{
foreach (var existingClaim in roleClaims)
{
identity.RemoveClaim(existingClaim);
}
var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
if (rolesElem is JsonElement roles)
{
if (roles.ValueKind == JsonValueKind.Array)
{
foreach (var role in roles.EnumerateArray())
{
identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
}
}
else
{
identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
}
}
}
}
return user;
}
}
In the Client app, register the factory in Program.cs:
builder.Services.AddApiAuthorization()
.AddAccountClaimsPrincipalFactory<CustomUserFactory>();
In the Server app, call AddRoles on the Identity builder, which adds role-related services:
using Microsoft.AspNetCore.Identity;
...
services.AddDefaultIdentity<ApplicationUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
API authorization options
In the Server app:
Configure Identity Server to put the name and role claims into the ID
token and access token.
Prevent the default mapping for roles in the JWT token handler.
Startup.cs (Program.cs in .NET6):
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
...
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
options.IdentityResources["openid"].UserClaims.Add("name");
options.ApiResources.Single().UserClaims.Add("name");
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
Microsofts guide says Use one of the following approaches: API authorization options or Profile Service but using a Profile Service like public class ProfileService : IProfileServic only works with Authorization Code Grant and not Resource Owner Password Credentials.
See here for more info:
https://stackoverflow.com/a/74058054/3850405
In Program.cs for ASP.NET Core 6.0 or later:
using System.Security.Claims;
...
builder.Services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
In Startup.ConfigureServices for versions of ASP.NET Core earlier than 6.0:
using System.Security.Claims;
...
services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
See my answer on seeding all kinds of data in ASP.NET Core (works in 3.1) using the IEntityTypeConfiguration
I have not tried it on blazor yet but i think it might worth the try.
Note: Changes requires a db migration.
Related
I have a Blazor client (WASM) app that integrates with AAD B2C for authentication.
After authentication, I want to call my own API for further authorisation information. The reason I want to do this rather than getting B2C to call my API is I will have series of different apps using the same B2C, with different claims, roles and other information etc.
I've tried every tutorial I can find, but nothing seems to wire up.
My Program.cs has this:
builder.Services.AddMsalAuthentication(options =>
{
var settings = config.AzureAdB2C;
var authentication = options.ProviderOptions.Authentication;
authentication.Authority = $"{settings.Instance}{settings.Domain}/{settings.SignInPolicy}";
authentication.ClientId = settings.ClientApplicationId;
authentication.ValidateAuthority = false;
options.ProviderOptions.DefaultAccessTokenScopes.Add($"{settings.ServerAppId}/{settings.DefaultScope}");
//options.ProviderOptions.Cache.CacheLocation = "localStorage";
});
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
And for example, I've tried this:
builder.Services.AddScoped<IClaimsTransformation, UserInfoClaims>();
public class UserInfoClaims : IClaimsTransformation
{
private static IEnumerable<SimpleClaim> roles;
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
...
But it doesn't get hit.
Is it possible to rewrite claims in WASM after B2C authentication?
And if not, is there an event I can wire up to after successful authentication to just manage my own role-like alternative?
This can be done by implementing your own AccountClaimsPrincipalFactory
public class ExampleClaimsPrincipalFactory<TAccount> : AccountClaimsPrincipalFactory<TAccount>
where TAccount : RemoteUserAccount
{
public ExampleClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor)
: base(accessor)
{
//Any dependency injection or construction of objects
//inside this constructor usually leads to wasm memory exceptions
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(TAccount account, RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
if (account != null)
{
//Add logic here to get custom user information
//Add Claims to the user identity like so
var identity = user.Identity as ClaimsIdentity;
identity.AddClaim(new Claim("type", "value"));
}
return user;
}
}
Then on start up when adding authentication you do the following
builder.Services.AddMsalAuthentication()
.AddAccountClaimsPrincipalFactory<ExampleClaimsPrincipalFactory<RemoteUserAccount>>();
I am trying to build a very simple playground server for me to study some ASP.NET Core authentication/authorization concepts. Basically a web app with a single, very simple controller, to be tested with Postman.
I came up with a minified version of my code, consisting of a single login endpoint which would authenticate the user (no credentials required) using Cookie Authentication, like that:
[ApiController]
public class MyController : ControllerBase
{
[HttpGet("/login")]
public async Task<IActionResult> Login()
{
var claims = new[] { new Claim("name", "bob") };
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(principal);
return Ok();
}
}
The thing is that the call to HttpContext.SignInAsync() is firing the following exception:
System.InvalidOperationException: SignInAsync when principal.Identity.IsAuthenticated is false is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true.
at Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
at MyController.Login() in C:\Users\vinic\Desktop\TEMP\TestesAuthorization\Controllers\MyController.cs:line 18
Then I tried to replace HttpContext.SignInAsync() by a call to HttpContext.AuthenticateAsync(), so that I could authenticate the user before trying to call SignInAsync() again:
[HttpGet("/login")]
public async Task<IActionResult> Login()
{
var authResult = await HttpContext.AuthenticateAsync();
if (authResult.Succeeded == false)
return StatusCode(500, "Failed to autenticate!");
return Ok();
}
But in that case the AuthenticateAsync() result always returns a failure (authResult.Succeeded = false), and later calls to HttpContext.SignInAsync() would fail with the same InvalidOperationException as before. By enabling "Trace"-level logging, the call to AuthenticateAsync() only logs the following (not very helpful) piece of information:
dbug: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[9]
AuthenticationScheme: Cookies was not authenticated.
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler: Debug: AuthenticationScheme: Cookies was not authenticated.
My project targets the net5.0 framework, has no external/explicit dependencies, and here's the Startup class I'm using:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IConfiguration configs)
{
app.UseRouting();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I know I must be missing something really basic here. I'm also not sure if the documentation I am basing myself on is actually up-to-date for .NET 5.0.
Why is the cookie authentication (HttpContext.SignInAsync() / HttpContext.AuthenticateAsync()) failing?
This was a breaking change since Asp.Net Core 3.0 Preview 6. The documentation is here https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnetcore#identity-signinasync-throws-exception-for-unauthenticated-identity, but it does not contain the motivation of the breaking change.
The real motivation is here:
https://github.com/dotnet/aspnetcore/issues/9255
In short, you need to specify auth scheme explicitly:
new ClaimsIdentity(claims, /*Explicit*/CookieAuthenticationDefaults.AuthenticationScheme)
I had the same issue, and this change helped in my case.
Just to build on Dmitriy's answer, here is a snippet of a working login (.NET 5.0, probably works for 3.0 and above):
var claims = new List<Claim>
{
// example claims from external API
new Claim("sub", userId),
new Claim(ClaimTypes.Name, username)
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var signIn = HttpContext.SignInAsync(new ClaimsPrincipal(claimsIdentity),
_userService.AuthenticationOptions(model.RememberMe));
I am evaluating using shared cookies for a set of .Net core apps. There would be one central app which would be configured to perform SAML authentication to our enterprise Okta, and would setup the appropriate cookie. Once authenticated, users would use a directory page to link out to whichever client app they wanted. The client apps would be able to read the cookie, but wouldn't be able to perform the SAML authentication loop themselves.
Both the central and client apps would have startup methods like whats shown below.
public void ConfigureServices(IServiceCollection services)
{
...
services.AddDataProtection()
.SetApplicationName("SharedCookieApps");
services
.AddAuthentication("Identity.Application")
.AddCookie("Identity.Application", options =>
{
options.Cookie.Name = ".AspNet.SharedCookie";
});
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
}
When we are doing development on the client apps, will we always need to run the 'central' app on the same machine to provide authentication? Or, can we do something to supply a fake cookie of some kind when we are doing local development? It will particularly help for devs if they can alter their roles through some local configuration.
By the way, if sharing cookies like this is a bad idea for some reason, I would be interested to hear why.
If it matters, the client apps will be a mix of .Net Core 2 and 3, Angular and Blazor.
When using ASP.NET Core Identity,Data protection keys and the app name must be shared among apps.
A common key storage location is provided to the PersistKeysToFileSystem method in the following examples.This may help.
Modification in the Startup.ConfigureServices file :
services.AddDataProtection()
.PersistKeysToFileSystem("{PATH TO COMMON KEY RING FOLDER}")
.SetApplicationName("SharedCookieApp");
services.ConfigureApplicationCookie(options => {
options.Cookie.Name = ".AspNet.SharedCookie";
});
An authentication cookie uses the HttpRequest.PathBase as its default Cookie.Path. If the app's cookie must be shared across different base paths, Path must be overridden:
services.AddDataProtection()
.PersistKeysToFileSystem("{PATH TO COMMON KEY RING FOLDER}")
.SetApplicationName("SharedCookieApp");
services.ConfigureApplicationCookie(options => {
options.Cookie.Name = ".AspNet.SharedCookie";
options.Cookie.Path = "/";
});
To Share authentication cookies between ASP.NET 4.x and ASP.NET Core apps refer link : https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-3.1#share-authentication-cookies-between-aspnet-4x-and-aspnet-core-apps
After some additional work, I came up with an option that will allow for local / offline dev. It doesn't require access to our SSO, and it allows devs to simulate any claims they want for testing.
When running in dev, I have launchSettings.json configured to use windowsAuthentication and disallow anonymousAuthentication.
Then, in dev, we use an IClaimsTransformation implementation to setup the claims.
public void ConfigureServices(IServiceCollection services)
{
...
if (Environment.IsDevelopment())
{
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddScoped<IClaimsTransformation, DevClaimsTransformer>();
}
services.AddDataProtection()
.SetApplicationName("SharedCookieApps");
services
.AddAuthentication("Identity.Application")
.AddCookie("Identity.Application", options =>
{
options.Cookie.Name = ".AspNet.SharedCookie";
});
}
Then the claims transformer is just reading from a json file that defines the claims, by type.
public class DevClaimsTransformer : IClaimsTransformation
{
private static Dictionary<string, List<DevClaim>> devRoleClaims;
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (devRoleClaims == null)
{
devRoleClaims =
JsonSerializer.Deserialize<Dictionary<string, List<DevClaim>>>(
File.ReadAllText("dev-claims.json"));
}
var identity = (ClaimsIdentity)(principal?.Identity ?? throw new ArgumentNullException(nameof(principal)));
if (identity.IsAuthenticated)
{
foreach (var rc in devRoleClaims)
{
foreach (var c in rc.Value)
{
if (c.Active)
{
identity.AddClaim(new Claim(rc.Key, c.Claim));
}
}
}
}
return new ClaimsPrincipal(identity);
}
private class DevClaim
{
public string Claim { get; set; }
public bool Active { get; set; }
}
}
I have created multiple claims that sit in the AspNetUserClaims table for identity and have assigned them to my user id.
I am currently trying to get these to pull through in the list of claims I receive in my client application.
I have managed to pull through all the roles from the AspNetUserRoles table by adding the 'roles' scope to my client identity settings and then also in identity configuration (using the EF database format a.k.a ConfigurationDbContext) created a record in the IdentityResources table which links to an identity claim called 'role'.
This is working as expected. However, I am not getting any of my UserClaims I have created through, do I need to create another specific scope?
Here is my client configuration:
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("cookie")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:44335/";
options.ClientId = "openIdConnectClient";
options.SignInScheme = "cookie";
options.ResponseType = "id_token";
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid profile roles all_claims");
});
services.AddAuthorization();
this is how I'm checking what claims the user has:
var claims = ((ClaimsIdentity)User.Identity).Claims;
and it returns all roles and profile claims (e.g. preferred_username) just not those specified within the AspNetUserClaims table.
For my client I have also set the property [AlwaysIncludeUserClaimsInIdToken] to true with no luck.
Does anyone know what I'm missing to pass through the user claims?
Do you have a IProfileService implementation to populate your custom claims?
You should implemet IProfileService as indicated in this answer.
Try other response_type than id_token since your application does not have an access token to call User Info endpoint. Maybe with id_token token to maintain the implicit flow grant of your client.
you can get the user claims like this:
var claims = User.Claims.Select(c => new { c.Type, c.Value });
you can implement this as an endpoint in your api which you stated as scope in your identity server:
using IdentityServer4;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
namespace IdentityServer4Demo.Api
{
[Route("/api/test")]
[Authorize]
public class TestController : ControllerBase
{
public IActionResult Get()
{
var claims = User.Claims.Select(c => new { c.Type, c.Value });
return new JsonResult(claims);
}
}
}
If you want to add more claim you need to add property to the class which implements IdentityUser and use it in your custom profile service
using Microsoft.AspNetCore.Identity;
namespace AuthServer.Infrastructure.Data.Identity
{
public class AppUser : IdentityUser
{
// Add additional profile data for application users by adding properties to this class
public string Name { get; set; }
}
}
your custom profile service:
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AuthServer.Infrastructure.Constants;
using AuthServer.Infrastructure.Data.Identity;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;
namespace AuthServer.Infrastructure.Services
{
public class IdentityClaimsProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<AppUser> _claimsFactory;
private readonly UserManager<AppUser> _userManager;
public IdentityClaimsProfileService(UserManager<AppUser> userManager, IUserClaimsPrincipalFactory<AppUser> claimsFactory)
{
_userManager = userManager;
_claimsFactory = claimsFactory;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
claims.Add(new Claim(JwtClaimTypes.GivenName, user.Name));
claims.Add(new Claim(IdentityServerConstants.StandardScopes.Email, user.Email));
// note: to dynamically add roles (ie. for users other than consumers - simply look them up by sub id
claims.Add(new Claim(ClaimTypes.Role, Roles.Consumer)); // need this for role-based authorization - https://stackoverflow.com/questions/40844310/role-based-authorization-with-identityserver4
context.IssuedClaims = claims;
}
}
I am using asp.net core web application, I want to restrict the login only to particular domains like #domain.com , I followed few steps involved in this video
https://www.youtube.com/watch?v=eCQdo5Njeew for google external authentication which is the older version and I followed this documentation https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?tabs=aspnetcore2x
The oauth is working but I want to restrict access to particular domain only, how to do this?
Restricting the domain upon login from OAuth is not reliable enough. I suggest you create a custom security policy as explained in Tackle more complex security policies for your ASP.NET Core app.
First, you need a requirement:
using Microsoft.AspNetCore.Authorization;
public class DomainRequirement : IAuthorizationRequirement
{
public string Domain { get; }
public DomainRequirement(string domain)
{
Domain = domain;
}
}
Then, you need a handler for this requirement:
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
public class DomainHandler : AuthorizationHandler<DomainRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
DomainRequirement requirement)
{
var emailAddressClaim = context.User.FindFirst(claim => claim.Type == ClaimTypes.Email);
if (emailAddressClaim != null && emailAddressClaim.Value.EndsWith($"#{requirement.Domain}"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
With those in place, you can add a new authorization policy in Startup.cs so that ASP.NET Core is aware of it:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthorization(options =>
{
options.AddPolicy("CompanyStaffOnly", policy => policy.RequireClaim(ClaimTypes.Email).AddRequirements(new DomainRequirement("company.com")));
});
// ...
}
Finally, you can refer to this policy when adding an [Authorize] on your controller actions:
[Authorize(Policy = "CompanyStaffOnly")]
public IActionResult SomeAction()
{
// ...
}
See also How to add global AuthorizeFilter or AuthorizeAttribute in ASP.NET Core? if you want to globally apply this policy.