I have an ASP.Net Core application project which uses IdentityServer4 Hybrid Auth Flow. It is setup as follows,
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
}).AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = IdentityServerUrl;
options.RequireHttpsMetadata = false;
options.ClientId = ClientId;
options.ClientSecret = ClientSecret;
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("offline_access");
options.Scope.Add("ApiAuthorizedBasedOnIdentity");
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters.NameClaimType = JwtClaimTypes.Name;
options.TokenValidationParameters.RoleClaimType = JwtClaimTypes.Role;
});
//Setup Tenant Role based authorization
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
services.AddProxy();
}
I'm able to authenticate and SaveTokens=true successfully saves the access token in the ASP.Net authentication cookie. Now I need to add a custom claim to this same authentication cookie from within a Controller Action (Not via a middleware) in my ASP.Net Core client project. Let's say the Index action of HomeController for example.
I also need this claim to persist in the authentication cookie so that it will persist across requests and controller actions.
I did some digging around and noticed that I could do this with ASP.Net Identity
if (User.Identity.IsAuthenticated)
{
var claimsIdentity = ((ClaimsIdentity)User.Identity);
if (!claimsIdentity.HasClaim(c => c.Type == "your-claim"))
{
((ClaimsIdentity)User.Identity).AddClaim(new Claim("your-claim", "your-value"));
var appUser = await userManager.GetUserAsync(User).ConfigureAwait(false);
await signInManager.RefreshSignInAsync(appUser).ConfigureAwait(false);
}
}
Authentication is done by the IdentityServer using ASP.Net Identity which is setup in that project. However to use SignInManager, UserManager, etc. in the client project I will need to bring in ASP.Net Identity into it. Setting up ASP.Net identity and stores in the client project also, just to update the authentication cookie with an additional claim seems like overkill. Is there any other way to do this?
Extend or add custom claims using IClaimsTransformationThe 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.
Claim based authorization checks are declarative - the developer embeds them within their code, against a controller or an action within a controller, specifying claims which the current user must possess, and optionally the value the claim must hold to access the requested resource.
You certainly don't need to include ASP.NET Core Identity in your client project, but you can use it for inspiration on just how to achieve what you're looking for. Let's start by looking at the implementation of RefreshSignInAsync
:
public virtual async Task RefreshSignInAsync(TUser user)
{
var auth = await Context.AuthenticateAsync(IdentityConstants.ApplicationScheme);
var authenticationMethod = auth?.Principal?.FindFirstValue(ClaimTypes.AuthenticationMethod);
await SignInAsync(user, auth?.Properties, authenticationMethod);
}
As can be seen above, this also calls into SignInAsync
, which looks like this:
public virtual async Task SignInAsync(TUser user, AuthenticationProperties authenticationProperties, string authenticationMethod = null)
{
var userPrincipal = await CreateUserPrincipalAsync(user);
// Review: should we guard against CreateUserPrincipal returning null?
if (authenticationMethod != null)
{
userPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod));
}
await Context.SignInAsync(IdentityConstants.ApplicationScheme,
userPrincipal,
authenticationProperties ?? new AuthenticationProperties());
}
The two calls we are most interested in are:
Context.AuthenticateAsync
, which creates an AuthenticateResult
containing both the ClaimsPrincipal
and AuthenticationProperties
that were read from the cookie.Context.SignInAsync
, which ends up rewriting the cookie with a ClaimsPrincipal
and associated AuthenticationProperties
.ASP.NET Core Identity creates a brand-new ClaimsPrincipal
, which is usually taken from the database, in order to "refresh" it. You don't need to do this, as you're just looking to use the existing ClaimsPrincipal
with an additional claim. Here's a complete solution for your requirements:
var authenticateResult = await HttpContext.AuthenticateAsync();
if (authenticateResult.Succeeded)
{
var claimsIdentity = (ClaimsIdentity)authenticateResult.Principal.Identity;
if (!claimsIdentity.HasClaim(c => c.Type == "your-claim"))
{
claimsIdentity.AddClaim(new Claim("your-claim", "your-value"));
await HttpContext.SignInAsync(authenticateResult.Principal, authenticateResult.Properties);
}
}
The call to HttpContext.AuthenticateAsync
will use the default scheme you've already set up in your configuration ("Cookies"
) to get access to both the ClaimsPrincipal
and the AuthenticationProperties
. After that, it's just a case of adding the new claim and performing a call to HttpContext.SignInAsync
, which will also use the default scheme ("Cookies"
).
If you don't have UserStore in your project, and I you are just using UserManager then you can do it easily by inheriting UserManager and override the GetClaimsAsync method
public override Task<IList<Claim>> GetClaimsAsync(T user)
{
// here you can return a list of claims and it will be in the cookie
return base.GetClaimsAsync(user);
}
by the way the default implementation of the base.GetClaimsAsync(user); is to call the related UserStore. so you can just drop it if you don't have UserStore with UserManager.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With