A security-focused ASP.NET Core Web API enforcing sender-constrained token validation.
Aligned with FAPI 2.0 Baseline and Advanced hardening goals.
Sentinel is a security-focused ASP.NET Core Web API that enforces sender-constrained token validation patterns aligned with FAPI 2.0 Baseline and Advanced hardening goals.
The current implementation provides:
- DPoP access token handling with proof validation and nonce issuance
- Redis-backed replay detection for both access token jti and DPoP proof jti
- ACR and scope-based authorization requirements
- mTLS certificate binding checks for cnf.x5t#S256-bound tokens
- Strict JWT algorithm constraints and zero clock skew lifetime validation
- Security telemetry, security headers, rate limiting, and structured error handling
- Specification: SPEC-0001 - User Authentication and Token Issuance
- Implementation plan: PLAN-0001 - Auth Implementation
- Task breakdown: TASK-0001 - Auth Implementation Tasks
Production-grade documentation for all audiences. Start with the Documentation Index.
| Document | Audience | Purpose |
|---|---|---|
| ARCHITECTURE.md | Architects, Engineers | 10 Architecture Decision Records (ADRs) explaining DPoP, replay cache, rate limiting, nonce management, middleware ordering, idempotency, and operational design |
| SDK_LESS_INTEGRATION_GUIDE.md | Client Developers | Complete HTTP/REST integration guide with DPoP proof generation in 5 languages (JS, Python, Java, C#, Go) and full end-to-end examples |
| LIVING_THREAT_MODEL.md | Security Teams, Auditors | 21 identified threats across 7 categories with mitigation analysis, likelihood × impact matrix, and residual risk assessment |
| SRE_SOC_RUNBOOKS.md | SRE, SOC, On-Call | Monitoring, alerting, incident response procedures, troubleshooting guides, and maintenance checklists with bash/PowerShell commands |
| COMPLIANCE_AUDIT_MATRIX.md | Compliance, Auditors | Compliance framework mapping (OAuth 2.0, JWT, DPoP RFC 9449, FAPI 2.0 Baseline) with 40+ audit checklist items |
| OPENAPI_3_1.yaml | API Consumers | Formal OpenAPI 3.1 specification; machine-readable API contract for SDK generation and API gateway integration |
| BUILD_CONFIGURATION_GUIDE.md | Developers, DevOps | Directory.Build.props explanation, build workflow, code analysis policy, AOT/reproducibility, and troubleshooting |
Quick Start by Role:
- Client Developer: Start with SDK_LESS_INTEGRATION_GUIDE.md and OPENAPI_3_1.yaml
- SRE / Operations: Start with SRE_SOC_RUNBOOKS.md
- Security / Compliance: Start with LIVING_THREAT_MODEL.md and COMPLIANCE_AUDIT_MATRIX.md
- Architect / Lead Engineer: Start with ARCHITECTURE.md
- Developer / DevOps: Start with BUILD_CONFIGURATION_GUIDE.md
| Area | Status | Notes |
|---|---|---|
| API host and middleware pipeline | Implemented | Security middleware chain and centralized exception handling are active |
| JWT validation and policy authorization | Implemented | Issuer, audience, lifetime, algorithms, ACR and scope enforcement |
| DPoP proof validation | Implemented | htm, htu, iat window, typ, alg, jwk, cnf.jkt checks; RFC 9449 compliant nonce challenge-response |
| Replay protection | Implemented | Atomic Redis-backed jti cache (SET NX), fail-closed behavior, 60s TTL alignment with token lifetime |
| DPoP nonce management | Implemented | Per-JWK-thumbprint rotating nonce; atomic compare-delete consumption; 60s TTL |
| mTLS token binding | Implemented | cnf.x5t#S256 compared with presented client certificate hash; optional second factor |
| Rate limiting | Implemented | Dual-partition chained limiter (per-identity + per-IP); per-anonymous-IP isolation |
| Idempotency enforcement | Implemented | Logout idempotency with state machine (IN_PROGRESS→409, COMPLETED→204) |
| Session management | Implemented | Refresh token rotation; session blacklist on logout; TTL aligned with Keycloak (8h default) |
| OpenTelemetry and metrics endpoint | Implemented | Tracing, metrics counters/histograms, Prometheus scrape endpoint; security event telemetry |
| Integration and unit testing | Implemented | 50/50 tests passing with full security scenario coverage |
| Full OAuth PAR and PKCE orchestration endpoint set | Planned/Externalized | Keycloak-driven flow orchestration remains infrastructure and client-driven |
The API pipeline applies controls in a defense-in-depth sequence:
- Global exception handler and problem details formatting
- Security response header hardening (HSTS, CSP, frame-deny, no-sniff, cache-control)
- Global fixed-window rate limiter (per-identity + per-IP dual partition)
- JWT authentication (issuer, audience, lifetime, algorithm validation)
- Rate limiter evaluation (both partitions must have available quota)
- DPoP validation middleware (proof structure, signature, htm/htu binding, nonce validation)
- mTLS certificate binding middleware (optional cnf.x5t#S256 validation)
- ACR presence validation middleware
- Authorization policy enforcement (scope, ACR requirements per endpoint)
- Controller endpoint execution
- Response headers (DPoP-Nonce for next request rotation)
- Prometheus scrape endpoint (/metrics)
- DPoP validator enforces RFC 9449 compliance: validates proof signature, type, algorithm, htm/htu binding, iat freshness (±60s), jti proof replay via atomic Redis cache, and per-thumbprint nonce consumption
- Replay cache stores JWT jti and proof jti with atomic SET NX (When.NotExists) semantics; fail-closed returns 503 on Redis unavailability
- Nonce store manages per-JWK-thumbprint rotating nonces; atomic compare-delete transaction prevents consumption race; challenges issue fresh nonce on stale/consumed mismatches
- Rate limiter implements dual-partition chained enforcement: identity partition (sub+client_id or per-IP if anonymous) and always-present IP partition; 429 response includes Retry-After header
- Security event emitter produces structured OpenTelemetry Activity events with correlation IDs for SIEM pivoting
- ACR and scope authorization handlers apply fine-grained policy checks at endpoint level using claims validation
Sentinel/
|- Sentinel.slnx
|- Directory.Build.props ← Centralized build config (all projects inherit)
|- docker-compose.yml
|- Makefile
|- docs/
| |- README.md ← Documentation index
| |- ARCHITECTURE.md ← ADRs (10 decisions)
| |- SDK_LESS_INTEGRATION_GUIDE.md ← Client integration (5 languages)
| |- LIVING_THREAT_MODEL.md ← Security threat analysis (21 threats)
| |- SRE_SOC_RUNBOOKS.md ← Operational procedures
| |- COMPLIANCE_AUDIT_MATRIX.md ← Regulatory framework mapping
| |- OPENAPI_3_1.yaml ← API specification (OpenAPI)
| |- BUILD_CONFIGURATION_GUIDE.md ← Build config explanation & workflow
|- artifacts/ ← Centralized build output (bin/obj)
| |- bin/
| |- obj/
|- infra/
| |- keycloak/
| |- realms/
| |- sentinel.json
|- .github/
| |- workflows/
| |- agents/
| |- prompts/
|- .specify/
| |- specs/
| |- plans/
| |- tasks/
|- src/
| |- Sentinel.Domain/
| |- Sentinel.Application/
| |- Sentinel.Infrastructure/
| |- Sentinel.AspNetCore/
|- tests/
| |- Sentinel.Tests/
| |- Integration/
| |- Unit/
|- artifacts/ ← Build output (bin/obj centralized)
| Component | Version | Usage |
|---|---|---|
| .NET SDK | 11.0 preview | Build and runtime |
| ASP.NET Core | 11.0 preview packages | API framework and middleware |
| Keycloak | 26.1 image in compose | Authorization server and realm management |
| Redis | 7.4 alpine | Replay cache backing store |
| OpenTelemetry | 1.13 to 1.14 packages | Tracing, metrics, exporter integration |
| xUnit + Testcontainers | Current project references | Unit and integration validation |
- Windows, Linux, or macOS development environment
- .NET 11 SDK preview installed
- Docker Desktop or Docker Engine
- Optional: Trivy for image vulnerability scanning
Directory.Build.props provides centralized configuration for all projects:
| Feature | Setting | Purpose |
|---|---|---|
| Artifacts Layout | UseArtifactsOutput: true |
Centralized bin/obj → artifacts/ folder (no tree pollution) |
| Framework | TargetFramework: net11.0 |
.NET 11 preview (latest) |
| Code Analysis | AnalysisLevel: latest-all |
Aggressive code quality checks (catch issues early) |
| Warnings as Errors | Release/CI only | Zero-warning policy; enforced at CI stage |
| AOT Compatibility | Enabled for executables | Native AOT readiness; trim-safe code analysis |
| Security | NuGetAudit, ControlFlowGuard | Block vulnerable packages; enable Control Flow Guard |
| Reproducible Builds | Deterministic: true |
Dev machine binary = CI binary (no variance) |
| Language | Nullable: enable, ImplicitUsings: enable |
Modern C# with strict null safety |
| Lock Files | RestorePackagesWithLockFile: true |
Frozen transitive dependencies for reproducibility |
All projects inherit these settings automatically; overrides in individual .csproj only when necessary.
Interactive API Reference (Development only): http://localhost:5260/scalar
dotnet restore Sentinel.slnx --locked-mode
dotnet build Sentinel.slnx -c Release
dotnet test Sentinel.slnxdocker-compose up --build -dServices:
- Keycloak: https://localhost:8443 and http://localhost:8080
- Redis: localhost:6379
- Sentinel API: http://localhost:5260
cd src/Sentinel.AspNetCore
dotnet rundocker-compose down -vSentinel enforces strong-name signing across all assemblies. Local development uses PublicSign with Sentinel.public.snk (committed to source control) when the private key is unavailable or when building on non-Windows systems, while CI/CD release builds perform full signing with a private key injected via secrets.
To prevent InternalsVisibleTo cryptographic conflicts with unsigned test projects during active local development, assembly signing is conditionally enabled only during official release packaging.
sn -k Sentinel.snksn -p Sentinel.snk Sentinel.public.snkTo build and package the production NuGet libraries with strong-name signing enabled, pass the SignSentinelRelease=true property:
dotnet pack Sentinel.slnx -c Release -p:SignSentinelRelease=true -o ./artifacts- Store the base64-encoded Sentinel.snk in GitHub Secrets (for example SENTINEL_SNK_BASE64).
- In .github/workflows/security-pipeline.yml, decode the secret into the workspace before the build:
- name: Restore strong-name key (Staging/Release Only)
if: ${{ secrets.SENTINEL_SNK_BASE64 != '' }}
shell: bash
run: echo "$SENTINEL_SNK_BASE64" | base64 -d > Sentinel.snk
env:
SENTINEL_SNK_BASE64: ${{ secrets.SENTINEL_SNK_BASE64 }}This keeps the private key out of source control while ensuring release builds are fully signed.
make build # locked restore + release build
make test # run all tests
make mutation # run mutation tests for DPoP-critical paths
make lint # dotnet format verification
make sec-scan # build image + trivy scan (critical/high)
make up # docker-compose up --build -d
make down # docker-compose down -v
make all # build + lint + test + sec-scan
Mutation testing is configured for DPoP-critical code paths via Stryker.NET. To avoid "Test Scope Leaks" and ensure sub-minute execution, always run Stryker from inside the specific test project directory:
dotnet tool restore
cd tests/Sentinel.Tests.DPoP/
dotnet stryker --config-file ../../stryker-config.json --project Sentinel.DPoP.csprojBaseline gate thresholds:
- break: 70
- low: 70
- high: 85
Minimum required keys:
| Key | Purpose | Example |
|---|---|---|
| ConnectionStrings:Redis | Replay cache backend | localhost:6379 |
| Keycloak:Authority | Token issuer authority | https://localhost:8443/realms/sentinel |
| Keycloak:Audience | Expected access token audience | sentinel-api |
| Keycloak:RequireHttpsMetadata | Dev metadata over HTTP toggle | false (development only) |
| FeatureFlags:Auth:DpopFlow | Feature toggle placeholder | true |
Notes:
- Production deployments should keep HTTPS metadata required.
- Redis availability is part of the security boundary; service degrades fail-closed for replay-protected flows when unavailable.
- GET /v1/Profile
Requirements:
- Authenticated JWT passed in Authorization header with DPoP scheme
- Matching DPoP proof in DPoP header
- Policy ReadProfile satisfied:
- scope includes profile
- acr claim meets minimum acr2
Example response model:
{
"sub": "subject-id",
"displayName": "display name",
"roles": ["user"]
}Implemented RFC 9449 (DPoP) + FAPI 2.0 Baseline hardening controls:
JWT Validation:
- Issuer and audience required; validated against Keycloak realm configuration
- Lifetime required with zero clock skew tolerance
- Signed tokens required; algorithm allow-list restricted to ES256, RS256, PS256
- JTI (JWT ID) replay detection: stored in Redis cache with TTL matching token lifetime (60s)
- Duplicate JTI rejection: return 503 Service Unavailable (fail-closed)
DPoP Proof Validation (RFC 9449):
- Token and proof format validation
- Type must be
dpop+jwt; algorithm restricted to ES256, RS256, PS256 - Embedded public JWK required; private key material rejected
- Signature validated against embedded JWK
htm(HTTP method) andhtu(HTTP URI) binding enforced and compared to requestiatfreshness window enforced (±60 seconds clock skew tolerance)- Access token
cnf.jktmust match proof JWK thumbprint (S256) - Proof JTI replay blocked with atomic Redis SET NX (When.NotExists) cache
- Per-JWK-thumbprint nonce required in proof claims
- Atomic nonce consumption via Redis transaction compare-delete (prevents reuse)
Nonce Challenge-Response (RFC 9449 §4.3):
- Server-issued nonce included in
DPoP-Nonceresponse header - Nonce tied to client's JWK thumbprint; per-identity nonce sequence
- Nonce lifetime: 60 seconds; expiration triggers challenge re-issuance
- Initial unauthenticated request returns 400 Bad Request + nonce challenge
- Client includes nonce in next proof; server validates before consumption
- Stale/consumed nonce triggers new challenge issuance with fresh nonce
- Client must update cached nonce from every response header
Access Token Replay Defense:
- JTI claim required and enforced
- Duplicate JTI within token lifetime (60s) rejected
- Redis outage triggers fail-closed behavior (503 Service Unavailable)
mTLS Sender-Constraining (Optional):
cnf.x5t#S256validated against presented client certificate SHA-256 hash- Enables optional second-factor binding (mTLS + DPoP)
Rate Limiting:
- Dual-partition chained enforcement:
- Identity partition:
{sub}:{client_id}if authenticated;{remote_ip}if anonymous - IP partition: Always
{remote_ip}(layered defense)
- Identity partition:
- Both partitions must have available quota; if either exhausted → 429 Too Many Requests
- Per-identity quota: 10-20 req/min (configurable; auth endpoints lower)
- Per-IP quota: 100 req/min (configurable; anonymous isolation)
- Gradeful degradation: 429 response includes
Retry-Afterheader
Session Management:
- Refresh token rotation enforced on every refresh
- Refresh token reuse detected; second use triggers session blacklist and forces re-authentication
- Session blacklist on logout with TTL aligned to Keycloak
SsoSessionMaxLifespanSeconds(default 8 hours) - Idempotency enforcement on logout:
Idempotency-Keyheader required - Idempotency state machine: IN_PROGRESS (409) vs COMPLETED (204) distinction
- Backchannel logout support (Keycloak-initiated session termination)
HTTP Response Hardening:
- HSTS (HTTP Strict-Transport-Security): 1 year max-age
- CSP (Content-Security-Policy): restrict inline scripts, external resources
- X-Content-Type-Options: nosniff (prevent MIME-type sniffing)
- X-Frame-Options: DENY (prevent clickjacking)
- X-XSS-Protection: 1; mode=block (browser XSS filter)
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: restrict API capabilities
- Cache-Control: no-store, must-revalidate (prevent caching of sensitive responses)
- Server and X-Powered-By headers removed
OpenTelemetry Security Telemetry:
- Security events emitted as Activity events with structured attributes
- Events:
security:authentication_success,security:invalid_dpop_proof,security:use_dpop_nonce,security:token_reuse_detected,security:rate_limit_exceeded,security:session_revoked - W3C Trace Context for correlation across distributed components
- Sensitive data excluded from attributes (PII masking)
- Activity source: Sentinel.Auth.Tracing
- Includes DPoP validation and replay cache operations
- Meter: Sentinel.Auth.Metrics
- Counters:
- auth.dpop.failures
- auth.jti.replays_total
- auth.token.issued
- Histogram:
- auth.token.validation.duration_ms
- Security events include correlation IDs for SIEM pivoting
- Replay and auth failure events map to runbook actions in docs/runbooks/auth-token-issuance.md
Focus areas:
- DPoP validator acceptance and replay rejection
- DPoP nonce store read-without-delete, atomic storage with clobber-safety, atomic consume-if-matches
- JTI replay cache atomic SET NX (When.NotExists) semantics and TTL handling
- Refresh token rotation and reuse detection
- mTLS binding checks with cert/no-cert branches
- ACR authorization ranking behavior
- Idempotency state machine (IN_PROGRESS vs COMPLETED branches)
- Security response header enforcement
Focus areas:
- End-to-end FAPI2-compliant authenticated flow with DPoP proof per request
- DPoP nonce challenge flow: unauthenticated 400 → nonce in header → prove with nonce → success
- Token replay detection (JTI already used within 60s window)
- DPoP proof replay detection (proof JTI + nonce reuse)
- Refresh token rotation and reuse detection (family tree)
- Expired token rejection
- Invalid audience rejection
- Missing required scope rejection
- Insufficient ACR rejection
- Rate limiter behavior: per-identity saturation, per-IP saturation, anonymous per-IP isolation
- DPoP key mismatch attack scenario rejection
- Real Keycloak integration (non-DPoP binding tests)
Current Test Status:
- 50/50 tests passing (100% pass rate)
- Integration test suites: AuthFlow (2), SecurityScenarios (6), RealKeycloak (2)
- Unit test suites: JtiReplayCache (2), DpopProofValidator (3), Idempotency (3), Auth (4), Backchannel (2), Services (2)
- All security scenario paths exercised (DPoP nonce, token replay, proof replay, rate limits, ACR, scope validation)
Acceptance tests validate end-to-end user journeys (such as high-value finance wire transfers under FAPI 2.0 bounds and continuous CAEP session revocations over SSF) against the live running API gateway.
The entire test lifecycle is fully automated. When you run the tests, the suite dynamically spins up the required local Docker containers (Redis + Keycloak), compiles and launches the self-hosted Minimal API on port 5260, executes the scenarios, and cleanly tears down all resources upon completion.
To execute the acceptance suite, simply run:
dotnet test tests/Sentinel.Tests.Acceptance/Sentinel.Tests.Acceptance.csproj -c Release- Multi-stage Docker build with locked restore and release publish
- Runtime image runs as non-root user 1654
- DOTNET_EnableDiagnostics disabled in container runtime
- Kestrel configured for TLS 1.3 and delayed client certificate mode
- FIPS compatibility switch enabled; Linux FIPS kernel flag is detected and logged
This project follows a spec-driven workflow with comprehensive documentation:
- Define or refine security behavior in SPEC-0001 and living threat model (LIVING_THREAT_MODEL.md)
- Plan implementation scope in PLAN-0001
- Track execution in TASK-0001
- Implement code and tests together (unit + integration)
- Validate with full test suite (target: 50/50+ tests passing)
- Security scan for vulnerabilities (Trivy)
- Update architecture documentation (ARCHITECTURE.md) when decisions change
- Update operational runbooks (SRE_SOC_RUNBOOKS.md) when behavior changes
- Update compliance matrix (COMPLIANCE_AUDIT_MATRIX.md) if standards impact
- Update API specification if endpoints/schemas change (OPENAPI_3_1.yaml)
All changes should:
- Align with FAPI 2.0 Baseline and RFC 9449 (DPoP) specifications
- Maintain or improve fail-closed security posture (e.g., 503 on cache unavailability, not bypass)
- Include comprehensive tests for both happy path and abuse-path behavior
- Maintain structured logging and OpenTelemetry telemetry semantics
- Pass full test suite (unit + integration); target 100% pass rate
- Pass security scan (Trivy; no critical/high vulnerabilities)
- Update architecture decisions (ARCHITECTURE.md) if design rationale changes
- Update threat model (LIVING_THREAT_MODEL.md) if new threats identified
- Update compliance matrix (COMPLIANCE_AUDIT_MATRIX.md) if framework alignment changes
- Update operational runbooks (SRE_SOC_RUNBOOKS.md) if operational procedures change
- Update API specification (OPENAPI_3_1.yaml) if endpoints or schemas change
- Update integration guide (SDK_LESS_INTEGRATION_GUIDE.md) if client-facing behavior changes
- The solution currently targets .NET 11 preview packages, which may introduce breaking changes before GA.
- HTTPS metadata validation is disabled in local development configuration and must be enforced in production.
- Some OAuth orchestration steps (e.g., PAR and PKCE client choreography) are primarily handled by Keycloak and external clients rather than API endpoints in this service.
- Redis is part of the security boundary; the service fails closed (returns 503) if Redis becomes unavailable, blocking all protected resource access until Redis recovers.
- DPoP nonce has 60-second lifetime; clients must handle nonce expiration gracefully by retrying with new nonce on 400 challenge responses.
- Refresh token rotation is enforced; clients cannot reuse rotated refresh tokens (second use triggers session blacklist and forces re-authentication).
- Rate limiting uses per-identity (authenticated) or per-IP (anonymous) partitions; multi-IP coordinated attacks require upstream CDN/WAF mitigation (see SRE_SOC_RUNBOOKS.md for DDoS procedures).
- See LIVING_THREAT_MODEL.md for complete threat assessment and ARCHITECTURE.md for design rationale behind all security decisions.
Proprietary. See LICENSE for usage terms.