Recently I found myself doing two things for which there's a dearth of information: writing an OAuth provider in Rails, and writing an extension for Devise. In the next few blog posts I'll detail my experiences with both.
My company wanted to rewrite our authentication stack as part of our migration to Rails 3 and Ruby 1.9.2. After discussing our options we decided we want to drop most of our legacy code and rely more on open source software and standards. This meant relying on Devise to handle most of the implementation details, and using OAuth for inter-app authentication.
We had a few unique restrictions and requirements:
1) We wanted single sign-on across all our applications
2) We already had an existing database which was the master of all our user data
3) The fields on our user table can't be changed
Given these constraints we decided to do the following:
1) Create a new Rails 3.0 application called Authentication that read from our account database
2) Create an authentication plugin that uses OAuth to authenticate against the above mentioned application
User Workflow
Suppose I have two applications, a Provider and a Client. A user tries to visit the Client site. Here's what should happen:
1) User is redirected to the Provider site
2) User logs in at Provider site
3) User is redirected to Client site with authentication credentials
This is what happens at a very high level. There are some other things going on under the covers to satisfy our requirements, but I wanted to start out implementing this bare minimum.
In this case our Provider site will act as the OAuth provider, and the Client will be an OAuth client. So first off I want to explain how to implement both sides of the OAuth specification.
Authentication Application
First off I want to credit Chad Fowler for writing the skeleton of what you're about to see. He basically read the OAuth specification and implemented the bare minimum to fulfill our requirements.
I'm also assuming you're familiar with how OAuth works. In this case specifically, the client will request an authorization token from the provider. It then uses the authorization token to get an access token, and finally it uses the access token to fetch the user data. Most of the client side stuff is implemented by Devise. What I'll be focusing on is what you need to implement in the provider to get this token exchange working.
I started off by creating a new Rails 3.0 project called Authentication. There are a few gems I want to install so I update my Gemfile:
gem 'oauth2'
gem 'devise', :git => "http://github.com/plataformatec/devise.git"
gem 'warden_oauth'
Make sure to run
bundle install to install all the gems. Next I create a basic User model:
rails generate model User \
username:string \
password_hash:string \
password_salt:string \
status:string \
expiration_date:date
Now I want to set up my routes:
Authentication::Application.routes.draw do
devise_for :users, :controllers => {:sessions => "users/sessions"}
match '/oauth/authorize' => 'oauth#authorize'
match '/oauth/access_token' => 'oauth#access_token'
match '/oauth/user' => 'oauth#user'
end
Since I wanted to customize my log in page, I passed an option to devise_for telling it where to look for the sessions controller (in this case, it'll be under the users namespace). I also added some manual paths for my OauthController which you'll see in a bit.
I want to create two additional models, AccessGrant and ClientApplication:
rails generate model AccessGrant \
code:string \
access_token:string \
refresh_token:string \
access_token_expires_at:datetime \
user_id:integer \
application_id:integer
rails generate model ClientApplication \
name:string \
app_id:string \
app_secret:string
The AccessGrant model represents our authorization and access tokens. It has a fairly simple implementation:
class AccessGrant < ActiveRecord::Base
belongs_to :user
belongs_to :application, :class_name => "ClientApplication"
before_create :generate_tokens
def self.prune!
delete_all(["created_at < ?", 3.days.ago])
end
def self.authenticate(code, application_id)
AccessGrant.where("code = ? AND application_id = ?", code, application_id).first
end
def generate_tokens
self.code, self.access_token, self.refresh_token = SecureRandom.hex(16), SecureRandom.hex(16), SecureRandom.hex(16)
end
def redirect_uri_for(redirect_uri)
if redirect_uri =~ /\?/
redirect_uri + "&code=#{code}&response_type=code"
else
redirect_uri + "?code=#{code}&response_type=code"
end
end
# Note: This is currently hard coded to 2 days, but it could be configurable per-user-type or per-application.
# No need for this to be constant like this.
def start_expiry_period!
self.update_attribute(:access_token_expires_at, 2.days.from_now)
end
end
The ClientApplication model is bare bones:
class ClientApplication < ActiveRecord::Base
belongs_to :user
def self.authenticate(app_id, app_secret)
where(["app_id = ? AND app_secret = ?", app_id, app_secret]).first
end
end
Now let's look at our User model:
class User < ActiveRecord::Base
devise :token_authenticatable, :custom_database_authenticatable
self.token_authentication_key = "access_token"
def self.find_for_token_authentication(conditions)
where(["access_grants.access_token = ? AND (access_grants.access_token_expires_at IS NULL OR access_grants.access_token_expires_at > ?)", conditions[token_authentication_key], Time.now]).joins(:access_grants).select("users.*").first
end
end
The
devise method call tells Devise which authentication strategies to use. In this case, it will try to authenticate off a token first. Failing that, it will prompt the user for a login/password and authenticate against the database.The
token_authenticatable strategy is a default one that come with Devise.The
custom_database_authenticatable is a Devise strategy I wrote. Because one of our restrictions is the inability to modify our existing User schema, I had to write a custom Devise extension that could authenticate off our table. I'll go into detail about how custom_database_authenticatable works in a later post but for now assume it's the same as the standard database_authenticatable.The
token_authentication_key part is me setting an option. The token_authenticatable strategy used by Devise will now look for a parameter called access_token to authenticate against. I couldn't find another way of setting this option so here I just set it directly.Finally you'll see a class method called
find_for_token_authentication. This is a method that Devise calls to find a User record for a particular token. We've already defined token_authentication_key to be access_token, so this method will check to see an access grant for this token exists.Here is my OauthController:
class OauthController < ApplicationController
before_filter :authenticate_user!, :except => [:access_token]
skip_before_filter :verify_authenticity_token, :only => [:access_token, :user]
def authorize
AccessGrant.prune!
access_grant = current_user.access_grants.create(:application => application)
redirect_to access_grant.redirect_uri_for(params[:redirect_uri])
end
def access_token
application = ClientApplication.authenticate(params[:client_id], params[:client_secret])
if application.nil?
render :json => {:error => "Could not find application"}
return
end
access_grant = AccessGrant.authenticate(params[:code], application.id)
if access_grant.nil?
render :json => {:error => "Could not authenticate access code"}
return
end
access_grant.start_expiry_period!
render :json => {:access_token => access_grant.access_token, :refresh_token => access_grant.refresh_token, :expires_in => access_grant.access_token_expires_at}
end
def user
hash = {
:user => current_user,
:person => current_user.person,
:company => current_user.person.company,
:staff_detail => current_user.person.staff_detail
}
if current_user.person.company_id == 9700
hash[:department] = current_user.person.staff_detail.department rescue nil
hash[:division] = current_user.person.staff_detail.department.division rescue nil
hash[:gl_division] = current_user.person.staff_detail.department.division.gl_division rescue nil
end
render :json => hash.to_json
end
protected
def application
@application ||= ClientApplication.find_by_app_id(params[:client_id])
end
end
There's a lot going on here.
First off notice the
authenticate_user! before filter. This is a method dynamically defined by Devise. It's active for everything except for access_token. The idea is that when the user tries to visit the Client app, they get redirected to /oauth/authorize on the Provider. But since they aren't authenticated yet, the filter triggers and they get redirected to the login page. Once they log in, they're once again redirected to /oauth/authorize which creates a new authorization token. They then get redirected back to the Client with the authorization token embedded in the URL.The Client uses this authorization token to make a call to
/oauth/access_token on Provider. Authentication for this action isn't required so the Client doesn't have to log in. All it needs to do is give up an authorization token. Provider checks this token against its local access_grants table to make sure the token exists and is still valid. Assuming it is, it gives out the access token as a JSON document.The Client now has an access token. It now makes one final call to
/oauth/user, passing along the access token. The Provider will now authenticate off this access token and render a JSON representation of the user. The Client can now cache all the user data locally and proceed, knowing that the current user is authenticated. A session cookie is created and the user no longer has to authenticate for the rest of the session.One final piece is defining our custom sessions controller. In
app/controllers/users/sessions_controller.rb:
module Users
class SessionsController < Devise::SessionsController
end
end
And the view:
<%= form_tag(user_session_path, :method => "post") do %>
<label>Username: <input type="text" class="text" name="user[username]"></label>
<label>Password: <input type="password" class="text" name="user[password]"></label>
<input type="submit" value="Submit">
<% end %>
Client Application
That was a lot of work, but things are dramatically simpler for the client.
I create a blank Rails 3.0 application called Client. Here's the Gemfile:
gem 'oauth2'
gem 'devise', :git => "http://github.com/plataformatec/devise.git"
gem 'warden_oauth'
One potentially tricky part is setting up the Devise configuration. In your config/initializers/devise.rb file:
config.oauth :provider, 'APP_ID', 'APP_SECRET', :site => "http://path/to/provider", :authorize_path => "/oauth/authorize", :access_token_path => "/oauth/access_token", :scope => %w(user)
You have to create an instance of ClientApplication in Provider, using an arbitrary application id and secret. You then plug them into this config above. You'll also change the site option but everything else can be left the same.
Here are the routes:
Client::Application.routes.draw do |map|
devise_for :users
root :to => 'home#index'
end
In ApplicationController:
class ApplicationController < ActionController::Base
protected
def authenticate_user_through_provider!
unless user_signed_in?
redirect_to user_oauth_authorize_url(:provider)
end
end
end
And in a protected controller:
class HomeController < ApplicationController
before_filter :authenticate_user_through_provider!
def index
end
end
We also have to mark our User model as OAuthable:
class User < ActiveRecord::Base
devise :oauthable
def self.find_for_provider_oauth(access_token, signed_in_resource = nil)
data = ActiveSupport::JSON.decode(access_token.get('/oauth/user'))["user"]
user = User.find_by_username(data["username"])
if user.nil?
user = User.create(data)
end
user
end
end
We've also defined a class method called
find_for_provider_oauth. Devise will call this method once it has obtained an access token. In this method we then make an HTTP call to /oauth/user to fetch the user data from the Provider. We then cache the user data locally and proceed with the request.When a user tries to hit the index page, the filter will trigger and they'll be redirected to Provider to log in. Assuming they log in successfully, they'll be redirected back to Client.
Thank you for writing this up... I believe it is missing the part where you ask the user if they want to authorize... ? e.g. if i send a request to the /oauth/authorize URL shouldn't I be prompted to accept or reject the authorization?
ReplyDeleteHi Todd.
ReplyDeleteYes, you are correct that if you wanted to make a generic provider you would want to ask the user permission if the client should be able to access your user data.
In our case all our client and provider apps are internal so we assume the client will always want to authorize access.
This comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteIn your access_token method you use params[:client_secret], shouldn't you be checking for a request signature here based on the client_secret?
ReplyDeleteSorry, I believe the answer to my question - is that this is oauth 2.0 not oauth 1.0... The longer answer is, I need to read more in the spec to understand :)
ReplyDeleteHi, I think I found a bug with User#find_for_provider_oauth in the client application. While trying this example I found that the method works fine the first time you authenticate the demo user because the last statement executed is user = User.create(data) which ends up returning the created user, however in subsequent logins, a the user variable is not getting returned, I corrected this by adding user to the bottom of the method:
ReplyDeletehttps://gist.github.com/919712
Thanks for catching that. I've corrected the article.
ReplyDeleteThanks for the tutorial, oauth is still a bit "new" for me. I want to create an application with an API like twitter. So i have a client application that wants data from the provider application. They authenticate via oauth like your tutorial. But i want to achieve something like the twitter api like returning a json object. So when i go to providerapp/posts i want to authenticate via oauth en then have the posts.json object in my clientapplication. Is this possible?
ReplyDeleteI don't see why not. In the OauthController above you could just substitute the user method with a posts method that returns your posts. You could even move it to its own controller as long as you had the appropriate before_filter to restrict access.
ReplyDeleteThis doesnt seem to work with the latest Devise, I get the following error on the client
ReplyDeleteinitializers/devise.rb:8:in `block in ': undefined method `oauth' for Devise:Module (NoMethodError)
Same problem, as Nic Pillinger has.
ReplyDeleteconfig/initializers/devise.rb:6:in `block in ': undefined method `oauth' for Devise:Module (NoMethodError)