auth google + jwt

This commit is contained in:
JSC
2025-06-27 13:14:29 +02:00
commit 8e2dbd8723
21 changed files with 1107 additions and 0 deletions

0
app/services/__init__.py Normal file
View File

View File

@@ -0,0 +1,143 @@
"""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

View File

@@ -0,0 +1,37 @@
"""Authentication decorators and middleware."""
from functools import wraps
from typing import Any, Callable
from flask import jsonify, request
from app.services.token_service import TokenService
def require_auth(f: Callable[..., Any]) -> Callable[..., Any]:
"""Decorator to require authentication for routes."""
@wraps(f)
def decorated_function(*args: Any, **kwargs: Any) -> Any:
token_service = TokenService()
access_token = request.cookies.get("access_token")
if not access_token:
return jsonify({"error": "Authentication required"}), 401
user_data = token_service.get_user_from_access_token(access_token)
if not user_data:
return jsonify({"error": "Invalid or expired token"}), 401
return f(*args, **kwargs)
return decorated_function
def get_current_user() -> dict[str, Any] | None:
"""Helper function to get current user from access token."""
token_service = TokenService()
access_token = request.cookies.get("access_token")
if not access_token:
return None
return token_service.get_user_from_access_token(access_token)

View File

@@ -0,0 +1,22 @@
"""Service for handling greeting-related business logic."""
class GreetingService:
"""Service for greeting operations."""
@staticmethod
def get_greeting(name: str | None = None) -> dict[str, str]:
"""Get a greeting message.
Args:
name: Optional name to personalize the greeting
Returns:
Dictionary containing the greeting message
"""
if name:
message = f"Hello, {name}!"
else:
message = "Hello from backend!"
return {"message": message}

View File

@@ -0,0 +1,75 @@
"""JWT token service for handling access and refresh tokens."""
import os
from datetime import datetime, timedelta, timezone
from typing import Any
import jwt
class TokenService:
"""Service for handling JWT tokens."""
def __init__(self) -> None:
"""Initialize the token service."""
self.secret_key = os.environ.get("JWT_SECRET_KEY", "jwt-secret-key")
self.algorithm = "HS256"
self.access_token_expire_minutes = 15
self.refresh_token_expire_days = 7
def generate_access_token(self, user_data: dict[str, Any]) -> str:
"""Generate an access token for the user."""
payload = {
"user_id": user_data["id"],
"email": user_data["email"],
"name": user_data["name"],
"type": "access",
"exp": datetime.now(timezone.utc) + timedelta(
minutes=self.access_token_expire_minutes
),
"iat": datetime.now(timezone.utc),
}
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
def generate_refresh_token(self, user_data: dict[str, Any]) -> str:
"""Generate a refresh token for the user."""
payload = {
"user_id": user_data["id"],
"type": "refresh",
"exp": datetime.now(timezone.utc) + timedelta(
days=self.refresh_token_expire_days
),
"iat": datetime.now(timezone.utc),
}
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
def verify_token(self, token: str) -> dict[str, Any] | None:
"""Verify and decode a JWT token."""
try:
payload = jwt.decode(
token, self.secret_key, algorithms=[self.algorithm]
)
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
def is_access_token(self, payload: dict[str, Any]) -> bool:
"""Check if the token payload is for an access token."""
return payload.get("type") == "access"
def is_refresh_token(self, payload: dict[str, Any]) -> bool:
"""Check if the token payload is for a refresh token."""
return payload.get("type") == "refresh"
def get_user_from_access_token(self, token: str) -> dict[str, Any] | None:
"""Extract user data from access token."""
payload = self.verify_token(token)
if payload and self.is_access_token(payload):
return {
"id": payload["user_id"],
"email": payload["email"],
"name": payload["name"],
}
return None