Scaffold pal-e-auth shared library (#1) #4

Merged
forgejo_admin merged 6 commits from 1-scaffold-pal-e-auth into main 2026-02-23 22:01:46 +00:00

Summary

  • Implements JWT + Google OAuth auth middleware as a shared PyPI package (pal-e-auth-ldraney)
  • AuthConfig dataclass, auth_router factory, FastAPI dependencies (get_current_user, optional_user, require_role)
  • Role model: admin, coach, parent, viewer
  • CSRF protection via OAuth state parameter
  • Google ID token validation (iss, aud, email_verified)
  • JWT includes iss/aud claims for multi-service safety
  • PyJWT (actively maintained) for token handling
  • 29 tests covering JWT, dependencies, OAuth routes, error cases
  • Poetry build system, ruff linting, py.typed PEP 561 marker

Closes #1

Test plan

  • poetry run pytest — 29/29 pass
  • ruff check . — clean
  • ruff format --check . — clean
  • pip install pal-e-auth-ldraney after PyPI publish

Review

  • Passed automated review-fix loop (6 rounds, 4 fix iterations)
  • User approved merge

🤖 Generated with Claude Code

## Summary - Implements JWT + Google OAuth auth middleware as a shared PyPI package (`pal-e-auth-ldraney`) - `AuthConfig` dataclass, `auth_router` factory, FastAPI dependencies (`get_current_user`, `optional_user`, `require_role`) - Role model: admin, coach, parent, viewer - CSRF protection via OAuth state parameter - Google ID token validation (iss, aud, email_verified) - JWT includes iss/aud claims for multi-service safety - PyJWT (actively maintained) for token handling - 29 tests covering JWT, dependencies, OAuth routes, error cases - Poetry build system, ruff linting, py.typed PEP 561 marker Closes #1 ## Test plan - [x] `poetry run pytest` — 29/29 pass - [x] `ruff check .` — clean - [x] `ruff format --check .` — clean - [ ] `pip install pal-e-auth-ldraney` after PyPI publish ## Review - [x] Passed automated review-fix loop (6 rounds, 4 fix iterations) - [ ] User approved merge 🤖 Generated with [Claude Code](https://claude.com/claude-code)
JWT + Google OAuth auth middleware for pal-e platform services.

- AuthConfig dataclass for service configuration
- JWT creation/verification with python-jose
- Google OAuth flow (authorization URL, code exchange, ID token decode)
- FastAPI dependencies: get_current_user, optional_user, require_role
- auth_router factory with /google, /callback, /logout, /me endpoints
- Role model: admin, coach, parent, viewer
- 20 tests covering tokens, dependencies, and routes
- Poetry build system, ruff linting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

Review fixes applied

Critical

  • Made decode_id_token private (_decode_id_token) with prominent warning docstring
  • Added CSRF protection via state parameter cookie

High

  • Added error handling for missing id_token/email in callback (400 responses)
  • Wrapped exchange_code httpx errors as ValueError, route returns 502
  • Fixed bare re-raise in decode_token, now catches ValidationError and wraps as JWTError

Medium

  • Fixed logout cookie deletion to match set_cookie params (domain, secure, httponly, samesite)
  • Added configurable login_redirect_url and logout_redirect_url to AuthConfig
  • Generic error message for role denial (Insufficient permissions)
  • Fixed CLAUDE.md to use poetry commands
  • Removed unused Sequence import in dependencies.py

Tests

  • 26 tests passing, including new tests for CSRF state validation, exchange failure (502), missing id_token (400), and missing email (400)
## Review fixes applied ### Critical - Made `decode_id_token` private (`_decode_id_token`) with prominent warning docstring - Added CSRF protection via `state` parameter cookie ### High - Added error handling for missing id_token/email in callback (400 responses) - Wrapped exchange_code httpx errors as ValueError, route returns 502 - Fixed bare re-raise in decode_token, now catches ValidationError and wraps as JWTError ### Medium - Fixed logout cookie deletion to match set_cookie params (domain, secure, httponly, samesite) - Added configurable `login_redirect_url` and `logout_redirect_url` to AuthConfig - Generic error message for role denial (`Insufficient permissions`) - Fixed CLAUDE.md to use poetry commands - Removed unused Sequence import in dependencies.py ### Tests - 26 tests passing, including new tests for CSRF state validation, exchange failure (502), missing id_token (400), and missing email (400)
- Renamed _decode_id_token to decode_id_token_unverified (public, intent-clear name)
- Added from None to exception re-raises in dependencies.py and routes.py
- Added explicit 10s timeout to httpx client in exchange_code
- Added __post_init__ validation for cookie_samesite values
- Fixed CLAUDE.md: hatchling -> poetry-core
- Added py.typed marker for PEP 561
- Added case normalization in require_role

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

Round 2 review fixes

  • Renamed _decode_id_token to decode_id_token_unverified (public, intent-clear name)
  • Added from None to exception re-raises in dependencies.py and routes.py
  • Added explicit 10s timeout to httpx client in exchange_code
  • Added __post_init__ validation for cookie_samesite values
  • Fixed CLAUDE.md: hatchling → poetry-core
  • Added py.typed marker for PEP 561
  • Added case normalization in require_role
## Round 2 review fixes - Renamed `_decode_id_token` to `decode_id_token_unverified` (public, intent-clear name) - Added `from None` to exception re-raises in dependencies.py and routes.py - Added explicit 10s timeout to httpx client in exchange_code - Added `__post_init__` validation for cookie_samesite values - Fixed CLAUDE.md: hatchling → poetry-core - Added py.typed marker for PEP 561 - Added case normalization in require_role
- Replace unmaintained python-jose (CVEs) with actively maintained PyJWT
- Add iss (issuer) and aud (audience) claims for multi-service JWT safety
- Validate access_token_expire_hours >= 1 in AuthConfig
- Fix samesite validation to use tuple for deterministic error messages
- Add TODO for JWKS verification on decode_id_token_unverified
- Clarify UserLookup callback parameter names in docstring

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

Round 3 review fixes

High

  • Replaced unmaintained python-jose with PyJWT (actively maintained, smaller deps)
  • Added TODO for JWKS verification on decode_id_token_unverified

Medium

  • Added iss/aud claims to JWT tokens (multi-service safety)
  • Validated access_token_expire_hours >= 1
  • Fixed samesite validation to use tuple for deterministic error messages
  • Clarified UserLookup callback parameter names in docstring
## Round 3 review fixes ### High - Replaced unmaintained python-jose with PyJWT (actively maintained, smaller deps) - Added TODO for JWKS verification on decode_id_token_unverified ### Medium - Added iss/aud claims to JWT tokens (multi-service safety) - Validated access_token_expire_hours >= 1 - Fixed samesite validation to use tuple for deterministic error messages - Clarified UserLookup callback parameter names in docstring
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

Round 4 review fixes

High

  • Added iss/aud validation on decoded Google ID token claims

Medium

  • Renamed jose_jwt alias to jwt, fixed CLAUDE.md (python-jose → PyJWT)
  • Added email_verified check on Google claims

Low

  • Fixed overly broad pytest.raises to use just PyJWTError
  • Removed unused exp field from TokenPayload
## Round 4 review fixes ### High - Added iss/aud validation on decoded Google ID token claims ### Medium - Renamed jose_jwt alias to jwt, fixed CLAUDE.md (python-jose → PyJWT) - Added email_verified check on Google claims ### Low - Fixed overly broad pytest.raises to use just PyJWTError - Removed unused exp field from TokenPayload
The test was passing by accident — mock_claims lacked iss/aud/email_verified
so it hit the issuer validation (400) before reaching the missing-email check.
Now properly tests the "No email in id_token claims" branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.