I am building a Web App and a separate API (so that users can share their collected data with someone if they want to) using Ruby on Rails. The users can log in on the web a
Usually it works like this. Your app issues a secret token for every user (it can be for example a md5 hash, it's long, and it's quite random). Token should be kept safe by user. You can do that by following two rules: - never disclose token publicly (all requests should be made from backend, no AJAX calls etc.) - all requests should be made over https, so they are encrypted
Reason to use token instead of username & password? In case token is compromised you can revoke it and user still has control over they account. Also, with token-based authentication certain operations like changing email or password associated with account should not be possible.
Token should be passed as a param with each request done to your API.
Rails uses sessions to keep track of the state of a user, which is stored in the user's cookies.
The documentation for sessions can be found here
If you leverage an authentication system such as Devise, you will have access to a current_user method in your controllers as well as a ton of different helpers which you can leverage, depending on your specific needs
When you say Web app server and a separate API server, which needs to talk to each other every time there is an update from a user on your Web app server. All I can suggest you to break them down to 3 entities as rails engine.
Why Core? Because, when you need to update your business logic, it will be just one place: Core Engine.
Now to answer your question further on authenticating API call from your web app server. You need to:
Once you're done with securing API, you can implement the authentication logic in your Web application. You can use OAuth2 for authenticating your app from API.
Also, to make your API available only to OAuth calls using doorkeeper: https://doorkeeper-provider.herokuapp.com/#client-applications
P.S.: I prefer json response from the APIs, it's a preferred trend I'd say. ;)
EDIT- postman is a chrome extension for making experimental/fake APIs before you actually write them for your application. It's much faster because you'd know what you finally have to design at the end of the day.
You could consider the doorkeeper gem for your API authorization. I considered it but decided against it because of complexity and lacking documentation for my use cases. Put simply I couldn't get it working properly.
There is a good article on authentication using warden without devise which should give you a good feel for the moving parts of an authentication system. Devise is not appropriate for API authentication and in fact Devise recently removed the one thing that could be useful for API's which was token based authentication, obviously API's are not part of their roadmap!
I used the guidance in the article referenced above to create my own JSON only Warden strategy that uses an OAUTH 2 Owner Password Credentials Grant type (See RFC 6749) to generate and return a bearer token for use on future API requests. API clients can easily create the JSON to do this kind of authentication to obtain an authorization access token.
I will provide some of the Rails code to get you started below, but you will have to integrate into your specific environment. No warranty offered :)
Warden initializer:
# config/initializers/warden.rb
Dir["./app/strategies/warden/*.rb"].each { |file| require file }
Rails.application.config.middleware.insert_after ActionDispatch::ParamsParser, Warden::Manager do |manager|
manager.default_strategies :null_auth, :oauth_access_token, :oauth_owner_password
manager.failure_app = UnauthorizedController
end
Warden strategy for OAUTH 2 password authentication:
# app/strategies/warden/oauth_owner_password_strategy.rb
module Warden
class OauthOwnerPasswordStrategy < Strategies::Base
def valid?
return false if request.get?
params['grant_type'] == 'password' && params['client_id'] == 'web' && ! params['username'].blank?
end
def authenticate!
user = User.with_login(params['username']).first
if user.nil? || user.confirmed_at.nil? || ! user.authenticate!(params['password'])
# delay failures for up to 20ms to thwart timing based attacks
sleep(SecureRandom.random_number(20) / 1000.0)
fail! :message => 'strategies.password.failed'
else
success! user, store: false
end
# ADD HERE: log IP and timestamp of all authentication attempts
end
end
Strategies.add(:oauth_owner_password, OauthOwnerPasswordStrategy)
end
Warden strategy for OAUTH 2 access token authentication:
# app/strategies/warden/oauth_access_token_strategy.rb
module Warden
class OauthAccessTokenStrategy < Strategies::Base
def valid?
# must be a bearer token
return false unless auth_header = request.headers['authorization']
auth_header.split(' ')[0] == 'Bearer'
end
def authenticate!
# Use a periodic cleaner instead
# clean out all old tokens. DOES NOT RUN CALLBACKS!
Token.expired.delete
# lookup bearer token
token = Token.active.first(purpose: 'access', token: request.headers['authorization'].split(' ')[1])
if token && (user = token.user) && user.confirmed_at
success! user, store: false
else
# delay failures for up to 20ms to thwart timing based attacks
sleep(SecureRandom.random_number(20) / 1000.0)
fail! message: 'strategies.oauth_access_token.failed'
end
end
end
Strategies.add(:oauth_access_token, OauthAccessTokenStrategy)
end
Null authentication strategy (can be useful in development, just set config.null_auth_user
within config/environments/development.rb):
# app/strategies/warden/null_auth_strategy.rb
module Warden
class NullAuthStrategy < Strategies::Base
def valid?
! Rails.configuration.null_auth_user.blank?
end
def authenticate!
user = User.with_login(params["username"]||Rails.configuration.null_auth_user).first
if user.nil?
fail! :message => "strategies.password.failed"
else
success! user, store: false
end
end
end
Strategies.add(:null_auth, NullAuthStrategy)
end
Warden failure application for JSON clients (uses a bare metal rails controller):
# app/controllers/unauthorized_controller.rb
class UnauthorizedController < ActionController::Metal
def self.call(env)
@respond ||= action(:respond)
@respond.call(env)
end
def respond(env)
self.status = 401
self.content_type = 'json'
self.response_body = { 'errors' => ['Authentication failure']}.to_json
end
end
Add the following in your base API controller:
before_filter :authenticate!
protected
helper_method :warden, :signed_in?, :current_user
def warden
request.env['warden']
end
def signed_in?
!current_user.nil?
end
def current_user
@current_user ||= warden.user
end
def authenticate!(*args)
warden.authenticate!(*args)
# ADD ANY POST AUTHENTICATION SETUP CODE HERE
end
A sessions controller:
class SessionsController < ApiController
skip_before_filter :authenticate!
# TODO exceptions and errors should return unauthorized HTTP response.
# see RFC for details
def create
# mandate the password strategy.
# don't use session store (don't want session cookies on APIs)
authenticate!(scope: :oauth_owner_password, store: false)
if signed_in?
# create access token
token = Token.create! purpose: 'access',
user: current_user,
expires_in: Rails.configuration.session_lifetime
# Ensure response is never cached
response.headers["Cache-Control"] = "no-store"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
# send the OAuth response
render json: {
access_token: token.token,
token_type: 'Bearer',
expires_in: token.expires_in,
scope: 'user'
}
end
end
def destroy
Token.current.delete
warden.logout
head :no_content
end
end
You will need to define your own User and Token models for tracking users and bearer tokens respectively, the Token model needs to have a scope called active
to limit the result set to unexpired tokens. Token generation should use SecureRandom.urlsafe_base64
If you to build json, xml based api, use rabl gem https://github.com/nesquena/rabl.
For simpler authentication, go for session based authentication which rails provided with session variable in controller. If you want neat, some function for user, then go for authologic gem https://github.com/binarylogic/authlogic.
If you want complete user management , then go for devise gem.