# Authentication for Windows-hosted apps: IIS, AD, JWT and OIDC

> Choose Windows/AD auth, Basic, JWT or OIDC deliberately, avoid double-hop surprises, and test 401 vs 403.

**Tested on**

- Build: 26100.33650
- Edition: Standard / Datacenter
- Architecture: x64
- Evidence: E1
- Verified: 2026-10-29

## The short answer

| Need | Choice |
|---|---|
| Internal Windows users, domain | Windows auth (Negotiate/Kerberos) |
| Legacy app, no SSO | Basic **only over TLS**, never public |
| SPA/API | OIDC authorization code + PKCE, JWT access tokens |
| Server-to-server | Client credentials/mTLS, not a shared password |
| Public consumer login | OIDC/IdP with MFA; do not build your own password store |

Authentication answers “who”; authorization answers “what”. A 401 means not authenticated; 403 means authenticated but not allowed.

## IIS Windows auth

Enable only the providers you need:

```powershell
Enable-WindowsOptionalFeature -Online -FeatureName IIS-WindowsAuthentication -All
```

In IIS: disable Anonymous, enable Windows Authentication; choose Negotiate/Kerberos (NTLM fallback only if needed). For Kerberos:

- SPN registered for the service account/host;
- DNS name matches the SPN;
- delegation configured only if you need double-hop.

Double-hop (IIS → SQL/file share as the user) fails without constrained delegation. Prefer a service identity for the backend instead of impersonating the user everywhere.

## Basic auth

Basic sends Base64 credentials; without TLS it is plaintext. Use it only for a legacy device on a private network, over HTTPS, with a scoped account. Never expose it to the internet.

## JWT/OIDC

- Validate signature, issuer, audience, expiry and clock skew.
- Do not trust `alg: none` or a token just because it decodes.
- Keep access tokens short; use refresh tokens carefully.
- Store browser tokens in HttpOnly/Secure/SameSite cookies where possible; avoid localStorage for long-lived tokens.
- Put authorization checks server-side; hiding a button is not authorization.

## Proxy auth

- Authenticate at the edge/IdP, then pass a signed identity (header/JWT) to the app.
- Strip client-supplied identity headers at the proxy; only the proxy may set them.
- Do not let the app trust `X-User` from the internet.

> **Caution: 401 vs 403 is diagnostic**
>
> A 401 with `WWW-Authenticate` means the client did not authenticate. A 403 means it did, but the identity/role is insufficient. Mixing them hides authorization bugs.

## Verify

```powershell
curl.exe -I https://app.example/
curl.exe -I -u user:pass https://app.example/private
```

Check the challenge, cookie flags (`Secure`, `HttpOnly`, `SameSite`), token expiry, and that a low-privilege user gets 403, not data.

## Lab boundary

E1. The lab used a local admin for WAC; it did not configure AD/Kerberos/OIDC for a web app. The decision table is the design to validate on Server 2025.

## Sources

- [Microsoft Learn — IIS authentication](https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/authentication/)
- [Microsoft Learn — ASP.NET Core authentication](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/)
- [OWASP — Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)

---

Source: https://winbook.fyi/en/server/security/auth/
