This is a risk-based authentication system designed for use with Shibboleth IdP. Behavioral data is collected and trained with a neural network. The neural network adapts scoring to each user's behavior and learns "how risky is this user's login compared to their previous logins". This is an important distinction from a neural network that learns how risky a user's login is compared to an average login, which is not the project's goal. The behavioral data is ensembled with other heuristics such as how risky an IP is and whether impossible travel occurred.
- Demo
- Requirements
- Installation
- Dataset Problem and Definition
- Data Collection and Training
- Deploying
- Reproducibility
- Considerations
- License
A demo with a test Shibboleth instance is available at: https://sp.sampacker.com. You'll use the credentials:
- Username:
demo - Password:
test123!
The API URL is available at https://shib-predict.sampacker.com. You can POST /score or GET /models.
to login. You can then see the system scoring you in real time with the ability to continue to the service provider.
- Shibboleth IdP with the RBA plugin
- PostgresSQL 17
- Python 3.11+
uv- Load balancer (recommended)
- Server with CUDA or MPS support (recommended)
This project uses uv to manage packages. First, create a uv virtual environment and install the necessary dependencies with the following commands:
uv venv
uv syncThen, activate the virtual environment using the standard commands:
source .venv/bin/activate # macOS / Linux
.\.venv\Scripts\activate # WindowsCopy the .env.example file and fill in the values. This involves putting in a MaxMind License Key in. You can sign
up here and once you have an account, go to "Manage License Keys". Then, create a new
key and put it in the environment file as follows:
MAXMIND_LICENSE_KEY=my_license_keyYou'll also want to add a PostgresSQL connection string to the .env file. An example is below:
POSTGRES_CONNECTION_STRING=postgresql://your_username_here:your_password_here@127.0.0.1:5432/your_database_hereThis project uses Postgres and Citus. You can follow this tutorial here to set up Postgres on Debian. To add the Citus extension to Postgres, please refer to the Citus installation guide here.
To ensure strong data consistency, the data collected from Shibboleth is versioned. The latest version of the metrics is
v4. When setting up your database, you should seed it with v4. You can seed your database manually or with the
uv run seed command. If you choose to do it manually, please refer to the seeds folder for the schema.
Sharding the database with Citus is incredibly easy and can be done as follows:
CREATE EXTENSION IF NOT EXISTS citus;
SELECT create_distributed_table('rba_device', 'device_uuid');
SELECT create_distributed_table('rba_login_event', 'username');You can then add worker nodes and set the shard count depending on how big your environment is.
You will want to set up MLFlow for this project. This uses the UC catalog, so you'll need to set up the schema. In MLFlow, go to New > Query and run this:
CREATE
CATALOG shibboleth_rba;
GRANT ALL PRIVILEGES ON CATALOG shibboleth_rba TO `your-user@domain.com`;
CREATE SCHEMA shibboleth_rba.models;For the environment setup, create a personal access token and change the host. You can do this in Account Settings >
Developer. The host depends on your MLFlow setup. Under the assumption you are using Databricks, it is the same as the
URL you use to access your MLFlow dashboard. Ensure you have these set in your .env file:
DATABRICKS_HOST=example.cloud.databricks.com
DATABRICKS_TOKEN=<your-personal-access-token>For more details on setting things up on the Shibboleth side, please view the RBA plugin.
- Legitimate logins: Real Shibboleth IdP authentication events
- Malicious logins: Synthetic attack generation (see
db/create_synthetic_data.py)
27 behavioral metrics including:
- Typing patterns (key_count, avg_key_delay_ms)
- Mouse behavior (pointer_distance_px, pointer_event_count)
- Session timing (total_session_time_ms, idle_time_total_ms, active_time_ms)
- Device fingerprinting (screen dimensions, platform, hardware_concurrency)
- Contextual data (GeoIP, timezone, IP reputation)
Target Variable: is_true_threat (derived from nn_score=1.0 AND human_verified=True OR impossible_travel=True)
F1 Score (balancing false positives vs false negatives for security/usability tradeoff)
Below are the evaluation metrics based on 1,100 logins from Shibboleth IdP:
Best threshold (determined on validation set): 0.045
Test evaluation results:
F1 Score: 0.9355
Precision: 0.9355
Recall: 0.9355
- Minimum: 1,000 logins
- Recommended: 5,000+ logins
- Per-user minimum: 10 logins for personalized embeddings
You will need malicious logins for the model to catch. A script that generates synthetic data is provided for you:
# Generate synthetic malicious logins
uv run db/create_synthetic_data.py --count 500 --write
# Generate attack campaigns
uv run db/create_synthetic_data.py --campaigns 10 --write
# Generate impossible travel scenarios
uv run db/create_synthetic_data.py --impossible-travel 5 --writeThere are multiple steps to productionalizing this. The first is collecting data. You should ensure that you have passthrough mode turned on. This collects data silently and allows logins through Shibboleth. After you have data (~ 1,000 logins minimum), you must calculate the scores for known good logins. We've developed a heuristic helper to do so. However, it still requires manual intervention and tuning depending on your results.
After calculating known good logins, you must figure out a way to get malicious logins. You have two options: flagging known malicious logins or using the synthetic generator to create malicious logins. After this, you are ready to train your neural network!
You can train the neural network using the following command:
uv run trainThis will train the model and log the results, including the optimal threshold score for denying logins. The threshold is calculated during training to balance security with usability based on your specific dataset. How the threshold is stored and used depends on your deployment mode (see below).
The DEFAULT neural network design is below. However, you can customize the input features and the number of hidden layers. Two layers is acceptable for a small dataset size. As time progresses, you may want to increase the number of layers. However, you probably do not need a neural network as big as you think. Staying conservative is ideal, otherwise you can run the risk of overfitting.
- Input: 27 behavioral features + user embedding (4-64 dims)
- Hidden layers: [32, 16] with ReLU activation and Dropout(0.2)
- Output: Sigmoid → Platt Calibration → Ensemble with IP risk
- Loss: Binary Cross-Entropy
- Optimizer: Adam (lr=0.005)
The key feature is user-specific embeddings which personalize the risk assessment (learns "is THIS risky for THIS user?")
You can generate a graph showing the feature importance and a confusion matrix using the command below:
uv run explainThis system supports two deployment modes for threshold management, each with different trade-offs and use cases.
In MLFlow mode, MLFlow serves as the single source of truth for both model versions and their corresponding thresholds.
When you train a model, the threshold is logged as a run parameter in MLFlow alongside the model registration. The API
maintains an in-memory cache that periodically queries MLFlow for the currently staged or production model versions and
their thresholds, exposing this information via the /models endpoint.
Shibboleth operates in "dynamic" mode, pulling thresholds directly from the API. When a login is scored, Shibboleth receives both the risk score and the model version used to generate it, then queries the API for the appropriate threshold for that specific model version. Because both the score and threshold are resolved at request time and versioned consistently, there is no race condition between model updates and threshold propagation—even across load balanced servers.
Key advantages:
- No Shibboleth configuration updates or restarts needed after training
- Safe for multi-node and load balanced deployments
- Model and threshold are always synchronized
- Train models anywhere; all API instances automatically stay in sync
Setup requirements:
- MLFlow must be enabled in
config.yml - Shibboleth
rba-beans.xmlmust be configured with the API endpoint URL (not a static threshold value)
In this mode, uv run shib-update is not used. Shibboleth's configuration remains static, and all threshold logic is
handled dynamically through API calls.
In Local mode, MLFlow is not used. This is intended for development or simple single-node setups where model
reproducibility and multi-server consistency are not concerns. After training, the computed threshold is written to a
local text file (nn_data/threshold.txt).
If you want to update Shibboleth with this threshold, you can optionally run:
uv run shib-updateThis reads the threshold from the local file and updates your Shibboleth XML configuration. You must then manually restart Shibboleth for the changes to take effect. Shibboleth uses this single, static threshold for all decisions until the next manual update.
Key limitations:
- Only appropriate for single scoring endpoint deployments
- Requires training on the same server running the API
- Requires manual Shibboleth restarts after threshold updates
- No protection against model/threshold version mismatches
- Not recommended for production use
Setup requirements:
- MLFlow must be disabled in
config.ymlfor the backend - The
rba-beans.xmlin Shibboleth must be configured with a static threshold value (not an API endpoint URL). See the RBA plugin for more instructions.
You can turn passthrough mode off after training and ensuring your API is properly set up. Your model will now start
truly scoring logins. At this point, the next step is to set up a scheduler to retrain your neural network frequently.
Regular retraining is essential as user behavior evolves over time and new attack patterns emerge. In MLFlow mode,
threshold updates happen automatically with no additional steps required. In Local mode, you must manually run
uv run shib-update and restart Shibboleth after each training run.
There are multiple approaches to deploying. You can run the API on the same server as your Shibboleth server or run it on separate servers. Under the assumption you have already done your data collection and model training, serving the API endpoint is quite simple. You will need to run the API within Gunicorn (macOS / Linux) or Waitress (Windows). This is automatically handled for you and you can use one simple command to detect your setup and automatically spin up the server:
uv run api-prodClone the GitHub repository, set the environment variables, update the config.yml, and run the API. This works on any cloud provider (AWS EC2, GCP Compute Engine, Azure VM, etc.) or local server. If you are not using MLFlow, you will need to train the model locally prior to running the API. You do not need to retrain the model on the server if you are using MLFlow.
A sample systemd script is provided below for your convenience. You can put this in
/etc/systemd/system/shib-predict.service:
[Unit]
Description = Shibboleth IdP RBA Prediction
After = network.target
[Service]
User = [your-user]
WorkingDirectory = /path/to/shib-predict
ExecStart = /path/to/uv run api-prod
Restart = always
RestartSec = 5
Environment = "PYTHONUNBUFFERED=1"
[Install]
WantedBy = multi-user.targetThis project is designed for full reproducibility at multiple levels:
- All hyperparameters, model architecture, and training settings are controlled via
config.yml - Random seeds are set globally (
RANDOM_STATE=41) for deterministic train/test splits - Preprocessing pipelines (scaling, imputation) are serialized and versioned alongside models
- Dependencies locked via
uv.lockfor exact version reproducibility - All external data sources (GeoIP, StopForumSpam) are automatically downloaded and versioned
- MLFlow logs all training runs with parameters, metrics, and artifacts
- Model versioning enables rollback to any previous trained model
- Every training run is tagged with dataset size, feature set, and threshold
- Trained models are stored in Unity Catalog with full lineage tracking
- API automatically loads the latest registered model version
- Load-balanced servers stay synchronized by pulling from the same MLFlow registry
- No manual model file transfers needed - deploy anywhere with MLFlow credentials
- API servers can be restarted, scaled, or replaced without losing model state
- All model artifacts (scaler, preprocessor, calibrator) are packaged together
This system learns personalized risk profiles for each user. The training data consists of real authentication events that cannot be publicly shared due to privacy constraints. However, the system is fully reproducible for any organization with Shibboleth IdP:
- Data Collection: Enable passthrough mode to collect your own login events
- Heuristic: Use
db/bootstrap_scores.pyto accurately bootstrap initial risk scores. - Synthetic Generation: Use
db/create_synthetic_data.pyto generate realistic attack patterns - Training: Run
uv run trainto build a personalized model for your user base
The methodology is reproducible, even though the exact dataset is institution-specific. Any Shibboleth deployment can replicate this pipeline with their own data.
It is highly recommended to load balance this application with software such as Caddy. Then, configure the Shibboleth plugin to point to your load balanced endpoint. If using MLFlow mode, load balancing is fully supported with no additional configuration needed, as all servers will automatically stay synchronized through MLFlow.
There is a PostgresSQL database requirement. It is very easy to shard the database with Citus and what I recommend. This will perform extremely well. You can scale it as much as you need depending on how active your Shibboleth IdP instance is. I recommend purging logins after a year (except malicious or human verified ones). Since this isn't time series data, there isn't a problem with having a cluster of older malicious logins while accumulating newer login data.
Each time you start the web application, it will check for updates from MaxMind and StopForumSpam for new files. If any are found, they will automatically be downloaded. It will also try and connect to the database and run a simple query. This is called a "preflight check" and confirms your environment is set up correctly.
This software is licensed under the PolyForm Noncommercial License 1.0.0. You may use, copy, and modify this software for noncommercial purposes only. See LICENSE.md for the full license text.
Copyright © 2025-2026 Sam Packer. Released under the PolyForm Noncommercial License 1.0.0.
