Guide

A Beginner’s Guide to Integration Authentication

Zachary Kirby
Co-Founder
Published On
July 25, 2023

A Beginner’s Guide to Integration Authentication

When you open up Postman to start playing with an API, you’ll quickly notice all the options you have for authentication: API keys, Bearer tokens, JWT, Basic Auth and OAuth. And that’s before you get into the platform specific authentication protocols for AWS or Akamai.

All this choice can make it worrying to get started with authentication. Even more so because it is so obviously important. Get it wrong and you can open up your users to huge security risks.

So having a fundamental grasp of what each of these are and how they work is important. It will let you understand what each of your integrations is trying to achieve with authentication (and its counterpart, authorization). It will also help you understand what you need to build for your customers to give them the right security guarantees.

The key concepts of authentication

APIs facilitate communication and data exchange between different software services. However, this process isn't as straightforward as a simple exchange. To ensure the security and privacy of data, we need a gatekeeper, a system that verifies the identity of the requester and dictates what they can and can't access. This is where authentication and authorization come into play.

Authentication is the process of verifying the identity of a user, while authorization determines what an authenticated user is allowed to do. There are several methods used for these crucial processes, each with its own strengths and drawbacks, and their selection depends on the specific needs of the application or service.

Before delving into the technical specifics of these different authentication methods, it's essential to understand some fundamental concepts in the realm of API integration and authentication.

These key concepts serve as the building blocks for understanding the different authentication types. As we explore OAuth and API Key Authentication, you'll see how these elements are used in different ways to achieve secure and effective communication between applications.

Here we’ll go through:

There are other options and variations on these methods (such as JWT bearer tokens) that allow you to either a) access specific platforms, or b) spend specific types of tokens and data for authorization. But with a good understanding of these four authentication methods, you’ll be able to integrate with almost every API and service. Let’s go.

Basic Authentication

Basic Authentication was one of the original API authentication schemes. It has seen declining use in recent years as more secure methods of authentication have been introduced. But a few APIs, such as WooCommerce and Freshdesk continue to use it, in part because of its simplicity for developers.

In Basic Authentication, the client sends a request with an 'Authorization' header. The value of this header is the word 'Basic' followed by a space and a Base64-encoded string of the user's username and password in the format 'username:password' (though sometimes different information can be used in place of username and password, such as client id and client secret. But the format remains the same).

The Basic Authentication Workflow is:

The main advantage of Basic Authentication is its simplicity; it's quick and easy to implement. The main disadvantage is its lack of security. The credentials are not encrypted but only Base64-encoded, which can be easily decoded.

Here is an example of using Basic Authentication in JavaScript with the Fetch API:

// The URL you are making the request to
const API_URL = 'https://api.example.com/data';

// Your username and password
const USERNAME = 'your-username';
const PASSWORD = 'your-password';

// Include the username and password in the header
const headers = {
    'Authorization': 'Basic ' + btoa(USERNAME + ':' + PASSWORD)
};

// Set up the request options
const requestOptions = {
    method: 'GET',
    headers: headers
};

// Async function to make the request
async function fetchData() {
    try {
        const response = await fetch(API_URL, requestOptions);
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        const data = await response.json();
        // Handle the response data...
    } catch (error) {
        console.error('Error:', error);
    }
}

// Call the async function
fetchData();

In this example, USERNAME and PASSWORD are your username and password, and API_URL is the endpoint you're trying to access.

We use the btoa function to Base64-encode the username and password. The fetch function is used to make the request, then we wrap the fetch call in an async function named fetchData.

Bearer Tokens

A Bearer token is a type of access token that is used to authenticate users in an application. The term "bearer token" essentially means "give access to the bearer of this token." In other words, whoever has this token can use it, making it crucial to keep these tokens secure to prevent unauthorized usage. It is used in APIs such as Writer, Twitter, and LogicMonitor.

Bearer tokens are often used in the context of OAuth and are generated by the authentication server. When a client receives a Bearer Token, it can use this token for authentication by including it in the HTTP Authorization header with the Bearer authentication scheme.

But Bearer tokens can also be used independently. In this way, the Bearer token workflow is:

The simplicity of Bearer tokens makes them suitable for a wide range of applications. However, since they're valid until they expire, it's essential to protect them from unauthorized users, as anyone with the token can use it, and why they are often paired with a stronger system such as OAuth.

