OAuth 2.0 User-Agent Flow and React JS app integration

JESCY QUERIMITJESCY QUERIMIT
6 min read

User-Agent is one of Salesforce OAuth flows that allows client applications, like UI apps built with React JS, to access Salesforce's protected resources and data.

The User-Agent flow uses the OAuth 2.0 implicit grant type, which depends on the resource owner's presence and a redirection URI. In this flow, the access token is included in the redirection URI and can be extracted from the URL's hash fragment.

The User-Agent flow follows these steps:

  1. The user opens the client app.

  2. The user is redirected to the Salesforce login page.

  3. After logging in successfully, the user is taken to the approval page, which displays the connected app scopes.

  4. The user approves access to Salesforce by clicking the "Allow" button.

  5. The user is redirected to the callback URL (defined in the Salesforce connected app), which includes the access token in the URL fragment.

  6. The client app uses the access token to access Salesforce data on the user's behalf.

For more details about the Salesforce User-Agent flow, check this documentation by Salesforce: User-Agent.

Connected App in Salesforce

Before we dive into the client app, let's first create the Connected App in Salesforce. Go to Setup, type "App Manager" in the search box, and click "App Manager." Click the "New Connected App" button and provide the following:

  1. Connected App Name and Contact Email

  2. Enable OAuth Settings

  3. Callback URL. My app is running locally on port 3006 and I intend to use one of the route, /token, for the redirect URI.

  4. Select the following scopes:

    1. Manage user data via APIs (api)

    2. Perform requests at any time (refresh_token, offline access)

  5. Save it.

  6. Still on the connected app page, click the "Manage Consumer Details" button.

  7. Provide the verification code.

  8. Copy the consumer key. (We don't need the client secret for this flow.)

Demo App

Our demo app consists of a back-end application using Express.js and a front-end application using React.js. All calls to Salesforce, including authentication and REST API requests for data or resources, are handled by the back-end. Our front end does not directly communicate with Salesforce APIs.

Below diagram describes the authentication sequence up to the retrieval and storage of access token which can be used to access a Salesforce Org’s protected resources. (Calls to access resources will be described later in this article.)

Authentication Sequence

App (UI)

Our front end consists of two routes (for now), / (renders HomePage) and /token (renders AuthSuccess).

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import HomePage from "./pages/Home";
import AuthSuccess from "./pages/AuthSuccess";

const router = createBrowserRouter([
  { path: '/', element: <HomePage/> },
  { path: '/token', element: <AuthSuccess/>}
]);

function App() {
  return <RouterProvider router={router}/>;
}

export default App;

HomePage (UI)

On the HomePage, when the "Connect to SF" button is clicked, a request is sent to our back-end API, which returns an HTML payload. This response payload from back end is in text/html format and, when rendered, displays a login page in the browser.

const handleClick = () => {
    fetch('http://localhost:3000/oauth2/user-agent')
      .then(async response => {
        const htmlStr = await response.text();
        console.log(htmlStr);
        const w = window.open();
        w?.document.write(htmlStr);
      }).catch(error => console.error("Error fetching data:", error));
  };

POST /authorize (Postman)

To get the access token, the user must authenticate and approve the access first. This can be done by sending a GET request to /authorize endpoint to launch the Salesforce authenticate/ authorize page. The request must pass the following parameters:

  1. client_id → the Consumer Key of the connected app

  2. response_type → ‘token‘

  3. redirect_uri → the URL where users are redirected after a successful authentication

If the request is successful, an HTML response is returned which client app can render to show the login or authorization page.

See sample request and response below using Postman:

This is the back-end endpoint that our client app calls when the button in the UI is clicked. This sends a POST request to Salesforce, similar to the Postman example above, to start the OAuth authorization flow.

app.get('/oauth2/user-agent', async (req, res) => {
    const tokentUrl = `${oauthUrl}/authorize`;
    const params = new URLSearchParams();
    params.append('response_type', 'token');
    params.append('client_id', CLIENT_ID);
    params.append('redirect_uri', 'http://localhost:3006/token');
    try {
        const response = await axios.post(tokentUrl, params);
        res.set('Access-Control-Allow-Origin', '*');
        res.set('Content-Type', 'text/html');
        res.send(Buffer.from(response.data));

    } catch (error) {
        console.log('Authentication errors', error);
    }
});

AuthSuccess (frontend)

Clicking Salesforce authorize/ authorization page "Allow" button will redirect the user to the callback URL, which is the AuthSuccess route in our React app. The access token is added to the URL in the hash fragment. Our code then extracts this token and sends it to our back end through the /oauth2/access_token endpoint.

function AuthSuccess() {
    const hashWithAccessToken = window.location.hash;
    if (hashWithAccessToken && hashWithAccessToken.indexOf('access_token')) {
        const access_token = hashWithAccessToken.substring(hashWithAccessToken.indexOf('=')+1, hashWithAccessToken.indexOf('&refresh_token'));

        fetch('http://localhost:3000/oauth2/access_token', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: `{"access_token": "${access_token}"}`
        }).then(result => console.log(result));

    }

    return (
        <div>
            <p>Authentication Successful, you may now close this page.</p>
        </div>
    )
}

