Rails 7 pass session params to pundit policy - ruby-on-rails

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

Related

Devise + JWT Not getting Correct User, after logging out from first user

So my Problem is : When i sign in with User A(is Admin) it works fine and i can list the List of User in the database.
The problem occurs when i logout and sign in with user B(regular user)I can list the users as well which shouldn't happen (and current_user is still User A). BUT if i delete the httpOnly cookie that the rails application sends back and then request the list of user with the token from JWT with user B, I get 401 which is what i want.
I have an initializer
module Devise
module Strategies
class JWTAuthenticatable < Base
def authenticate!
token = get_token
return fail(:invalid) unless token.present?
payload = WebToken.decode(token)
return fail(:invalid) if payload == :expire
resource = mapping.to.find(payload['user_id'])
return fail(:not_found_in_database) unless resource
success! resource
end
private
def get_token
auth_header.present? && auth_header.split(' ').last
end
def auth_header
request.headers['Authorization']
end
end
end
end
And My application controller is
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session, prepend: true
before_action :authenticate_user!
end
Which only gets hit once from the first request but never any requests after that, and current_user stays stuck on the first signed in user. But again when the httpOnly cookie is deleted that Module gets hit and set current_user to User B.
I send JWT with every request but funny thing is when deleted after the first request i still get a response as if JWT exists.
Hope im explaining this correctly
Ive been searching for answers for about a week now. Any help would be appreciated
Found an answer in another post- Had to override devises store? to return false instead Solution Here

Allow unconfirmed users to access certain pages which require authentication

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.

With Ruby on Rails 5, how can I have Pundit redirect back when not authorized to the same stored_location_for() Devise uses?

I have followed the Devise wiki on how to set up a store_user_location! method to redirect back to the previous page after sign_in/sign_out and would like to use this same method for redirecting with Pundit after a user_not_authorized is triggered but I'm not sure what to supply for the "resource_or_scope". Usually this is something Devise supplies in it's callbacks (after_sign_in_path_for(resource_or_scope)). Is it possible to use this same method with Pundit and what do I supply as the resource_or_scope?
def user_not_authorized
flash[:error] = "You are not authorized to perform this action."
stored_location_for(what_goes_here?)
end
Try to the following below, I usually approach that like this:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
# Redirects the browser to the page that issued the request (the referrer) if possible,
# otherwise redirects to the root location.
def user_not_authorized
redirect_back(fallback_location: root_path)
end
end
Hope it helps!
I found what I was looking for and the answer to "what_goes_here" is ":user", which I got to from the linked wiki's function:
def store_user_location
# :user is the scope we are authenticating
store_location_for(:user, request.fullpath)
end
Unfortunately, this does not work like I was hoping as my before_action :store_user_location!, if: :storable_location? stores the location attempting to be accessed before it is authorized and if the user ends up being not authorized it just redirects back to the same URL they are not authorized for and an infinite redirect loop ensues.
I also tried the redirect_back fallback: root_path option and for whatever reason, it just always takes me back to the root_path, which I do not like from a usability standpoint.
I'm now looking into other options but it looks like it's going to end up just displaying a 401/404 error page when not authorized and the user can just use their browser's back button.

Checking authorized flag in devise

I have a rails app where I have added a boolean field named 'authorized' to the user model. Basically, I want to lock the app down such that only authorized users can access the app. I am trying to do this in my application controller:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
def authorized
redirect_to root_path, alert: "Not Authorized" if !current_user.authorized?
end
end
However, I am getting a redirect error when I do this as I have the root route set to a path where authentication is required.
I now I can do this check in the view or another controller, but I'd like to do it in the app controller as I want the entire app locked down.
You can override these methods in your user model.
def active_for_authentication?
super && approved?
end
def inactive_message
'my custom inactive message' unless approved?
super
end
I recommend you to change the authorized name by approved, I'm not sure but authorized sounds like an internal Devise method name.
Here was my solution...
In the controller where I wanted the 'authorized' authentication I added...
before_filter :authorized, :except => :not_authorized
...
def authorized
if !current_user.authorized?
redirect_to not_authorized_path
end
end
In my routes, I added..
get 'not_authorized' => 'my_controller#not_authorized'
...and added a simple app/views/my_controller/not_authorized.html.erb

How to setup a remote json API for Rails for authentication and session

I'm new to rails and are have a pretty basic understanding of the Devise Gem. Besides the CRUD and views I'm not clear on what it provides that could help me for a AngularJs app talking to a Rails Json Api.
At the moment I'm hand rolling things ie. for security I have I exchange a HTTP Header token between client (js) and server. I'm also using the Railscast #250 for user authentication - but as I don't see how to apply the SessionController for a remote client.
Are there any strategies I could employ for authentication and managing session via a remote json API?
Thanks!
I personally wouldn't use devise for something like this because there's only a small part of it you'd be using anyways
Dont
You pretty much just don't use a session. All you need to do is pass in basic authentication each time, and in the application controller you determine if its valid, if not just send them back an auth error.
Example request: http://username:password#example.com/api/endpoint
class ApplicationController
before_filter :check_auth!
private
def check_auth!
username, password = ActionController::HttpAuthentication::Basic::user_name_and_password(request)
user = User.find_by(username: username)
if user && user.encrypted_password == SomeEncryptFunction(params[:password])
#current_user = user
else
raise "error"
end
end
end
But if you want to...
Then what you can do is update a DateTime field on the user when they first auth (which starts their session), then on subsequent calls they can just pass a token you give them that you you check for each time they sign in. You also check that only a certain amount of time has passed since they first authed, otherwise their session is invalid.
class SessionsController < ApplicationController
skip_before_filter :check_auth!
before_filter :login!
private
# Note: I don't remember the actual devise method for validating username + password
def login!
user = User.find_by(username: params[:username])
if user && user.valid_password(params[:password])
current_user = user
current_user.update_attributes(
authenticated_at: DateTime.now,
authentication_token: Devise.friendly_token
)
else
raise "error"
end
end
end
class ApplicationController
before_filter :check_auth!
private
def check_auth!
if valid_token(params[:token])
current_user = User.find_by(authentication_token: params[:token])
else
raise "error"
end
end
# Returns true if token belongs to a user and is recent enough
def valid_token(token)
user = User.find_by(authentication_token: params[:token])
user && user.authenticated_at < DateTime.now - 1.day
end
end

Resources