NUSGreyhats/ctfd-team-token-plugin

GitHub: NUSGreyhats/ctfd-team-token-plugin

Stars: 0 | Forks: 0

# CTFd Team Token Plugin CTFd challenge-type plugin that issues a **unique DB-backed token per `(team, challenge)`** and substitutes `{TEAM_TOKEN}` in challenge descriptions. External services resolve tokens through a plugin-specific server-to-server API. See [docs/PRD.md](docs/PRD.md) for requirements and acceptance criteria. ## Quick start (Docker) From the repo root: ./scripts/dev-up.sh python scripts/acceptance_tests.py Or manually: cd docker docker compose up -d --build docker compose --profile seed run --rm seed python ../scripts/acceptance_tests.py Services: | Service | URL | |---------|-----| | CTFd | http://localhost:8000 | | Example external challenge | http://localhost:5001 | | Plugin config | http://localhost:8000/admin/config → **Team Token** tab | Default credentials: | Account | Password | Team | |---------|----------|------| | `admin` | `Password123!` | — | | `alpha` | `Password123!` | TeamAlpha | | `beta` | `Password123!` | TeamBeta | Dev plugin API secret (server-to-server): `dev-plugin-secret-for-testing` ## Install on an existing CTFd Clone into CTFd's plugins directory (the folder name must be `team-token`): git clone https://github.com/NUSGreyhats/ctfd-team-token-plugin.git CTFd/plugins/team-token Restart CTFd, then open **Admin → Configuration → Team Token** to view the resolve API secret. Dev-only paths (`docker/`, `scripts/`, `example-challenge/`, `docs/`) live alongside the plugin files at the repo root; CTFd ignores them. ## Admin workflow 1. Create a challenge with type **team_token**. 2. Put `{TEAM_TOKEN}` anywhere in the description. 3. Add a normal static flag. 4. Share the plugin API secret only with trusted challenge backends. Example description: Your token: `{TEAM_TOKEN}` curl -H "X-Team-Token: {TEAM_TOKEN}" https://your-challenge.example/start ## Resolve API GET /plugins/team-token/api/v1/resolve?token=tt_... Authorization: Bearer Responses: {"valid": true, "team_id": 1, "team_name": "TeamAlpha", "challenge_id": 3, "solved": false, "solved_at": null} {"valid": false} Configure the secret and enable/disable the API under **Admin → Configuration → Team Token**. The resolve endpoint is a **path**, not a full URL: GET {CTFD_BASE_URL}/plugins/team-token/api/v1/resolve?token=tt_... Authorization: Bearer {team_token_plugin_secret} Use whatever base URL your external challenge server can reach (`http://ctfd:8000` inside Docker, your public hostname in production). CTFd cannot pick that for you. ## Example external challenge The `example-challenge/` service demonstrates the intended flow: 1. Player copies `{TEAM_TOKEN}` from CTFd. 2. Browser sends it to `POST /api/enter` with header `X-Team-Token`. 3. Backend calls CTFd resolve API with the **plugin secret** (not a CTFd admin token). 4. UI unlocks once the team is identified and `solved` is false. Try it after seeding: curl -H "Authorization: Bearer dev-plugin-secret-for-testing" \ "http://localhost:8000/plugins/team-token/api/v1/resolve?token=YOUR_TEAM_TOKEN" ## Plugin layout The repo root is the CTFd plugin (standard layout for `git clone` installs): team-token/ # clone target: CTFd/plugins/team-token ├── __init__.py ├── api.py ├── challenge.py ├── config.json ├── models.py ├── tokens.py ├── migrations/ ├── assets/ ├── templates/ ├── docker/ # local dev stack only ├── scripts/ ├── example-challenge/ └── docs/ ## Development notes - Tokens are created lazily on first challenge view. - Admins editing challenges still see the raw `{TEAM_TOKEN}` placeholder. - MVP targets **team mode** only. - Docker CTFd data volumes are gitignored under `docker/.data/` if used locally. ## Acceptance tests With the stack running and seeded: CTFD_RESOLVE_URL=http://localhost:8000/plugins/team-token/api/v1/resolve \ CTFD_TEAM_TOKEN_PLUGIN_SECRET=dev-plugin-secret-for-testing \ python scripts/acceptance_tests.py Covers PRD tests T1–T5, T6/T7, and T8.