143 lines
4.7 KiB
Python
143 lines
4.7 KiB
Python
"""Authentication service for Google OAuth."""
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
from authlib.integrations.flask_client import OAuth
|
|
from flask import Flask, make_response, request
|
|
|
|
from app.services.token_service import TokenService
|
|
|
|
|
|
class AuthService:
|
|
"""Service for handling Google OAuth authentication."""
|
|
|
|
def __init__(self, app: Flask | None = None) -> None:
|
|
"""Initialize the authentication service."""
|
|
self.oauth = OAuth()
|
|
self.google = None
|
|
self.token_service = TokenService()
|
|
if app:
|
|
self.init_app(app)
|
|
|
|
def init_app(self, app: Flask) -> None:
|
|
"""Initialize the service with Flask app."""
|
|
self.oauth.init_app(app)
|
|
|
|
# Configure Google OAuth
|
|
self.google = self.oauth.register(
|
|
name="google",
|
|
client_id=os.getenv("GOOGLE_CLIENT_ID"),
|
|
client_secret=os.getenv("GOOGLE_CLIENT_SECRET"),
|
|
server_metadata_url="https://accounts.google.com/.well-known/openid_configuration",
|
|
client_kwargs={"scope": "openid email profile"},
|
|
)
|
|
|
|
def get_login_url(self, redirect_uri: str) -> str:
|
|
"""Generate Google OAuth login URL."""
|
|
if not self.google:
|
|
msg = "Google OAuth not configured"
|
|
raise RuntimeError(msg)
|
|
return self.google.authorize_redirect(redirect_uri).location
|
|
|
|
def handle_callback(self) -> tuple[dict[str, Any], Any]:
|
|
"""Handle OAuth callback and exchange code for token."""
|
|
if not self.google:
|
|
msg = "Google OAuth not configured"
|
|
raise RuntimeError(msg)
|
|
|
|
token = self.google.authorize_access_token()
|
|
user_info = token.get("userinfo")
|
|
|
|
if user_info:
|
|
user_data = {
|
|
"id": user_info["sub"],
|
|
"email": user_info["email"],
|
|
"name": user_info["name"],
|
|
"picture": user_info.get("picture"),
|
|
}
|
|
|
|
# Generate JWT tokens
|
|
access_token = self.token_service.generate_access_token(user_data)
|
|
refresh_token = self.token_service.generate_refresh_token(user_data)
|
|
|
|
# Create response and set HTTP-only cookies
|
|
response = make_response({
|
|
"message": "Login successful",
|
|
"user": user_data,
|
|
})
|
|
|
|
response.set_cookie(
|
|
"access_token",
|
|
access_token,
|
|
httponly=True,
|
|
secure=True,
|
|
samesite="Lax",
|
|
max_age=15 * 60, # 15 minutes
|
|
)
|
|
|
|
response.set_cookie(
|
|
"refresh_token",
|
|
refresh_token,
|
|
httponly=True,
|
|
secure=True,
|
|
samesite="Lax",
|
|
max_age=7 * 24 * 60 * 60, # 7 days
|
|
)
|
|
|
|
return user_data, response
|
|
|
|
msg = "Failed to get user information from Google"
|
|
raise ValueError(msg)
|
|
|
|
def get_current_user(self) -> dict[str, Any] | None:
|
|
"""Get current user from access token."""
|
|
access_token = request.cookies.get("access_token")
|
|
if not access_token:
|
|
return None
|
|
|
|
return self.token_service.get_user_from_access_token(access_token)
|
|
|
|
def refresh_tokens(self) -> Any:
|
|
"""Refresh access token using refresh token."""
|
|
refresh_token = request.cookies.get("refresh_token")
|
|
if not refresh_token:
|
|
return None
|
|
|
|
payload = self.token_service.verify_token(refresh_token)
|
|
if not payload or not self.token_service.is_refresh_token(payload):
|
|
return None
|
|
|
|
# For refresh, we need to get user data (in a real app, from database)
|
|
# For now, we'll extract what we can from the refresh token
|
|
user_data = {
|
|
"id": payload["user_id"],
|
|
"email": "", # Would need to fetch from database
|
|
"name": "", # Would need to fetch from database
|
|
}
|
|
|
|
# Generate new access token
|
|
new_access_token = self.token_service.generate_access_token(user_data)
|
|
|
|
response = make_response({"message": "Token refreshed"})
|
|
response.set_cookie(
|
|
"access_token",
|
|
new_access_token,
|
|
httponly=True,
|
|
secure=True,
|
|
samesite="Lax",
|
|
max_age=15 * 60, # 15 minutes
|
|
)
|
|
|
|
return response
|
|
|
|
def logout(self) -> Any:
|
|
"""Clear authentication cookies."""
|
|
response = make_response({"message": "Logged out successfully"})
|
|
response.set_cookie("access_token", "", expires=0)
|
|
response.set_cookie("refresh_token", "", expires=0)
|
|
return response
|
|
|
|
def is_authenticated(self) -> bool:
|
|
"""Check if user is authenticated."""
|
|
return self.get_current_user() is not None |