Replace pal-e-auth with Keycloak OIDC JWT validation #77
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/basketball-api#77
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Lineage
plan-2026-03-08-tryout-prep→ Phase 5 → Phase 5c (basketball-api OIDC)Repo
forgejo_admin/basketball-apiUser Story
As a platform operator
I want basketball-api to validate Keycloak-issued JWTs instead of using the custom pal-e-auth library
So that the auth chain is unified through Keycloak IdP and we can remove the custom auth dependency
Context
Keycloak IdP is deployed at
keycloak.tail5b443a.ts.netwith realmwestside-basketball. It issues standard OIDC JWTs with roles inrealm_access.roles. The custompal-e-auth-ldraneylibrary wraps Google OAuth + custom JWT issuance — this is being replaced entirely. Keycloak handles authentication; basketball-api only needs to validate incoming JWTs via the JWKS endpoint.The existing
Usertype from pal_e_auth andrequire_role()function are used in 3 route files + 2 test files. The new auth module must provide the same interface so route files need minimal changes (just import path).Architecture decision: No client secret needed in basketball-api. It validates tokens via Keycloak's public JWKS endpoint. Only westside-app (the OIDC Relying Party) needs the client secret.
File Targets
Files to modify or create:
src/basketball_api/auth.py— CREATE: new auth module withUserdataclass,get_current_user()FastAPI dependency,require_role()factorysrc/basketball_api/main.py— removepal_e_authimports (AuthConfig,auth_router), remove_build_auth_config(), removeauth_router(_auth_config), removeapp.state.auth_config. Keep lifespan for Stripe.src/basketball_api/config.py— replacejwt_secret_key,google_client_id,google_client_secretwithkeycloak_realm_url(default:http://keycloak.keycloak.svc.cluster.local:80/realms/westside-basketball)src/basketball_api/routes/admin.py— changefrom pal_e_auth import User, require_roletofrom basketball_api.auth import User, require_rolesrc/basketball_api/routes/roster.py— same import changesrc/basketball_api/routes/tryouts.py— same import changepyproject.toml— removepal-e-auth-ldraney>=0.1.0, addpython-jose[cryptography]>=3.3andhttpx>=0.27(httpx already in dev deps, move to main)k8s/deployment.yaml— replace Google OAuth env vars withBASKETBALL_KEYCLOAK_REALM_URLtests/conftest.py— removeBASKETBALL_JWT_SECRET_KEY,BASKETBALL_GOOGLE_CLIENT_ID,BASKETBALL_GOOGLE_CLIENT_SECRETenv setup. Add JWKS mock setup.tests/test_admin.py— changefrom pal_e_auth import Usertofrom basketball_api.auth import Usertests/test_tryouts.py— same import changeFiles NOT to touch:
src/basketball_api/routes/register.py— no authsrc/basketball_api/routes/health.py— no authsrc/basketball_api/routes/webhooks.py— Stripe webhook verification, not JWT authsrc/basketball_api/routes/coach.py— check if it uses auth, modify only if needed/api/roster/*,/tryouts/roster/*,/pay) — no auth, don't add anyNew auth.py Design
Acceptance Criteria
pal-e-auth-ldraneyis removed from dependencies and not imported anywhere/admin/*,/tenants/*/roster,POST /tryouts/admin/*/checkin/*) return 401 without a valid JWT/api/roster/*,/tryouts/roster/*,/healthz,/pay) work without any auth headerBASKETBALL_KEYCLOAK_REALM_URLinstead of Google OAuth env varspal-e-auth-secretsk8s Secret reference is removed from deploymentTest Expectations
get_current_userextracts roles correctlyrequire_role("admin")returns 403 for user with onlyplayerrolepytest tests/ -vConstraints
Userdataclass must have arolesfield (list of strings) — used byrequire_rolerequire_role("admin")/require_role("admin", "coach")call signaturesrequire_admin = require_role("admin")pattern must still work (used in admin.py and tryouts.py)get_current_userdependency should be async (Keycloak JWKS fetch is async)Depends(require_admin)may need to become async if they aren't already — check and convert as neededChecklist
Related
plan-2026-03-08-tryout-prep— parent planwestside-basketballis live