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.
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.
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....
delete the successfully used Rails.cache entry when user is created
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
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
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, ...)
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
...
I have a little different approach.
Show the question,receive the response and verify it.
Set the encrypted session.
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
So... May be it should be in module Validatable?
Generate Validatables controler with this Tools
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...
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).
Related
My Rails app uses Devise for authentication. It has a sister iOS app, and users can log in to the iOS app using the same credentials that they use for the web app. So I need some kind of API for authentication.
Lots of similar questions on here point to this tutorial, but it seems to be out-of-date, as the token_authenticatable module has since been removed from Devise and some of the lines throw errors. (I'm using Devise 3.2.2.) I've attempted to roll my own based on that tutorial (and this one), but I'm not 100% confident in it - I feel like there may be something I've misunderstood or missed.
Firstly, following the advice of this gist, I added an authentication_token text attribute to my users table, and the following to user.rb:
before_save :ensure_authentication_token
def ensure_authentication_token
if authentication_token.blank?
self.authentication_token = generate_authentication_token
end
end
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.find_by(authentication_token: token)
end
end
Then I have the following controllers:
api_controller.rb
class ApiController < ApplicationController
respond_to :json
skip_before_filter :authenticate_user!
protected
def user_params
params[:user].permit(:email, :password, :password_confirmation)
end
end
(Note that my application_controller has the line before_filter :authenticate_user!.)
api/sessions_controller.rb
class Api::SessionsController < Devise::RegistrationsController
prepend_before_filter :require_no_authentication, :only => [:create ]
before_filter :ensure_params_exist
respond_to :json
skip_before_filter :verify_authenticity_token
def create
build_resource
resource = User.find_for_database_authentication(
email: params[:user][:email]
)
return invalid_login_attempt unless resource
if resource.valid_password?(params[:user][:password])
sign_in("user", resource)
render json: {
success: true,
auth_token: resource.authentication_token,
email: resource.email
}
return
end
invalid_login_attempt
end
def destroy
sign_out(resource_name)
end
protected
def ensure_params_exist
return unless params[:user].blank?
render json: {
success: false,
message: "missing user parameter"
}, status: 422
end
def invalid_login_attempt
warden.custom_failure!
render json: {
success: false,
message: "Error with your login or password"
}, status: 401
end
end
api/registrations_controller.rb
class Api::RegistrationsController < ApiController
skip_before_filter :verify_authenticity_token
def create
user = User.new(user_params)
if user.save
render(
json: Jbuilder.encode do |j|
j.success true
j.email user.email
j.auth_token user.authentication_token
end,
status: 201
)
return
else
warden.custom_failure!
render json: user.errors, status: 422
end
end
end
And in config/routes.rb:
namespace :api, defaults: { format: "json" } do
devise_for :users
end
I'm out of my depth a bit and I'm sure there's something here that my future self will look back on and cringe (there usually is). Some iffy parts:
Firstly, you'll notice that Api::SessionsController inherits from Devise::RegistrationsController whereas Api::RegistrationsController inherits from ApiController (I also have some other controllers such as Api::EventsController < ApiController which deal with more standard REST stuff for my other models and don't have much contact with Devise.) This is a pretty ugly arrangement, but I couldn't figure out another way of getting access the methods I need in Api::RegistrationsController. The tutorial I linked to above has the line include Devise::Controllers::InternalHelpers, but this module seems to have been removed in more recent versions of Devise.
Secondly, I've disabled CSRF protection with the line skip_before_filter :verify_authentication_token. I have my doubts about whether this is a good idea - I see a lot of conflicting or hard to understand advice about whether JSON APIs are vulnerable to CSRF attacks - but adding that line was the only way I could get the damn thing to work.
Thirdly, I want to make sure I understand how authentication works once a user has signed in. Say I have an API call GET /api/friends which returns a list of the current user's friends. As I understand it, the iOS app would have to get the user's authentication_token from the database (which is a fixed value for each user that never changes??), then submit it as a param along with every request, e.g. GET /api/friends?authentication_token=abcdefgh1234, then my Api::FriendsController could do something like User.find_by(authentication_token: params[:authentication_token]) to get the current_user. Is it really this simple, or am I missing something?
So for anyone who's managed to read all the way to the end of this mammoth question, thanks for your time! To summarise:
Is this login system secure? Or is there something I've overlooked or misunderstood, e.g. when it comes to CSRF attacks?
Is my understanding of how to authenticate requests once users are signed in correct? (See "thirdly..." above.)
Is there any way this code can be cleaned up or made nicer? Particularly the ugly design of having one controller inherit from Devise::RegistrationsController and the others from ApiController.
Thanks!
You don't want to disable CSRF, I have read that people think it doesn't apply to JSON APIs for some reason, but this is a misunderstanding. To keep it enabled, you want to make a few changes:
on there server side add a after_filter to your sessions controller:
after_filter :set_csrf_header, only: [:new, :create]
protected
def set_csrf_header
response.headers['X-CSRF-Token'] = form_authenticity_token
end
This will generate a token, put it in your session and copy it in the response header for selected actions.
client side (iOS) you need to make sure two things are in place.
your client needs to scan all server responses for this header and retain it when it is passed along.
... get ahold of response object
// response may be a NSURLResponse object, so convert:
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
// grab token if present, make sure you have a config object to store it in
NSString *token = [[httpResponse allHeaderFields] objectForKey:#"X-CSRF-Token"];
if (token)
[yourConfig setCsrfToken:token];
finally, your client needs to add this token to all 'non GET' requests it sends out:
... get ahold of your request object
if (yourConfig.csrfToken && ![request.httpMethod isEqualToString:#"GET"])
[request setValue:yourConfig.csrfToken forHTTPHeaderField:#"X-CSRF-Token"];
Final piece of the puzzle is to understand that when logging in to devise, two subsequent sessions/csrf tokens are being used. A login flow would look like this:
GET /users/sign_in ->
// new action is called, initial token is set
// now send login form on callback:
POST /users/sign_in <username, password> ->
// create action called, token is reset
// when login is successful, session and token are replaced
// and you can send authenticated requests
Your example seems to mimic the code from the Devise blog - https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
As mentioned in that post, you are doing it similar to option 1, which they say is the insecure option. I think the key is that you don't want to simply reset the authentication token every time the user is saved. I think the token should be created explicitly (by some kind of TokenController in the API) and should expire periodically.
You'll notice I say 'I think' since (as far as I can tell) nobody has any more information on this.
The top 10 most common vulnerablites in web applications are documented in the OWASP Top 10. This question mentioned that Cross-Site Request Forgery(CSRF) protection was disabled, and CSRF is on the OWASDP Top 10. In short, CSRF is used by attackers to perform actions as an authenticated user. Disabling CSRF protection will lead to high risk vulnerabilities in an application, and undermines the purpose of having a secure authentication system. Its likely that the CSRF protection was failing, because the client is failing to pass the CSRF synchronization token.
Read the entire OWASP top 10, failing to do so is extremely hazardous. Pay close attention to Broken Authentication and Session Management, also check out the Session Management Cheat Sheet.
I'm Using Devise and i'm trying to build a requirement that only emails that are included in my white list can actually Sign Up.
Over Time emails will be added to that list. Meaning that Today there are 10 emails, tomorrow another 20+.
But i don't know quite yet how to achieve this.
I know that i have to Create my own Registrations Controller, and for the Validation i think i need something similar to this:
before_validation :whitelisted?
def whitelisted?
unless WhiteList.exists?(:email => email)
errors.add :email, "is not on our beta list"
end
end
However, i am clueless on how to start or continue this. I don't even know if that's the best Practice.
How do i add emails to that whitelist and where is even that whitelist?
If someone could be noob-friendly enough to explain this to me.
Try the following i think this could help you.
create new registration controller
class RegistrationsController < Devise::RegistrationsController
def create
unless WhiteList.exists?(:email => params[:user][:email])
errors.add :email, "is not on our beta list"
else
super
end
end
end
and in routes file replace existing with following
devise_for :users, controllers: { registrations: "registrations" }
Create new model WhiteList using following
rails g model whitelist email:string
and run rake db:migrate command.
after this start Rails console add email's using following command.
Whitelist.create(email: "test#user.com")
I found #Amit Sharma's answer useful, but it doesn't work straight out of the box. Here's what I came up with:
class RegistrationsController < Devise::RegistrationsController
def create
if WhiteList.exists?(:email => params[:user][:email].downcase)
super
else
flash[:error] = "Your email is not on our beta list."
redirect_to new_user_registration_path
end
end
end
class WhiteList < ActiveRecord::Base
before_save :downcase_email
validates :email, presence: true
def downcase_email
self.email = email.downcase
end
end
This solves for case sensitivities when whitelisting an email and produces a flash error message when a Whitelisted email isn't matched.
For the purpose of a new web app, I would need on my sign up page (which is administrator only) just only one email field.
The thing is that I'm totally new at rails and so even basics things like that are for me really difficult...
I created my authentification using Railscast #270 which uses has_secure_password method.
For now, everything works great except that I dont need all this bullcrap...
I also want to use Action Mailer to send the generated password to his email adress.
A hex(8) password would be perfect (I have seen SecureRandom but it seems to be depreciated)
Users_Controller:
class UsersController < ApplicationController
skip_before_filter :is_connected?, :only => [:new, :create]
def new
#user = User.new
end
def create
#user = User.new(params[:user])
if #user.save
# Tell the Mailer to send a welcome Email after save
Mailer.confirm_email(#user).deliver
redirect_to root_url, :notice => "Signed up!"
else
render "new"
end
end
end
User_model:
class User < ActiveRecord::Base
attr_accessible :email
has_secure_password
validates_presence_of :password, :email, :on => :create
end
For now, in my view, I have 2 fields. But as I said earlier, I only want one.
I would like to keep using has_secure_password which seems to offer a pretty good security regarding hash/salt.
Rails provides ActiveSupport::SecureRandom which either (depending on the Ruby version) is just a bridge to Ruby's SecureRandom or reimplemented it on older versions of Ruby (if my memory is correct SecureRandom was added in 1.8.7)
Now that all of the versions of Ruby that Rails supports have SecureRandom built-in ActiveSupport::SecureRandom is no longer needed and has been deprecated. SecureRandom itself is going nowhere -
require 'securerandom'
SecureRandom.hex(8)
should do fine (you might want to consider SecureRandom.urlsafe_base64 for a more compact representation of the same amount of actual randomness)
Here is one simple code for random password with lenth 8
rand_password=('0'..'z').to_a.shuffle.first(8).join
Hope it will help.
Sometimes things from Rails are deprecated because they duplicate functionality that has been added to Ruby core, and SecureRandom seems to be one of those things.
You can use any of those random generator methods to produce a one-time-use password.
To Create Random and unique token/password
class User < ActiveRecord::Base
before_create :generate_password
def generate_password
self.password = loop do
random_token = SecureRandom.urlsafe_base64
# If you are using FFaker gem then you can use it otherwise
# SecureRandom is great choice
# random_token = FFaker::Internet.password
break random_token unless User.exists?(password: random_token)
end
end
end
The main object here is to generate random token and do not repeat that token in the database. It can be really useful for some cases like generating unique token, unique invoice number, etc
Given I'm on Rails 3.1, Ruby 1.9.2 with a standard setup for devise to log a user in by name and password (e.g. similar to https://github.com/RailsApps/rails3-devise-rspec-cucumber ):
I need to add a layer that authenticates a posted username and password against an external service before creating a new user (sign up) or when a user signs in.
(FWIW, further down the road I plan on using the https://github.com/chicks/devise_aes_encryptable strategy/gem to encrypt the sensitive password and, when logging in with a local password, decrypt the one for the remote service, authenticate, then continue with logging in, that is have two passwords, one encrypted one-way, the other reversible... don't ask why, anyway)
In lib/util/authenticate.rb I have an authentication class that returns a boolean against this service e.g.
Util::Authenticate.authenticate(username,password)
But, I can't figure out how to add a filter to authenticate against it on form post before authentication continues (for sign up or sign in).
What I've tried:
I haver a User model and I thought to put a
before_filter :authenticate_against_my_service, :only => [:create, :new]
in the UserController but that didn't work
So, I tried opening up the Devise Sessions controller, which didn't work, nor did subclassing it (e.g. in the README ),
class User::SessionsController < Devise::SessionsController
# something
end
# in config/routes.rb
devise_for :users, :controllers => { :sessions => "users/sessions" }
nor subclassing the Devise Registrations controller (e.g. http://www.tonyamoyal.com/2010/07/28/rails-authentication-with-devise-and-cancan-customizing-devise-controllers/ )and adding a before_filter (same as above).
# in app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
before_filter :check_permissions, :only => [:new, :create, :cancel]
skip_before_filter :require_no_authentication
def check_permissions
authorize! :create, resource
end
end
# and in config/routes.rb
root :to => "home#index"
# replace "devise_for :users" with the below
devise_for :users, :controllers => { :registrations => "users/registrations" }
# other related code
devise_for :users do
get 'logout' => 'devise/sessions#destroy'
end
# resources :users, :only => :show MUST be below devise_for :users
resources :users, :only => :show
I think I have to do this in the controller because once the params get to the model, I won't have an unencrypted password to send to the external service.
I looked at some extensions for ideas such as https://raw.github.com/nbudin/devise_cas_authenticatable/master/lib/devise_cas_authenticatable/strategy.rb e.g.
require 'devise/strategies/base'
module Devise
module Strategies
class CasAuthenticatable < Base
# True if the mapping supports authenticate_with_cas_ticket.
def valid?
mapping.to.respond_to?(:authenticate_with_cas_ticket) && params[:ticket]
end
# Try to authenticate a user using the CAS ticket passed in params.
# If the ticket is valid and the model's authenticate_with_cas_ticket method
# returns a user, then return success. If the ticket is invalid, then either
# fail (if we're just returning from the CAS server, based on the referrer)
# or attempt to redirect to the CAS server's login URL.
def authenticate!
ticket = read_ticket(params)
if ticket
if resource = mapping.to.authenticate_with_cas_ticket(ticket)
# Store the ticket in the session for later usage
if ::Devise.cas_enable_single_sign_out
session['cas_last_valid_ticket'] = ticket.ticket
session['cas_last_valid_ticket_store'] = true
end
success!(resource)
elsif ticket.is_valid?
username = ticket.respond_to?(:user) ? ticket.user : ticket.response.user
redirect!(::Devise.cas_unregistered_url(request.url, mapping), :username => username)
#fail!("The user #{ticket.response.user} is not registered with this site. Please use a different account.")
else
fail!(:invalid)
end
else
fail!(:invalid)
end
end
protected
def read_ticket(params)
#snip
end
end
end
end
Warden::Strategies.add(:cas_authenticatable, Devise::Strategies::CasAuthenticatable)
and read about devise/warden authentication strategies, e.g. https://github.com/hassox/warden/wiki/Strategies but wonder if I need to actually create a new strategy (and if I can figure out how to do that)
EDIT, POSSIBLE SOLUTIONS:
I like alno's suggestion and will try that, though it seems more like a monkeypatch than how devise/warden is meant to be used
module Devise::Models::DatabaseAuthenticatable
alias_method :original_valid_password?, :valid_password?
def valid_password?
if Util::Authenticate.authenticate(username,password)
original_valid_password?
else
false
end
end
end
alternatively, I've looked into adding a warden authentication strategy, but it's hard to figure out all the moving parts, e.g. from Custom authentication strategy for devise
initializers/authentication_strategy.rb: # this could be in initializers/devise.rb as well, no?
Warden::Strategies.add(:custom_external_authentication) do
def valid?
# code here to check whether to try and authenticate using this strategy;
return true # always use the strategy as only user's authenticate
end
def authenticate!
# code here for doing authentication; if successful, call success!
# whatever you've authenticated, e.g. user; if fail, call fail!
if Util::Authenticate.authenticate(username, password)
success!(true) # I don't think I want to return a user, as I'll let database authenticatable handle the rest of the authentication
# I don't think I'm using success! correctly here: https://github.com/hassox/warden/wiki/Strategies
# success!(User.find(someid))
else
fail!("Username and password not valid for external service. Please ensure they are valid and try again.")
end
end
end
add following to initializers/devise.rb
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies.unshift :custom_external_authentication # will this check before or after database authentication? I want before, I think
end
end
OR from How do I add a strategy to Devise
class ExternalServiceStrategy
def valid?
true # always use this
end
def authenticate!
# external boolean service call
end
end
Warden::Strategies.add(:database_authenticatable, ExternalServiceStrategy) # will this work before the db authentication?
If you see in devise source, you will find an valid_password? method, which accepts unencrypted password, so you may override it to authenticate versus some extenal service.
Something like:
def valid_password?(password)
ExternalService.authenticate(email, password)
end
You should be making your changes in the model layer, not in the controller. In fact I would advice you create a file in /lib/whatever that handles talking to the external service, and then modify your User model so that it checks the external service.
I'm using Devise with login credentials: email/password - no usernames
I just noticed that the login process is case sensitive for emails. so if you register with bob#apPle.com, and then try to log in with Bob#apple.com you get an error. Very confusing.
How can I make devise log people in with their email/password, and the email being case insensitive?
You can easily fix the issue like below.
# config/initializers/devise.rb
Devise.setup do |config|
config.case_insensitive_keys = [:email, :username]
end
One option is to override the find method used by devise. Something like:
# User.rb
before_save do
self.email.downcase! if self.email
end
def self.find_for_authentication(conditions)
conditions[:email].downcase!
super(conditions)
end
I added this to my User model to store it case-sensitive but make it case-insensitive during sign in:
def self.find_for_database_authentication(conditions = {})
self.where("LOWER(email) = LOWER(?)", conditions[:email]).first || super
end
It works on Heroku.
By the way, this is just a temporary fix as the issue has been resolved and this will be the default behavior on Devise 1.2. See this pull request for details.
I also had some solution which making work with email is case-insensitive for all Devise controllers (functionality):
class ApplicationController < ActionController::Base
...
...
prepend_before_filter :email_to_downcase, :only => [:create, :update]
...
...
private
...
...
def email_to_downcase
if params[:user] && params[:user][:email]
params[:user][:email] = params[:user][:email].downcase
end
end
...
...
end
I know it is not the best solution: it involves another controllers of another models and executes code which is not necessary for them. But it was just makeshift and it works (at least for me ;) ).
Kevin and Andres, thanks for your answers. It is really good solutions and useful. I wanted to vote them up, but I haven't enough reputation yet. So, I just tell 'thanks' to you. ;)
Lets wait for Devise 1.2
Devise addresses the issue here:
https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address