(You can view the complete sample app on GitHub if you have trouble following along here.)
Our sample app is comprised of two Ruby source files:
evernote_config.rb
, which contains a few bits of configuration information.
en_oauth.rb
, the application source code.
Configuration
evernote_config.rb
will look something like this:
The two require
statements at the top are for the Ruby OAuth gem, followed by the evernote_oauth
gem. This gem depends on the oauth
gem and implements the entire Evernote Cloud API, plus additional OAuth-related functionality.
Below that, we have our Consumer Key and Consumer Secret values (which should be set to the corresponding parts of your Evernote Cloud API Key).
Finally, we have a boolean variable SANDBOX
which controls whether our app will connect to the Sandbox development server. If false, our app would connect to the production Evernote service.
Our App’s Components
Within en_oauth.rb
, we have a few different pieces of our application: the before
filter, the helpers
functions, our get
routes and a pair of simple ERB templates.
First, let’s cover what I’m lovingly calling “the preamble”:
First, we include the Sinatra gem and enable cookie-based sessions (part of Sinatra). Second, we include the current directory in the $LOAD_PATH
(so we can load our configuration file). Finally, we load said configuration file. Pretty straightforward.
Now, on with the meat of the app…
before
This filter runs when the app is initialized and verifies that the OAUTH_CONSUMER_KEY
and OAUTH_CONSUMER_SECRET
constants have been set in evernote_config.rb
. If either constant is initialized to empty string, the error message is displayed:
helpers
A collection of plain Ruby functions that will be used in our application. Many of them simply represent variables that we’ve abstracted out of the routes portion of the application. As it happens, this section also contains the calls to the Evernote Cloud API. Let’s quickly look at each of these:
auth_token
stores the authentication token we’ll be using to make calls against the Evernote Cloud API. Because our web app is necessarily stateless, this value is stored in Sinatra's session
:
client
represents the instance of EvernoteOAuth::Client
. It is instantiated using auth_token
(if we have one), OAUTH_CONSUMER_KEY
, OAUTH_CONSUMER_SECRET
and SANDBOX
.
user_store
and note_store
are the two services of the Evernote Cloud API: the former deals with user and account information, the latter with the user’s notes, notebooks, etc.
en_user
represents a single Evernote User
instance retrieved from the Evernote Cloud API.
notebooks
contains a collection of Notebook
objects as returned by the Evernote Cloud API when NoteStore.listNotebooks()
is called.
total_note_count
is the only function in our app that performs logic beyond simply returning a value and deserves a little more explanation…
We begin with filter
, a new (empty) instance of NoteFilter
. Since we want to query the entire account, we won’t be adding any additional filtering criteria (see the full NoteFilter
definition here).
Next, we call findNoteCounts
, passing our auth_token
and filter
as parameters (the false
parameter indicates that we don’t want notes from the account’s Trash container). findNoteCounts
returns an instance of NoteCollectionCounts
— a hash of notebook GUIDs mapped to the number of notes in the corresponding notebook. Iterating over all of our notebooks, we ask counts
(our instance of NoteCollectionCounts
) for the number of notes in that notebook—using its GUID—and return the total of these counts.
get
Routes
Each instance of get
in our app represents an HTTP request and a corresponding URL pattern. Our application has a total of seven routes. Four of them deal directly with our OAuth implementation and will be discussed in the next section. The remaining three are:
The root of our site, this route simply displays the index
template (defined as @@index
at the end of en_auth.rb
) and prompts the user to begin the OAuth authentication process.
In this route, we first clear the session
object (which is used to track the user’s progress and state across a necessarily stateless web interaction), then redirect the user to /
so they can begin the process again.
This method collects various information about the user’s account (using methods defined in the helpers
section of our app), adds them to the current session
object and renders the index
template. If there are any problems, the rescue
block of our code is triggered and the @last_error
variable is populated with the cause of the error and is sent to the error
template (also defined at the end of en_auth.rb
).
OAuth
As with most OAuth flows, our authentication takes places over a series of steps:
- Retrieve a request token from the Evernote Cloud API. This token is used to request an authentication token in the next step.
- Using the request token, send the user to the Evernote site where they will authenticate with their login credentials and authorize our application to access their account.
- The Evernote website will redirect the user back to our defined callback URL and append, among other things, the authentication token we will use when making calls against the Evernote Cloud API.
This process is somewhat automated in our sample application (at least, in terms of user interaction). That is to say, as each step of the process is completed, the next step will be invoked automatically using Sinatra’s redirect
facility.
Here, we ask the Evernote Cloud API for our temporary request token.
To start, we take the current URL as defined by the request
object’s url
member, remove the requesttoken
portion and replace it with callback
to build our callback URL. Then, we call client.authentication_request_token
to request our token (and sending the callback_url
variable to tell the Evernote Cloud API where to send the response. If all of this goes well, the session[:request_token]
is populated with the token value and the user is scuttled off to /authorize
:
Once we verify that session[:request_token]
exists, we grab the authorize_url
from the request token and send the user there where they’ll be prompted to login with their Evernote account credentials and authorize our application to access their account. If session[:requesttoken]
is not set, the user will see an error and be prompted to start the process over.
After successfully authenticating with Evernote and authorizing our app, they’re sent to /callback
(which we defined during the /requesttoken
route):
When the user is sent back to our callback URL, we verify that either:
- The
oauth_verifier
parameter is set in the URL.
- The
request_token
value is already present in the session
.
If one of these two conditions is true, we include the oauth_verifier
parameter in our session
. If neither of these conditions is true, the user is shown an error.
Next, we extract the access token (used to make API calls) from the and add it to session
. If it’s not there, an error is displayed. If it is, we send the user to the /list
route we defined earlier. Assuming everything worked as advertised, the user will see their username, number of notes in their account and the name of each notebook in their account.
Templates
Our app utilizes two ERB templates: index
and error
:
Clearly, this is a very simple template. First, we allow the user to authenticate with the Evernote Cloud API. This is displayed no matter what. If session[:notebooks]
is defined, we can be reasonably sure that the user’s data has been retrieved from the Evernote Cloud API. Then we display each piece of data.
Even simpler than index
is error
, which displays the value of @last_error
and prompts the user to reset their session and begin the process again.