Today, a web-portal that does not support APIs is not even considered a web portal! This post explains how we can implement Rails APIs in our application. Rails APIs require the following minimum implementation:
- API keys.
- API Versioning.
- API Request Authentication.
- Request throttling (optional).
Let’s see how this can be done.
API key generation
Lets generate the model.
$ rails g model User api_key:string
or if the model already exists,
$ rails g migration AddApiKeyToUsers api_key:string
Now lets edit this model and add the logic to generate the API key when a record is created using the method generate_api_key
.
def generate_api_key loop do token = SecureRandom.base64.tr('+/=', 'Qrt') break token unless User.exists?(api_key: token).any? end end
In this code we use SecureRandom.base64.tr('+/=', 'Qat')
to generate a token. Since, it may contain the special characters like +/=
, we shall replace them with some random alphabets using tr
, so that the API key looks good. Now lets add the before_create
callback.
before_create do |doc| doc.api_key = doc.generate_api_key end
API controller and routes versioning
Now, lets generate the controller called Api
in the api
namespace. This helps us avoid the ApplicationController
as APIs do not need the basic controller functionalities like protection from request forgery (the CSRF token), session authentication and the view helpers. Our Api::ApiController
is in fact the ApplicationController
for all our API controllers.
$ rails g controller api/api
To ensure we get basic controller functionality we need our Api::ApiController
to inherit directly from ActionController::Base
. So, be sure to replace the code as follows:
class Api::ApiController < ActionController::Base # Authentication and other filters implementation. end
Lets take an example to help understand all this better. Lets create an API for events that has two fields name
and occurred_at
that tell us which event occurred at which time. We shall write APIs to get events.
$ rails g model event name:string occurred_at:time $ rails g controller api/v1/events
APIs need to be versioned to ensure backward compatibility and future extensibility. The above approach provides us an easy way to do this and keep our code maintainable. Remember to version only when required – v2 would be added when either there is entirely new functionality that would not work with v1 clients, i.e. potentially broken compatibility. (This may be a good time to mention jsonapi.org. This is what the next generation of Web APIs will do, so it’s definitely worth a read).
# app/controllers/api/v1/events_controller.rb class Api::V1::EventsController < Api::ApiController end
Routes versioning
This is an art and you should ensure that your routes are well designed. Here is an example.
namespace :api do namespace :v1 do resources :events end end
This will generate the routes with ‘api/v1’ namespace prefix.
Prefix Verb URI Pattern Controller#Action api_v1_events GET /api/v1/events(.:format) api/v1/events#index {:format=> :json} POST /api/v1/events(.:format) api/v1/events#create {:format=> :json} new_api_v1_event GET /api/v1/events/new(.:format) api/v1/events#new {:format=> :json} edit_api_v1_event GET /api/v1/events/:id/edit(.:format) api/v1/events#edit {:format=> :json} api_v1_event GET /api/v1/events/:id(.:format) api/v1/events#show {:format=> :json} PATCH /api/v1/events/:id(.:format) api/v1/events#update {:format=> :json} PUT /api/v1/events/:id(.:format) api/v1/events#update {:format=> :json} DELETE /api/v1/events/:id(.:format) api/v1/events#destroy {:format=> :json}
If want to remove the API prefix (/api) and use a subdomain like api.example.com
, then we can use options like path
and constraints
namespace :api, path: '', constraints: {subdomain: 'api'} do namespace :v1 do resources :events end end
Using path
option we can change the namespace name. Now routes look like this
Prefix Verb URI Pattern Controller#Action api_v1_events GET /v1/events(.:format) api/v1/events#index {:format=> :json, :subdomain=> "api"} POST /v1/events(.:format) api/v1/events#create {:format=> :json, :subdomain=> "api"} new_api_v1_event GET /v1/events/new(.:format) api/v1/events#new {:format=> :json, :subdomain=> "api"} edit_api_v1_event GET /v1/events/:id/edit(.:format) api/v1/events#edit {:format=> :json, :subdomain=> "api"} api_v1_event GET /v1/events/:id(.:format) api/v1/events#show {:format=> :json, :subdomain=>"api"} PATCH /v1/events/:id(.:format) api/v1/events#update {:format=>:json, :subdomain=>"api"} PUT /v1/events/:id(.:format) api/v1/events#update {:format=> :json, :subdomain=> "api"} DELETE /v1/events/:id(.:format) api/v1/events#destroy {:format=>:json, :subdomain=> "api"}
If we want to process every request as a JSON request by default, then add format: :json
to the defaults
in the namespace options. So any request will now be considered as a JSON request and will give JSON response.
namespace :api, path: '', constraints: {subdomain: 'api'}, defaults: {format: 'json'}
Here is an example of our API controller action.
class Api::V1::EventsController < Api::ApiController respond_to :json def index @events = Event.all respond_with @events end end
API Authentication
This is important and we can implement the authentication in the Api::ApiController
itself so that it is applicable for all the API controllers. There are different ways to authenticate:
- Use the inbuilt rails method
authenticate_or_request_with_http_token
- Do it manually.
If we use authenticate_or_request_with_http_token
(ref: token auth api), our code looks like this.
class Api::ApiController < ActionController::Base private def authenticate authenticate_or_request_with_http_token do |token, options| @user = User.where(api_key: token).first end end end
Now, in our EventsController
we need to ensure we add the filter. (Note: Rails3 would require this to be before_filter
).
class Api::V1::EventsController < Api::ApiController respond_to :json before_action :authenticate def index @events = Event.all respond_with @events end end
Now, to test this API out we can use curl.
$ curl -H "Authorization: Token token=WwEsPpOCYMsyPsmmvKBqQDOaEJ4t" http://localhost:3000/events
If we pass the api_key
in the request header (i.e the X-API-KEY
header field), then we need to do the authentication manually.
def authenticate api_key = request.headers['X-Api-Key'] @user = User.where(api_key: api_key).first if api_key unless @user head status: :unauthorized return false end end
Again, we can test this with curl.
$ curl -H "X-Api-Key: WwEsPpOCYMsyPsmmvKBqQDOaEJ4t" http://localhost:3000/v1/events.json
NOTE 1 Never pass the API key in the params(for example: http://test.com?api_key= WwEsPpOCYMsyPsmmvKBqQDOaEJ4t
or in the POST or PUT parameters). The params will get logged and the API keys would be seen in the logs. If someone has access to your logs or purges the logs or even by mistake pastes a log snippet on stackoverflow, then God help you! We can of course use filtering configuration in our application but the best way is to avoid this situation and pass the API key in the request header always.
NOTE 2 Always use the HTTP status codes for different response status to make the HTTP status code more readable.
render status: :unauthorized # 401 render status: :too_many_requests # 424 render status: :unprocessable_entity # 422 # and many more!
In my next post, I shall talk about Request throttling. The information here is enough for you to get started on writing good Rails APIs that are versioned, clean and authenticated. If you want to see a sample working code, see Github.
Feedback is welcome.
Reblogged this on bob-roberts.net.
For most up-to-date information you have to visit internet and on the web I found this website as
a most excellent website for hottest updates.
Thanks for the link to a github example! Going to give this a try since Devise has dropped support for tokenized auth 😦
Great post, thanks for the clarity on security with passing Auth tokens in request headers.
Hi Am getting following error when I follow this blog…, Please help me out
NoMethodError in Devise::RegistrationsController#create
undefined method `exists’ for #
Extracted source (around line #36):
loop do
token = SecureRandom.base64.tr(‘+/=’, ‘Qrt’)
break token unless User.exists ? api_key: token.any?
end
end
Rails.root: /home/ubuntu/workspace/sample_app
Application Trace | Framework Trace | Full Trace
app/models/user.rb:36:in `block in generate_api_key’
app/models/user.rb:34:in `loop’
app/models/user.rb:34:in `generate_api_key’
app/models/user.rb:30:in `block in ‘
Great post!!!
One question: when do you pass de token to the user client? In the response after a successful login?
and does the token change on each sign ?
after each sign in ? ***
It’s depend on you. You can set expire token field.
Greatest tutorial!!
User.exists?(api_key: token) will return a boolean value. ‘any?’ should not be called on the result of exists?.
Documentation: http://apidock.com/rails/ActiveRecord/FinderMethods/exists%3F
Otherwise, helpful 🙂
Is this secure for large-scale application? If not, what should I use to accomplish this?
Nice overview! Thank you for writing this.