Building a Custom OAuth Strategy for Doorkeeper

As Rails developers, we're all very familiar with using tools like OmniAuth and Devise to authenticate the users of our applications. In fact, I've used OmniAuth with so many of its integrations--Google, Twitter, Facebook, etc.––for so long, without ever really wondering how OmniAuth was working with these third parties to authenticate.

Recently, while working on what will eventually be a public-facing API that authorizes users with the help of Doorkeeper, I began to think more about this OAuth process. Having relied so heavily on the OmniAuth library, I found I didn't actually know what the most basic OAuth steps where, or how to enact them from scratch.

So, I set out to break down the OAuth flow, and build my own OmniAuth strategy and gem, so that future developers aiming to access the API in question would be able to easily sign in users via the API's parent application, receive a user token, and make authenticated API requests.

Background: The Learn API

At the Flatiron School, we've been steadily building out an API that exposes data from the Learn application. The Learn API will allow authenticated users to access data on Learn and/or Flatiron School students, courses, lessons, and more.

The Learn API is exposed by the Learn application itself, and although it's not quite public yet, eventually Learn and Flatiron students will be able to register applications with Learn, receive application keys and secrets and authenticate users in order to get their user token to make further API requests.

So, Learn and Flatiron students will soon be able to work with real data regarding their peers and the lessons they're working through, to build third party applications. For example, students might build applications that schedule lunches or study groups for their fellow students, visualize data regarding lesson completion or lesson content, connect students from different parts of the country, and much more, all while they gain mastery of full-stack web development.

In order to facilitate this future collaboration, we wanted to make authentication via Learn, and therefore retrieval of API access tokens, conventional and approachable for our users.

Before we take a closer look at how this authentication strategy was built out, let's take a closer look at OAuth in general.

What is OAuth?

OAuth is a protocol for secure authentication on the web. It's a set of conventions that attempt to standardize the way in which a web application can authenticate a user via a third party service, like Google, Twitter, Facebook, etc.

With OAuth, your application does not have to store a user's password. Instead, your application will request user information from an external application (again, like Google, Twitter, Facebook, etc). Not only does this shift the burden of securing sensitive user information and passwords off of your shoulders, it makes life a little simpler for your users, who don't have to set up and remember yet another password, to access your application.

How does OAuth Work?

Specific implementation details will vary depending on the third party application that you are trying to authorize with. However, the basic flow remains the same across the board.

Step 1: Register Your Application with the Third Party Service

You'll register your application with the third party service, like Google, or Facebook or Learn, and receive an application key and secret in return. You'll also specify your callback URL when you register (more on this soon).

Why must you register your application? The third party service is going to keep track of the applications that are registered to authorize via the third party service. In other words, Google/Facebook/Learn/Twitter is storing all the registered applications, the key and secret associated with those applications, and the callback URLs associated with those applications. Then, when your application sends the authentication request to the third party app, that app will look at its registered applications to see whether or not it can complete the request.

Step 2: Make the Authentication Code Request

With your application key and secret, your app will make an initial authorization request to the third party application. If that request is authorized, i.e. if you correctly registered you app and are using your correct key and secret, then the third party app will send some kind of authorization code back to the callback URL you specified when you registered your app.

Step 3: Make the OAuth Token Request

Then, with the authorization code you receive in response to the first request, your app can make a subsequent request to get the current user's OAuth access token. With that token, you can proceed to step 4.

Step 4: Use The Access Token to Get Current User Info

Using the access token of the user who is the current user of the third party service, you can make a final request to get that user's personal info (i.e. their name, email, avatar, etc), from the third party app.

Now that we understand basic OAuth flow, let's enact it for the specific Learn API scenario. Then, we'll build out our custom OmniAuth strategy and gem, so that we never have to enact that flow ourselves again!

OAuth with a Third Party App that Uses Doorkeeper

Doorkeeper is a Ruby gem that implements a lot of OAuth functionality for you. It will maintain authentication and token endpoints for your app, among other things. The Learn API uses Doorkeeper to handle OAuth, so we'll use the example of authorizing through Learn to walk through these steps.

Let's say we've registered our application and received an application key and secret. We'll also assume that we've set our callback URL to http://localhost:3000/auth/learn/callback.

We'll assume that in our (Rails) app, we created the following route, with corresponding controller action:

get '/auth/:provider/callback', to: "sessions#create" 

