Edit in GitHubLog an issue

How to Integrate with OAuth

This tutorial will show you how to implement the OAuth workflow in an XD plugin, using the Dropbox API as an example.

info Auth workflows are necessarily complex, so this tutorial will be on the longer side and make use of some advanced concepts. Please read the each section carefully, especially the Prerequisites and Configuration sections.

Prerequisites#

  • Basic knowledge of HTML, CSS, and JavaScript.

  • Quick Start Tutorial

  • Debugging Tutorial

  • Familiarity with your OS's command line application

  • Familiarity with OAuth

  • A registered app on Dropbox with the following settings:

    1. Choose "Dropbox API"
    2. Choose "Full Dropbox" for the access type
    3. In Redirect URIs, add your own https ngrok URL (example: "https://476322de.ngrok.io/callback") or a secure public URL if you have one

Technology Used#

  1. [Install required][Node.js](https://nodejs.org/en/) and the npm package manager
  2. OAuth
  3. ngrok
  4. Dropbox API

Overview of the OAuth workflow#

There are three parts of this workflow:

  • Your XD plugin
  • Your server endpoints (for this development example, we'll create a local Node.js server)
  • The service providers OAuth endpoints (for this example, the Dropbox API)

The high-level workflow is as follows:

  1. The XD plugin pings the server to get the session ID
  2. The server returns a unique ID for the user's XD session
  3. Plugin opens a tab in user's default browser with a URL pointing to an endpoint on the server
  4. The server handles the entire OAuth code grant workflow
  5. The user gives necessary permissions to the plugin
  6. The server saves the access token paired with the session ID
  7. The plugin polls the server to check if the access token is available for the session ID. If the token is available, the server sends the access token back
  8. The plugin uses the access token to make API calls to the service API

Configuration#

Info Complete code for this plugin can be found on GitHub.

The following steps will help you get the sample code from our GitHub repo up and running.

1. Install Node.js packages#

Inside the sample repo's server folder, there is a package.json file that contains a list of dependencies. Run the following command from the top level directory of the repo to install the dependencies:

Copied to your clipboard
1$ cd server
2$ npm install

2. Use ngrok to create a public SSL URL#

You can use either ngrok to create a public SSL endpoint, or use your own public URL.

To use ngrok, first download it to your machine.

You can run ngrok from anywhere on your machine, but since we're already in the server folder, we'll move ngrok there for convenience.

Copied to your clipboard
mv ~/Downloads/ngrok ./

Then we run it:

Copied to your clipboard
./ngrok http 8000

Now ngrok is forwarding all HTTP requests from port 8000 to a public SSL endpoint.

You can see the forwarding endpoint currently being used in the ngrok terminal output. Note the forwarding endpoint; we'll use it in the next step.

3. Set your API credentials and public URL#

Enter the required credentials in public/config.js. You'll need:

  • Your Dropbox API key
  • Your Dropbox API secret
  • Your ngrok public URL
Copied to your clipboard
1const dropboxApiKey = "YOUR-DROPBOX-API-KEY";
2const dropboxApiSecret = "YOUR-DROPBOX-SECRET";
3const publicUrl = "YOUR-PUBLIC-URL"; // e.g. https://476322de.ngrok.io/
4
5try {
6 if (module) {
7 module.exports = {
8 dropboxApiKey: dropboxApiKey,
9 dropboxApiSecret: dropboxApiSecret,
10 publicUrl: publicUrl,
11 };
12 }
13} catch (err) {
14 console.log(err);
15}

Our server will make use of these settings in a later step.

4. Start the server#

After completing the configuration steps, start the server from the server folder:

Copied to your clipboard
$ npm start

Now you have a running server with an HTTPS endpoint and your Dropbox credentials ready to go.

Development Steps#

Now we can get back to the XD plugin side of things!

1. Create plugin scaffold#

First, edit the manifest file for the plugin you created in our Quick Start Tutorial.

Replace the uiEntryPoints field of the manifest with the following:

Copied to your clipboard
1"uiEntryPoints": [
2 {
3 "type": "menu",
4 "label": "How to Integrate with OAuth (Must run Server first)",
5 "commandId": "launchOAuth"
6 }
7]

If you're curious about what each entry means, see the manifest documentation, where you can also learn about all manifest requirements for a plugin to be published in the XD Plugin Manager.

Then, update your main.js file, mapping the manifest's commandId to a handler function.

Replace the content of your main.js file with the following code (note the presence of the async keyword, which we'll look at in a later step):

Copied to your clipboard
1async function launchOAuth(selection) {
2 // The body of this function is added later
3}
4
5module.exports = {
6 commands: {
7 launchOAuth,
8 },
9};

The remaining steps in this tutorial describe additional edits to the main.js file.

2. Require in XD API dependencies#

For this tutorial, we just need access to two XD scenegraph classes.

Add the following lines to the top of your plugin's top-level main.js file:

Copied to your clipboard
const { Text, Color } = require("scenegraph");

Now the Text and Color classes are required in and ready to be used.

3. Store the public URL#

Your plugin will also need to know your public URL. Since we used ngrok earlier, we'll make a constant with that URL:

Copied to your clipboard
const publicUrl = "YOUR-PUBLIC-URL"; // e.g. https://476322de.ngrok.io/

This url will be used to send requests to your server.

4. Create a variable to store the access token#

Once you receive the access token from your server, you can use the token for API calls as long as the token is stored in memory and the XD session is alive.

Copied to your clipboard
let accessToken;

We'll assign the value later.

5. Write a helper function for XHR requests#

Copied to your clipboard
1// XHR helper function
2function xhrRequest(url, method) {
3 return new Promise((resolve, reject) => {
4 // [1]
5 const req = new XMLHttpRequest();
6 req.timeout = 6000; // [2]
7 req.onload = () => {
8 if (req.status === 200) {
9 try {
10 resolve(req.response); // [3]
11 } catch (err) {
12 reject(`Couldn't parse response. ${err.message}, ${req.response}`);
13 }
14 } else {
15 reject(`Request had an error: ${req.status}`);
16 }
17 };
18 req.ontimeout = () => {
19 console.log("polling.."); // [4]
20 resolve(xhrRequest(url, method));
21 };
22 req.onerror = (err) => {
23 console.log(err);
24 reject(err);
25 };
26 req.open(method, url, true); // [5]
27 req.responseType = "json";
28 req.send();
29 });
30}
  1. This helper function returns a promise object
  2. Request timeout is set to 6000 miliseconds
  3. On a successful request, the promise is resolved with req.response. In any other scenarios, the promise is rejected
  4. If the request was timed out after 6000 miliseconds, the function loops and keeps sending XHR request until the response is received
  5. The function sends the request to the specified url with the specified method

6. Get the session ID#

We'll make an XHR request.

Copied to your clipboard
1const rid = await xhrRequest(`${publicUrl}/getRequestId`, "GET").then(
2 (response) => {
3 return response.id;
4 }
5);

This part of the function sends a GET request to your server's getRequestId endpoint and returns response.id.

Let's take a look at the code on the server side:

Copied to your clipboard
1/* Authorized Request IDs (simulating database) */
2const requestIds = {}; // [1]
3
4app.get("/getRequestId", function (req, res) {
5 /* Simulating writing to a database */
6 for (let i = 1; i < 100; i++) {
7 // [2]
8 if (!(i in requestIds)) {
9 requestIds[i] = {};
10 console.log(i);
11 res.json({ id: i });
12 break;
13 }
14 }
15});
  1. Note that there is a global variable, requestIDs, which is an empty JavaScript object. For the sake of simplicity, we are using this object to simulate a database
  2. This loop function simulates writing to a database by creating a new id, save the id in the global object, and res.json with the id

7. Open the default browser with the URL pointing to your server#

To open the machine's default browser from an XD plugin, we can use UXP's shell module:

Copied to your clipboard
require("uxp").shell.openExternal(`${publicUrl}/login?requestId=${rid}`);

This will open the browser with the url pointing to an endpoint on your server.

Let's take a look at the code on the server side.

Copied to your clipboard
1app.get("/login", function (req, res) {
2 let requestId = req.query.requestId; // [1]
3 /* This will prompt user with the Dropbox auth screen */
4 res.redirect(
5 `https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=${dropboxApiKey}&redirect_uri=${publicUrl}/callback&state=${requestId}`
6 ); // [2]
7});
8
9app.get("/callback", function (req, res) {
10 /* Retrieve authorization code from request */
11 let code = req.query.code; // [3]
12 let requestId = req.query.state;
13
14 /* Set options with required paramters */
15 let requestOptions = {
16 // [4]
17 uri: `https://api.dropboxapi.com/oauth2/token?grant_type=authorization_code&code=${code}&client_id=${dropboxApiKey}&client_secret=${dropboxApiSecret}&redirect_uri=${publicUrl}/callback`,
18 method: "POST",
19 json: true,
20 };
21
22 /* Send a POST request using the request library */
23 request(requestOptions) // [5]
24 .then(function (response) {
25 /* Store the token in req.session.token */
26 req.session.token = response.access_token; // [6]
27
28 /* Simulating writing to a database */
29 requestIds[requestId]["accessToken"] = response.access_token; // [7]
30 res.end();
31 })
32 .catch(function (error) {
33 res.json({ response: "Log in failed!" });
34 });
35});
  1. /login route grabs the requestId from the query parameter
  2. and redirects to the Dropbox's authorize endpoint and pass the requestId to the optional parameter, state. This redirect will prompt the login screen on the user's browser
  3. Once the dropbox API returns the code to the specified callback endpoint, /callback, which then parses the code and the requestId
  4. Set requestOptions object with Dropbox's token URI
  5. Use the request library to send the POST request
  6. Store the access token received from Dropbox in the session object
  7. Simulate writing to a database by paring the access token with requestId and storing it to requestIds global object

8. Poll the server until access token is received#

Copied to your clipboard
1accessToken = await xhrRequest(
2 `${publicUrl}/getCredentials?requestId=${rid}`,
3 "GET"
4).then((tokenResponse) => {
5 return tokenResponse.accessToken;
6});

As noted in step #4, the xhrRequest helper function is designed to poll the server if the initial request is not responded in 6000 miliseconds. Once the user completes the OAuth workflow in the browser, polling should stop and this request should be returned with the access token.

9. Show a dialog indicating the token has been received#

Copied to your clipboard
1// create the dialog
2let dialog = document.createElement("dialog"); // [1]
3
4// main container
5let container = document.createElement("div"); // [2]
6container.style.minWidth = 400;
7container.style.padding = 40;
8
9// add content
10let title = document.createElement("h3"); // [3]
11title.style.padding = 20;
12title.textContent = `XD and Dropbox are now connected`;
13container.appendChild(title);
14
15// close button
16let closeButton = document.createElement("button"); // [4]
17closeButton.textContent = "Got it!";
18container.appendChild(closeButton);
19
20closeButton.onclick = (e) => {
21 // [5]
22 dialog.close();
23};
24
25document.body.appendChild(dialog); // [6]
26dialog.appendChild(container);
27dialog.showModal();

Just like HTML DOM APIs, you can use document.createElement method to create UI objects. Elements have the style property which contains metrics properties you can set

  1. The dialog element is the modal window that pops down in XD
  2. Create a container div element
  3. Create a h3 element to let the user know the auth workflow has been completed
  4. You need at least one exit point. Create a close button and add it to the container
  5. Create a listener for the click event and close the dialog
  6. Attach the dialog to the document, add the container, and use showModal method to show the modal

10. Make an API call to Dropbox#

Copied to your clipboard
1const dropboxProfileUrl = `https://api.dropboxapi.com/2/users/get_current_account?authorization=Bearer%20${accessToken}`; // [1]
2const dropboxProfile = await xhrRequest(dropboxProfileUrl, "POST"); // [2]
  1. Note that received accessToken is included in this Dropbox API call to retrieve the current account's profile
  2. xhrRequest helper function is used again to make this POST call

10. Create a text element to show the profile information inside the current artboard#

Copied to your clipboard
1const text = new Text(); // [1]
2text.text = JSON.stringify(dropboxProfile); // [2]
3text.styleRanges = [
4 // [3]
5 {
6 length: text.text.length,
7 fill: new Color("#0000ff"),
8 fontSize: 10,
9 },
10];
11selection.insertionParent.addChild(text); // [4]
12text.moveInParentCoordinates(100, 100); // [5]
  1. Create a new Text instance in XD
  2. Populate the text with the stringified version of the profile json object
  3. Add the styleRanges for the text
  4. Insert the text
  5. Move the text inside the artboard to make it visible
Was this helpful?
Copyright © 2021 Adobe. All rights reserved.