Embed a CLA Signing Page Using NodeJS with the HelloSign and GitHub APIs

"Embed a CLA Signing Page Using NodeJS with the HelloSign and GitHub APIs" header image

Overview

It’s common practice to ask developers contributing to an open source project to sign a Contributors License Agreement or CLA. A CLA lays out simple rules to ensure the contributor has the rights to use the code being incorporated as well as adherence to particular style or policies.

This tutorial shows one of the multiple ways to facilitate this process by implementing a CLA signing flow embedded in a webpage using the developer’s GitHub identity and the embedded signing capability of the HelloSign API. More specifically, we will implement a minimalist Web Server using Node.JS and Express that will kick off an OAuth authorization flow with GitHub; It will first authenticate the user, then request authorization for our application to access the email of the contributor.  With this information, we will directly present to the user a signing page containing the CLA document using the HelloSign API. Additionally, the document to sign is created from a template and we will pre-fill it with the username and email pulled via the GitHub API.

Here is a screen capture of the flow we will implement:

One important thing about this flow, is that we first validate the signers identity. We delegate this task to GitHub as ultimately this is the identity required to make code contributions. This is a critical element before embedding the HelloSign signing experience directly in your application as identity validation is left to the developer in an embedded scenario where HelloSign is not responsible for sending emails to the signers.

This project will use the following tech stack:

Running the code

If you simply want to run the code, follow the steps below, otherwise, follow the tutorial for a step by step.

  1. Clone the code from GitHub.
  2. Make sure Node.js is installed on your machine, if that is not the case, go to Nodejs.org and get the latest version
  3. Run npm install to install all the dependencies listed in the package.json file
  4. Configure both GitHub and HelloSign apps as shown in sections 4 and 7 respectively
  5. Create a document template as shown in section 8
  6. Run the code by executing node index.js in your terminal and go to http://localhost:3000/cla in your browser

Preparing the server

Make sure Node.JS is installed on your machine, if that is not the case, go to Nodejs.org and get the latest version. At the time of building this sample code, we have used version 15.5.1. The minimum version required to support this sample code is 8.2.1.

To start your project, simply create a folder, navigate to it and initialize your project with a minimal package.json file

  
    mkdir hs-embedded-cla
    cd hs-embedded-cla
    npm init -y
  

We will now install the following important dependencies:

  • express as the Web application framework
  • morgan that will give us Http login and enable us to see server activity in the terminal
  • hbs which is is the handlebars view engine used by Express. The view engine allows express to create dynamic HTML content using templates stored inside the /views folder.
  • dotenv used to load environment variables from an .env file


Run the following command to install the above libraries

npm install --save express morgan hbs dotenv

Now create an index.js file and add the following code to initialize those libraries:

index.js

  
    const express = require('express');
    const app = express();
    const logger = require('morgan'); // Adds Http logging into the console
    require('hbs');  // Handlebars as view engine
    require('dotenv').config({silent: true}); // Read values from .env file

    const PORT = 3000;

    app.set('view engine', 'hbs');
    app.use(logger('dev'));

    // Starts server
    app.listen(PORT, () => console.log(`Server listening on port ${PORT}!`));
  

At this point you should be able to start your server with node index.js though any call to localhost will result on a 404 response as we haven’t implemented any route yet.

Keeping sensitive information safe

There is sensitive information that should not be hardcoded like application keys and secrets. There are also items that should be configured for a local test vs server production such as the redirectURL of the OAuth flow.  The dotenv library loaded in the previous section will help us keep sensitive or environment specific information separate in an .env file. It is important to make sure you don’t commit this file to any repository. For a git repository, add it to the .gitignore file.

As a next step, create a .env file and add the following values which you will be replacing in the next sections.

.env

  
    GITHUB_CLIENT_ID="<Client ID from GitHub config page>"
    GITHUB_SECRET="<Client secret from GitHub config page>"
    GITHUB_AUTH_CALLBACK_URL="<GitHub Redirect URL for OAuth>"
    SESSION_SECRET="<Any random Secret for session storage>"
    HELLOSIGN_CLIENT_ID="<Client ID from HelloSign API config page>"
    HELLOSIGN_API_KEY="<API Key from HelloSign API config page>"
    CLA_TEMPLATE_ID="<Template ID from HelloSign.com>"
    SIGNER_ROLE_NAME="<Role name established in the HelloSign template>"
  

Configuring the GitHub app

In order to request access to private information such as the user’s email, you need to register an OAuth application using your personal GitHub account.  In this process, you will need to specify an Auth Callback URL which is the URL that GitHub will redirect to when the authorization is complete. In the GitHub app registration process enter the following value:

GitHub Authorization Callback URL

http://localhost:3000/auth

