Creating Static Integrations with Authentication

The more advanced type of static integrations that can be built are the ones that employ authentication. In this step-by-step guide, you will learn how to create an SSH block with all mandatory authentication details. Commands will be executed only when the authentication check has passed.

Setting up this static integration type involves the following steps:

  1. Create a Folder Structure – Define and create a folder structure for the integration. It should have the following structure:

  • auth schemas- – references the integration name

  • flows

    • integration name

  • javascript – references the integration name

  • icons – they are optional

An example folder structure
  1. Create a manifest.json file – You need to create the manifest.json file in the main directory of the integration. In it, you can add all dependencies that are used in the package.json file.

    {
        "manifest_version": "1.0",
        "name": "TestIntegration",
        "version": "1.0.0",
        "description": "Some description",
        "author": {
            "name": "The name of the creator",
            "email": "test@test.io"
        },
        "dependencies": {
            "ltrim": "^1.0.0"
        },
        "changelog": {
            "1.0.0": "What changed"
        }
    }

    The following parameters can be defined here:

    • manifest_version – the version of the manifest file. You can update the versioning when updating the file to bring awareness of changes.

    • name – The name of the integration package.

    • version – The version of the integration package.

    • changelog – Allows you to set changelog entries referencing notes for each integration package

    • description – A description of the integration package.

    • author – you can define the author here. Sub-parameters are the “name” and “email” of the associated author.

    • dependencies – you can define any dependent package.

  2. Putting an image inside the icons folder (optional)

The file should be small-sized and with a transparent background in a SVG format.
The associated icon should be named after the integration name.

  1. Create a JSON file inside the authschemas/integrationName directory – Create this file to match the integration name. Inside the Schema/properties enter all fields that are required for the successful authentication. In our example, the host, port, username, and password fields are of plaintext type. If necessary, you can add a boolean or string type field with enum arrays.

    In our example, the host, port, and username are mandatory fields.

    {
      "serviceName": "SalamiPepperoni",
      "title": "SalamiPepperoni",
      "schema": {
        "type": "object",
        "title": " ",
        "properties": {
          "host": {
            "type": "string",
            "description": "some description",
            "title": "Host"
          },
          "port": {
            "type": "integer",
            "title": "Port",
            "default": 22
          },
          "username": {
            "type": "string",
            "title": "Username"
          },
          "password": {
            "type": "string",
            "format": "password",
            "title": "Password"
          }
        },
        "required": ["host", "port", "username"]
      }
    }
    
  1. Create the two necessary files in the javascript/integrationName folder – Create the two necessary files for this integration. The first authenticate.js checks if the authentication is cached and retrieves it from the user.

'use strict';

function authenticate(pid, authKey) {
  let configCache = require('@sdk/config_cache')(pid);

  return new Promise((resolve, reject)=> {
    let cachedConfig = configCache.retrieve(authKey);
    if (cachedConfig) {
      resolve();
      return;
    }

    configCache.getAuthData(authKey).then((auth) => {
      if (!auth) {
        reject('authKey not provided.');
      }
      if (!auth.data) {
        reject('Invalid SalamiPepperoni authentication credentials!!!');
      } else {

        /**
         *  Here is where the magic must happen
         *  If needed any auth action must be done here.
         *  For example we can call(requestp) auth function of an API and pass the token
         *  as part of the second param of configCache.register()
         *  
         *  auth.token = "the token we got" 
         *  configCache.register(authKey, auth)
         *  // Then pass auth to register and use in the request
         *
         *  // Now in the request.js we can have both the token
         *  // and the fields we filled for the auth above:
         *  // ["protocol", "password", "host", "username"]
         *  // auth.data.host etc.
         */
         
        configCache.register(authKey, auth.data);
        resolve();

        /**
         * End of magic
         */
        
      }
    }).catch(reject);
  });
}

module.exports = authenticate;

authenticate.js example code block

With the second file called request.js you can make calls to the integration and also hold inside the logic for the authentication data preparation.

'use strict';
const authenticate = require('./authenticate');

function authenticated_request(auth, params) {
  var opts = {
    host: auth.host,
    port: auth.port,
    username: auth.username,
    password: auth.password,
    tryKeyboard: true
  };

  if (params.username) {
    opts.username = params.username;
  }
  if (params.password) {
    opts.password = params.password;
  }
  var Client = require('ssh2').Client;

  return new Promise((resolve, reject) => {
    let result = {
      data: "",
      error: "",
      success: true
    };
    let conn = new Client()

    conn.on('ready', function () {
      conn.exec(params.command, function (err, stream) {
        if (err) {
          result.error = err;
          result.success = false;
          resolve(result);
        } else {
          stream.on('close', function (code, signal) {
            resolve(result);
          }).on('data', function (data) {
            result.data += data.toString();
          }).stderr.on('data', function (data) {
            result.error += data.toString();
          });
        }
      });
    }).on('error', function (err) {
      console.log('error:' + err);
      resolve(err);
    }).on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) {
      finish([opts.password]);
    }).connect(opts);
  });
}

async function promise(userId, authKey, params) {
    await authenticate(userId, authKey);
    const configCache = require('@sdk/config_cache')(userId);

    return new Promise((resolve, reject) => {
        const auth = configCache.retrieve(authKey);
        if (!auth) {
            reject('AuthError: Missing authentication configuration');
            return;
        }
        resolve(auth);
    }).then((auth) => authenticated_request(auth, params));
}

module.exports = promise;

request.js example code block

  1. Create the workflow schema block – Create the containing the JSON guidance code that controls how the block will be shown and how it will handle input and output data. There are no strict requirements for the filename. In comparison with the previous code sample, there are some differences to note. There are no entryNode and exitNode. Instead, the first element is called start. In this code, there are variables called authKey that contain the meta.authType property, which must be set as the same name as serviceName in the authschemas/integrationName/integrationName.json file.

{
    "platform": "node",
    "language": "js",
    "runProcess": "main",
    "name": "SSH",
    "processes": [
        {
            "name": "main",
            "nodes": [
                {
                    "name": "start",
                    "function": "this.moduleAwait('./SalamiPepperoni/authenticate', null, this.i['authKey']);",
                    "nextNodes": [
                        "process_parameters"
                    ]
                },
                {
                    "name": "process_parameters",
                    "function": "this.i['r_params'] = {};\nthis.i['r_params']['username'] = this.i['username']\nthis.i['r_params']['password'] = this.i['password']\nthis.i['r_params']['command'] = this.i['command'];\n",
                    "nextNodes": [
                        "request"
                    ]
                },
                {
                    "name": "request",
                    "function": "this.moduleAwait('./SalamiPepperoni/request', null, this.i['authKey'], this.i['r_params']);",
                    "nextNodes": [
                        "finish"
                    ]
                },
                {
                    "name": "finish",
                    "function": "this.i['result'] = this.l['request'];"
                }
            ],
            "variables": [
                {
                    "name": "authKey",
                    "isInput": true,
                    "isOutput": true,
                    "required": true,
                    "meta": {
                        "description": "Auth Key",
                        "authType": "SalamiPepperoni"
                    },
                    "type": {
                        "type": "string"
                    }
                },
                {
                    "name": "command",
                    "level": "INTERMEDIATE",
                    "type": {
                        "type": "string"
                    },
                    "value": "",
                    "isInput": true,
                    "isOutput": false,
                    "required": true,
                    "meta": {
                        "syntax": "shell",
                        "displayName": "Command",
                        "description": "Command"
                    }
                },
                {
                    "name": "username",
                    "level": "INTERMEDIATE",
                    "type": {
                        "type": "string"
                    },
                    "value": "",
                    "isInput": true,
                    "isOutput": false,
                    "required": false,
                    "meta": {
                        "displayName": "Username",
                        "description": "Username"
                    }
                },
                {
                    "name": "password",
                    "level": "INTERMEDIATE",
                    "type": {
                        "type": "string"
                    },
                    "value": "",
                    "isInput": true,
                    "isOutput": false,
                    "required": false,
                    "meta": {
                        "displayName": "Password",
                        "description": "Password"
                    }
                },
                {
                    "name": "result",
                    "isOutput": true,
                    "meta": {},
                    "type": {
                        "type": "string"
                    }
                }
            ],
            "meta": {}
        }
    ],
    "meta": {
        "color": "blue",
        "type": "action",
        "title": "SSH",
        "info": "Run commands via SSH on remote systems",
        "template": "Process",
        "logo": null
    }
}

Example flow code block

The function parameter of the nodes in this example includes the following elements:

  • start as the first element of process.nodes[]. It calls authenticate.js and passes on the authentication key.

    this.moduleAwait('./SalamiPepperoni/authenticate', null, this.i['authKey']);

start example code block

  • process_parameters, a one-liner that prepares objects into block fields.

this.i['r_params'] = {};
if (this.i['username'] !== undefined) this.i['r_params']['username'] = this.i['username']
if (this.i['password'] !== undefined) this.i['r_params']['password'] = this.i['password']
if (this.i['command'] !== undefined) this.i['r_params']['command'] = this.i['command'];

An example code block utilizing username, password, and command properties as input fields.

  • request which calls the actual request file from javascript/integrationName/request.js

this.moduleAwait('./SalamiPepperoni/request', null, this.i['authKey'], this.i['r_params']);

An example request code block

  • Here finish sets the output field with the result of the request

    this.i['result'] = this.l['request'];
  1. Package the contents in a ZIP file – Once all relevant files are ready, they can be packaged. Create a ZIP package with all files and name them using an appropriate name following this template: integrationName_version.zip.

Note: The zip package must contain the root tree structure itself, do not package the folder holding the files.

  1. Import the package into a service instance – Follow the steps outlined in the Integrations page to import your custom package.

  2. Create a workflow with the new package – You can create a new authentication configuration for the new integration package. Use the Authentications section in the Workflow Editor.

  3. Enter the mandatory fields in the shown screen.

  1. Once this is complete, your custom integration package will show up in the Workflow Editor.

  1. For a sample solution drag a SSH block onto the Workflow Editor.

  1. Choose the new authKey and the Command fields. You set the credentials access using the username and password fields.

  2. You can now start your workflow!