MFA Setup
Multi-factor authentication (MFA) in tiCrypt strengthens security by requiring one or more additional authentication factors beyond the user's private key. This page covers how to deploy and integrate an MFA authority. For the ticrypt-auth.conf MFA configuration parameters, see the MFA section in the Auth Service reference.
MFA is deployed separately from the main backend and requires its own web server and SSO integration.
How MFA Works in tiCrypt
Login Flow
- During login, the tiCrypt backend informs the frontend that MFA factors must be satisfied and provides URLs for each factor.
- The frontend opens each factor URL in a separate frame/tab.
- The MFA server authenticates the user (via SSO, push notification, etc.) and returns a signed certificate to the frontend via
window.postMessage(). - The frontend submits all MFA certificates to the backend to complete the login.
Validation
The backend validates MFA factors using only the signed message received from the MFA server. There is no direct communication between the MFA servers and the tiCrypt backend. The frontend mediates all communication.
For each MFA factor, the backend needs:
- The public key of the MFA server (configured in
ticrypt-auth.conf) - The URL where users authenticate
Setting Up MFA with Shibboleth/SSO and PHP
The simplest approach is to host a PHP script on a web page protected by your organization's Shibboleth/SSO. The script:
- Reads the user's email and other properties from Shibboleth environment variables
- Reads the user's IP address
- Generates a timestamp (to prevent replay attacks)
- Signs the payload with the MFA private key
- Returns the signed message to the tiCrypt frontend
Hosting info.php
- Choose a host within your organization's infrastructure to serve the PHP script. This host should not be the same server as the tiCrypt backend (hosting it on the same machine weakens security).
- Ensure the host uses HTTPS.
- Place the
info.phpscript at a route on that host. Best practice is to use a dedicated subdomain. - Protect the route (or virtual host) with your organization's Shibboleth/SSO.
The web server used for the MFA page does not matter. Apache with mod-php is the easiest combination with Shibboleth.
Setting Up Credentials
Generate an RSA key pair for the MFA signing:
openssl genrsa -out /etc/pki/tls/certs/login_rsa_2048_priv.pem 2048
openssl rsa -in /etc/pki/tls/certs/login_rsa_2048_priv.pem -pubout -out /etc/pki/tls/certs/login_rsa_2048_pub.pem
chown nginx:nginx /etc/pki/tls/certs/login_rsa_2048_p*
Adjust the chown command for your web server user (e.g., apache:apache for Apache httpd).
Place the keys as follows:
- Private key on the server hosting
info.php(protected like any other private key, readable by PHP) - Public key path and the
info.phpURL inticrypt-auth.confunder the MFA factors section
After updating the config, restart ticrypt-auth:
systemctl restart ticrypt-auth
Once MFA is active, only users who can satisfy both the MFA factor and the primary factor with the exact same email will be able to log in. Any discrepancy (e.g., case differences) will prevent session creation.
Customizing info.php
The info.php script can be customized to fit your organization's requirements or branding. The following parts are critical and must be preserved:
- The
$dataarray must contain at least the required fields (email,sourceIP,timestamp) - The digital signature mechanism must remain intact
- The
$msgvalue assembly must not be modified - The
<script>section that sends the message viapostMessagemust remain unchanged
Keep the error messages about missing keys and failed signatures. Debugging the script without them is difficult.
Optional fields for the $data array (values from SSO that auto-populate user registration):
| Field | Description |
|---|---|
firstName | User's first name |
lastName | User's last name |
contactEmail | Contact email (if different from login email) |
department | User's department |
position | User's position/title |
Any field populated by info.php cannot be edited by the user during registration.
Extract firstName and lastName from SSO when possible. Users sometimes write their names differently, which creates confusion.
MFA Payload Schema
The window.postMessage() payload sent to the frontend must include:
| Field | Type | Required | Description |
|---|---|---|---|
payload | String | ✅ | JSON-encoded signed payload object |
authorizer | String | ✅ | Factor ID matching a factor in ticrypt-auth.conf |
algo | String | ✅ | Signature algorithm. See Signature Algorithms. |
signature | String | ✅ | Base64-encoded signature of the payload |
The payload JSON object must include:
| Field | Type | Required | Description |
|---|---|---|---|
email | String | ✅ | tiCrypt username (normally the user's email) |
sourceIP | String | ✅ | User's IP address as seen by the MFA server |
timestamp | String or Int | ✅ | ISO-8601 datetime string or millisecond UNIX timestamp |
Server-Side Validation
When the backend receives the MFA response, it:
- Looks up the factor configuration using the
authorizerfield. Rejects if no matching factor exists. - Decodes the
signaturefrom Base64. Rejects on decode failure. - Validates the signature on
payloadusing the configured public key and specified algorithm. Rejects on mismatch. - Decodes
payloadas JSON and validates required fields. - Checks that
timestampis not in the future and not older than the factor'scert-ttl. - Looks up the user by
email. - Checks for duplicate submission (replay). Rejects if previously seen.
- Generates an MFA token valid for the factor's
token-ttl.
Duo Integration
The simplest way to integrate Duo MFA is to add it as an extra factor in the Shibboleth login flow. This way, tiCrypt is not aware of Duo and no additional configuration is needed. Alternatively, a separately hosted info.php protected by Duo can be set up as its own factor.
Adding Duo to Shibboleth creates a 3-factor authentication method (private key + SSO + Duo), making compliance requirements easier to satisfy.
Multiple SSO Servers
If your organization uses different SSO servers for different user populations, you can set up a single MFA factor that supports multiple SSO providers:
- Host a
login.htmlpage (not protected by Shibboleth) that presents a choice of credential types. - For each Shibboleth server, host an independent
info.phpscript. All scripts must use the same private key. login.htmlredirects to the appropriateinfo.phpbased on user selection.- The selected
info.phpcompletes authentication and returns the MFA token to the tiCrypt frontend.
Example info.php
<?php
// Second-factor authentication script for Shibboleth-based servers
$request_method = $_SERVER['REQUEST_METHOD'];
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
if ($request_method === 'OPTIONS') {
header('HTTP/1.1 204 No Content');
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Length: 0');
exit;
} else if ($request_method !== 'GET') {
header('HTTP/1.1 405 Method Not Allowed');
exit;
}
// Location of the private key used to sign the replies
$privKeyFile = "file:///etc/pki/gv-mfa/private/shibboleth_private_key.pem";
$timezone = new DateTimeZone('UTC');
$timestamp = array_key_exists('REQUEST_TIME', $_SERVER) ? $_SERVER['REQUEST_TIME'] : time();
$date = new DateTime('@' . $timestamp, $timezone);
$dateStr = $date->format(DateTime::ATOM);
$data = array(
"email" => array_key_exists("REMOTE_USER", $_SERVER) ? $_SERVER["REMOTE_USER"] : $_SERVER["eppn"],
"sourceIP" => $_SERVER["REMOTE_ADDR"],
"timestamp" => $dateStr
);
$fPort = $_GET["port"];
$pkey = openssl_pkey_get_private($privKeyFile);
if (!$pkey) {
echo "<!-- Private key file $privKeyFile not found -->";
?>
<!DOCTYPE html>
<html>
<body>
<span style="color: red; font-size: 40px; text-align: center; position: absolute; width: 95%; margin: 0 auto; top: 50%;">
Private key for digital signature could not be found
</span>
</body>
</html>
<?php
exit();
}
$dataStr = json_encode($data);
if (!openssl_sign($dataStr, $signature, $pkey, "SHA256")) {
echo "<!-- Digital signature failed -->";
?>
<!DOCTYPE html>
<html>
<body>
<span style="color: red; font-size: 40px; text-align: center; position: absolute; width: 95%; margin: 0 auto; top: 50%;">
Digital signature generation failed
</span>
</body>
</html>
<?php
exit();
}
$sig = base64_encode($signature);
$msg = json_encode(array(
'payload' => $dataStr,
'authorizer' => $_SERVER["AUTH_TYPE"],
'algo' => 'RSA-PKCS1-SHA256',
'signature' => $sig
));
?>
<!DOCTYPE html>
<html>
<body>
<span style="color: blue; font-size: 40px; text-align: center; position: absolute; width: 95%; margin: 0 auto; top: 50%;">
Successful Authentication
</span>
</body>
<script type="text/javascript">
var msg = <?php echo $msg; ?>;
if (window.self != window.top) {
if (parent && parent.postMessage) {
try {
parent.postMessage(msg, '<?php echo $formOrigin; ?>')
} catch(e) {
// ignore
}
}
} else {
const urlParams = new URLSearchParams(window.location.search);
const port = "<?=$fPort?>";
const uri = encodeURI("http://127.0.0.1:" + port + "/mfa.html?msg=" + JSON.stringify(msg));
window.location.assign(uri);
}
</script>
</html>
Example: Shibboleth + NGINX Deployment
This section walks through a complete Shibboleth SP deployment with NGINX on Rocky Linux 8.
Build Shibboleth SP with FastCGI
On a machine with Docker:
git clone https://github.com/nginx-shib/shibboleth-fastcgi.git
cd shibboleth-fastcgi
make
Copy the resulting RPMs from build/ to the target host.
Install Shibboleth
Add the Shibboleth repository:
# /etc/yum.repos.d/shibboleth.repo
[shibboleth]
name=Shibboleth (Rocky_8)
type=rpm-md
mirrorlist=https://shibboleth.net/cgi-bin/mirrorlist.cgi/Rocky_8
gpgcheck=1
gpgkey=https://shibboleth.net/downloads/service-provider/RPMS/repomd.xml.key
https://shibboleth.net/downloads/service-provider/RPMS/cantor.repomd.xml.key
enabled=1
exclude=shibboleth
Install and start:
dnf install shibboleth-3.3.0-1.x86_64.rpm
systemctl enable --now shibd
systemctl status shibd
Set Up FastCGI Authorizer and Responder
dnf install supervisor
Create /etc/supervisord.d/shib.ini:
[fcgi-program:shibauthorizer]
command=/usr/lib64/shibboleth/shibauthorizer
socket=unix:///run/supervisor/shibauthorizer.sock
socket_owner=shibd:shibd
socket_mode=0666
user=shibd
stdout_logfile=/var/log/supervisor/shibauthorizer.log
stderr_logfile=/var/log/supervisor/shibauthorizer.error.log
[fcgi-program:shibresponder]
command=/usr/lib64/shibboleth/shibresponder
socket=unix:///run/supervisor/shibresponder.sock
socket_owner=shibd:shibd
socket_mode=0666
user=shibd
stdout_logfile=/var/log/supervisor/shibresponder.log
stderr_logfile=/var/log/supervisor/shibresponder.error.log
systemctl enable --now supervisord
systemctl status supervisord
Set Up PHP
dnf module list php
dnf module reset php
dnf -y module install php:8.0
dnf -y install php php-fpm
systemctl enable --now php-fpm
Edit /etc/php-fpm.d/www.conf to set the user/group:
user = nginx
group = nginx
systemctl reload php-fpm
Place your info.php script in the Shibboleth-protected directory (e.g., /var/www/ticrypt/dart/secure/).
Build NGINX Shibboleth Module
Build on a separate host, then copy the module to the target:
nginx -V # Note the version (e.g., 1.14.1)
# Find the matching tag at https://hg.nginx.org/pkg-oss/tags
wget https://hg.nginx.org/pkg-oss/raw-file/1.14.1-2/build_module.sh
chmod a+x build_module.sh
mkdir /root/rpmbuild
./build_module.sh -v 1.14.1 https://github.com/nginx-shib/nginx-http-shibboleth.git
When the build pauses, edit the Makefile to match your nginx -V configure arguments:
vi /tmp/build_module.sh.*/pkg-oss/rpm/SPECS/Makefile
# Remove flags not in your nginx -V output, such as:
# --with-compat \
# --with-threads \
Continue the build, then copy the module:
cp /root/rpmbuild/BUILD/nginx-module-shibboleth-*/objs/ngx_http_shibboleth_module.so /usr/lib64/nginx/modules/
Repeat for the headers-more module:
./build_module.sh -v 1.14.1 https://github.com/openresty/headers-more-nginx-module.git
# Same Makefile edit as above
cp /root/rpmbuild/BUILD/nginx-module-headersmore-*/objs/ngx_http_headers_more_filter_module.so /usr/lib64/nginx/modules/
Configure NGINX
Load the modules:
echo 'load_module /usr/lib64/nginx/modules/ngx_http_shibboleth_module.so;' > /usr/share/nginx/modules/shibboleth.conf
echo 'load_module /usr/lib64/nginx/modules/ngx_http_headers_more_filter_module.so;' >> /usr/share/nginx/modules/shibboleth.conf
Add Shibboleth locations to /etc/nginx/default.d/shibboleth.conf:
# FastCGI authorizer for Auth Request module
location = /shibauthorizer {
internal;
include fastcgi_params;
fastcgi_pass unix:/run/supervisor/shibauthorizer.sock;
}
# FastCGI responder
location /Shibboleth.sso {
include fastcgi_params;
fastcgi_pass unix:/run/supervisor/shibresponder.sock;
}
# Shibboleth error page resources
location /shibboleth-sp {
alias /usr/share/shibboleth/;
}
Add the protected location to your site config (e.g., /etc/nginx/conf.d/ticrypt.conf):
server {
server_name your-mfa-host.example.edu;
# ...
location /dart/secure {
root /var/www/ticrypt;
shib_request /shibauthorizer;
shib_request_use_headers on;
more_clear_input_headers 'remote_user' 'email' 'staticemail'
'firstname' 'lastname' 'department' 'position';
}
}
Configure Shibboleth SP
Edit /etc/shibboleth/shibboleth2.xml:
<SPConfig ...>
...
<RequestMapper type="XML">
<RequestMap>
<Host name="your-mfa-host.example.edu"
authType="shibboleth"
requireSession="true"
redirectToSSL="443" />
</RequestMap>
</RequestMapper>
<ApplicationDefaults entityID="https://your-mfa-host.example.edu"
REMOTE_USER="emailAddress"
homeURL="https://your-mfa-host.example.edu/dart/secure/"
cipherSuites="DEFAULT:!EXP:!LOW:!aNULL:!eNULL:!DES:!IDEA:!SEED:!RC4:!3DES:!kRSA:!SSLv2:!SSLv3:!TLSv1:!TLSv1.1">
<Sessions ...>
<SSO entityID="urn:mace:incommon:your-organization">
SAML2
</SSO>
...
<Handler type="Session" Location="/Session" showAttributeValues="true"/>
...
</Sessions>
...
<MetadataProvider type="XML" validate="true" path="idp-metadata.xml" />
...
</ApplicationDefaults>
</SPConfig>
Add attribute mappings in /etc/shibboleth/attribute-map.xml:
<Attributes ...>
...
<Attribute name="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" id="emailAddress"/>
<Attribute name="Department" id="Department"
nameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"/>
<Attribute name="Position" id="Position"
nameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"/>
<Attribute name="Email" id="Email"
nameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"/>
<Attribute name="FirstName" id="FirstName"
nameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"/>
<Attribute name="LastName" id="LastName"
nameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"/>
<Attribute name="StaticEmail" id="StaticEmail"
nameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"/>
</Attributes>
Create IdP metadata files (e.g., /etc/shibboleth/idp-metadata.xml) with your identity provider's metadata.
Register the SP with Your IdP
Provide your IdP with the SP metadata:
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
entityID="https://your-mfa-host.example.edu">
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:AssertionConsumerService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://your-mfa-host.example.edu/Shibboleth.sso/SAML2/POST"
index="1"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>
Verify
shibd -t
systemctl restart shibd
systemctl restart supervisord
systemctl restart nginx