authenticationcookiesoauth-2.0identityserver4openid-connect

How to Get Rid of Unwanted Issued Cookie Claims in IdentityServer4


I am using IdentityServer4 with id_token flow. At the moment, I am only storing about 2 custom claims, but my cookie is already nearing size limit 3623/4093 bytes, because apparently IdentityServer4 is injecting, if I may say, bunch of junk into the cookie (I understand some of them are necessary for the protocol, but I would like to get rid of atleast the nonce claim as it is taking up way too much space):

Cookie claims

Here is my client app configuration:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
var openIdConnectConfig = builder.Configuration.GetSection("OpenIdConnect");

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "cookies";
    options.DefaultChallengeScheme = "oidc";
})
    .AddCookie("cookies", options =>
    {
        options.Cookie.Name = "MyCookie";
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.HttpOnly = true;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    })
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = openIdConnectConfig["Authority"];
        options.ClientId = openIdConnectConfig["ClientId"];
        options.ClientSecret = openIdConnectConfig["ClientSecret"];

        //options.Scope.Clear();
        options.Scope.Add("openid");
        options.Scope.Add("profile");

        options.MapInboundClaims = false;
        options.GetClaimsFromUserInfoEndpoint = true;

        options.SaveTokens = false; // what does this even do?

        // is this the correct flow?
        options.ResponseType = "id_token";

        // these have no effect...
        options.ClaimActions.DeleteClaim("nonce");
        options.ClaimActions.DeleteClaim("aud");
        options.ClaimActions.DeleteClaim("azp");
        options.ClaimActions.DeleteClaim("acr");
        options.ClaimActions.DeleteClaim("amr");
        options.ClaimActions.DeleteClaim("iss");
        options.ClaimActions.DeleteClaim("iat");
        options.ClaimActions.DeleteClaim("nbf");
        options.ClaimActions.DeleteClaim("exp");
        options.ClaimActions.DeleteClaim("at_hash");
        options.ClaimActions.DeleteClaim("c_hash");
        options.ClaimActions.DeleteClaim("auth_time");
        options.ClaimActions.DeleteClaim("ipaddr");
        options.ClaimActions.DeleteClaim("platf");
        options.ClaimActions.DeleteClaim("ver");
        options.ClaimActions.DeleteClaim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
        options.ClaimActions.DeleteClaim("AspNet.Identity.SecurityStamp");
        options.ClaimActions.DeleteClaim("role");
        options.ClaimActions.DeleteClaim("preferred_username");
        options.ClaimActions.DeleteClaim("email_verified");

        // I added this just so `User` has `Name` property.
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name",
            RoleClaimType = "role"
        };

        options.UsePkce = true; 

        options.CallbackPath = "/signin-oidc";
        options.AccessDeniedPath = "/AccessDenied";
        
        // I got this from somewhere when IdentityServer4 was giving some error about "challenge code", not sure if it's doing anything right now.
        options.Events.OnRedirectToIdentityProvider = context =>
        {
            // only modify requests to the authorization endpoint
            if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
            {
                // generate code_verifier
                var codeVerifier = CryptoRandom.CreateUniqueId(32);

                // store codeVerifier for later use
                context.Properties.Items.Add("code_verifier", codeVerifier);

                // create code_challenge
                string codeChallenge;
                using (var sha256 = SHA256.Create())
                {
                    var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
                    codeChallenge = Base64Url.Encode(challengeBytes);
                }

                // add code_challenge and code_challenge_method to request
                context.ProtocolMessage.Parameters.Add("code_challenge", codeChallenge);
                context.ProtocolMessage.Parameters.Add("code_challenge_method", "S256");
            }

            return Task.CompletedTask;
        };
    });

And here is my auth server Startup.cs:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

var builder = services.AddIdentityServer(options =>
    {
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;

        // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
        options.EmitStaticAudienceClaim = true;
    })
    .AddInMemoryIdentityResources(Config.IdentityResources)
    .AddInMemoryApiScopes(Config.ApiScopes)
    .AddInMemoryClients(Config.Clients)
    .AddAspNetIdentity<ApplicationUser>()
    .AddProfileService<MyProfileService>();

And client config on auth server:

new Client
{
    ClientId = "kiosk_admin",
    ClientName = "Kiosk Admin",

    ClientSecrets = { new Secret("8696ba8c-b6ba-438a-9211-ee62ceea0144".Sha256()) },
    
    AllowPlainTextPkce = true,
    RequirePkce = true,

    // `id_token` flow matches `implicit` gramt type
    AllowedGrantTypes = GrantTypes.Implicit,

    // where to redirect to after login
    RedirectUris = { "https://localhost:7177/signin-oidc" },

    // where to redirect to after logout
    PostLogoutRedirectUris = { "https://localhost:7177/signout-callback-oidc" },

    FrontChannelLogoutUri = "https://localhost:7177/signout-oidc",

    AllowOfflineAccess = true,

    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        "api1"
    }
}

And lastly, here is my custom profile service, as I use user info endpoint to retrieve claims:

public class MyProfileService : IProfileService
{
    protected UserManager<ApplicationUser> _userManager;
    protected ApplicationDbContext _context;

    public MyProfileService(UserManager<ApplicationUser> userManager, ApplicationDbContext context)
    {
        _userManager = userManager;
        _context = context;
    }

    /// <summary>
    /// user to manually inject desired claims
    /// </summary>
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        //>Processing
        var user = await _userManager.GetUserAsync(context.Subject);

        if (user != null)
        {
            var claims = _context.UserClaims
                        .Where(x => x.UserId == user.Id)
                        .Select(x => new Claim(x.ClaimType, x.ClaimValue))
                        .ToList();

            claims.Add(new Claim("Name", user.UserName));

            context.IssuedClaims.AddRange(claims.AsEnumerable());
            context.IssuedClaims.RemoveAll(c => 
                c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" // we already have an email claim
                || c.Type == "preferred_username" // we already have a name claim
                || c.Type == "amr" // Authentication method reference. It tells you how the user authenticated (e.g., pwd means password). Useful for some scenarios.
                || c.Type == "idp" // Identity provider. Indicates how the user authenticated (e.g., local means they authenticated directly with your server). Useful for some scenarios.
            );
        }
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {
        //>Processing
        var user = await _userManager.GetUserAsync(context.Subject);

        context.IsActive = (user != null) && user.IsActive;
    }
}

What I need is a simple SSO solution. I have multiple applications, and I want to be able to use the same cookie to authenticate the user on all projects. Not only do I log in the users, but also I do claims-based authorization in each of my projects, where each "role" of the user is a separate claim, which is why I'm trying to make room in my cookie. Therefore, I make use of OIDC. Besides from clearing random claims, if you see any mistakes in my approach based on what I'm trying to achieve, please do let me know, too. For example, I'm not sure this is the right auth flow and response type for my use case. I will appreciate any help. Thanks in advance.


Solution

  • An alternative is to not storing the cookies in the browser at all, and instead store it in a separate SessionStore. The Cookie handler has built-in support for this.

    You can add using:

    }).AddCookie(options =>
    {
       ...     
       options.SessionStore = new MySessionStore();
    
    })
    

    Internally, this is what happens:

    enter image description here

    You can find a sample implementation in this blog post https://www.red-gate.com/simple-talk/development/dotnet-development/using-auth-cookies-in-asp-net-core/

    The store can be in memory or preferable in a database so that it can be persisted over time.

    enter image description here