Skip to content

Instantly share code, notes, and snippets.

@kivanio
Forked from Fire-Dragon-DoL/application_controller.rb
Last active August 29, 2015 14:16
Show Gist options
  • Save kivanio/1b40410bb5bc9c08a409 to your computer and use it in GitHub Desktop.
Save kivanio/1b40410bb5bc9c08a409 to your computer and use it in GitHub Desktop.
class ApplicationController < ActionController::API
include AbstractController::Translation
include ActionController::MimeResponds
include ActionController::ImplicitRender
include ActionController::StrongParameters
# Gems that tries to include something in ActionController::Base
include JSend::Rails::Controller
include CanCan::ControllerAdditions
include ::TokenAuthentication # HERE FOR TOKEN AUTHENTICATION <-- ONLY REQUIRED LINE
rescue_from ::CanCan::AccessDenied, with: :cancan_to_api_error
rescue_from ::ApiError, with: :render_api_error
protected
def cancan_to_api_error(error)
raise ::AuthorizationError.from_cancan(error)
end
def render_api_error(error)
render_jsend error: error.message, code: error.code, data: {
backtrace: error.backtrace
}
end
end
module TokenAuthentication
extend ActiveSupport::Concern
included do
private :authenticate_user_with_token!
private :fetch_username
private :fetch_token
attr_reader :current_user
before_action :authenticate_user_with_token!
end
def fetch_username
username = params[:username]
username ||= request.headers['X-UserSession-Username']
username ||= request.headers['X_UserSession_Username']
end
def fetch_token
token = params[:token]
token ||= request.headers['X-UserSession-Token']
token ||= request.headers['X_UserSession_Token']
end
def authenticate_user_with_token!
username = fetch_username
token = fetch_token
if username.nil? || token.nil?
raise ::AuthenticationError, I18n.t('authentication.failure.invalid_username')
end
@current_user ||= User.find_by(username: username).try(
:authenticate_with_token,
token
)
# XXX: Token expiration checked in :authenticate_with_token
unless current_user
raise ::AuthenticationError, I18n.t('authentication.failure.invalid_token')
end
current_user.renew_token!
end
end
class SignInUser
include Interactor
def perform
user = User.find_by(username: context[:username])
if user.nil?
context.fail! message: I18n.t('authentication.failure.invalid_username')
else
if user.authenticate(context[:password])
user.generate_token! if user.token_expired?
user.renew_token
user.save!
context[:user] = user
else
context.fail! message: I18n.t('authentication.failure.invalid_password')
end
end
end
end
class SignOutUser
include Interactor
def perform
user = context[:user]
user.token = nil
user.token_updated_at = nil
user.save!
end
end
module Tokenable
TOKEN_EXPIRATION = 1.hours
# NOTE: The two following methods are copied from Devise v3.3.0
# https://github.com/plataformatec/devise/blob/df43642cd5e5a4ed666fc8e57a1bee46de864394/lib/devise.rb
# Generate a friendly string randomly to be used as token.
def self.friendly_token
SecureRandom.urlsafe_base64(15).tr('lIO0', 'sxyz')
end
# constant-time comparison algorithm to prevent timing attacks
def self.secure_compare(a, b)
return false if a.blank? || b.blank? || a.bytesize != b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
def generate_token
loop do
auth_token = Tokenable.friendly_token
unless self.class.find_by(token: auth_token)
break auth_token
end
end
end
def generate_token!
self.token = generate_token
end
def renew_token
self.token_updated_at = Time.zone.now
end
def renew_token!
renew_token
save!
end
def token_expire_in
TOKEN_EXPIRATION
end
def token_expired?
return true if token_updated_at.nil?
(token_updated_at + token_expire_in) < Time.zone.now
end
def authenticate_with_token(token)
return nil if self.token.nil? || token_expired?
Tokenable.secure_compare(self.token, token) ? self : nil
end
end
class User < ActiveRecord::Base
include Tokenable
has_secure_password
end
class UserSessionsController < ApplicationController
skip_before_action :authenticate_user_with_token!, only: [:create]
def create
authorize! :create, UserSessionsController
if SignInUser.perform_on(self, users_session_params).success?
render_jsend success: {
username: @user.username,
token: @user.token,
role: @user.role
}
else
render_jsend fail: { message: @message }
end
end
def destroy
authorize! :destroy, UserSessionsController
SignOutUser.perform(user: current_user)
render_jsend :success
end
private
def users_session_params
params.require(:user_session).permit(:username, :password)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment