Add comprehensive Google OAuth integration for easy login/signup
Features: - Complete Google OAuth 2.0 integration with ID token and authorization code flows - Enhanced User model with Google OAuth fields (google_id, is_google_user, email_verified, profile_picture) - Google OAuth service for token verification and user info extraction - Multiple authentication endpoints: - GET /auth/google/oauth-url (get OAuth URL for frontend) - POST /auth/google/login-with-token (direct ID token login) - POST /auth/google/login-with-code (authorization code exchange) - Smart user handling: creates new users or links existing accounts - Issues own JWT tokens after Google authentication - Database migration 004 for Google OAuth fields - Enhanced login logic to handle Google vs password users - Comprehensive README with Google OAuth setup instructions - Frontend integration examples for both OAuth flows Google OAuth automatically: - Creates user accounts on first login - Links existing email accounts to Google - Extracts profile information (name, picture, locale) - Verifies email addresses - Issues secure JWT tokens for API access
This commit is contained in:
parent
3e90deba20
commit
99937b3fd7
69
README.md
69
README.md
@ -38,6 +38,11 @@ Create a `.env` file in the root directory with the following variables:
|
||||
# Authentication
|
||||
SECRET_KEY=your-secret-key-change-this-in-production
|
||||
|
||||
# Google OAuth Configuration
|
||||
GOOGLE_CLIENT_ID=your-google-client-id
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
|
||||
|
||||
# AWS S3 Configuration
|
||||
AWS_ACCESS_KEY_ID=your-aws-access-key
|
||||
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
|
||||
@ -76,8 +81,11 @@ The API will be available at:
|
||||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
- `POST /auth/register` - User registration
|
||||
- `POST /auth/login` - User login
|
||||
- `POST /auth/register` - User registration with email/password
|
||||
- `POST /auth/login` - User login with email/password
|
||||
- `GET /auth/google/oauth-url` - Get Google OAuth URL for frontend
|
||||
- `POST /auth/google/login-with-token` - Login/signup with Google ID token
|
||||
- `POST /auth/google/login-with-code` - Login/signup with Google authorization code
|
||||
|
||||
### Profile Management
|
||||
- `GET /profile/` - Get user profile
|
||||
@ -104,9 +112,59 @@ The API will be available at:
|
||||
### Results
|
||||
- `GET /process/results/{video_id}` - Get complete processing results
|
||||
|
||||
## Google OAuth Setup
|
||||
|
||||
### 1. Create Google OAuth Application
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select existing one
|
||||
3. Enable the Google+ API
|
||||
4. Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client IDs"
|
||||
5. Choose "Web application"
|
||||
6. Add authorized redirect URIs:
|
||||
- `http://localhost:3000/auth/google/callback` (for development)
|
||||
- Your production callback URL
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
Add these to your `.env` file:
|
||||
```env
|
||||
GOOGLE_CLIENT_ID=your-google-oauth-client-id
|
||||
GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret
|
||||
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
|
||||
```
|
||||
|
||||
### 3. Frontend Integration
|
||||
|
||||
**Option 1: Direct Token Method**
|
||||
```javascript
|
||||
// Use Google's JavaScript library to get ID token
|
||||
const response = await fetch('/auth/google/login-with-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id_token: googleIdToken })
|
||||
});
|
||||
```
|
||||
|
||||
**Option 2: Authorization Code Method**
|
||||
```javascript
|
||||
// Redirect user to Google OAuth URL, then exchange code
|
||||
const oauthUrl = await fetch('/auth/google/oauth-url').then(r => r.json());
|
||||
// Redirect to oauthUrl.oauth_url
|
||||
// On callback, exchange code:
|
||||
const response = await fetch('/auth/google/login-with-code', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
code: authorizationCode,
|
||||
redirect_uri: 'http://localhost:3000/auth/google/callback'
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Register/Login** to get JWT token
|
||||
1. **Register/Login** (Email/Password or Google OAuth) to get JWT token
|
||||
2. **Upload Video** with source and target languages
|
||||
3. **Transcribe** the audio from the video
|
||||
4. **Translate** the transcribed text
|
||||
@ -119,6 +177,9 @@ The API will be available at:
|
||||
| Variable | Description | Required |
|
||||
|----------|-------------|----------|
|
||||
| `SECRET_KEY` | JWT secret key for authentication | Yes |
|
||||
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | No* |
|
||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | No* |
|
||||
| `GOOGLE_REDIRECT_URI` | Google OAuth redirect URI | No* |
|
||||
| `AWS_ACCESS_KEY_ID` | AWS access key for S3 | Yes |
|
||||
| `AWS_SECRET_ACCESS_KEY` | AWS secret key for S3 | Yes |
|
||||
| `AWS_REGION` | AWS region (default: us-east-1) | No |
|
||||
@ -126,6 +187,8 @@ The API will be available at:
|
||||
| `OPENAI_API_KEY` | OpenAI API key for Whisper and GPT-4 | Yes |
|
||||
| `ELEVENLABS_API_KEY` | ElevenLabs API key for voice cloning | Yes |
|
||||
|
||||
*Required only if Google OAuth is enabled
|
||||
|
||||
## File Storage Structure
|
||||
|
||||
Files are stored in S3 with the following structure:
|
||||
|
81
alembic/versions/004_add_google_oauth_fields.py
Normal file
81
alembic/versions/004_add_google_oauth_fields.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""Add Google OAuth fields to users table
|
||||
|
||||
Revision ID: 004
|
||||
Revises: 003
|
||||
Create Date: 2024-01-01 00:00:03.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '004'
|
||||
down_revision: Union[str, None] = '003'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def column_exists(table_name, column_name):
|
||||
"""Check if a column exists in a table"""
|
||||
bind = op.get_bind()
|
||||
inspector = Inspector.from_engine(bind)
|
||||
columns = [c['name'] for c in inspector.get_columns(table_name)]
|
||||
return column_name in columns
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# List of Google OAuth columns to add
|
||||
google_oauth_columns = [
|
||||
('google_id', sa.Column('google_id', sa.String(), nullable=True)),
|
||||
('is_google_user', sa.Column('is_google_user', sa.Boolean(), nullable=True)),
|
||||
('email_verified', sa.Column('email_verified', sa.Boolean(), nullable=True)),
|
||||
('profile_picture', sa.Column('profile_picture', sa.String(), nullable=True))
|
||||
]
|
||||
|
||||
# Add Google OAuth columns only if they don't exist
|
||||
for column_name, column_def in google_oauth_columns:
|
||||
if not column_exists('users', column_name):
|
||||
op.add_column('users', column_def)
|
||||
|
||||
# Create index for google_id if column exists
|
||||
if column_exists('users', 'google_id'):
|
||||
try:
|
||||
op.create_index(op.f('ix_users_google_id'), 'users', ['google_id'], unique=True)
|
||||
except Exception:
|
||||
# Index might already exist, ignore
|
||||
pass
|
||||
|
||||
# Set default values for existing users
|
||||
if column_exists('users', 'is_google_user'):
|
||||
op.execute("UPDATE users SET is_google_user = 0 WHERE is_google_user IS NULL")
|
||||
if column_exists('users', 'email_verified'):
|
||||
op.execute("UPDATE users SET email_verified = 0 WHERE email_verified IS NULL")
|
||||
|
||||
# Make password_hash nullable for Google OAuth users
|
||||
# Note: SQLite doesn't support modifying column constraints directly
|
||||
# So we'll handle this in the application logic
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# List of columns to remove (in reverse order)
|
||||
google_oauth_columns = [
|
||||
'profile_picture',
|
||||
'email_verified',
|
||||
'is_google_user',
|
||||
'google_id'
|
||||
]
|
||||
|
||||
# Drop index first if it exists
|
||||
try:
|
||||
op.drop_index(op.f('ix_users_google_id'), table_name='users')
|
||||
except Exception:
|
||||
# Index might not exist, ignore
|
||||
pass
|
||||
|
||||
# Remove columns only if they exist
|
||||
for column_name in google_oauth_columns:
|
||||
if column_exists('users', column_name):
|
||||
op.drop_column('users', column_name)
|
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean
|
||||
from datetime import datetime
|
||||
from app.db.base import Base
|
||||
|
||||
@ -8,13 +8,20 @@ class User(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
password_hash = Column(String, nullable=False)
|
||||
password_hash = Column(String, nullable=True) # Made nullable for Google OAuth users
|
||||
first_name = Column(String, nullable=True)
|
||||
last_name = Column(String, nullable=True)
|
||||
phone = Column(String, nullable=True)
|
||||
bio = Column(String, nullable=True)
|
||||
preferred_language = Column(String, default="en")
|
||||
timezone = Column(String, default="UTC")
|
||||
|
||||
# Google OAuth fields
|
||||
google_id = Column(String, unique=True, nullable=True, index=True)
|
||||
is_google_user = Column(Boolean, default=False)
|
||||
email_verified = Column(Boolean, default=False)
|
||||
profile_picture = Column(String, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), nullable=True)
|
||||
updated_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
@ -23,4 +30,23 @@ class User(Base):
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.utcnow()
|
||||
if not self.updated_at:
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
"""Get user's full name"""
|
||||
if self.first_name and self.last_name:
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
elif self.first_name:
|
||||
return self.first_name
|
||||
elif self.last_name:
|
||||
return self.last_name
|
||||
return self.email.split('@')[0] # Fallback to email username
|
||||
|
||||
def is_password_user(self):
|
||||
"""Check if user uses password authentication"""
|
||||
return self.password_hash is not None and not self.is_google_user
|
||||
|
||||
def can_login_with_password(self):
|
||||
"""Check if user can login with password"""
|
||||
return self.password_hash is not None
|
@ -1,10 +1,13 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.utils.auth import get_password_hash, verify_password, create_access_token
|
||||
from app.services.google_oauth_service import GoogleOAuthService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -89,7 +92,25 @@ async def register(user: UserCreate, db: Session = Depends(get_db)):
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(user: UserLogin, db: Session = Depends(get_db)):
|
||||
db_user = db.query(User).filter(User.email == user.email).first()
|
||||
if not db_user or not verify_password(user.password, db_user.password_hash):
|
||||
|
||||
# Check if user exists
|
||||
if not db_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Check if user can login with password
|
||||
if not db_user.can_login_with_password():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="This account uses Google sign-in. Please use Google to login.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Verify password
|
||||
if not verify_password(user.password, db_user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
@ -97,4 +118,191 @@ async def login(user: UserLogin, db: Session = Depends(get_db)):
|
||||
)
|
||||
|
||||
access_token = create_access_token(data={"sub": db_user.email})
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
# Google OAuth Models
|
||||
class GoogleTokenLogin(BaseModel):
|
||||
id_token: str
|
||||
|
||||
|
||||
class GoogleCodeLogin(BaseModel):
|
||||
code: str
|
||||
redirect_uri: str
|
||||
|
||||
|
||||
class GoogleOAuthURL(BaseModel):
|
||||
oauth_url: str
|
||||
|
||||
|
||||
class GoogleUserResponse(BaseModel):
|
||||
id: int
|
||||
email: str
|
||||
first_name: Optional[str]
|
||||
last_name: Optional[str]
|
||||
full_name: str
|
||||
is_google_user: bool
|
||||
email_verified: bool
|
||||
profile_picture: Optional[str]
|
||||
created_at: str
|
||||
is_new_user: bool
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# Google OAuth Endpoints
|
||||
@router.get("/google/oauth-url", response_model=GoogleOAuthURL)
|
||||
async def get_google_oauth_url():
|
||||
"""Get Google OAuth URL for frontend redirect"""
|
||||
oauth_url = GoogleOAuthService.get_google_oauth_url()
|
||||
if not oauth_url:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Google OAuth not configured"
|
||||
)
|
||||
|
||||
return {"oauth_url": oauth_url}
|
||||
|
||||
|
||||
@router.post("/google/login-with-token", response_model=Token)
|
||||
async def google_login_with_token(
|
||||
google_data: GoogleTokenLogin,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Login/Register with Google ID token"""
|
||||
try:
|
||||
logger.info("Google OAuth login attempt with ID token")
|
||||
|
||||
# Verify Google token
|
||||
user_info = await GoogleOAuthService.verify_google_token(google_data.id_token)
|
||||
if not user_info:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid Google token"
|
||||
)
|
||||
|
||||
# Check if user exists by email
|
||||
db_user = db.query(User).filter(User.email == user_info['email']).first()
|
||||
|
||||
if db_user:
|
||||
# Existing user - update Google info if not already a Google user
|
||||
if not db_user.is_google_user:
|
||||
db_user.google_id = user_info['google_id']
|
||||
db_user.is_google_user = True
|
||||
db_user.email_verified = user_info['email_verified']
|
||||
if user_info.get('picture'):
|
||||
db_user.profile_picture = user_info['picture']
|
||||
if user_info.get('first_name') and not db_user.first_name:
|
||||
db_user.first_name = user_info['first_name']
|
||||
if user_info.get('last_name') and not db_user.last_name:
|
||||
db_user.last_name = user_info['last_name']
|
||||
db_user.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
logger.info(f"Updated existing user with Google OAuth: {db_user.email}")
|
||||
else:
|
||||
# New user - create account
|
||||
db_user = User(
|
||||
email=user_info['email'],
|
||||
google_id=user_info['google_id'],
|
||||
is_google_user=True,
|
||||
email_verified=user_info['email_verified'],
|
||||
first_name=user_info.get('first_name'),
|
||||
last_name=user_info.get('last_name'),
|
||||
profile_picture=user_info.get('picture'),
|
||||
preferred_language=user_info.get('locale', 'en')
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
logger.info(f"Created new Google user: {db_user.email}")
|
||||
|
||||
# Create JWT token
|
||||
access_token = create_access_token(data={"sub": db_user.email})
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Google OAuth error: {str(e)}")
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Internal server error during Google authentication"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/google/login-with-code", response_model=Token)
|
||||
async def google_login_with_code(
|
||||
google_data: GoogleCodeLogin,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Login/Register with Google authorization code"""
|
||||
try:
|
||||
logger.info("Google OAuth login attempt with authorization code")
|
||||
|
||||
# Exchange code for ID token
|
||||
id_token = await GoogleOAuthService.exchange_code_for_token(
|
||||
google_data.code,
|
||||
google_data.redirect_uri
|
||||
)
|
||||
|
||||
if not id_token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Failed to exchange authorization code"
|
||||
)
|
||||
|
||||
# Verify the ID token and process user
|
||||
user_info = await GoogleOAuthService.verify_google_token(id_token)
|
||||
if not user_info:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid Google token"
|
||||
)
|
||||
|
||||
# Same logic as token login
|
||||
db_user = db.query(User).filter(User.email == user_info['email']).first()
|
||||
|
||||
if db_user:
|
||||
if not db_user.is_google_user:
|
||||
db_user.google_id = user_info['google_id']
|
||||
db_user.is_google_user = True
|
||||
db_user.email_verified = user_info['email_verified']
|
||||
if user_info.get('picture'):
|
||||
db_user.profile_picture = user_info['picture']
|
||||
if user_info.get('first_name') and not db_user.first_name:
|
||||
db_user.first_name = user_info['first_name']
|
||||
if user_info.get('last_name') and not db_user.last_name:
|
||||
db_user.last_name = user_info['last_name']
|
||||
db_user.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
else:
|
||||
db_user = User(
|
||||
email=user_info['email'],
|
||||
google_id=user_info['google_id'],
|
||||
is_google_user=True,
|
||||
email_verified=user_info['email_verified'],
|
||||
first_name=user_info.get('first_name'),
|
||||
last_name=user_info.get('last_name'),
|
||||
profile_picture=user_info.get('picture'),
|
||||
preferred_language=user_info.get('locale', 'en')
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
|
||||
access_token = create_access_token(data={"sub": db_user.email})
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Google OAuth code exchange error: {str(e)}")
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Internal server error during Google authentication"
|
||||
)
|
121
app/services/google_oauth_service.py
Normal file
121
app/services/google_oauth_service.py
Normal file
@ -0,0 +1,121 @@
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
from google.auth.transport import requests as google_requests
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.exceptions import GoogleAuthError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Google OAuth configuration
|
||||
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
|
||||
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
|
||||
|
||||
if not GOOGLE_CLIENT_ID or not GOOGLE_CLIENT_SECRET:
|
||||
logger.warning("Google OAuth credentials not configured")
|
||||
|
||||
|
||||
class GoogleOAuthService:
|
||||
@staticmethod
|
||||
async def verify_google_token(token: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Verify Google ID token and return user information
|
||||
"""
|
||||
if not GOOGLE_CLIENT_ID:
|
||||
logger.error("Google Client ID not configured")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Verify the token
|
||||
id_info = id_token.verify_oauth2_token(
|
||||
token,
|
||||
google_requests.Request(),
|
||||
GOOGLE_CLIENT_ID
|
||||
)
|
||||
|
||||
# Verify that the token is issued by Google
|
||||
if id_info['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
|
||||
logger.error("Invalid token issuer")
|
||||
return None
|
||||
|
||||
# Extract user information
|
||||
user_info = {
|
||||
'google_id': id_info.get('sub'),
|
||||
'email': id_info.get('email'),
|
||||
'email_verified': id_info.get('email_verified', False),
|
||||
'first_name': id_info.get('given_name'),
|
||||
'last_name': id_info.get('family_name'),
|
||||
'full_name': id_info.get('name'),
|
||||
'picture': id_info.get('picture'),
|
||||
'locale': id_info.get('locale', 'en')
|
||||
}
|
||||
|
||||
logger.info(f"Successfully verified Google token for user: {user_info['email']}")
|
||||
return user_info
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid Google token: {e}")
|
||||
return None
|
||||
except GoogleAuthError as e:
|
||||
logger.error(f"Google auth error: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error verifying Google token: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_google_oauth_url() -> Optional[str]:
|
||||
"""
|
||||
Generate Google OAuth URL for frontend to redirect users
|
||||
"""
|
||||
if not GOOGLE_CLIENT_ID:
|
||||
return None
|
||||
|
||||
redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", "http://localhost:3000/auth/google/callback")
|
||||
|
||||
oauth_url = (
|
||||
f"https://accounts.google.com/o/oauth2/auth?"
|
||||
f"client_id={GOOGLE_CLIENT_ID}&"
|
||||
f"redirect_uri={redirect_uri}&"
|
||||
f"scope=openid email profile&"
|
||||
f"response_type=code&"
|
||||
f"access_type=offline&"
|
||||
f"prompt=consent"
|
||||
)
|
||||
|
||||
return oauth_url
|
||||
|
||||
@staticmethod
|
||||
async def exchange_code_for_token(code: str, redirect_uri: str) -> Optional[str]:
|
||||
"""
|
||||
Exchange authorization code for ID token
|
||||
"""
|
||||
if not GOOGLE_CLIENT_ID or not GOOGLE_CLIENT_SECRET:
|
||||
logger.error("Google OAuth credentials not configured")
|
||||
return None
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
token_url = "https://oauth2.googleapis.com/token"
|
||||
|
||||
data = {
|
||||
'client_id': GOOGLE_CLIENT_ID,
|
||||
'client_secret': GOOGLE_CLIENT_SECRET,
|
||||
'code': code,
|
||||
'grant_type': 'authorization_code',
|
||||
'redirect_uri': redirect_uri
|
||||
}
|
||||
|
||||
response = requests.post(token_url, data=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
token_data = response.json()
|
||||
return token_data.get('id_token')
|
||||
else:
|
||||
logger.error(f"Failed to exchange code for token: {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exchanging code for token: {e}")
|
||||
return None
|
@ -12,4 +12,7 @@ ruff==0.1.6
|
||||
requests==2.31.0
|
||||
ffmpeg-python==0.2.0
|
||||
python-dotenv==1.0.0
|
||||
email-validator==2.1.0
|
||||
email-validator==2.1.0
|
||||
google-auth==2.25.2
|
||||
google-auth-oauthlib==1.1.0
|
||||
google-auth-httplib2==0.2.0
|
Loading…
x
Reference in New Issue
Block a user