Woodshop for old agent generation
Try the new agent generation
  • What is woodshop
  • How to's
    • Smart Relationship
      • GetIdsFromRequest
    • Smart views
      • Display a calendar view
      • Create a custom tinder-like validation view
      • Create a custom moderation view
      • Create a dynamic calendar view for an event-booking use case
    • Configure environment variables
      • NodeJS/Express projects
    • Elasticsearch Integration
      • Interact with your Elasticsearch data
      • Elasticsearch service/utils
      • Another example
    • Zendesk Integration
      • Authentication, Filtering & Sorting
      • Display Zendesk tickets
      • Display Zendesk users
      • View tickets related to a user
      • Bonus: Direct link to Zendesk + change priority of a ticket
    • Dwolla integration
      • Display Dwolla customers
      • Display Dwolla funding sources
      • Display Dwolla transfers
      • Link users and Dwolla customers
      • Dwolla service
    • Make filters case insensitive
    • Use Azure Table Storage
    • Create multiple line charts
    • Create Charts with AWS Redshift
    • View soft-deleted records
    • Send Smart Action notifications to Slack
    • Authenticate a Forest Admin API against an OAuth protected API Backend
    • Translate your project into TypeScript
      • V8
        • Migrate Mongoose files
        • Migrate Sequelize files
      • v7
        • Migrate Mongoose files
        • Migrate Sequelize files
      • v6
    • Geocode an address with Algolia
    • Display/edit a nested document
    • Send an SMS with Zapier
    • Hash a password with bcrypt
    • Display a customized response
    • Search on a smart field with two joints
    • Override the count route
    • Make a field readOnly with Sequelize
    • Hubspot integration
      • Create a Hubspot company
      • Display Hubspot companies
    • Impersonate a user
    • Import data from a CSV file
    • Import data from a JSON file
    • Load smart fields using hook
    • Pre-fill a form with data from a relationship
    • Re-use a smart field logic
    • Link to record info in a smart view
    • Display data in html format
    • Upload files to AWS S3
    • Display AWS S3 files from signed URLs
    • Prevent record update
    • Display, search and update attributes from a JSON field
    • Add many existing records at the same time (hasMany-belongsTo relationship)
    • Track users’ logs with morgan
    • Search on relationship fields by default
    • Export related data as CSV
    • Run automated tests
  • Forest Admin Documentation
Powered by GitBook
On this page
  • Requirements
  • Authentication Flow Overview
  • Sample Code

Was this helpful?

  1. How to's

Authenticate a Forest Admin API against an OAuth protected API Backend

PreviousSend Smart Action notifications to SlackNextTranslate your project into TypeScript

Last updated 3 years ago

Was this helpful?

In some cases, you may want to authenticate a Forest Admin user against your API backend that is hosted elsewhere and protected via OAuth. Your API backend may be using a hosted authentication provider such as Auth0, Okta, or Ping, or you may have implemented OAuth yourself.

To achieve authentication between the Forest Admin API and your API backend, the client credentials grant () is appropriate. When implemented, a client credentials grant eliminates the need to transmit a static or pre-shared, per-user, API key between Forest Admin and your API backend.

Requirements

  • An admin backend running on forest-express-sequelize/forest-express-mongoose

  • An API protected via OAuth

Authentication Flow Overview

  1. When you need to call your backend API from a Forest Admin route (via a or , for example), your Forest Admin API must first call your OAuth token endpoint, including the appropriate Client ID, Client Secret, and Forest Admin username (retrieved via Forest Admin’s field). The Client ID and Client Secret should be stored securely and protected against disclosure. The implementation of this step is likely specific to your authentication provider. See Auth0 documentation: .

  2. Upon success, your authentication provider returns a signed access token (typically as a JWT) that includes your Forest Admin username as a custom claim. You must configure your authentication provider to include this custom claim in the token response. See Auth0 documentation: .

  3. The signed token is received by your Forest Admin API and stored in memory or in a database if the user needs to make additional authenticated calls to your API backend. Future calls using this token should inspect the expiry date of the token to determine if a token refresh is required.

  4. Finally, the Forest Admin API makes a call to your API backend, including the signed token received from the authentication provider in step 2. Your API validates the signed token and inspects the custom claims to retrieve the username of the Forest Admin user. This username should either match or be mapped to an appropriate user entry in your backend so that the correct authorization (via role or access control restrictions) can be applied.

