Skip to content

feat(registration): verify FACEIT team ownership - #11

Open
gouveags wants to merge 2 commits into
mainfrom
feat/verify-faceit-team-ownership
Open

feat(registration): verify FACEIT team ownership#11
gouveags wants to merge 2 commits into
mainfrom
feat/verify-faceit-team-ownership

Conversation

@gouveags

@gouveags gouveags commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add FACEIT Authorization Code + PKCE endpoints with signed, short-lived state
  • issue an HTTP-only ownership proof only when the authenticated FACEIT account is the current team leader
  • require and revalidate that proof before reading registration uploads or creating a registration
  • canonicalize www.aceprodutora.com.br to the apex host before host-only OAuth cookies are issued
  • require valid returned state on provider-denial callbacks before clearing OAuth state
  • preserve the current closed-registration UI and active claimKey safeguards from main

Problem

Anyone who knew a public FACEIT team URL could register that team without proving control of it. This allowed team-name squatting and could force the real team representative into a manual recovery path. The initial OAuth implementation also allowed the apex/www split to strand a __Host- state cookie and allowed an invalid error callback to cancel a valid flow.

Security design

  • OAuth state and PKCE verifier expire after 10 minutes
  • ownership proof expires after 15 minutes and uses Secure, HttpOnly, SameSite=Lax, and __Host- cookies in production
  • Caddy permanently redirects www requests, including their path and query, to https://aceprodutora.com.br
  • callback redirects are derived only from the configured FACEIT redirect URI
  • success and provider-denial callbacks must match the signed state cookie
  • invalid or missing returned state cannot clear an in-progress state cookie
  • the current FACEIT leader is checked after OAuth and checked again immediately before registration
  • requests without a valid proof are rejected before the multipart body is read
  • OAuth start attempts retain the existing trusted-proxy IP rate-limit model

Validation

  • npm ci
  • npm run db:generate
  • npm run db:migrate
  • npm run test:security
  • npm run lint
  • npx tsc --noEmit
  • npm run test:tournaments
  • npx prisma validate
  • npm audit (0 vulnerabilities)
  • npm run build
  • manual OAuth/state/cookie/redirect/replay and secret-exposure review

Focused coverage includes apex callback/return URLs, the www to apex Caddy contract, production __Host- naming, valid provider denial, invalid and missing returned state, tampered state/proof tokens, mocked token exchange, and rejection before multipart parsing.

Deployment validation still required

Create a FACEIT OAuth2 client with callback https://aceprodutora.com.br/api/faceit/ownership/callback, then set FACEIT_OAUTH_CLIENT_ID, FACEIT_OAUTH_CLIENT_SECRET, FACEIT_OAUTH_REDIRECT_URI, and a 32+ character FACEIT_OAUTH_COOKIE_SECRET. Before registrations reopen, restore the registration UI wired to the ownership-start endpoint and run a real apex/www, success/denial/replay/leader-change matrix with the deployed FACEIT client. caddy validate was not available in this local environment and must run on the deployment target before reload.

@gouveags

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@cespedesdan

Copy link
Copy Markdown
Owner

Vamos verificar o funcionamento de forma local antes do deploy

@gouveags

Copy link
Copy Markdown
Collaborator Author

Independent security review

Current state/checks: OPEN but currently CONFLICTING/DIRTY against main; the recorded Validar aplicação run passed on the original base and the PR-only AWS publish job was skipped. No unresolved review threads were present. No real FACEIT OAuth round trip is evidenced yet, matching the owner's request to verify locally before deploy.

Actionable (blocking): Production state uses host-only __Host- cookies (src/lib/faceit-ownership.ts:101-115), while deploy/Caddyfile:10 serves both aceprodutora.com.br and www.aceprodutora.com.br without canonical redirection and the documented callback is on the apex host. Starting verification from www sets the state cookie on www; FACEIT returns to the apex, which cannot receive that cookie, so callback validation fails. Canonicalize all traffic to one host before OAuth (preferred) or otherwise make start/callback use one guaranteed origin while retaining host-only cookies, and test both public hostnames.

Actionable (lower severity): src/app/api/faceit/ownership/callback/route.ts:37-38,72 accepts any error callback without validating returned state, then clears the state cookie. A cross-site top-level GET can therefore cancel an in-progress flow. FACEIT error callbacks should carry state; validate it before treating the error as belonging to this flow or clearing the cookie.

Residual/informational: The documented FACEIT OIDC endpoints, PKCE S256, client_secret_basic, signed expiries, proof/team/leader binding, and pre-body rejection are coherent. Reconcile #2's active claimKey path plus #3/#4/#9, then rerun CI and a real apex/www, success/denial/replay/leader-change matrix before deployment.

@gouveags

Copy link
Copy Markdown
Collaborator Author

Addressed both independent-review findings in commit b15b01e.

  • merged current main and preserved the deployed active claimKey logic
  • retained main's closed-registration UI instead of resurrecting the removed form
  • split the Caddy hosts so every www path/query permanently redirects to https://aceprodutora.com.br before __Host- OAuth cookies are created
  • provider-error callbacks now validate the signed returned state first
  • invalid or missing state no longer clears a legitimate in-progress state cookie
  • added focused apex/www, production cookie-name, denial, invalid-state, and missing-state coverage

Validation passed: current migrations, npm run test:security, lint, TypeScript, tournament tests, Prisma validation, npm audit (0 vulnerabilities), and the production build. Manual security review found no unresolved code issue: state is timing-safe and signed, PKCE/access tokens are not logged, proof and current leader/team are rebound before body processing, and current claim safeguards remain.

Deployment validation remains explicit: a real FACEIT OAuth success/denial/replay/leader-change matrix requires the production OAuth client and credentials. The local environment did not contain the Caddy binary, so caddy validate must also run on the target before reload.

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.

2 participants