Authorization Code Flow - PHP

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 PHP on the server.

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 PHP. We recommend MAMP.
  2. Familiarity using a command line interface (CLI) such as Terminal or Windows Command Prompt.
  3. Sign up for a GitHub account, if you don't already have one. The source code for this tutorial is stored in GitHub repository.
  4. Install Git and have the ability to clone or fork a repo.
  5. A reliable Internet connection.

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. With this particular tenant, keep in mind that you share this sandbox with other developers. You can access the Developer Sandbox tenant and learn the various endpoints 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 — Grab the Source Code

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

Use a command prompt to clone the sky-api-tutorial-auth-code-php repo which creates a working directory by the same name that contains the code for the tutorial:

$ git clone https://github.com/blackbaud/sky-api-tutorial-auth-code-php.git

Step 4 — Prepare Your Environment

  1. Open the sky-api-tutorial-auth-code-php working directory and copy the configuration file config.php.sample as config.php. The config.php file contains the application's environment variables for the PHP environment.
  2. Update config.php 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:8888/auth/callback.php.
    AUTH_SUBSCRIPTION_KEY Your Blackbaud Developer Subscription Key.
    Use either the Primary key or Secondary key, visible on your Blackbaud Developer Profile.
  3. Save the config file.
  4. Review the .gitignore file. The purpose of the file is to specify the untracked files to ignore. Note that config.php file is ignored. This prevents the config file from being synced to GitHub and protects your registered application's keys and other sensitive data from being exposed.

Step 5 — Install and Configure MAMP

After you have your subscription key, Application ID, Application secret, and the sky-api-auth-tutorial-php source code, 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 PHP with MAMP to serve the project locally.

  1. Download and install MAMP.
  2. Launch MAMP and edit the following in Preferences.

    Tab Field Description
    Web Server Document Root Change this path to point at your cloned sky-api-tutorial-auth-code-php directory.
    MAMP Document Root
  3. Select Ok and Start Servers.
  4. In a Web browser, navigate to localhost:8888. The Web server displays our app.

