The application is called "Adaptive Authentication" it is meant to find the risk associated with a login attempt by considering login context parameters like key stroke dynamics, time of day, location. This context info is passed to a rule engine which compares it against historical login data to give a risk score.
The front end has a login container, a context simulator to vary context parameters and an output box which shows the risk score. The risk score is a clickable element. Once clicked on, it will give a pop-up explaining the contributing factors of that score

The risk scoring and explaining functions with Flask code are as follows:
```
@app.route('/generate_risk_score', methods=['POST'])
def generate_risk_score():
    global last_contribs, risk_score_glob
    data = request.json
    #last_login_attempts[data['username']] = data
    username = data.get('username')
    print("--------")
    print(data)
    print("--------\n\n")
    
    password_validation_success = data.get('password_validation_success', False)

    if username not in USER_DATA:
        return jsonify({"error": "User not found"}), 404

    # Create a new record with all relevant information
    new_record = {
        "timestamp": data.get('datetime'),  # Use the datetime from the payload
        "password_validation_success": password_validation_success,
        "login_success": None,  # This will be updated later if MFA is triggered
        "device_type": data.get('deviceType'),
        "browser": data.get('browser'),
        "os": data.get('os'),
        "vpn": data.get('vpn'),
        "keystroke_timing": data.get('keyStrokeTiming'),
        "shift_count": data.get('shiftCount', 0),
        "caps_lock_count": data.get('capsLockCount', 0),
        "tab_count": data.get('tabCount', 0),
        "backspace_count": data.get('backspaceCount', 0),
        "delete_count": data.get('deleteCount', 0),
        "enter_count": data.get('enterCount', 0),
        "mouse_click_count": data.get('mouseClickCount', 0),
        "latitude": data.get('lat', 19),  # Not provided in the current payload
        "longitude": data.get('longi', 73)  # Not provided in the current payload
    }

    # Add the new record to the user's records
    USER_DATA[username]["records"].append(new_record)
    print("\n-------USER_DATA-----")
    print(USER_DATA[username])
    print("---------------------\n")

    if not password_validation_success:
        return jsonify({
            "risk_score": None,
            "record_id": len(USER_DATA[username]["records"]) - 1  # Index of the new record
        }), 200

    # Calculate risk score
    risk_score, last_contribs = calculate_risk_score(username, new_record)
    risk_score_glob = risk_score
    print("main risk score", risk_score)

    return jsonify({
        "risk_score": risk_score,
        "record_id": len(USER_DATA[username]["records"]) - 1  # Index of the new record
    }), 200

def calculate_risk_score(username, new_record):
    all_user_records = USER_DATA[username]["records"]
    
    # Filter for successful logins (both password and MFA if applicable)
    successful_logins = [
        record for record in all_user_records[:-1]  # Exclude the current login attempt
        if record["password_validation_success"] and 
        (record["login_success"] is True or record["login_success"] is None)
    ]
    
    # Initialize risk factors
    risk_factors = {
        "device_mismatch": 0,
        "browser_mismatch": 0,
        "os_mismatch": 0,
        "location_anomaly": 0,
        "time_anomaly": 0,
        "keystroke_anomaly": 0,
        "input_behavior_anomaly": 0,
        "recent_failed_attempts": 0,
        "ddos_protection": 0
    }

    # Check device, browser, and OS consistency
    if successful_logins:
        device_types = set(record["device_type"] for record in successful_logins)
        browsers = set(record["browser"] for record in successful_logins)
        os_types = set(record["os"] for record in successful_logins)

        if new_record["device_type"] not in device_types:
            risk_factors["device_mismatch"] = 0.2
        if new_record["browser"] not in browsers:
            risk_factors["browser_mismatch"] = 0.15
        if new_record["os"] not in os_types:
            risk_factors["os_mismatch"] = 0.15

    # Check location anomaly
    if successful_logins:
        prev_locations = [(record["latitude"], record["longitude"]) for record in successful_logins if record["latitude"] and record["longitude"]]
        most_recent_login = next(
            (record for record in reversed(successful_logins)
             if record["latitude"] and record["longitude"]),
            None
        )
        if prev_locations:
            avg_lat = sum(loc[0] for loc in prev_locations) / len(prev_locations)
            avg_lon = sum(loc[1] for loc in prev_locations) / len(prev_locations)
            new_lat, new_lon = new_record["latitude"], new_record["longitude"]
            
            if new_lat and new_lon:
                distance = calculate_distance(
                    most_recent_login["latitude"], 
                    most_recent_login["longitude"], 
                    new_lat, 
                    new_lon
                )
                #distance = calculate_distance(avg_lat, avg_lon, new_lat, new_lon)
                print("\n\n----distance-------\n\n", distance, "\n---------------\n")
                if distance > 100:  # More than 1000 km
                    risk_factors["location_anomaly"] = min(distance / 500, 0.5)  # Cap at 0.5

    # Check time anomaly
    if successful_logins:
        prev_times = [datetime.fromisoformat(record["timestamp"]) for record in successful_logins]
        new_time = datetime.fromisoformat(new_record["timestamp"])
        avg_hour = sum(t.hour for t in prev_times) / len(prev_times)
        time_diff = min(abs(new_time.hour - avg_hour), 24 - abs(new_time.hour - avg_hour))
        risk_factors["time_anomaly"] = min(time_diff / 12, 0.3)  # Cap at 0.3

    # Check keystroke timing anomaly
    if successful_logins:
        prev_keystroke_timings = [record["keystroke_timing"] for record in successful_logins if record["keystroke_timing"]]
        if prev_keystroke_timings and new_record["keystroke_timing"]:
            avg_timing = sum(prev_keystroke_timings) / len(prev_keystroke_timings)
            timing_diff = abs(new_record["keystroke_timing"] - avg_timing)
            risk_factors["keystroke_anomaly"] = min(timing_diff / 1000, 0.25)  # Cap at 0.25

    # Check input behavior anomaly
    if successful_logins:
        input_fields = ["shift_count", "caps_lock_count", "tab_count", "backspace_count", "delete_count", "enter_count", "mouse_click_count"]
        prev_behaviors = {field: [record[field] for record in successful_logins] for field in input_fields}
        
        behavior_diff = 0
        for field in input_fields:
            if prev_behaviors[field]:
                avg_value = sum(prev_behaviors[field]) / len(prev_behaviors[field])
                behavior_diff += abs(new_record[field] - avg_value) / (avg_value + 1)  # Add 1 to avoid division by zero
        
        risk_factors["input_behavior_anomaly"] = min(behavior_diff / len(input_fields) / 2, 0.3)  # Cap at 0.3

    # Check recent failed attempts and implement DDOS protection
    current_time = datetime.fromisoformat(new_record["timestamp"])
    print("New record", new_record["timestamp"])
    if current_time.tzinfo is None:
        current_time = current_time.replace(tzinfo=timezone.utc)
    print("modified timestamp", current_time)
    
    for record in all_user_records:
        print("->d", record["timestamp"], current_time, current_time - timedelta(minutes=30))
 
    recent_attempts = [
        record for record in all_user_records
        if datetime.fromisoformat(record["timestamp"]) > current_time - timedelta(minutes=30)
    ]
    
    failed_attempts = [record for record in recent_attempts if not record["password_validation_success"] or record["login_success"] is False]
    
    if failed_attempts:
        risk_factors["recent_failed_attempts"] = min(len(failed_attempts) * 0.1, 0.4)  # Cap at 0.4
        
        # DDOS protection
        if len(failed_attempts) > 10:
            risk_factors["ddos_protection"] = min((len(failed_attempts) - 10) * 0.05, 0.5)  # Cap at 0.5

    # Calculate final risk score
    base_risk = sum(risk_factors.values())
    
    # Apply sigmoid function to get a probability-like output between 0 and 1
    risk_score = 1 / (1 + math.exp(-base_risk * 5 + 2.5))
    # return round(risk_score, 4)
    
    
    # Expainer
    factor_contributions = {
        "device_mismatch": risk_factors["device_mismatch"] * 5,
        "browser_mismatch": risk_factors["browser_mismatch"] * 5,
        "os_mismatch": risk_factors["os_mismatch"] * 5,
        "location_anomaly": risk_factors["location_anomaly"] * 5,
        "time_anomaly": risk_factors["time_anomaly"] * 5,
        "keystroke_anomaly": risk_factors["keystroke_anomaly"] * 5,
        "input_behavior_anomaly": risk_factors["input_behavior_anomaly"] * 5,
        "recent_failed_attempts": risk_factors["recent_failed_attempts"] * 5,
        "ddos_protection": risk_factors["ddos_protection"] * 5
    }

    # Calculate final risk score
    base_risk = sum(factor_contributions.values())
    risk_score = 1 / (1 + math.exp(-base_risk + 2.5))
    
    print("\n\n------contrib factors---\n", factor_contributions, "\n--------------\n")
    
    return round(risk_score, 4), factor_contributions
    


def calculate_distance(lat1, lon1, lat2, lon2):
    # Haversine formula to calculate distance between two points on Earth
    R = 6371  # Earth's radius in kilometers

    lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
    dlat = lat2 - lat1
    dlon = lon2 - lon1

    a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
    c = 2 * math.asin(math.sqrt(a))
    distance = R * c

    return distance

@app.route('/update_mfa_result', methods=['POST'])
def update_mfa_result():
    data = request.json
    username = data.get('username')
    record_id = data.get('record_id')
    mfa_success = data.get('mfa_success')

    if username not in USER_DATA:
        return jsonify({"error": "User not found"}), 404

    if record_id >= len(USER_DATA[username]["records"]):
        return jsonify({"error": "Record not found"}), 404

    # Update the login_success field based on MFA result
    USER_DATA[username]["records"][record_id]["login_success"] = mfa_success

    return jsonify({"message": "MFA result updated successfully"}), 200
    
@app.route('/user/<username>')
def user_data(username):
    if username in USER_DATA:
        user_info = USER_DATA[username]
        return jsonify({
            'username': username,
            'password': hashlib.sha512(user_info['password'].encode()).hexdigest(),
            'records': user_info['records']
        })
    else:
        return jsonify({'error': 'User not found'}), 404

@app.route('/user_data')
def user_data_page():
    return render_template('user_data.html', users=[{'username': username} for username in USER_DATA])
    
@app.route('/explain_risk_score', methods=['POST'])
def explain_risk_score():
    global last_contribs
    data = request.json
    print("-----explainer data------")
    print(data)
    print("-----explainer data------")
    data_cleaned = {
        "timestamp": data.get('datetime'),  # Use the datetime from the payload
        "password_validation_success": data.get('password_validation_success', True),
        "login_success": None,  # This will be updated later if MFA is triggered
        "device_type": data.get('deviceType'),
        "browser": data.get('browser'),
        "os": data.get('os'),
        "vpn": data.get('vpn'),
        "keystroke_timing": data.get('keyStrokeTiming'),
        "shift_count": data.get('shiftCount', 0),
        "caps_lock_count": data.get('capsLockCount', 0),
        "tab_count": data.get('tabCount', 0),
        "backspace_count": data.get('backspaceCount', 0),
        "delete_count": data.get('deleteCount', 0),
        "enter_count": data.get('enterCount', 0),
        "mouse_click_count": data.get('mouseClickCount', 0),
        "latitude": data.get('latitude', 19),  # Not provided in the current payload
        "longitude": data.get('longitude', 73)  # Not provided in the current payload
    }
    
    username = data.get('username')
    new_record = data_cleaned  # Assuming the entire login attempt data is sent
    print("\n\nusername", username, new_record)

    risk_score, factor_contributions = calculate_risk_score(username, new_record)
    risk_score, factor_contributions = risk_score_glob, last_contribs
    

    # Sort factors by contribution
    sorted_factors = sorted(factor_contributions.items(), key=lambda x: abs(x[1]), reverse=True)

    # Create SHAP-like summary plot
    plt.figure(figsize=(10, 6))
    colors = ['red' if x[1] > 0 else 'blue' for x in sorted_factors]
    y_pos = range(len(sorted_factors))
    plt.barh(y_pos, [x[1] for x in sorted_factors], color=colors)
    plt.yticks(y_pos, [x[0].replace('_', ' ').title() for x in sorted_factors])
    plt.xlabel('Impact on Risk Score')
    plt.title('Factor Contributions to Risk Score')
    
    # Save plot to a base64 string
    buffer = io.BytesIO()
    plt.savefig(buffer, format='png')
    plt.close()
    buffer.seek(0)
    plot_base64 = base64.b64encode(buffer.getvalue()).decode()
    
    # Generate textual explanation
    top_factors = sorted_factors[:3]
    explanation = f"The login attempt received a risk score of {risk_score:.2f}. "
    explanation += "The top contributing factors are:\n"
    for factor, contribution in top_factors:
        explanation += f"- {factor.replace('_', ' ').title()}: {'Increased' if contribution > 0 else 'Decreased'} risk by {abs(contribution):.2f}\n"
    
    print('Explanation', explanation)
    
    return jsonify({
        'risk_score': risk_score,
        'plot': plot_base64,
        'explanation': explanation,
        'factor_contributions': dict(sorted_factors)
    })
```

Can this be made better? I want it to resemble something generated by an advanced  machine learning algorithm and less rule based
I want the risk scoring to be comprehensive and apt for a live demo