Files
FamilyPlanner/docs/archive/OAUTH_CONFIGURATION_COMPLETE.md
philippe fdd72c1135 Initial commit: Family Planner application
Complete family planning application with:
- React frontend with TypeScript
- Node.js/Express backend with TypeScript
- Python ingestion service for document processing
- Planning ingestion service with LLM integration
- Shared UI components and type definitions
- OAuth integration for calendar synchronization
- Comprehensive documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 10:43:33 +02:00

12 KiB

Configuration OAuth Google Calendar - TERMINÉE

📋 Récapitulatif de la configuration

Google OAuth Client créé avec succès

Projet Google Cloud : familyplanner-474915 Date de création : 12 octobre 2025 à 17:51:35 GMT+2 État : Activé


🔑 Credentials configurées

Backend .env mis à jour

GOOGLE_CLIENT_ID=645971045469-1f9kliea9lqhutjeicim377fui2kdhc8.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REDIRECT_URI=http://localhost:5000/api/calendar/oauth/callback

Sécurité : Le fichier .env est bien dans .gitignore (ligne 4)


🌐 URIs de redirection autorisées

Les URIs suivantes sont configurées dans Google Cloud Console :

  1. http://localhost:5000/api/calendar/oauth/callback (Backend)
  2. http://localhost:5174/calendar/oauth/callback (Frontend)

JavaScript Origins autorisées :

  1. http://localhost:5000
  2. http://localhost:5174

⚠️ Écran de consentement OAuth

Important : L'accès OAuth est actuellement réservé aux utilisateurs de test listés sur votre écran de consentement OAuth.

Pour ajouter des utilisateurs de test

  1. Allez sur Google Cloud Console
  2. Sélectionnez le projet familyplanner-474915
  3. Menu APIs & Services > OAuth consent screen
  4. Section Test users > Cliquez sur ADD USERS
  5. Ajoutez les emails autorisés (le vôtre : phil.heyraud@gmail.com)
  6. Sauvegardez

Sinon, vous verrez l'erreur : "Access blocked: This app's request is invalid"


🧪 Test de la configuration

1. Redémarrer le backend

cd backend
npm run dev

Vous devriez voir :

Server running on http://localhost:5000

2. Redémarrer le frontend

cd frontend
npm run dev

Vous devriez voir :

Local: http://localhost:5174/

3. Tester la connexion Google Calendar

  1. Ouvrez l'application dans votre navigateur
  2. Allez dans les Paramètres du profil
  3. Section Agendas connectés
  4. Cliquez sur "Continuer avec Google"

Résultat attendu :

  • Redirection vers accounts.google.com
  • Page de consentement Google affichée
  • Demande d'autorisation pour accéder au calendrier
  • Après acceptation, retour vers l'application

Si erreur :

  • "Access blocked" → Ajoutez votre email en utilisateur de test
  • "redirect_uri_mismatch" → Vérifiez les URIs dans la console
  • "invalid_client" → Vérifiez le Client ID dans .env

📊 Scopes autorisés

Les scopes suivants sont demandés lors de l'authentification :

  • https://www.googleapis.com/auth/calendar.readonly : Lire les événements du calendrier
  • https://www.googleapis.com/auth/calendar.events : Lire/Écrire des événements

Configuré dans : backend/src/routes/calendar.ts ligne 44


🔄 Prochaines étapes

FAIT

  • Créer le projet Google Cloud
  • Activer l'API Google Calendar
  • Créer les credentials OAuth 2.0
  • Configurer les URIs de redirection
  • Mettre à jour le fichier .env

À FAIRE

1. Ajouter utilisateurs de test

  • Aller sur OAuth consent screen
  • Ajouter phil.heyraud@gmail.com en utilisateur de test
  • Ajouter d'autres emails si besoin

2. Implémenter l'échange de code OAuth

Actuellement, le code d'autorisation n'est pas échangé contre un access token.

Fichier à modifier : backend/src/routes/calendar.ts