Okay, we're ready to make our first authentication request!

Initial Authentication Request: Getting the Authorization Code

require 'httparty'

HTTParty.get("https://learn.co/oauth/authorize?client_id=&response_type=code&redirect_uri=http://localhost:3000/auth/learn/callback")

Doorkeeper, inside the Learn application, will:

  • Check the persisted registered applications and see if there is one with my application key and redirect URL
  • If so, send the following payload back to that callback URL:
# app/controllers/sessions_controller.rb
  ...
  def create
    params
    # => {code: ""}
  end

We can use this authorization code, along with our existing key and secret, to enact the next step...

Second Authorization Request: Getting the User Token

We need to make a POST request to this endpoint:

https://learn.co/oauth/token

The request body should include our key, secret, the authorization code we received in response to our previous request, and our callback URL.

  HTTParty.post("https://learn.co/oauth/token", 
  body: {
   client_id: , 
   client_secret: , 
   redirect_uri:   
     "http://localhost:3000/auth/learn/callback/",
   grant_type: "authorization_code", 
   authorization_code: 
  }.to_json, 
  headers: {'Content-Type' => 'application/json'}
)

Doorkeeper's /oauth/token endpoint will receive this request, and if your key, secret, callback URL and authorization code are valid, it will send the following payload back to your callback ULR:

  # app/controllers/sessions_controller.rb
  ...
  def create
    ...
    params
    # => {
     "access_token": "",
     "token_type": "bearer",
     "created_at": 1468336033
    }
  end

The access token sent to us in response to our request is the access token of the user who is currently signed into Learn (or whichever third party service you are authenticating with). If no one was logged in to Learn when the request was sent, the browser will likely open a window and prompt the user to log in.

We almost have our current user's information!

Requesting Current User Info