Application Starting Point

  1. Open the index.html file. MAMP serves this file as our applications root where we initialize our app and load assets to build our page.
  2. The body tag includes an attribute named ng-app. The front-end of our application uses AngularJS to interact with our PHP server.
  3. <!-- INITIALIZE THE APP -->
        <body ng-app="AuthCodeFlowTutorial">
            <div class="container" ng-controller="ConstituentCtrl" ng-cloak>
            ... 
  4. Open your browser to http://localhost:8888 to request the Home page. When the front page loads, the AngularJS on the Home page (index.html) makes a request to the /auth/authenticated.php endpoint:

    (function (angular) {
      'use strict';
    
      angular.module('AuthCodeFlowTutorial', [])
        .controller('ConstituentCtrl', function ($scope, $http, $window) {
    
          // Check user access token.
          $http.get('/auth/authenticated.php').then(function (res) {
            $scope.isAuthenticated = res.data.authenticated;
            if ($scope.isAuthenticated === false) {
              $scope.isReady = true;
              return;
            }
            ...

    The call to the endpoint routes to the corresponding php page (/auth/authenticated.php) and returns a Boolean representing the current user's authentication status. First, it requires and loads the /includes/blackbaud/blackbuad.php file.

    <?php
        require_once '../includes/blackbaud/blackbaud.php';
    
        echo json_encode(array(
        'authenticated' => blackbaud\Session::isAuthenticated()
        ));
            ...
  5. Open the /includes/blackbaud/blackbaud.php file. The PHP code pulls in the rest of our required controllers, config environment variables, and http library for making API requests. We can see **require_once 'session.php';** pulls in the includes/blackbaud/session.php file.

        <?php
        // Error reporting, for development only.
        ini_set('display_errors', 1);
        ini_set('display_startup_errors', 1);
        error_reporting(E_ALL);
    
        require_once 'session.php';
        require_once join(DIRECTORY_SEPARATOR, array($_SERVER['DOCUMENT_ROOT'], 'config.php'));
        require_once join(DIRECTORY_SEPARATOR, array('api', 'constituents.php'));
        require_once 'auth.php';
        require_once 'http.php';
            ...
  6. Open the /includes/blackbaud/session.php file. It begins by defining our namespace blackbaud, allowing the methods and variables defined beneath it to be accessed by any other classes also under that namespace. Our original call to /auth/authenticated/ made use of the isAuthenticated() method in this Session object. This method checks for the existance of a PHP $_SESSION object and looks for an access_token. If the application has not yet been authorized by the end user, then IsAuthenticated() will return false.

    <?php
        public static function isAuthenticated() {
        return isset($_SESSION[self::$tokenName]) && isset($_SESSION[self::$tokenName]['access_token']);
        }
            ...

Display the Log in button

  1. If the user is not authenticated, a Log in button is displayed.

    Note: The browser may display a warning that the connection is not private. For this tutorial, you can ignore this message. To proceed, select Show advanced and then select Proceed to localhost (unsafe).

    Login
  2. Open the index.html file. Notice that the body tag includes an attribute named ng-app. The front-end of our application uses AngularJS to interact with our Web server routes. The div.container element includes an attribute named ng-controller which references an AngularJS controller to handle the model data.

    <body ng-app="AuthCodeFlowTutorial">
      <div class="container" ng-controller="ConstituentCtrl" ng-cloak>
        ...
  3. When the Home page first loads the app will not have received authorization from the user to access their SKY API data. As a result the isAuthenticated scope variable will be false and the Home page's Angular HTML template displays the Log in button.

    <div ng-if="!isAuthenticated">
        <a href="/auth/login" class="btn btn-primary">Log in</a>
    </div>
    ...

Obtain an Access Token

  1. Open /includes/blackbaud/auth.php.
  2. When the user selects the Log in button, a call is made to auth/login.php, which redirects the request to /includes/blackbaud/auth.php file. Here, the redirect() method is called, which redirects the browser to SKY API’s authorization endpoint to start the authentication and authorization process. The user must authenticate with their Blackbaud credentials (if they are not already signed in) and authorize your application to access their SKY API data.

    <?php
    class Auth {
      public static function redirect() {
        $auth_uri = self::getAuthorizationUri();
        header("Location: $auth_uri", true, 301);
        exit();
      }
        ...
      private static function getAuthorizationUri() {
        return AUTH_BASE_URI . 'authorization?' . 
          http_build_query(array(
            'client_id'=> AUTH_CLIENT_ID,
            'response_type' => 'code',
            'redirect_uri' => AUTH_REDIRECT_URI
          ));
      }
        ...
  3. Once authorized, SKY API redirects the user back to the /auth/callback.php URI with an authorization code. Once an authorization code has been obtained, it exchanges the code for an access token. The app is then redirected back to the Home page.

    <?php
    require_once '../includes/blackbaud/blackbaud.php';
    
    blackbaud\Auth::exchangeCodeForAccessToken($_GET['code']);
    
    header('Location: /');
    exit();
    
        ...
    // build out the request body with the code from the redirect URI, and call the fetchTokens() method, passing the newly made $body array.
    public static function exchangeCodeForAccessToken($code = 0) {
    $body = array(
        'code' => $code,
        'grant_type' => 'authorization_code',
        'redirect_uri' => AUTH_REDIRECT_URI
    );
    return self::fetchTokens($body);
    }
        ...
    // build out the appropriate token headers, and make the reqeust to the SKY API endpoint.   Grab the returned JSON response and
    // store the tokens in the Session.
    private static function fetchTokens($body = array()) {
    $headers = array(
        'Content-type: application/x-www-form-urlencoded',
        'Authorization: Basic ' . base64_encode(AUTH_CLIENT_ID . ':' . AUTH_CLIENT_SECRET)
    );
    
    $url = AUTH_BASE_URI . 'token';
    
    $response = Http::post($url, $body, $headers);
    $token = json_decode($response, true);
    Session::setToken($token);
    return $response;
    }
    

Retrieve Constituent Data

  1. Open the index.html file.
  2. AngularJS again makes the request to auth/authenticated, which now returns true. Since the user is authorized, AngularJS then makes a request the application’s constituent API endpoint /api/constituents.php?id=280 to retrieve a constituent record.

    angular.module('AuthCodeFlowTutorial', [])
        .controller('ConstituentCtrl', function ($scope, $http, $watch) {
    
           // Check user access token.
            $http.get('/auth/authenticated.php').then(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.php?id=280').then(res => {
                $scope.constituent = res.data.constituent;
                $scope.isReady = true;
              });
            });
            ...
  3. Open api/constituents.php and includes/blackbaud/api/constituents.php.
  4. The get request in index.html file directs the request to api/constituents.php, where the access token is refreshed and the call is passed along to /includes/blackbaud/api/constituents.php with the id from the route param ?id=280 that we passed into the request.

    <?php
        ...
       $response = blackbaud\Auth::refreshAccessToken();
        $token = json_decode($response, true);
        if (!isset($token['access_token'])) {
          echo json_encode($token);
          return;
        }
        $data = blackbaud\Constituents::getById($_GET['id']);
        ...

    The getById() method interacts directly with the SKY API endpoints making a get request to the https://api.sky.blackbaud.com/constituent/v1/constituents/280 endpoint and passing in the required headers.

    <?php
        ...
            self::$headers = array(
            'Bb-Api-Subscription-Key: ' . AUTH_SUBSCRIPTION_KEY,
            'Authorization: Bearer ' . Session::getAccessToken(),
            'Content-type: application/x-www-form-urlencoded'
            );
            self::$baseUri = SKY_API_BASE_URI . 'constituent/v1/';
        }
    
        public static function getById($id = 0) {
            $url = self::$baseUri . 'constituents/' . $id;
            $response = Http::get($url, self::$headers);
            return json_decode($response, true);
        }
        ...

    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

  5. Once the constituent information is retrieved and added to the front page, Log Out and Refresh Access Token buttons are displayed.
  6. Open /auth/logout.php and includes/blackbaud/session.php.
  7. If the user selects Log Out, they are redirected to /auth/logout.php, which calls the blackbaud\Session::logout() in includes/blackaud/session.php. The logout() method destroys the token stored in the PHP server's $_SESSION.
  8. <?php
        ...
        public static function logout() {
          unset($_SESSION[self::$tokenName]);
        }
        ...
  9. If the user selects Refresh Access Token, AngularJS makes a request to /auth/refresh-token.php. The call is passed to the refreshAccessToken() method in includes/blackbaud/auth.php, which builds out the request body with the required fields. The grant_type is set to refresh_token and the refresh_token field is populated with the refresh_token we have stored in the $_SESSION. This body is then passed to the fetchTokens() method where the post to the SKY API endpoint is made.
  10. <?php
      ...
      public static function refreshAccessToken() {
        $body = array(
          'grant_type' => 'refresh_token',
          'refresh_token' => Session::getRefreshToken()
        );
        return self::fetchTokens($body);
      }
      ...
        private static function fetchTokens($body = array()) {
        $headers = array(
          'Content-type: application/x-www-form-urlencoded',
          'Authorization: Basic ' . base64_encode(AUTH_CLIENT_ID . ':' . AUTH_CLIENT_SECRET)
        );
    
        $url = AUTH_BASE_URI . 'token';
    
        $response = Http::post($url, $body, $headers);
        $token = json_decode($response, true);
        Session::setToken($token);
        return $response;
      }
      ...
      

    The JSON response from SKY API is then parsed, the new set of tokens are stored in our $_SESSION, and the data is sent back to Angular to be displayed on the page for your reference.

That's it!

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