When I was building this API project I got a lot of recommendations to use ‘devise’. I checked the project, but to me, it was simply too much. It was like having an F1 in a rink. However I gave it a try just to be sure I was not overreacting. I wanted to set token based authentication, but for the version I checked it seemed they removed that support. After trying to make it work, I gave up, I told myself there should be an easier way to implement it. Then, I found a great article, written by Jon McCartie. There, he explains an easy way to implement it. I only had to adapt it to my specific situation, and here’s the result.
Token-Based API Authentication
In previous articles I’ve told I’m not treating the User
model as an abstract one. The question now is, why? And the reason has to do with the fact it was going to be a problem to deal with authentications for both Producer
s and Warehouse
s. Now, if you think about it, it does make sense to delegate that issue to the User
model itself. That’s why I’ve chosen an inheritance relationship association, in order to take advantage of it.
Preparing the User
model.
The first step towards the API Token-based authentication is to prepare the User
model to hold tokens. To do it so we have to add a new migration that to include the token for each user and a date to tell when the token was created. The idea is to have tokens being valid not only because of the combination of user’s email and password, but for a max lifetime for each token.
1# /db/migrate/20170502140640_add_auth_token_to_users.rb
2class AddAuthTokenToUsers < ActiveRecord::Migration[5.1]
3 def change
4 add_column :users, :auth_token, :string
5 add_column :users, :token_created_at, :datetime
6 add_index :users, [:auth_token, :token_created_at]
7 end
8end
Run the migration via rake db:migrate
, to have this changes effectively applied.
Adapting the base controller
Since the base controller tells how inherited controllers will behave in a global manner, here we can set the protection for them setting a couple of methods. Here’s where you have to organize how your methods will interact with the public, surely you will allow the public usage of certain methods, and the rest of them only will work if your users are properly authenticated. No matter how that organization happens, here is where you tell your controllers how to authenticate.
1# /app/controllers/application_controller.rb
2class ApplicationController < ActionController::API
3 include ActionController::HttpAuthentication::Token::ControllerMethods
4 include Concerns::Response
5 include Concerns::ExceptionHandler
6
7 before_action :require_login!
8
9 helper_method :user_signed_in?, :current_user
10
11 def require_login!
12 return true if authenticate_token
13 json_response({errors: [{detail: 'Access denied'}]}, :unauthorized)
14 end
15
16 def user_signed_in?
17 current_user.present?
18 end
19
20 def current_user
21 @_current_user ||= authenticate_token
22 end
23
24 private
25 def authenticate_token
26 authenticate_with_http_token do |token, options|
27 User.where(auth_token: token).where('token_created_at >= ?', 1.month.ago).first
28 end
29 end
30end
As you can see, before allowing to execute any action (i.e. execute any controller method), the require_login!
is invoked. It calls the authenticate_token
method, if the user is not properly authenticated, then we have to return an error object with the unauthorized http response. Otherwise it means the user is properly authenticated and they can execute any allowed controller method.
What the authenticate_token
does is to rely on the built-in authenticate_with_http_token
method, which automatically checks the Authorization
request header for a token and passes it as an argument to the upcoming block. Then it will find the user by their token (passed by the built-in method) and check if the token creation date has less than a month of being created.
Then we have two helper methods we can use all around: current_user
and user_signed_in?
.
Signing in & Signing out
Handling sessions is something not hardly related to the API versioning itself. No matter how many versions of your API you have, it’s not likely to change the way you get your users logged in and out. Thus, the routing for the sign-in and sign-out goes as follows in a newer version of the routing file.
1# /config/routes.rb
2Rails.application.routes.draw do
3 ...
4
5 # API Definition
6 scope module: 'api' do
7 post '/sign-in', to: 'sessions#create'
8 delete '/sign-out', to: 'sessions#destroy'
9
10 namespace :v1 do
11 ...
12 end
13 end
14end
Also, the folders and files’ structure will be like this:
1.
2└── app
3 └── controllers
4 ├── api
5 │ ├── sessions_controller.rb
6 │ └── v1
7 │ ├── containers_controller.rb
8 │ └── ...
9 ├── application_controller.rb
10 └── concerns
Let’s check how does the SessionController
looks.
1# /app/controllers/api/sessions_controller.rb
2module Api
3 class SessionsController < ApplicationController
4 skip_before_action :require_login!, only: [:create]
5 def create
6 resource = User.find_by(email: params[:user_login][:email])
7 return invalid_login_attempt unless resource
8
9 if resource.valid_password?(params[:user_login][:password])
10 auth_token = resource.generate_auth_token
11 json_response({token: auth_token, userType: resource.type.to_s.downcase})
12 else
13 invalid_login_attempt
14 end
15 end
16
17 def destroy
18 resource = current_user
19 resource.invalidate_auth_token
20 head :ok
21 end
22
23 private
24 def invalid_login_attempt
25 json_response({errors: [{detail: 'Error with your login or password'}]}, :unauthorized)
26 end
27 end
28end
This controller has two methods: create
and destroy
. Of course, I have to avoid asking for the authentication if I’m about to authenticate, so I’m skipping the require_login!
for the create
method.
It delegates to the User
model the logic to find the user based on the email and validate if the password is correct. Then I’m answering with the token and the user type in the response. Otherwise I sent an unauthorized http response for that combination of user and password.
The destroy
method uses the helper methods defined in the ApplicationController
to get the current_user
and invalidate the current token.
Let’s see how does the User
model looks.
1# /app/models/user.rb
2class User < ApplicationRecord
3 include Localizable
4
5 validates_presence_of :username
6 validates_presence_of :password
7 validates_presence_of :email
8
9 def generate_auth_token
10 token = SecureRandom.hex
11 self.update_columns(auth_token: token, token_created_at: Time.zone.now)
12 token
13 end
14
15 def invalidate_auth_token
16 self.update_columns(auth_token: nil, token_created_at: nil)
17 end
18
19 def valid_password?(psswrd)
20 self.password == psswrd
21 end
22
23 def destroy
24 self.places.update_all(show: false)
25 update_attribute(:auth_token, nil)
26 super
27 end
28end
generate_auth_token
and invalidate_auth_token
basically generates tokens randomly updating the field in the DB, and deletes that token out of the DB respectively.
valid_password?
checks if the password matchs (I know I’m using a super secure password checking strategy here ). Here’s an example of how the sign in request does work:
POST /sign-in HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 76
Content-Type: application/json
{
"user_login": {
"email": "user@provider.com",
"password": "SuperSecurePassword"
}
}
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"814f495c575891543cbe418b8df5d03a"
Transfer-Encoding: chunked
Vary: Origin
X-Request-Id: 6e90a042-9392-412b-bc2f-aa3694ca046b
X-Runtime: 0.013186
{
"token": "b782494fb658ba74b07f414f51fd1008",
"userType": "producer"
}
Let’s check how the sign-out request works:
DELETE /sign-out HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 0
authorization: Token token=b782494fb658ba74b07f414f51fd1008
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: text/html
Transfer-Encoding: chunked
Vary: Origin
X-Request-Id: 2475739c-f7f8-4f69-85f7-1ec743e23d5d
X-Runtime: 0.271955
Security concerns
If you recall the original routes.rb
file, you will notice I used the built-in resources
word. So I could get information about any Producer
no matter if that one was the Producer
I was logged in via http :3000/v1/producers/2
(suppose the logged Producer
had the id #5). It doesn’t feel right to talk about the producers
resource when you have just implemented the authentication methodology. producers
? Nah… Fortunatelly Rails offers a reserved word for that: resource
(read again, it’s singular, not plural). Let’s check the file.
1# /config/routes.rb
2Rails.application.routes.draw do
3 # API Definition
4 scope module: 'api' do
5 post '/sign-in', to: 'sessions#create'
6 delete '/sign-out', to: 'sessions#destroy'
7
8 namespace :v1 do
9 ...
10 resources :products, only: [:index, :show]
11
12 resource :producer, concerns: [:localizable, :user, :user_crops]
13 resource :warehouse, concerns: [:localizable, :user]
14 end
15 end
16end
Advisedly, I’ve let three resources here. Check that for products
I’ve used resources
and it is plural. This generates these routes:
1$ rake routes
2...
3 v1_products GET /v1/products(.:format) api/v1/products#index
4 v1_product GET /v1/products/:id(.:format) api/v1/products#show
5...
Nonetheless, check that for producer
and warehouse
I’m using the reserved word resource
, and they in turn are singular. So they, and any nested routing, will be like:
1$ rake routes
2...
3 v1_producer_places GET /v1/producer/places(.:format) api/v1/places#index
4 POST /v1/producer/places(.:format) api/v1/places#create
5 v1_producer_place GET /v1/producer/places/:id(.:format) api/v1/places#show
6 PATCH /v1/producer/places/:id(.:format) api/v1/places#update
7 PUT /v1/producer/places/:id(.:format) api/v1/places#update
8 DELETE /v1/producer/places/:id(.:format) api/v1/places#destroy
9...
10 v1_producer GET /v1/producer(.:format) api/v1/producers#show
11 PATCH /v1/producer(.:format) api/v1/producers#update
12 PUT /v1/producer(.:format) api/v1/producers#update
13 DELETE /v1/producer(.:format) api/v1/producers#destroy
14 POST /v1/producer(.:format) api/v1/producers#create
15...
16 v1_warehouse_places GET /v1/warehouse/places(.:format) api/v1/places#index
17 POST /v1/warehouse/places(.:format) api/v1/places#create
18 v1_warehouse_place GET /v1/warehouse/places/:id(.:format) api/v1/places#show
19 PATCH /v1/warehouse/places/:id(.:format) api/v1/places#update
20 PUT /v1/warehouse/places/:id(.:format) api/v1/places#update
21 DELETE /v1/warehouse/places/:id(.:format) api/v1/places#destroy
22...
23 v1_warehouse GET /v1/warehouse(.:format) api/v1/warehouses#show
24 PATCH /v1/warehouse(.:format) api/v1/warehouses#update
25 PUT /v1/warehouse(.:format) api/v1/warehouses#update
26 DELETE /v1/warehouse(.:format) api/v1/warehouses#destroy
27 POST /v1/warehouse(.:format) api/v1/warehouses#create
28...
This makes our API smart enough to recognize what logged user are we talking about, and also protect other users sensitive information.
Conclusion
Pre-conclusion actually. I’m planning to have a fully dedicated conclusion article when finishing this series, but I’m compelled to recognize Jon McCartie’s work here. I don’t want to dismiss devise at all, maybe they are good for bigger projects or alternatives, however in Jon McCartie’s words:
As Ruby developers, we have a littany of incredible Gems available to us. And when faced with the question “Should I build this myself? Or use a gem?”, there’s usually incredible value to not re-inventing the wheel. But make sure your reliance on other libraries never blinds you to simpler solutions that can be fully customized to your needs. Sometimes “rolling your own” can actually save you time.