Decouple Gmail permissions from Google Sign-In OAuth flow

The Google Sign-In flow now only requests basic profile scopes (openid,
email, profile) instead of also requesting Gmail API scopes. Users can
grant Gmail access separately via the "Connect Gmail" button in Settings.

- Remove GMAIL_SCOPES from GOOGLE_LOGIN_SCOPES in auth.py
- Remove Gmail credential auto-provisioning from google_oauth endpoint
- Simplify /auth/google/authorize-url (no offline access or forced consent)
- Clean up auth_service.py OAuth registration to only use login scopes
- Remove unused imports (GmailCredential, GmailService, encrypt_credential, etc.)

Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/91d2db11-811f-46f5-ac89-29952ab358b5

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-27 21:01:46 +00:00
parent ad3cb81709
commit 4844377342
4 changed files with 13 additions and 81 deletions
+8 -77
View File
@@ -6,36 +6,32 @@ from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone
from urllib.parse import quote as urlquote
import logging
from app.core.config import settings
from app.core.database import get_db
from app.core.security import verify_password, get_password_hash, encrypt_credential
from app.core.security import verify_password, get_password_hash
from app.core.metrics import (
AUTH_LOGINS_TOTAL,
AUTH_REGISTRATIONS_TOTAL,
OAUTH_CALLBACKS_TOTAL,
)
from app.models.database_models import User, SubscriptionTier, GmailCredential
from app.models.database_models import User, SubscriptionTier
from app.models.schemas import Token, UserCreate, UserResponse, GoogleAuthRequest
from app.services.auth_service import oauth_service
from app.services.gmail_service import GmailService, GMAIL_SCOPES
from app.utils.gmail_labels import build_gmail_credential_scopes
router = APIRouter()
logger = logging.getLogger(__name__)
# All scopes requested during Google Sign-In so users only go through one OAuth
# consent screen for both login and Gmail API access.
# GMAIL_SCOPES (gmail.insert, gmail.labels, gmail.readonly) are imported from
# gmail_service so the scope list stays in sync with what GmailService uses.
# Scopes requested during Google Sign-In only basic profile information.
# Gmail API access is granted separately via the "Connect Gmail" flow in
# Settings (/providers/gmail/authorize-url).
GOOGLE_LOGIN_SCOPES = [
"openid",
"email",
"profile",
*GMAIL_SCOPES,
]
@@ -251,69 +247,6 @@ async def google_oauth(
await db.commit()
await db.refresh(user)
# If Gmail tokens were returned (Gmail scopes were granted), store them so
# users don't need a separate "Connect Gmail" step after signing in.
google_access_token = user_info.get("access_token")
google_refresh_token = user_info.get("refresh_token")
if google_access_token:
try:
gmail_service = GmailService(
access_token=google_access_token,
refresh_token=google_refresh_token,
client_id=settings.GOOGLE_CLIENT_ID,
client_secret=settings.GOOGLE_CLIENT_SECRET,
)
is_valid = await gmail_service.verify_access()
if is_valid:
token_expiry = datetime.now(timezone.utc) + timedelta(
seconds=int(user_info.get("expires_in", 3600))
)
encrypted_access = encrypt_credential(google_access_token)
encrypted_refresh = (
encrypt_credential(google_refresh_token)
if google_refresh_token
else None
)
scope_list = user_info.get("scope", "").split()
cred_result = await db.execute(
select(GmailCredential).where(GmailCredential.user_id == user.id)
)
existing_cred = cred_result.scalar_one_or_none()
if existing_cred:
existing_cred.gmail_email = email # type: ignore[assignment]
existing_cred.encrypted_access_token = encrypted_access # type: ignore[assignment]
if encrypted_refresh:
existing_cred.encrypted_refresh_token = encrypted_refresh # type: ignore[assignment]
existing_cred.token_expiry = token_expiry # type: ignore[assignment]
existing_cred.scopes = build_gmail_credential_scopes( # type: ignore[assignment]
scope_list,
existing_cred.import_label_templates,
)
existing_cred.is_valid = True # type: ignore[assignment]
existing_cred.last_verified_at = datetime.now(timezone.utc) # type: ignore[assignment]
else:
new_cred = GmailCredential(
user_id=user.id,
gmail_email=email,
encrypted_access_token=encrypted_access,
encrypted_refresh_token=encrypted_refresh,
token_expiry=token_expiry,
scopes=build_gmail_credential_scopes(scope_list),
is_valid=True,
last_verified_at=datetime.now(timezone.utc),
)
db.add(new_cred)
await db.commit()
logger.info(f"Gmail credentials stored for user: {email}")
except Exception as e:
# Non-fatal: login succeeds even if Gmail credential storage fails
logger.warning(
f"Could not store Gmail credentials during Google login for {email}: {e}"
)
# Create tokens
tokens = oauth_service.create_tokens_for_user(user)
@@ -323,7 +256,7 @@ async def google_oauth(
@router.get("/google/authorize-url")
async def get_google_authorize_url(redirect_uri: str):
"""Get Google OAuth2 authorization URL requesting all necessary scopes."""
"""Get Google OAuth2 authorization URL for sign-in (profile scopes only)."""
scope = urlquote(" ".join(GOOGLE_LOGIN_SCOPES))
auth_url = (
"https://accounts.google.com/o/oauth2/v2/auth"
@@ -331,9 +264,7 @@ async def get_google_authorize_url(redirect_uri: str):
"&response_type=code"
f"&scope={scope}"
f"&redirect_uri={redirect_uri}"
"&access_type=offline"
"&prompt=consent"
"&include_granted_scopes=true"
"&prompt=select_account"
)
return {"authorization_url": auth_url}