OAuth 2 All the Things with oPRO: Customization

Share this article

This is the third and the last part of this series. Today we will finalize our authentication provider built with oPRO. Specifically, I will instruct you how to work with the scope, implement limited lifespan for access tokens, and introduce rate limitation. Also, we’ll discuss some advanced configuration options, such as using a custom authentication solution and exchanging user’s credentials for an access token. Let’s proceed!

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

Scope

When users authenticate via oPRO, they are requested to permit read and write actions (or, the “scope” of the permissions) to the app. Read is the mandatory action and this permission cannot be revoked. It only allows an app to perform GET requests to the actions that are whitelisted with the allow_oauth! method (See the previous article.)

Write, on the other hand, can be revoked by the user. This permission allows the app to perform any actions using HTTP DELETE, PATCH, and other verbs.

If you need more granular control, modify oPRO’s initializer file:

config/initializers/opro.rb

[...]
config.request_permissions = [:write, :list, :invite]
[...]

Next, you’ll need to enforce checking of these attributes for the desired actions. This is done in the following way:

[...]
require_oauth_permissions :list, only: :index
[...]

If the app does not have the required permission, a 401 error will be raised. To skip permissions checking, use skip_oauth_permissions (just like the default skip_before_action):

[...]
skip_oauth_permissions :list, only: :index
[...]

You can even code your own permission-checking logic by redefining the method oauth_client_can_#{permission}?:

def oauth_client_can_email?
  # your custom logic here
end

Don’t forget that such methods have to return either true or false.

Limiting Access Token’s Lifespan

All popular services provide an access token with a limited lifespan for security purposes and you’ll probably want to do the same. By default, oPRO generates an “eternal” token, but that can be changed, so let’s do it now.

Modify oPRO’s initializer file:

config/initializers/opro.rb

[...]
config.require_refresh_within = 12.hours
[...]

Set the value that works for you and don’t forget to reboot the server. Now, expires_in in the authentication hash will contain a number of seconds equal to token’s lifespan. Let’s convert it to DateTime and store in the table:

models/user.rb

[...]
def from_opro(auth = nil)
  [...]
  user.expires_at = auth['expires_in'].seconds.from_now
  [...]
end
[...]

If a user tries to perform a request with an expired token, oPRO will raise a 401 error (unauthorized), therefore the client app probably needs to rescue from it:

[...]
rescue_from RestClient::Unauthorized do
  flash[:warning] = 'No access rights to perform that action or your token has expired :('
  redirect_to root_path
end
[...]

What I want to do now is check whether a token has expired. If yes – refresh it and save a new one to the table. As you remember, we have a before_action :check_token in the ApiTestsController and that seems like a nice place to perform such check:

api_tests_controller.rb

[...]
def check_token
  redirect_to new_opro_token_path and return if
    !current_user || current_user.token_missing? ||
      (current_user.token_expired? && current_user.refresh_token_missing?)
  if current_user.token_expired?
    updated_current_user = current_user.refresh_token!
    if updated_current_user
      login updated_current_user
    else
      flash[:warning] = "There was an error while trying to refresh your token..."
      redirect_to root_path
    end
  end
end
[...]

If a user has no token or if the token has expired, but no refresh token is present, we ask them to authenticate once again. Otherwise, if the token did expire, refresh it and login the user with a new token.

Here are the new model actions that are employed:

models/user.rb

[...]
def refresh_token_missing?
  !self.refresh_token.present?
end

def token_expired?
  self.expires_at < Time.current
end

def refresh_token!
  return unless token_expired?
  client = OproApi.new(refresh_token: self.refresh_token)
  User.from_opro(client.refresh!)
end
[...]

refresh! is yet another API method:

models/opro_api.rb

[...]
def refresh!
  return unless refresh_token
  JSON.parse(RestClient.post(TOKEN_URL,
                             {
                                 client_id: ENV['opro_client_id'],
                                 client_secret: ENV['opro_client_secret'],
                                 refresh_token: refresh_token
                             },
                             accept: :json))
end
[...]

Refreshing a token is pretty much like getting an access token, except for sending refresh_token parameter instead of code. oPRO responds with a full authentication hash, therefore we pass the response to the User.from_opro method to save the new data.

Now you may edit expires_at field manually and set it to some past date. The refresh token process should be working transparently to the user!

Rate Limiting

Suppose the application is becoming really popular and users employ the API quite extensively. However, you started to notice that some users abuse it too much. This can be handled by limiting the maximum number of API calls per day a user is allowed. By default oPRO does not provide any limitations, however you can easily change that.

