Authorization Code Flow - NodeJS

GitHub

Overview

We use OAuth 2.0 to secure access to a user's SKY API data. In this tutorial we obtain user authorization using the Authorization Code Flow. From the user's perspective, the user authenticates as a Blackbaud user with the normal credentials for Blackbaud NXT and then authorizes (or denies) your application. To accomplish this, your application obtains an authorization code from the Blackbaud Authorization 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:

  • Ensure that you signed up for a developer account and obtained your subscription to an API product.
  • Register an application with SKY API.
  • Obtain authorization to access user data for a specific tenant.
  • 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.

Prerequisites

  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. Sign up for a GitHub account, if you don't already have one. The source code for this tutorial is stored in GitHub repository.
  5. Install Git and have the ability to clone or fork a repo.
  6. A reliable Internet connection to clone the repo and install the project's dependencies.

Step 1 — Get Your Keys

If you have not already done so, complete the Getting Started guide. The tutorial guides you through signing up for a Blackbaud developer account and requesting a subscription to an API product. After you are approved, your subscription contains a Primary key and 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.

Developer Sandbox Tenant

After your subscription is approved, your developer account can access the Developer Sandbox tenant that represents a sample database. Keep in mind that you share this sandbox with other developers. You can access the Developer Sandbox tenant through the interactive SKY API Console within the API Reference.

Step 2 — Register Your App

To call the SKY API, first register your application to obtain its unique set of credentials, which your users will use to enable your app to access their data

  1. From My Applications, click Register app.

  2. Enter the name, description, and logo of your application, as well as your organization's name. This information appears for users when they enable access to your application during the authorization process or in their product.

  3. In the Application website URL field, enter where users can learn more about your application online.

  4. Specify the URIs to use to redirect users back to your application during the authorization process. Note: The URIs must be absolute and use HTTPS. However, we do support http://localhost:port or http://127.0.0.1:port for local development.

    Important!   When your application requests authorization to access a Blackbaud user's data, it includes a redirect_uri parameter in its query string. To authorize your application, this value must match exactly against one of the URIs you provide, including any trailing slashes. For more information, see common authorization issues.

  5. Click Save.

After you register an app, note the ID and secret that appear under Application Credentials. These credentials are unique to your application, and verify its identity during the authorization process.

ID -
Your application's unique identifier. Your users will need this ID to enable your application to access their Blackbaud data. You can't modify this ID; if you need to change it for any reason, delete the application and re-register it.

Secret -
The key your application provides 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, click Show.

Very Important!   Keep the application secret private and safe! If the secret is compromised, regenerate it. Blackbaud reserves the right to remove or deactivate your application to protect customer data.

Step 3 — Install Node.js

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.

  • Download and install Node.js. Use the default settings for your development environment.
  • Create a file named testserver.js and add the following code:

      // 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/');
  • Save the file in a folder named testnodejs.

  • From a command prompt, change the directory to testnodejs and run the testserver.js file:

    $ cd testnodejs
    $ node testserver.js

    The Web server listens for requests on your localhost, port 1337.

Starting Hello World from command prompt

  • In a Web browser, navigate to localhost:1337. The Web server displays a page with with "Hello World."

Hello World

  • To stop the Web server, type CTRL-C in the command line.

Step 4 — Grab the Source Code

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 clone https://github.com/blackbaud/sky-api-auth-tutorial.git

Step 5 — Prepare Your Environment

  • 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.
  • 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.
  • Save the environment file.
  • 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.

Step 6 — Install Dependencies

  • 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 app'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.

    $ cd sky-api-auth-tutorial
    $ npm install
  • After you run npm install, verify that the sky-api-auth-tutorial working directory contains the node_modules subfolder.

    npm install depends on a reliable Internet connection to install dependencies. If you have issues running the command, you can hard delete the node_modules folder and run npm install again.

Step 7 — Run the Application

  • Using Command Prompt/Terminal, ensure you are in the working directory.
  • Type npm start to start the application server at http://localhost:5000.

    $ npm start

Application starting point

  • 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.
  • To request the home page open your browser to http://localhost:5000/. It displays the authorization options. Login

    Your browser may display a warning that the connection is not private. For this tutorial, ignore this message. To proceed, click Show advanced, and then click Proceed to localhost (unsafe).

  • Open the ui folder and the index.html file. This opens the home page for our application, where we can initialize our app and load assets to build our page.

  • The body tag includes an attribute named ng-app. The front-end of our application uses AngularJS to interact with our Node.js server routes.
  • Below the body tag, the ng-view tag is used as a hook for our Angular Router to load our desired template view.

    <!-- INITIALIZE THE APP -->
    <body ng-app="AuthCodeFlowTutorial">
    
      <!-- LOAD OUR VIEWS -->
      <ng-view></ng-view>
      ...
    

Set up the router

  • Open the app folder and the main.js file. Our application's logic lives here.
  • First, we declare our angular module and inject the ngRoute dependency.

    angular.module('AuthCodeFlowTutorial', ['ngRoute'])
  • Next, we initialize our Angular Router to manage our views and controllers.

    angular.module('AuthCodeFlowTutorial', ['ngRoute'])
            .config(function ($routeProvider) {
                $routeProvider
                    .when('/home', {
                        templateUrl: './app/main-template.html',
                        controller: 'MainController'
                    })
                    .when('/auth-success', {
                        template: '

    Login Successful

    ', controller: 'AuthController' }) .otherwise({ redirectTo: '/home' })

    In this example, we use two views and two controllers. As your app grows, you can add more views and controllers.

Display the authorize buttons

  • Open the app folder and the main-template.html file. This is our application's core view.
  • The main.js file holds the logic to initiate our AngularJS Controllers and our Angular Router.
  • Just after the page title, the authorize buttons reference the server's authorization endpoint. When a user clicks 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.

      <div ng-if="!isAuthenticated" class="col-sm-12 well">
      ...
      <div class="col-sm-5 well login-options">
        <span class="login-options-label">Login using redirect</span>
        <a href="/auth/login" class="btn btn-primary btn-block btn-lg">Log in</a>
      </div>
    • The Authorize using popup button opens a popup window that is directed to the server's authorization endpoint to initiate the authentication process.

      <div ng-if="!isAuthenticated" class="col-sm-12 well">
      ...
      <div class="col-sm-5 col-sm-offset-2 well login-options">
        <span class="login-options-label">Login using popup</span>
        <button ng-click="popupLogin()" class="btn btn-primary btn-block btn-lg" target="login-iframe">Log in</button>
      </div>

      Using the popup option, our Angular code performs the following actions:

      1. It calls the popupLogin() method. This method opens a window at the specified URL, and we pass in a ?redirect= parameter and set it to the hash /%23/auth-success, which translates to:/#/auth-success.

        .controller('MainController', function ($scope, $http, $window) {
        ...
        $scope.popupLogin = function () {
           var popup
        
           popup = window.open('auth/login?redirect=/%23/auth-success', 'login', 'height=450,width=600,');
           if (window.focus) {
               popup.focus();
           }
        }
        
      2. When the browser redirects to the /#/auth-success, the AuthController closes the current window and redirects the browser to the route '/' so our router will redirect the page to #/home.

        .controller('AuthController', function ($window) {
           ...
           $window.opener.location = '/';
           $window.close();
        })
        
  • The JavaScript in main.js in the MainController uses Ajax calls to the /auth/authenticated endpoint to determine whether the users are logged in. If the users access token has expired or is invalid, the page displays the Authorize buttons. Users must log in to obtain a valid access token.

    angular.module('AuthCodeFlowTutorial', ['ngRoute'])
      ...
      .controller('MainController', function ($scope, $http) {
          ...
          /**
          *  Check user access token.
          */
          $http.get('/auth/authenticated').then(function (res) {
              $scope.isAuthenticated = res.data.authenticated;
    
              /**
              *  Access token is valid. Fetch constituent record.
              */
              if ($scope.isAuthenticated === false) {
                  $scope.isReady = true;
                  return;
              }
          });
      });

Obtain an Access Token

  1. Click one of the Authorize buttons and enter your Blackbaud account credentials. After authentication, your browser redirects to the Blackbaud Authorization Service authorization form.

    Authorize

  2. Open index.js and /server/routes/auth.js.

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

    // 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 creating 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 Authorization Service along with the token endpoint.

    oauth2 = require('simple-oauth2')({
      clientID: process.env.AUTH_CLIENT_ID,
      clientSecret: process.env.AUTH_CLIENT_SECRET,
      site: 'https://oauth2.sky.blackbaud.com/',
      tokenPath: '/token'
    });

    To obtain an authorization code, the getLogin() function uses simple-oauth2's authorizeURL() function to display the Blackbaud Authorization 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.

    function getLogin(request, response) {
      request.session.redirect = request.query.redirect;
      request.session.state = crypto.randomBytes(48).toString('hex');
      response.redirect(oauth2.authCode.authorizeURL({
          redirect_uri: process.env.AUTH_REDIRECT_URI,
          state: request.session.state
      }));
    }

    The access token exchange should occur server side. Do not expose the Application secret to users in client-side code. Do not expose your Application secret in a source code repository such as GitHub.

    The path /auth/callback is routed to the getCallback() function that exchanges authorization codes for access tokens.

    function getCallback(request, response) {
      ...
    
      options = {
          code: request.query.code,
          redirect_uri: process.env.AUTH_REDIRECT_URI
      };
      oauth2.authCode.getToken(options, function (errorToken, ticket) {
          if (errorToken) {
              error = errorToken.message;
          } else {
              redirect = request.session.redirect || '/';
    
              request.session.redirect = '';
              request.session.state = '';
    
              saveTicket(request, ticket);
              response.redirect(redirect);
          }
      });
    
      ...
    }

    The access token value is not passed back to the client. Instead, the saveTicket() function saves it to the session state.

    function saveTicket(request, ticket) {
      request.session.ticket = ticket;
      request.session.expires = (new Date().getTime() + (1000 * ticket.expires_in));
    }

Retrieve constituent data

The response redirects users to the home page and AppController. The AppController verifies that users are logged in through calls to the web server's /auth/authenticated endpoint. After verification, calls are made to the web server's api/constituents/280.

angular.module('AuthCodeFlowTutorial', [])
.controller('AppController', function ($scope, $http, $window) {

    //  Checks the user access token.

    $http.get('/auth/authenticated').then(function (res) {
        $scope.isAuthenticated = res.data.authenticated;
        if ($scope.isAuthenticated === false) {
            $scope.isReady = true;
            return;
        }

        //  Access token is valid. Fetch constituent record.

        $http.get('/api/constituents/280').then(function (res) {
            $scope.constituent = res.data;
            $scope.isReady = true;
        });
    });
  • Open /server/libs/sky.js and /server/routes/api.js.
  • The route in the app'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:

    function proxy(request, method, endpoint, body, callback) {
        var options = {
            json: true,
            method: method,
            body: body,
            url: 'https://api.sky.blackbaud.com/constituent/v1/'' + 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.


    function get(request, endpoint, callback) { return proxy(request, 'GET', endpoint, '', callback); }

    The data is returned as JSON to the browser where the model's data is projected through the view of the Angular template.

    <div ng-if="isAuthenticated">
      <h3>Constituent: {{ constituent.name }}</h3>
      <p>
        See <a href="https://developer.sky.blackbaud.com/contract-reference#Constituent" target="_blank">Constituent</a>
        within the SKY API contact reference for a full listing of properties.
      </p>
      <div class="table-responsive">
        <table class="table table-striped table-hover">
          <thead>
            <tr>
              <th>Name</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>

    GET Constituent

Summary

You should now have a fully functioning application using implicit-flow. Users of your app should be able to log in with their Blackbaud credentials, authorize the app, 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.