When developing solutions locally, our goal is to predict and adapt our code to work in a production environment. Depending on our application, predicting production can be straightforward or have several hidden quirks that can lead to hours of head-scratching debugging.
In today’s post, I want to show you a behavior happening in your local development environment that you might not be fully aware of, and how it could be impacting the understanding of your application. We’ll also explore a hidden .NET 10 feature that helps you more accurately mimic a production environment locally, thereby improving your deployment confidence when you’re ready to ship.
The Issue with localhost
Localhost is where most developers feel comfortable, and it's likely one of the most frequently mentioned terms for ASP.NET Core developers. It permeates conversations, documentation, and templates, but it hides a not-so-obvious issue: It’s a single domain. Why is this an issue?
Let’s read the Mozilla documentation on cookies and see how cookies relate to domains.
“The
Domain
andPath
attributes define the scope of a cookie: what URLs the cookies are sent to… TheDomain
attribute specifies which server can receive a cookie. If specified, cookies are available on the specified server and its subdomains. For example, if you setDomain=mozilla.org
frommozilla.org
, cookies are available on that domain and subdomains likedeveloper.mozilla.org
.” – Mozilla Documentation.
For folks developing multiple applications locally, they can already see the issue. Any cookies set at localhost
will be passed and processed by any other ASP.NET Core application hosted on the local domain. Cookies can bleed from one application to another, causing a string of issues:
- Cookie name conflicts and overwrites
- Behavioral changes
- Slow requests as all cookies are parsed and processed
- Bad Requests as cookies overwhelm the size limits
It’s not good. At best, this issue can leave you spending precious time clearing cookies, and at worst, spending days debugging confusing bugs that only manifest in environments that share the same domain but host multiple applications.
Reaching into the Cookie Jar
To illustrate the localhost
issue, let’s build two web applications that both rely on accessing a cookie. Let’s start with the first ASP.NET Core application. In a new Empty
template, let’s create two endpoints.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpContext ctx) =>
{
ctx.Response.Cookies.Append("cookie", "chocolate chip", new ()
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict
});
return TypedResults.Text(
// lang=html
"""
<html lang="en">
<body>
<h1>Cookie Jar</h1>
<a href="/cookie">Cookie</a>
</body>
</html>
""",
"text/html; charset=utf-8");
});
app.MapGet("/cookie", (HttpContext ctx) =>
{
var cookie = ctx.Request.Cookies["cookie"];
return TypedResults.Text(
// lang=html
$"""
<html lang="en">
<body>
<h1>{cookie}</h1>
<a href="/">Home</a>
</body>
</html>
""",
"text/html; charset=utf-8");
});
app.Run();
This application will run on localhost:7014
.
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7014;http://localhost:5138",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
Upon starting the application, we can see that it sets a cookie in the client, which we can access on the /cookie
endpoint.
As expected. Let’s create a new web application using the same Empty
template.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpContext ctx) =>
{
var cookie = ctx.Request.Cookies["cookie"] ?? "No Cookies For You!";
return TypedResults.Text(
// lang=html
$"""
<html lang="en">
<body>
<h1>🍪 Cookie Jar Two</h1>
<h2>{cookie}</h2>
</body>
</html>
""",
"text/html; charset=utf-8");
});
app.Run();
This application will run on localhost:7128
as seen in the launchSettings.json
file.
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7128;http://localhost:5219",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
Let’s start this application and see what happens.
Oh no, what just happened?! Well, based on the Mozilla documentation, any app hosted on localhost
domain can access any cookie written with a matching domain. We can actually see the Domain
value using our browser’s dev tools.
How do we solve this issue? Well, .NET 10 to the rescue.
.NET 10’s Localhost Certificate
When you install .NET 10, the first use of the CLI will ask you to install the new developer certificate, the one used by ASP.NET Core applications to provide HTTPS support. If not prompted, you can install the latest certificate using the following commands.
dotnet dev-certs https --clean
dotnet dev-certs https --trust
The commands should install the certificate to your operating systems certificate store.
You’ll notice something interesting about the latest certificate. It has a few more DNS entries: .dev.localhost
, *.dev.internal
, host.docker.internal
, and host.containers.internal
.
A new .NET 10 feature allows you to use this certificate to run local applications with a unique domain name, as long as it ends in one of these domains. Let’s update our launchSettings.json
file to give each application a unique domain name. The names should somewhat match your application name, but be careful not to include any .
values, as they’ll become a new subdomain that is unsupported by the certificate.
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://cookiejarsone.dev.localhost:7014",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
And for the second application:
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://cookiejarstwo.dev.localhost:7128",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
Note that the port value is essential, since these applications are still running locally, and a unique port is necessary to avoid hosting conflicts. Let’s run both applications and see what behavior we see now.
Cookies are now working as you’d expect across applications, with them being isolated correctly to the application. As you can see in the screenshot, the Domain
value matches that of the application, and is cookiejarsone.dev.localhost
.
What’s the BIG Deal?!
For us at Duende, authentication relies heavily on cookies, whether that’s IdentityServer or our latest product, the Backend for Frontend Security Framework. For ASP.NET Core developers, cookies are a vital part of development, and having access to domain names that help us mimic a production environment is a step in the right direction.
With this feature built in as part of .NET 10, it gives developers more confidence that what they’re building and testing locally will behave similarly in staging and production environments.
Conclusion
While the latest update to the certificate may feel small compared to all the changes coming in .NET 10, this one should make ASP.NET Core development more straightforward and predictable, especially for folks building authentication solutions with Duende IdentityServer.
If you have a solution locally, we recommend updating your launchSettings.json
to use this latest feature, as it will avoid one less issue when implementing security or upgrading your existing applications to .NET 10.
As always, please share your thoughts in the comments section below and let us know about your experience with cookies. We’d love to hear about them.