Authorization code flow confidential application - NodeJS

We use OAuth 2.0 to secure access to a user's Blackbaud data. In this tutorial we obtain user authorization using the Authorization Code Flow. From the user's perspective, the user authenticates using their Blackbaud ID credentials and then authorizes (or denies) your application. To accomplish this, your application obtains an authorization code from the Blackbaud OAuth 2.0 Service. The authorization code is then exchanged for an access token that signs requests to the SKY API on behalf of the user. The exchange involves your registered application's Application secret. For security reasons, the exchange is done through direct server-to-server communication. For this reason, we use Node.js, a server-side platform.

In this tutorial, we will accomplish the following tasks:

  1. Ensure that you signed up for a developer account, obtained your subscription to an API product, and registered a new SKY application.
  2. Obtain authorization to access user data.
  3. Retrieve data from a SKY API endpoint.

For this tutorial, we strip down the user interface to highlight the Authorization Code Flow. Our Barkbaud code sample provides a rich user interface using SKY UX.

  1. A server such as your local machine that is capable of running Node.js.
  2. Familiarity with Node.js, using NPM to install project dependencies, and environment variables including setting them in either an OSX/Linux or Windows environment.
  3. Familiarity using a command line interface (CLI) such as Terminal or Windows Command Prompt.
  4. A GitHub account. The source code for this tutorial is stored in a GitHub repository.
  5. Install Git and have the ability to clone or fork a repo.
  6. Complete the Getting Started guide.

If you have not already done so, get a subscription to an API product. Your subscription contains a Primary key and a Secondary key. You can use either key as the subscription key value for the Bb-Api-Subscription-Key request header in calls to the API.

After you register a SKY application, you'll find its ID and secret displayed at the top of the page. These credentials are unique to your application, and will be used to verify its identity during the authorization process.

Review the ID and secret credentials for an application.
  • Application ID - This value unique identifies your application. This value is not considered sensitive, and Blackbaud customer admins will need it when enabling your application to access their Blackbaud data. This value cannot be changed - if you need to change it for some reason, delete the application and re-register it.
  • Application secret - This value is a secret key that your application will provide when it requests an access token to call the SKY API during the authorization process. This value is sensitive, so don't share it with anyone else! To display the secret, select Show.

After you have your subscription key, Application ID, and Application secret, it's time to establish your development environment. Since we are using the Authorization Code Flow, we need to use a server-side software platform. For this tutorial, we will use Node.js.

  1. Download and install Node.js. Use the default settings for your development environment.
  2. Create a file named testserver.js and add the following code:
    JavaScript
            // Create a very simple HTTP web server on your local machine.
            // Set up a HTTP Web server and client, require('http').
            var http = require('http');
    
            // createServer returns a new instance of the http server.
            // A function is used as a request listener.
            // req is an instance of the incoming request.
            // res is an instance of the server response.
            // When you browse to http://localhost:1337/, a 'request' event occurs and
            //   "Hello World" is written from the HTTP Web server back to your browser.
            http.createServer(function (req, res) {
              res.writeHead(200, { 'Content-Type': 'text/plain' });
              res.end('Hello World');
            }).listen(1337, "localhost");
    
            console.log('Server running at http://localhost:1337/');
          
    // Create a very simple HTTP web server on your local machine.
    // Set up a HTTP Web server and client, require('http').
    var http = require('http');
    
    // createServer returns a new instance of the http server.
    // A function is used as a request listener.
    // req is an instance of the incoming request.
    // res is an instance of the server response.
    // When you browse to http://localhost:1337/, a 'request' event occurs and
    //   "Hello World" is written from the HTTP Web server back to your browser.
    http.createServer(function (req, res) {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('Hello World');
    }).listen(1337, "localhost");
    
    console.log('Server running at http://localhost:1337/');
  3. Save the file in a folder named testnodejs.
  4. From a command prompt, change the directory to testnodejs and run the testserver.js file:
    Bash
            $ cd testnodejs
            $ node testserver.js
          
    $ cd testnodejs
    $ node testserver.js
    The Web server listens for requests on your localhost, port 1337.
    Command prompt as the server listens for requests.
  5. In a Web browser, go to localhost:1337. The Web server displays a page with with "Hello World."
    A page with 'Hello World.'
  6. To stop the Web server, type CTRL-C in the command line.

The sky-api-auth-tutorial repo on GitHub provides a starter project to work through the Authorization Code Flow.

Use a command prompt to clone the sky-api-auth-tutorial. The following command creates a working directory named sky-api-auth-tutorial that contains the code for the tutorial:

Git
    $  git clone https://github.com/blackbaud/sky-api-auth-tutorial.git
  
$  git clone https://github.com/blackbaud/sky-api-auth-tutorial.git
  1. Open the sky-api-auth-tutorial working directory and copy the configuration file sky.env-sample as sky.env. The sky.env file contains the application's environment variables for NodeJS environments.
  2. Update sky.env with the following values:
    AUTH_CLIENT_ID Your registered application's Application ID (from Step 2). See Managing your apps.
    AUTH_CLIENT_SECRET Your registered application's Application Secret (from Step 2). See Managing your apps.
    AUTH_REDIRECT_URI One of your registered application's Redirect URIs (from Step 2). For this tutorial, enter http://localhost:5000/auth/callback. See My Applications.
    AUTH_SUBSCRIPTION_KEY Your Blackbaud Developer Subscription Key. Use either the Primary key or Secondary key visible on your Blackbaud Developer Profile.
    PORT The Web server port that will run the application. For this tutorial, enter 5000.
  3. Save the environment file.
  4. Review the .gitignore file. The purpose of the file is to specify the untracked files to ignore. Note that any .env files are ignored. This prevents the environment files from being synced to GitHub and protects your registered application's keys and other sensitive data from being exposed.
  1. From the working directory, run npm install to install the project dependencies.

    NPM is the package manager that comes bundled with Node.js. Since you already installed Node.js, you also have NPM. The command npm install downloads any dependencies listed in the package.json file and adds them to the application's node_modules directory. This command also creates this directory if it doesn't already exist. Each dependency is represented as a child directory of node_modules.

    Git
            $ cd sky-api-auth-tutorial
            $ npm install
          
    $ cd sky-api-auth-tutorial
    $ npm install
  2. After you run npm install, verify that the sky-api-auth-tutorial working directory contains the node_modules subfolder.
  1. Using Command Prompt/Terminal, ensure you are in the working directory.
  2. Type npm start to start the application server at http://localhost:5000.

    Git
            $ npm start
          
    $ npm start

  1. Open the index.js file. This is the starting point of our application. This code runs on the server side and is not visible to the application user. The code performs the following:
    • It registers our application dependencies such as Express.
    • It handles authorization and user requests to the home page and data endpoints.
    • It creates a web server on your local machine at https://localhost:5000.
  2. To request the home page open your browser to http://localhost:5000/. It displays the authorization options.
    Open your browser to http://localhost:5000/. It displays the authorization options.
  3. Open the ui folder and the index.html file. This opens the home page for our application, where we can initialize our application and load assets to build our page.
  4. The body tag includes a container element with an ID of app. We're using JavaScript to manage routing.
  5. Below the body tag, a container element with an ID of app is used as a hook for our JavaScript code to load our desired template view.
    Markup
              <body>
    
                <div id="app">
    
                </div>
                ...
              <body>
            
    <body>
    
      <div id="app">
    
      </div>
      ...
    <body>
  1. Open the app folder and the main.js file. Our application's logic lives here.
  2. First, we add an event listener to the DOMContentLoaded event, which will trigger our initialization function when the HTML document has finished loading.
    JavaScript
              document.addEventListener("DOMContentLoaded", function () )
          
    document.addEventListener("DOMContentLoaded", function () )
  3. Next, we initialize our application by calling the initApp function, which manages the rendering of our views and handles user authentication.
    JavaScript
              function initApp() {
                  // Check if the user is authenticated
                  fetch('/auth/authenticated')
                      .then(response => response.json())
                      .then(data => {
                          if (data.authenticated) {
                              // User is authenticated, fetch constituent data
                              fetch('/api/constituents/280')
                                  .then(response => response.json())
                                  .then(data => {
                                      const constituent = data;
                                      renderConstituentData(constituent);
                                  });
                          } else {
                              renderLoginOptions();
                          }
                      });
              }
              
    function initApp() {
        // Check if the user is authenticated
        fetch('/auth/authenticated')
            .then(response => response.json())
            .then(data => {
                if (data.authenticated) {
                    // User is authenticated, fetch constituent data
                    fetch('/api/constituents/280')
                        .then(response => response.json())
                        .then(data => {
                            const constituent = data;
                            renderConstituentData(constituent);
                        });
                } else {
                    renderLoginOptions();
                }
            });
    }
  1. The main.js file holds the logic to initiate our application, using JavaScript and HTML.
  2. Just after the page title, the authorize buttons reference the server's authorization endpoint. When a user selects one of these buttons, the authorization process begins. When a session is authenticated, the authorize buttons are hidden.
    • The Authorize using redirect button initiates the authorization process by redirecting the browser to the authorization endpoint to initiate the authentication process.
      JavaScript
       <a href="/auth/login" class="btn btn-primary btn-block btn-lg"><i class="fa fa-external-link"></i> Authorize using redirect</a> 
      <a href="/auth/login" class="btn btn-primary btn-block btn-lg"><i class="fa fa-external-link"></i> Authorize using redirect</a>
    • The Authorize using popup button opens a popup window that is directed to the server's authorization endpoint to initiate the authentication process.

      JavaScript adds a click handler to the button. When clicked, the handler checks whether we've already launched the authorization popup window previously. If not, we open the new authorization popup window and set up a timer with setInterval that will check the following every 500 milliseconds:

      • Whether the popup has been closed by the user.
      • Whether the user has authorized.
      If either of these conditions is satisfied, we'll stop the timer and clear our popup and timer references. We also close the authorization popup window if it hasn't already been closed. Finally, if the user is authorized, we'll fetch and render constituent with ID 280.
      JavaScript
                  document.getElementById("popup-login").addEventListener("click", function () {
                      if (popup && !popup.closed) {
                          popup.focus();
                          return;
                      }
      
                      // Open a new popup window for login
                      popup = window.open('auth/login?redirect=/%23/auth-success', 'login', 'height=450,width=600');
      
                      // Focus the popup window if possible
                      if (window.focus) {
                          popup.focus();
                      }
      
                      // Set an interval to check every 500ms if the user has authenticated
                      intervalId = setInterval(function () {
                      
                          // Check if the popup window is still open
                          if (popup && popup.closed) {
                              // If the popup is closed, clear the interval and reset variables
                              clearInterval(intervalId);
                              intervalId = null;
                              popup = null;
                              return;
                          }
      
                          // Check if the user is authenticated by making a 
                          // request to /auth/authenticated
                          fetch('/auth/authenticated')
                              .then(response => response.json())
                              .then(data => {
                                  // If the user is authenticated, clear the interval and 
                                  //close the popup
                                  if (data.authenticated) {
                                      clearInterval(intervalId);
                                      intervalId = null;
                                      popup.close();
                                      popup = null;
                                      // Fetch constituent data now that the user is authenticated
                                      fetch('/api/constituents/280')
                                          .then(response => response.json())
                                          .then(data => {
                                              const constituent = data;
                                              // Render the constituent data
                                              renderConstituentData(constituent);
                                          });
                                  }
                              });
                      }, 500);
                  });
              
      document.getElementById("popup-login").addEventListener("click", function () {
          if (popup && !popup.closed) {
              popup.focus();
              return;
          }
      
          // Open a new popup window for login
          popup = window.open('auth/login?redirect=/%23/auth-success', 'login', 'height=450,width=600');
      
          // Focus the popup window if possible
          if (window.focus) {
              popup.focus();
          }
      
          // Set an interval to check every 500ms if the user has authenticated
          intervalId = setInterval(function () {
      
              // Check if the popup window is still open
              if (popup && popup.closed) {
                  // If the popup is closed, clear the interval and reset variables
                  clearInterval(intervalId);
                  intervalId = null;
                  popup = null;
                  return;
              }
      
              // Check if the user is authenticated by making a
              // request to /auth/authenticated
              fetch('/auth/authenticated')
                  .then(response => response.json())
                  .then(data => {
                      // If the user is authenticated, clear the interval and
                      //close the popup
                      if (data.authenticated) {
                          clearInterval(intervalId);
                          intervalId = null;
                          popup.close();
                          popup = null;
                          // Fetch constituent data now that the user is authenticated
                          fetch('/api/constituents/280')
                              .then(response => response.json())
                              .then(data => {
                                  const constituent = data;
                                  // Render the constituent data
                                  renderConstituentData(constituent);
                              });
                      }
                  });
          }, 500);
      });
  1. Select one of the Authorize buttons and enter your Blackbaud account credentials. After authentication, your browser redirects to the Blackbaud OAuth 2.0 Service authorization form.
    Blackbaud Authorization Service authorization form.
  2. Open index.js and /server/routes/auth.js.

    The Authorize button prompt a request to the web server's /auth/login endpoint. The route in the application's main index.js file directs requests to the getLogin() function within /server/routes/auth.js.

    JavaScript
            // Register our OAUTH2 routes
            app.get('/auth/authenticated', routes.auth.getAuthenticated);
            app.get('/auth/login', routes.auth.getLogin);
            app.get('/auth/callback', routes.auth.getCallback);
            app.get('/auth/logout', routes.auth.getLogout);
          
    // Register our OAUTH2 routes
    app.get('/auth/authenticated', routes.auth.getAuthenticated);
    app.get('/auth/login', routes.auth.getLogin);
    app.get('/auth/callback', routes.auth.getCallback);
    app.get('/auth/logout', routes.auth.getLogout);
    The auth.js file relies on the simple-oauth2 client library. To create an object you must provide your registered application's Application ID and Application secret values, which reside in the project's sky.env file as the AUTH_CLIENT_ID and AUTH_CLIENT_SECRET environment variables. You need the URL to the Blackbaud OAuth 2.0 Service along with the token endpoint.
    JavaScript
            var { AuthorizationCode } = require('simple-oauth2');
    
            config = {
                client: {
                    id: process.env.AUTH_CLIENT_ID,
                    secret: process.env.AUTH_CLIENT_SECRET
                },
                auth: {
                    tokenHost: 'https://oauth2.sky.blackbaud.com',
                    authorizePath: '/authorization',
                    tokenPath: '/token'
                }
            };
    
            authCodeClient = new AuthorizationCode(config);
          
    var { AuthorizationCode } = require('simple-oauth2');
    
    config = {
        client: {
            id: process.env.AUTH_CLIENT_ID,
            secret: process.env.AUTH_CLIENT_SECRET
        },
        auth: {
            tokenHost: 'https://oauth2.sky.blackbaud.com',
            authorizePath: '/authorization',
            tokenPath: '/token'
        }
    };
    
    authCodeClient = new AuthorizationCode(config);
    To obtain an authorization code, the getLogin() function uses simple-oauth2's authorizeURL() function to display the Blackbaud OAuth 2.0 Service's authorization form. After users approve or deny the request, the responses are redirected using the value of the AUTH_REDIRECT_URI environment variable: https://localhost:5000/auth/callback.
    JavaScript
            function getLogin(request, response) {
                var codeVerifier,
                    codeChallenge,
                    challengeDigest;
    
                function base64URLEncode(str) {
                    return str.toString('base64')
                        .replace(/\+/g, '-')
                        .replace(/\//g, '_')
                        .replace(/=/g, '');
                }
    
                request.session.redirect = request.query.redirect;
                request.session.state = crypto.randomBytes(48).toString('hex');
    
                codeVerifier = base64URLEncode(crypto.randomBytes(32));
                challengeDigest = crypto
                    .createHash("sha256")
                    .update(codeVerifier)
                    .digest();
    
                codeChallenge = base64URLEncode(challengeDigest);
                request.session.code_verifier = codeVerifier;
    
                response.redirect(authCodeClient.authorizeURL({
                    redirect_uri: process.env.AUTH_REDIRECT_URI,
                    state: request.session.state,
                    code_challenge: codeChallenge,
                    code_challenge_method: 'S256'
                }));
            }
          
    function getLogin(request, response) {
        var codeVerifier,
            codeChallenge,
            challengeDigest;
    
        function base64URLEncode(str) {
            return str.toString('base64')
                .replace(/\+/g, '-')
                .replace(/\//g, '_')
                .replace(/=/g, '');
        }
    
        request.session.redirect = request.query.redirect;
        request.session.state = crypto.randomBytes(48).toString('hex');
    
        codeVerifier = base64URLEncode(crypto.randomBytes(32));
        challengeDigest = crypto
            .createHash("sha256")
            .update(codeVerifier)
            .digest();
    
        codeChallenge = base64URLEncode(challengeDigest);
        request.session.code_verifier = codeVerifier;
    
        response.redirect(authCodeClient.authorizeURL({
            redirect_uri: process.env.AUTH_REDIRECT_URI,
            state: request.session.state,
            code_challenge: codeChallenge,
            code_challenge_method: 'S256'
        }));
    }
    The path /auth/callback is routed to the getCallback() function that exchanges authorization codes for access tokens.
    JavaScript
            async function getCallback(request, response) {
                ...
    
                options = {
                    code: request.query.code,
                    redirect_uri: process.env.AUTH_REDIRECT_URI,
                    code_verifier: request.session.code_verifier
                };
                try {
                    accessToken = await authCodeClient.getToken(options);
    
                    redirect = request.session.redirect || '/';
    
                    request.session.redirect = '';
                    request.session.state = '';
                    request.session.code_verifier = undefined;
    
                    saveTicket(request, accessToken.token);
                    response.redirect(redirect);
                } catch (errorToken) {
                    error = errorToken.message;
                }
    
                ...
            }
          
    async function getCallback(request, response) {
        ...
    
        options = {
            code: request.query.code,
            redirect_uri: process.env.AUTH_REDIRECT_URI,
            code_verifier: request.session.code_verifier
        };
        try {
            accessToken = await authCodeClient.getToken(options);
    
            redirect = request.session.redirect || '/';
    
            request.session.redirect = '';
            request.session.state = '';
            request.session.code_verifier = undefined;
    
            saveTicket(request, accessToken.token);
            response.redirect(redirect);
        } catch (errorToken) {
            error = errorToken.message;
        }
    
        ...
    }
    The access token value is not passed back to the client. Instead, the saveTicket() function saves it to the session state.
    JavaScript
            function saveTicket(request, ticket) {
                request.session.ticket = ticket;
                request.session.expires = (new Date().getTime() + (1000 * ticket.expires_in));
            }
          
    function saveTicket(request, ticket) {
        request.session.ticket = ticket;
        request.session.expires = (new Date().getTime() + (1000 * ticket.expires_in));
    }

The application initializes by checking if the user is authenticated through a call to the /auth/authenticated endpoint. If the user is authenticated, the application fetches the constituent data by making a call to the /api/constituents/280 endpoint.

JavaScript
 
      function initApp() { 
      fetch('/auth/authenticated') 
       .then(response => response.json()) 
       .then(data => { 
        if (!data.authenticated) { 
        renderLoginOptions(); 
        } else { 
          fetch('/api/constituents/280') 
           .then(response => response.json())
           .then(data => { const constituent = data; 
           renderConstituentData(constituent);); 
           }
      });
     } 
    
function initApp() {
 fetch('/auth/authenticated')
  .then(response => response.json())
  .then(data => {
   if (!data.authenticated) {
   renderLoginOptions();
   } else {
     fetch('/api/constituents/280')
      .then(response => response.json())
      .then(data => { const constituent = data;
      renderConstituentData(constituent););
      }
 });
}
  1. Open /server/libs/sky.js and /server/routes/api.js.
  2. The route in the application's main index.js file directs the request to the getConstituent() function in /server/libs/sky.js. The call is passed along to /server/routes/api.js, which interacts directly with SKY API endpoints. Eventually the call makes its way to the proxy() function. Here, we can see the use of the Bb-Api-Subscription-Key and Authorization request headers:
    JavaScript
            function proxy(request, method, endpoint, body, callback) {
                var options = {
                    json: true,
                    method: method,
                    body: body,
                    url: 'Blackbaud OAuth 2.0 Service/' + endpoint,
                    headers: {
                      'bb-api-subscription-key': process.env.AUTH_SUBSCRIPTION_KEY,
                      'Authorization': 'Bearer ' + request.session.ticket.access_token
                    }
                };
    
                promise(options)
                  .then(callback)
                  .catch(function (err) {
                      console.log('Proxy Error: ', err);
                  });
            }
          
    function proxy(request, method, endpoint, body, callback) {
        var options = {
            json: true,
            method: method,
            body: body,
            url: '/' + endpoint,
            headers: {
              'bb-api-subscription-key': process.env.AUTH_SUBSCRIPTION_KEY,
              'Authorization': 'Bearer ' + request.session.ticket.access_token
            }
        };
    
        promise(options)
          .then(callback)
          .catch(function (err) {
              console.log('Proxy Error: ', err);
          });
    }

    The Bb-Api-Subscription-Key value represents your Blackbaud developer account's approved subscription to an API product. You can use your account's Primary key or Secondary key. The Authorization value represents your authorization to use the API. The Authorization header starts with Bearer followed by a space and then the value for the access token.

    A call to the Constituent (Get) endpoint retrieves constituent data and sends it back to the browser.

    JavaScript
            function get(request, endpoint, callback) {
                return proxy(request, 'GET', endpoint, '', callback);
            }
          
    function get(request, endpoint, callback) {
        return proxy(request, 'GET', endpoint, '', callback);
    }
    The browser receives the JSON data and uses JavaScript to dynamically update the HTML content, rendering the constituent data in a table format.
    Markup
              <div>
                <h1>SKY API Authorization Code Flow Tutorial</h1>
                <div class="well">
                  <h3 class="well-title">Constituent: {{ constituent.name }}</h3>
                  <p>
                    See <a href="https://developer.blackbaud.com/skyapi/products/renxt/constituent" target="_blank">Constituent</a>
                    within the Blackbaud SKY API contact reference for a full listing of properties.
                  </p>
                </div>
                <div class="table-responsive">
                  <table class="table table-striped table-hover">
                    <thead>
                      <tr>
                        <th>Key</th>
                        <th>Value</th>
                      </tr>
                    </thead>
                    <tbody>
                      <tr>
                        <td>id</td>
                        <td>{{ constituent.id }}</td>
                      </tr>
                      <tr>
                        <td>type</td>
                        <td>{{ constituent.type }}</td>
                      </tr>
                      <tr>
                        <td>lookup_id</td>
                        <td>{{ constituent.lookup_id }}</td>
                      </tr>
                      <tr>
                        <td>first</td>
                        <td>{{ constituent.first }}</td>
                      </tr>
                      <tr>
                        <td>last</td>
                        <td>{{ constituent.last }}</td>
                      </tr>
                    </tbody>
                  </table>
                </div>
                <a href="/auth/logout" class="btn btn-primary">Log out</a>
              </div>
            
    <div>
      <h1>SKY API Authorization Code Flow Tutorial</h1>
      <div class="well">
        <h3 class="well-title">Constituent: {{ constituent.name }}</h3>
        <p>
          See <a href="https://developer.blackbaud.com/skyapi/products/renxt/constituent" target="_blank">Constituent</a>
          within the Blackbaud SKY API contact reference for a full listing of properties.
        </p>
      </div>
      <div class="table-responsive">
        <table class="table table-striped table-hover">
          <thead>
            <tr>
              <th>Key</th>
              <th>Value</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>id</td>
              <td>{{ constituent.id }}</td>
            </tr>
            <tr>
              <td>type</td>
              <td>{{ constituent.type }}</td>
            </tr>
            <tr>
              <td>lookup_id</td>
              <td>{{ constituent.lookup_id }}</td>
            </tr>
            <tr>
              <td>first</td>
              <td>{{ constituent.first }}</td>
            </tr>
            <tr>
              <td>last</td>
              <td>{{ constituent.last }}</td>
            </tr>
          </tbody>
        </table>
      </div>
      <a href="/auth/logout" class="btn btn-primary">Log out</a>
    </div>
    Data returned as JSON to the browser.

You should now have a fully functioning application. Users of your app should be able to log in with their Blackbaud credentials, authorize the application, and get constituent data after they are authenticated.

Be sure to take a look at our other code samples. You can create an issue to report a bug or request a feature for this code sample. For all other feature requests, see ideas.