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
|
# Authentication
|
||||||
SECRET_KEY=your-secret-key-change-this-in-production
|
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 S3 Configuration
|
||||||
AWS_ACCESS_KEY_ID=your-aws-access-key
|
AWS_ACCESS_KEY_ID=your-aws-access-key
|
||||||
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
|
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
|
||||||
@ -76,8 +81,11 @@ The API will be available at:
|
|||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
- `POST /auth/register` - User registration
|
- `POST /auth/register` - User registration with email/password
|
||||||
- `POST /auth/login` - User login
|
- `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
|
### Profile Management
|
||||||
- `GET /profile/` - Get user profile
|
- `GET /profile/` - Get user profile
|
||||||
@ -104,9 +112,59 @@ The API will be available at:
|
|||||||
### Results
|
### Results
|
||||||
- `GET /process/results/{video_id}` - Get complete processing 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
|
## 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
|
2. **Upload Video** with source and target languages
|
||||||
3. **Transcribe** the audio from the video
|
3. **Transcribe** the audio from the video
|
||||||
4. **Translate** the transcribed text
|
4. **Translate** the transcribed text
|
||||||
@ -119,6 +177,9 @@ The API will be available at:
|
|||||||
| Variable | Description | Required |
|
| Variable | Description | Required |
|
||||||
|----------|-------------|----------|
|
|----------|-------------|----------|
|
||||||
| `SECRET_KEY` | JWT secret key for authentication | Yes |
|
| `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_ACCESS_KEY_ID` | AWS access key for S3 | Yes |
|
||||||
| `AWS_SECRET_ACCESS_KEY` | AWS secret key for S3 | Yes |
|
| `AWS_SECRET_ACCESS_KEY` | AWS secret key for S3 | Yes |
|
||||||
| `AWS_REGION` | AWS region (default: us-east-1) | No |
|
| `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 |
|
| `OPENAI_API_KEY` | OpenAI API key for Whisper and GPT-4 | Yes |
|
||||||
| `ELEVENLABS_API_KEY` | ElevenLabs API key for voice cloning | Yes |
|
| `ELEVENLABS_API_KEY` | ElevenLabs API key for voice cloning | Yes |
|
||||||
|
|
||||||
|
*Required only if Google OAuth is enabled
|
||||||
|
|
||||||
## File Storage Structure
|
## File Storage Structure
|
||||||
|
|
||||||
Files are stored in S3 with the following 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 datetime import datetime
|
||||||
from app.db.base import Base
|
from app.db.base import Base
|
||||||
|
|
||||||
@ -8,13 +8,20 @@ class User(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
email = Column(String, unique=True, index=True, nullable=False)
|
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)
|
first_name = Column(String, nullable=True)
|
||||||
last_name = Column(String, nullable=True)
|
last_name = Column(String, nullable=True)
|
||||||
phone = Column(String, nullable=True)
|
phone = Column(String, nullable=True)
|
||||||
bio = Column(String, nullable=True)
|
bio = Column(String, nullable=True)
|
||||||
preferred_language = Column(String, default="en")
|
preferred_language = Column(String, default="en")
|
||||||
timezone = Column(String, default="UTC")
|
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)
|
created_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
updated_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:
|
if not self.created_at:
|
||||||
self.created_at = datetime.utcnow()
|
self.created_at = datetime.utcnow()
|
||||||
if not self.updated_at:
|
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 fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.utils.auth import get_password_hash, verify_password, create_access_token
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -89,7 +92,25 @@ async def register(user: UserCreate, db: Session = Depends(get_db)):
|
|||||||
@router.post("/login", response_model=Token)
|
@router.post("/login", response_model=Token)
|
||||||
async def login(user: UserLogin, db: Session = Depends(get_db)):
|
async def login(user: UserLogin, db: Session = Depends(get_db)):
|
||||||
db_user = db.query(User).filter(User.email == user.email).first()
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Incorrect email or password",
|
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})
|
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
|
requests==2.31.0
|
||||||
ffmpeg-python==0.2.0
|
ffmpeg-python==0.2.0
|
||||||
python-dotenv==1.0.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