---
id: CHG-20261014-RG-01
title: Login: send PKCE code challenge (TikTok refused authorize without it)
date: 2026-10-14
author: RG
type: bugfix
scope: [backend]
status: done
breaking: false
patterns: []
files:
  - app/auth.py
  - app/tiktok_api.py
  - tests/test_auth_pkce.py
gui_routes: []
api_endpoints: []
parity_exception: "Login is a browser-only OAuth flow; it has no API twin by design (API-PARITY browser exception for auth routes)."
api_doc_updated: n/a
security_impact: "improves — the authorization code is now bound to a per-flow PKCE verifier kept server-side in the session, so an intercepted code cannot be redeemed without it"
tests:
  - tests/test_auth_pkce.py
---

## What
"Continue with TikTok" now reaches TikTok's consent screen. Before, TikTok answered with "Something
went wrong … code_challenge" and no account could ever be connected.

## Why
**Problem:** TikTok's authorize endpoint rejected the request with `error_type=code_challenge`
(seen 14 Oct 2026 with the configured client key). The app sent no PKCE parameters.
**Fix:** every login flow generates a fresh code verifier, sends its challenge with
`code_challenge_method=S256`, and sends the verifier back on the token exchange.

## How
`auth.pkce_pair()` returns a 64-character verifier from `secrets.token_urlsafe(48)` (only
unreserved characters) and its challenge. TikTok documents the challenge as the **hex** SHA-256 of
the verifier ("You must use hex encoding of SHA256"), not RFC 7636's base64url, so the code follows
TikTok. The verifier lives in the Flask session next to `oauth_state` and is popped once on the
callback; a callback without it is refused like a state mismatch. `tiktok_api.exchange_code` takes
an optional `code_verifier` and stays Flask-free.

## API impact
None. Login is browser-only (see `parity_exception`).

## Security review
- Input validated? yes — the callback still requires code + matching state, and now also a verifier from the same session
- Auth guard present on every new route? no new route
- State change on GET? no change: `/auth/callback` is the OAuth redirect target, as before
- Secrets, tokens or personal data touched, logged or returned? the verifier is kept in the signed session only; it is never logged, never put in a URL (a test asserts the redirect URL does not contain it)
- New dependency? no (`hashlib`, `secrets` from the standard library)

## Verification
- `.venv\Scripts\python -m pytest` → 71 passed (4 new PKCE tests).
- Manual, before the fix: `/auth/start` → TikTok page "correct the following: code_challenge".
- Manual check after the fix: see `## Verification` of CHG-20261014-RG-02 (the consent screen is reached once the container is rebuilt).
