I recently needed to add Dropbox support to a Lift app that I’m working on, and I didn’t find a full lock-and-key solution. After a little research and contributing to liftmodules omniauth, I was able to successfully write a file from my Lift app to my Dropbox account. After reading this blog post, you will know how to build a Lift application that allows a user to log into a Dropbox account, giving your application access to the files in a folder dedicated to the application. As with all of my tutorial posts, I’ve written it to be thorough enough for a newbie to follow.
Before we dive in, let me give a brief overview of how all this works. You will register your application with Dropbox. There will be some authentication keys associated with it which your Lift application will utilize to identify itself. When I user is redirected to the link to log into Dropbox, the lift-omniauth module will use the keys and communicate with Dropbox to get a token for the user. You then pickup the token from the Omniauth API and create a Dropbox API client. With this client, you have access to the Dropbox folder for your application to read and write files as needed.
Create an application in Dropbox
First thing is to register an application with Dropbox. This lets them know of your intention to have users authenticate and grant you access to their Dropboxes.
- Sign up for a Dropbox account, if you don’t already have one.
- Once you’re signed in navigate to the App Console.
- Mash the Create app button on the right to get to the Dropbox Platform app creation page.
- Select the Dropbox API app radio button on the right.
- Select the radio button for Files and datastores
- Select the radio button for Yes – My app only needs access to files it creates
- Enter the name you want to use for your application. I’ve used Awesome-App because surely no one has done that yet, right?
- Enter
http://localhost:8080/auth/dropbox/callback
into the OAuth redirect URIs box and press the Add button. - OPTIONAL: If you plan to have other people play with your application while in development, be sure to click the Enable additional users button. This will allow up to 100 Dropbox users to utilize your application.
For good measure, this is what you should see:
Once you submit the new application, you will get the config page for your app. The last thing we need to do in the Dropbox UI is add your redirect URIs.
You’re welcome to add all of the URIs you plan to use, with the auth/dropbox/callback
path. Just note that they must all be https URIs.
Be sure to leave this page open. We’ll need the App key and App secret in a few moments.
Add and configure lift-omniauth module
Now we will update your Lift project to utilize the lift-omniauth module. After you complete this section, the users will be able to log into the Dropbox UI and your application will be given an OAuth token for communicating with the Dropbox API.
- Add the module to your build file. Version 0.8 or later is needed for Dropbox support:
- Append the OmniAuth’s site map to your
siteMap
: - Add OmniAuth’s
init
method to yourBoot
: - Add these properties to
src/main/resources/props/default.props
: - Add menu location for success URL to site map which grabs the authentication token:
- Provide a link or a redirect to the authentication URL,
/auth/dropbox/signin
:
libraryDependencies ++= { val liftVersion = "2.5.1" val liftEdition = "2.5" Seq( "net.liftweb" %% "lift-webkit" % liftVersion % "compile", "net.liftmodules" %% ("omniauth_"+liftEdition) % "0.8" % "compile" // ... ) }
def menus = List( home.menu // ... ) ++ omniauth.Omniauth.sitemap def siteMap: SiteMap = SiteMap(menus:_*)
package bootstrap.liftweb class Boot extends Loggable { def boot { omniauth.Omniauth.init // ... } }
omniauth.dropboxkey=[App key] omniauth.dropboxsecret=[App secret] omniauth.baseurl=http://localhost:8080/ omniauth.successurl=/dbx omniauth.failureurl=/error
For successurl
and failureurl
, substitute as appropriate for your site. These are the URLs the user will be directed to after his interactions with the Dropbox login screen.
// We will keep the token in a session variable. object DbxToken extends SessionVar[Option[omniauth.AuthInfo]](None) // Get the token and place it in session var. val getDbxToken = EarlyResponse(() => { omniauth.Omniauth.currentAuth.map { a => DbxToken(a) } S.redirectTo("/") }) // Create menu item for the successurl with getDbxToken val dropbox = Menu(Loc( "Dropbox Authenticated", List("dbx"), // Must match omniauth.successurl S.?("dropbox"), getDbxToken )) // Add this dropbox menu to the site map def menus = List( home.menu, dropbox.menu // ... ) ++ omniauth.Omniauth.sitemap def siteMap: SiteMap = SiteMap(menus:_*)
<a href="/auth/dropbox/signin">Log into Dropbox</a>
Or perhaps…
S.redirectTo("/auth/dropbox/signin")
Do Dropbox stuff
Now that you have the user’s token, you can call the Dropbox API.
- Add the Dropbox API to your build file:
- Create a
DbxClient
object, passing the user’s Dropbox OAuth token from theSessionVar
we created earlier: - Reference the Dropbox API for
DbxClient
and write code for whatever you want to do. For instance, create a new folder:
libraryDependencies ++= { val liftVersion = "2.5.1" val liftEdition = "2.5" Seq( "net.liftweb" %% "lift-webkit" % liftVersion % "compile", "net.liftmodules" %% ("omniauth_"+liftEdition) % "0.8" % "compile", "com.dropbox.core" % "dropbox-core-sdk" % "1.7.6" % "compile" // ... ) }
import com.dropbox.core._ DbxToken.get.map { auth => val client = new DbxClient( new DbxRequestConfig( "Awesome-App", S.locale.toString), auth.token) // ... }
import com.dropbox.core._ import scala.collection.JavaConverters._ import java.io.ByteArrayInputStream DbxToken.get.map { auth => val client = // ... // Get the folders val folders = for { f <- client.getMetadataWithChildren("/").children.asScala if f.isFolder } yield f.asFolder // Make sure there's a place for the awesome val awesomeFolder = folders.find(_.name contains "awesome") .getOrElse(client createFolder "/awesome-stuph") // Create some awesome stuph val awesomeStuph = "This stuph is awesome." .getBytes("UTF-8") // Upload the awesome to Dropbox client.uploadFile( awesomeFolder.path + "/the-awesome.txt", DbxWriteMode.force(), awesomeStuph.length, new ByteArrayInputStream(awesomeStuph) ) }
That’s it! At this point, you are wired up to Dropbox and ready to create your application. The next step for most folks at this point will be to read into the aforementioned Dropbox Java API to know what all can be done.
Excellent article and thank you for taking the time to document all of this.
While trying to implement I ran into a number of issues — the most interesting one was as follows:
Apparently this does NOT work:
Omniauth.siteAuthBaseUrl = “http://localhost:8080/”
Omniauth.successRedirect = “/dbx”
Omniauth.failureRedirect = “/error”
Omniauth.initWithProviders(List(new DropboxProvider(“MyKey”, “MySecret”)))
And amusingly this DOES work
Omniauth.initWithProviders(List(new DropboxProvider(“MyKey”, “MySecret”)))
Omniauth.siteAuthBaseUrl = “http://localhost:8080/”
Omniauth.successRedirect = “/dbx”
Omniauth.failureRedirect = “/error”
It is not clear that these are sequentially constrained and must appear in this order. Thankfully this is open source and peaking at the code shed light as to why.
The second challenge was the fact that it appears the default Lift example “sites” do not properly pick up defaults.prop
Anyone else having trouble implementing this then my trials and tribulations on this module, for what they are worth, are mapped out into the Lift Group here: https://groups.google.com/forum/#!searchin/liftweb/omni$20auth/liftweb/2DSPYEFdI1s/tNGiR3lAmdUJ
One more thing that tripped me up — my properties file did not load from the “standard place”
src/main/resources/props/default.props:
Instead I had to use
src/main/resources/default.props:
You can check that it is working with something like this
Props.requireOrDie(“omniauth.baseurl”)
early in the Boot.scala file and you can also see what mode you are in by adding this line:
println(net.liftweb.util.Props.mode)
Lift presumes that you want to define the differences between run modes (development versus production) in external files like this rather than active decisions in code at runtime.
If the defaults don’t work (and I was working with the basic lift demo app as supplied directly from GitHub and sadly mine didn’t) then you might get side-tracked into thinking active code is a better choice.
One more thing — you’ll need a valid certificate on your site to move to production, plus both http and https paths to the server if in the cloud (or at least at your firewall), as the various providers (noted in the article) will not allow for non-https connections. That, as they say, is another kettle of fish entirely but just be prepared for it as you’ll really really really want to score your first twitter loving promptly once dev is working :)
Next I’m currently looking at a way to implement a decision tree based on which service is being asked for so that “real code” can be called and some “real work” gets done… (My use case sees multiple profiles potentially being used for user participation — e.g. login with both twitter and Google+ for example)
Wish me luck!