"""Authentication routes for Entra ID login/logout""" import secrets from flask import session, redirect, url_for, request, current_app, render_template, flash from . import auth_bp from app.services.msal_service import MSALService from app.models.user import UserModel @auth_bp.route('/login') def login(): """Initiate Entra ID login flow""" # Generate state token for CSRF protection state = secrets.token_urlsafe(16) session['auth_state'] = state # Store the original requested URL to redirect after login if 'next' not in session and request.args.get('next'): session['next'] = request.args.get('next') # Get authorization URL from MSAL auth_url = MSALService.get_auth_url(state=state) return redirect(auth_url) @auth_bp.route('/callback') def callback(): """Handle the callback from Entra ID after authentication""" # Verify state to prevent CSRF state = request.args.get('state') if state != session.get('auth_state'): current_app.logger.error("State mismatch in auth callback - possible CSRF attack") flash('Authentication failed: Invalid state parameter', 'error') return redirect(url_for('main.index')) # Clear the state session.pop('auth_state', None) # Check for errors if 'error' in request.args: error = request.args.get('error') error_description = request.args.get('error_description', 'Unknown error') current_app.logger.error(f"Auth error: {error} - {error_description}") flash(f'Authentication failed: {error_description}', 'error') return redirect(url_for('main.index')) # Get authorization code auth_code = request.args.get('code') if not auth_code: flash('Authentication failed: No authorization code received', 'error') return redirect(url_for('main.index')) # Exchange code for token token_response = MSALService.acquire_token_by_auth_code(auth_code) if 'error' in token_response: error = token_response.get('error') error_description = token_response.get('error_description', 'Unknown error') current_app.logger.error(f"Token error: {error} - {error_description}") flash(f'Authentication failed: {error_description}', 'error') return redirect(url_for('main.index')) # Get user info from Microsoft Graph access_token = token_response.get('access_token') user_info = MSALService.get_user_info(access_token) if not user_info: flash('Failed to retrieve user information', 'error') return redirect(url_for('main.index')) # Get or create user in database user = UserModel.get_or_create_from_azure( email=user_info['email'], name=user_info['name'], azure_oid=user_info['azure_oid'], default_admin_email=current_app.config['DEFAULT_ADMIN_EMAIL'] ) # Store user in session session['user'] = { 'id': user['id'], 'email': user['email'], 'name': user['name'], 'role': user['role'] } current_app.logger.info(f"User {user['email']} logged in successfully") # Redirect to the originally requested page or home next_url = session.pop('next', None) if next_url: return redirect(next_url) return redirect(url_for('main.index')) @auth_bp.route('/logout') def logout(): """Log out the current user""" if 'user' in session: user_email = session['user'].get('email') current_app.logger.info(f"User {user_email} logged out") # Clear session session.clear() # Remove from MSAL cache MSALService.remove_account() # Redirect to Azure AD logout logout_url = ( f"{current_app.config['AUTHORITY']}{current_app.config['AZURE_TENANT_ID']}/oauth2/v2.0/logout" f"?post_logout_redirect_uri={request.url_root}" ) return redirect(logout_url) @auth_bp.route('/unauthorized') def unauthorized(): """Show unauthorized access page""" return render_template('unauthorized.html'), 403