A key step in developing an application is handling user authentication and datastore. The use cases of your data may vary, but the process through which the data is stored and accessed is largely the same. OmniAuth is a Ruby gem with several strategies, or provider-specific gems, that provides authentication methods for many systems, such as Facebook, Google, GitHub, etc. Each strategy is a Rack middleware, so it’s very easy to integrate into your web framework, whether that’s Rails, [Sinatra, Padrino, or even a less popular one like [Lotus]. Additionally, the Orchestrate Ruby Client is comprehensive, well-documented, and framework-agnostic.

In this tutorial, we’ll use Sinatra, a popular Ruby web micro-framework and domain-specific language, with OmniAuth Twitter to build an online community titled Twomnistrate where each user shares their favorite phrase as a profile element, stored in Orchestrate.

File/Folder Structure

First, we set up our file/folder directory in a way that Sinatra understands by convention. Our main application file will contain all the Ruby code in this tutorial– app.rb. We’ll include all [ERB templating in our views folder.

orchestrate-finder

Gems

At the top of our main Ruby file, we require the necessary gems for our project.

require 'orchestrate'
require 'sinatra'
require 'omniauth'
require 'omniauth-twitter'

Note that this assumes the two gems are already installed on your system. Generally, I use Bundler to manage my application’s gems, but I won’t include the code for that in this tutorial since there are alternatives, such as dep and just self-managing with gem install.

Settings

Then, we enable one of Sinatra’s many built-in settings, sessions, a fork of Rack::Session::Cookies, “simple cookie based session management”. We will use sessions to keep track of whether a user is logged-in or not.

enable :sessions

APIs

Next, we setup the Orchestrate clients.

The Orchestrate Gem provides two interfaces currently, the method client and the object client. The method client is a solid but basic interface that provides a single entry point to an Orchestrate Application. The object client uses the method client under the hood, and maps Orchestrate’s domain objects (Collections, KeyValues, etc) to Ruby classes, and is still very much in progress.

In this project, we will use both interfaces, as the object client is simpler and includes everything we need except for the delete operation.

app = Orchestrate::Application.new(api_key)
users = app[:users]
client = Orchestrate::Client.new(api_key)

Note that you must define api_key with the API Key of your Orchestrate application, which can be created or revoked from the Orchestrate Dashboard on a per-application basis. I always recommend defining any API keys on a shell-specific level instead of a code-specific level via an environment variable. You can find more information on working with environment variables in Ruby in documentation for the ENV class.

The last thing we need to initialize before writing any application code is OmniAuth with the Twitter strategy.

use OmniAuth::Builder do
 provider :twitter, ENV['CONSUMER_KEY'], ENV['CONSUMER_SECRET']
end

You need to register your app with Twitter. This is easily done – just head over to Twitter Developer Console and login using your Twitter credentials. Then, click on the ‘create a new application’ button and fill in the form. In the callback URL field, you need to append /auth/twitter/callback to whichever URL you used in the website field. If you haven’t configured a domain yet, I recommend that you use ngrok to create a secure introspect tunnel to localhost. For example, ngrok 4567 will create an public subdomain tunnel to your address localhost:4567, which is the default address for Sinatra’s server.

orchestrate-ngork

At this time, our app only needs read-only permissions from Twitter, but this can be changed in the “permissions” tab of your app in the developer console.

orchestrate-twomnistrate

Helper Methods

Before progressing to routes and application logic, we must declare a top-level helper method. Helper methods are available in all route handlers and templates.

helpers do
  def logged_in?
    session[:authed]
  end
end

The logged_in? method checks if the user is logged-in via a session variable. Since the method name ends in a question mark, it must return a boolean value. When the user is logged out, we will define session[:authed] nil which is falsey in Ruby and when the user is logged-in, we will define it true.

Homepage

Our application will start off with a splash page. When the user is logged out, this will be serve as their homepage, but when the user is logged-in, /all will take over this role.

get '/' do
  if logged_in?
    redirect '/all'
  else
    erb :home
  end
end

All Phrases

On /all, we map the users collection to an array of arrays in the format [username, phrase] to @data. Then, we loop through the instance variable in ERB templating to generate an HTML table.

get '/all' do
  @data = users.map {|user| [user.key, user.value['phrase']]}
  erb :all
end
<% @data.each do |user| %>
  <tr>
  <td><%= user[0] %></td>
  <td><%= user[1] %></td>
  </tr>
<% end %>

My Phrases

On /me, users can update their phrase. Unlike /all, this route is restricted to logged-in users.

get '/me' do
  halt(401,'Not Authorized') unless logged_in?
  @phrase = users[session[:username]][:phrase] unless users[session[:username]].nil?
  erb :me
end
<form method="post">
  <label>Phrase</label>
  <input type="text" name="phrase" value="<%= @phrase %>">
  <input type="submit" value="Submit">
</form>

User Input

Looking back at the form on /me, the method attribute is set to POST; therefore, an HTTP POST request is sent to /me with the parameter phrase on submit.

post '/me' do
  if users[session[:username]].nil?
    users.create(session[:username], { 'phrase' => params[:phrase] }) unless params[:phrase].empty?
  else
    if params[:phrase].empty?
      doc = client.get(:users, session[:username])
      client.delete(:users, session[:username], doc.ref)
    else
      users.set(session[:username], { 'phrase' => params[:phrase] })
    end
  end
  redirect '/all'
end

If the user isn’t already in our Orchestrate collection and the phrase parameter is not empty, we create it with the Twitter username as it’s key and phrase as one of its values. If it does exist but the phrase is empty, we delete the user from the users collection since phrase since phrase is the only value for any given key in the case of our app. Since the delete function is only present on the method client, we use that here. Lastly, if there is already a phrase associated with the user but another one is submitted, we update the value in the collection.

{
  "count": 3,
  "results": [
  {
    "path": {
      "collection": "users",
      "key": "adamd",
      "ref": "dd95d9ead0e46704",
      "reftime": 1417496743474
  },
    "value": {
      "phrase": "The early bird gets the first worm"
    },
    "reftime": 1417496743474
 },
 {
   "path": {
     "collection": "users",
     "key": "simplyianm",
     "ref": "57cc54e81ee8c562",
     "reftime": 1417836638115
   },
   "value": {
     "phrase": "Test"
   }
 ]
}

Logout

To logout, session[:authed] is defined nil.

get '/logout' do
  session[:authed] = nil
  erb :out
end

Login

After authenticating, Twitter will redirect the user to our callback URL set earlier in the Developer Dashboard. Here, we will define session[:authed] true and save the username to session[:username].

get '/auth/twitter/callback' do
  session[:authed] = true
  session[:username] = request.env['omniauth.auth']['info']['nickname']
  erb :in
end

Login Failure

In the case of authentication failure, the user is directed to /auth/failure and the full error message is printed. In practicality, you’d want to mask this error message with a coated page via erb :error and log it in full to the filesystem, but this method makes debugging in development much simpler.

Conclusion

This application in full has been released as a public, open-source repo on GitHub and deployed to Heroku. In the future, you can create additional collections and interact with them in the same way described here. You can also utilize the power of Orchestrate and its gem to execute powerful search queries by value and order results by a number of factors, including value or timestamp.

Additionally, once user authentication is setup with Twitter, you can use the Twitter gem to perform operations using their Twitter account using the accesstoken and accesstoken_secret found in request.env[‘omniauth.auth’] on /auth/twitter/callback, but that is out of the scope of Orchestrate.

As you can see, Orchestrate provides a developer-friendly, powerful interface to your application data in an inexpensive service, and integrating user authentication and datastore with it is no different!