Once registered, you will be able to retrieve from the GitHub app configuration page the Client ID and the Client Secret. In the .env file replace the first three variables with the values from GitHub like shown below

.env

  
    GITHUB_CLIENT_ID="123456789abcdef"
    GITHUB_SECRET="123456789abcdef"
    GITHUB_AUTH_CALLBACK_URL="http://localhost:3000/auth"
  

Implementing GitHub OAuth

With GitHub configuration in place, we can implement the OAuth authorization flow which will not only authenticate the user, but it will also give us access to the contributors email after proper authorization. Here’s a diagram that shows the sequence of events on that flow.

Diagram detailing the OAuth authorization flow
OAuth authorization flow

  • When we receive a request in the server in the /cla route, we generate a random state value using the crypto library and save it within the current session. To manage sessions we will use the express-session library and for testing purposes we will use local storage, while a production app should use a proper database.
  • The server will then redirect to GitHub authorization URL passing over the client_id, scope and state. Our app will be limited to request the user:email scope.
  • After successful authentication and authorization, GitHub will redirect to the URL we registered in the app configuration step and will give us back a code and the state value we provided earlier.
  • We will compare the state value received with the one stored in the current session; with this, we mitigate possible CSRF attacks. For additional security, we only allow this validation once before clearing out the state value in the current session.
  • Finally, we will exchange the code for a token via a POST request and using GitHub credentials. To make API calls we will use the node-fetch library which allows us to make asynchronous Https requests in a simple fashion.

With a valid authorization token we can now retrieve the primary email and the GitHub username. We will save both values in the current session, so if we receive another call from the same user, we don’t need to authorize the app again as long as the session is valid. So to complete the loop, we will check first if the user has an email and username in the existing session and if not, we will kick start the OAuth flow.

Let us first install all the libraries required for the OAuth implementation

npm install --save express-session node-fetch

And then, create a github.js file with the following code

github.js

  
    const crypto = require('crypto'); // To create random state values for OAuth
    const fetch = require('node-fetch');

    // Kick starts the OAuth code flow
    const authorize = (req,res)=>{
      // Random state value
      let state = crypto.randomBytes(16).toString('hex');
      req.session.oauth_state = state;

      // Get authentication URL and redirect
      authUrl = "https://github.com/login/oauth/authorize?"
                +"client_id=" + process.env.GITHUB_CLIENT_ID
                +"&scope=user:email"
                +"&state=" + state;
      res.redirect(authUrl);
    }

    // Call back for an authorization request
    const auth = async (req, res) =>{
      // State received should be the same as the one stored within the session
      let state = req.query.state;
      let stored_state = req.session.oauth_state;
      req.session.oauth_state = null; // For security, validation is allowed once

      if(!stored_state || state != stored_state){
        res.status(401);
        return res.send("<h2>Authentication error. Try again</h2>");
      }
      if(!req.query.code){
        res.status(500);
        return res.send("<h2>Error authorizing Application</h2>");
      }

      try {
        let github_access_token = await getGitHubToken(req.query.code,state);
        req.session.email = await getGitHubEmail(github_access_token);
        req.session.github_username = await getGitHubUserName(github_access_token);
        return res.redirect("/cla");

      } catch (error) {
        console.log(error);
        res.status(500);
        return res.send("<h2>An error ocurred with the GitHub API</h2>");
      }
    }

    // Exchanges code for an access token in GitHub
    async function getGitHubToken(code,state){

      let url = "https://github.com/login/oauth/access_token";
      let body = {
          client_id : process.env.GITHUB_CLIENT_ID,
          client_secret : process.env.GITHUB_SECRET,
          code : code,
          redirect_uri : process.env.GITHUB_AUTH_CALLBACK_URL,
          state : state
        };
      let options = {
          method: 'post',
          headers: {
            'Content-Type': 'application/json',
            'Accept': "application/json"
          },
          body: JSON.stringify(body),
        }

      let response = await fetch(url,options);
      if(response.status != 200) throw new Error("couldn't get token from GitHub");
      let token = await response.json();
      return token.access_token;
    }

    module.exports = {
      authorize: authorize,
      auth: auth
    };
  

Now let’s add the session initialization and the two routes in the index.js file: the /cla route to kick off OAuth, and the /auth route GitHub will redirect to after this has been completed.  For this, add the following lines to the index.js file right after the port definition.


index.js

  
    const github = require('./github.js');
    const session = require('express-session'); // Session management
    // Session initialization
    // Uses local storage, for testing purposes only
    app.use(session({
      secret: process.env.SESSION_SECRET,
      resave: false,
      saveUninitialized: true
    }));
  

and add the routes piece right after the line where the server is started


