Home / Diary of a solo dev building a web app / Tech note: Dropbox login edit
Try Documentalist, my app that offers fast, offline access to 190+ programmer API docs.

A tech note is a deeper description of some technical problem I’ve encountered and solved.
Here’s how to implement Dropbox login for web apps.
The high-level overview is:
  • what you need is access token to use to authenticate Dropbox API calls for a given user
  • using Dropbox JavaScript SDK generate authorization URL which is URL on dropbox.com that the user has to navigate to, login to their Dropbox account and authorize your app to access their Dropbox data
  • there are different kinds of authentication flows. For web app you want offline which provides expiring access token and refresh token to get new access token when it expires
  • for offline you also need to provide a code verifier value which is used in web apps instead of client secret (you can’t keep secrets in a web app)
  • that URL includes a redirect URL on your site that Dropbox will call if the user gives your app access
  • part of that URL is code which you can convert to access token using code verifier you provided when constructing the authorization URL
For SPA (Single Page Application) there is a wrinkle: you probably don’t want to loose the state of the app but that’s what will happen if you redirect the user from your website to Dropbox website.
The trick is to open a new web page just for the authentication flow.
But now we have a problem: a new web page is a completely new instance of your app. Once you extract access token from Dropbox callback you need to pass it back to the original app instance.
Turns out we can do it via localStorage. With document.addEventListener("storage", eventHandler) your eventHandler function will be called if there’s a write to localStorage done by a different window so you can send access token simply by storing it in localStorage.
Here are the crucial parts of the code:
import * as Dropbox from "https://esm.sh/[email protected]"; // or you can use local copy of Dropbox SDK
// from https://github.com/dropbox/dropbox-sdk-js
const dropboxClientID = "your client id"; // you must register you app with Dropbox to get clientID

// create Dropbox client
function(accessToken, refreshTOken) {
	let opts = {
		clientId: dropboxClientID,
	};
	if (accessToken) {
		// once you have access token, you'll provide it here:
		opts.accessToken = accessToken;
		opts.refreshTOken = refreshTOken;
	}
	return new Dropbox.Dropbox(opts);
}

const dbx = getDropboxClient();

// the URL on your server that Dropbox will call to complete authorization process
// I chose /auth/dropboxlogin
function dbxRedirectURL() {
    const l = window.location;
    return l.protocol + "//" + l.host + "/auth/dropboxlogin";
}

// create URL on dropbox.com that you'll send the user to authorize your app
// the important part is: "code" and "offline"
let authURL = await dbx.auth.getAuthenticationUrl(
  dbxRedirectURL(),
  undefined,
  "code",
  "offline",
  undefined,
  undefined,
  true
);
// you need to remember code verifier to extract access token from redirect URL
// we'll store it in localStorage
const lsKeyDrobBoxCodeVerifier = "dropbox-code-verifier";
let cv = dbx.auth.codeVerifier;
localStorage.setItem(lsKeyDrobBoxCodeVerifier, cv);

// listen on localStorage changes to complete auth flow when auth code
// is written to local storage
window.addEventListener("storage", waitForDropboxToken);
// open a new window with a dropbox.com URL so that the user authorizes your app
openBrowserWindow(authURL, "Dropbox Login", 840, 760);

async function waitForDropboxToken(e) {
  console.log("waitForDropboxToken", e);
  if (e.key !== lsKeyDropboxToken) {
    return;
  }
	// the other window completed exchange with dropbox.com
	// unsubscribe to not get multiple notifications
  window.removeEventListener("storage", waitForDropboxToken);
	const result = JSON.parse(e.newValue);
  const accessToken = result.access_token;
  const refreshToken = result.refresh_token;	
  // you should persist accessToken and refreshToken in e.g. localStorage
  // use them to create authenticated client
	const dbx = getDropboxClient(accessToken, refreshToken);
}
This is the part that runs in second window
export function completeDropboxAuth() {
  console.log(
    "completeDropboxAuth: location.search:",
    window.location.search
  );
	// when Dropbox call your callback url it includes: ?code=<encryptedAccessToken>
  let code = parseQueryString(window.location.search).code;
  const dbx = getDropboxClient();
	// we stored this value before calling Dropbox
  let cv = localStorage.getItem(lsKeyDrobBoxCodeVerifier);
  dbx.auth.setCodeVerifier(cv);
  function onResponse(rsp) {
		let result = rsp.result;
		let js = JSON.stringify(result);
    // this will trigger waitForDropboxToken in original window
    localStorage.setItem(lsKeyDropboxToken, js);
    window.close();
  }
	// extract accessToken from code
  dbx.auth
    .getAccessTokenFromCode(dbxRedirectURL(), code)
    .then(onResponse)
    .catch(onError);
}

// in my SPA, I dispatch completeDropboxAuth() based on the url. So in my index.html:
if (window.location.pathname == "/auth/dropboxlogin") {
  completeDropboxAuth();
} else {
	// code for other URLs
}
For completeness:
// parseQueryString comes from Dropbox SDK example
// https://github.com/dropbox/dropbox-sdk-js/blob/main/examples/javascript/utils.js#L3
function parseQueryString(str) {
  const ret = Object.create(null);

  if (typeof str !== "string") {
    return ret;
  }

  str = str.trim().replace(/^(\?|#|&)/, "");

  if (!str) {
    return ret;
  }

  str.split("&").forEach((param) => {
    const parts = param.replace(/\+/g, " ").split("=");
    // Firefox (pre 40) decodes `%3D` to `=`
    // https://github.com/sindresorhus/query-string/pull/37
    let key = parts.shift();
    let val = parts.length > 0 ? parts.join("=") : undefined;

    key = decodeURIComponent(key);

    // missing `=` should be `null`:
    // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
    val = val === undefined ? null : decodeURIComponent(val);

    if (ret[key] === undefined) {
      ret[key] = val;
    } else if (Array.isArray(ret[key])) {
      ret[key].push(val);
    } else {
      ret[key] = [ret[key], val];
    }
  });

  return ret;
}

Feedback about page:

Feedback:
Optional: your email if you want me to get back to you:

Need fast, offline access to 190+ programmer API docs? Try my app Documentalist for Windows