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;
}
}
Related
I am trying to build a login system combining Microsoft.Identity and AzureAD login integration. Currently, the user is able to login with their Microsoft account through the AD portal, but during this process my customized UserClaimsPrincipalFactory is never reached (I've checked with breakpoints during debugging with Visual Studio 2022, and checked the data from the results of the login).
My current setup is as following:
builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<IdentityRole>()
.AddUserManager<UserManager<ApplicationUser>>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddRoleStore<RoleStore<IdentityRole, UserAppContext>>()
.AddClaimsPrincipalFactory<ApplicationUserClaimsFactory>()
.AddEntityFrameworkStores<UserAppContext>();
I have also tried to not use "AddClaimsPrincipalFactory" or any add function besides "AddEntityFrameworkStores()" and initialize them through AddScoped instead, but the result remains the same.
My UserClaimsPrincipalFactory looks as following
public class ApplicationUserClaimsFactory: UserClaimsPrincipalFactory<ApplicationUser>
{
public ApplicationUserClaimsFactory(
UserManager<ApplicationUser> userManager,
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, optionsAccessor)
{
}
public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
var identity = (ClaimsIdentity)principal.Identity;
var claims = new List<Claim>
{
new Claim("Test", "true")
};
identity.AddClaims(claims);
return principal;
}
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
{
ClaimsIdentity claims = await base.GenerateClaimsAsync(user);
claims.AddClaim(new Claim("name", "Aris"));
claims.AddClaim(new Claim("Test", "true"));
return claims;
}
}
Nothing I've tried using UserClaimsPrincipalFactory works. I've been able to assign new claims using the IClaimsTransformation interface, but I want to be able to pull data from a database and use that data to determine which claims to assign a given user.
I have an authentication setup where I store a session ID and an unverified user ID in my claims. Then, in a normal controller, I would look up the session in my DB to verify that it matches the userID and consider the user logged in.
I am trying to use Azure SignalR. I want to be able to send messages to connected users by userID, and I need to implement IUserIdProvider. The GetUserId method on it isn't async, but what I need to do is perform the same logic, which verifies the session ID and userID in the claims against the database before it considers the user valid. This code would be async, but the GetUserId method isn't async.
What options do I have?
Thanks!
I believe the reasoning behind IUserIdProvider.GetUserId(HubConnectionContext) not being async is that calls to the DB or other external resource would occur earlier in the request pipeline. This could include operations like mapping an external user id or session id to an internal user id.
The way I solved a similar problem was to place my DB lookups in an IClaimsTransformation implementation which stored the values I looked up in Claims on the User that flows through the request.
public static class ApplicationClaimTypes
{
public static string UserId => "user-id";
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Identity.Web;
class MsalClaimsTransformation : IClaimsTransformation
{
// IMapExternalUsers represents the actions you must take to map an external id to an internal user id
private readonly IMapExternalUsers _externalUsers;
private ClaimsPrincipal _claimsPrincipal;
public MsalClaimsTransformation(IMapExternalUsers externalUsers)
{
_externalUsers = externalUsers;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal claimsPrincipal)
{
_claimsPrincipal = claimsPrincipal;
// This check is important because the IClaimsTransformation may run multiple times in a single request
if (!claimsPrincipal.HasClaim(claim => claim.Type == ApplicationClaimTypes.UserId))
{
var claimsIdentity = await MapClaims();
claimsPrincipal.AddIdentity(claimsIdentity);
}
return claimsPrincipal;
}
private async Task<ClaimsIdentity> MapClaims()
{
var externalIds = new[]
{
// Extensions from Microsoft.Identity.Web
_claimsPrincipal.GetHomeObjectId(),
_claimsPrincipal.GetObjectId()
}
.Distinct()
.Where(id => !string.IsNullOrWhiteSpace(id))
.ToArray();
// Replace with implementation specific to your use case for mapping session/external
// id to authenticated internal user id.
var userId = await _externalUsers.MapExternalIdAsync(externalIds);
var claimsIdentity = new ClaimsIdentity();
AddUserIdClaim(claimsIdentity, userId);
// Add other claims as needed
return claimsIdentity;
}
private void AddUserIdClaim(ClaimsIdentity claimsIdentity, Guid? userId)
{
claimsIdentity.AddClaim(new Claim(ApplicationClaimTypes.UserId, userId.ToString()));
}
}
Once you have your internal user id stored in the User object as a claim it is accessible to SignalR's GetUserId() method without the need for async code.
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
internal class SignalrUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
var httpContext = connection.GetHttpContext();
// If you have a multi-tenant application, you may have an
// extension method like this to get the current tenant the user
// is in. Otherwise just remove it.
var tenantIdentifier = httpContext.GetTenantIdentifier();
var userId = connection.User.FindFirstValue(ApplicationClaimTypes.UserId);
if (string.IsNullOrWhiteSpace(userId))
{
return string.Empty;
}
return $"{tenantIdentifier}-{userId}";
}
}
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.
My problem:
[Authorize(Roles = "L1")] and User.IsInRole("L1") are looking for the claim name "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" instead of "role"
In concrete:
I have created an IdentityServer4 with the standard identity database by the following steps (http://docs.identityserver.io/en/release/quickstarts/6_aspnet_identity.html):
Create new Project
ASP.NET Core Web Application(.NET Core)
ASP.NET Core 1.1
Web Application
Change Authentication
Individual User Accounts
Add IdentityServer4.AspNetIdentity + Config.cs + ...
Then I have created a MVC-Client. The Authentication works fine. I get also a list of claims.
I am using the tables AspNetUsers, AspNetUserRoles and AspNetRoles in for the IdentityServer4 to configure role. The roles are added to the claims with the claim name "role".
If I try to authorize my controller actions in the MVC-Client it seems that the claim name for my roles is wrong.
How can I solve the conflict? Do I have to map "role" to "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"?
Here is my controller in the MVC-Client:
[Route("api/[controller]")]
public class CompaniesController : Controller
{
[HttpGet]
//[Authorize(Roles = "L1")] // This looks for the claim http://schemas.microsoft.com/ws/2008/06/identity/claims/role instead of role
public async Task<IEnumerable<Company>> GetCompaniesAsync()
{
var c = User.Identities.Count(); // 1
var nameOfExptectedRoleClaimType = User.Identities.First().RoleClaimType; // http://schemas.microsoft.com/ws/2008/06/identity/claims/role
var b0 = User.HasClaim(nameOfExptectedRoleClaimType, "L1"); // false
var b1 = User.HasClaim("role", "L1"); // true
var b2 = User.IsInRole("L1"); // false; looks for claim http://schemas.microsoft.com/ws/2008/06/identity/claims/role; used by [Authorize(Roles = "L1")]
var companies = await _crmApi.GetCompaniesAsync();
return companies;
}
}
I have found this answer (https://stackoverflow.com/a/34226538/272357) but I do not know how to "register" CustomPrinciple.
I have found an answer for myself.
I have to mention that I am using
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
to avoid the renaming of the claim names (see the code).
The resulting problem was that ClaimsPrinciple still looks for roles named with "http://schemas.microsoft.com/ws/2008/06/identity/claims/role".
This could be fixed indirectly with OpenIdConnectOptions.TokenValidationParameters.
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// ...
// Avoid claim mapping to old ms soap namespaces. Avoid replace "role" by "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
// ...
// https://leastprivilege.com/2016/08/21/why-does-my-authorize-attribute-not-work/
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role", // The role claim type is named "role" instead of "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
}
});
}
}
I am in the process of creating my own webapi in asp.net using oauth as authorization provider.
The api wil basically serve as a provider for different modules as i call them. One could be a image gallery, the other could just be a user login module with different types of users.
I have the oauth part working fine. Api users can register and then ask for a Token by calling the /Token endpoint with the login credentials.
However i now want to create another seperate user module in the api that is only accessible by apiusers that registered . I want this module to have another register and login function and have their own endpoint to login (/UserModuleToken or something like that). The users coming from the user module are different users than the Api users. So the apiusers are the actual developers that want to call specific modules in my api, and the users from the user module are users that register on the site where that module is implemented.
All of my apicontrollers wil have the [Authorize] attribute for the api user, and i want specific ones, for example some function in the user module, to be decorated with [UserModuleAuthorize] attribute.
Below you can see my api user entity model:
public class ApiUserEntity : BaseEntity
{
public string Username { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string Salt { get; set; }
public ApiUserLevel Level { get; set; }
}
The userservice function that can validate an api user:
public UserLoginResult LoginUser(ApiUserEntityLoginForm userForm)
{
// retrieve user from database
var user = _userRepository.GetUser(userForm.UserName);
if(user == null)
return _modelStateWrapper.AddError(UserLoginResult.UserNotFound, "User does not exist");
var passwordHash = PasswordHash.HashPassword(user.Salt, userForm.Password);
// check if password matches with database.
if (passwordHash != user.Password)
return _modelStateWrapper.AddError(UserLoginResult.IncorrectPassword, "Incorrect password");
return UserLoginResult.Success;
}
And calling the /Token endpoint in my webapi will call the following function of the token provider:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
// create a userloginform object :
var loginForm = new ApiUserEntityLoginForm {UserName = context.UserName, Password = context.Password};
// pass it into the login validation function of the userservice:
var loginResult = _userService.LoginUser(loginForm);
// if login result was not sucesful, return an error.
if (loginResult != UserLoginResult.Success)
{
var jsonSerialiser = new JavaScriptSerializer();
var json = jsonSerialiser.Serialize(_userService.Errors());
context.SetError("invalid_grant", json);
return;
}
// result was succesful, grant the token.
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "user"));
context.Validated(identity);
}
i configure my oauth provider and define the /Token endpoint with the following function:
public static void ConfigureOAuth(IAppBuilder app, IUnityContainer container)
{
var simpleAuthorizationServerProvider = container.Resolve<SimpleAuthorizationServerProvider>();
var OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = simpleAuthorizationServerProvider
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
Now my question is if it is somehow possible to have multiple token endpoints so i can have a token for apiusers and then another one for a user that is using the custom user module and protect certain functionality based on those 2 users.
I couldnt find any information about this after an extensive amount of searching the internet. So im beginning to believe this is not good practice or not possible. If anyone would be able to point me in the right direction that would be great!
Well I believe you need to configure users authorization based on Roles, what you are trying to do is just complicating your solution.
What you can do is the following: inside method GrantResourceOwnerCredentials you need to obtain the correct role(s) for the authenticated user from the DB store i.e "Admin" and then add them as claims with type "Role" as the code below:
identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
identity.AddClaim(new Claim(ClaimTypes.Role, "Supervisor"));
Now on your controllers that you want just user with role "Admin" to access; you need to attribute with [Authorize(Roles="Admin")] or maybe multiple roles [Authorize(Roles="Admin,User")]
This is the straightest way to achive your goal.
Btw this code from http://bitoftech.net, right? Glad to see my code samples used :)
Let me know if you need further clarifications.