Files
gh-christianlouis-quizzical…/musicround/helpers/auth_helpers.py
T
Christian Krakau-Louis 03b982e7c2 Initial clean commit
2025-05-13 08:59:02 +00:00

280 lines
10 KiB
Python

"""
Authentication helper functions for OAuth providers
"""
import os
from flask import current_app, url_for, session, flash, redirect, request
from authlib.integrations.flask_client import OAuth
from flask_login import login_user, current_user
from functools import wraps
from datetime import datetime, timedelta
import requests
from musicround.models import db, User
# Initialize OAuth object
oauth = OAuth()
def init_oauth(app):
"""
Initialize OAuth with the Flask app and register providers
"""
oauth.init_app(app)
# Register Google OAuth client
if app.config.get('GOOGLE_CLIENT_ID') and app.config.get('GOOGLE_CLIENT_SECRET'):
oauth.register(
name='google',
client_id=app.config.get('GOOGLE_CLIENT_ID'),
client_secret=app.config.get('GOOGLE_CLIENT_SECRET'),
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={
'scope': 'openid email profile'
}
)
app.logger.info("Google OAuth client registered")
else:
app.logger.warning("Google OAuth client not registered - missing client ID or secret")
# Register Authentik OAuth client
if app.config.get('AUTHENTIK_CLIENT_ID') and app.config.get('AUTHENTIK_CLIENT_SECRET'):
oauth.register(
name='authentik',
client_id=app.config.get('AUTHENTIK_CLIENT_ID'),
client_secret=app.config.get('AUTHENTIK_CLIENT_SECRET'),
server_metadata_url=app.config.get('AUTHENTIK_METADATA_URL'),
client_kwargs={
'scope': 'openid email profile'
}
)
app.logger.info("Authentik OAuth client registered")
else:
app.logger.warning("Authentik OAuth client not registered - missing client ID or secret")
# Register Dropbox OAuth client
if app.config.get('DROPBOX_APP_KEY') and app.config.get('DROPBOX_APP_SECRET'):
oauth.register(
name='dropbox',
client_id=app.config.get('DROPBOX_APP_KEY'),
client_secret=app.config.get('DROPBOX_APP_SECRET'),
authorize_url='https://www.dropbox.com/oauth2/authorize',
authorize_params=None,
access_token_url='https://api.dropboxapi.com/oauth2/token',
access_token_params=None,
refresh_token_url='https://api.dropboxapi.com/oauth2/token',
client_kwargs={
'scope': 'files.content.write account_info.read'
}
)
app.logger.info("Dropbox OAuth client registered")
else:
app.logger.warning("Dropbox OAuth client not registered - missing app key or secret")
return oauth
def get_google_user_info(token):
"""
Get Google user info from the token
"""
try:
resp = oauth.google.get('https://www.googleapis.com/oauth2/v3/userinfo')
profile = resp.json()
# Create a standardized user info dictionary
user_info = {
'id': profile.get('sub'), # Google uses 'sub' as the unique identifier
'email': profile.get('email'),
'name': profile.get('name'),
'given_name': profile.get('given_name'),
'family_name': profile.get('family_name'),
'picture': profile.get('picture')
}
# Add 'sub' field explicitly for backwards compatibility
if profile.get('sub'):
user_info['sub'] = profile.get('sub')
return user_info
except Exception as e:
current_app.logger.error(f"Error getting Google user info: {str(e)}")
return None
def get_authentik_user_info(token):
"""
Get Authentik user info from the token
"""
try:
resp = oauth.authentik.get('userinfo')
profile = resp.json()
return {
'id': profile.get('sub'),
'email': profile.get('email'),
'name': profile.get('name'),
'given_name': profile.get('given_name', ''),
'family_name': profile.get('family_name', ''),
'picture': profile.get('picture', '')
}
except Exception as e:
current_app.logger.error(f"Error getting Authentik user info: {str(e)}")
return None
def get_dropbox_user_info(token):
"""
Get Dropbox user info from the token
"""
try:
# Add debug logging for token
current_app.logger.debug(f"Retrieving Dropbox user info with token: {token}")
# Make sure we have an access token
access_token = token.get("access_token")
if not access_token:
# Try direct token string if token is not a dict
if isinstance(token, str):
access_token = token
else:
current_app.logger.error("No access token found in token object")
return None
# Set proper headers for Dropbox API - no Content-Type for null body
headers = {
'Authorization': f'Bearer {access_token}'
}
# The Dropbox API for get_current_account actually expects a null body with no Content-Type header
response = requests.post(
'https://api.dropboxapi.com/2/users/get_current_account',
headers=headers,
data=None # Send null body
)
# Check for successful response
if response.status_code != 200:
current_app.logger.error(f"Dropbox API error: {response.status_code} - {response.text}")
return None
# Parse response
profile = response.json()
current_app.logger.debug(f"Dropbox user info response: {profile}")
# Create a standardized user info dictionary
user_info = {
'id': profile.get('account_id', ''),
'email': profile.get('email', ''),
'name': profile.get('name', {}).get('display_name', ''),
'given_name': profile.get('name', {}).get('given_name', ''),
'family_name': profile.get('name', {}).get('surname', ''),
'picture': profile.get('profile_photo_url', '')
}
return user_info
except Exception as e:
current_app.logger.error(f"Error getting Dropbox user info: {str(e)}")
return None
def find_or_create_user(user_info, auth_provider):
"""
Find existing user or create a new one based on OAuth user info
"""
if not user_info:
return None
# First try to find user by provider-specific ID
if auth_provider == 'google':
user = User.query.filter_by(google_id=user_info['id']).first()
elif auth_provider == 'authentik':
user = User.query.filter_by(authentik_id=user_info['id']).first()
elif auth_provider == 'dropbox':
user = User.query.filter_by(dropbox_id=user_info['id']).first()
else:
return None
# If not found by provider ID, try email
if user is None and user_info.get('email'):
user = User.query.filter_by(email=user_info['email']).first()
# If user exists but doesn't have provider ID, update it
if user:
if auth_provider == 'google':
user.google_id = user_info['id']
elif auth_provider == 'authentik':
user.authentik_id = user_info['id']
elif auth_provider == 'dropbox':
user.dropbox_id = user_info['id']
db.session.commit()
current_app.logger.info(f"Updated existing user {user.username} with {auth_provider} ID")
# If user still not found, check if new signups are allowed before creating
if user is None:
# Check system setting if new signups are allowed
from musicround.models import SystemSetting
allow_signups = SystemSetting.get('allow_signups', 'true') == 'true'
if not allow_signups:
current_app.logger.warning(f"OAuth signup attempted for {auth_provider} but new signups are disabled")
return None
# Generate a username from email
email = user_info.get('email', '')
base_username = email.split('@')[0] if email else f"{auth_provider}_{user_info['id']}"
# Ensure username is unique
username = base_username
counter = 1
while User.query.filter_by(username=username).first():
username = f"{base_username}_{counter}"
counter += 1
# Create new user
user = User(
username=username,
email=user_info.get('email', ''),
first_name=user_info.get('given_name', ''),
last_name=user_info.get('family_name', ''),
auth_provider=auth_provider,
created_at=datetime.now(),
last_login=datetime.now()
)
# Set provider-specific fields
if auth_provider == 'google':
user.google_id = user_info['id']
elif auth_provider == 'authentik':
user.authentik_id = user_info['id']
elif auth_provider == 'dropbox':
user.dropbox_id = user_info['id']
db.session.add(user)
try:
db.session.commit()
current_app.logger.info(f"Created new user {username} with {auth_provider} auth")
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error creating user: {str(e)}")
return None
return user
def update_oauth_tokens(user, tokens, auth_provider):
"""
Update user's OAuth tokens
"""
if auth_provider == 'google':
user.google_token = tokens.get('access_token')
user.google_refresh_token = tokens.get('refresh_token')
elif auth_provider == 'authentik':
user.authentik_token = tokens.get('access_token')
user.authentik_refresh_token = tokens.get('refresh_token')
elif auth_provider == 'dropbox':
user.dropbox_token = tokens.get('access_token')
user.dropbox_refresh_token = tokens.get('refresh_token')
if tokens.get('expires_in'):
user.dropbox_token_expiry = datetime.now() + timedelta(seconds=int(tokens.get('expires_in')))
user.last_login = datetime.now()
try:
db.session.commit()
return True
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error updating {auth_provider} tokens: {str(e)}")
return False