Sample Code

In the following example, we override the CREATE user route to authenticate the Forest Admin user against our own API backend protected via OAuth.

//routes/users.js

const { createUser } = require('../services/your-api')
...

// Create a User
router.post('/user', permissionMiddlewareCreator.create(), (request, response, next) => {
  const recordSerializer = new RecordSerializer(User)
  createUser(request, request.body.data.attributes).then(async (response) => {
    response.send(await recordSerializer.serialize(response.body.data))
  }).catch(error => {
    response.status(400).send({ error: error.message })
  })
})
// service/your-api.js

require('dotenv').config();
const got = require('got');

// userAccessTokens will store any retrieved tokens mapped to Forest Admin usernames
const userAccessTokens = {};
const oAuthClient = got.extend({
  prefixUrl: process.env.YOUR_OAUTH_DOMAIN,
  headers: {
    'user-agent': 'forest-admin',
    'content-type': 'application/json',
  },
  responseType: 'json',
});

const getToken = async (username) => {
  return oAuthClient.post('oauth/token', {
    json: {
      client_id: process.env.YOUR_API_CLIENT_ID,
      client_secret: process.env.YOUR_API_CLIENT_SECRET,
      audience: process.env.YOUR_API_AUDIENCE,
      grant_type: 'client_credentials',
      forest_admin_username: username,
    },
  })
    .then((response) => {
      const now = new Date();
      return {
        accessToken: response.body.access_token,
        expiryDate: now.setSeconds(now.getSeconds() + response.body.expires_in),
      };
    })
    .catch((error) => {
      throw error;
    });
};

const tokenDetailsForUser = async (username) => {
  if (!(username in userAccessTokens)
    || userAccessTokens[username].expiryDate === undefined
    || userAccessTokens[username].expiryDate < new Date()) {
    // if there is no stored token or the expiry date has already passed
    // get a token fro your oAuth provider
    return getToken(username)
      .then((tokenDetails) => {
        userAccessTokens[username] = tokenDetails;
        return tokenDetails;
      })
      .catch((error) => {
        throw error;
      });
  }

  // otherwise, use the stored token
  return userAccessTokens[username];
};

// This is an API Client that will call your backend
// Before each request, it is configured to add the correct
// user access token (a signed JWT) to the header so that
// your backend API can inspect for the username
const YourAPIClient = got.extend({
  prefixUrl: process.env.YOUR_API_BASE_URL,
  headers: {
    'user-agent': 'forest-admin',
    'content-type': 'application/json',
  },
  responseType: 'json',
  hooks: {
    beforeRequest: [
      async (options) => {
        try {
          const userAccessTokenDetails = await tokenDetailsForUser(options.context.user);
          options.headers.authorization = `Bearer ${userAccessTokenDetails.accessToken}`;
        } catch (error) {
          throw new Error('Unable to fetch access token.');
        }
      },
    ],
  },
});

const createUser = async (request, data) => {
  return YourAPIClient.post('users', {
    context: {
      user: request.user.email,
    },
    json: data,
  })
    .then((response) => {
      console.log(response.body);
      return response;
    })
    .catch((error) => {
      console.log('error creating user');
      throw error;
    });
};
module.exports = {
  createUser,
};
https://tools.ietf.org/html/rfc6749#section-4.4
Smart Action
route override
request.user.email
https://auth0.com/docs/flows/guides/client-credentials/call-api-client-credentials
https://auth0.com/docs/scopes/current/sample-use-cases#add-custom-claims-to-a-token