Files
workspace/docs/plan-google-auth.md
Roberto Musso 37d7e65b35 feat: implement Step 2 Google OAuth backend — provider abstraction, PKCE routes, user linking
Adds api/app/auth/oauth_providers.py with GoogleOAuthProvider (httpx-based,
no authlib needed) and generate_pkce_pair(). New routes:
GET /auth/oauth/{provider}/authorize and POST /auth/oauth/{provider}/callback
with state/PKCE validation and three-way user resolution (existing OAuth link,
email auto-link, new social-only user). Updates settings.py with
GOOGLE_AUTH_CLIENT_ID/SECRET and OAUTH_REDIRECT_URI.

Also includes Step 1 backend changes (already marked complete in plan):
oauth_accounts table migration, nullable password_hash, avatar_url on User.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:21:14 +02:00

8.1 KiB

Piano: Integrazione Google Login + Avatar

Contesto

L'app adiuvAI ha un sistema auth custom (email+password, bcrypt, JWT HS256) funzionante. Obiettivo: aggiungere Google Login come primo social provider, con la prospettiva di aggiungerne 2-3 in futuro (Microsoft, GitHub).

Decisione architetturale: OAuth diretto nel sistema esistente (non Keycloak, non Auth0).

  • Il backend continua a emettere i propri JWT
  • Ogni provider e' un'implementazione concreta di un'astrazione condivisa
  • Email+password resta il metodo primario di registrazione/login
  • L'avatar utente viene preso da Google; per utenti email si usano iniziali o icona default

Nota encryption: La encryption_key Fernet e' generata random, NON derivata dalla password. Il social login non rompe la crittografia server-side. L'unico fix necessario e' sganciare il backup encryption locale (_cachedPassword) dalla password utente.


Regole

  • Completamento step: aggiornare questo documento marcando lo step come [x] e annotare eventuali lessons learned
  • Commit: al termine di ogni step, eseguire commit del codice

Step 1: Backend — DB + Model + Schema

Status: [x] Completato

Cosa fare:

  • Migration Alembic: creare tabella oauth_accounts (id, user_id FK, provider, provider_user_id, provider_email, created_at; UNIQUE su provider, provider_user_id)
  • Migration Alembic: rendere users.password_hash nullable
  • Migration Alembic: aggiungere colonna users.avatar_url (VARCHAR, nullable)
  • Model OAuthAccount in api/app/models.py con relationship a User
  • Campo avatar_url: str | None sul model User
  • Aggiornare UserProfile in api/app/schemas.py con avatar_url

File coinvolti:

  • api/alembic/versions/XXX_add_oauth_and_avatar.py (nuovo)
  • api/app/models.py
  • api/app/schemas.py

Lessons learned:

  • get_current_user in middleware/auth.py esegue una query separata per name/surname — aggiornare quella query per includere avatar_url (non solo il model). Altrimenti il campo viene ignorato anche se presente in DB.
  • update_profile in routes/auth.py costruisce UserProfile manualmente: va aggiornato esplicitamente con avatar_url=user.avatar_url.
  • La OAuthAccount.user relationship richiede forward reference: il model deve essere dichiarato dopo User ma la relationship su User usa OAuthAccount — SQLAlchemy risolve automaticamente con le stringhe lazy evaluation, ma occorre che il model sia nello stesso modulo.

Step 2: Backend — OAuth Provider + Route

Status: [x] Completato

Cosa fare:

  • Creare astrazione provider OAuth riusabile in api/app/auth/oauth_providers.py (nuovo)
    • Classe base con: get_authorization_url(), exchange_code(), get_userinfo()
    • Implementazione concreta GoogleOAuthProvider
  • Aggiungere settings in api/app/config/settings.py: GOOGLE_AUTH_CLIENT_ID, GOOGLE_AUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI
    • Separati da GMAIL_CLIENT_ID/SECRET (scope diversi: openid email profile vs gmail.readonly)
  • Aggiungere route in api/app/api/routes/auth.py:
    • GET /auth/oauth/{provider}/authorize — genera state + PKCE code_challenge, ritorna authorize URL
    • POST /auth/oauth/{provider}/callback — valida state, scambia code, fetch userinfo, crea/linka utente, salva avatar_url da Google, emette JWT
  • Logica utente nel callback:
    • oauth_accounts match? -> login utente esistente
    • Email match + email_verified=true? -> link account a utente esistente (aggiorna avatar se mancante)
    • Nessun match? -> crea nuovo utente (Fernet key, password_hash=None, avatar da Google)
  • Aggiungere dependency authlib in requirements

Sicurezza:

  • PKCE obbligatorio (desktop app = public client)
  • Auto-link email solo se email_verified=true da Google
  • State param per prevenire CSRF