Now that we have Learn's current user's access token, we can use it to request more information about that user. With this info (for example the user's name, email, avatar, etc.) we can find or create the corresponding user from our own database and log them into our app.

In order to request this info, we need to use our access token to make the following request:

HTTParty.get("http://learn.co/api/users/me", headers: {"Authorization" => "Bearer "})

This will return the following information:

{
    "id": 7,
    "admin": false,
    "email": "charlotte@flatironschool.com",
    "first_name": "charlotte",
    "last_name": "tobin",
    "full_name": "charlotte tobin",
    "github_username": "tobin",
    "github_gravatar":   "https:://avatars.githubusercontent.com/u/7558665",
  "last_sign_in_at": null
  }

And that's it for our manual OAuth flow. Now that we understand what is required to enact such a flow, let's built out a custom OmniAuth strategy, which we'll use with the OmniAuth gem to abstract away many of these steps.

Building a Custom OmniAuth Strategy

The OmniAuth gem provides a flexible, extensible library of code that makes it easy to incorporate OAuth for any number of third party applications into your Rails (or Sinatra) app.

An OmniAuth "strategy" is a smaller library of code that is used with the main OmniAuth gem to authenticate with specific services like Google, Facebook, and now, Learn. These strategies are usually released as their own gems, and it is not difficult to build and bundle your own OmniAuth strategy.

Let's take a look at the Learn OmniAuth strategy and gem.

Defining a Custom Strategy

Here's a basic boilerplate for a custom OmniAuth strategy:

require 'omniauth-oauth2'

module OmniAuth
  module Strategies
    class MyCustomStrategy < OmniAuth::Strategies::OAuth2
      include OmniAuth::Strategy

      option :client_options, {
               site: "your site's base URL",
               authorize_url: "your site's initial auth URL",
               token_url: "your site's token auth URL"
             }

      def request_phase
        super
      end

      info do
        raw_info.merge("token" => access_token.token)
      end

      uid { raw_info["id"] }

      def raw_info
        @raw_info ||= 
          access_token.get("/your site's current user endpoint").parsed
      end
    end
  end
end

Let's generate our own gem and customize this strategy for Learn.

Building a Custom OmniAuth Strategy Gem

In your terminal, run

bundle gem omniauth-learn

This will generate a gem with the following file structure:

├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── Rakefile
├── lib
│   ├── omniauth
│   │   └── strategies
│   │       └── learn.rb
│   ├── omniauth-learn
│   │   └── version.rb
│   └── omniauth-learn.rb
├── omniauth-learn.gemspec
└── spec
    ├── omniauth-learn
    │   └── strategies
    │       └── learn_spec.rb
    └── spec_helper.rb

We'll define our custom strategy in lib/omniauth/strategies/learn.rb

require 'omniauth-oauth2'

module OmniAuth
  module Strategies
    class Learn < OmniAuth::Strategies::OAuth2
      include OmniAuth::Strategy

      option :client_options, {
               site: "https://learn.co",
               authorize_url: 
                 "https://learn.co/oauth/authorize",
               token_url: "https://learn.co/oauth/token"
             }

      def request_phase
        super
      end

      info do
        raw_info.merge("token" => access_token.token)
      end

      uid { raw_info["id"] }

      def raw_info
        @raw_info ||= 
          access_token.get('/api/users/me').parsed
      end
    end
  end
end

We're doing a few things here:

  • Setting our authorization and token URLs, so that OmniAuth knows which endpoints to send requests to.
  • Defining the request_phase method to simply use the OmniAuth::Strategies::OAuth2 parent class' request_phase method. This method more or less wraps up the first two authorization requests in the OAuth process.
  • Populating the @raw_info variable with the payload returned from hitting the current user endpoint, /api/users/me. OmniAuth will then shove this data into the request object available to us in a Rail's controller action.

Once we (thoroughly test) and release our gem, we'll be able to include it in our Rails app to make the "authorization via Learn" process even easier.

Gotcha: Custom OmniAuth Strategies + Doorkeeper = Messed Up Callback URLs

After releasing the OmniAuth Learn gem, including it in a Rails app, and attempting to authorize my users via Learn, I ran into a slight (read: 2-day long) hiccup.

When OmniAuth makes the initial authorization request to retrieve the authorization code from Doorkeeper, it will then append the authorization code to the callback URL like this:

  https://localhost:3000/auth/learn/callback?

It's my understanding that other third party services that allow OAuth, i.e. NOT Doorkeeper, will be able to parse that callback URL when it is included in subsequent authorization requests (i.e. the next request for the OAuth token). However, Doorkeeper refuses to recognize that altered callback URL as valid/matching the callback URL you would have entered when you registered your app with Learn.

So, I had to create a fork of the OAuth2 gem and edit the Client#request method to remove this appended authorization code from the callback URL, ensuring that the callback URL is properly formatted for the token request.

I opened an issue on the OAuth2 repo here, if anyone is interested/has any thoughts on this.

Using the OmniAuth Learn Gem

Now that our OmniAuth Learn gem has been built and released, we can include it in our Rails app to simply the authorization process.

Step 1: Include the Gems

We need all of the following gems in our Gemfile, since the OAuth2 gem had to be customized to fix the bug described above.

gem "omniauth"
gem "omniauth-oauth2"
gem "oauth2", git: "git@github.com:flatiron-school/oauth2.git"
gem "omniauth-learn"

Step 2: Configure OmniAuth Learn

Create an initializer, config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :developer unless Rails.env.production?
  provider :learn, ENV['LEARN_CLIENT_ID'],
    ENV['LEARN_CLIENT_SECRET'],
  provider_ignores_state: true
end

Step 3: Define your Callback URL Route and Log In Link

In your routes.rb file:

get '/auth/:provider/callback', to: "sessions#create"

In app/views/sessions/new.html.erb

<%= link_to "log in with Learn", '/auth/learn' %>

Step 4: Access the Current User Info

Thanks to OmniAuth Learn, the current user info payload can be accessed inside the controller action that maps to our callback URL, Sessions#create, like this:

  request.env["omniauth.auth"]["info"]

  # =>
  {
    "id"=>281,
    "first_name"=>"Sophie",
    "last_name"=>"DeBenedetto",
    "github_username"=>"SophieDeBenedetto",
    "email"=>"sophie.debenedetto@gmail.com",
    "last_sign_in_at"=>null,
    "token"=>"45c254daxxxxx4d7ae7xxxxxxxacd65fd3fa09a67a"
  }

With this info, we can identify and log in our current user.

Conclusion

Although this post laid out examples specific to working with the Learn API, there are some more general take-aways.

The basic sequence of first requesting an authorization code, then requesting a user token, and lastly, using that token to request current user info, describes the OAuth flow more or less across the board.

If you have any questions or comments, or if you also ran into Doorkeeper + custom OmniAuth weirdness, hit me up below.

Happy coding!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus