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*asDropboxfrom"https://esm.sh/dropbox@10.30.0";// or you can use local copy of Dropbox SDK
// from https://github.com/dropbox/dropbox-sdk-js
constdropboxClientID="your client id";// you must register you app with Dropbox to get clientID
// create Dropbox client
function(accessToken,refreshTOken){letopts={clientId:dropboxClientID,};if(accessToken){// once you have access token, you'll provide it here:
opts.accessToken=accessToken;opts.refreshTOken=refreshTOken;}returnnewDropbox.Dropbox(opts);}constdbx=getDropboxClient();// the URL on your server that Dropbox will call to complete authorization process
// I chose /auth/dropboxlogin
functiondbxRedirectURL(){constl=window.location;returnl.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"
letauthURL=awaitdbx.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
constlsKeyDrobBoxCodeVerifier="dropbox-code-verifier";letcv=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);asyncfunctionwaitForDropboxToken(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);constresult=JSON.parse(e.newValue);constaccessToken=result.access_token;constrefreshToken=result.refresh_token;// you should persist accessToken and refreshToken in e.g. localStorage
// use them to create authenticated client
constdbx=getDropboxClient(accessToken,refreshToken);}
This is the part that runs in second window
exportfunctioncompleteDropboxAuth(){console.log("completeDropboxAuth: location.search:",window.location.search);// when Dropbox call your callback url it includes: ?code=<encryptedAccessToken>
letcode=parseQueryString(window.location.search).code;constdbx=getDropboxClient();// we stored this value before calling Dropbox
letcv=localStorage.getItem(lsKeyDrobBoxCodeVerifier);dbx.auth.setCodeVerifier(cv);functiononResponse(rsp){letresult=rsp.result;letjs=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
functionparseQueryString(str){constret=Object.create(null);if(typeofstr!=="string"){returnret;}str=str.trim().replace(/^(\?|#|&)/,"");if(!str){returnret;}str.split("&").forEach((param)=>{constparts=param.replace(/\+/g," ").split("=");// Firefox (pre 40) decodes `%3D` to `=`
// https://github.com/sindresorhus/query-string/pull/37
letkey=parts.shift();letval=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;}elseif(Array.isArray(ret[key])){ret[key].push(val);}else{ret[key]=[ret[key],val];}});returnret;}