DEV Community

Cover image for SSH Multi-factor Authentication with tru.ID
Greg Holmes for tru.ID

Posted on • Updated on • Originally published at developer.tru.id

SSH Multi-factor Authentication with tru.ID

Secure Shell (SSH) is a network protocol that enables users to securely connect from one computer to another remotely. The majority of people that use SSH are system administrators, and the most common use for this protocol is to manage a server remotely.

It doesn't matter what point of your technical career you are at; if you deal with servers, you will have needed SSH as a protocol to modify or maintain remote servers on countless occasions. You may have increased security on your server by using an SSH certificate instead of a username and password. However, someone with malicious intent could still gain access to these authentication methods. That’s why it’s worth enabling multi-factor authentication (MFA) for your SSH server.

The tru.ID PhoneCheck API confirms the ownership of a mobile phone number by checking for the presence of an active SIM card with the same number. As well as creating a frictionless user experience, this method is significantly more secure than legacy methods such as SMS OTP. tru.ID APIs call the mobile network directly rather than relying on vulnerable SS7 protocols, providing a real-time SIM card check in under 2 seconds.

Before you begin

To follow along with this tutorial, you'll need:

Getting started

A repository branch has been created on GitHub containing the foundation code needed to get you started.

In your Terminal, clone the starter-files branch for this repository with the following command:

git clone -b starter-files git@github.com:tru-ID/tru-id-ssh-auth.git
Enter fullscreen mode Exit fullscreen mode

If you're interested in the finished code, you can find the complete example in the main branch. To get this code, run:

git clone -b main git@github.com:tru-ID/tru-id-ssh-auth.git
Enter fullscreen mode Exit fullscreen mode

A tru.ID Account is required to make the PhoneCheck API requests, so before proceeding, make sure you've created one.

Now navigate to the tru.ID console and create a project. Once created, before you close the window or navigate away, be sure you download the tru.json credentials file and move it to the root directory of your tru-id-ssh-auth project directory.

Within the credentials file, you'll find your project_id, project_name, the scopes available to your project, and your client_id and client_secret, which you'll use to create an auth token to make API requests.

Define core variables

Throughout your code, you'll be reusing several bits of information; these are variables such as the PhoneCheck URL and the destination directory where your project is installed, for example. Below the line VERSION=1 in the file ssh-auth, add the following:

# Base URL information
DATA_RESIDENCY=eu
BASE_URL="https://$DATA_RESIDENCY.api.tru.id"

# API Check URLs
PHONE_CHECK_URL="$BASE_URL/phone_check/v0.2/checks"
GET_PHONE_CHECK_URL="$BASE_URL/phone_check/v0.2/checks/"
CREATE_AUTH_TOKEN_URL="$BASE_URL/oauth2/v1/token"

APP_ROOT=`dirname $0`
CONFIG_FILE="$APP_ROOT/tru-id-ssh-auth.conf"
DEST_DIRECTORY="/usr/local/bin/"

# Empty Global Variables
BASIC_AUTH_USER=
BASIC_AUTH_PASSWORD=
ACCESS_TOKEN=
Enter fullscreen mode Exit fullscreen mode

The only two values available for you to change are:

  • DATA_RESIDENCY, which could be either of the two supported data residencies; eu or in,
  • DEST_DIRECTORY, which is the destination for the project once installed.

Create the install

The majority of this project will be within one single bash file, with multiple commands available. The first is the install command, which copies the relevant code to the desired installation directory. You'll also apply a ForceCommand to the SSHd config file, which you will add later in the tutorial.

Start by locating the function require_curl within the ssh-auth file. Below this require_curl function, add the following code, which is your new install() function:

function install() {
    config_file="${DEST_DIRECTORY}tru-id-ssh-auth/tru-id-ssh-auth.conf"

    set -e
    echo "Making directory in ${DEST_DIRECTORY}"
    mkdir "/usr/local/bin/tru-id-ssh-auth"
    set +e

    if [[ ! -r `dirname "${DEST_DIRECTORY}"` ]]
    then
      echo "${DEST_DIRECTORY} is not writable. Try again using sudo"
      return $FAIL
    fi

    echo "Copying /root/tru-id-ssh-auth/ to ${DEST_DIRECTORY}..."
    cp -r "/root/tru-id-ssh-auth" "${DEST_DIRECTORY}"

    echo "Setting up permissions..."
    chmod 755 $DEST_DIRECTORY

    if [[ ! -f ${config_file} ]]
    then
      echo "Generating initial config on ${config_file}..."
      echo "header=tru.ID SSH Auth initialised." > "${config_file}"
    else
      echo "A config file was found on ${config_file}. Edit it manually if you want to change the API key"
    fi

    chmod 644 ${config_file}

    echo ""
    echo "To enable tru.ID authentication on a user the following command: "
    echo ""

    echo "   ${DEST_DIRECTORY}tru-id-ssh-auth/ssh-auth register-user <username> <phone-number-inc-country-code>"
    echo "   Example: ${DEST_DIRECTORY}tru-id-ssh-auth/ssh-auth register-user test 447000000000"

    echo ""
    echo "To uninstall tru.ID SSH type:"
    echo ""
    echo "   ${DEST_DIRECTORY}tru-id-ssh-auth/ssh-auth uninstall"
    echo ""
    echo "      Restart the SSH server to apply changes"
    echo ""
}
Enter fullscreen mode Exit fullscreen mode

There are several steps included in this new function. These are:

  • Make the directory for the location of the installed application, /usr/local/bin/tru-id-ssh-auth.
  • Check the directory is writeable. If not, then throw an error.
  • Copy the project files to the newly created destination directory.
  • Check if a config file exists. If not, then create one.
  • Output text to Terminal, instructing users how to use and uninstall the application.

As previously mentioned, more functionality is needed to uninstall this script. Below your newly created install() function, add the following uninstall() function:

function uninstall() {
    find_sshd_config

    if [[ $1 != "quiet" ]]
    then
      echo "Uninstalling tru.ID SSH from $SSHD_CONFIG..."
    fi

    if [[ -w $SSHD_CONFIG ]]
    then
        sed -ie '/^ForceCommand.*tru-id-ssh-auth.*/d' $SSHD_CONFIG
    fi

    if [[ $1 != "quiet" ]]
    then
        echo "tru.ID SSH was uninstalled."
        echo "Now restart the ssh server to apply changes and then remove ${DEST_DIRECTORY}/tru-id-ssh-auth"
    fi
}
Enter fullscreen mode Exit fullscreen mode

The first line within this new function is to call a function called find_sshd_config(), so let's add this:

function find_sshd_config() {
    echo "Trying to find sshd_config file"
    if [[ -f /etc/sshd_config ]]
    then
        SSHD_CONFIG="/etc/sshd_config"
    elif [[ -f /etc/ssh/sshd_config ]]
    then
        SSHD_CONFIG="/etc/ssh/sshd_config"
    else
        echo "Cannot find sshd_config in your server. tru.ID SSH Auth will be enabled when you add your specific ForceCommand to the sshd config file."
    fi
}
Enter fullscreen mode Exit fullscreen mode

The new function locates your server's sshd_config file and stores this file location as a global variable.

Both the install() and uninstall() functions are inaccessible until you add them as a parameter. In the list of commands for this project, find the line:

case $1 in
Enter fullscreen mode Exit fullscreen mode

This section of code contains the functionality that understands various arguments in the command when running your bash script. So below the case line, add the following two new command-line arguments:

install)
    check_dependencies
    install
    ;;
uninstall)
    uninstall
    ;;
Enter fullscreen mode Exit fullscreen mode

Registering users

The SIM authentication process requires users to be enabled individually by entering their phone number and linking it with their username to allow them to verify with tru.ID's PhoneCheck API.

To register a new user, create a new register_user() function by copying the below example in your ssh_auth file:

function register_user() {
  config_file="${DEST_DIRECTORY}tru-id-ssh-auth/tru-id-ssh-auth.conf"

  if [[ $2 && $3 ]]
  then
      echo "user=$2:$3" >> ${config_file}
      echo "" >> ${config_file}
      echo "User was registered"
  else
      echo "Cannot register user"
  fi
}
Enter fullscreen mode Exit fullscreen mode

