Production-style authentication and authorization demo using:
- ASP.NET Core Web API (
net10.0) - Keycloak in Docker
- Postgres for Keycloak persistence
- JWT bearer validation with policy-based authorization (roles + scopes)
+----------------------+ +-----------------------+
| Client / Swagger | | Service Client |
| (Auth Code + PKCE) | | (Client Credentials) |
+----------+-----------+ +-----------+-----------+
| |
| tokens | tokens
v v
+----------------------+ validates +------------------------+
| Keycloak +------------------>+ ASP.NET Core API |
| realm: auth-demo | | JWT + policies + CORS |
+----------+-----------+ +------------+-----------+
| |
| persists realm/users/clients |
v v
+----------------------+ +-------------------+
| Postgres | | Protected APIs |
+----------------------+ +-------------------+
- Keycloak realm import (
auth-demo) with:- Roles:
user,manager,admin - Scopes:
api.read,api.write - Users:
alice,bob,carol - Clients:
auth-demo-api(API audience)auth-demo-swagger(public, auth code + PKCE)auth-demo-service-read(client credentials, read scope)auth-demo-service-write(client credentials, read/write scopes)
- Roles:
- API security:
- JWT Bearer auth
- Policy-based authorization by role and scope
- Keycloak role mapping from
realm_accessandresource_access
- Operational concerns:
- Health checks (
/health/live,/health/ready) - Rate limiting on write endpoint
- CORS policy
- Structured JSON logging + correlation id header (
X-Correlation-ID) - ProblemDetails for consistent errors
- Health checks (
- Start services:
docker compose up -d --build- Open Keycloak Admin Console:
http://localhost:8080/admin
- Use admin credentials from
.env(demo defaults):
- Username:
admin - Password:
admin123!
- Open API Swagger:
http://localhost:5083/swagger
| Endpoint | Method | Requirement |
|---|---|---|
/api/demo/public |
GET | Anonymous |
/api/demo/me |
GET | Authenticated token |
/api/demo/reports |
GET | api.read scope OR admin role |
/api/demo/reports |
POST | api.write scope OR admin role (+ rate limit) |
/api/demo/admin |
GET | admin realm role |
curl -X POST "http://localhost:8080/realms/auth-demo/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=auth-demo-service-read&client_secret=demo-read-secret"curl -X POST "http://localhost:8080/realms/auth-demo/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=auth-demo-service-write&client_secret=demo-write-secret"curl -X POST "http://localhost:8080/realms/auth-demo/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&client_id=auth-demo-swagger&username=carol&password=carol123&scope=openid profile email api.read api.write"Use the access_token as:
curl "http://localhost:5083/api/demo/reports" -H "Authorization: Bearer <TOKEN>"Use KeyCloakTest/KeyCloakTest.http for ready-to-run requests in sequence.
- Realm import not applied:
- Stop and remove volumes, then restart:
docker compose down -v && docker compose up -d --build
- 401 from protected endpoints:
- Check issuer is
http://localhost:8080/realms/auth-demo - Check
audincludesauth-demo-api
- Check issuer is
- 403 from write/admin endpoints:
- Token authenticated but missing scope/role
- This is a demo with non-production secrets and
start-devKeycloak mode. - For production, use HTTPS, secret management, hardened Keycloak config, and stronger token/session policies.