File coinvolti:

  • api/app/auth/__init__.py (nuovo)
  • api/app/auth/oauth_providers.py (nuovo)
  • api/app/api/routes/auth.py
  • api/app/config/settings.py
  • api/requirements.txt (o pyproject.toml)

Lessons learned:

  • authlib non è necessaria: il flow PKCE con Google si implementa direttamente con httpx (già in requirements). Aggiungere authlib sarebbe una dipendenza inutilizzata. Se si vorrà usare authlib in futuro (es. per provider con flow più complessi), aggiungerla allora.
  • State store in-memory: _pending_states è un dict a livello modulo — funziona in dev con un solo processo, ma non sopravvive a restart e non scala su più worker. In produzione va sostituito con Redis (o un campo temporaneo su DB).
  • _issue_refresh_token helper: la logica di emissione token è condivisa tra i tre branch del callback (link esistente, link email, nuovo utente) — fattorizzarla in un helper evita duplicazione.
  • tuple_ import non usato: l'import sql_tuple da sqlalchemy può essere rimosso (non serve per le query attuali).
  • Route provider tipizzata come Literal["google"]: FastAPI valida automaticamente il parametro path e risponde 422 per provider sconosciuti, rendendo superfluo un check manuale. Il dict _PROVIDERS serve come fallback di sicurezza.
  • await db.flush() prima di creare OAuthAccount: necessario per ottenere new_user.id prima del commit, altrimenti il FK fallisce.

Status: [ ] Da fare

Cosa fare:

  • Registrare protocollo custom adiuvai:// in adiuvAI/forge.config.ts (packagerConfig.protocols)
  • In adiuvAI/src/main/index.ts:
    • app.setAsDefaultProtocolClient('adiuvai')
    • Windows/Linux: gestire second-instance event, parsare argv per deep link
    • macOS: gestire open-url event
    • Forward code+state all'auth manager
  • In adiuvAI/src/main/auth/auth-manager.ts:
    • Nuovo metodo loginWithOAuth(provider): chiama authorize, apre browser, attende callback, scambia code, salva token
    • Nuovo metodo handleOAuthCallback(code, state): risolve la promise pendente
  • In adiuvAI/src/main/router/index.ts:
    • Aggiungere procedura tRPC auth.loginWithOAuth

File coinvolti:

  • adiuvAI/forge.config.ts
  • adiuvAI/src/main/index.ts
  • adiuvAI/src/main/auth/auth-manager.ts
  • adiuvAI/src/main/router/index.ts

Lessons learned:

  • (da compilare a step completato)

Step 4: Electron — UI Login + Avatar

Status: [ ] Da fare

Cosa fare:

  • In adiuvAI/src/renderer/components/auth/LoginForm.tsx:
    • Email+password resta il form principale (prima opzione, in alto)
    • Divider "oppure" sotto il form
    • Bottone "Sign in with Google" sotto il divider
    • Stato "In attesa del browser..." durante il flow OAuth
    • Su successo: invalidare auth.status
  • Mostrare avatar nel profilo utente:
    • Avatar da avatar_url (immagine circolare) se disponibile
    • Fallback: iniziali del nome o icona default per utenti senza avatar
  • Aggiornare tipo UserProfile in adiuvAI/src/shared/api-types.ts con avatar_url

File coinvolti:

  • adiuvAI/src/renderer/components/auth/LoginForm.tsx
  • adiuvAI/src/shared/api-types.ts
  • Componenti profilo utente (da identificare)

Lessons learned:

  • (da compilare a step completato)

Step 5: Fix Backup Encryption + Test

Status: [ ] Da fare

Cosa fare:

  • Sganciare BackupManager da _cachedPassword:
    • Generare chiave backup random 256-bit al primo login (qualsiasi metodo auth)
    • Salvarla in electron-store via safeStorage (stessa pattern di token.ts)
    • Usare questa chiave al posto di _cachedPassword per AES backup
    • Per utenti esistenti: generare nuova chiave, usarla per backup futuri
  • Test backend (api/tests/test_auth.py):
    • Test authorize URL generation
    • Test callback: creazione utente, linking account, duplicati
    • Test callback con email match -> auto-link
    • Test state mismatch -> 401
  • Test manuale end-to-end:
    • Click "Sign in with Google" -> browser -> consent -> redirect -> autenticato
    • Account linking: stessa email via password e Google -> stesso utente
    • Utente solo-social: nuovo utente senza password
    • Backup con utente social -> chiave device-specific

File coinvolti:

  • adiuvAI/src/main/auth/auth-manager.ts
  • BackupManager (da identificare path esatto)
  • api/tests/test_auth.py

Lessons learned:

  • (da compilare a step completato)