Your new register_user() function finds the tru-id-ssh-auth.conf file and adds the following as a new line: user={username}:{phone number}.

Near the bottom of the file, find the line case $1 in. Below this line, enable the register_user() function as a command-line argument by adding the following:

    register-user)
        check_dependencies
        register_user "$@"
        ;;
Enter fullscreen mode Exit fullscreen mode

Allow the User in

Once the PhoneCheck is successful, the function run_shell will be called, which tells the plugin the user is allowed access to their SSH session, in the ssh-auth file, add the following function:

# Once successful, allows user to proceed with access to server
function run_shell() {
    if [[ "$SSH_ORIGINAL_COMMAND" != "" ]]
    then
        exec /bin/bash -c "${SSH_ORIGINAL_COMMAND}"
    elif [ $SHELL ]
    then
        exec -l $SHELL
    fi

    exit $?
}
Enter fullscreen mode Exit fullscreen mode

Creating a PhoneCheck

Retrieve credentials

To make calls to the tru.ID API, you'll need to retrieve your project's credentials, which you can find in the tru.json file you moved into your project earlier. The get_credentials() function below retrieves your project's client_id and client_secret from this file, then sets them as two global variables: BASIC_AUTH_USER and BASIC_AUTH_PASSWORD. Add this new function to your ssh-auth file:

function get_credentials() {
  FILE="${DEST_DIRECTORY}tru-id-ssh-auth/tru.json"

  if [ -f "$FILE" ]; then
    BASIC_AUTH_USER=$(jq -r ".credentials[].client_id" ${FILE})
    BASIC_AUTH_PASSWORD=$(jq -r ".credentials[].client_secret" ${FILE})

    return $OK
  fi

  echo "Unable to retrieve project credentials. Please make sure your project directory has a `tru.json` file."
  exit $FAIL
}
Enter fullscreen mode Exit fullscreen mode

Create access tokens

The next part to creating a PhoneCheck via API is to add functionality to generate access tokens using your tru.ID project's credentials. This new functionality will make a POST request to https://{DATA_RESIDENCY}.api.tru.id/oauth2/v1/token, with a header containing your BASIC_AUTH_USER and BASIC_AUTH_PASSWORD, concatenated with a : in between, and then the entire string encoded with base64. The access token received will be what you use to make further secure API requests to tru.ID's API.

Copy the create_access_token() function below into your ssh_auth file.

# Creates an access token needed to create a PhoneCheck request.
function create_access_token() {
    get_credentials

    CREDENTIALS=$(echo -ne "$BASIC_AUTH_USER:$BASIC_AUTH_PASSWORD" | base64 -w 0);

    # Make request to get access token
    response=`curl \
        --header "Authorization: Basic $CREDENTIALS" \
        --header "Content-Type: application/x-www-form-urlencoded" \
        --request POST $CREATE_AUTH_TOKEN_URL\
        --data-urlencode "grant_type=client_credentials" \
        --data-urlencode "scope=phone_check coverage" --silent`

    curl_exit_code=$?

    # Parses response to get the access token
    ACCESS_TOKEN=$(jq -r .access_token <<< "${response}" )

    if [ $curl_exit_code -ne 0 ]
    then
        echo $curl_exit_code
        echo "Error running curl"
    fi
}
Enter fullscreen mode Exit fullscreen mode

Create a PhoneCheck

You've created functions that allow you to retrieve your project's credentials from your tru.json file; your next function then uses the credentials from this file to generate an access token that you're going to use to make authenticated API calls.

The next step in this tutorial is to initialise the PhoneCheck, which will do the following:

  • Retrieve the current user attempting to log in.
  • Read the config file to find whether this user has their phone number registered to require the MFA step.
  • Make a POST request to https://{DATA_RESIDENCY}.api.tru.id/phone_check/v0.2/checks" with the user's phone number.

Copy the function below into your ssh_auth file to add the functionality described into your project:

function create_check() {
    config_file="${DEST_DIRECTORY}tru-id-ssh-auth/tru-id-ssh-auth.conf"
    current_user=$USER;
    phone_number="";

    while read line; do
        if [[ $line =~ user=$current_user: ]] ; then
            phone_number=$(echo "${line}" | sed s/user=${current_user}://g);
        fi
    done <${config_file}

    if [ "$phone_number" == "" ]; then
        echo "Phone Number is empty"

        return 0
    fi

    # Checking Credentials are installed
    get_credentials

    # Creating an Access Token
    create_access_token

    # Making a Phone Check request"
    response=`curl \
        --header "Authorization: Bearer $ACCESS_TOKEN" \
        --header "Content-Type: application/json" \
        --request POST $PHONE_CHECK_URL\
        --data-raw "{\"phone_number\":\"${phone_number}\"}" --silent`

    curl_exit_code=$?

    if [ $curl_exit_code -ne 0 ]
    then
        echo "Error running curl"
        return 1;
    fi

    # Handling Phone Check Response
    check_id=$(jq -r .check_id <<< "${response}" )
    status=$(jq -r .status <<< "${response}" )
    check_url=$(jq -r ._links.check_url.href <<< "${response}" )

    return $CHECK_STATUS;
}
Enter fullscreen mode Exit fullscreen mode

Generate a QR code

When the user has created a PhoneCheck, the next step in the MFA flow is for the user's device to open their mobile network operator's check URL.

The easiest way for the user to open the URL is with a QR code. If you check your Dockerfile within your project directory, you've already installed the library qr when you built the Docker container. So the next step is to take the check_url retrieved from the PhoneCheck response. This check_url needs to be converted into a QR code and displayed for the user.

Inside create_check(), find the line check_url=$(jq -r ._links.check_url.href <<< "${response}" ), and below it, add the following:

# Generate QR code
qr --ascii "${check_url}" > "qrcode.txt"

sleep 1

cat ~/qrcode.txt
Enter fullscreen mode Exit fullscreen mode

Set up polling

The next step in this process is for your application to check tru.ID's API endpoint https://{DATA_RESIDENCY}.api.tru.id/phone_check/v0.2/checks/{check_id} to determine whether there was a match between the phone number and the request to the check URL or not. There is a two-minute window when the check is created for the user to open the URL. The application will make requests to the API every five seconds for a status update.

If the status value is COMPLETED and the response body contains the value of match as true, then allow the user through; otherwise, refuse entry to the server.

Copy the new start_polling() function into your ssh_auth file:

function start_polling() {
    # Check every 5 seconds for status on Check.
    interval_in_seconds=5
    CHECK_STATUS=$FAIL

    while true;
    do
        # Check status of phone check
        response=`curl \
            --header "Authorization: Bearer $ACCESS_TOKEN" \
            --header "Content-Type: application/json" \
            --request GET $GET_PHONE_CHECK_URL/${check_id}  --silent`

        curl_exit_code=$?

        if [ $curl_exit_code -ne 0 ]
        then
            echo "Error running curl"

            return $FAIL;
        fi

        status=$(jq -r .status <<< "${response}" )
        match=$(jq -r .match <<< "${response}" )

        # If check is complete, output
        if [[ "$status" != "PENDING" && "$status" != "ACCEPTED" ]]; then
            if [ "$status" == "COMPLETED" ]; then
                if [ "$match" == "true" ]; then
                  CHECK_STATUS=$OK;

                  run_shell
                else
                  echo "No match found!"
                  CHECK_STATUS=$FAIL

                  return $FAIL;
                fi
            elif [ "$status" == "EXPIRED" ]; then
                echo "Check Expired";
                CHECK_STATUS=$FAIL

                return $FAIL;
            elif [ "$status" == "ERROR" ]; then
                echo "Check Error Received";
                CHECK_STATUS=$FAIL

                return $FAIL;
            else
                echo "$status"
                echo "404, no status was found";
                CHECK_STATUS=$FAIL

                return $FAIL;
            fi

            break;
        fi

        # Otherwise continue
        sleep $interval_in_seconds;
    done
}
Enter fullscreen mode Exit fullscreen mode

You need to call this new function within your create_check() function. Find the line cat ~/qrcode.txt, and below this, add the following to trigger the start_polling functionality:

# Start polling
start_polling
Enter fullscreen mode Exit fullscreen mode

Add the create_check as a command-line argument by finding the line: case $1 in, and adding the following:

    create-check)
        check_dependencies
        create_check "$@"
        ;;
