Short version: Is there a way to determine the content of a cookie that’s created by the CookieAuthentication mechanism in .net-Core-2
Context:
I’m currently working on making a site GDPR compliant, among other things this means documenting all the cookies used and set on our site.
We use asp.net core 2 and Identity to handle user log in and log out. The problem is all the cookies sett by identity and how to document them.
Cookie 1.
This cookie is straight forward, it contains login information, privileges etc. etc. If you delete it, you will be logged out from our site. It’s critical to our application and thereby nothing to worry about.
The problem is the following cookie (Note. it’s one cookie split into three parts)
It's used and set by the cookieAuthentication but dont realy seem to do anything. You can delete them from the browser and still use the site seemingly fine. Making changes and save i for example, and ou wont be forcefully logged out.
This poses a problem because according to the new GDPR law, if these cookie chunks are optional the user should be able to deactivate them. (A problem for later)
The question then becomes: How can I find out what this cookie does and what information it contains. Both are equally important to find out.
Tried methods: I’ve tried to look through the source code to find answers, but the project development has been on and off for the last 2 years and is thereof quite the patchwork. I’ve also tried to decrypt the cookie using this post How to manually decrypt an ASP.NET Core Authentication cookie? as guide, but I’ve only been developing in C# for less than 4 months so I could not make it work since it seems to target an older/different version of .net-core.
Note! Since this is my first question on stack I welcome suggestions and tips on how I can make my question clearer or more useful to the general populace of this site.
Edit_1: Well there realy is'nt much config in the startupfile
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true
});
There is some code to enable OpenId with microsoft. But would that be put in this cookie?
// Configure the OWIN Pipeline to use OpenId Connect Authentication
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["ClientId"],
Authority = string.Format(Configuration["AadInstance"],
Configuration["Tenant"]),
PostLogoutRedirectUri = Configuration["PostLogoutRedirectUri"],
ClientSecret = Configuration["ClientSecret"],
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
},
Events = new OpenIdConnectEvents
{
OnAuthenticationFailed = OnAuthenticationFailed,
OnRedirectToIdentityProvider = (context) =>
{
if (context.HttpContext.Request.Query.ContainsKey("domain"))
{
context.ProtocolMessage.DomainHint =
context.HttpContext.Request.Query["domain"];
}
return Task.FromResult(0);
},
OnTokenValidated = OnTokenValidated
}
});
edit_2: Removed DI and misc logging but this is basicaly the whole file
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Serialization;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
public class Startup
{
public UserManager<ApplicationUser> UserManager { get; set; }
public SignInManager<ApplicationUser> SignInManager { get; set; }
public IConfigurationRoot Configuration { get; set; }
public Startup(IHostingEnvironment env)
{
// Set up configuration sources.
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsDevelopment())
{
// For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
builder.AddUserSecrets<Startup>();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Adds a default in-memory implementation of IDistributedCache
services.AddMemoryCache();
services.AddSession();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("DefaultConnection")));
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddIdentity<ApplicationUser, IdentityRole>(o => {
// configure identity options
o.Password.RequireDigit = true;
o.Password.RequireLowercase = true;
o.Password.RequireUppercase = true;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, options => options.ResourcesPath = "Resources")
.AddDataAnnotationsLocalization()
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
});
// OpenID Connect Authentication Requires Cookie Auth
services.AddAuthentication(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public async void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
SampleDataInitializer sampleData,
SeedData seedData)
{
app.UseSession();
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
try
{
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>()
.CreateScope())
{
serviceScope.ServiceProvider.GetService<ApplicationDbContext>()
.Database.Migrate();
}
}
catch { }
}
else
{
app.UseExceptionHandler("/Home/Error");
// For more details on creating database during deployment see http://go.microsoft.com/fwlink/?LinkID=615859
try
{
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>()
.CreateScope())
{
serviceScope.ServiceProvider.GetService<ApplicationDbContext>()
.Database.Migrate();
}
}
catch { }
}
app.UseStaticFiles();
app.UseIdentity();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true
});
// Configure the OWIN Pipeline to use OpenId Connect Authentication
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["ClientId"],
Authority = string.Format(Configuration["AadInstance"], Configuration["Tenant"]),
PostLogoutRedirectUri = Configuration["PostLogoutRedirectUri"],
ClientSecret = Configuration["ClientSecret"],
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
},
Events = new OpenIdConnectEvents
{
OnAuthenticationFailed = OnAuthenticationFailed,
OnRedirectToIdentityProvider = (context) =>
{
if (context.HttpContext.Request.Query.ContainsKey("domain"))
{
context.ProtocolMessage.DomainHint = context.HttpContext.Request.Query["domain"];
}
return Task.FromResult(0);
},
OnTokenValidated = OnTokenValidated
}
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
await seedData.SeedBuyOutPoliciesForDepartments();
}
private async Task<Task> OnTokenValidated(TokenValidatedContext context)
{
try
{
// Get tenant Id from Claims
var claim = context.Ticket.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid");
if (claim?.Value == null)
{
context.HttpContext.Session.SetError("Not able to access tenant id from claim");
return Task.FromResult(true);
}
var tenantId = claim.Value;
var roleClaims = context.Ticket.Principal.Claims.Where(c => c.Type.ToLower().EndsWith("identity/claims/role")).ToList();
//// Get group claims from logged in user
var groupClaims = context.Ticket.Principal.Claims.Where(c => c.Type.ToLower() == "groups").ToList();
var checkClaim = context.HttpContext.Session.GetString("CheckClaim");
if (!string.IsNullOrEmpty(checkClaim))
{
context.HttpContext.Session.Remove("CheckClaim");
var claimInfo = "";
foreach (var currentUserClaim in context.Ticket.Principal.Claims)
{
claimInfo += $"Type: {currentUserClaim.Type} Value: {currentUserClaim.Value} <br/>";
}
// Add groups to claimInfo string
claimInfo += "<br/>Groups: <br/>";
foreach (var groupClaim in groupClaims)
{
claimInfo += $"{groupClaim.Value}, ";
}
// Add role(s) to claimInfo string
claimInfo += "<br />Roles: <br />";
foreach (var roleClaim in roleClaims)
{
claimInfo += $"{roleClaim.Value}, ";
}
context.HttpContext.Session.SetString("Claim", claimInfo);
return Task.FromResult(true);
}
var applicationDbContext = context.HttpContext.RequestServices.GetService<ApplicationDbContext>();
// Check that tenant id exist in database
var customer = applicationDbContext.Customers.Include(c => c.Departments).FirstOrDefault(x => x.TenantId == tenantId);
if (customer == null)
{
context.HttpContext.Session.SetError("Your company is not configured to use log in with work account");
return Task.FromResult(true);
}
// Used to find user in database or create new user
var userManager = context.HttpContext.RequestServices.GetService<UserManager<ApplicationUser>>();
//// Used to update user role
var roleManager = context.HttpContext.RequestServices.GetService<RoleManager<IdentityRole>>();
// Get email adress from AD user
var email = context.Ticket.Principal.Identity.Name;
// Check if user exist in database
var user = await applicationDbContext.Users.Include(u => u.Department).FirstOrDefaultAsync(u => u.Email == email);
if (user == null)
{
if (!customer.AllowCreateUserOnSignIn)
{
context.HttpContext.Session.SetError(
"Your user does not exist in the portal and your company is not configured to allow creation of user on sign in. Please contact your administrator.", customer.ErrorText);
return Task.FromResult(true);
}
// Create new user with AdUser property set to true
var givenName =
context.Ticket.Principal.Claims.FirstOrDefault(
x => x.Type.ToLower().Contains("givenname"));
var surName =
context.Ticket.Principal.Claims.FirstOrDefault(
x => x.Type.ToLower().Contains("surname"));
user = new ApplicationUser
{
AdUser = true,
UserName = email,
Email = email,
FirstName = givenName != null ? givenName.Value : "",
LastName = surName != null ? surName.Value : "",
};
if (user.DepartmentId == null)
{
user.DepartmentId = customer.Departments.FirstOrDefault(d => d.Default)?.Id;
}
// Add user to database
var result = await userManager.CreateAsync(user);
if (!result.Succeeded)
{
context.HttpContext.Session.SetError(
"Your user does not exist in the portal. Please contact your administrator", customer.ErrorText);
return Task.FromResult(true);
}
}
else
{
//Not allowed to login if user is deactivated/resigned
if (user.ResignedDate != null)
{
context.HttpContext.Session.SetError("Your user is deactivated. Please contact your administrator", customer.ErrorText);
return Task.FromResult(true);
}
}
// Compare tenant id with user tenant
var userCustomer = await applicationDbContext.Customers.FirstOrDefaultAsync(c => c.Id == user.Department.CustomerId);
if (userCustomer == null || userCustomer.TenantId != tenantId)
{
context.HttpContext.Session.SetError("Your user does not exist in the portal. Please contact your administrator", customer.ErrorText);
return Task.FromResult(true);
}
// Sign in user
var signInManager = context.HttpContext.RequestServices.GetService<SignInManager<ApplicationUser>>();
await signInManager.SignInAsync(user, true);
}
catch (Exception ex)
{
context.HttpContext.Session.SetError("Sorry, an error occured during log in. Please contact your administrator");
}
return Task.FromResult(0);
}
// Entry point for the application.
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
You get a chunked cookie if the Principal that is being stored is too big for a single cookie (think lots of roles, or claims). Cookies have a limited size, so chunking will kick in to allow full serialization of the principal.
When it comes to using OAuth/OIDC/WS-Fed in ASP.NET Core, yes, generally it gets serialized into a cookie authentication cookie in order for it to be saved across requests. OAuth can dumb a huge amount of claims into its results, so you can get up with chunked cookies.
So it's really one big cookie, in multiple parts, it's all the same cookie. So they're not optional when it comes to your site functioning as expected. (This is not legal advice for GDPR, that needs to come from an actual lawyer).
As an aside ASP.NET Core 2.1 comes with support for optional cookies, you can mark a cookie as required or not (Cookie authentication marks its cookies as required) and ASP.NET will not write optional cookies unless a user has consented to their use.
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