Ruby
Article

OAuth 2 All the Things with oPRO: Users and API

By Ilya Bodrov-Krukowski

Authentication in Rails

o2

In my previous article we met oPRO – a Rails engine to build full-fledged OAuth 2 providers. We’ve already created a server (the actual provider) and a client app. For now, the server has a basic authentication system powered by Devise. Users are able to create applications to receive client and secret keys, authenticate via OAuth 2 and perform sample API requests. However, currently there is no storage mechanism for the user’s data on the client side. Moreover, we don’t get any information about a user apart from their tokens. This article will address these issues, as well as introducing some more API actions and refactoring the code.

The source code for the server and client applications can be found on GitHub.

Introducing Users

Storing tokens inside the user’s session isn’t really convenient, therefore I’d like to introduce a new users table for the client app and save all the relevant data there. Having a model in place will also allow us to extract some methods to tidy up the controllers.

Create a new migration:

$ rails g model User email:string uid:string access_token:string refresh_token:string expires_at:string

Apart from tokens and expiry information, we will also store the user’s uid and email – as you probably do when using authentication providers like Twitter or Facebook.

Modify migration to include some indexes:

migrations/xxx_create_users.rb

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :email, index: true
      t.string :uid, index: true, unique: true
      t.string :access_token
      t.string :refresh_token
      t.string :expires_at

      t.timestamps null: false
    end
  end
end

and apply it:

$ rake db:migrate

Also, add basic validation rules:

models/user.rb

[...]
validates :access_token, presence: true
validates :refresh_token, presence: true
validates :expires_at, presence: true
validates :uid, presence: true, uniqueness: true
[...]

Having this in place, we can tweak the SessionsController (for now, I’ll just code its skeleton):

sessions_controller.rb

[...]
def create
  user = authenticate_and_save_user
  if user
    login_user
    flash[:success] = "Welcome, #{user.email}!"
  else
    flash[:warning] = "Can't authenticate you..."
  end
  redirect_to root_path
end
[...]

There are two things that we have to take care of: perform user authentication and actually log them in. We might leave the code from the previous iteration

JSON.parse RestClient.post( [...] )

right inside the controller’s action, but that’s not a great idea. In the production environment, you’d probably create a separate authentication adapter for your app and serve it as a gem. However, for our purposes, it will be enough to extract the code somewhere to a separate file. I’ll stick with a model, but you may also place it inside the lib directory (just don’t forget to require that file).

models/opro_api.rb

class OproApi
  TOKEN_URL = "#{ENV['opro_base_url']}/oauth/token.json"
  API_URL = "#{ENV['opro_base_url']}/api"

  attr_reader :access_token, :refresh_token

  def initialize(access_token: nil, refresh_token: nil)
    @access_token = access_token
    @refresh_token = refresh_token
  end

  def authenticate!(code)
    return if access_token
    JSON.parse(RestClient.post(TOKEN_URL,
                               {
                                   client_id: ENV['opro_client_id'],
                                   client_secret: ENV['opro_client_secret'],
                                   code: code
                               },
                               accept: :json))
  end
end

TOKEN_URL and API_URL are just convenience constants.

access_token and refresh_token are instance variables that will be used in various methods of this class.

When defining the initialize method, I list arguments in a hash style – this cool feature is supported in newer versions of Ruby and allows to pass arguments easily (you don’t have to remember their order).

authenticate! contains the code from the previous iteration (I only added a return statement). It sends
a request and returns parsed JSON.

Now, return to the controller:

sessions_controller.rb

[...]
def create
  user = User.from_opro(OproApi.new.authenticate!(params[:code]))
  if user
    login_user
    flash[:success] = "Welcome, #{user.email}!"
  else
    flash[:warning] = "Can't authenticate you..."
  end
  redirect_to root_path
end
[...]

Here we are taking advantage of newly created authenticate! method. The next step is to code the from_opro class method that takes the user’s auth hash and stores it into the database. If you read my article about OAuth 2, I introduced a very similar method there called from_omniauth.

models/user.rb

[...]
class << self
  def from_opro(auth = nil)
    return false unless auth
    user = User.find_or_initialize_by(uid: auth['uid'])
    user.access_token = auth['access_token']
    user.refresh_token = auth['refresh_token']
    user.email = auth['email']
    user.save
    user
  end
end
[...]

If, for some reason, the auth hash has no value, just return false – this will make our controller render an error. Otherwise, find or initialize a user by the uid, update all the attributes, and return the record as a result.

Great! The last step is to perform logging in. For this, I’ll introduce a couple of helper methods:

application_controller.rb

[...]
private
[...]

def login(user)
  session[:user_id] = user.id
  current_user = user
end

def current_user
  @current_user ||= User.find_by(id: session[:user_id])
end

def current_user=(user)
  @current_user = user
end

helper_method :new_opro_token_path, :current_user
[...]

These are very basic methods that you’ve probably seen countless times. current_user will be employed in the views, so I mark it as helper.

Let’s also modify the view:

views/pages/index.html.erb

<h1>Welcome!</h1>
<% if current_user %>
  <ul>
    <li><%= link_to 'Show some money', api_tests_path %></li>
  </ul>
<% else %>
  <%= link_to 'Authenticate via oPRO', new_opro_token_path %>
<% end %>

I guess users might want to logout, so present a corresponding action as well:

config/routes.rb

[...]
delete '/logout', to: 'sessions#destroy', as: :logout
[...]

sessions_controller.rb

[...]
def destroy
  logout
  flash[:success] = "See you!"
  redirect_to root_path
end
[...]

logout is yet another method:

application_controller.rb

[...]
def logout
  session.delete(:user_id)
  current_user = nil
end
[...]

Present a new link:

views/pages/index.html.erb

<h1>Welcome!</h1>
<% if current_user %>
  <ul>
    <li><%= link_to 'Show some money', api_tests_path %></li>
  </ul>
  <%= link_to 'Logout', logout_path, method: :delete %>
<% else %>
  <%= link_to 'Authenticate via oPRO', new_opro_token_path %>
<% end %>

Tweaking the Authentication Hash

At this point you might be thinking, “How in the world can we fetch the user’s email and uid if, by default, oPRO only lists tokens and an expiry information?” Well, you are right, an something has to be done about it. The problem, however, is the current stable release has no way to redefine TokenController. This controller actually takes care or generating all the tokens and creating an authentication hash. However, I did some tweaking) to the oPRO’s source code that were merged into the master branch. For now you’ll have to specify it directly:

Gemfile

[...]
gem 'opro', github: 'opro/opro', branch: 'master'
[...]

If something changes in the future, I will update this article as necessary.

First of all, introduce a new route:

config/routes.rb

[...]
mount_opro_oauth controllers: {
    oauth_new: 'oauth/auth',
    oauth_token: 'oauth/token'
}, except: :docs
[...]

Create a custom controller inheriting from the original one:

controllers/oauth/token_controller.rb

class Oauth::TokenController < Opro::Oauth::TokenController
end

We don’t need to monkey-patch any action, as the only thing that has to be done is changing the view:

views/oauth/token/create.json.jbuilder

json.access_token @auth_grant.access_token
json.token_type Opro.token_type || 'bearer'
json.refresh_token @auth_grant.refresh_token
json.expires_in @auth_grant.expires_in
json.uid @auth_grant.user.id
json.email @auth_grant.user.email

jbuilder takes care of creating the proper JSON for us.

Everything, apart from the uid and email, is taken from the original view. You may add any other fields here, as necessary.

You are good to go. Boot the server and try to authenticate. All of the user’s information should be stored properly.

Working with the API

A Bit of Refactoring

We’ve done a good job, but I still don’t like the way our code performs API requests. Let’s extract it to the opro_api.rb file:

models/opro_api.rb

[...]
def test_api
  JSON.parse(RestClient.get("#{ENV['opro_base_url']}/oauth_tests/show_me_the_money.json",
                            params: {
                                access_token: access_token
                            },
                            accept: :json))

end
[...]

Now it can be called it from the controller’s action:

api_tests_controller.rb

