Home
Software
Writings
Tech note: Dropbox login
in: Diary of a solo dev building a web app
💡
This is a dev diary of implementing Filerion, a web-based file manager for online storage (Dropbox, OneDrive, Google Drive, s3 and more).
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:
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/dropbox@10.30.0"; // 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;
}
Written on Jun 14 2022.
home
Found a mistake, have a comment? Let me know.

Feedback about page:

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