Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I add a custom claim to authentication cookie generated by OpenIdConnect middleware in ASP.Net Core after authentication?

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?

like image 406
Harindaka Avatar asked May 02 '19 10:05

Harindaka


People also ask

How can add additional claims in core identity in asp net?

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.

How do I apply a claim in .NET Core?

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.


2 Answers

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:

  1. Context.AuthenticateAsync, which creates an AuthenticateResult containing both the ClaimsPrincipal and AuthenticationProperties that were read from the cookie.
  2. 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").

like image 60
Kirk Larkin Avatar answered Nov 03 '22 01:11

Kirk Larkin


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.

like image 22
Feras Taleb Avatar answered Nov 03 '22 01:11

Feras Taleb