Enter fullscreen mode Exit fullscreen mode

Add ForceCommand

In your ssh-auth file, you've already called the function add_force_command, but it doesn't yet exist to add the ForceCommand to your sshd_config file. So add this function:

function add_force_command() {
    echo "Trying to add force command to $SSHD_CONFIG"
    find_sshd_config
    auth_ssh_command="$1"

    if [[ -w $SSHD_CONFIG ]]
    then
      echo "Adding 'ForceCommand ${auth_ssh_command} login' to ${SSHD_CONFIG}"
      uninstall "quiet" # remove previous installations

      echo -e "\nForceCommand ${auth_ssh_command} login" >> ${SSHD_CONFIG}
      echo ""

      check_sshd_config_file

      echo "    MAKE SURE YOU DO NOT MOVE/REMOVE ${auth_ssh_command} BEFORE UNINSTALLING tru.ID SSH Plugin"
      sleep 5
    fi
}
Enter fullscreen mode Exit fullscreen mode

If you're following along with this tutorial using Docker, open the sshd_config file within your project directory and at the bottom of this file add the following force command:

ForceCommand /usr/local/bin/tru-id-ssh-auth/ssh-auth create-check
Enter fullscreen mode Exit fullscreen mode

Once added you can skip to Complete PhoneCheck. However, if you're installing this on a server, please continue with the instructions below:

Note: If you're following along with this tutorial using the Docker container, you won't need this code snippet; however, when installing on a server other than Docker, the line is needed and needs to be uncommented. Find the line chmod 644 ${config_file} inside your install() function and add the following; you'll also need to uncomment it.

    # When following this tutorial, leave the line below commented out.
    # Restarting your SSH server within Docker will restart the whole Docker container.
    # add_force_command "${DEST_DIRECTORY}tru-id-ssh-auth/ssh-auth create-check"
Enter fullscreen mode Exit fullscreen mode

Complete PhoneCheck

The final step in the PhoneCheck process is to complete the PhoneCheck. When the mobile device opens the mobile network operator's check URL, they're eventually redirected back to your redirect_url, which on the final request, will have a response containing a code. This code needs to be submitted through the API with your credentials to complete the PhoneCheck.

In your starter-files repository, you will find a template webserver built in node. This is the webserver that will contain the code of your redirect_url that will carry out this functionality.

Install third party dependencies

In your Terminal, navigate to the webserver directory and run the following command to install the third party libraries required for this tutorial. These libraries include express for the webserver functionality, ngrok to provide a publicly accessible URL, and http-signature to verify the signature in the API request.

npm install
Enter fullscreen mode Exit fullscreen mode

Create the webhook

Find the endpoint:

app.get("/", async (req, res) => {
  res.send('hello');
});
Enter fullscreen mode Exit fullscreen mode

And replace it with:

app.get("/complete-check", async (req, res) => {
  if (!req.query) {
    res.status(400).send("body missing");

    return;
  }

  const { code, check_id } = await req.query;

  if (!code) {
    res.status(400).send("code missing");

    return;
  }

  if (!check_id) {
    res.status(400).send("check_id missing");

    return;
  }

  if (req.query.redirect_url) {
    const verified = await tru.verifySignature(req.query.redirect_url);

    if (!verified) {
      res.status(400).send("signature not verified");

      return;
    }
  }

});
Enter fullscreen mode Exit fullscreen mode

In the above example, you're creating a new webhook /complete-check which is accessible with a GET request. This request takes three parameters in the URL, check_id, code, and redirect_url. These parameters are used to complete the PhoneCheck. Once checks have been made to make sure they've been included, the code verifys the signature is valid for the redirect url.

Next, within the webserver directory, create a new file tru.js which is where you're going to add the tru.ID API functionality. In this file, the first thing is to add the dependencies that will be used, as well as defining the global variables and objects. Add the following:

const moment = require("moment");
const fetch = require("node-fetch");
const httpSignature = require("http-signature");
const jwksClient = require("jwks-rsa");
const config = require("../tru.json");

