---
id: CHG-20261014-RG-04
title: Sign-in allowlist: only listed TikTok accounts can connect
date: 2026-10-14
author: RG
type: security
scope: [backend, web-ui, docs]
status: done
breaking: true
patterns: []
files:
  - app/config.py
  - app/__init__.py
  - app/auth.py
  - app/models.py
  - migrations/versions/0002_account_union_id.py
  - .env.example
  - DOC/DEPLOY.md
  - tests/test_allowlist.py
gui_routes:
  - GET /auth/callback
api_endpoints: []
parity_exception: "Sign-in is a browser-only OAuth flow; the REST API is already limited to holders of API_KEY and to accounts that are connected."
api_doc_updated: n/a
security_impact: "improves — once the app is public, only TikTok accounts listed in TIKTOK_ALLOWED_ACCOUNTS can connect; production refuses to start without the list"
tests:
  - tests/test_allowlist.py
---

## What
Only the TikTok accounts listed in `TIKTOK_ALLOWED_ACCOUNTS` (open_id or union_id, comma-separated)
can sign in. Any other account is refused at the callback, nothing of it is stored, and the login
page shows its ID so the administrator can add it. A session whose account was removed from the
list is closed on the next page. `*` lets any account sign in.

## Why
The owner will run the app publicly at `ttok.thinkera.academy` but limited to specific accounts
(14 Oct 2026). Without a list, any TikTok user could connect once the app is approved.

## How
`auth.account_allowed()` checks the list; `save_account_from_login()` calls it after the token
exchange and profile fetch and before any database write; `@login_required` re-checks the active
account. `union_id` (same across the Sandbox and Production apps of one developer, unlike open_id)
is stored on the account by migration `0002_account_union_id`. `check_access_config()` makes the
app refuse to start in production with an empty list (fail closed); in development an empty list
stays open so local work needs no setup.

## API impact
None (see `parity_exception`). `GET /api/v1/accounts` lists only connected accounts, which passed
the allowlist when they connected.

## Security review
- Input validated? the list comes from the server's `.env` only; values are compared as exact strings
- Auth guard present on every new route? no new route; `@login_required` now also enforces the list
- State change on GET? `/auth/callback` (the OAuth redirect target) refuses before writing anything
- Secrets, tokens or personal data touched, logged or returned? a refused user sees their own ID only; the log line carries no ID or token
- New dependency? no

## Verification
- `.venv\Scripts\python -m pytest` → 75 passed (4 new: listed union_id signs in and is stored, unlisted account refused with its ID and not stored, removed account's session closed, production refuses an empty list).
