Files
triviathang/backend/routes/auth.py
2025-12-22 14:47:25 -05:00

125 lines
4.1 KiB
Python

from flask import Blueprint, request, jsonify, redirect, make_response, current_app, g
from backend.auth import oauth
from backend.auth.middleware import require_auth
from backend.models import db, User
from datetime import datetime
bp = Blueprint('auth', __name__, url_prefix='/api/auth')
@bp.route('/login')
def login():
"""Redirect to Authelia login page"""
redirect_uri = current_app.config['OIDC_REDIRECT_URI']
return oauth.authelia.authorize_redirect(redirect_uri)
@bp.route('/callback')
def callback():
"""Handle OIDC callback from Authelia"""
try:
# Exchange authorization code for tokens
token = oauth.authelia.authorize_access_token()
# Parse ID token to get user info
user_info = token.get('userinfo')
if not user_info:
user_info = oauth.authelia.parse_id_token(token)
# Get or create user
user = User.query.filter_by(authelia_sub=user_info['sub']).first()
if not user:
user = User(
authelia_sub=user_info['sub'],
email=user_info.get('email'),
name=user_info.get('name'),
preferred_username=user_info.get('preferred_username'),
groups=user_info.get('groups', [])
)
db.session.add(user)
else:
user.email = user_info.get('email')
user.name = user_info.get('name')
user.preferred_username = user_info.get('preferred_username')
user.groups = user_info.get('groups', [])
user.last_login = datetime.utcnow()
db.session.commit()
# Redirect to frontend with tokens in URL fragment (SPA pattern)
frontend_url = current_app.config.get('FRONTEND_URL', 'http://localhost:3000')
# Create response with refresh token in HTTP-only cookie
response = make_response(redirect(
f"{frontend_url}/auth/callback#access_token={token['access_token']}"
f"&id_token={token['id_token']}"
f"&expires_in={token.get('expires_in', 900)}"
))
# Set refresh token as HTTP-only cookie
if token.get('refresh_token'):
response.set_cookie(
'refresh_token',
value=token['refresh_token'],
httponly=True,
secure=current_app.config.get('SESSION_COOKIE_SECURE', False),
samesite='Strict',
max_age=7*24*60*60 # 7 days
)
return response
except Exception as e:
current_app.logger.error(f"OIDC callback error: {e}")
frontend_url = current_app.config.get('FRONTEND_URL', 'http://localhost:3000')
return redirect(f"{frontend_url}/login?error=auth_failed")
@bp.route('/refresh', methods=['POST'])
def refresh():
"""Refresh access token using refresh token"""
refresh_token = request.cookies.get('refresh_token')
if not refresh_token:
return jsonify({'error': 'No refresh token'}), 401
try:
# Exchange refresh token for new access token
new_token = oauth.authelia.fetch_access_token(
grant_type='refresh_token',
refresh_token=refresh_token
)
return jsonify({
'access_token': new_token['access_token'],
'expires_in': new_token.get('expires_in', 900)
}), 200
except Exception as e:
current_app.logger.error(f"Token refresh failed: {e}")
return jsonify({'error': 'Token refresh failed'}), 401
@bp.route('/logout', methods=['POST'])
def logout():
"""Logout user and revoke tokens"""
# Clear refresh token cookie
response = make_response(jsonify({'message': 'Logged out'}), 200)
response.set_cookie('refresh_token', '', expires=0)
# Return Authelia logout URL for frontend to redirect
authelia_logout_url = f"{current_app.config['OIDC_ISSUER']}/logout"
return jsonify({
'message': 'Logged out',
'logout_url': authelia_logout_url
}), 200
@bp.route('/me')
@require_auth
def get_current_user():
"""Get current user info (requires auth)"""
return jsonify(g.current_user.to_dict()), 200