From 062354ab57c25ffc4cf21730ba280f5dcd07bee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 22 Jun 2026 07:20:41 +0200 Subject: [PATCH] feat(fitbit): add PKCE code_verifier column to the OAuth-state ledger The classic Fitbit Web API authorises with PKCE (Authorization Code + S256), so the connect route mints a code_verifier and the callback presents it on the token exchange. Carry it on the short-lived FitbitOAuthState row. Additive + nullable (migration 0191): a legacy in-flight row carries no verifier, so the change is reversible and rerun-safe with no data loss. --- .../migration.sql | 8 ++++++++ prisma/schema.prisma | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 prisma/migrations/0191_v1200_fitbit_pkce_verifier/migration.sql diff --git a/prisma/migrations/0191_v1200_fitbit_pkce_verifier/migration.sql b/prisma/migrations/0191_v1200_fitbit_pkce_verifier/migration.sql new file mode 100644 index 000000000..edb1e15e1 --- /dev/null +++ b/prisma/migrations/0191_v1200_fitbit_pkce_verifier/migration.sql @@ -0,0 +1,8 @@ +-- v1.20.0 — classic Fitbit Web API (api.fitbit.com) authorises with PKCE +-- (Authorization Code + S256). Carry the per-handshake `code_verifier` from the +-- connect route to the callback on the short-lived OAuth-state ledger row. +-- +-- Additive + nullable: a legacy in-flight row (or any non-PKCE path) simply +-- carries no verifier, so this is reversible and rerun-safe with no data loss. +-- `IF NOT EXISTS` guards a prod rerun. +ALTER TABLE "fitbit_oauth_states" ADD COLUMN IF NOT EXISTS "code_verifier" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e6839cdce..1e1660403 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2644,11 +2644,20 @@ model FitbitConnection { /// v1.12.0 — short-lived `(nonce → userId)` ledger backing the Fitbit OAuth /// state check, identical in shape + lifecycle to `WhoopOAuthState`. +/// +/// v1.20.0 — the classic Fitbit Web API (`api.fitbit.com`) authorises with PKCE +/// (Authorization Code + S256), so the connect route mints a `code_verifier` +/// alongside the nonce and stashes it here; the callback reads it back to present +/// on the token exchange. Nullable + additive: a legacy row (or a non-PKCE path) +/// simply carries no verifier. model FitbitOAuthState { - nonce String @id - userId String @map("user_id") - createdAt DateTime @default(now()) @map("created_at") - expiresAt DateTime @map("expires_at") + nonce String @id + userId String @map("user_id") + /// PKCE code_verifier carried from connect to callback (v1.20.0). Null when no + /// PKCE handshake is in flight (legacy rows). + codeVerifier String? @map("code_verifier") + createdAt DateTime @default(now()) @map("created_at") + expiresAt DateTime @map("expires_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade)