SSH Multi-factor Authentication with tru.ID
Tags: tutorial, javascript, bash, programming
Published: 2022-05-05 12:28:46

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](https://developer.tru.id/docs/phone-check) 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](https://tru.id/blog/phone-verification-hidden-threat). **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:

- A [**tru.ID** Account](https://tru.id)
- A mobile phone with an active data SIM card
- [Docker](https://www.docker.com/) installed locally
- [Node](https://nodejs.org/en/) installed locally

## Getting started

A repository branch has been created on [GitHub](https://github.com/tru-ID/tru-id-ssh-auth) containing the foundation code needed to get you started.

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

```bash
git clone -b starter-files git@github.com:tru-ID/tru-id-ssh-auth.git
```

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

```bash
git clone -b main git@github.com:tru-ID/tru-id-ssh-auth.git
```

A [**tru.ID** Account](https://tru.id) is required to make the `PhoneCheck` API requests, so before proceeding, make sure you've created one.

Now navigate to the [**tru.ID** console](https://developer.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:

```bash
# 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=
```

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:

```bash
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 ""
}
```

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:

```bash
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
}
```

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

```bash
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
}
```

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:

```bash
case $1 in
```

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:

```bash
install)
    check_dependencies
    install
    ;;
uninstall)
    uninstall
    ;;
```

## 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:

```bash
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
}
```

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:

```bash
    register-user)
        check_dependencies
        register_user "$@"
        ;;
```

## 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:

```bash
# 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 $?
}
```

## 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:

```bash
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
}
```

### 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.

```bash
# 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
}
```

### 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:

```bash
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;
}
```

### 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`](https://github.com/tru-ID/tru-id-ssh-auth/blob/main/Dockerfile#L8) 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:

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

sleep 1

cat ~/qrcode.txt
```

## 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:

```bash
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
}
```

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:

```bash
# Start polling
start_polling
```

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

```bash
    create-check)
        check_dependencies
        create_check "$@"
        ;;
```

### 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:

```bash
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
}
```

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:

```bash
ForceCommand /usr/local/bin/tru-id-ssh-auth/ssh-auth create-check
```

Once added you can skip to [Complete PhoneCheck](#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.

```bash
    # 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"
```

## 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.

```bash
npm install
```

### Create the webhook

Find the endpoint:

```js
app.get("/", async (req, res) => {
  res.send('hello');
});
```

And replace it with:

```js
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;
    }
  }

});
```

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:

```js
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,
};
```

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

```js
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;
}
```

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:

```js
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;
}
```

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:

```js
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;
  }
}
```

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:

```js
module.exports = {
  patchPhoneCheck,
  verifySignature
};
```

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

```js
const tru = require("./tru");
```

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

```js
  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;
  }
```

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:

```bash
npm start
```


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:

```bash
BACKEND_SERVER="<Your NGROK URL>"
REDIRECT_URL="$BACKEND_SERVER/complete-check"
```

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

```bash
--data-raw "{\"phone_number\":\"${phone_number}\"}" --silent`
```

And replace it with:

```bash
--data-raw "{\"phone_number\":\"${phone_number}\", \"redirect_url\":\"${REDIRECT_URL}\"}" --silent`
```

## Setting up the Docker container

This tutorial makes use of a [Docker](https://www.docker.com/) 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:

```bash
docker-compose up --build -d
docker-compose exec ssh bash
```

## 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`

```bash
cd /root/tru-id-ssh-auth/
./ssh-auth install
```

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`.

```bash
/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
```

## 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:

```bash
ssh test@127.0.0.1 -p 223
```

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

- [tru.ID PhoneCheck Integration guide](/docs/phone-check/guide)
- [PhoneCheck API Reference](/docs/reference/api#tag/phone_check)