Protocol OAuth2: let’s play with Doorkeeper & Omniauth/OAuth2. Part 1.
Know your sandbox: shallow dive into OAuth2 protocol with Rails and Doorkeeper.
Introduction
For several years we and our colleagues from the USA have been engaged in the development of one financial B2B project. The main application had an interface for suppliers, investors and CRM for customer’s employees. Over time the functionality had expanded and there was a need to create a separate application for business partners. So the project was divided for the first time.
The issue
The new project has become a user account for the partners. And the registration of the partners was on the main project and consisted of the following steps:
The CRM User approves the registration request and creates a user with the role of «partner».
After a user account has been created, an email with a pair username / password is sent to his email address.
The partner gets an email and enters the username and password given to him.
Thus there’s a user account for the partner in the local partner database.
The partner application as a client of the main app had to interwork with the latter by means of the API. So there was a problem with authorization requests, particularly with the authorization of the user, on whose behalf the execution of this or that action was requested. OAuth2 helped solve the problem. Everything presented below in this article is the result of the protocol studying process and its implementation on both sides.
OAuth2
First of all let’s get acquainted with key concepts picked up from the authors who presented their ideas in the RFC.
Protocol overview
OAuth2 is an open protocol or a framework, as it is positioned by IETF, which allows the third party access to server resources. The proposed protocol solves 5 problems of the classical client-server authorization scheme:
Third-party applications are required to store the resource owner's credentials for future use, typically a password in clear-text.
Servers are required to support password authentication, despite the security weaknesses inherent in passwords.
Third-party applications gain overly broad access to the resource owner's protected resources, leaving resource owners without any ability to restrict duration or access to a limited subset of resources.
Resource owners cannot revoke access to an individual third party without revoking access to all third parties, and must do so by changing the third party's password.
Compromise of any third-party application results in compromise of the end-user's password and all of the data protected by that password.
Roles
Protocol defines four roles:
- Resource owner.
An entity capable of granting access to a protected resource. When the resource owner is a person, they are referred to as an end-user.
- Resource server.
The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens
- Client.
An application making protected resource requests on behalf of the resource owner and with its authorization. The term "client" does not imply any particular implementation characteristics (e.g., whether the application executes on a server, a desktop, or other devices).
- Authorization server.
The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization.
Fetching the resources
Fetching the resources consists of the following steps:
The client requests permission to access the resources from the resource owner directly or from the authorization server.
The resource owner or authorization server gives permission and passes the grant to the client.
The client sends a request to receive the token access to the authorization server with the grant received earlier.
The authorization server validates the grant and responds with the token access.
The client asks for the required data from the resource server by sending the token access in its request.
The resource server validates the received token access and transmits the required data in response.
Grant is an abstraction representing the permission of the resource owner to a third party to use the data.
Types of grants
The protocol describes the following types of grants:
The code of authorization: The type of grant where the resource owner is redirected to the authorization server where he passes the procedure of providing the access to the client. After that, the authorization server makes a request to the URL specified when the owner of the resource is redirected. It’s considered to be the most acceptable type of grant described in the protocol security-wise since grant transfer occurs directly between the authorization server and the client.
User’s data: username and password. This type of grant is better to use in rare cases when the client is absolutely trusted by the resource owner.
Client’s data.
The theory is best confirmed in practice. In our projects we use Doorkeeper authorization server implementation.
Authorization server implementation
Doorkeeper
Configuration
According to the page at GitHub, Doorkeeper gem is an awesome OAuth2 provider for your Rails app. The easiest way to use Doorkeeper is for implementation of a combined solution ’authentication/resource server’. Installation doesn’t differ from the other gem upload, just add it to your Gemfile.
gem 'doorkeeper'
Then, following the instruction:
rails generate doorkeeper:install
rails generate doorkeeper:migration
rails db:migrate
Configuration Doorkeeper is accomplished through the initializer. The code of resourceownerauthenticator may differ: in this case AuthLogic is used for authorization. In order not to be distracted by unnecessary details we will also skip the client authorization.
Doorkeeper.configure do
resource_owner_authenticator do
if user = UserSession.find.try(:record)
user
else
session[:return_to] = request.url
redirect_to(auth_sign_in_url)
end
end
reuse_access_token
skip_authorization do
true
end
# For Password Grant
resource_owner_from_credentials do |routes|
user = User.find_by_email(params[:username].downcase)
if user && user.valid_password?(params[:password])
user
end
end
end
Now we need MeResource, which would provide user data. OAuth2 protocol is usually used for authorizing requests to the API. Our project will not be an exception: we use Grape for our API.
class V1::MeResource < V1::Base
namespace :me do
get '/' do
{
uid: current_user.id,
email: current_user.email
}
end
end
end
I would like to add that Doorkeeper provides the set of helpers for the integration with Grape API, that makes method doorkeeper_authorize!
available.
require 'doorkeeper/grape/helpers'
class V1::Base < Grape::API
def self.inherited(subclass)
super
subclass.instance_eval do
before do
doorkeeper_authorize!
end
helpers V1::Helpers::Authentication,
Doorkeeper::Grape::Helpers
end
end
end
And helper module itself:
module V1
module Helpers
module Authentication
def current_user
return @current_user if defined?(@current_user)
if doorkeeper_token&.resource_owner_id&.present?
@current_user = User.find(doorkeeper_token.resource_owner_id)
end
end
end
end
end
Password Credentials
This configuration is sufficient to obtain a token access to the resources using an owner password as a grant, and we will do it now. Old faithful cURL will help with this one:
curl -F grant_type=password \
-F username=email@mail.com \
-F password=qwerty123 \
-X POST http://localhost:3000/oauth/token
...
{"access_token":"9c8944c43697b3b0520bed19da180c0dbcc4685a92ee9074692a5cce132d1016","token_type":"bearer","expires_in":7200,"created_at":1519247019}
In its turn we will use the obtained token access to get the resource:
curl -H 'Authorization: Bearer 9c8944c43697b3b0520bed19da180c0dbcc4685a92ee9074692a5cce132d1016' \
http://localhost:3000/me.json
This type of grant is not secure, since it requires the transfer of the username and password to the client to receive token access every time. But in our case it’s not critical, since the client and authorization server are parts of one project.
Authorization Code
In general, the best variant is to use an authorization code. Client registration on the authorization server is usually required for this purpose. Our case won’t be an exception. We will edit the configuration Doorkeeper in order to do it and remove the bypassing of client authorization.
skip_authorization do
true
end
Doorkeeper provides a standard controller and view for the app registration from the box, but being disciples we have the right to cut the path: register a new application in the console.
Doorkeeper::Application.create(name: 'test_client', redirect_uri: 'http://client:3001/auth/auth_server/callback')
#<Doorkeeper::Application:0x007fc514982408
id: 1,
name: "test_client",
uid: "9f311f064a316a9a31ef009006cf578c637220d6f0445bce8f22cb83e69cc830",
secret: "6b0261a3d4170418c581940a80a6fdf8aedafbdcdbe5d3bece04fa87f5c59e3f",
redirect_uri: "http://client:3001/auth/auth_server/callback",
scopes: "",
created_at: Sun, 25 Feb 2018 17:12:05 UTC +00:00,
updated_at: Sun, 25 Feb 2018 17:12:05 UTC +00:00>
In real life we have to use a separate controller for the registration. Keep in mind that any interaction over OAuth2 must be done only over the protected TLS channel in production, but while we have dev it would be more effective to use an unprotected connection - we will leave redirect URI as is.
Now we can get a token access by authorization code by making POST request to /oauth/authorize
. You need to disable the protection from CSRF to make this request work.
curl -v -F response_type=code \
-F client_id=9f311f064a316a9a31ef009006cf578c637220d6f0445bce8f22cb83e69cc830 \
-F client_secret=6b0261a3d4170418c581940a80a6fdf8aedafbdcdbe5d3bece04fa87f5c59e3f \
-F redirect_uri=http://client:3001/auth/auth_server/callback \
-F username=user@mail.com \
-X POST http://localhost:3000/oauth/authorize
...
HTTP/1.1 302 Found
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Location: http://client:3001/auth/auth_server/callback?code=4368dd101f83dee16502015f15039ec339c583909aced4e738c0813a74fa5f27
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache
X-Request-Id: 1318ff5f-f1e0-474c-9ac9-096800a1e2ba
X-Runtime: 0.038971
<html><body>You are being <a href="http://client:3001/auth/auth_server/callback?code=4368dd101f83dee16502015f15039ec339c583909aced4e738c0813a74fa5f27">redirected</a>.</body></html>
Now briefly about the aforementioned: we, acting as a user agent, requested the authorization code. The server processed the request, generated the authorization code and responded with the redirect to the client’s URI. After that the user agent will follow the link specified in the header location, by a lucky chance always coinciding with the link provided by us when there’s a client registration, except for a specific code. Doorkeeper also provides endpoint GET /oauth/authorization/:code
.
curl http://localhost:3000/oauth/authorize/code=4368dd101f83dee16502015f15039ec339c583909aced4e738c0813a74fa5f27
...
<main role="main">
code=4368dd101f83dee16502015f15039ec339c583909aced4e738c0813a74fa5f27
</main>
...
Client Credentials
This type of grant is the simplest one and we have everything fixed for it. But nevertheless it’s better to use it only while getting resources, belonging or being connected directly with a client. An example is the metric "the number of requests from the client."
curl -X POST http://localhost:3000/oauth/token \
-F grant_type=client_credentials \
-F client_id=9f311f064a316a9a31ef009006cf578c637220d6f0445bce8f22cb83e69cc830 \
-F client_secret=6b0261a3d4170418c581940a80a6fdf8aedafbdcdbe5d3bece04fa87f5c59e3f
...
{"access_token":"050b7781f58b63dbf82b6d57f0655fdf3d5c15e1b15a4fe4297aa3dc2387c926","token_type":"bearer","expires_in":7200,"created_at":1519594476}
Doorkeeper also lets us use Basic HTTP authentication, then the only obligatory parameter will be grant_type=client_credentials
, and then client_id
and client_secret
will go to HTTP Basic Authentication header.
Conclusion
So, we have looked at the main aspects of the authorization service implementation with Doorkeeper. In the next article, we'll look at the implementation of the client with Omniauth/OAuth2, it will let us automate the routine procedures of interaction with the authorization server and implement the basic functionality. Also, we’ll return to Doorkeeper to play with Refresh Tokens.