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 theOmniAuth::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 therequest
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!