index.js

  
    // CLA signature page
    app.get('/cla', async (req,res)=>{
      if(!req.session.email){
        github.authorize(req,res);
      }else{
        //Temporary response
        res.send(req.session.email);
      }
    });
    
    // GitHub OAuth callback
    app.get('/auth', github.auth);
  

Make sure to add any random value to the session secret in the .env file. Remember that for testing purposes we use local storage. For a production setup, please review the express-session documentation.


.env

SESSION_SECRET="dog-cow-cat"

With all the above, you should now be able to successfully authorize the Github application. To test it, first start the server using node index.js then, navigate in your browser to http://localhost:3000/cla. After the OAuth flow, you should see the primary email retrieved from GitHub displayed in your browser.

Configuring HelloSign API app

To present the embedded signature page we will be making server calls to the HelloSign API and also using a client-side library to display the signing experience in the browser. In order to use these two elements, you will need a HelloSign API app configured. This API provides a Test mode which allows users to make non-legally binding signature requests for testing purposes for free and without having to enter a credit card. Making valid signature request calls requires a paid plan. For more information visit the HelloSign API website.


To configure the HelloSign API app:

  1. Create an account if you don't already have one and retrieve your API Key from the settings page. Copy this value into the .env file
  2. Create an API app (login required). You will need to enter a domain. If you don’t have a domain, just enter any sample domain like example.com as we are only going to be using Test mode and bypassing domain validation. Note that a production app requires this validation to be successful and client library will only work on a registered domain.


After creating the app, you'll be presented a Client ID. Copy the value into the .env file and with this, you should have now these two values in the .env file


.env

  
    HELLOSIGN_CLIENT_ID="123456789abcdef"
    HELLOSIGN_API_KEY="123456789abcdef"
  

Using HelloSign Templates

One important element for this example is the ability to automatically pull a templated contract and pre-fill it with information about the user obtained from the authentication with GitHub. While the information we fill automatically into the contract such as GitHub username and email should not be allowed to be changed, we can also provide some fields to be filled by the signer at signature execution time. To learn more about templates, visit the template documentation page.

  • Login to HelloSign and click on the create a template option. Even if you don’t have a paid plan, you can create templates to try the API in Test mode
  • Upload a document and click on Next at the top
  • You will need to enter a signer role, this is important as you will need to specify it in the API calls. In this case, we will call the signer Contributor and will add it to the .env file


Now you need to prepare the template with some special considerations as described below

Screenshot showing template preparation in the HelloSign app
Template preparation in HelloSign

In general, what you will do is dragging fields from the left, place them on the document and configure them on the right. Each time you place a field, you can assign an owner and specify if it is required or not, a solid filling indicates that a field is required.


We will add two text boxes, one for username and another one for email.  These are particular because we want to pre-fill them and we don’t want them to be editable as we are retrieving this information from GitHub ensuring that we can trace this contract to the user making a contribution. To accomplish this, you will need to assign them to the Sender role (which is added by default) as shown in the image (which is why they appear in purple color). Additionally, you need to give them a reference name that you can use with the API, in this interface is called the Merge Field.  So, for the Merge Field we will use the following values:  github_email and github_username. These will be referenced later in the source code.


Finally when you are saving your template, you can specify an email that will receive copies of signed documents and give your template a name, resulting in a message with a template id which you will also add to your .env file, completing the project configuration.


.env

  
    CLA_TEMPLATE_ID="123456789abcdef"
    SIGNER_ROLE_NAME="Contributor"
  

Embedding the signing experience

Part of the magic of the HelloSign API is allowing to embed the signature experience directly in a website. This saves a back and forth of emails as we will send the user directly to the signature page once the authentication is complete on GitHub.


Embedding the page is accomplished in three steps:

  1. Creating an embedded signature request using a template with the HelloSign API, which will return an array of signature_ids corresponding to each person who will sign the document. In this example, we only use one signer so we pick the first signature_id. In this call we will pass the template id and the values for the Merge Fields defined in the template. Additionally, we will use the test mode so we don’t need a paid subscription.
  2. Obtaining an embedded sign URL with the HelloSign API.
  3. Passing the URL into the hellosign-embedded client-side library, which will load it and kick out the signing experience.


Note: To know more about embedded singing, visit the HelloSign API documentation


To accomplish the first two steps, we will add a new file called hellosign.js which will be responsible for making backend API calls. To make things simpler, we will use the HelloSign NodeJS SDK.

npm install --save hellosign-sdk

Add the following code to the new file


hellosign.js

  
    const hellosign = require("hellosign-sdk")({ key: process.env.HELLOSIGN_API_KEY });

    const getEmbedURL = async (github_email,github_username)=>{
      let options = {
        test_mode: 1,
        clientId: process.env.HELLOSIGN_CLIENT_ID,
        template_id: process.env.CLA_TEMPLATE_ID,
        subject: 'Contributor License Agreement',
        signers: [
          {
            email_address: github_email,
            name: 'Contributor',
            role: process.env.SIGNER_ROLE_NAME
          }
        ],
        custom_fields: {   //Merge fields defined in the template
          github_username: github_username,
          github_email: github_email
        }
      }
    
      // 1st create an embeded signature request using a template
      let response = await hellosign.signatureRequest.createEmbeddedWithTemplate(options);
      let signature_id = response.signature_request.signatures[0].signature_id;
    
      // 2nd fetch the url to embed specific for the first (and only) signer
      let embedded_resp = await hellosign.embedded.getSignUrl(signature_id);
      return embedded_resp.embedded.sign_url;
    }
    
    module.exports = {
      getEmbedURL: getEmbedURL
    };
  

Now we need to implement the client part of the embedding process. For this example we will use a minimalistic approach using handlebars as the view engine but you can use any framework you want. We will create a views folder and put there an .hbs file that will contain HTML code and inline JavaScript which will load the library.  When the server calls a view to be passed onto the client, we can dynamically replace values contained in a handlebar expression, which is a value contained within a {{ and a }}.


so create a /views/index.hbs file with the following content


/views/index.hbs

  
    <!DOCTYPE html>
    <html>
      <head>
        <script src="https://cdn.hellosign.com/public/js/embedded/v2.9.0/embedded.development.js"></script>
      </head>
      <body>
        <script type="text/javascript">
          const hs_client = new window.HelloSign({clientId: "{{hellosign_client_id}}" });
          // Event triggered when the file is sent for signature
          hs_client.on('sign', (data) => {
            document.body.innerHTML = "<h2>Thanks for signing!</h2>"
          });

          let hs_options = { skipDomainVerification: true };
          let embed_url = "{{{embed_url}}}";
          hs_client.open(embed_url,hs_options);
        </script>
      </body>
    </html>
  

A few things to notice:

  • We make use of the hellosign-embedded client-side library. At the time we are writing this tutorial the version used was 2.9.0. Make sure you are using the latest version.
  • We need to pass to this file two values: first the {{hellosign_client_id}} and second the {{{embed_url}}} which will be loaded by the HelloSign client library. Notice that we are using {{{}}} here as this makes sure that the URL is not escaped. More information here.
  • Notice that we are skipping the domain verification in the HelloSign library, this only works for signatures requested under test mode. Production apps require domain verification.

To complete the code, we will make a small modification to the routing sections in the index.js file. The modified code will work like this:

  • When we receive a call to the /cla route, we check if we have an email stored in the session
  • If no, we will get it from GitHub via OAuth, store it in the session and redirect to /cla
  • Once we have the email and username in the session, we retrieve it and make a call to our HelloSign function to obtain the URL to embed
  • We load the hbs file passing both the client id and the embed url. This will create and respond an HTML file that contains the JavaScript code that loads the client.


Modify the /cla route in the index.js by adding a require line referencing the hellosign.js file we created above and then replacing the /cla routing code with the one below.


index.js

  
    const hellosign = require('./hellosign.js');

    // CLA signature page
    app.get('/cla', async (req,res)=>{
    
      if(!req.session.email){
        github.authorize(req,res);
      }else{
    
        let embed_url;
        try{
          embed_url = await hellosign.getEmbedURL(req.session.email,req.session.github_username);
        }catch(error){
          res.status(500);
          return res.send("<h2>An error ocurred with HelloSign</h2>");
        }
    
        let args = {
          layout: false,
          hellosign_client_id: process.env.HELLOSIGN_CLIENT_ID,
          embed_url: embed_url,
        };
        res.render("index", args);
      }
    });
  

With this modification, you should now be able to run the whole sample code by starting the server executing node index.js in your terminal and visiting http://localhost:3000/cla in your browser

License

Apache 2.0

What to do from here

This code sample is intended for test and API exploration purposes, If you want to build upon this code sample, here’s a list of important considerations for you to make:

  • HelloSign API apps that use embedded flows require going through an app approval process. More information in the HelloSign API Embedded Signing guide.
  • For session management, we use local storage. In a production environment you want to use a proper database.
  • A good way to build upon this sample is to allow the signer to download to the signed document. For this you can directly fetch the document after it has been signed using the Get Files endpoint.

Other resources

HelloSign API Getting Started guide

HelloSign API Code Samples in GitHub

Tips On Finding the Best Integration Model for HelloSign API

Ready to integrate signatures into your app or website?

Let us help you build a custom API plan that fits your unique business needs.

Get news, insights and posts directly in your inbox

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form
Oops! Something went wrong while submitting the form.