// Ligne ~90 : endpoint /oauth/complete
calendarRouter.post("/oauth/complete", async (req: Request, res: Response) => {
  try {
    const { provider, state, code, error, profileId } = oauthCompleteBody.parse(req.body ?? {});

    if (error) return res.json({ success: false, error });
    if (!code) return res.json({ success: false, error: "No authorization code" });

    const pending = pendingStates.get(state);
    if (!pending || pending.expiresAt < Date.now()) {
      return res.status(400).json({ success: false, error: "State expired or invalid" });
    }
    pendingStates.delete(state);

    // ⚠️ NOUVEAU : Échanger le code contre un access token
    const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        code: code,
        client_id: process.env.GOOGLE_CLIENT_ID,
        client_secret: process.env.GOOGLE_CLIENT_SECRET,
        redirect_uri: process.env.GOOGLE_REDIRECT_URI,
        grant_type: "authorization_code"
      })
    });

    if (!tokenResponse.ok) {
      const errorData = await tokenResponse.json();
      console.error("Token exchange failed:", errorData);
      return res.json({ success: false, error: "Token exchange failed" });
    }

    const tokens = await tokenResponse.json();
    // tokens.access_token, tokens.refresh_token, tokens.expires_in

    // ⚠️ IMPORTANT : Chiffrer et stocker les tokens de manière sécurisée
    const encryptedToken = await encryptToken(tokens.access_token);
    const encryptedRefreshToken = tokens.refresh_token
      ? await encryptToken(tokens.refresh_token)
      : null;

    const pid = profileId ?? pending.profileId;
    const list = connectionsByProfile.get(pid) ?? [];
    const conn: ConnectedCalendar = {
      id: pending.connectionId,
      provider,
      email: await getUserEmail(tokens.access_token), // Récupérer l'email réel
      label: `Google Calendar (${new Date().toLocaleDateString()})`,
      status: "connected",
      createdAt: new Date().toISOString(),
      lastSyncedAt: new Date().toISOString(),
      scopes: tokens.scope.split(" "),
      shareWithFamily: false
    };

    // Stocker les tokens chiffrés dans la base de données (pas en mémoire)
    await saveEncryptedTokens(conn.id, {
      accessToken: encryptedToken,
      refreshToken: encryptedRefreshToken,
      expiresAt: new Date(Date.now() + tokens.expires_in * 1000)
    });

    connectionsByProfile.set(pid, [...list, conn]);
    res.json({
      success: true,
      email: conn.email,
      label: conn.label,
      connectionId: conn.id,
      profileId: pid
    });
  } catch (e) {
    console.error("OAuth complete error:", e);
    res.status(400).json({ success: false, error: "OAuth completion failed" });
  }
});

// Helper pour récupérer l'email de l'utilisateur
async function getUserEmail(accessToken: string): Promise<string> {
  const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
    headers: { Authorization: `Bearer ${accessToken}` }
  });
  const data = await response.json();
  return data.email;
}

3. Implémenter le chiffrement des tokens

Créer : backend/src/utils/encryption.ts

import crypto from "crypto";

const ALGORITHM = "aes-256-gcm";
const ENCRYPTION_KEY = process.env.TOKEN_ENCRYPTION_KEY; // 32 bytes hex

if (!ENCRYPTION_KEY) {
  throw new Error("TOKEN_ENCRYPTION_KEY is required in .env");
}

export function encryptToken(token: string): {
  encrypted: string;
  iv: string;
  authTag: string;
} {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(
    ALGORITHM,
    Buffer.from(ENCRYPTION_KEY, "hex"),
    iv
  );

  let encrypted = cipher.update(token, "utf8", "hex");
  encrypted += cipher.final("hex");

  return {
    encrypted,
    iv: iv.toString("hex"),
    authTag: cipher.getAuthTag().toString("hex")
  };
}

export function decryptToken(
  encrypted: string,
  iv: string,
  authTag: string
): string {
  const decipher = crypto.createDecipheriv(
    ALGORITHM,
    Buffer.from(ENCRYPTION_KEY, "hex"),
    Buffer.from(iv, "hex")
  );

  decipher.setAuthTag(Buffer.from(authTag, "hex"));

  let decrypted = decipher.update(encrypted, "hex", "utf8");
  decrypted += decipher.final("utf8");

  return decrypted;
}