Here's how you might use a Bearer token with JavaScript's Fetch API:

// The Bearer Token you received from the server
const BEARER_TOKEN = 'your-bearer-token';

// The URL you are making the request to
const API_URL = 'https://api.example.com/data';

// Include the Bearer Token in the header
const headers = {
    'Authorization': `Bearer ${BEARER_TOKEN}`
};

// Set up the request options
const requestOptions = {
    method: 'GET',
    headers: headers
};

// Async function to make the request
async function fetchData() {
    try {
        const response = await fetch(API_URL, requestOptions);
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        const data = await response.json();
        // Handle the response data...
    } catch (error) {
        console.error('Error:', error);
    }
}

// Call the async function
fetchData();

In this example, BEARERTOKEN is the Bearer Token you received from the server, and APIURL is the endpoint you're trying to access. We're using a GET request as an example, but you'd change the method and potentially add a body to the requestOptions for other types of requests. We use the fetch function to make the request, again wrapped in an async function called fetchData.

Hopefully, now we’ve covered a couple of methods you’ll have noticed two things so far:

Now let’s look at the next option, and again we’ll see these similarities.

API Keys

API Key authentication provides a simple, fast, and effective method for securing access. In API Key Authentication, the user or client is given a unique key (a string of characters), which they must include in all their API calls. The server checks this key and only processes the request if the key is valid. API keys are used across a huge array of products and services, including Mailchimp, ActiveCampaign, and Zendesk.

The process for using API Key Authentication generally follows these steps:

API Key authentication is useful for simpler use cases where you need to control access to certain resources. It's commonly used in public APIs for services like weather data, stock prices, and news updates.

The primary advantage of API Key authentication is its simplicity. It's easy to implement and doesn't require the client to follow a complicated series of steps. However, its simplicity is also its downfall. API keys can be vulnerable if not handled correctly, since anyone with the key can use the API. They should be transmitted securely and not embedded in client-side code or shared publicly. GitHub has now incorporated Secret Scanning to help people find API keys (and some of the other secrets we’re talking about here) in their repos quickly before they can be stolen.

Here is an example of using an API key for authentication in JavaScript using the Fetch API:

// The API key you received from the server
const API_KEY = 'your-api-key';

// The URL you are making the request to
const API_URL = 'https://api.example.com/data';

// Include the API key in the header
const headers = {
    'Authorization': `Bearer ${API_KEY}`
};

// Set up the request options
const requestOptions = {
    method: 'GET',
    headers: headers
};

// Async function to make the request
async function fetchData() {
    try {
        const response = await fetch(API_URL, requestOptions);
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        const data = await response.json();
        // Handle the response data...
    } catch (error) {
        console.error('Error:', error);
    }
}

// Call the async function
fetchData();

We can see this is basically the same as a bearer token–it’s just a special type of bearer token!

OAuth

OAuth 2.0 is the current best model for authentication/authorization used widely. HubSpot, Zoho, Pipedrive, and Outreach all use OAuth 2.0.

(Here we’re using OAuth to mean OAuth 2.0. OAuth 1.0 and OAuth 2.0 are both protocols for secure authorization, but they differ in complexity and security measures. OAuth 1.0 involves a complex request-signing process and includes built-in security, allowing it to operate without reliance on SSL/TLS for secure communication. On the other hand, OAuth 2.0 simplifies the process, relies on SSL/TLS for security, and introduces additional features like refresh tokens, making it more adaptable for various types of applications. OAuth 2.0 is widely adopted now.)

OAuth is an open-standard authorization protocol and the basic workflow of OAuth 2.0 involves four roles:

Whenever you are able to sign in with an existing account, like Google or Facebook, OAuth is behind that. This allows the application to access the user's data without needing their password, increasing the security and convenience for the user. But, as we’ll see, OAuth is more complex to implement than other methods due to its numerous interactions between the client, user, and servers.

With the methods above, we were using a simple fetch method to interact with the API, with the authentication information in the header. OAuth works differently, with several steps involving different servers. The following steps are carried out in an OAuth 2.0 workflow:

image

Before we move on to the code, let’s talk about two important and related concepts we are going to see with OAuth.

The Authn vs Authz overlap

