How to make custom user claims be available in API requests - c#

I have a solution consisting of:
ASP.NET Core 2.1 running IdentityServer4 on top of ASP.NET Identity Core.
ASP.NET Core 2.1 Web API set to use the IdentityServer as the authentication provider.
A React SPA web application using oidc-client javascript library.
When I create new users I set some custom claims that are saved in the AspNetUserClaims table which looks like this:
Then, on my API project, inside a controller I want to get those user claims of the authenticated user.
I was expecting this.User.Claims to get me those, but instead that's returning the following, which seem to be claims related to the client app, not the user.
How can I access those custom user claims (address, location, tenant_role) from a controller inside the Web API project?
Bare in mind that the API project doesn't have access to the UserManager class or anything ASP.NET Identity Core related.

So, I order for my custom user claim to be available in every API request I had to do the following when setting up the ApiResource on the IdentityServer startup.
//Config.cs
public static IEnumerable<ApiResource> GetApiResources()
{
ApiResource apiResource = new ApiResource("api1", "DG Analytics Portal API")
{
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email,
AnalyticsConstants.TenantRoleClaim // my custom claim key/name
}
};
return new List<ApiResource>
{
apiResource
};
}
This method is passed to the services.AddInMemoryApiResources (or whatever storage method you're using)
IIdentityServerBuilder builder = services
.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources()) // here
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<ApplicationUser>();
With that setup, whenever an API endpoint is hit, my custom TenantRole claim is present so I can simply do User.FindFirst(AnalyticsConstants.TenantRoleClaim) to get it.

You'll need to define identity resources and scopes a la:
http://docs.identityserver.io/en/latest/topics/resources.html
And then ensure that they are exposed by your IProfileService or IClaimsService implementation in your identity server:
http://docs.identityserver.io/en/latest/reference/profileservice.html
They claims can either be included in the tokens themselves or be access via the user info endpoint as needed - this is sensible if your claim data is particularly large (i.e. in the 1000s of characters).

You can use ClaimsPrincipal.FindFirst() to access your customized claims.
Doc:
https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal.findfirst?view=netcore-2.1
Example: User.FindFirst("your_claim_key").Value

Related

claims missing using ASP.net core identity and itfoxtec with okta

I implemented the sample solution using .NET, Okta and ITFOXTEC found here.
Everything worked fine. I then tried to integrate this solution into our main codebase which is also using ASP.NET Identity. Once I added the identity configuration, it seems whatever identity that was created by ITFoxtec was overwritten (none of the SAML claims are present after login).
I reproduced the issue by adding the following to the Startup.cs in the sample solution above.
services.AddDbContext<ApplicationDbContext>();
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
I'm hoping to use both SAML and Identity framework in the same solution, but not sure they are compatible.
I'm not sure it this is possible, I have never tried it. However, I think it is possible.
It sounds like the received SAML 2.0 identity is overridden and thereby also the claims.
Please share your findings if you succeed.
I know this is a little late but we have got SAML working with ASP.NET Identity in an ASP.NET MVC Core 6 application. We used the same sample mentioned above and did this in the CreateClaimsPrincipal method of the ClaimsTransform class:
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using ITfoxtec.Identity.Saml2.Claims;
...
// add the ASP.NET Identity roles as Claims
var idClaim = GetClaim(incomingPrincipal, Saml2ClaimTypes.NameId);
if (idClaim != null)
{
IdentityUser identityUser = await userManager.FindByEmailAsync(idClaim.Value);
if (identityUser != null)
{
foreach (var r in await userManager.GetRolesAsync(identityUser))
{
claims.Add(new Claim(type: ClaimTypes.Role, value: r));
}
}
}
We probably should make ClaimsTransform non static and inject the UserManager<IdentityUser> userManager but right now it's being passed in as a parameter from the call in AuthController.
After this normal ASP.NET Identity stuff worked. Authorize attributes on Controllers and Actions:
[Authorize(Roles = "Identity Role 1, Identity Role 2")]
And User.IsInRole("Identity Role 1") checks in code and .cshtml
And in program.cs we do the SAML configuration last - just before var app = builder.Build();
The only other thing that you might want to do is prevent access to some/all of the default Identity pages. We did this in program.cs to prevent users accessing the Registration page but you can also scaffold the identity pages and edit them as Microsoft suggests here.
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/Identity/Account/Register", context => Task.Factory.StartNew(() => context.Response.Redirect("/", true, true)));
endpoints.MapPost("/Identity/Account/Register", context => Task.Factory.StartNew(() => context.Response.Redirect("/", true, true)));
}

Accessing user information via `IHttpContextAccessor` from project created with `dotnet new react -au Individual`?

Background
I've been following the documentation for using IdentityServer4 with single-page-applications on ASP.NET-Core 3.1 and as such created a project via the dotnet new react -au Individual command.
This creates a project which uses the Microsoft.AspNetCore.ApiAuthorization.IdentityServer NuGet package.
So far it's been really great and it got token-based authentication for my ReactJS application working without any pain!
From my ReactJS application, I can access the user information populated by the oidc-client npm package such as the username.
Also, calls to my Web APIs with the [Authorize] attribute work as expected: only calls with a valid JWT access token in the request header have access to the API.
Problem
I'm now trying to access basic user information (specifically username) from within a GraphQL mutation resolver via an injected IHttpContextAccessor but the only user information I can find are the following claims under IHttpContextAccessor.HttpContext.User:
nbf: 1600012246
exp: 1600015846
iss: https://localhost:44348
aud: MySite.HostAPI
client_id: MySite
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: (actual user GUID here)
auth_time: 1600012235
http://schemas.microsoft.com/identity/claims/identityprovider: local
scope: openid
scope: profile
scope: MySite.HostAPI
http://schemas.microsoft.com/claims/authnmethodsreferences: pwd
The same issue happens for Web API controllers as well.
Details
MySite is the namespace of my solution and is also what I have defined as a client in my appsettings.json file:
{
"IdentityServer": {
"Clients": {
"MySite": {
"Profile": "IdentityServerSPA"
}
}
}
}
My web application project's name is MySite.Host so MySite.HostAPI the name of the resource and scope that are automatically generated by calling AuthenticationBuilder.AddIdentityServerJwt().
... this method registers an <<ApplicationName>>API API resource with IdentityServer with a default scope of <<ApplicationName>>API and configures the JWT Bearer token middleware to validate tokens issued by IdentityServer for the app.
Research
According to a few answers on Stack Overflow, adding IdentityResources.Profile() resource via IIdentityServerBuilder.AddInMemoryIdentityResources() should do the trick but it looks like it's already available via the claims I posted above (scope: profile).
I nevertheless tried it but the result is that the authentication flow becomes broken: the redirect to the login page does not work.
All of the answers I've found also make a reference to a Config class like in this demo file which holds configurations that are mainly fed to IIdentityServerBuild.AddInMemory...() methods.
However, it seems that Microsoft.AspNetCore.ApiAuthorization.IdentityServer does most of this in its implementation and instead offers extendable builders to use.
From the IdentityServer documentation, I don't believe I need to add a Client because the access token already exists. The client ReactJS application uses the access_token from oidc-client to make authorised calls to my Web APIs.
It also doesn't appear like I need to add a Resource or Scope for the username information because I believe these already exist and are named profile. More to this point is that the documentation for "IdentityServerSPA" client profile states that:
The set of scopes includes the openid, profile, and every scope defined for the APIs in the app.
I also looked at implementing IProfileService because according to the documentation this is where additional claims are populated. The default implementation is currently being used to populate the claims that are being requested by the ProfileDataRequestContext.RequestedClaimTypes object and this mechanism already works because this is how the ReactJS client code receives them. This means that when I'm trying to get the user claims from ASP.NET-Core Identity, it's not properly populating ProfileDataRequestContext.RequestedClaimTypes or perhaps not even calling IProfileServices.GetProfileDataAsync at all.
Question
Considering that my project uses Microsoft.AspNetCore.ApiAuthorization.IdentityServer, how can I view the username from my ASP.NET-Core C# code, preferably with IHttpContextAccessor?
What you need to do is to extend the default claims requested by IdentityServer with your custom ones. Unfortunately, since you're using the minimalistic IdentityServer implementation by Microsoft, the correct way of making the client request the claims isn't easy to find. However, assuming you have only one application (as per the template), you could say that the client always wants some custom claims.
Very important first step:
Given your custom IProfileService called, say, CustomProfileService, after these lines:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
you have to get rid of the implementation used in the scaffolded template, and use your own:
services.RemoveAll<IProfileService>();
services.AddScoped<IProfileService, CustomProfileService>();
Next, the actual implementation of the custom IProfileService isn't really hard if you start from Microsoft's version:
public class CustomProfileService : IdentityServer4.AspNetIdentity.ProfileService<ApplicationUser>
{
public CustomProfileService(UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory) : base(userManager, claimsFactory)
{
}
public CustomProfileService(UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
ILogger<ProfileService<ApplicationUser>> logger) : base(userManager, claimsFactory, logger)
{
}
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
string sub = context.Subject?.GetSubjectId();
if (sub == null)
{
throw new Exception("No sub claim present");
}
var user = await UserManager.FindByIdAsync(sub);
if (user == null)
{
Logger?.LogWarning("No user found matching subject Id: {0}", sub);
return;
}
var claimsPrincipal = await ClaimsFactory.CreateAsync(user);
if (claimsPrincipal == null)
{
throw new Exception("ClaimsFactory failed to create a principal");
}
context.AddRequestedClaims(claimsPrincipal.Claims);
}
}
With those two steps in place, you can start tweaking CustomProfileService's GetProfileDataAsync according to your needs. Notice that ASP.NET Core Identity by default already has the email and the username (you can see these in the claimsPrincipal variable) claims, so it's a matter of "requesting" them:
// ....
// also notice that the default client in the template does not request any claim type,
// so you could just override if you want
context.RequestedClaimTypes = context.RequestedClaimTypes.Union(new[] { "email" }).ToList();
context.AddRequestedClaims(claimsPrincipal.Claims);
And if you want to add custom data, for example, the users first and last name:
// ....
context.RequestedClaimTypes = context.RequestedClaimTypes.Union(new[] { "first_name", "last_name" }).ToList();
context.AddRequestedClaims(claimsPrincipal.Claims);
context.AddRequestedClaims(new[]
{
new Claim("first_name", user.FirstName),
new Claim("last_name", user.LastName),
});
User information can be retrieved via the scoped UserManager<ApplicationUser> service which is set up by the project template. The users's claims contains "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" (ClaimTypes.NameIdentifier) whose value is the user identifier. UserManager<>.FindByIdAsync() can then be used to retrieve the ApplicationUser associated with the user and which contains additional user information.
Note that this contacts the user store each time it's invoked. A better solution would be to have the extra user information in the claims.
First, explicitly add the IHttpContextAccessor service if you haven't already by calling services.AddHttpContextAccessor();
From within an arbitrary singleton service:
public class MyService
{
public MyService(
IHttpContextAccessor httpContextAccessor,
IServiceProvider serviceProvider
)
{
var nameIdentifier = httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
using (var scope = serviceProvider.CreateScope())
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var user = await userManager.FindByIdAsync(nameIdentifier);
// Can access user.UserName.
}
}
}
UserManager<ApplicationUser> can be accessed directly within Razor pages and Controllers because these are already scoped.

