You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today CcasServer has no authentication, authorization, CORS, or rate-limiting layer — routes are concatenated bare and served directly, with no middleware seam (CcasServer.scala). That is fine while CCAS is single-user on loopback, but a future frontend makes the server multi-user and publicly reachable, at which point two properties are required:
Users must not read or modify data for clubs they do not manage.
Even for clubs they manage, a user must not be able to overwhelm the server (or, transitively, Chess.com).
This issue tracks that work. The design is already laid out in the CLI roadmap (ultra-sparkling-unicorn.md, "Phase 2 — Chess.com OAuth" + "Abuse defenses"); this issue makes it a tracked epic.
Not a priority yet — gated behind Phase 1 CLI work and the hosting decision (#60). Filed now so the design is recorded.
Approach (anchored on the roadmap)
Identity = Chess.com OAuth (Authorization Code + PKCE). The only flow Chess.com supports. Server holds the PKCE verifier; a pseudo-device-flow lets the CLI authenticate without binding a local port.
Authorization collapses to a join. Chess.com username (from the id_token) is exactly the key ClubAdmin rows use. Per request: resolve <club-slug> → ClubId, then SELECT 1 FROM club_admin WHERE club_id = ? AND username = ?; 403 if absent. ClubAdmin (already populated by ClubDataApp) stays the source of truth, so there is no separate ownership state to drift.
Opaque, DB-backed session tokens (hashed in user_session_token) — instantly revocable.
Maintainer powers via CCAS_MAINTAINERS env var, fail-closed at startup (server-side, unforgeable).
Same-origin serving (SPA + API on one host).
Checklist
Auth & authz:
New ccas.server.auth package: ChessComOAuthClient (PKCE, code exchange, JWKS verify), AuthMiddleware, session-token model
Tables: user_account, user_chess_com_oauth (tokens encrypted at rest), user_session_token, cli_pending_login (schemas in roadmap Phase 2 §"DB schema")
Server auth endpoints: /api/auth/cli/{start,poll}, GET /login, GET /auth/chess-com/callback, /api/auth/{logout,me}
AuthMiddleware wired around JobRoutes/ScheduleRoutes/BlacklistRoutes/RecruitmentCriteriaRoutes; HealthRoutes + auth endpoints stay public. Auth-required is the default in route registration so a new route can't accidentally ship ungated
clubAdminGuard(clubId) helper + ForbiddenException added to the UserFacingError ADT
Opportunistic ClubAdmin refresh when cached admin list is stale (keeps authz current)
Per-user quotas on authed routes: concurrency cap (extend the existing (kind,club_id) WHERE status='Running' index with a per-user COUNT) + daily job budget
Context
Today
CcasServerhas no authentication, authorization, CORS, or rate-limiting layer — routes are concatenated bare and served directly, with no middleware seam (CcasServer.scala). That is fine while CCAS is single-user on loopback, but a future frontend makes the server multi-user and publicly reachable, at which point two properties are required:This issue tracks that work. The design is already laid out in the CLI roadmap (
ultra-sparkling-unicorn.md, "Phase 2 — Chess.com OAuth" + "Abuse defenses"); this issue makes it a tracked epic.Not a priority yet — gated behind Phase 1 CLI work and the hosting decision (#60). Filed now so the design is recorded.
Approach (anchored on the roadmap)
id_token) is exactly the keyClubAdminrows use. Per request: resolve<club-slug>→ClubId, thenSELECT 1 FROM club_admin WHERE club_id = ? AND username = ?; 403 if absent.ClubAdmin(already populated byClubDataApp) stays the source of truth, so there is no separate ownership state to drift.user_session_token) — instantly revocable.CCAS_MAINTAINERSenv var, fail-closed at startup (server-side, unforgeable).Checklist
Auth & authz:
ccas.server.authpackage:ChessComOAuthClient(PKCE, code exchange, JWKS verify),AuthMiddleware, session-token modeluser_account,user_chess_com_oauth(tokens encrypted at rest),user_session_token,cli_pending_login(schemas in roadmap Phase 2 §"DB schema")/api/auth/cli/{start,poll},GET /login,GET /auth/chess-com/callback,/api/auth/{logout,me}AuthMiddlewarewired aroundJobRoutes/ScheduleRoutes/BlacklistRoutes/RecruitmentCriteriaRoutes;HealthRoutes+ auth endpoints stay public. Auth-required is the default in route registration so a new route can't accidentally ship ungatedclubAdminGuard(clubId)helper +ForbiddenExceptionadded to theUserFacingErrorADTClubAdminrefresh when cached admin list is stale (keeps authz current)Abuse defenses / hardening (roadmap "Abuse defenses" section):
Ref[Map[Ip,Counter]]); 429 +Retry-After(kind,club_id) WHERE status='Running'index with a per-userCOUNT) + daily job budgetstateparam (HMAC-SHA256) to prevent replay/tampercli_pending_login, olduser_session_token/job_run, old job logs)audit_eventtable (login, logout, token revoke, job submit, 403, quota-exceeded)ccas_appruntime vsccas_adminfor DDL)CCAS_READ_ONLYkill-switch + monitoring/alerts (429/5xx spikes, per-user submit rate)Reference
ultra-sparkling-unicorn.md— "Phase 2 — Chess.com OAuth, multi-device, hosted server" and "Abuse defenses (Phase 2 onward)".