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

408 lines
12 KiB
Markdown

# ✅ 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<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`
```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<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
- [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).