const tru_api_base_url = 'https://eu.api.tru.id';
const keyClient = jwksClient({
  jwksUri: `${tru_api_base_url}/.well-known/jwks.json`,
});

// token cache in memory
const TOKEN = {
  accessToken: undefined,
  expiresAt: undefined,
};
Enter fullscreen mode Exit fullscreen mode

When making a request to an endpoint in tru.ID's API, you'll need an access token:

async function getAccessToken() {
  // check if existing valid token
  if (TOKEN.accessToken !== undefined && TOKEN.expiresAt !== undefined) {
    // we already have an access token let's check if it's not expired
    // I'm removing 1 minute just in case it's about to expire better refresh it anyway
    if (
      moment()
        .add(1, "minute")
        .isBefore(moment(new Date(TOKEN.expiresAt)))
    ) {
      // token not expired
      return TOKEN.accessToken;
    }
  }

  const url = `${tru_api_base_url}/oauth2/v1/token`;

  const toEncode = `${config.credentials[0].client_id}:${config.credentials[0].client_secret}`;
  const auth = Buffer.from(toEncode).toString('base64');

  const requestHeaders = {
    Authorization: `Basic ${auth}`,
    "Content-Type": "application/x-www-form-urlencoded",
  };

  const res = await fetch(url, {
    method: "post",
    headers: requestHeaders,
    body: new URLSearchParams({
      grant_type: "client_credentials",
      scope: "phone_check coverage",
    }),
  });

  if (!res.ok) {
    return res.status(400).body("Unable to create access token")
  }

  const json = await res.json();

  // update token cache in memory
  TOKEN.accessToken = json.access_token;
  TOKEN.expiresAt = moment().add(json.expires_in, "seconds").toString();

  return json.access_token;
}
Enter fullscreen mode Exit fullscreen mode

A new function patchPhoneCheck is needed, which will make a PATCH request to tru.ID's API: /phone_check/v0.2/checks/${checkId}, with the code contained in the body. This is a method to verify the owner of the SIM card was the one that requested the PhoneCheck. In your tru.js file add the following new function:

