prose :: and :: conz


Integrating Dropbox into a Lift app

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.

  1. Sign up for a Dropbox account, if you don’t already have one.
  2. Once you’re signed in navigate to the App Console.
  3. Mash the Create app button on the right to get to the Dropbox Platform app creation page.
  4. Select the Dropbox API app radio button on the right.
  5. Select the radio button for Files and datastores
  6. Select the radio button for Yes – My app only needs access to files it creates
  7. Enter the name you want to use for your application. I’ve used Awesome-App because surely no one has done that yet, right?
  8. For good measure, this is what you should see:
    create_app

    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.

  9. Enter http://localhost:8080/auth/dropbox/callback into the OAuth redirect URIs box and press the Add button.
  10. 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.

  11. 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.

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.

  1. Add the module to your build file. Version 0.8 or later is needed for Dropbox support:
  2. libraryDependencies ++= {
      val liftVersion = "2.5.1"
      val liftEdition = "2.5"
      Seq(
        "net.liftweb"      %% "lift-webkit"             % liftVersion  % "compile",
        "net.liftmodules"  %% ("omniauth_"+liftEdition) % "0.8"        % "compile"
        // ...
      )
    }
  3. Append the OmniAuth’s site map to your siteMap:
  4.   def menus = List(
        home.menu
        // ...
      ) ++ omniauth.Omniauth.sitemap
    
      def siteMap: SiteMap = SiteMap(menus:_*)
    
  5. Add OmniAuth’s init method to your Boot:
  6. package bootstrap.liftweb
    
    class Boot extends Loggable {
      def boot {
        omniauth.Omniauth.init
        // ...
      }
    }
    
  7. Add these properties to src/main/resources/props/default.props:
  8. 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.

  9. Add menu location for success URL to site map which grabs the authentication token:
  10.   // 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:_*)
    
  11. Provide a link or a redirect to the authentication URL, /auth/dropbox/signin:
  12. <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.

  1. Add the Dropbox API to your build file:
  2. 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"
        // ...
      )
    }
  3. Create a DbxClient object, passing the user’s Dropbox OAuth token from the SessionVar we created earlier:
  4. import com.dropbox.core._
    
    DbxToken.get.map { auth =>
      val client = new DbxClient(
        new DbxRequestConfig(
          "Awesome-App", 
          S.locale.toString), 
        auth.token)
    
      // ...
    }
    
  5. Reference the Dropbox API for DbxClient and write code for whatever you want to do. For instance, create a new folder:
  6. 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.


Olde Comments
  1. techano says:

    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

    • techano says:

      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!

Tagged with: scala (41), functional-programming (31), web-development (19), liftweb (9), tutorial (5), dropbox (2)