The are many ways to achieve this task, therefore oPRO leaves the implementation logic to you. Let’s keep things simple and create two fields in the opro_client_apps table:

  • requests_count (integer) – default is 0. Incremented each time an API request is made;
  • last_request_at (date) – used to track when the last request was made. If the date is smaller than today, set requests_count to 0, otherwise increment it by 1.

Create and apply the corresponding migration:

$ rails g migration add_requests_count_and_last_request_at_to_opro_client_apps requests_count:integer last_request_at:date
$ rake db:migrate

Inside the ApplicationController we need to modify two methods that are initially empty: oauth_client_record_access! and oauth_client_rate_limited?:

application_controller.rb

[...]
private

def oauth_client_record_access!(client_id, params)
  client_app = Opro::Oauth::ClientApp.find(client_id)
  if client_app.last_request_at < Date.today
    client_app.last_request_at = Date.today
    client_app.requests_count = 1
  else
    client_app.requests_count += 1
  end
  client_app.save
end
[...]

Here is how the logic described above can be implemented: If no requests were made today, reset the counter, otherwise increment it by 1. client_id contains the application’s id and params contains parameters sent along with the request (for example, access_token).

application_controller.rb

[...]
def oauth_client_rate_limited?(client_id, params)
  client_app = Opro::Oauth::ClientApp.find(client_id)
  client_app.requests_count > 2
end
[...]

Simply find the required app and check if it has exceeded the threshold. Obviously, for the real app you’ll want to provide a bit larger number of available requests per day. Now try to perform a couple of requests and after a third one you’ll get an “Unauthorized” error – this means that our rate limiting is working as expected!

Other Configuration Options

Using a Custom Authentication Solution

Currently, out-of-the-box oPRO supports only Devise, but you can tweak a couple of settings and bring your own logic into play:

config/initializers/opro.rb

Opro.setup do |config|
  [...]
  config.login_method do |controller, current_user|
    controller.sign_in(current_user, :bypass => true)
  end
  config.logout_method do |controller, current_user|
    controller.sign_out(current_user)
  end
  config.authenticate_user_method do |controller|
    controller.authenticate_user!
  end
  [...]
end

Exchanging E-mail and Password for a Token

If you have access to the user’s e-mail and password, you can still exchange them for an access token. The process is pretty much the same, so let’s update our API adapter:

models/opro_api.rb

[...]
attr_reader :access_token, :refresh_token, :email, :password

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

def authenticate_with_email!
  return unless email && password
  JSON.parse(RestClient.post(TOKEN_URL,
                             {
                                 client_id: ENV['opro_client_id'],
                                 client_secret: ENV['opro_client_secret'],
                                 email: email,
                                 password: password
                             },
                             accept: :json))
end
[...]

As you see, instead of sending code, we provide the user’s credentials. If the email and/or password is incorrect, a 401 error will be raised. Don’t forget that you have to use a secure connection to prevent stealing user’s password!

Apart from that, you may allow this functionality only to some applications. To do that, override oauth_valid_password_auth? inside your ApplicationController:

application_controller.rb

[...]
def oauth_valid_password_auth?(client_id, client_secret)
  client_id == 'some client id'
end
[...]

Of course, this is a very naive implementation, because you’ll likely have an array of permitted client ids (oPRO calls them “blessed ids”) and perform checks against them.

Go ahead and try it out:

$ rails c
$ client = OproApi.new(email: 'test@example.com', password: '123')
$ client.authenticate_with_email!

Please note that if you are using a custom authentication solution like I showed in the previous section, one more step is needed. You have to introduce custom logic to check whether credentials are valid or not.

Luckily, this is done with only one setting:

config/initializers/opro.rb

[...]
config.find_user_for_auth do |controller, params|
  user = User.find(params[:email])
  user.valid_password?(params[:password]) ? user : false
end
[...]

This method should either return a user or false if credentials are invalid.

Conclusion

Finally, you’ve made it to the end of this tutorial. My congratulations! During our journey we’ve discussed many interesting things and hopefully by now you are ready to put this authentication provider into production and extend it further.

Have you ever used oPRO? Would you consider using it in the future? I personally liked working with it even though some things have to be tweaked. If you have any questions, don’t hesitate to ask.

In the upcoming article I am going to cover Doorkeeper – yet another solution to build authentication providers, so see you soon!

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

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.

authenticationGlennGRuby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week