Skip to content

feat: API authentication with hashed keys and roles (#11) - #39

Merged
cursor[bot] merged 2 commits into
mainfrom
cursor/api-authentication-1cc6
Aug 17, 2026
Merged

cursor[bot] merged 2 commits into
mainfrom
cursor/api-authentication-1cc6

Conversation

@leo-aa88

Copy link
Copy Markdown
Member

Closes #11.

Adds HTTP API authentication so raglogs can be exposed to another service. Default remains off so local demo and existing TestClient tests keep working.

API keys

  • Authorization: Bearer rlk_… keys stored argon2-hashed in api_keys (Alembic 0004_api_keys).
  • Each key has a role (ingest / query / admin) and a scope string (default default) for later G8. Log queries are not filtered by scope in this PR.
  • CLI: raglogs keys create|list|revoke. The plaintext secret is printed once and never logged.

Roles

Route Roles
GET /health, GET /metrics* exempt
POST /ingestions ingest, admin
GET /ingestions* query, admin
POST /query/* query, admin
GET /config admin
UI /, /static*, /docs query, admin

Missing/invalid bearer → 401 { "error_code": "AUTH_UNAUTHORIZED", "message": "..." }. Wrong role → 403 AUTH_FORBIDDEN.

OIDC

Optional JWT validation when AUTH_MODE=oidc or both (PyJWT + JWKS cache). Unit tests mock JWKS; no live IdP required.

Insecure bind

AUTH_ENABLED=false on a non-loopback bind logs a warning. AUTH_REFUSE_INSECURE_BIND=true refuses startup. Docker compose documents AUTH_ENABLED=true for a networked API.

Tests

Unit tests cover /health bypass, 401/403 bodies, role enforcement, revoked keys, mocked OIDC, and the bind guard. Default auth-off path leaves existing API tests unchanged.

Open in Web Open in Cursor 

cursoragent and others added 2 commits August 17, 2026 06:50
Protect the HTTP API behind Bearer keys (argon2) and optional JWTs while
keeping AUTH_ENABLED=false so local demo and existing tests stay open.

Closes #11

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
Keep src.api.auth.__init__ empty so TestClient imports do not pull
PasswordHasher until a key is actually verified.

Refs #11

Co-authored-by: Leonardo <leo-aa88@users.noreply.github.com>
@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Code review — PR #39 (issue #11)

CI is green. Review is of origin/main...origin/cursor/api-authentication-1cc6 (c41053a + ce53e38). G8 scope filtering and G3 /v1 were treated as out of scope.

Checked hashing/logging, middleware exemptions, role map (including GET vs POST /ingestions), 401/403 bodies, default AUTH_ENABLED=false, OIDC sig/iss/exp, bind guard, migration 0004 vs untouched 0001–0003, unprefixed settings, and the new unit tests. No auth bypass, secret in the diff, or broken argon2 path showed up.

must-fix

None

should-fix

None

nice-to-have

  • src/api/auth/oidc.py:103-113 (get_jwks) and _key_from_jwks: JWKS is cached for process lifetime and an unknown kid does not refetch. After a routine IdP rotation, valid JWTs 401 until restart.
  • src/api/app.py:19 (warn_if_insecure_bind(settings.api_bind_host, …)): the guard reads API_BIND_HOST (default 127.0.0.1), not uvicorn’s --host. make api / Compose set both; the README uvicorn … --host 0.0.0.0 one-liner does not, so AUTH_ENABLED=false on all interfaces can start with no warning and no refuse.
  • src/api/auth/oidc.py:142-152: when OIDC_AUDIENCE is empty, verify_aud is off. Any JWT from that issuer (including tokens minted for another app) authenticates with default role query (role_from_claims fallback). Consider requiring audience whenever AUTH_MODE is oidc or both.

Verdict

must-fix count: 0
should-fix count: 0

What looks correct (not findings): keys are argon2-hashed (PasswordHasher / $argon2), plaintext is returned only from create_api_key / the CLI panel and is not logged; revoked rows are excluded in SQL and skipped in find_verified_key (401); exemptions are /health and /metrics only (/docs is 401); ingest cannot POST /query/*, query cannot POST /ingestions, admin can both; 401/403 bodies use error_code; default auth-off leaves tests/unit/test_api.py valid; OIDC uses an algorithm allowlist (no none/HS*), checks iss/exp, and AUTH_MODE=api_key does not call validate_oidc_token; 0004 is a new revision off 0003; settings are AUTH_* / OIDC_* / API_BIND_HOST with no RAGLOGS_ prefix.

@cursor
cursor Bot merged commit 60be5f3 into main Aug 17, 2026
1 check passed
@leo-aa88
leo-aa88 deleted the cursor/api-authentication-1cc6 branch August 17, 2026 07:23
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.

feat(G2): API authentication (hashed API keys, roles, optional OIDC)

2 participants