auth email/password
This commit is contained in:
@@ -1,21 +1,29 @@
|
||||
"""Authentication service for Google OAuth."""
|
||||
"""Authentication service for multiple OAuth providers."""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
from flask import Flask, make_response, request
|
||||
from flask import Flask, jsonify
|
||||
from flask_jwt_extended import (
|
||||
get_jwt_identity,
|
||||
jwt_required,
|
||||
set_access_cookies,
|
||||
set_refresh_cookies,
|
||||
unset_jwt_cookies,
|
||||
)
|
||||
|
||||
from app.models.user import User
|
||||
from app.services.oauth_providers.registry import OAuthProviderRegistry
|
||||
from app.services.token_service import TokenService
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""Service for handling Google OAuth authentication."""
|
||||
"""Service for handling multiple OAuth providers authentication."""
|
||||
|
||||
def __init__(self, app: Flask | None = None) -> None:
|
||||
"""Initialize the authentication service."""
|
||||
self.oauth = OAuth()
|
||||
self.google = None
|
||||
self.provider_registry = None
|
||||
self.token_service = TokenService()
|
||||
if app:
|
||||
self.init_app(app)
|
||||
@@ -24,120 +32,219 @@ class AuthService:
|
||||
"""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"},
|
||||
)
|
||||
# Initialize provider registry
|
||||
self.provider_registry = OAuthProviderRegistry(self.oauth)
|
||||
|
||||
def get_login_url(self, redirect_uri: str) -> str:
|
||||
"""Generate Google OAuth login URL."""
|
||||
if not self.google:
|
||||
msg = "Google OAuth not configured"
|
||||
def redirect_to_login(self, provider_name: str, redirect_uri: str):
|
||||
"""Redirect to OAuth provider login."""
|
||||
provider = self.provider_registry.get_provider(provider_name)
|
||||
if not provider:
|
||||
msg = f"OAuth provider '{provider_name}' not configured"
|
||||
raise RuntimeError(msg)
|
||||
return self.google.authorize_redirect(redirect_uri).location
|
||||
|
||||
def handle_callback(self) -> tuple[dict[str, Any], Any]:
|
||||
client = provider.get_client()
|
||||
return client.authorize_redirect(redirect_uri)
|
||||
|
||||
def get_login_url(self, provider_name: str, redirect_uri: str) -> str:
|
||||
"""Generate OAuth provider login URL (for testing or manual use)."""
|
||||
provider = self.provider_registry.get_provider(provider_name)
|
||||
if not provider:
|
||||
msg = f"OAuth provider '{provider_name}' not configured"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
return provider.get_authorization_url(redirect_uri)
|
||||
|
||||
def handle_callback(self, provider_name: str) -> Any:
|
||||
"""Handle OAuth callback and exchange code for token."""
|
||||
if not self.google:
|
||||
msg = "Google OAuth not configured"
|
||||
provider = self.provider_registry.get_provider(provider_name)
|
||||
if not provider:
|
||||
msg = f"OAuth provider '{provider_name}' not configured"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
token = self.google.authorize_access_token()
|
||||
user_info = token.get("userinfo")
|
||||
token = provider.exchange_code_for_token(None, None)
|
||||
raw_user_info = provider.get_user_info(token)
|
||||
user_data = provider.normalize_user_data(raw_user_info)
|
||||
|
||||
if user_info:
|
||||
user_data = {
|
||||
"id": user_info["sub"],
|
||||
"email": user_info["email"],
|
||||
"name": user_info["name"],
|
||||
"picture": user_info.get("picture"),
|
||||
if user_data and user_data.get("id"):
|
||||
# Find or create user in database
|
||||
user, oauth_provider = User.find_or_create_from_oauth(
|
||||
provider=provider_name,
|
||||
provider_id=user_data["id"],
|
||||
email=user_data["email"],
|
||||
name=user_data["name"],
|
||||
picture=user_data.get("picture"),
|
||||
)
|
||||
|
||||
# Check if user account is active
|
||||
if not user.is_active:
|
||||
response = jsonify({"error": "Account is disabled"})
|
||||
response.status_code = 401
|
||||
return response
|
||||
|
||||
# Prepare user data for JWT token
|
||||
jwt_user_data = {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"picture": user.picture,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active,
|
||||
"provider": oauth_provider.provider,
|
||||
"providers": [p.provider for p in user.oauth_providers],
|
||||
}
|
||||
|
||||
# Generate JWT tokens
|
||||
access_token = self.token_service.generate_access_token(user_data)
|
||||
refresh_token = self.token_service.generate_refresh_token(user_data)
|
||||
access_token = self.token_service.generate_access_token(
|
||||
jwt_user_data
|
||||
)
|
||||
refresh_token = self.token_service.generate_refresh_token(
|
||||
jwt_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 = jsonify(
|
||||
{
|
||||
"message": "Login successful",
|
||||
"user": jwt_user_data,
|
||||
}
|
||||
)
|
||||
|
||||
response.set_cookie(
|
||||
"refresh_token",
|
||||
refresh_token,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite="Lax",
|
||||
max_age=7 * 24 * 60 * 60, # 7 days
|
||||
)
|
||||
# Set JWT cookies
|
||||
set_access_cookies(response, access_token)
|
||||
set_refresh_cookies(response, refresh_token)
|
||||
|
||||
return user_data, response
|
||||
return response
|
||||
|
||||
msg = "Failed to get user information from Google"
|
||||
msg = f"Failed to get user information from {provider.display_name}"
|
||||
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
|
||||
def get_available_providers(self) -> dict[str, Any]:
|
||||
"""Get list of available OAuth providers."""
|
||||
if not self.provider_registry:
|
||||
return {}
|
||||
|
||||
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
|
||||
providers = self.provider_registry.get_available_providers()
|
||||
return {
|
||||
name: {"name": provider.name, "display_name": provider.display_name}
|
||||
for name, provider in providers.items()
|
||||
}
|
||||
|
||||
# Generate new access token
|
||||
new_access_token = self.token_service.generate_access_token(user_data)
|
||||
@jwt_required()
|
||||
def get_current_user(self) -> dict[str, Any] | None:
|
||||
"""Get current user from JWT token."""
|
||||
from flask_jwt_extended import get_jwt
|
||||
|
||||
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
|
||||
current_user_id = get_jwt_identity()
|
||||
claims = get_jwt()
|
||||
|
||||
if current_user_id:
|
||||
return {
|
||||
"id": current_user_id,
|
||||
"email": claims.get("email", ""),
|
||||
"name": claims.get("name", ""),
|
||||
"picture": claims.get("picture"),
|
||||
"role": claims.get("role", "user"),
|
||||
"is_active": claims.get("is_active", True),
|
||||
"provider": claims.get("provider", "unknown"),
|
||||
"providers": claims.get("providers", []),
|
||||
}
|
||||
return None
|
||||
|
||||
def register_with_password(
|
||||
self, email: str, password: str, name: str
|
||||
) -> Any:
|
||||
"""Register new user with email and password."""
|
||||
try:
|
||||
# Create user with password
|
||||
user = User.create_with_password(email, password, name)
|
||||
|
||||
# Prepare user data for JWT token
|
||||
jwt_user_data = {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"picture": user.picture,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active,
|
||||
"provider": "password",
|
||||
"providers": ["password"],
|
||||
}
|
||||
|
||||
# Generate JWT tokens
|
||||
access_token = self.token_service.generate_access_token(
|
||||
jwt_user_data
|
||||
)
|
||||
refresh_token = self.token_service.generate_refresh_token(
|
||||
jwt_user_data
|
||||
)
|
||||
|
||||
# Create response and set HTTP-only cookies
|
||||
response = jsonify(
|
||||
{
|
||||
"message": "Registration successful",
|
||||
"user": jwt_user_data,
|
||||
}
|
||||
)
|
||||
|
||||
# Set JWT cookies
|
||||
set_access_cookies(response, access_token)
|
||||
set_refresh_cookies(response, refresh_token)
|
||||
|
||||
return response
|
||||
|
||||
except ValueError as e:
|
||||
response = jsonify({"error": str(e)})
|
||||
response.status_code = 400
|
||||
return response
|
||||
|
||||
def login_with_password(self, email: str, password: str) -> Any:
|
||||
"""Login user with email and password."""
|
||||
# Authenticate user
|
||||
user = User.authenticate_with_password(email, password)
|
||||
|
||||
if not user:
|
||||
response = jsonify(
|
||||
{"error": "Invalid email, password or disabled account"}
|
||||
)
|
||||
response.status_code = 401
|
||||
return response
|
||||
|
||||
# Prepare user data for JWT token
|
||||
oauth_providers = [p.provider for p in user.oauth_providers]
|
||||
if user.has_password():
|
||||
oauth_providers.append("password")
|
||||
|
||||
jwt_user_data = {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"picture": user.picture,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active,
|
||||
"provider": "password",
|
||||
"providers": oauth_providers,
|
||||
}
|
||||
|
||||
# Generate JWT tokens
|
||||
access_token = self.token_service.generate_access_token(jwt_user_data)
|
||||
refresh_token = self.token_service.generate_refresh_token(jwt_user_data)
|
||||
|
||||
# Create response and set HTTP-only cookies
|
||||
response = jsonify(
|
||||
{
|
||||
"message": "Login successful",
|
||||
"user": jwt_user_data,
|
||||
}
|
||||
)
|
||||
|
||||
# Set JWT cookies
|
||||
set_access_cookies(response, access_token)
|
||||
set_refresh_cookies(response, refresh_token)
|
||||
|
||||
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)
|
||||
response = jsonify({"message": "Logged out successfully"})
|
||||
unset_jwt_cookies(response)
|
||||
return response
|
||||
|
||||
def is_authenticated(self) -> bool:
|
||||
"""Check if user is authenticated."""
|
||||
return self.get_current_user() is not None
|
||||
Reference in New Issue
Block a user