Wave 3a: restore auth foundation (closes JWT verification gap) - #9
Merged
Conversation
…-AI-Blueprints#169) * feat: add register_token_fetcher plugin hook for auth extensibility Allow external plugins to register additional token sources without monkey-patching. Registered fetchers are tried in priority order before the default Context cookie lookup. This enables internal extensions (e.g., Starfleet auth) to integrate cleanly via register_token_fetcher(get_starfleet_token) instead of replacing get_auth_token at import time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback on auth plugin hook - Add threading.Lock for thread-safe register/clear operations - Iterate a snapshot in get_auth_token to prevent concurrent mutation - Add deduplication check (identity-based) to prevent double registration - Strengthen clear_token_fetchers docstring warning about production use - Add test for deduplication behavior --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s#173) * update nat version and compatibility fixes * middleware for auth for api access * add unit tests for aiq api auth * fix issues with missing dep packages * direct import from module * add nvidia-nat-core dependency * make auth error user facing * remove duplicate status field * edit pyproject.toml * remove log which prints token * fix bugs in validator * fix ruff check * fix ruff check * fix ruff version * add aiq api as known first party * lint fixes for new ruff * fix dead links * revert links
* set size cap on reads * add failure log
Upstream test_matches_key_by_kid sets `_jwks_keys_fetched_at = 0.0` with `_jwks_cache_ttl = 999999.0`. On long-uptime hosts where `time.monotonic()` returns >11.6 days (~999999 s), the TTL check trips and the test follows the cache-refresh branch instead of the kid-match branch. Caught locally on a 13-day-uptime DGX Spark. Trivial fix: use `time.monotonic()` for the fetched-at marker so the cache is "just fetched" regardless of host uptime. No production-code change. Plus uv.lock regenerated locally to reflect Wave 3a's PyJWT[cryptography] addition under our NAT 1.6 baseline (rather than upstream's 1.5 lock diff). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First step of Wave 3 — restores the auth subsystem deleted from
red-hat-v2.1.0at fork time. Closes the JWT signature-verification gap flagged inproject_jwt_verification_gap.md(the standing MUST-FIX). 4 cherry-picks from upstream/develop + 1 small follow-up fix.880fbececa963c7fec6847fe05081a5747ce51e0f8287f7d561af1495d93b8fWhat this PR does
Restores
frontends/aiq_api/src/aiq_api/auth/(deleted on RH at migration), which adds:JWTValidator— PyJWT-backed RS256/ES256/ES512 signature verification, OIDC discovery (${issuer}/.well-known/openid-configuration), JWKS fetch + cache (6h TTL, 64 KB read cap from fix: set size cap on reads NVIDIA-AI-Blueprints/aiq#180)AuthMiddleware— raw ASGI middleware that gates non-allowlisted endpoints whenREQUIRE_AUTH=true./docs,/redoc,/openapi.jsonallowlisted (fix: allow access to /docs NVIDIA-AI-Blueprints/aiq#178)TokenValidatorABC +AuthError— extension contract for additional validators (e.g. API keys, opaque tokens)register_token_fetcherplugin hook (Add register_token_fetcher plugin hook for auth extensibility NVIDIA-AI-Blueprints/aiq#169) onaiq_agent.auth— extensibility for upstream-aligned token sourcingPlus integrations:
chat_researcherreadsskip_clarifierfrom authenticated user context (set by future validators for headless / API callers)deep_researcherpropagatesAuthErrorto the user instead of generic retryfrontends/aiq_api/plugin.pyregisters the middleware, scansaiq_api.validatorsentry-point group at startupAIQ_EXTERNAL_HOSTNAMES,REQUIRE_AUTH,AIQ_JWT_ISSUER,AIQ_JWT_AUDIENCE(added todeploy/.env.examplein a later wave)Why now
Closes the JWT verification gap identified during PR #7 (Wave 2a) review. Per
feedback_stay_in_sync_with_upstream.md: the deletion of upstream's auth/ subsystem onred-hat-v2.1.0was an undocumented deviation introducing real attack surface (any code path that puts attacker-controlled tokens into theidTokencookie could spoof identity). Restoring upstream is the cleanest remediation and aligns with the "prefer NVIDIA" principle.The middleware is permissive by default (
REQUIRE_AUTH=false). Enforcement gets turned on by deployment configuration once the env-var documentation is staged in Wave 3c (NVIDIA-AI-Blueprints#203). This staging keeps the PR low-risk for existing consumers — Wave 3a delivers the machinery; the legacy unverifieddecode_jwt_payloadretires in 3b/3c.Skipped / deviated from upstream
pyproject.tomladdsnvidia-nat-core==1.5.0andnvidia-nat-eval==1.5.0==1.6.0; kept our existing 1.6.0 separate-pin layoutuv.lockupstream diffuv lockfrontends/cli/auth/local_token_store.py(122 lines, fork-side dead code)scripts/setup.shdata-source install lines--no-depsremoval for the lines they modify; added ourserper_news_searchline to the cleaned-up sequencefrontends/aiq_api/tests/test_auth.py::test_matches_key_by_kidtime.monotonic()for the JWKS fetched-at marker_jwks_keys_fetched_at = 0.0makes the test flaky on long-uptime hosts (>11.6 days) wheretime.monotonic()exceeds_jwks_cache_ttl=999999.0and trips the cache-refresh branch. Trivial test fix, no production-code changeAll other upstream lines applied verbatim per
feedback_stay_in_sync_with_upstream.md.Branding / vLLM exception scan — clean
Per the sharpened rule (
feedback_stay_in_sync_with_upstream.md): "prefer NVIDIA unless it touches branding or vLLM."frontends/ui/docs/source/customization/README.md,MIGRATION.mddeploy/.env.example,configs/config_web_vllm.yml,docs/source/customization/vllm-*.mdfrontends/aiq_api/README.md— NEW upstream content only, NVIDIA-authored, no fork-framing claimsscripts/setup.sh— RHserper_news_searchinstall line preserved; upstream's--no-depsremoval appliedVerification
Static
uv lock --check— 366 packages, lockfile self-consistentruff check .— cleanpytest tests/ frontends/aiq_api/tests/— 975 passed, 19 skipped, 4.90s (up 73 from PR chore: bump NeMo Agent Toolkit pin to 1.6.0 #8 baseline of 902 —#169adds ~5,#173adds ~68 intest_auth.py)uv run python -c "from aiq_api.auth.{base,errors,jwt_validator,middleware} import *"— all 4 modules import cleanlyBroader smoke (per
feedback_upstream_sync_planning.md)Sync
/chat/streamagainst NIM (Nemotron3-Nano-30B):Async
/v1/jobs/async/submit + /streamagainst NIM:WebSocket
/websocketagainst NIM:Auth allowlist (
REQUIRE_AUTH=false):Auth gate (
REQUIRE_AUTH=true, no validators registered):✅ Refuses to start (correct behavior — prevents footgun deploys).
What's NOT in this PR
aiq_agent.auth.__init__.pyAPI surface (Principal, get_verified_current_user, etc.). Per-file branding-aware UI review required.routes/jobs.pyheavily,deploy/.env.example(vLLM exception territory — needs careful diff),docs/source/{deployment,customization}/(fork-framing exception territory).🤖 Generated with Claude Code