.NET Core does not show user claims in middleware when authenticating with Azure AD

I have a multi-tenant .NET Core web app where the current user's tenant is resolved via middleware. In particular, tenants are resolved with a library called SaasKit.Multitenancy.
To use this library, you put this line in ConfigureServices():
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// (omitted for brevity)
// The 'Tenant' type is what you resolve to, using 'ApplicationTenantResolver'
services.AddMultitenancy<Tenant, ApplicationTenantResolver>();
// ...
}
And you put this line in Configure() to add it to the middleware pipeline:
public void Configure(IApplicationBuilder app)
{
// ...
app.UseAuthentication();
app.UseMultitenancy<Tenant>(); //this line
app.UseMvc(ConfigureRoutes);
// ...
}
This causes the following method in the middleware to be executed, which resolves the current user's tenant:
public async Task<TenantContext<Tenant>> ResolveAsync(HttpContext context)
{
//whatever you need to do to figure out the tenant goes here.
}
This allows the result of this method (whichever tenant is resolved) to be injected into any class you want, like so:
private readonly Tenant _tenant;
public HomeController(Tenant tenant)
{
_tenant = tenant;
}
Up until now, we have been authenticating users with the .NET Identity platform, storing user data in our app's database. However, a new tenant of ours wants to be able to authenticate their users via SSO.
I have figured out most of the SSO stuff--I am using Azure AD to sign in users, and my organization's Azure AD tenant will be able to federate with their Identity Provider. In short, this code in ConfigureServices adds the Identity and AzureAD authentication:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// rest of the code is omitted for brevity
services.AddIdentity<ApplicationUser, ApplicationRole>(config =>
{
config.User.RequireUniqueEmail = true;
config.Password.RequiredLength = 12;
}).AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => _configuration.Bind("AzureAd", options)).AddCookie();
// policy gets user past [Authorize] if they are signed in with Identity OR Azure AD
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder(
AzureADDefaults.AuthenticationScheme,
IdentityConstants.ApplicationScheme
).RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
}
When using Identity, I have been able to resolve the users's tenant with the UserManager, like so:
public async Task<TenantContext<Tenant>> ResolveAsync(HttpContext context)
{
TenantContext<Tenant> tenantContext = new TenantContext<Tenant>(new ApplicationTenant());
if (context.User.Identity.IsAuthenticated)
{
var user = await _userManager.Users
.Include(x => x.Tenant)
.FirstOrDefaultAsync(x => x.UserName == email);
if (user?.Tenant != null)
{
tenantContext = new TenantContext<Tenant>(user.Tenant);
_logger.LogDebug("The current tenant is " + user.Tenant.Name);
return await Task.FromResult(tenantContext);
}
}
return await Task.FromResult(tenantContext);
}
My plan was to modify this code so grabbed the current User's claims, which can be used to infer which tenant the user belongs to. However, when authenticating a user via Azure AD, HttpContext.User is always empty in the middleware, despite the user being signed in. It's not null, but HttpContext.User.Identity.IsAuthenticated is always false and HttpContext.User.Claims is empty. I only see the value of HttpContext.User populated once routing is complete and the code has reached a Controller.
I have tried reorganizing the middleware in pretty much every feasible way to no avail. What's confusing to me is that HttpContext.User is populated in the tenant resolver when the user is authenticated with Identity. With this in mind, I'm not sure how I can access the user's claims in the middleware when authenticating via Azure AD.
The best solution I can think of is to modify every instance the current tenant is injected into the code with a call to a method that resolves the tenant via claims. If the tenant is null in an area restricted with the [Authorize] attribute, it would imply the user is signed in via Azure AD, which would allow me to look at their claims. However, it really bothers me that I can't access the user's claims in the middleware, as I'm not sure what's really going on here.
Since you are trying to access HttpContext from a custom component you are going to want to add HttpContextAccessor to your service collection as follows:
services.AddHttpContextAccessor();
You can now resolve HttpContext as needed using dependency injection. This may or may not be helpful depending on how much control you have of the middleware that you are using.
For what it's worth, I've had little issue authenticating against AAD just using MSAL without additional third-party middleware. Good luck!
I suspect you might be running on .NET Core 2.0 and running into this issue.

Checking for additional privileges with policy based authorization in ASP.NET Core 2.1

I am using ASP.NET Core 2.1 with policy based authorization against Active Directory groups configured in appsettings.json. I have several calls to configure each policy similar to the following:
services.AddAuthorization(options =>
{
options.AddPolicy("UserGroup"), policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole(jsonAuthSection.UserGroup);
});
});
I decorate various API controllers with different policies as needed as follows:
[Authorize("UserGroup")]
However, I now have models where some properties require administrative membership to change. To facilitate this, I use nullable types and when the property is bound (implying a change / value was specified) I need to check for additional permissions:
AuthorizationResult isAdmin = await this.authorizationService.AuthorizeAsync(this.User, "AdminGroup");
if (updateModel.Locked != null && !isAdmin.Succeeded)
{
return this.StatusCode(StatusCodes.Status401Unauthorized, "some message");
}
// More checks...
Users are prompted for credentials instead of simply failing. What is a more appropriate strategy for accomplishing this?
You should be returning 403 - Forbidden.

Overload WEB API Token Controller

Visual Studio provides a nice ready template for ASP.NET WEB API project. There we have a set of account management functions dealing with ASP.NET Identity. However, one most fundamental function is missing both from automatically generated controllers and documentation. Namely, "~/Token" URL, which is used to grant WEB API access tokens is not mentioned anywhere.
I would like to write a custom controller to intercept all "~/Token" calls to make some logging and additional processing in a way similar to other WEB API controllers. How can I do it in a simple and natural way?
It seems you need OWIN OAuth 2.0 Authorization Server. This is the Microsoft extension to add the required functionality. It creates an oauth endpoint (e.g. /token) that you can use to get a token. You don't have a controller directly, but there is a special OWIN class connected to it that you will need to extend to add whatever you need.
You can find more details here and here.
It's a bit long reading, but it works and I have used it in a few projects.
Here is a simple example how you can do it (GrantResourceOwnerCredentials is the most important method for you):
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Add CORS e.g.
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
using (AuthRepository _repo = new AuthRepository())
{
IdentityUser user = await _repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "user"));
context.Validated(identity);
}
}

Categories