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.
Familiarity with your OS's command line application
Familiarity with OAuth
A registered app on Dropbox with the following settings:
- Choose "Dropbox API"
- Choose "Full Dropbox" for the access type
- In
Redirect URIs
, add your ownhttps
ngrok
URL (example: "https://476322de.ngrok.io/callback") or a secure public URL if you have one
Technology Used
- [Install required][Node.js](https://nodejs.org/en/) and the
npm
package manager - OAuth
- ngrok
- 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:
- The XD plugin pings the server to get the session ID
- The server returns a unique ID for the user's XD session
- Plugin opens a tab in user's default browser with a URL pointing to an endpoint on the server
- The server handles the entire OAuth code grant workflow
- The user gives necessary permissions to the plugin
- The server saves the access token paired with the session ID
- 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
- 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$ cd server$ 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 clipboardmv ~/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 clipboardconst dropboxApiKey = "YOUR-DROPBOX-API-KEY";const dropboxApiSecret = "YOUR-DROPBOX-SECRET";const publicUrl = "YOUR-PUBLIC-URL"; // e.g. https://476322de.ngrok.io/try {if (module) {module.exports = {dropboxApiKey: dropboxApiKey,dropboxApiSecret: dropboxApiSecret,publicUrl: publicUrl,};}} catch (err) {console.log(err);}
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"uiEntryPoints": [{"type": "menu","label": "How to Integrate with OAuth (Must run Server first)","commandId": "launchOAuth"}]
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 clipboardasync function launchOAuth(selection) {// The body of this function is added later}module.exports = {commands: {launchOAuth,},};
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 clipboardconst { 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 clipboardconst 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 clipboardlet accessToken;
We'll assign the value later.
5. Write a helper function for XHR requests
Copied to your clipboard// XHR helper functionfunction xhrRequest(url, method) {return new Promise((resolve, reject) => {// [1]const req = new XMLHttpRequest();req.timeout = 6000; // [2]req.onload = () => {if (req.status === 200) {try {resolve(req.response); // [3]} catch (err) {reject(`Couldn't parse response. ${err.message}, ${req.response}`);}} else {reject(`Request had an error: ${req.status}`);}};req.ontimeout = () => {console.log("polling.."); // [4]resolve(xhrRequest(url, method));};req.onerror = (err) => {console.log(err);reject(err);};req.open(method, url, true); // [5]req.responseType = "json";req.send();});}
- This helper function returns a promise object
- Request timeout is set to 6000 miliseconds
- On a successful request, the promise is resolved with
req.response
. In any other scenarios, the promise is rejected - If the request was timed out after 6000 miliseconds, the function loops and keeps sending XHR request until the response is received
- The function sends the request to the specified
url
with the specifiedmethod
6. Get the session ID
We'll make an XHR request.
Copied to your clipboardconst rid = await xhrRequest(`${publicUrl}/getRequestId`, "GET").then((response) => {return response.id;});
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/* Authorized Request IDs (simulating database) */const requestIds = {}; // [1]app.get("/getRequestId", function (req, res) {/* Simulating writing to a database */for (let i = 1; i < 100; i++) {// [2]if (!(i in requestIds)) {requestIds[i] = {};console.log(i);res.json({ id: i });break;}}});
- 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 - 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 clipboardrequire("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 clipboardapp.get("/login", function (req, res) {let requestId = req.query.requestId; // [1]/* This will prompt user with the Dropbox auth screen */res.redirect(`https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=${dropboxApiKey}&redirect_uri=${publicUrl}/callback&state=${requestId}`); // [2]});app.get("/callback", function (req, res) {/* Retrieve authorization code from request */let code = req.query.code; // [3]let requestId = req.query.state;/* Set options with required paramters */let requestOptions = {// [4]uri: `https://api.dropboxapi.com/oauth2/token?grant_type=authorization_code&code=${code}&client_id=${dropboxApiKey}&client_secret=${dropboxApiSecret}&redirect_uri=${publicUrl}/callback`,method: "POST",json: true,};/* Send a POST request using the request library */request(requestOptions) // [5].then(function (response) {/* Store the token in req.session.token */req.session.token = response.access_token; // [6]/* Simulating writing to a database */requestIds[requestId]["accessToken"] = response.access_token; // [7]res.end();}).catch(function (error) {res.json({ response: "Log in failed!" });});});
/login
route grabs therequestId
from the query parameter- and redirects to the Dropbox's
authorize
endpoint and pass therequestId
to the optional parameter,state
. This redirect will prompt the login screen on the user's browser - Once the dropbox API returns the
code
to the specified callback endpoint,/callback
, which then parses thecode
and therequestId
- Set
requestOptions
object with Dropbox's token URI - Use the
request
library to send thePOST
request - Store the access token received from Dropbox in the session object
- Simulate writing to a database by paring the access token with
requestId
and storing it torequestIds
global object
8. Poll the server until access token is received
Copied to your clipboardaccessToken = await xhrRequest(`${publicUrl}/getCredentials?requestId=${rid}`,"GET").then((tokenResponse) => {return tokenResponse.accessToken;});
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// create the dialoglet dialog = document.createElement("dialog"); // [1]// main containerlet container = document.createElement("div"); // [2]container.style.minWidth = 400;container.style.padding = 40;// add contentlet title = document.createElement("h3"); // [3]title.style.padding = 20;title.textContent = `XD and Dropbox are now connected`;container.appendChild(title);// close buttonlet closeButton = document.createElement("button"); // [4]closeButton.textContent = "Got it!";container.appendChild(closeButton);closeButton.onclick = (e) => {// [5]dialog.close();};document.body.appendChild(dialog); // [6]dialog.appendChild(container);dialog.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
- The
dialog
element is the modal window that pops down in XD - Create a container
div
element - Create a
h3
element to let the user know the auth workflow has been completed - You need at least one exit point. Create a close button and add it to the container
- Create a listener for the click event and close the dialog
- 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 clipboardconst dropboxProfileUrl = `https://api.dropboxapi.com/2/users/get_current_account?authorization=Bearer%20${accessToken}`; // [1]const dropboxProfile = await xhrRequest(dropboxProfileUrl, "POST"); // [2]
- Note that received
accessToken
is included in this Dropbox API call to retrieve the current account's profile xhrRequest
helper function is used again to make thisPOST
call
10. Create a text element to show the profile information inside the current artboard
Copied to your clipboardconst text = new Text(); // [1]text.text = JSON.stringify(dropboxProfile); // [2]text.styleRanges = [// [3]{length: text.text.length,fill: new Color("#0000ff"),fontSize: 10,},];selection.insertionParent.addChild(text); // [4]text.moveInParentCoordinates(100, 100); // [5]
- Create a new
Text
instance in XD - Populate the text with the stringified version of the profile
json
object - Add the
styleRanges
for the text - Insert the text
- Move the text inside the artboard to make it visible