# ✅ 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 ```env 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](https://console.cloud.google.com/) 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 ```bash cd backend npm run dev ``` Vous devriez voir : ``` Server running on http://localhost:5000 ``` ### 2. Redémarrer le frontend ```bash 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 - [x] Créer le projet Google Cloud - [x] Activer l'API Google Calendar - [x] Créer les credentials OAuth 2.0 - [x] Configurer les URIs de redirection - [x] 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` ```typescript // 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 { 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` ```typescript 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`** : ```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é** : ```bash 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` ```typescript export class GoogleCalendarService { async getEvents( accessToken: string, startDate: Date, endDate: Date ): Promise { 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 - [x] `.env` dans `.gitignore` - [x] 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](https://portal.azure.com/) et mettez à jour : ```env 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).