This repository is a sample project that demonstrates how to build and run a .NET 10 Web API protected by Windows Authentication using the Microsoft.AspNetCore.Authentication.Negotiate package, with an Angular SPA served by the API in production.
The solution is intended to show the full development and hosting flow for a same-origin Angular + ASP.NET Core application where the API owns authentication and the SPA is deployed into the API project's wwwroot folder. Because the SPA and API are hosted from the same origin in production, no CORS configuration is needed.
- Configure a .NET 10 Web API to use Windows Authentication through Negotiate.
- Host the Angular production build from the API project's
wwwrootfolder. - Configure an Angular development proxy that supports authentication negotiation when calling the API during local development.
- Orchestrate the API and Angular client during development with .NET Aspire.
- Use HTTPS only for the local development experience.
- Configure Aspire so the Angular client is exposed through HTTPS during development.
- Configure Content Security Policy headers for both the Angular development server and the .NET API host.
aspire/AppHost: .NET Aspire orchestration project for the local development environment.aspire/ServiceDefaults: Shared Aspire service defaults.aspire.config.json: Aspire CLI configuration that points to the AppHost project..github/skills: Agent skills generated by the Aspire CLI for Aspire, .NET API inspection, and Playwright workflows..vscode/mcp.json: Workspace MCP configuration generated by the Aspire CLI so VS Code agents can use the Aspire MCP server..vscode/settings.json: Workspace settings that associateaspire.config.jsonwith the Aspire CLI JSON schema.src/Web: ASP.NET Core Web API host secured with Windows Authentication.src/Web.Angular: Angular SPA that is served by the API in production, run with the Angular dev server during development, and included in the solution throughWeb.Angular.esproj.
The root aspire.config.json file tells the Aspire CLI where the AppHost project lives. It can be created with aspire config set commands:
{
"appHost": {
"path": "./aspire/AppHost/AppHost.csproj",
"language": "csharp"
}
}The workspace .vscode/settings.json file associates that config file with the Aspire CLI schema so VS Code can provide validation and completion:
{
"json.schemas": [
{
"fileMatch": [
"/aspire.config.json"
],
"url": "https://aspire.dev/reference/cli/configuration/schema.json"
}
]
}The Aspire CLI agent integration was initialized with:
aspire agent initThat command added workspace agent support files, including .github/skills and .vscode/mcp.json. The MCP configuration starts the Aspire MCP server through the Aspire CLI:
{
"servers": {
"aspire": {
"type": "stdio",
"command": "aspire",
"args": [
"agent",
"mcp"
]
}
}
}The Aspire AppHost runs both projects under src during local development:
src/Webis added as a .NET project resource namedweb.src/Web.Angularis added as a JavaScript app resource namedangularand runs the Angularstartscript.- The Angular app waits for the Web API and references it so Aspire can provide service discovery details.
- The Angular app is exposed with an HTTPS endpoint only.
- Aspire provides a development certificate to the Angular dev server and passes the generated certificate and key paths to Angular CLI with
--ssl,--ssl-cert, and--ssl-key. - The Web API launch profile was reduced to HTTPS only by removing the HTTP profile and leaving
https://localhost:7219as the application URL.
The AppHost configuration currently looks like this:
var builder = DistributedApplication.CreateBuilder(args);
var web = builder.AddProject<Projects.Web>("web");
#pragma warning disable ASPIRECERTIFICATES001
builder.AddJavaScriptApp("angular", "../../src/Web.Angular", "start")
.WaitFor(web)
.WithReference(web)
.WithHttpsEndpoint(env: "HTTPS_PORT")
.WithHttpsDeveloperCertificate()
.WithHttpsCertificateConfiguration(static ctx =>
{
ctx.Arguments.Add("--");
ctx.Arguments.Add("--port");
ctx.Arguments.Add("%HTTPS_PORT%");
ctx.Arguments.Add("--ssl");
ctx.Arguments.Add("--ssl-cert");
ctx.Arguments.Add(ctx.CertificatePath);
ctx.Arguments.Add("--ssl-key");
ctx.Arguments.Add(ctx.KeyPath);
return Task.CompletedTask;
});
#pragma warning restore ASPIRECERTIFICATES001
builder.Build().Run();The ASPIRECERTIFICATES001 warning is suppressed around the Angular dev server certificate configuration because this sample intentionally uses Aspire's development certificate support to run the SPA over HTTPS during local development.
The Web API groups protected sample endpoints under /api and requires authorization for the whole group:
var api = app.MapGroup("/api")
.RequireAuthorization();The current sample endpoints are:
GET /api/user: returns the current authenticated Windows identity withname,authenticationType, andisAuthenticated.GET /api/weatherforecast: returns the sample weather forecast from the default ASP.NET Core template, moved under the protected API group.
Authentication and authorization middleware are enabled before the route group is executed:
app.UseAuthentication();
app.UseAuthorization();The sample intentionally does not return user claims from /api/user; the page only displays the current identity summary.
The generated Angular starter page was replaced with a small dashboard that calls the protected API and displays:
- the current authenticated Windows user;
- the weather forecast returned by
/api/weatherforecast.
Angular uses HttpClient with withCredentials: true for both protected API calls so browser credentials are included explicitly:
private readonly apiRequestOptions = { withCredentials: true } as const;
this.http.get<CurrentUser>('/api/user', this.apiRequestOptions);
this.http.get<WeatherForecast[]>('/api/weatherforecast', this.apiRequestOptions);During development, Angular serves the SPA through the Angular dev server and proxies /api/** to the Aspire-managed Web API. The proxy reads the API HTTPS endpoint from the WEB_HTTPS environment variable provided by the AppHost reference:
const target = process.env['WEB_HTTPS'];The proxy uses a keep-alive HTTP/HTTPS agent so the development proxy keeps connection affinity for Windows Authentication negotiation.
For production builds, Angular writes its output directly to src/Web/wwwroot, allowing the ASP.NET Core host to serve the SPA from the same origin as the API.
The Web API project contains an AngularExtensions helper that configures ASP.NET Core to serve the Angular production build and fall back to index.html for client-side routes:
app.UseAngularStaticFiles();
// API routes are mapped before the SPA fallback.
await app.RunWithAngularFallbackAsync();The fallback also maps unmatched /api/** requests to 404 so missing API routes do not accidentally return the Angular shell.
Server-side static file responses add security headers such as X-Content-Type-Options, Referrer-Policy, Cross-Origin-Resource-Policy, and source map headers when matching .map files exist. The index.html response adds the stricter document-level headers, including:
Content-Security-PolicyPermissions-PolicyX-Frame-OptionsCross-Origin-Opener-PolicyCross-Origin-Embedder-Policy
The Angular development server mirrors the same policy through angular.json serve.options.headers, so local development receives the same Content Security Policy and related browser security headers while running through ng serve.
The Angular production build configuration uses an empty browser output folder so the generated files are written directly into src/Web/wwwroot:
"outputPath": {
"base": "../Web/wwwroot",
"browser": ""
}dotnet new sln -n AngularAspireNegotiate -f slnx
dotnet new aspire-apphost -n AppHost -o ./aspire/AppHost -lang C#
dotnet new aspire-servicedefaults -n ServiceDefaults -o ./aspire/ServiceDefaults -lang C#
dotnet new webapi -n Web -o ./src/Web -au windows -minimal
ng new web-angular --ai-config copilot --directory src/Web.Angular --package-manager npm --routing true --skip-git --skip-install --ssr false --style scss --zoneless true
@'
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.5171056">
<PropertyGroup>
<ShouldRunNpmInstall>false</ShouldRunNpmInstall>
<ShouldRunBuildScript>false</ShouldRunBuildScript>
</PropertyGroup>
</Project>
'@ | Set-Content -Path ./src/Web.Angular/Web.Angular.esproj -Encoding UTF8
dotnet sln AngularAspireNegotiate.slnx add ./aspire/AppHost/AppHost.csproj ./aspire/ServiceDefaults/ServiceDefaults.csproj --solution-folder aspire
dotnet sln AngularAspireNegotiate.slnx add ./src/Web/Web.csproj ./src/Web.Angular/Web.Angular.esproj --solution-folder src
aspire config set appHost.path ./aspire/AppHost/AppHost.csproj
aspire config set appHost.language csharp
aspire add javascript
dotnet add ./aspire/AppHost/AppHost.csproj reference ./src/Web/Web.csproj
New-Item -ItemType Directory -Force -Path ./.vscode
@'
{
"json.schemas": [
{
"fileMatch": [
"/aspire.config.json"
],
"url": "https://aspire.dev/reference/cli/configuration/schema.json"
}
]
}
'@ | Set-Content -Path ./.vscode/settings.json -Encoding UTF8
aspire agent init