Adobe
Adobe

Reputation: 13477

Prepend sign_up page with a condition?

I want to ask user a question, and let him sign up only if the user answers my question correctly. I searched devise how-to acticles but my case doesn't seem to be there.

Is there an idiomatic way to deal with this situation?

The first thought might be to use javascript, but answers are stored in LDAP, and I expect it will be easier to deal with this in rails.

I was also thinking about disabling /users/sign_up route, invoke the action (devise/registration#new) manually and render the view (devise/registration/new).

Another way I can think of, is to run a background daemon, which will collect session id, where user answered the questions correctly. On correct answer user will be redirected to the publicly available sign up page, which will get check user's session id with the daemon.

Upvotes: 14

Views: 297

Answers (5)

bbozo
bbozo

Reputation: 7311

To improve on security of previous suggestions, the best one seems to be by coreyward, but it's insecure (regardless if cookies are encrypted or not - see my comment on the OP)

# app/controllers/preauth_controller.rb
def new
end

def create
  if params[:answer] == 'correct answer'
    # Create a secret value, the `token`, we will share it to the client
    # through the `session` and store it on the server in `Rails.cache`
    # (or you can use the database if you like)
    #
    # The fact that this token expires (by default) in 15 minutes is
    # a bonus, it will secure the system against later use of a stolen
    # cookie. The token is also secure against brute force attack
    @token = SecureRandom.base64
    session[:preauthorization_token] = @token
    Rails.cache.write("users/preauthorization_tokens/#{@token}", "OK")
    redirect_to sign_up_path
  else
    flash[:error] = 'Incorrect answer'
    render :new
  end
end


# app/controllers/users_controller.rb
before_filter :verify_preauth, only: [:new, :create]

def verify_preauth
  # When we approve preauthorization we confirm that the
  # `token` is known to the client, if the client knows the token
  # let him sign up, else make him go away
  token = session[:preauthorization_token]
  redirect_to new_preauth_path unless token and Rails.cache.read("users/preauthorization_tokens/#{token}") == "OK"
end

Optional things to do / play with....

  1. delete the successfully used Rails.cache entry when user is created
  2. play with :expires_in settings if you want, you generally want it as short as possible and as long as needed :) but the rails default of 15 minutes is pretty good
  3. there are nicer ways of going around this and similar security issues with cookies - namely you can create a server_session object which does basically the same as session but stores the data in Rails.cache with a random expirable token stored in session used to access the cache entry in much the same way we do here
  4. simply go to server-side sessions and don't worry about session security, but this means longer response times due to your Rails.cache round trip (redis, memcache, AR, ...)
  5. instead of OK into the cache value you can store a hash of values, if you need more data, safely stored on the host, to work this out
  6. ...

Upvotes: 4

coreyward
coreyward

Reputation: 80051

Assuming you have cookie data signed (as is the default in Rails 3), you could do as you say and use the session:

# app/controllers/preauth_controller.rb
def new
end

def create
  if params[:answer] == 'correct answer'
    session[:preauthorized] = true
    redirect_to sign_up_path
  end
  flash[:error] = 'Incorrect answer'
  render :new
end


# app/controllers/users_controller.rb
before_filter :verify_preauth, only: [:new, :create]

def verify_preauth
  redirect_to new_preauth_path unless session[:preauthorized]
end

If cookie data is not signed, however, the preauthorized key can be tampered with by the client and thus should not be trusted.

Provided that your page is encrypted in transit with HTTPS via TLS and you don't have any XSS vulnerabilities present, this should be sufficiently secure for your needs. If you feel this is a particularly sensitive piece of code, you would want more than the passing thoughts of a StackOverflow user to guide and implement a comprehensive approach to securing your application.

Upvotes: 6

hedgesky
hedgesky

Reputation: 3311

I think the easiest way to do it is to change default devise controller with custom one with before_action in it:

# routes.rb
devise_for :users, :controllers => {:registrations => "registrations"}

With following controller implementation:

# app/controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
  before_action :check_answers, only: :new # :new action is responsible for :sign_up route

  private

  def check_answers
    unless session[:gave_answers]
      redirect_to ask_questions_path
      false
    end
  end
end 

And setting session like this:

# somewhere in questions controller:
if answers_correct?
  session[:gave_answers] = true
  redirect_to new_registration_path
end

As soon as this controller inherits from Devise::RegistrationsController all behavior stays default except checking answers functionality.

Regarding your question about idiomatic way - this approach was described in official documentation. Your app - your logic, it's OK.

UPDATE:

In comments @bbozo pointed on certain security issues with this and other answers. To make it more securely, you could add expiration time and set some random secret token (more information in comments).

Upvotes: 1

So... May be it should be in module Validatable?

  1. Generate Validatables controler with this Tools
  2. Customize this controller something like this: (Code of this module You could see This)

...

base.class_eval do
          validates_presence_of   :email, if: :email_required?
          validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
          validates_format_of     :email, with: email_regexp, allow_blank: true, if: :email_changed?

          validates_presence_of     :password, if: :password_required?
          validates_confirmation_of :password, if: :password_required?
          validates_length_of       :password, within: password_length, allow_blank: true

          validates_presence_of     :question, if: :question_required?
          validates_format_of       :question, with: answered_regexp, if: :answered_changed?          
        end
      end

...
  def email_required?
    true
  end
  def question_required?
    true
  end

This is not complied solution, but I hope it help You...

Upvotes: 3

sureshprasanna70
sureshprasanna70

Reputation: 1045

I have a little different approach.

  1. Show the question,receive the response and verify it.
  2. Set the encrypted session.
  3. Override the devise's registration controller with this so that even if they visit the url directly and tried signing up they won't be able to

    #app/controllers/registration_controller.rb
    class RegistrationsController < Devise::RegistrationsController
     before_filter :check_answered_or_not,only:[:create,:new]
      def check_answered_or_not
       if not session[:answered]==true
          redirect_to question_path
       end
      end
      private
      def sign_up_params
        params.require(:user).permit(:name,:phone,:password,:password_confirmation,:email)
      end
      def account_update_params
         params.require(:user).permit(:name,:phone,:password,:password_confirmation,:email,:current_password)
     end
    end
    

my 2cents

Upvotes: 4

Related Questions