async function patchPhoneCheck(checkId, code) {
  const url = `${tru_api_base_url}/phone_check/v0.2/checks/${checkId}`;
  const body = [{ op: "add", path: "/code", value: code }];

  const token = await getAccessToken();
  const requestHeaders = {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json-patch+json",
  };

  const res = await fetch(url, {
    method: "patch",
    headers: requestHeaders,
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    return res;
  }

  const json = await res.json();

  return json;
}
Enter fullscreen mode Exit fullscreen mode

As previously explained, you need to verify the signature provided with the redirect_url, to ensure it hasn't been altered in any way. This can be carried out with the following function, so add this to your file:

async function verifySignature(originalUrl) {
  try {
    const url = new URL(originalUrl);
    const signature = Buffer.from(
      url.searchParams.get("authorization"),
      "base64"
    ).toString("utf-8");
    const date = Buffer.from(url.searchParams.get("date"), "base64").toString(
      "utf-8"
    );

    url.searchParams.delete("authorization");
    url.searchParams.delete("date");

    const originalRequest = {
      url: `${url.pathname}${url.search}`,
      method: "get",
      hostname: url.hostname,
      headers: {
        date,
        host: url.host,
        authorization: signature,
      },
    };

    const parsedOriginalRequest = httpSignature.parseRequest(originalRequest, {
      clockSkew: 300,
    });

    const jwk = await keyClient.getSigningKey(parsedOriginalRequest.keyId);
    const verified = httpSignature.verifySignature(
      parsedOriginalRequest,
      jwk.getPublicKey()
    );
    return verified;
  } catch (err) {
    console.error(err);
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

The two new functions patchPhoneCheck and verifySignature need to be accessed in your index.js file, so at the bottom of the tru.js file add the following exports for these two:

module.exports = {
  patchPhoneCheck,
  verifySignature
};
Enter fullscreen mode Exit fullscreen mode

Back in your index.js file, at the top among the requires, add the line:

const tru = require("./tru");
Enter fullscreen mode Exit fullscreen mode

Find the complete-check endpoint, and at the bottom of this function, add the following:

  try {
    const check = await tru.patchPhoneCheck(check_id, code);

    if (check.status === "COMPLETED" && check.match) {
      res.status(200).send('Verification complete, please close this tab and return to your SSH session.');

      return;
    } else {
      // verification failed = user not authenticated
      res.status(401).send("Verification failed, false match");

      return;
    }
  } catch (err) {
    console.log(err);

    if (err.status) {
      res.status(err.status || 500).send(err.message);
    } else {
      res.status(500).send("Unexpected Server error");
    }

    return;
  }
Enter fullscreen mode Exit fullscreen mode

The above code calls the patchPhoneCheck created in your tru.js file, which makes a PATCH request to the tru.ID API with the body containing the code needed to complete the PhoneCheck process.

Now in your Terminal, inside the webserver directory, run the following command and make note of the ngrok url output:

npm start
Enter fullscreen mode Exit fullscreen mode

In your ssh-auth file, find the line: # API Check URLs and below this add the following, replacing <Your NGROK URL>, with the ngrok URL you made a note of in the previous step:

BACKEND_SERVER="<Your NGROK URL>"
REDIRECT_URL="$BACKEND_SERVER/complete-check"
Enter fullscreen mode Exit fullscreen mode

To have the final redirect be your specified webhook URL, find the line below:

--data-raw "{\"phone_number\":\"${phone_number}\"}" --silent`
Enter fullscreen mode Exit fullscreen mode

And replace it with:

--data-raw "{\"phone_number\":\"${phone_number}\", \"redirect_url\":\"${REDIRECT_URL}\"}" --silent`
Enter fullscreen mode Exit fullscreen mode

Setting up the Docker container

This tutorial makes use of a Docker container for development purposes. To build this Docker container and have it running, you'll need to:

  • Build and run your Docker container. This process will also map the internal port 22 (ssh) to an externally accessible port 223.
  • Open the Docker container with a bash session.

So, in your Terminal, run the following two commands:

docker-compose up --build -d
docker-compose exec ssh bash
Enter fullscreen mode Exit fullscreen mode

Installing the SSH Plugin

You've now added multi-factor authentication to your SSH authentication process. With your Docker container built and running, change to the project directory in the same terminal instance. For this, the default directory is /root/tru-id-ssh-auth/. Then run the command ./ssh-auth install to install your copy of your project directory over to /usr/local/bin/.

Note: This is defined in your Dockerfile at the line: ADD . /root/tru-id-ssh-auth

cd /root/tru-id-ssh-auth/
./ssh-auth install
Enter fullscreen mode Exit fullscreen mode

The command ./ssh-auth install will do the following:

  • Copy your project directory from /root/tru-id-ssh-auth/ to /usr/local/bin/tru-id-ssh-auth/.
  • Create a /usr/local/bin/tru-id-ssh-auth/tru-id-ssh-auth.conf config file.

Registering a user

With the plugin installed, you now need to enable the check for the user(s). This stores the user's name and phone number into your recently created config file. The application will then compare this with the credentials entered when the user attempts to log in. Still in the same Terminal, run the following command, swapping out the placeholders for your valid details:

Note: The Docker user and password are both test.

/usr/local/bin/tru-id-ssh-auth/ssh-auth register-user <username> <phone-number-inc-country-code>
# For example: /usr/local/bin/tru-id-ssh-auth/ssh-auth register-user test 447000000000
Enter fullscreen mode Exit fullscreen mode

Login attempt

You've now set everything up. To check everything is working, open a new Terminal session and run the following command to SSH into your SSH server:

ssh test@127.0.0.1 -p 223
Enter fullscreen mode Exit fullscreen mode

The Docker config example uses the username test and the password test.

Wrapping up

That's it! You've now introduced a multi-factor authentication step for your server's SSH authentication process using tru.ID's PhoneCheck API. The beauty of this is it limits the user's input, by only having to require the user to enter their SSH credentials and then scan the QR code, they don’t need to wait for a code to come through SMS or other means for example. The MFA process is all carried out in the background once the QR code has been scanned on their mobile device.

Resources

Top comments (0)