Reputation: 13477
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
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....
Rails.cache
entry when user is created: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 goodserver_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 hereRails.cache
round trip (redis, memcache, AR, ...)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 outUpvotes: 4
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
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
Reputation: 492
So... May be it should be in module Validatable
?
...
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
Reputation: 1045
I have a little different approach.
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