Skip to main content

MFA Setup

The multi-factor authentication (MFA) mechanism in tiCrypt strengthens the security of tiCrypt using one or more MFAs. This document describes how to set up an MFA authority; configuring tiCrypt to use it can be found in ticrypt-auth.conf in the mfa section.

The MFA mechanism supported in tiCrypt requires setting up a webpage protected by an external factor (e.g. Shibboleth or other SSO). Alternatively, an actual server that provides the same interface can be substituted.

note

Since Shibboleth is the most commonly used SSO, we use it for our example. tiCrypt is not dependent on Shibboleth, any SSO can be used.

MFA Mechanism in tiCrypt

The specific requirements for a tiCrypt MFA mechanism can be found here. We give a high-level overview of what is happening to help understand the setup.

Steps to acquire MFA factors

  1. tiCrypt Frontend, as part of the login procedure, is informed by the tiCrypt Backend that MFA factors must be satisfied. URL links to all the factors are provided.
  2. tiCrypt Frontend opens a separate tab and navigates to the MFA pages.
  3. The MFA server (hosting the MFA page) uses any authentication it sees fit and provides a signed certificate back to the tiCrypt Frontend, proving the successful login.
  4. tiCrypt Frontend sends all the MFA certificates to the tiCrypt Backend to complete the login.

Validation of MFA factors

tiCrypt Backend validates the MFA factors using exclusively the message and the digital signature received from the MFA server. As explained here, for each MFA factor, a public key needs to be provided and a URL on how it can be accessed.

info

There is no direct communication between the MFA servers/hosts and tiCrypt Backend. The only connection is the public key of the MFA server. tiCrypt Frontend mediates communication.

Setting up MFA using Shibboleth/SSO and PHP

The simplest way to set up MFA in tiCrypt is to host an info.php script on a web page protected by the Shibboleth/SSO mechanism. The script, an example of which is provided here, performs the following operations:

  1. It grabs the user's email and other properties if needed, as presented by Shibboleth (environment variable).
  2. It grabs the user IP (environment variable)
  3. It generates a timestamp (to prevent replay)
  4. It generates a digitally signed message (signed with the private key) containing all the info above.
  5. It hands the message to the tiCrypt front.

Hosting info.php

  1. Find a host within your organization's infrastructure to serve a PHP script. Not only does this not need to be on the same server as tiCrypt Backend, but it is strongly preferable that it is not. Hosting it on the same machine weakens security.
  2. Make sure the host uses https.
  3. On any route within that host, place the info.php script. The best practice is to have a sub-domain dedicated to this and to call the script info.php
  4. Protect the route (or virtual domain) with the organization Shibboleth/SSO.
info

The web server used for the MFA page does not matter. It is a little easier to combine Shibboleth with Apache and to use the mod-php module to host the info.php script.

Setting up credentials

For the script to work, a public-private key has to be generated.

  • The private key is placed on the server hosting info.php (and should be protected like any other private key). The key must be accessible by PHP.
  • The public key and the URL to the info.php page are placed in the ticrypt-auth.conf file (the public key is somewhere on the disk and the path to it is in the config).
  • The ticrypt-auth component gets restarted (systemctrl restart ticrypt-auth). At this point, all sessions need to satisfy the 2FA as well as the primary factor to be complete.
danger

At this point, only users that can satisfy the 2FA and the primary factor with EXACTLY the same email will be able to log in. The smallest discrepancy will result in the session not being granted.

Generating the public-private key pair can be done using openssl:

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*
tip

The above code snippet assumes you use NGINX and sets permissions for nginx user and group. Modify for your web server.

Customizing info.php