export default AuthSuccess;

/oauth2/access_token (backend)

This is the endpoint in our back-end app that saves the access token using a JavaScript closure. Note that this method of saving the access token is temporary, meaning the data is lost when the server restarts. A persistent data approach is not covered in this article, and in-memory storage is sufficient for our demo purposes.

function createInMemoryStore() {
  const dataStore = {};

  return {
    setItem: (key, value) => {
      dataStore[key] = value;
    },
    getItem: (key) => {
      return dataStore[key];
    },
    deleteItem: (key) => {
      delete dataStore[key];
    },
    getAllItems: () => {
      return { ...dataStore };
    },
  };
}

const storage = createInMemoryStore(); 

app.post('/oauth2/access_token', jsonParser, (req, res) => {
    const { access_token } = req.body;
    storage.setItem('access_token', access_token);
    res.set('Content-Type', 'application/json');
    res.send({ message: 'Access token stored successfully!' });
})

Now that we have the access token, we can now use it to access target Salesforce org data/ resources.

/contacts (backend)

For demonstration purposes, I’ve added an endpoint /contacts in our back end that allows us to retrieve contacts from Salesforce. This endpoint calls the Salesforce REST API, using SOQL in the query parameters and the access token in the Authorization header. Since we’ve already stored the access token, we can simply retrieve it from the "closure storage."

app.get('/contacts', async (req, res) => {
  const queryP = {
      q: `SELECT Name, FirstName, LastName, Email, MailingAddress, Title FROM Contact WHERE Title != null ORDER BY Name ASC LIMIT 10`,
    };

    const result = await axios.get(`${TARGET_ORG_ENDPOINT}/services/data/v${API_VERSION}.0/query`, {
      params: queryP,
      headers: { Authorization: `Bearer ${storage.getItem('access_token')}` }
    });

    res.set('Content-Type', 'application/json');
    res.send(result.data);
})

ContactList

I’ve also added another route /contacts so we can display the contact list from the backend. This route is mapped to a new component ContactList, as shown below:

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import HomePage from "./pages/Home";
import AuthSuccess from "./pages/AuthSuccess";
import ContactList from "./pages/ContactList";

const router = createBrowserRouter([
  { path: '/', element: <HomePage/> },
  { path: '/token', element: <AuthSuccess/>},
  { path: '/contacts', element: <ContactList/>},
]);

function App() {
  return <RouterProvider router={router}/>;
}

export default App;

Here’s the ContactList component that fetches Salesforce contacts by calling the backend API /contacts. The result is then displayed as an unordered list of contacts' names and titles.

import { useState, useEffect } from 'react';

function ContactList() {
    const [data, setData] = useState([]);
    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch('http://localhost:3000/contacts');
                const result = await response.json();
                setData(result.records);
            } catch (error) {
                console.error("Error fetching data:", error);
            }
        };
        fetchData();
    }, []);

    return (
        <ul>
            {data.map(item => (
                <li key={item.Id}>
                    {item.Name + ' ' + item.Title}
                </li>
            ))}
        </ul>
    )
}

export default ContactList;

Here’s the list of contacts displayed on the ContactList React component.

The diagram above has been updated with the request to Salesforce resources.

With that, we have completed the integration between Salesforce and our client app using React and Express JS using Oauth 2 user-agent flow. Next, we’ll be exploring web-server flow, one of Salesforce OAuth2 flows.

0
Subscribe to my newsletter

Read articles from JESCY QUERIMIT directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

JESCY QUERIMIT
JESCY QUERIMIT