Skip to content

Add OIDC JWT authentication and group-based authorization#32

Open
hanstrompert wants to merge 13 commits into
mainfrom
feature/oidc-jwt-auth
Open

Add OIDC JWT authentication and group-based authorization#32
hanstrompert wants to merge 13 commits into
mainfrom
feature/oidc-jwt-auth

Conversation

@hanstrompert
Copy link
Copy Markdown
Member

Summary

  • Add opportunistic OIDC JWT validation using PyJWT: tokens are validated when present (OIDC ingress path) but requests without a Bearer token pass through (mTLS ingress path), enabling a single deployment behind both ingresses
  • Group-based authorization via OIDC userinfo endpoint with configurable group claim and TTL-cached responses
  • 9 new environment variables (OIDC_ENABLED, OIDC_ISSUER, OIDC_AUDIENCE, etc.) — auth is disabled by default for backward compatibility
  • Structured logging at INFO (authn/authz decisions), WARNING (rejected tokens), DEBUG (claims, userinfo, JWKS details)

Test plan

  • 120 tests pass (91 existing + 29 new auth tests)
  • ruff lint clean
  • mypy strict mode clean
  • Deploy with OIDC_ENABLED=false — verify all endpoints work without tokens (backward compat)
  • Deploy with OIDC_ENABLED=true behind OIDC ingress — verify JWT validation and /docs access
  • Deploy with OIDC_ENABLED=true behind mTLS ingress — verify passthrough without JWT
  • Verify /health returns 200 without a token in all configurations
  • Verify group-based authorization with OIDC_REQUIRED_GROUPS set

🤖 Generated with Claude Code

hanstrompert and others added 13 commits April 30, 2026 13:36
- Add PyJWT[crypto] dependency for RS256 JWT validation
- Create dds_proxy/auth.py with OIDCProvider class (JWKS key
  retrieval via PyJWKClient, userinfo endpoint with TTL cache)
- Add get_authenticated_user FastAPI dependency applied to all
  data routes via include_router(dependencies=...)
- Token validation is opportunistic: JWTs are validated when
  present but requests without a Bearer token pass through,
  enabling a single deployment behind both mTLS and OIDC ingresses
- Add 9 OIDC settings to config.py (issuer, audience, JWKS URI,
  userinfo URI, group claim, required groups, cache TTLs)
- Support comma-separated and JSON array formats for
  OIDC_REQUIRED_GROUPS via field validator
- Separate vanilla httpx.AsyncClient for OIDC calls (not the
  mTLS DDS client), with auto-discovery from .well-known endpoint
- Add structured logging at INFO (authn/authz decisions),
  WARNING (rejected tokens), and DEBUG (claims, userinfo, JWKS)
- Add 29 tests covering token extraction, signature validation,
  claim validation, group authorization, passthrough, config parsing
- Update CLAUDE.md, README.md, and dds_proxy.env

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Document that OIDC_REQUIRED_GROUPS must be `[]` (not empty string)
  when no groups are required — pydantic-settings JSON-parses list env
  vars before field validators run, so `""` causes a SettingsError
- Update README.md default from _(empty)_ to `[]` with explanation
- Add caveat to CLAUDE.md Key Design Decisions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Log each request header name and value at DEBUG level in the auth
  dependency to diagnose header forwarding issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The nginx ingress controller has a known issue where it clears the
Authorization header from auth subrequest responses. Custom X-* headers
are forwarded correctly.

- Check Authorization: Bearer first, fall back to
  X-Auth-Request-Access-Token when absent (set by oauth2-proxy with
  --pass-access-token)
- Add info logging for which token source is used
- Parametrize signature validation test for both header paths
- Document the nginx issue and fallback behavior in README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove per-header debug loop added for diagnosing nginx header
  forwarding; targeted debug logs for token claims, userinfo, and
  group checks remain

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- check_groups now returns the matched groups list
- Log email and matched_groups at INFO level on successful group
  authorization for easier auditing
- Update check_groups tests to verify returned matched groups

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace oidc_enabled with auth_enabled and add mtls_header setting
- Rewrite auth flow: auth off = passthrough, auth on = OIDC or mTLS
  must succeed, otherwise 401
- OIDC is active when oidc_issuer is set; mTLS is active when
  mtls_header is set and the header is present in the request
- Add mTLS test class with 5 tests covering header present, absent,
  JWT priority, and fallback scenarios
- Update existing tests for new auth_enabled setting
- Update README, CLAUDE.md, and dds_proxy.env template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add test for invalid JWT not rescued by mTLS header (security path)
- Add tests for empty and whitespace mTLS header values
- Add test for auth enabled with neither method configured
- Add test for mTLS success without X-Client-DN header
- Consolidate mTLS-only tests into parametrized test
- Extract _mtls_only_client and _dual_auth_client helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bound userinfo cache to 1024 entries to prevent unbounded memory growth
- Consolidate 4 JWT exception handlers into single handler with message
  lookup table
- Replace two-pass cache eviction with single-pass dict comprehension
- Read X-Auth-Request-Access-Token header once, reuse for group lookup
- Extract _mock_dds_http_client() helper to deduplicate test setup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pydantic-settings JSON-parses list env vars, so bare strings can
  cause startup errors. Use JSON array to match documented guidance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Mermaid flowchart showing mTLS and OIDC ingress paths, auth
  services, header flows, and dds-proxy auth decision logic
- Defense-in-depth measures and header flow summary tables
- Link from README Authentication section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Inline Mermaid diagram, defense-in-depth table, and header flow
  summary directly into the Authentication section of README
- Remove standalone artwork/dual-ingress-auth.md (artwork/ is for
  images, not documentation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Reorder except blocks so PyJWKClientError is caught before
  PyJWTError (it's a subclass), returning generic "Token validation
  failed" instead of leaking internal error details
- Sanitize userinfo fetch error response to not expose internal
  error messages to clients
- Wrap json.loads in OIDC_REQUIRED_GROUPS validator with proper
  error handling for malformed JSON input
- Validate OIDC discovery returns both jwks_uri and
  userinfo_endpoint, failing fast at startup if incomplete
- Add tests for JWKS failure, sanitized error messages, and
  malformed JSON config
- Document error responses table in README authentication section
- Clarify access token requirement for group-based authorization
- Use "nsi-auth" consistently instead of "auth service"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant