I use the Rails Stack with
devise
warden
confirmable
Now I have a certain requirement related to email confirmation and access provision to unverified users. Let's say there are 3 categories of pages:
case 1 - requires no authentication.
case 2 - requires authentication and also require the user to be confirmed.
case 3 - requires authentication (correct username and password combination) but user need not be confirmed to access them.
With devise and confirmable, implementing case 1 and case 2 is a breeze. Once the user does login/signup, I redirect to "confirm your email page".
My problem is with case 3. Suppose the user does his login/signup. He is yet to confirm his email address but should be allowed to visit certain case 3 routes. When he visits a case 3 routes, I expect:
no redirection
valid session
Devise with confirmable either allows all the pages to be visited by confirmed users or none. It does not allow access to certain pages with authentication but without confirmation.
I tried overriding the devise confirmed? by implementing this logic:
class ApplicationController < ActionController::Base
before_filter :verify_user
def verify_user
$flag = true if CERTAIN_ROUTES.include?(action_class)
end
end
class User < ActiveRecord::Base
def confirmed?
$flag || !!confirmed_at
end
end
This barely works for sign in but not for sign up. Also, this is an extremely bad way to achieve it. How should I approach this problem? Other functionalities work fine.
Instead of overwriting confirmed? you could just overwrite the confirmation_required? model method (docs):
# user.rb
class User < ActiveRecord::Base
protected
def confirmation_required?
false
end
end
After that you can handle the confirmation logic yourself, by redirecting all unconfirmed users in a before_action when the controllers require confirmation, or you can pull this into your authorization logic (e.g. with pundit).
class ApplicationController < ActionController::Base
def needs_confirmation
redirect_to root_path unless current_user.confirmed?
end
end
class SomeController < ApplicationController
before_action :needs_confirmation
end
You should take a look at the gem 'pundit' - it works well with devise.
https://github.com/varvet/pundit
Rather than writing controller before_actions etc, you write policies which will cover each of your authorization requirements, and then use those policies inside your controllers.
For example, in a controller:
class ExampleController < ApplicationController
before_action { authorize :example }
def case_one
# action
end
def case_two
# action
end
def case_three
# action
end
end
Then your policy would be kept under app/policies/example_policy.rb
class ExamplePolicy < ApplicationPolicy
attr_reader :user
def initialize(user, _)
#user = user
end
def case_one?
true
end
def case_two?
user.present? && user.confirmed_at.present?
end
def case_three?
user.present?
end
end
It works really well, especially in other cases where you are determining authorization against a type of resource.
Related
I'm using Devise to authenticate User in my Rails 7 app and Pundit for authorization. I would like to extend the standard login flow to check if the user has met the 2FA requirements. 2FA is simple and delivered by an external microservice - the user at each login enters the SMS (text message) code he got on his phone.
In a nutshell new login flow should be:
user authentication via Devise
allow to authorize if session[:_2fa] == 'success'
redirect to provide_code_path if session[:_2fa] == 'failed'
Because the user must go through the 2FA process every time he logs in, I've store such info inside the session params as session[:_2fa]. Now to make all the authorization and redirect dance I want to have access to that session to allow or not. I'm aware of this topic but it's 7y old so maybe today there is some modern approach instead of creating fake model just to get access to the session?
Code below:
# application_controller.rb
class ApplicationController < ActionController::Base
before_action :authenticate_user!, :authorized_user
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def authorized_user
authorize :global, :_2fa_passed?
end
private
def user_not_authorized
flash[:alert] = t('errors.user_not_authorized')
redirect_to provide_code_path
end
end
# global_policy.rb
class GlobalPolicy < ApplicationPolicy
def _2fa_passed?
session[:_2fa] == 'success'
end
end
I have a table of users with enum user_type [Manager, Developer, QA]. Currently, I'm handling sign in using Devise and after login I'm using the following logic to display the appropriate webpage:
class HomeController < ApplicationController
before_action :authenticate_user!
def index
if current_user.manager?
redirect_to manager_path(current_user.id)
end
if current_user.developer?
redirect_to developer_path(current_user.id)
end
if current_user.quality_assurance?
redirect_to qa_path(current_user.id)
end
end
end
I want to use pundit gem to handle this. From the documentation, it transpired that this logic will be delegated to policies but I can't figure out how. Can somebody help me in implementing pundit in my project?
This is my users table:
I have created a user_policy but its mostly empty:
class UserPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
end
User model:
You want to use Pundit to authorize a user, as in check if that user should be allowed to visit a controller action. If the user is not authorized for a specific action it raises a Pundit::NotAuthorizedError
You can check if a user is allowed to perform an action in the pundit policy, in which you have access to record (the instance thats passed to authorize) and user. So assuming you have a Flat Model, where only the owner can edit the Flat you might do this:
# flats_policy.rb
def edit?
record.user == user
end
Now lets say you also want to allow admins to edit you might do this
# flats_policy.rb
def owner_or_admin?
record.user == user || user.admin # where admin is a boolean
end
def edit?
owner_or_admin?
end
and the controller:
# flats_controller.rb
def edit
#flat = Flat.find(params[:id])
authorize #flat
# other code here
end
Now the index action is the odd one out because you would essentially have to call authorize on each instance, so the way Pundit handles this is with the Scope:
# flats_policy.rb
class Scope < Scope
def resolve
scope.all
end
end
and a corresponding index action might look like:
def index
#flats = policy_scope(Flat) # note that we call the model here
end
So lets say a user can only see flats that he/she owns:
# flats_policy.rb
class Scope < Scope
def resolve
scope.where(user: user)
end
end
and if admins can see all flats:
# flats_policy.rb
class Scope < Scope
def resolve
if user.admin
scope.all
else
scope.where(user: user)
end
end
end
In any case if the user is not allowed to perform an action you can rescue from the error like so:
# application_controller
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_to(root_path)
end
I guess you could do some dirty redirecting here, as in send admins to an admins_root_path, users to a default_root_path and so on...
On a final note, since this post is already too long you can check a policy in the view like this:
<% if policy(restaurant).edit? %>
You can see me if you have edit rights
<% end %>
I'm trying to make signed up users to take some actions on my website, so I want to send them, by e-mail, a link directly to this action.
The problem is that I want them to be automatically logged in when clicking on this link.
I can do something obvious as creating an unique token and pass it through the url mysite.com/my_funky_action?login_bypass_token=af123fa127ba32 but this seems to me as a problem "solved many times before"
So, there is a simple way out there to do this using rails / devise? I've searched on devise documentation without success.
Using as basis the code from devise's recoverable, I did this
model:
class User < ActiveRecord::Base
def set_login_bypass_token
raw, enc = Devise.token_generator.generate(User, :login_bypass_token)
self.login_bypass_token = enc
self.login_bypass_token_set_at = Time.now.utc
self.save(validate: false)
raw
end
def self.by_bypass_token(token)
original_token = Devise.token_generator.digest(self, :login_bypass_token, token)
User.find_by(:login_bypass_token => original_token)
end
end
mailer:
class SomeMailer < ActionMailer::Base
def send_something
...
#login_bypass_token = #user.set_login_bypass_token
...
end
end
application_controller:
class ApplicationController < ActionController::Base
layout :application_layout
protect_from_forgery with: :exception
before_action :bypass_login
before_action :authenticate_user!
private
def bypass_login
if params[:login_bypass_token]
user = User.by_bypass_token(params[:login_bypass_token])
sign_in(user, :bypass => true) if user
redirect_to request.path
end
end
end
email template (in haml)
= link_to 'View this awesome page without login!', awesomeness_url(login_bypass_token: #login_bypass_token)
Generally not a good thing to do.
If you don't use a token, that means you'd need to build a path that includes the email address in clear, e.g.
http://my_app.com/special_action?email=john#sample.com
Given that, anybody would be able to sign on as any registered user simply by sending a url structured as the above, substituting whatever email they want.
Go for a token, make sure it expires when used or after the shortest time you can get away with.
I working on an app with user authorization. It has a List and User classes. The authentication was built with Ryan Bates http://railscasts.com/episodes/270-authentication-in-rails-3-1
I'm not sure about authorization process. I read about cancan gem. But i could not understand.
I want to achieve this:
User only able to view/edit/delete his own list.
User only able to view/edit/delete his own profile(user class).
I don't implement user level right now. No guess or admin.
How to use before_filter method in list and User controller with current_user instance?
Since you are defining current_user in the application controller, this is easy. You can use before_filter like this in the Users controller:
class ItemsController < ApplicationController
before_filter :check_if_owner, :only => [:edit, :update, :show, :destroy]
def check_if_owner
unless current_user.admin? # check whether the user is admin, preferably by a method in the model
unless # check whether the current user is the owner of the item (or whether it is his account) like 'current_user.id == params[:id].to_i'
flash[:notice] = "You dont have permission to modify this item"
redirect_to # some path
return
end
end
end
###
end
You should add a similar method to UsersController to check if it is his profile, he is editing.
Also, have a look at Devise which is the recommended plugin for authentication purposes.
For this I'd not use devise. It's way to much for this simple use.
I'd make a seperate controller for the public views and always refere to current_user
Remember to make routes for the actions in the PublicController
class PublicController < ApplicationController
before_filter :login_required?
def list
#list = current_user.list
end
def user
#user = current_user
end
def user_delete
#user = current_user
# do your magic
end
def user_update
#user = current_user
# do your magic
end
# and so on...
end
I'm using devise and have a quick question. How can I redirect the :authenticate_user! before_filter to the user sign up page instead of sign in? I've been going through https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/helpers.rb but haven't had much luck figuring out a solution.
I had a similar issue where I needed to redirect to the signup if the user was not logged in.
I fixed it by adding a method to the application_controller.rb and using it as a before filter in the other controllers.
Keep in mind that is is more of a temporary solution because it skips a bunch of deviseĀ“s abstractions.
before_filter :auth_user
def auth_user
redirect_to new_user_registration_url unless user_signed_in?
end
You're going to have to create a custom FailureApp that inherits from Devise's FailureApp as seen here: https://github.com/plataformatec/devise/wiki/How-To:-Redirect-to-a-specific-page-when-the-user-can-not-be-authenticated
I added a wiki page showing the correct way to do this with a failure app (as Steven initially hinted at):
Redirect to new registration (sign up) path if unauthenticated
The key is to override the route method, like so:
# app/lib/my_failure_app.rb
class MyFailureApp < Devise::FailureApp
def route(scope)
:new_user_registration_url
end
end
and then have Devise use your failure app:
# config/initializers/devise.rb
config.warden do |manager|
manager.failure_app = MyFailureApp
end
This approach is preferable to overriding authenticate_user! in your controller because it won't clobber a lot of "behind the scenes" stuff Devise does, such as storing the attempted URL so the user can be redirected after successful sign in.
With multiple user types
If you have Admin and User Devise resources, you'll probably want to keep the default "new session" functionality for admins. You can do so quite easily by checking what type of scope is being processed:
# app/lib/my_failure_app.rb
class MyFailureApp < Devise::FailureApp
def route(scope)
scope.to_sym == :user ? :new_user_registration_url : super
end
end