Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: (Classic)

This recipe describes how to connect a KRL application to Dropbox using the OAuth 1.0a protocol that Dropbox supports. This recipe should be readily adaptable to other APIs using OAuth 1.0a and will offer hints at how to do OAuth 2, which is considerably easier to support. 

Warning

If you simply want to use Dropbox in your application, they you should explore the Dropbox Module available on Github, including the accompanying test application. This recipe is intended to show how I built OAuth support for Dropbox in an effort to show how it could also be done for other services.


Info

We will be using the PLAINTEXT signature method in OAuth 1.0a which means that this is only secure over SSL. Since Dropbox uses SSL for all API interactions, this won't be a problem.

Registering Our Application with Dropbox

The first step is to register our application with Dropbox and get our App key and App secret. Go to the Dropbox Developer Home and click on the App Console menu item on the left. Click the "Create an app" button and fill in the fields. We'll be creating a "Core" application. The Core API is what allows programatic access to the linked Dropbox account. You'll be asked whether you want full access to the user's Dropbox or just a single directory in the /Apps directory. For purposes of this recipe, I chose the a single directory. At this point, you should have an App key and App secret from Dropbox. You'll put these in the Keys section of the meta directory of your ruleset:

Code Block
themelanguageConfluencejavascript
languagethemejavascriptConfluence
key dropbox {
 "app_key" : "<dropbox app key>",
 "app_secret" : "<dropbox app secret>"
} 

Creating the OAuth Header Value

We'll have to create an OAuth Authorization header for various interactions with the Dropbox server. This is a complicated string that is better to create using a function than by hand each time it's needed.  For getting the request token, the header is simpler and looks like this:

Code Block
language
languagejavascript
themeConfluencejavascript
Authorization: OAuth oauth_version="1.0", oauth_signature_method="PLAINTEXT", oauth_consumer_key="<app-key>", oauth_signature="<app-secret>&"

For getting the access token and interactions with the API, we need to add the token and token secret like so:

Code Block
languagejavascript
themeConfluencelanguagejavascript
Authorization: OAuth oauth_version="1.0", oauth_signature_method="PLAINTEXT", oauth_consumer_key="<app-key>", oauth_token="<request-token>", oauth_signature="<app-secret>&<request-token-secret>"

This function will create both. If the function is given just two parameters, then it creates the first Authorization header, if given four, it creates the second Authentication header shown above:

Code Block
language
languagejavascript
themeConfluencejavascript
create_oauth_header_value = function(key, key_secret, token, token_secret) {
  'OAuth oauth_version="1.0", oauth_signature_method="PLAINTEXT", oauth_consumer_key="'+ 
  key +
  (token => '", oauth_token="'+token+'", ' | '", ') +
  'oauth_signature="' +
  key_secret +
  '&' +
  token_secret +
  '"'; 
}

We'll use this function often in the step below. 

Get a Request Token

The Dropbox API provides an endpoint for getting request tokens:

Code Block
languagejavascript
themeConfluencelanguagejavascript
POST https://api.dropbox.com/1/oauth/request_token

...

Info
We're writing this app to work within the CloudOS UI framework, so we'll be using CloudOS UI functions to interact with the user. If you're not familiar with writing CloudOS, applications, there is a recipe available in the cookbook.

Getting the access token requires POSTing to the endpoint with the correct Authorization header. The following rule, which is selected when the user launches the application we're writing inside CloudOS and fires when the application is not authorized to access Dropbox does that:

Code Block
languagejavascript
themeConfluence
languagejavascript
 rule get_accessrequest_token { 
  select when oauthweb responsecloudAppSelected
  if(not authorized) then {
    http:post(dropbox_base_url+"/oauth/request_token") with
        body = {} and
        headers = {"Authorization" : create_oauth_header_value(keys:dropbox('app_key'), keys:dropbox('app_secret'))
		  } and
        autoraise = "request_token"
  }    
}

...

When the http:post() action autoraises an event, the event has event domain http and event type post. The value of the autoraise parameter in the action above will be in the event attribute label so that we can write a rule that responds specifically to this autoraise. The following rule is selected by that autoraised event, creates a UI panel with a "Click to Link Dropbox" button, and saves the request token and request token secret in entity variables for later use. 

Code Block
languagejavascript
themeConfluencelanguagejavascript
rule process_request_token {
  select when http post label "request_token"
  pre {
    tokens = decode_content(event:attr('content'));
    callback = 'http://' + meta:host() + '/blue/event/oauth/response/' + meta:rid() + '/' + math:random(999999);
    url = "https://www.dropbox.com/1/oauth/authorize?oauth_token=" + tokens{'oauth_token'} + "&oauth_callback=" + callback;
    my_html = <<
<div style="margin: 0px 0px 20px 20px">
<a href="#{url}" class="btn btn-large btn-primary">Click to Link to Dropbox<a>
</div>
>>;
  }
  CloudRain:createLoadPanel("Link to Dropbox", {}, my_html);
  always {
    set ent:request_token_secret tokens{'oauth_token_secret'};
    set ent:request_token tokens{'oauth_token'};
  }
}

...

When this rule runs, the user will see something like this:

Screen Shot 2013-05-30 at 11.31.07 AM.pngImage Added

After the click, they'll see this dialog at Dropbox:Screen Shot 2013-05-30 at 11.32.52 AM.png

Image Added

