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.
-
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 ownhttpsngrokURL (example: "https://476322de.ngrok.io/callback") or a secure public URL if you have one
- [Install required] Node.js and the
npmpackage manager - OAuth
- ngrok
- Dropbox API
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
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.
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:
$ cd server
$ npm installYou 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.
mv ~/Downloads/ngrok ./Then we run it:
./ngrok http 8000Now 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.
Enter the required credentials in public/config.js. You'll need:
- Your Dropbox API key
- Your Dropbox API secret
- Your
ngrokpublic URL
const 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.
After completing the configuration steps, start the server from the server folder:
$ npm start
Now you have a running server with an HTTPS endpoint and your Dropbox credentials ready to go.
Now we can get back to the XD plugin side of things!
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:
"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):
async 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.
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:
const { Text, Color } = require("scenegraph");Now the Text and Color classes are required in and ready to be used.
Your plugin will also need to know your public URL. Since we used ngrok earlier, we'll make a constant with that URL:
const publicUrl = "YOUR-PUBLIC-URL"; // e.g. https://476322de.ngrok.io/This url will be used to send requests to your server.
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.
let accessToken;We'll assign the value later.
// XHR helper function
function 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
urlwith the specifiedmethod
We'll make an XHR request.
const 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:
/* 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.jsonwith the id
To open the machine's default browser from an XD plugin, we can use UXP's shell module:
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.
app.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!' });
});
})/loginroute grabs therequestIdfrom the query parameter- and redirects to the Dropbox's
authorizeendpoint and pass therequestIdto the optional parameter,state. This redirect will prompt the login screen on the user's browser - Once the dropbox API returns the
codeto the specified callback endpoint,/callback, which then parses thecodeand therequestId - Set
requestOptionsobject with Dropbox's token URI - Use the
requestlibrary to send thePOSTrequest - Store the access token received from Dropbox in the session object
- Simulate writing to a database by paring the access token with
requestIdand storing it torequestIdsglobal object
accessToken = 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.
// create the dialog
let dialog = document.createElement("dialog"); // [1]
// main container
let container = document.createElement("div"); // [2]
container.style.minWidth = 400;
container.style.padding = 40;
// add content
let title = document.createElement("h3"); // [3]
title.style.padding = 20;
title.textContent = `XD and Dropbox are now connected`;
container.appendChild(title);
// close button
let 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
dialogelement is the modal window that pops down in XD - Create a container
divelement - Create a
h3element 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
showModalmethod to show the modal
const 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
accessTokenis included in this Dropbox API call to retrieve the current account's profile xhrRequesthelper function is used again to make thisPOSTcall
const 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
Textinstance in XD - Populate the text with the stringified version of the profile
jsonobject - Add the
styleRangesfor the text - Insert the text
- Move the text inside the artboard to make it visible
Ready to explore further? Take a look at our other resources: