Search code examples
c#asp.net-coreasp.net-core-mvcidentityserver4

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?


Solution

  • 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").