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!
Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.