The first is the difference between authentication and authorization. We describe this above as “who” (authentication, sometimes shortened to authn) and “what” (authorization, sometimes shortened to authz). They are separate but very linked. As you can see from the graphic above, we are making authorization requests with OAuth, the authentication only happens right at the start.  OAuth is fundamentally about giving access to protected resources to the right people.

Scopes

That leads on to the second concept–scopes. In the realm of authentication and authorization, a "scope" is essentially a permission. It represents a specific action or a set of actions that a client application is allowed to perform on behalf of a user, given the user has granted permission to the application.

A scope can correspond to a type of resource or a level of access that is being requested by the application. For example, in a CRM, scopes might be defined for actions like crm.lists.read and crm.lists.write to have read and write access, respectively. These scopes are usually represented as strings, and the exact names and definitions of these scopes are defined by the specific API.

Scopes are extremely valuable in controlling access and ensuring the principle of least privilege, i.e., a client application only has the access it needs to do its job and nothing more. For example, a third-party application may need to read a user's profile information but doesn't need the ability to modify that data. With the use of scopes, users can grant read access to their profile information while denying write access.

A working OAuth example–HubSpot

Here we’re going to work through a real-life example, one we know well at Vessel–HubSpot OAuth. HubSpot OAuth allows integration access to a user's HubSpot account (or rather the parts of the account that are defined in the scopes).

HubSpot has a repository with the basic code to get this up and running. Let’s step through it to show how the flow above actually works in code. We’re not covering everything here–the actual code has the imports and some session helper functionality–what we want to do is see OAuth in action.

We start with some setup:

//===========================================================================//
//  HUBSPOT APP CONFIGURATION
//
//  All the following values must match configuration settings in your app.
//  They will be used to build the OAuth URL, which users visit to begin
//  installing. If they don't match your app's configuration, users will
//  see an error page.

// Replace the following with the values from your app auth config, 
// or set them as environment variables before running.
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;

// Scopes for this app will default to `crm.objects.contacts.read`
// To request others, set the SCOPE environment variable instead
let SCOPES = ['crm.objects.contacts.read'];


// On successful install, users will be redirected to /oauth-callback
const REDIRECT_URI = `http://localhost:3000/oauth-callback`;

//===========================================================================//

The CLIENTID and CLIENTSECRET are environment variables that store confidential details for the app's OAuth2 client. They need to match the settings configured in the HubSpot app's OAuth settings.

The SCOPES variable holds the permissions that the app is requesting. By default, it's set to crm.objects.contacts.read, which means the app is asking to read contact objects in HubSpot's CRM. Other permissions can be requested by changing this array.

Finally, REDIRECT_URI is the URL to which users will be redirected after successfully installing the app. In this case, it's set to http://localhost:3000/oauth-callback.

With that set up we can build our authorization URL:

//================================//
//   Running the OAuth 2.0 Flow   //
//================================//

// Step 1
// Build the authorization URL to redirect a user
// to when they choose to install the app
const authUrl =
  'https://app.hubspot.com/oauth/authorize' +
  `?client_id=${encodeURIComponent(CLIENT_ID)}` + // app's client ID
  `&scope=${encodeURIComponent(SCOPES)}` + // scopes being requested by the app
  `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}`; // where to send the user after the consent page

// Redirect the user from the installation page to
// the authorization URL
app.get('/install', (req, res) => {
  ...
  res.redirect(authUrl);
  ...
});

Here we are building the authUrl which is the critical part of the authorization process.

The authUrl is constructed by combining the authorization URL (https://app.hubspot.com/oauth/authorize) with:

These are encoded as URL parameters to ensure that they are transmitted properly.

The script then defines an /install route. When a user visits this route, the server will redirect them to the constructed authUrl, initiating the OAuth 2.0 authorization flow with HubSpot. This action starts the process where the user is prompted by HubSpot to give consent for the requested scopes, which are the permissions defined earlier.

The user is prompted to give the app access to the requested resources:

image

When the user clicks to connect their account they are then redirected to the REDIRECT_URI. Here is the code handling the callback URL part of the OAuth 2.0 authorization flow with a HubSpot app.

// Step 3
// Receive the authorization code from the OAuth 2.0 Server,
// and process it based on the query parameters that are passed
app.get('/oauth-callback', async (req, res) => {
  console.log('===> Step 3: Handling the request sent by the server');

  // Received a user authorization code, so now combine that with the other
  // required values and exchange both for an access token and a refresh token
  if (req.query.code) {
    console.log('       > Received an authorization token');

    const authCodeProof = {
      grant_type: 'authorization_code',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      redirect_uri: REDIRECT_URI,
      code: req.query.code
    };

    // Step 4
    // Exchange the authorization code for an access token and refresh token
    console.log('===> Step 4: Exchanging authorization code for an access token and refresh token');
    const token = await exchangeForTokens(req.sessionID, authCodeProof);
    if (token.message) {
      return res.redirect(`/error?msg=${token.message}`);
    }

    // Once the tokens have been retrieved, use them to make a query
    // to the HubSpot API
    res.redirect(`/`);
  }
});

The code checks for the presence of an authorization code from HubSpot in the query parameters (req.query.code). If it exists, the script acknowledges receipt of the authorization code, which is then packaged with other necessary information into an object called authCodeProof.

Next, it attempts to exchange the authorization code for an access token and a refresh token by calling the function exchangeForTokens. This is where the app proves to the HubSpot server that it received the correct authorization code from the user. If successful, these tokens are used to make authorized API calls on behalf of the user:

//==========================================//
//   Exchanging Proof for an Access Token   //
//==========================================//

const exchangeForTokens = async (userId, exchangeProof) => {
  try {
    const responseBody = await request.post('https://api.hubapi.com/oauth/v1/token', {
      form: exchangeProof
    });
    // Usually, this token data should be persisted in a database and associated with
    // a user identity.
    const tokens = JSON.parse(responseBody);
    refreshTokenStore[userId] = tokens.refresh_token;
    accessTokenCache.set(userId, tokens.access_token, Math.round(tokens.expires_in * 0.75));

    console.log('       > Received an access token and refresh token');
    return tokens.access_token;
  } catch (e) {
    console.error(`       > Error exchanging ${exchangeProof.grant_type} for access token`);
    return JSON.parse(e.response.body);
  }
};

const refreshAccessToken = async (userId) => {
  const refreshTokenProof = {
    grant_type: 'refresh_token',
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    redirect_uri: REDIRECT_URI,
    refresh_token: refreshTokenStore[userId]
  };
  return await exchangeForTokens(userId, refreshTokenProof);
};

const getAccessToken = async (userId) => {
  // If the access token has expired, retrieve
  // a new one using the refresh token
  if (!accessTokenCache.get(userId)) {
    console.log('Refreshing expired access token');
    await refreshAccessToken(userId);
  }
  return accessTokenCache.get(userId);
};

const isAuthorized = (userId) => {
  return refreshTokenStore[userId] ? true : false;
};

There are four functions here that help take that initial authorization grant and transfer it into tokens that can be used to access resources:

With that all done, we can finally get access to the resource API:

//====================================================//
//   Using an Access Token to Query the HubSpot API   //
//====================================================//

const getContact = async (accessToken) => {
  
  try {
    const headers = {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    };
    
    const result = await request.get('https://api.hubapi.com/contacts/v1/lists/all/contacts/all?count=1', {
      headers: headers
    });

    return JSON.parse(result).contacts[0];
  } catch (e) {
    console.error('  > Unable to retrieve contact');
    return JSON.parse(e.response.body);
  }
};

getContact is used to make an authorized request to the HubSpot API using an access token. Specifically, it retrieves a single contact from HubSpot's contact API.

Here we can see how Bearer tokens are used with OAuth. We use the accessToken as a Bearer token to request the data from the server. If you are building an integration, this is where you start accessing all the data to pass to your application!

Taking security and privacy seriously

All of this is for one thing–to protect your users. You want to build integrations that are helpful, and that means accessing their data. But you also have to treat that data with the utmost respect and caution. A holistic and proactive approach to data security is fundamental to ensure the integrity and confidentiality of user information. Ensuring the use of robust authorization protocols like OAuth is a part of this responsibility. Ultimately, maintaining user trust is about balancing the value of personalized experiences against the necessity of rigorous privacy safeguards.

Demonstrating such respect for user privacy and security strengthens the relationship between your users and your product, fostering trust and loyalty. With stringent data protection measures in place, you are not just safeguarding their data, but also empowering them to use your platform confidently, knowing they are in safe hands. Hence, prioritizing security and privacy is a win-win strategy that elevates the user experience and bolsters your reputation as a responsible service provider.