An OpenID Connect authorization server that uses Windows Integrated Authentication as its identity source, built on the OpenIddict library.
Note: "IdentityServer" in this project refers to the assembly name and configuration section — it is not related to Duende IdentityServer.
- Rationale
- Prerequisites
- Installation
- Configuration
- Supported flows, scopes, and claims
- Testing
- Troubleshooting
- License
A drop-in OpenID Connect authorization server that requires no database, no certificate management, and no persistent state. It performs Windows Integrated Authentication against the local machine or Active Directory and returns the resulting identity as standard OIDC tokens.
Tradeoffs to be aware of:
- Signing and encryption keys are ephemeral — they are regenerated on every application start. Tokens issued before a restart or app pool recycle cannot be validated afterwards.
- There is no refresh token flow: tokens have a fixed lifetime and clients must re-authenticate when they expire.
- Suitable for intranet scenarios where a short-lived token model is acceptable and the user population is already authenticated to Windows.
- Windows Server or Windows 10/11 with IIS
- .NET 10.0 runtime (ASP.NET Core Hosting Bundle)
- IIS with both Windows Authentication and Anonymous Authentication roles/features installed
- For Active Directory integration: the host machine must be domain-joined (local-only accounts are supported with reduced claims — see Supported flows, scopes, and claims)
- Publish the project (
dotnet publish -c Release) and copy the output to your IIS server. - Create an IIS site or application pointing at the publish folder. The application pool must run under an identity with permission to query Active Directory (typically
ApplicationPoolIdentityworks on domain-joined machines). - In IIS Manager, open the site's Authentication feature and enable both:
- Windows Authentication (required — this is how users are authenticated)
- Anonymous Authentication (required — the
/connect/tokenand/.well-known/*endpoints must be reachable without a Windows challenge)
- Because tokens do not survive application restarts, configure the app pool's recycle settings:
- Disable idle timeout or set Idle Time-out Action to
Suspend(rather thanTerminate) - Move the daily recycle to a low-traffic hour, or disable it in favor of a fixed schedule
- Disable idle timeout or set Idle Time-out Action to
Configuration is read from appsettings.json. Environment variables and command-line arguments are also supported through the standard ASP.NET Core configuration pipeline.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
},
"IdentityServer": {
"ServerUri": "*",
"UseForwardedHeaders": false,
"Hosts": [
"http://localhost",
"https://localhost",
"https://myserver.com",
"https://oidcdebugger.com"
],
"Clients": [
{ "ClientId": "my-public-app" },
{ "ClientId": "my-confidential-app", "ClientSecret": "changeme" }
],
"Groups": [
"^MyServer .*$",
"^Domain Admins$"
]
}
}The full URI the server should report in the issuer and endpoint fields of /.well-known/openid-configuration. If the app is installed at https://myserver.com/IdentityServer/, that is what should go here.
Set to "*" (the default) to auto-detect the issuer from the incoming request URL. Convenient when the deployment address isn't known in advance, but the issuer will vary if the app is reached via multiple hostnames.
Set to true to enable ASP.NET Core's forwarded-headers middleware, which rewrites the request scheme and host from X-Forwarded-Proto and X-Forwarded-Host. Enable this when the app sits behind a reverse proxy (IIS ARR, nginx, etc.) so issuer auto-detection and redirect URIs reflect the public-facing address. Defaults to false.
An allowlist of hosts that may appear in a client's redirect_uri. Authorization requests whose redirect_uri resolves to a host not in this list are rejected. Only the host portion of each URL is compared — scheme, port, and path are ignored during validation.
List of permitted clients. Each entry must have a ClientId. An optional ClientSecret can be provided for confidential clients — if present, it will be verified on token requests.
- Use
"*"as aClientIdto accept any client without enumerating them. - Use
"*"as aClientSecretvalue to accept any secret without validation (useful for dev/test).
"Clients": [
{ "ClientId": "my-public-app" },
{ "ClientId": "my-confidential-app", "ClientSecret": "changeme" },
{ "ClientId": "*" }
]If this key is absent or empty, any client_id is accepted (open access).
A list of .NET regular expressions (case-insensitive) matched against the authenticating user's Active Directory group names. Matching groups are returned as role claims in the token.
"Groups": [
"^MyServer .*$",
"^Domain Admins$",
".*-Readers$"
]Only groups whose common name matches at least one pattern are emitted. This keeps tokens small and prevents leaking internal group membership to relying parties.
Supported OAuth 2.0 / OIDC flows:
| Flow | response_type values |
|---|---|
| Authorization Code | code |
| Implicit | id_token, token, id_token token |
| Hybrid | code id_token, code token, code id_token token |
PKCE is supported for the Authorization Code flow and recommended for public clients.
Supported scopes and the claims each adds to the token:
| Scope | Claims |
|---|---|
openid |
sub (Windows SID), name |
profile |
windowsaccountname; plus givenname, surname, homephone when available from Active Directory |
email |
email (from AD mail attribute, falling back to username@localhost) |
roles |
role (one per AD group matching IdentityServer:Groups) |
Profile, email, and role claims that require Active Directory are only populated for domain users. For users logged on with a local machine account, only sub, name, windowsaccountname, and a synthetic email of username@localhost are returned.
All claims are included in both the access token and the ID token.
If you run the project in Visual Studio, the OpenID configuration document is available at:
oidcdebugger.com is a browser-based tool for constructing and sending OpenID Connect authorization requests and inspecting the results. Fill in the form fields as described below, then click Send Request. Your browser will be redirected to the authorization endpoint, Windows authentication will occur transparently, and the debugger will display the tokens or authorization code returned.
| Field | Value |
|---|---|
| Authorize URI | http://localhost:5000/connect/authorize |
| Redirect URI | https://oidcdebugger.com/debug |
| Client ID | my-public-app |
| Scope | openid profile email roles |
| Nonce | (leave as auto-generated) |
The https://oidcdebugger.com host is already in the default IdentityServer:Hosts allowlist, so no configuration change is needed.
| Field | Value |
|---|---|
| Response type | id_token |
| Response mode | form_post |
Use token instead of id_token to receive an access token, or check both to receive both in a single response.
| Field | Value |
|---|---|
| Response type | code |
| Response mode | query or form_post |
| Token URI | http://localhost:5000/connect/token (required only if using PKCE; see below) |
The debugger will display the authorization code. For public clients (my-public-app), enable Use PKCE? (SHA-256 is recommended) — the debugger will auto-generate the code verifier and challenge and can perform the token exchange automatically when Token URI is provided. For confidential clients (my-confidential-app), the debugger cannot supply client_secret, so use a tool such as Postman or curl to exchange the code manually:
POST http://localhost:5000/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<code from debugger>
&redirect_uri=https://oidcdebugger.com/debug
&client_id=my-confidential-app
&client_secret=changeme
| Field | Value |
|---|---|
| Response type | code + id_token (check both) |
| Response mode | form_post |
| Token URI | http://localhost:5000/connect/token |
You can also combine code + token or all three (code, token, id_token) depending on what the client needs.
Browser prompts for Windows credentials repeatedly (401 loop). Anonymous Authentication is likely disabled in IIS, or Windows Authentication is not enabled at all. Both must be turned on. Also confirm the browser trusts the site for integrated authentication (for IE/Edge/Chrome, the site must be in the Local Intranet zone or explicitly whitelisted).
Tokens issued before a restart fail validation afterwards. Expected. Signing and encryption keys are ephemeral. Configure the IIS app pool to suspend rather than terminate on idle, and to recycle on a predictable schedule.
Issuer in tokens doesn't match the URL clients use.
Either set IdentityServer:ServerUri to the canonical public URL explicitly, or — if behind a reverse proxy — set IdentityServer:UseForwardedHeaders to true and ensure the proxy is sending X-Forwarded-Proto and X-Forwarded-Host.
Authorization request rejected with invalid_client.
Either the client_id is not in IdentityServer:Clients, or the redirect_uri host is not in IdentityServer:Hosts. The application log records which check failed and the offending value.
Expected role claims are missing.
Verify the user is on a domain-joined machine (local accounts do not get role claims) and that the group names match the regex patterns in IdentityServer:Groups. Remember the patterns are regex — plain strings like "MyServer Admins" will match, but a pattern like "MyServer *" does not mean glob-style wildcard; it means the literal letter r zero or more times. Use "MyServer .*" for "starts with 'MyServer '".