info.php script can be customized to fit any organization's requirement or to achieve any specific look. The critical parts of the script that will prevent it from running correctly are:

  • $data array must contain at least the provided fields.
  • The digital signature mechanism needs to be preserved.
  • Recommended: Keep the error messages about missing keys and failed digital signatures. Debugging the script without the messages is difficult.
  • $msg value assembly
  • ` section with no changes. The mechanism is very specific.

Other supported fields for $data array are:

  • firstName
  • lastName
  • contactEmail
  • department
  • position
danger

Any field filled in by the info.php script, cannot be edited during registration.

tip

Use fields like firstName and lastName extracted from SSO since users sometimes write their names differently; this can create confusion.

Integration with Duo

The simplest way to integrate with Duo MFA, is to require it as an extra factor in the Shibboleth login. This way, tiCrypt is not aware of the extra factor and no extra work is needed. Alternatively, a separately hosted info.php protected by DUO can be set up.

tip

By adding Duo to Shibboleth, a 3-factor authentication method is employed, making compliance even easier.

Multiple Shibboleth/SSO servers

If desired, multiple Shibboleth servers can be used to provide a single MFA factor for tiCrypt. This is needed, for example, if your organization uses different SSO servers for different parts.

To accomplish this, you can:

  1. Host a page login.html on some server. This page should not be protected by Shibboleth and should provide a choice of which types of credentials the user will make use of.
  2. For each Shibboleth server, an independent info.php script is hosted using the advice in the previous section. All the scripts must have access to and use the same private key.
  3. login.html redirects to one of the info.php pages, depending on user selection.
  4. The info.php page completes the mechanism and hands over the MFA token to tiCrypt Frontend.

Example info.php

<?php
// This script implements the second-factor authentication for Shibboleth-based servers
// Can't do proper origin checking, as Shibboleth does not pass through the
// original origin or referer when the login page is injected.
$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";
// Expecting REQUEST_TIME to be specified as seconds since UNIX epoch, so
// the time zone should be UTC
$timezone = new DateTimeZone('UTC');
$timestamp = array_key_exists('REQUEST_TIME', $_SERVER) ? $_SERVER['REQUEST_TIME'] : time();
// Need to prepend an '@' to the timestamp to mark it as a UNIX timestamp
$date = new DateTime('@' . $timestamp, $timezone);
// Format it as ISO-8601. Note that the DateTime::ISO8601 format isn't
// actually ISO-8601 compliant, so we use ATOM instead.
$dateStr = $date->format(DateTime::ATOM);
// Collect the info we need to return
$data = array(
"email" => array_key_exists("REMOTE_USER", $_SERVER) ? $_SERVER["REMOTE_USER"] : $_SERVER["eppn"],
"sourceIP" => $_SERVER["REMOTE_ADDR"],
"timestamp" => $dateStr
);
// We might have a port argument, get it
$fPort = $_GET["port"];
/* Load the private key from the key file */
$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);
// Test if the signature worked
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%;">Private key for digital signature could not be found</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%;">Succesful 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 {
// We are in a tab, redirect to the local page
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 setup

  • Build shibboleth-sp with fastcgi support on a computer with docker
    git clone https://github.com/nginx-shib/shibboleth-fastcgi.git
    cd shibboleth-fastcgi
    make # May need to update the docker-compose.yml to work with rocky8
  • Copy RPMs in build/ to host where shibboleth-sp should run
  • Goto shibboleth latest RPMS and generate
shibboleth.repo

then place in

/etc/yum.repos.d/shibboleth.repo
    shell
vi /etc/yum.repos.d/shibboleth.repo
[shibboleth]
name=Shibboleth (CentOS_8)
# Please report any problems to https://shibboleth.atlassian.net/jira
type=rpm-md
mirrorlist=https://shibboleth.net/cgi-bin/mirrorlist.cgi/CentOS_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 shibboleth

    dnf install shibboleth-3.3.0-1.x86_64.rpm
    systemctl enable --now shibd
    systemctl status shibd
  • Setup shibauthorizer and shibresponder

    dnf install supervisor
    vi /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
  • Setup 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
    vim /etc/php-fpm.d/www.conf
    user = nginx
    group = nginx
    systemctl reload php-fpm
    echo "<?php phpinfo();" > /var/www/ticrypt/dart/secure/phpinfo.php
    systemctl restart nginx
  • Create

/var/www/ticrypt/dart/secure/login.php
  • Build nginx-http-shibboleth (Probably best to build this on a separate host, then copy the file over)

    nginx -V # Get nginx version
    # nginx version: nginx/1.14.1
    # Goto https://hg.nginx.org/pkg-oss/tags and find the latest tag for the version of nginx (1.14.1-2)
    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 # Cause the build to pause so the Makefile can be updated
    ./build_module.sh -v 1.14.1 https://github.com/nginx-shib/nginx-http-shibboleth.git
    # When the installation says: Press Enter to continue or Ctrl+C to exit
    vi /tmp/build_module.sh.*/pkg-oss/rpm/SPECS/Makefile
    # remove the following to match the configure arguments from `nginx -V`
    --with-compat \
    --with-threads \
    # Continue the build
    cp /root/rpmbuild/BUILD/nginx-module-shibboleth-1.14.1/objs/ngx_http_shibboleth_module.so /usr/lib64/nginx/modules/
  • Build headers-more-nginx-module (Probably best to build this on a separate host, then copy the file over)

    ./build_module.sh -v 1.14.1 https://github.com/openresty/headers-more-nginx-module.git
    # When the installation says: Press Enter to continue or Ctrl+C to exit
    vi /tmp/build_module.sh.*/pkg-oss/rpm/SPECS/Makefile
    # remove the following to match the configure arguments from `nginx -V`
    --with-compat \
    --with-threads \
    # Continue the build
    cp /root/rpmbuild/BUILD/nginx-module-headersmore-1.14.1/objs/ngx_http_headers_more_filter_module.so /usr/lib64/nginx/modules/
  • Setup NGINX integration with Shibboleth SP

    • Configure the module load

      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
    • Setup nginx

      vi /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;
      }

      #Resources for the Shibboleth error pages. This can be customised.
      location /shibboleth-sp {
      alias /usr/share/shibboleth/;
      }
      vi /etc/nginx/conf.d/ticrypt.conf
      server {
      server_name granite.dartmouth.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';
      }
  • Configuration SP

    vi /etc/shibboleth/shibboleth2.xml
    <SPConfig ...>
    ...
    <RequestMapper type="XML">
    <RequestMap>
    <Host name="granite.dartmouth.edu" authType="shibboleth" requireSession="true" redirectToSSL="443" />
    </RequestMap>
    </RequestMapper>
    <ApplicationDefaults entityID="https://granite.dartmouth.edu" REMOTE_USER="emailAddress" homeURL="https://granite.dartmouth.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:dartmouth.edu">
    SAML2
    </SSO>
    ...
    <Handler type="Session" Location="/Session" showAttributeValues="true"/>
    ...
    </Sessions>
    ...
    <MetadataProvider type="XML" validate="true" path="dartmouth-idp-metadata.xml" />
    <MetadataProvider type="XML" validate="true" path="hitchcock-idp-metadata.xml" />
    ...
    vi /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"/>
  • Create

/etc/shibboleth/dartmouth-idp-metadata.xml

and

/etc/shibboleth/hitchcock-idp-metadata.xml

with the IdP metadata

  • Test configuration
    shibd -t
    systemctl restart shibd
    systemctl restart supervisord
    systemctl restart nginx
  • Setup Granite in the IdPs
    <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://granite.dartmouth.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://granite.dartmouth.edu/Shibboleth.sso/SAML2/POST" index="1"/>
    </md:SPSSODescriptor>
    </md:EntityDescriptor>