# Guide — Config parfaite Google OAuth (Better Auth) pour SEO Insight

- **Date** : 2026-07-23
- **Scope** : login Google + connexion Search Console (scope `webmasters.readonly`).
- **Console** : [console.cloud.google.com](https://console.cloud.google.com) → APIs & Services.

## 0. Le piège n°1 — refresh tokens qui meurent en 7 jours

**Symptôme vécu** : la sync GSC marche quelques jours puis s'arrête (« Non connecté » alors que l'org était connectée). Cause : l'**écran de consentement OAuth en mode « Testing »** fait expirer TOUS les refresh tokens Google au bout de **7 jours**. C'est la cause la plus fréquente et la plus vicieuse.

**Correctif** : passer l'écran de consentement en **« Production »** (published). Détaillé en §3.

## 1. Client OAuth (celui de ton screenshot) — ✅ déjà bon

- **Type** : Application Web.
- **URI de redirection autorisés** — les deux sont corrects, ne rien changer :
  - `http://localhost:3000/api/auth/callback/google` (dev)
  - `https://dash.seoinsight.app/api/auth/callback/google` (prod)
  - Le chemin `/api/auth/callback/google` est imposé par Better Auth (`providerId: "google"`). Ne jamais mettre l'ancien `dashboard.` ici.
- **Origines JavaScript autorisées** : **peuvent rester vides**. Better Auth fait le flux OAuth *server-side* (redirect + callback), pas de token exchange dans le navigateur. À ne remplir que si tu ajoutes un jour du One-Tap / GSI client-side (`https://dash.seoinsight.app` + `http://localhost:3000`).
- **Délai** : Google prévient « 5 min à quelques heures » pour propager un changement d'URI — en pratique quelques minutes.

## 2. Client secret

- Le secret n'est plus affichable après création (normal). S'il est perdu → **« Add secret »**, mets à jour l'env, supprime l'ancien après bascule (rotation sans downtime, Google permet 2 secrets actifs).
- **Jamais dans le repo.** Il vit dans `.env.production` (prod) et `.env.local` (dev), lus par `env.GOOGLE_CLIENT_SECRET`.

## 3. Écran de consentement OAuth — LA partie qui compte

APIs & Services → **OAuth consent screen**.

### 3a. User type — le choix structurant
- **Internal** (recommandé si tu as Google Workspace sur `elevaseo.com`) : réservé aux comptes de ton Workspace. **Aucune vérification Google requise**, **pas d'expiration 7 jours**, scopes sensibles (GSC) autorisés d'office. Le plus simple si tous les utilisateurs SEO Insight sont internes.
- **External** : n'importe quel compte Google. Mais :
  - En **Testing** → refresh tokens 7 jours + limité aux « test users » listés.
  - En **Production** → refresh tokens durables, mais le scope `webmasters.readonly` étant **sensible**, Google demande une **vérification** (formulaire + parfois vidéo) au-delà de 100 utilisateurs. En-dessous de 100, tu peux publier sans vérification complète (bandeau « unverified » toléré).

**Décision pour SEO Insight** : si `elevaseo.com` est un Workspace → **Internal**, c'est réglé. Sinon **External + Publish (Production)**.

### 3b. Publishing status
- **Publish app** → statut **« In production »**. C'est ce qui tue le bug des 7 jours.
- Tant que tu es en Testing (External), ajoute tes comptes dans **Test users** (sinon « accès bloqué »).

### 3c. Scopes déclarés
Sur l'écran de consentement, déclare exactement les scopes que le code demande :
- `openid`, `.../auth/userinfo.email`, `.../auth/userinfo.profile` (login — ajoutés d'office par Better Auth).
- `https://www.googleapis.com/auth/webmasters.readonly` (Search Console — **sensible**).

## 4. Config côté code (Better Auth) — ✅ déjà correcte

`src/lib/auth.ts` (ne rien changer, c'est le bon réglage) :

```ts
providers.google = {
  clientId: env.GOOGLE_CLIENT_ID,
  clientSecret: env.GOOGLE_CLIENT_SECRET,
  scope: ["https://www.googleapis.com/auth/webmasters.readonly"],
  accessType: "offline",       // → délivre un refresh_token
  prompt: "select_account",    // → impose le CHOIX du compte (pas le consent)
};
```

- `accessType: "offline"` délivre le **refresh token** (sans quoi le token expire en 1h sans renouvellement) — au **premier** consentement.
- `prompt: "consent"` est **volontairement écarté** depuis le 2026-07-24 : l'écran de consentement est en Production, Google réutilise l'autorisation existante, et re-forcer le consentement à chaque connexion rendait le login pénible.
- `prompt: "select_account"` a été **ajouté le 2026-07-26**. Il n'impose que le choix du compte. Sans lui, un utilisateur n'ayant qu'une session Google ouverte voit Google auto-sélectionner ce compte : le bouton « Lier un autre compte Google » de `/gsc` revient sans rien faire, et le cas agence (plusieurs Search Console clientes sur des comptes distincts) est inatteignable. C'est une option de **provider** — donc valable pour toutes les connexions Google, login compris : un clic de choix de compte à chaque fois, contrepartie assumée.
- Active la **Search Console API** dans APIs & Services → Library (sinon les appels GSC renvoient 403 même avec le bon scope).

## 5. Variables d'environnement

```
GOOGLE_CLIENT_ID=777582861777-...apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=<le secret, jamais commité>
BETTER_AUTH_URL=https://dash.seoinsight.app   # prod — DOIT matcher le domaine réel (cf. incident dashboard→dash)
```

Rappel incident 2026-07-22 : `BETTER_AUTH_URL` doit **exactement** matcher le domaine servi. Une URL sur l'ancien vhost (`dashboard.`) → cookies rejetés → sessions cassées.

## 6. Checklist « config parfaite »

- [ ] Redirect URIs : localhost + `dash.seoinsight.app` (pas `dashboard.`) ✅
- [ ] Search Console API activée dans la Library
- [ ] Écran de consentement : **Internal** (si Workspace) **OU External + Published**
- [ ] Si External + Testing : comptes dans Test users (sinon accès bloqué)
- [ ] Scope `webmasters.readonly` déclaré sur l'écran de consentement
- [ ] `accessType: offline` + `prompt: select_account` dans le code ✅ (déjà là ; `prompt: consent` écarté, cf. §4)
- [ ] `BETTER_AUTH_URL=https://dash.seoinsight.app` en prod ✅ (corrigé)
- [ ] Secret dans l'env, jamais dans le repo

## 7. Vérifier qu'un refresh token est bien stocké

Après une (re)connexion Google, en base :

```sql
select u.email,
       (a.access_token is not null) as has_access,
       (a.refresh_token is not null) as has_refresh,
       a.scope
from account a join "user" u on u.id = a.user_id
where a.provider_id = 'google';
```

`has_refresh = t` → OK, la sync GSC tiendra. `has_refresh = f` → soit il manque `accessType: offline`, soit l'utilisateur avait déjà consenti : Google ne renvoie le refresh token qu'au **premier** consentement. `prompt: select_account` ne le re-délivre PAS (il ne force que le choix du compte) — pour le récupérer, révoquer l'accès de l'app côté Google (myaccount.google.com → Sécurité → Applications tierces) puis se reconnecter, ou passer ponctuellement `prompt: "consent"`.
