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.