IdentityServer4 GetProfileDataAsync is not called for Identity Token, only Access Token - c#

Using IdentityServer 4 (4.1.2), I've added a class implementing the IProfileService interface. The method GetProfileDataAsync is supposed to be called several times (for each token), but my method is only called for the Access Token (ClaimsProviderAccessToken).
public class LocalUserProfileService : IProfileService
{
private readonly IIdentityProviderUserService _identityProviderUserService;
public LocalUserProfileService(IIdentityProviderUserService identityProviderUserService)
{
_identityProviderUserService = identityProviderUserService ??
throw new ArgumentNullException(nameof(identityProviderUserService));
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var subjectId = context.Subject.GetSubjectId();
var claims = (await _identityProviderUserService.GetUserClaimsBySubjectAsync(subjectId)).ToList();
Debug.WriteLine($"Adding claims to {context.Caller}");
context.IssuedClaims.AddRange(claims);
}
public async Task IsActiveAsync(IsActiveContext context)
{
var subjectId = context.Subject.GetSubjectId();
context.IsActive = await _identityProviderUserService.IsUserActiveAsync(subjectId);
}
}
I could manage to only use my access token to get the custom claims, but I would like to know why the code is not called for the identity_token as I prefer to have the claims in both tokens.

Have you set the AlwaysIncludeUserClaimsInIdToken flag to true? Otherwise the claims for the ID-token will be provided through the UserInfo endpoint instead.
A common way to reduce the size of the ID-token is to not include all the claims in the ID-token. Large tokens will also result in large session cookies. By default, the tokens are stored inside the session cookie.

Related

How to prevent ClaimsTransformerService from running multiple times per user login?

I'm using windows authentication and IClaimsTransformation in my Blazor ServerApp (.NET 6.0)
I don't know why the ClaimsTransformerService runs 4 times per user login or per page refresh. Is there any way to prevent this?
The following code basically tries to check if a username is available in a database and if it is, record the login event in another table.
namespace MyServerApp.Services
{
public class ClaimsTransformerService : IClaimsTransformation
{
private IUserService _userService;
private ILoginRecordService _loginRecordService;
public ClaimsTransformerService(IUserService userService, ILoginRecordService loginRecordService)
{
_userService = userService;
_loginRecordService = loginRecordService;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var user = await _userService.GetUser(principal.Identity.Name.ToLower());
((ClaimsIdentity)principal.Identity).AddClaim(new Claim(ClaimTypes.Role, "MyUser"));
return principal;
}
}
}
I've read this thread and it didn't help.
The Microsoft documentation states that the TransformAsync() method may get called multiple times. From the docs:
The IClaimsTransformation interface can be used to add extra claims to the ClaimsPrincipal class. The interface requires a single method TransformAsync. This method might get called multiple times. Only add a new claim if it does not already exist in the ClaimsPrincipal. A ClaimsIdentity is created to add the new claims and this can be added to the ClaimsPrincipal.
So you should write your AsyncMethod defensively and only add claims if they do not already exist:
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (!principal.HasClaim(claim => claim.Type == ClaimTypes.Role && claim.Value == "MyUser"))
{
var user = await _userService.GetUser(principal.Identity.Name.ToLower());
((ClaimsIdentity)principal.Identity).AddClaim(new Claim(ClaimTypes.Role, "MyUser"));
}
return principal;
}

How to implement async IUserIdProvider for SignalR

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}";
}
}

How to get JWT token information in a Service in asp.net core api?

I want to access the token informations like claims of stored token in a service. I have tried injecting AuthorizationHandlerContext in my service. But the api can't to resolve the AuthorizationHandlerContext and throws exception. Is there any other way to access the token information inside a service?
You can inject IHttpContextAccessor to your service then get all user claims or user information from JWT like this.
Register IHttpContextAccessor into DI
services.AddHttpContextAccessor();
private readonly IHttpContextAccessor _httpContextAccessor;
public YourService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
var username =_httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
Try this code on OnAuthorization(IAuthorizationFilter) action:
string email=context.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "email").Value;
Since you want to access the JWT token informations, A more structured approach will be,
Step 1. Create a Model
public class AuthorizedUser
{
public ClaimsPrincipal Claims { get; set; }
}
Step 2. Use this model inside your authorized Controller method. like:
[Authorized]
[HttpPost]
public async Task<IActionResult> Modify([FromForm] RequestDto dto)
{
var user = new AuthorizedUser()
{
Claims = User
};
return Ok(await _service.MakeChanges(user, dto));
}
Step 3. Create an extension function for ease of access
public static string GetUserId(this ClaimsPrincipal claims)
{
return claims.Claims.Where(c => c.Type == "sub")
.Select(c => c.Value).SingleOrDefault();
}
Step 4. Simply access that users claims and other JWT properties like,
public Task<object> MakeChanges(AuthorizedUser user, RequestDto dto){
var userId = user.Claims.GetUserId();
}
That's it.

IdentityServer4 claims are not part of the token on hybrid flow