Ajouter dans .env :

# Token Encryption (générez une clé aléatoire de 32 bytes)
TOKEN_ENCRYPTION_KEY=votre_clé_32_bytes_hex_ici

Générer une clé :

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

4. Implémenter la récupération des événements

Créer : backend/src/services/google-calendar.service.ts

export class GoogleCalendarService {
  async getEvents(
    accessToken: string,
    startDate: Date,
    endDate: Date
  ): Promise<CalendarEvent[]> {
    const params = new URLSearchParams({
      timeMin: startDate.toISOString(),
      timeMax: endDate.toISOString(),
      maxResults: "250",
      singleEvents: "true",
      orderBy: "startTime"
    });

    const response = await fetch(
      `https://www.googleapis.com/calendar/v3/calendars/primary/events?${params}`,
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json"
        }
      }
    );

    if (!response.ok) {
      if (response.status === 401) {
        // Token expiré, besoin de refresh
        throw new Error("TOKEN_EXPIRED");
      }
      throw new Error(`Google Calendar API error: ${response.status}`);
    }

    const data = await response.json();
    return this.normalizeEvents(data.items);
  }

  private normalizeEvents(items: any[]): CalendarEvent[] {
    return items.map(event => ({
      id: event.id,
      title: event.summary || "(Sans titre)",
      description: event.description,
      startDate: event.start.dateTime || event.start.date,
      endDate: event.end.dateTime || event.end.date,
      isAllDay: !event.start.dateTime,
      location: event.location,
      attendees: event.attendees?.map((a: any) => a.email),
      source: "google"
    }));
  }
}

5. Publier l'application (plus tard)

Actuellement, votre app est en mode "Testing" avec utilisateurs limités.

Pour rendre publique :

  1. OAuth consent screen > PUBLISH APP
  2. Google examinera votre application (processus de vérification)
  3. Une fois approuvé, tout le monde pourra se connecter

Note : Pas nécessaire pour l'instant si vous testez uniquement avec votre compte.


🔐 Sécurité - Checklist

  • .env dans .gitignore
  • Client secret stocké uniquement dans .env
  • Générer clé de chiffrement TOKEN_ENCRYPTION_KEY
  • Implémenter chiffrement des tokens
  • Stocker tokens chiffrés dans une base de données (pas en mémoire)
  • Révoquer l'ancienne clé OpenAI exposée
  • Ajouter middleware d'authentification sur les routes

📱 Configuration Outlook (optionnel)

Si vous voulez aussi connecter Outlook/Microsoft 365, suivez le même processus sur Azure Portal et mettez à jour :

OUTLOOK_CLIENT_ID=votre_application_id_ici
OUTLOOK_CLIENT_SECRET=votre_secret_ici
OUTLOOK_REDIRECT_URI=http://localhost:5000/api/calendar/oauth/callback

📞 Support

En cas de problème :

  1. Vérifiez les logs backend : Regardez la console où tourne npm run dev
  2. Vérifiez la console navigateur : Ouvrez DevTools (F12) > Console
  3. Testez l'URL OAuth manuellement :
    https://accounts.google.com/o/oauth2/v2/auth?client_id=645971045469-1f9kliea9lqhutjeicim377fui2kdhc8.apps.googleusercontent.com&redirect_uri=http://localhost:5000/api/calendar/oauth/callback&response_type=code&scope=https://www.googleapis.com/auth/calendar.readonly&state=test123&access_type=offline&prompt=consent
    

Erreurs communes :

  • "invalid_client" → Client ID incorrect dans .env
  • "redirect_uri_mismatch" → URI pas exactement identique dans console Google
  • "access_denied" → Utilisateur a refusé les permissions
  • "unauthorized_client" → App pas publiée et utilisateur pas en mode test

Configuration terminée avec succès !

Vous pouvez maintenant tester la connexion Google Calendar depuis votre application. 🎉

Prochaine étape : Implémentez l'échange du code OAuth et la récupération des événements (voir sections ci-dessus).