125 lines
4.1 KiB
Python
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
|