mr_muscle
mr_muscle

Reputation: 2900

Rails 7 pass session params to pundit policy

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:

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

Upvotes: 1

Views: 330

Answers (1)

Tadas Sasnauskas
Tadas Sasnauskas

Reputation: 2313

Pundit documents this in Additional Context section of the readme.

Instead of user instance you can pass pundit user context object which would hold user instance and extra session context like 2FA authentication status.

class UserContext
  attr_reader :user, :second_factor_authenticated

  def initialize(user, second_factor_authenticated)
    @user = user
    @second_factor_authenticated = second_factor_authenticated
  end
end

class ApplicationController
  include Pundit::Authorization

  def pundit_user
    second_factor_authenticated = session[:_2fa] == 'success'
    UserContext.new(current_user, second_factor_authenticated)
  end
end

Then in policy classes rename your user attribute to context and refer to user instance through context.user and 2FA status through context.second_factor_authenticated.

So to answer the question - seven years later it is still perfectly acceptable solution.

Given you're using Devise an alternative could be adding ephemeral attribute second_factor_authenticated to user class and implementing your own .serialize_into_session / .serialize_from_session to store it as part of user "vector" (by default it's only user id + auth salt).

Upvotes: 0

Related Questions