class ApiTestsController < ApplicationController
  before_action :prepare_client

  def index
    @response = @client.test_api
  end

  private

  def prepare_client
    @client = OproApi.new(access_token: current_user.access_token)
  end
end

We will require @client to perform any API request, therefore I’ve placed it inside the before_action.

That’s nice, but what if a current user does not have a token for some reason? Let’s check that prior to doing anything else:

api_tests_controller.rb

class ApiTestsController < ApplicationController
  before_action :check_token
  before_action :prepare_client
  [...]

  private
  [...]
  def check_token
    redirect_to new_opro_token_path and return if !current_user || current_user.token_missing?
  end
end

models/user.rb

[...]
def token_missing?
  !self.access_token.present?
end
[...]

Now if a token does not exist, the user will be asked to authenticate once again.

Introducing More Actions

Let’s add two more custom API actions: fetching information about a user and updating a user. The first step is adding a new Api::UsersController on the server app:

config/routes.rb

[...]
namespace :api do
  resources :users, only: [:show, :update]
end
[...]

controllers/api/users_controller.rb

class Api::UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    @user.last_sign_in_ip = params[:ip]
    render json: {result: @user.save}
  end
end

It does not really matter what the update does, so for demonstration purposes let’s simply modify the last_sign_in_ip column introduced by Devise.

Don’t forget a view:

views/api/users/show.json.jbuilder

json.user do
  json.email @user.email
end

Once again, I am returning some sample data here.

There are two thing to note, however. Before actually performing those actions, we haven’t checked whether an access token is in the request and if it is valid. oPRO provides an allow_oauth! method that takes only and except options just like before_action. By default, no actions are allowed to be performed based on the access token, so add this line to the controller:

controllers/api/users_controller.rb

class Api::UsersController < ApplicationController
  allow_oauth!
  [...]
end

Another thing to take into consideration is that we won’t send a CSRF token when performing the update action, so Rails will raise an exception. To avoid that, modify this line:

application_controller.rb

[...]
protect_from_forgery
[...]

Now request forgery protection will use null_session, meaning that a session will be nullified if the CSRF token is not provided, but not reset completely. As long as we are relying on the access token to perform authentication, everything should be fine.

Return to the client’s app and tweak our poor man’s API adapter:

models/opro_api.rb

[...]
def get_user(id)
  JSON.parse(RestClient.get("#{API_URL}/users/#{id}.json",
                            params: {
                                access_token: access_token
                            },
                            accept: :json))
end

def update_user(id)
  JSON.parse(RestClient.patch("#{API_URL}/users/#{id}.json",
                              {
                                  access_token: access_token,
                                  ip: "#{rand(100)}.1.1.1"
                              },
                              accept: :json))
end
[...]

Now the controller:

api_tests_controller.rb

[...]
def show
  @response = @client.get_user(params[:id])
end

def update
  @response = @client.update_user(params[:id])
end
[...]

Views will simply render a response:

views/api_tests/show.html.erb

<pre><%= @response.to_yaml %></pre>

views/api_tests/update.html.erb

<pre><%= @response.to_yaml %></pre>

Add new routes:

config/routes.rb

[...]
resources :api_tests, only: [:index, :show, :update]
[...]

Provide links to these new actions:

views/pages/index.html.erb

[...]
<ul>
  <li><%= link_to 'Show some money', api_tests_path %></li>
  <li><%= link_to 'Get a user', api_test_path(1) %></li>
  <li><%= link_to 'Update a user', api_test_path(1), method: :patch %></li>
</ul>
[...]

I just hard-coded the user’s id here, but that does not really matter for this demo. Go ahead and play with the API a bit! Note, however, that for the update action to work correctly, you must permit “write” access to the app when authenticating. I’ll speak more about various permissions in a later section.

Conclusion

Our application is starting to look pretty nice, but once again that’s not enough. We still have a handful of things to take care of:

  • Access tokens should have a limited lifespan and some rate limitation should be introduced.
  • We haven’t discussed other advanced topics, like working with scope, introducing your own authentication solution and exchanging user’s credentials for a token.

So, the last, but not the least, part of this article will cover all these topics. Hold on tight!

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.