I'm trying to build a hybrid flow, and have claims on the returned access token with IdentityServer4. I'm using the QuickStart UI controlles.
In my AccountController after the user was authenticated successfully, I have the following code which signs him in:
await HttpContext.SignInAsync("anon#nymous.com", "anon#nymous.com", null, new Claim("MyName", "Ophir"));
In the MVC website that is causing this flow, on the page I want to "protect" I have the following code:
[Authorize]
public IActionResult RestrictedMvcResource()
{
var token = _httpContext.HttpContext.GetTokenAsync("access_token").Result;
var identity = User.Identity;
return View();
}
After a successful login, the debugger hits this code fine and I'm getting the access token.
The problem is that if I decode my access token (I'm using https://jwt.io/) I see the name and subject, but I do not see the MyName claim that I have defined.
(I have another flow in my system for client_credentials which does return the claims on the token - but it uses a different code flow).
How do I return the claims on the token for the hybrid flow?
EDIT:
Solving this problem was a combination of 2 things:
Implementing IProfileService as suggested in (selected) answer. here's my implementation:
public class ProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.AddRequestedClaims(context.Subject.Claims);
foreach (Claim claim in context.Subject.Claims)
{
if (context.IssuedClaims.Contains(claim))
continue;
context.IssuedClaims.Add(claim);
}
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.FromResult(0);
}
}
This will add any claim that isn't already on the token.
When calling HttpContext.SignInAsync you must pass the list of claims, otherwise no additional claims will be in the context.Subject.Claims collection.
You can implement custom IProfileService if you want to add custom claims to the token.
You can find more info in Identity Server 4 docs.
An example of simple custom profile service would be:
public class CustomProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.AddRequestedClaims(context.Subject.Claims);
context.IssuedClaims.Add(new Claim("MyName", "Ophir"));
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.FromResult(0);
}
}
Once you have this, just register it to the DI:
services.AddTransient<IProfileService, CustomProfileService>();
It will get called whenever an access_token or id_token is requested. You would need to check context.Caller as per Ruard's comment if you only wanted the extra claims in certain type of token.
EDIT:
Also alternatively, you can add the claims directly to the user configuration as per example in one of the Identity Server 4 quickstarts:
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new []
{
new Claim("MyName", "Ophir")
}
},
If you end up not implementing custom IProfileService and keep using DefaultProfileService, then you would also need add a custom IdentityResource in your configuration:
return new List<IdentityResource>
{
//..Your other configured identity resources
new IdentityResource(
name: "custom.name",
displayName: "Custom Name",
claimTypes: new[] { "MyName" });
};
Any clients wanting to have this claim added in the token would need request for custom.name scope.
AspNet.Security.OpenIdConnect.Server does not serialize claims that do not have destinations set. I ran into this when using OpenIdDict.
try this:
var claim = new Claim("MyName", "Ophir");
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken);
await HttpContext.SignInAsync("anon#nymous.com", "anon#nymous.com", null, claim);
You will probably need to add these namespaces:
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
There are two steps in which I add claims to tokens for our identityserver.
Through a custom profile service like one of the other answers shows, you can define your own claims for an user.
Those claims can then be requested through the userinfo endpoint.
Or you create an Api ( resource) called for example IncludeNameInAccessToken that adds the name claim by default to the access token if you request that Api as a scope.

Temporarily Changing Identity with WebApi 2

I have a WebApi controller that initially authenticates as a specific WebApi user. Subsequent accesses to the web api will pass a user that operations should be performed as, without having to actually authenticate as that user.
I have some services/managers that perform functions as those proper users as part of an MVC project. I now want to use those services and managers with the WebApi project, but I don't want to have to pass the user around.
I'm hoping I can temporarily change the identity of the Web Api call after the user passed in the Web Api call has been validated, but I want to make sure that when the call is complete, the cookie returned is for the validation of the WebApi user, not the end user that is represented as a part of the call.
My question is, what can I do to temporarily change the identity to the validated user in the call, and then change back to the web api identity?
Loosely using code from the links in the post, I created a IDisposable object that would temporarily change the identity.
Usage is:
try
{
using(new Impersonate(userManager, userName))
{
/* do your stuff as userName */
}
}
catch (ImpersonateException) {}
The Impersonate class is as follows:
public class Impersonate : IDisposable
{
private UserManager<ApplicationUser> userManager;
public Impersonate(UserManager<ApplicationUser> userManager, string userName)
{
this.userManager = userManager;
if (ValidateUser(userName))
{
this.ImpersonateUser(userName);
}
else
{
throw new ImpersonateException("Current user does not have permissions to impersonate user");
}
}
private bool ValidateUser(string userName)
{
/* validate that the current user can impersonate userName */
}
public void Dispose()
{
this.RevertImpersonation();
}
private void ImpersonateUser(string userName)
{
var context = HttpContext.Current;
var originalUsername = context.User.Identity.Name;
var impersonatedUser = this.userManager.FindByName(userName);
var impersonatedIdentity = impersonatedUser.GenerateUserIdentity(this.userManager, "Impersonation");
impersonatedIdentity.AddClaim(new Claim("UserImpersonation", "true"));
impersonatedIdentity.AddClaim(new Claim("OriginalUsername", originalUsername));
var impersonatedPrincipal = new ClaimsPrincipal(impersonatedIdentity);
context.User = impersonatedPrincipal;
Thread.CurrentPrincipal = impersonatedPrincipal;
}
private void RevertImpersonation()
{
var context = HttpContext.Current;
if (!ClaimsPrincipal.Current.IsImpersonating())
{
throw new ImpersonationException("Unable to remove impersonation because there is no impersonation");
}
var originalUsername = ClaimsPrincipal.Current.GetOriginalUsername();
var originalUser = this.userManager.FindByName(originalUsername);
var originalIdentity = originalUser.GenerateUserIdentity(this.userManager);
var originalPrincipal = new ClaimsPrincipal(originalIdentity);
context.User = originalPrincipal;
Thread.CurrentPrincipal = originalPrincipal;
}
}
This differs the linked code in that it only sets the identity temporarily, so doing having to SignIn/SignOut is not required.
Also, since the bulk of the work is done in the constructor, I had to remove the Async aspects that the linked code uses. There might be a way around it, but I'm not experienced enough with Async, or patient enough to bother.

Categories

Resources