When the user clicks "Allow", Dropbox will GET the callback URL that we supplied. This will raise oauth:response event to the current ruleset. 

Getting the Access Token

Because we set the callback URL as an ESL that raises a oauth:response event to the current ruleset, we need a rule that is selected on that event. The rule can use the access token endpoint to retrieve the token and its associated secret. 

Code Block
languagejavascript
themeConfluence
languagejavascript
rule get_access_token {
 select when oauth response
    if(not authorized) then {
 
    http:post(dropbox_base_url+"/oauth/access_token") with
 
      body = {} and
 
      headers = {"Authorization" : create_oauth_header_value(keys:dropbox('app_key'),  	
                                                      keys:dropbox('app_secret'), 
							       ent:request_token, 
							       ent:request_token_secret)
		  } and
 
      autoraise = "access_token"
 
  }     
 }

Like the rule that gets the request token above, this rule POSTs to the Dropbox access endpoint and automatically raises an event to process the response. The primary difference is that we create the Authorization header value with the request token and secret that we stored in the last section to generate a request with a correctly formatted Authorization header. We give the automatically raised even the label access_token so that we can select it in the processing rule.

Processing the access token response  is similar to processing the request token response in that we want to decode the response and store the access token and secret in entity variables. We have to redirect the user's browser to the right URL. The right URL depends on what you're trying to do. If you were using KRL to augment another page, it would be that page's URL and you could simply use the Web action redirect(). In this case, we're working with a CloudOS application running at SquareTag.com, so we need to redirect to the the app created by the current ruleset. We use the recipe for redirecting without runtime support to accomplish that. 

Code Block
languagejavascript
themeConfluencelanguagejavascript
rule process_access_token {
  select when http post label "access_token"
  pre {
    tokens = decode_content(event:attr('content'));
    url = "https://squaretag.com/app.html#!/app/#{meta:rid()}/show";
    js = <<
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title></title>
  <META HTTP-EQUIV="Refresh" CONTENT="0;#{url}">
  <meta name="robots" content="noindex"/>
  <link rel="canonical" href="#{url}"/>
</head>
<body>
<p>
You are being redirected to <a href="#{url}">#{url}</a>
</p>
<script type="text/javascript">
window.location = #{url};
</script>
</body>
</html>
>>;
  }
  send_raw("text/html")
      with content= js
  always {
    set ent:dropbox_uid tokens{'uid'};
    set ent:access_token_secret tokens{'oauth_token_secret'};
    set ent:access_token tokens{'oauth_token'};
  }
}

This rule looks long, but most of it is the redirect HTML stored in the variable named js. The send_raw() action and the rule postlude do the real work. 

Are We Authorized?

If you've been paying attention, you'll notice that most of the preceding rules are conditional on whether or not the user has already authorized the app to use Dropbox. Obviously, we only want the user to see the  authorization flow if the app isn't already authorized. We need to set a variable named authorized in the global block of the ruleset that the rules can test. How this test works depends on the API you're working against. Dropbox provides an account information endpoint that we can call to check if we're authorized:

Code Block
languagejavascript
themeConfluencelanguagejavascript
GET https://api.dropbox.com/1/account/info

We define a function that does a GET on the Dropbox API to help us accomplish this and other API calls:

Code Block
languagejavascript
themeConfluencelanguagejavascript
dropbox_core_api_call = function(method) {
  http:get(dropbox_base_url+method, 
           {},
           {"Authorization" : create_oauth_header_value(keys:dropbox('app_key'), 
	                                                keys:dropbox('app_secret'), 
		 			                ent:access_token, 
							ent:access_token_secret)
           });
}

...

We can use this function to define authorized as follows:

Code Block
language
languagejavascript
themeConfluencejavascript
account_info_result = dropbox_core_api_call('/account/info');
authorized = account_info_result{'status_code'} eq '200';

We make a call for account info and if we get a 200 status code, we assume that we're authorized since Dropbox uses other 4xx and 5xx status codes to indicate authorization and other problems. 

Using the API

Of course, other rules would use the API to provide the needed user experience. This simple rule uses the account information and the metadata endpoints from the Dropbox API along with a CloudOS UI function to paint a simple information screen for the user that shows any files stored for this application at Dropbox:

Code Block
language
languagejavascript
themeConfluencejavascript
rule show_account_info { 
  select when web cloudAppSelected
  pre {
    account_info = account_info_result{'content'}.decode();
    name = account_info{'display_name'};
    uid = account_info{'uid'};


    metadata_result = dropbox_core_api_call('/metadata/sandbox/?list=true');
    metadata = metadata_result{'content'}.decode();
    files = metadata{'contents'}.map(function(x){x{'path'}}).join('<br/>');


    my_html = <<
<div style="margin: 0px 0px 20px 20px">
<p>Your Dropbox name is #{name} and your UID is #{uid}.</p>
<p>Files:<br/>#{files}</p>
</div>
>>;
  }
  if(authorized) then {
    CloudRain:createLoadPanel("Dropbox Account Info", {}, my_html);
  }
}

...

The result looks like this:

Screen Shot 2013-05-30 at 11.33.45 AM.pngImage Added

Conclusion

This recipe has shown how to use the Dropbox API and OAuth 1.0a from within KRL. The OAuth interactions have been handled completely within KRL. This recipe could be easily adapted to any OAuth 1.0a protected resource as long as it supports PLAINTEXT signatures. OAuth 2 is considerably simpler and should be easily doable using the techniques outlined above.