auth google + jwt
This commit is contained in:
23
app/__init__.py
Normal file
23
app/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import os
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create and configure the Flask application."""
|
||||
app = Flask(__name__)
|
||||
|
||||
# Configure session
|
||||
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-key")
|
||||
|
||||
# Initialize authentication service
|
||||
auth_service = AuthService(app)
|
||||
|
||||
# Register blueprints
|
||||
from app.routes import main, auth
|
||||
app.register_blueprint(main.bp, url_prefix="/api")
|
||||
app.register_blueprint(auth.bp, url_prefix="/api/auth")
|
||||
|
||||
return app
|
||||
0
app/routes/__init__.py
Normal file
0
app/routes/__init__.py
Normal file
52
app/routes/auth.py
Normal file
52
app/routes/auth.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Authentication routes."""
|
||||
|
||||
from flask import Blueprint, url_for
|
||||
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
bp = Blueprint("auth", __name__)
|
||||
auth_service = AuthService()
|
||||
|
||||
|
||||
@bp.route("/login")
|
||||
def login() -> dict[str, str]:
|
||||
"""Initiate Google OAuth login."""
|
||||
redirect_uri = url_for("auth.callback", _external=True)
|
||||
login_url = auth_service.get_login_url(redirect_uri)
|
||||
return {"login_url": login_url}
|
||||
|
||||
|
||||
@bp.route("/callback")
|
||||
def callback():
|
||||
"""Handle OAuth callback from Google."""
|
||||
try:
|
||||
user_data, response = auth_service.handle_callback()
|
||||
return response
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
|
||||
|
||||
@bp.route("/logout")
|
||||
def logout():
|
||||
"""Logout current user."""
|
||||
return auth_service.logout()
|
||||
|
||||
|
||||
@bp.route("/me")
|
||||
def me() -> dict[str, str] | tuple[dict[str, str], int]:
|
||||
"""Get current user information."""
|
||||
user = auth_service.get_current_user()
|
||||
if not user:
|
||||
return {"error": "Not authenticated"}, 401
|
||||
|
||||
return {"user": user}
|
||||
|
||||
|
||||
@bp.route("/refresh")
|
||||
def refresh():
|
||||
"""Refresh access token using refresh token."""
|
||||
response = auth_service.refresh_tokens()
|
||||
if not response:
|
||||
return {"error": "Invalid or expired refresh token"}, 401
|
||||
|
||||
return response
|
||||
38
app/routes/main.py
Normal file
38
app/routes/main.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Main routes for the application."""
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from app.services.decorators import get_current_user, require_auth
|
||||
from app.services.greeting_service import GreetingService
|
||||
|
||||
bp = Blueprint("main", __name__)
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
def index() -> dict[str, str]:
|
||||
"""Root endpoint that returns a greeting."""
|
||||
return GreetingService.get_greeting()
|
||||
|
||||
|
||||
@bp.route("/hello")
|
||||
@bp.route("/hello/<name>")
|
||||
def hello(name: str | None = None) -> dict[str, str]:
|
||||
"""Hello endpoint with optional name parameter."""
|
||||
return GreetingService.get_greeting(name)
|
||||
|
||||
|
||||
@bp.route("/protected")
|
||||
@require_auth
|
||||
def protected() -> dict[str, str]:
|
||||
"""Protected endpoint that requires authentication."""
|
||||
user = get_current_user()
|
||||
return {
|
||||
"message": f"Hello {user['name']}, this is a protected endpoint!",
|
||||
"user": user
|
||||
}
|
||||
|
||||
|
||||
@bp.route("/health")
|
||||
def health() -> dict[str, str]:
|
||||
"""Health check endpoint."""
|
||||
return {"status": "ok"}
|
||||
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
143
app/services/auth_service.py
Normal file
143
app/services/auth_service.py
Normal 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
|
||||
37
app/services/decorators.py
Normal file
37
app/services/decorators.py
Normal 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)
|
||||
22
app/services/greeting_service.py
Normal file
22
app/services/greeting_service.py
Normal 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}
|
||||
75
app/services/token_service.py
Normal file
75
app/services/token_service.py
Normal 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
|
||||
Reference in New Issue
Block a user