I have my Banks Controller
class Api::V1::BanksController < ApplicationController
before_action :authenticate_user!
respond_to :json
# PUT /api/v1/banks/:id.json
def update
#bank = UserBank.find_by!(uuid: params[:id])
if #bank.update_attributes bank_params
render json: #bank
else
render json: #bank.errors, status: :unprocessable_entity
end
end
private
def bank_params
params.require(:bank).permit(:iban, :bic)
end
end
I'm using devise for the authentication. My problem comes from the fact that any users can update another user's bank object just by getting the access-token from the login response.
Is there a clean/secure/automatic way of preventing a user to interact with somebody else's details ?
or should I just make sure that the bank object I'm updating belongs to the logged-in user ?
thanks a lot
Your mixing up authentication and authorization.
Authentication is concerned with the identity of the user.
Authorization is a set of rules for who is allowed to do what in your application.
Devise provides authentication, you can either create your own authorization system or use a library (recommended) such as Pundit or CanCanCan.
A hacky home rolled authorization check would look like:
class AuthorizationError < StandardError; end
class ApplicationController
rescue_from AuthorizationError, with: :deny_access
def deny_access
head :unauthorized and return
end
end
class Api::V1::BanksController < ApplicationController
before_action :authenticate_user!
before_action :set_bank!
before_action :authorize!
respond_to :json
# PUT /api/v1/banks/:id.json
def update
#bank = UserBank.find_by!(uuid: params[:id])
respond_with #bank.update(bank_params)
end
private
def set_bank!
#bank = UserBank.find_by!(uuid: params[:id])
end
def authorize!
# this is the authorization rule.
unless #bank.user == current_user
raise AuthorizationError
end
end
def bank_params
params.require(:bank).permit(:iban, :bic)
end
end
Related
I have a Ruby on Rails backend, currently using cookies & sessions to authenticate and login users. I believe session's default end time is when the user closes their browser, but I want the session to end 1 hour after being created.
Below are the controllers used to create the session and authenticate users:
sessions_controller.rb
class SessionsController < ApplicationController
skip_before_action :authorize, only: [:create]
include ::ActionController::Cookies
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
render json: user
else
render json: {errors: "check email and password"}, status: :unauthorized
end
end
def destroy
session.delete :user_id
head :no_content
end
end
users_controller.rb
class UsersController < ApplicationController
skip_before_action :authorize, only: [:create]
def show
render json: #current_user
end
end
application_controller.rb
class ApplicationController < ActionController::API
include ActionController::Cookies
before_action :authorize
private
def authorize
#current_user ||= User.find_by(id: session[:user_id])
render json: {errors: "Not authorized"}, status: :unauthorized unless #current_user
end
end
You can set the expiry time in an intializer like so:
Rails.application.config.session_store :cookie_store, key: '_your_custom_session_key', expire_after: 1.hour.to_i
This will make sure your sessions expire after 1 hour. You can try with a smaller time frame like 1.minute to verify.
I am working on rails and trying to make a simple blog site and its working the way i want to on my local machine but when pushed to production its being blocked by the callback functions.
My before_action :authorized_user? callback is being called and it prompts for logging if not logged in for performing any method on the blog , and if logged in all methods create, update and destroy methods are working perfectly in my development environment but in production even after the user is logged in also and when the create method is being called it asks for to log in . I am unable to understand from where or what code is causing this to happen because the same is working perfectly fine on local machine.
Any help will he highly appreciated.
My blog_controller.rb file is
class BlogsController < ApplicationController
before_action :set_blog, only: [:show, :update, :destroy, :lock_blog, :pin_blog]
before_action :authorized_user?, except: [:index, :show]
def index
#blogs = Blog.all
render json: { blogs: #blogs },status: :ok
end
def show
comments = #blog.comments.select("comments.*, users.username").joins(:user).by_created_at
render status: :ok, json: { blog: #blog, blog_creator: #blog.user, comments: comments }
end
def create
#blog = Blog.new(blog_params.merge(user_id: #current_user.id))
if authorized?
if #blog.save
render status: :ok,
json: {blog: #blog , notice: "Blog Successfully created"}
else
errors = #blog.errors.full_messages.to_sentence
render status: :unprocessable_entity, json: {error:errors}
end
end
end
def update
if authorized?
if #blog.update(blog_params)
render status: :ok,
json: {blog: #blog, notice:"Blog successfully updated"}
else
render status: :unprocessable_entity,
json: {errors: #blog.errors.full_messages.to_sentence}
end
else
handle_unauthorized
end
end
def destroy
if authorized?
if #blog.destroy
render status: :ok,
json: {notice:'Blog deleted'}
else
render status: :unprocessable_entity,
json: {errors: #blog.errors.full_messages.to_sentence}
end
else
handle_unauthorized
end
end
private
def set_blog
#blog = Blog.find(params[:id])
end
def blog_params
params.require(:blog).permit(:title,:body,:image,:is_pinned, :is_locked)
end
def authorized?
#blog.user_id == #current_user.id || #current_user.admin_level >= 1
end
def handle_unauthorized
unless authorized?
render json:{notice:"Not authorized to perform this task"}, status:401
end
end
end
and application_controller.rb file is
class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token
include CurrentUserConcern
include ExceptionHandlerConcern
include TokenGenerator
def authorized_user?
render json: { notice: 'Please log in to continue' }, status: :unauthorized unless #current_user
end
def authorized_admin?
authorized_user?
render json: {errors: 'Insufficient Administrative Rights'}, status: 401
end
private
end
current_user_concern.rb file
module CurrentUserConcern
extend ActiveSupport::Concern
included do
before_action :set_current_user
end
def set_current_user
if session[:token]
#current_user = User.find_by(token: session[:token])
end
end
end
Its generally recommended to use libraries for authentication and authorization instead of reinventing the wheel unless its for learning purposes. They have many eyes looking for bugs and insecurites and are battle hardened by tons of users. Home-rolled authentication systems are a very common source of security breaches which could lead to very expensive consequences.
If you're going to roll your own authorization and authentication solution I would suggest you take a page from the libraries like Devise, Pundit and CanCanCan and raise an error when a user is not authorized or authenticated so that you immediately halt whatever the controller is doing and stop the callback chain from executing further.
# app/errors/authentication_error.rb
class AuthenticationError < StandardError; end
# app/errors/authorization_error.rb
class AuthorizationError < StandardError; end
# app/controllers/concerns/
module Authenticable
extend ActiveSupport::Concern
included do
helper_method :current_user, :user_signed_in?
before_action :authenticate_user
rescue_from AuthenticationError, with: :handle_unauthorized
end
def current_user
#current_user ||= find_user_from_token if session[:token].present?
end
def find_user_from_token
User.find_by(token: session[:token])
end
def user_signed_in?
current_user.present?
end
def authenticate_user
raise AuthenticationError.new('Please log in to continue') unless user_signed_in?
end
def handle_unauthenticated(error)
render json: {
notice: error.message
},
status: :unauthorized
end
end
end
# app/controllers/concerns/authorizable.rb
module Authorizable
extend ActiveSupport::Concern
included do
rescue_from AuthenticationError, with: :handle_unauthorized
end
def authorize_admin
raise UserAuthenticationError.new('Insufficient Administrative Rights') unless current_user.admin?
end
def handle_unauthorized(error)
render json:{
notice: error.message
}, status: :unauthorized
end
end
class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token
include Authenticable
include Authorizable
# Should you really be mixing this into the controller? Seperate the responsibilites!
include TokenGenerator
end
It also makes debugging much easier as you can disable rescue_from in testing so that you get an exception instead of just a cryptic failure message.
You should also setup your authorization system so that it always authenticates (and authorizes) unless you explicitly opt out. This is a best practice that reduces the possible of security breaches simply due to programmer omission. You opt out by calling skip_before_action :authorize_user.
Instead of your set_current_user use a memoized getter method (current_user) to remove issues caused by the ordering of callbacks. ||= is conditional assignment and will prevent it from querying the database again if you have already fetched the user. This should be the ONLY method in the system that knows how the user is stored. Do not access #current_user directly to avoid leaking the implementation details into the rest of the application.
Methods ending with ? are by convention predicate methods in Ruby and should be expected to return a boolean. Name your modules by what their responsibility is and not what code they contain - avoid the postfix Concern as it tells you nothing about what it does.
I am currently building a simple web app with Ruby on Rails that allows logged in users to perform CRUD actions to the User model. I would like to add a function where:
Users can select which actions they can perform per controller;
Ex: User A can perform actions a&b in controller A, whereas User B can only perform action B in controller A. These will be editable via the view.
Only authorized users will have access to editing authorization rights of other users. For example, if User A is authorized, then it can change what User B will be able to do, but User B, who is unauthorized, will not be able to change its own, or anyone's performable actions.
I already have my users controller set up with views and a model
class UsersController < ApplicationController
skip_before_action :already_logged_in?
skip_before_action :not_authorized, only: [:index, :show]
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
redirect_to users_path
else
render :new
end
end
def show
set_user
end
def edit
set_user
end
def update
if set_user.update(user_params)
redirect_to user_path(set_user)
else
render :edit
end
end
def destroy
if current_user.id == set_user.id
set_user.destroy
session[:user_id] = nil
redirect_to root_path
else
set_user.destroy
redirect_to users_path
end
end
private
def user_params
params.require(:user).permit(:email, :password)
end
def set_user
#user = User.find(params[:id])
end
end
My sessions controller:
class SessionsController < ApplicationController
skip_before_action :login?, except: [:destroy]
skip_before_action :already_logged_in?, only: [:destroy]
skip_before_action :not_authorized
def new
end
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to user_path(user.id), notice: 'You are now successfully logged in.'
else
flash.now[:alert] = 'Email or Password is Invalid'
render :new
end
end
def destroy
session[:user_id] = nil
redirect_to root_path, notice: 'You have successfully logged out'
end
end
The login/logout function works, no problem there.
I started off by implementing a not_authorized method in the main application controller which by default prevents users from accessing the respective actions if the user role is not equal to 1.
def not_authorized
return if current_user.nil?
redirect_to users_path, notice: 'Not Authorized' unless current_user.role == 1
end
the problem is that I would like to make this editable. So users with role = 1 are able to edit each user's access authorization, if that makes sense.
How would I go about developing this further? I also do not want to use gems, as the sole purpose of this is for me to learn.
Any insights are appreciated. Thank you!
The basics of an authorization system is an exception class:
# app/errors/authorization_error.rb
class AuthorizationError < StandardError; end
And a rescue which will catch when your application raises the error:
class ApplicationController < ActionController::Base
rescue_from 'AuthorizationError', with: :deny_access
private
def deny_access
# see https://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses
redirect_to '/somewhere', status: :forbidden
end
end
This avoids repeating the logic all over your controllers while you can still override the deny_access method in subclasses to customize it.
You would then perform authorization checks in your controllers:
class ThingsController
before_action :authorize!, only: [:update, :edit, :destroy]
def create
#thing = current_user.things.new(thing_params)
if #thing.save
redirect_to :thing
else
render :new
end
end
# ...
private
def authorize!
#thing.find(params[:id])
raise AuthorizationError unless #thing.user == current_user || current_user.admin?
end
end
In this pretty typical scenario anybody can create a Thing, but the users can only edit things they have created unless they are admins. "Inlining" everything like this into your controllers can quickly become an unwieldy mess through as the level of complexity grows - which is why gems such as Pundit and CanCanCan extract this out into a separate layer.
Creating a system where the permissions are editable by users of the application is several degrees of magnitude harder to both conceptualize and implement and is really beyond what you should be attempting if you are new to authorization (or Rails). You would need to create a separate table to hold the permissions:
class User < ApplicationRecord
has_many :privileges
end
class Privilege < ApplicationRecord
belongs_to :thing
belongs_to :user
end
class ThingsController
before_action :authorize!, only: [:update, :edit, :destroy]
# ...
private
def authorize!
#thing.find(params[:id])
raise AuthorizationError unless owner? || admin? || privileged?
end
def owner?
#thing.user == current_user
end
def admin?
current_user.admin?
end
def privileged?
current_user.privileges.where(
thing: #thing,
name: params[:action]
)
end
end
This is really a rudimentary Role-based access control system (RBAC).
I have an application that is using both Devise and Knock. It is using Devise to power the authentication for Active Admin and Knock gem is providing the authentication for my API's
The issue I have is that Knock can't seem to find current_user and I believe this is likely because I am using Devise in the same project.
I have the following setup:
Api Controller
class ApiController < ActionController::API
include Knock::Authenticable
end
User Controller (for API not ActiveAdmin)
module Api
module V1
class UsersController < ApiController
before_action :set_user, only: [:show, :update, :destroy]
# GET /users/1
def show
render json: #user
end
# POST /users
def create
#user = User.new(user_params)
if #user.save
render json: #user.id, status: :created
else
render json: #user.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /users/1
def update
if #user.update(user_params)
render json: #user
else
render json: #user.errors, status: :unprocessable_entity
end
end
# DELETE /users/1
def destroy
#user.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
#user = User.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
end
end
Auth Controller
module Api
module V1
class AuthController < ApiController
def auth
render json: { status: 200, user: current_user }
end
end
end
end
Current User in this Auth controller returns nothing however in another project I have, without devise, this will correctly return the user.
Is there a way to redefine what current_user is or assign it to something different for the purposes of using Knock?
Try this in your ApplicationController
# JWT: Knock defines it's own current_user method unless one is already
# defined. As controller class is cached between requests, this method
# stays and interferes with a browser-originated requests which rely on
# Devise's implementation of current_user. As we define the method here,
# Knock does not reimplement it anymore but we have to do its thing
# manually.
def current_user
if token
#_current_user ||= begin
Knock::AuthToken.new(token: token).entity_for(User)
rescue
nil
end
else
super
end
end
private
# JWT: No need to try and load session as there is none in an API request
def skip_session
request.session_options[:skip] = true if token
end
# JWT: overriding Knock's method to manually trigger Devise's auth.
# When there is no token we assume the request comes from the browser so
# has a session (potentially with warden key) attached.
def authenticate_entity(entity_name)
if token
super(entity_name)
else
current_user
end
end
I have two layouts Admin and Domain. And I don't need any extra configuration in Admin layout. but if user tries to access Domain layout they must be in their valid domain.
This means that, I need to customize all of my Domain policy to include both current_user as well as current_domain. I found this can be done with UserContext and pundit_user... so here is what I have done:
application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
def pundit_user
UserContext.new(current_user, current_domain)
end
def after_sign_out_path_for(resource)
root_path
end
def current_domain
#current_domain ||= Domain.where(name: requested_domain).first
end
helper_method :current_domain
private
def requested_domain
return request.env["SERVER_NAME"]
end
def user_not_authorized
# reset_session
flash[:alert] = "You are not authorized to perform this action"
redirect_to(request.referrer || root_path)
end
end
Note that, when I access Admin layout, current_domain will be nil and if I visit any routes of Domain layout, then current_domain will set to currently accessing domain.
user_context.rb
class UserContext
attr_reader :current_user, :current_domain
def initialize(current_user, current_domain)
#current_user = current_user
#current_domain = current_domain
end
end
PROBLEM
Suppose I have this policy:
user_policy.rb
class UserPolicy < ApplicationPolicy
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def index?
binding.pry # debugging
current_user.admin? ||
current_user.domain == current_domain
end
private
def current_user
# return user.is_a?(User) ? user : user.current_user
user.current_user
end
def current_domain
# return user.is_a?(User) ? nil : user.current_domain
user.current_domain
end
end
when application runs current_user and current_domain must available in UserPolicy as per documentation(https://github.com/elabs/pundit#additional-context).
But I am getting
undefined method `current_user' for #<User:0x007fcefbc2b150>
That means, still I have user object in it, not user.current_user and user.current_domain
Please let me know, if you need further description. What am I missing here?
It was my own dumb mistake.
PROBLEM
I had a before_filter call in domain/base_controller.rb something like:
class Domain::BaseController < ApplicationController
before_action :authenticate_user!
before_action :domain_exists?
before_action :verify_domain!
private
def verify_domain!
# PROBLEM: this line was updating pundit_user again to user object
raise Pundit::NotAuthorizedError unless DomainConsolePolicy.new(current_user, current_domain).authorized?
end
def domain_exists?
if current_domain.blank?
redirect_to root_path, alert: 'Domain that you provided is not valid or is permanently removed!'
end
end
end
SOLUTION:
I have used headless policy for this because now I have both current_user and current_domain set with pundit_user in application_controller
domain/base_controller.rb
class Domain::BaseController < ApplicationController
before_action :authenticate_user!
before_action :domain_exists?
before_action :verify_domain!
private
def verify_domain!
# SOLUTION
authorize :domain_console, :has_access?
end
def domain_exists?
if current_domain.blank?
redirect_to root_path, alert: 'Domain that you provided is not valid or is permanently removed!'
end
end
end
policy/domain_console_policy.rb
class DomainConsolePolicy < Struct.new(:user, :domain_console)
def has_access?
user.current_user.admin? ||
user.current_user.domain_id == user.current_domain.id
end
end
Thanks