A lightweight ASP.NET Core 10 app that echoes all HTTP request headers back as a web page. It is designed to validate that Microsoft Entra Application Proxy correctly injects HTTP headers derived from Entra ID token claims — including custom claims injected by a Custom Claims Provider (CCP).
The app highlights specific identity headers (UPN, Object ID, and a CCP-injected favoriteColor claim) in the UI so you can quickly confirm the end-to-end flow is working.
Security note: This app trusts all incoming headers unconditionally. It must only be reachable from the App Proxy connector host. Use a Windows Firewall rule or IIS IP address restriction to enforce this — never expose this app directly to the internet.
User → Entra ID (auth + CCP enrichment) → App Proxy (header injection) → IIS → This App
- The user authenticates with Entra ID.
- A Custom Claims Provider Azure Function adds a
favoriteColorclaim to the token. - App Proxy (header-based SSO) maps token claims to HTTP headers and forwards the request to IIS.
- This app renders all received headers, highlighting the identity-related ones.
| Requirement | Notes |
|---|---|
| Windows Server 2016 or later (or Windows 10/11) | IIS must be enabled |
| IIS 10 or later | With the ASP.NET Core Module (ANCM) |
| .NET 10 Hosting Bundle | Installs the runtime and the IIS ANCM module |
Critical: You must install the .NET 10 Hosting Bundle — not just the Runtime or SDK. The Hosting Bundle is the only installer that registers the ASP.NET Core Module with IIS.
Download: https://dotnet.microsoft.com/download/dotnet/10.0
Look for: "Hosting Bundle" under the Windows column.
| Requirement | Notes |
|---|---|
| .NET 10 SDK | https://dotnet.microsoft.com/download/dotnet/10.0 |
| Git | To clone the repo |
In an elevated PowerShell session:
Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerRole, IIS-WebServer, IIS-CommonHttpFeatures, IIS-HttpErrors, IIS-HttpLogging, IIS-RequestFiltering, IIS-StaticContent, IIS-DefaultDocument, IIS-ApplicationDevelopment, IIS-ASPNET45, IIS-NetFxExtensibility45, IIS-ISAPIExtensions, IIS-ISAPIFilter -AllOr via Server Manager → Add Roles and Features → Web Server (IIS).
Download dotnet-hosting-10.x.x-win.exe from https://dotnet.microsoft.com/download/dotnet/10.0 and run it on the server. After installation, run:
iisresetVerify ANCM is registered:
Get-WebConfiguration "system.webServer/globalModules/*" | Where-Object { $_.name -like "AspNetCore*" }You should see AspNetCoreModuleV2 in the output.
On your build machine (or directly on the server if the SDK is installed):
git clone https://github.com/JeffBley/header-based-app.git
cd header-based-app
dotnet publish -c Release -o publishThis produces a self-contained set of files in the publish\ folder.
Copy the contents of the publish\ folder to the server. A common location:
C:\inetpub\HeaderEchoApp\publish\
In an elevated PowerShell session on the server:
Import-Module WebAdministration
# Create the app pool — must use "No Managed Code" for ASP.NET Core
New-WebAppPool -Name "HeaderEchoApp"
Set-ItemProperty "IIS:\AppPools\HeaderEchoApp" -Name managedRuntimeVersion -Value ""Replace headerapp.yourdomain.com with your internal hostname (e.g. headerapp.contoso.com):
New-Website -Name "HeaderEchoApp" `
-PhysicalPath "C:\inetpub\HeaderEchoApp\publish" `
-ApplicationPool "HeaderEchoApp" `
-Port 80 `
-HostHeader "headerapp.yourdomain.com"Grant the app pool identity read access to the publish folder:
$acl = Get-Acl "C:\inetpub\HeaderEchoApp\publish"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"IIS AppPool\HeaderEchoApp", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($rule)
Set-Acl "C:\inetpub\HeaderEchoApp\publish" $aclFrom the server itself, pass the host header explicitly since the site is bound to a hostname:
Invoke-WebRequest http://localhost -Headers @{ Host = "headerapp.yourdomain.com" } -UseBasicParsing | Select-Object StatusCodeExpected: StatusCode: 200
Or browse to http://headerapp.yourdomain.com from a machine that can resolve the hostname — you should see a page listing all HTTP headers received by the app.
Once the app is accessible on the internal network:
-
In the Entra admin center, go to Enterprise Applications → New application → On-premises application.
-
Set the Internal URL to
http://headerapp.yourdomain.com(port 80, matching the IIS host header binding). -
Set Pre-authentication to
Microsoft Entra ID. -
Under Single sign-on → Header-based, add header mappings:
Header name Source Claim X-MS-CLIENT-PRINCIPAL-NAMEAttribute user.userprincipalnameX-MS-CLIENT-PRINCIPAL-IDAttribute user.objectidX-Favorite-ColorAttribute customClaim.favoriteColor -
Assign users/groups to the application.
| Symptom | Likely cause | Fix |
|---|---|---|
| HTTP 500.19 | ANCM not registered in IIS | Install/reinstall the .NET 10 Hosting Bundle and run iisreset |
| HTTP 500.30 | App fails to start | Check Event Viewer → Application for ANCM errors; verify publish\ folder contains web.config |
| HTTP 403.14 | Wrong physical path | Ensure IIS site physical path points to the publish\ subfolder, not the parent |
| Headers show "not present" | App Proxy SSO not configured | Verify header mappings are saved in App Proxy SSO settings; check connector health |
| App pool stops immediately | App pool set to managed runtime | Set managedRuntimeVersion to "" (No Managed Code) |
├── HeaderEchoApp.csproj # Project file (.NET 10, IIS in-process hosting)
├── Program.cs # Entire application — single GET / endpoint
├── appsettings.json # Default ASP.NET Core config (logging, etc.)
└── web.config # IIS AspNetCoreModuleV2 configuration
If you're using Microsoft's official sample documentation to issue custom claims, you'll need to update the function app code with the following so it emits the favoriteColor claim.
#r "Newtonsoft.Json"
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
log.LogInformation("CCP OnTokenIssuanceStart invoked.");
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
// Build the response with the favoriteColor claim
ResponseContent r = new ResponseContent();
r.data.actions[0].claims.FavoriteColor = "Blue";
return new OkObjectResult(r);
}
public class ResponseContent {
[JsonProperty("data")]
public Data data { get; set; }
public ResponseContent() {
data = new Data();
}
}
public class Data {
[JsonProperty("@odata.type")]
public string odatatype { get; set; }
public List<Action> actions { get; set; }
public Data() {
odatatype = "microsoft.graph.onTokenIssuanceStartResponseData";
actions = new List<Action>();
actions.Add(new Action());
}
}
public class Action {
[JsonProperty("@odata.type")]
public string odatatype { get; set; }
public Claims claims { get; set; }
public Action() {
odatatype = "microsoft.graph.tokenIssuanceStart.provideClaimsForToken";
claims = new Claims();
}
}
public class Claims {
[JsonProperty("favoriteColor", NullValueHandling = NullValueHandling.Ignore)]
public string FavoriteColor { get; set; }
}