67

Hi I am using Devise for my user authentication suddenly my new user registration was not working.

this was error I am getting.

ActionController::InvalidAuthenticityToken

Rails.root: /home/example/app
Application Trace | Framework Trace | Full Trace

Request

Parameters:

{"utf8"=>"✓",
 "user"=>{"email"=>"example@gmail.com",
 "password"=>"[FILTERED]",
 "password_confirmation"=>"[FILTERED]"},
 "x"=>"0",
 "y"=>"0"}

this is my registrations controller

class RegistrationsController < Devise::RegistrationsController
  prepend_before_filter :require_no_authentication, :only => [ :new, :create, :cancel ]
  prepend_before_filter :authenticate_scope!, :only => [:edit, :update, :destroy]

  before_filter :configure_permitted_parameters

  prepend_view_path 'app/views/devise'

  # GET /resource/sign_up
  def new
    build_resource({})
    respond_with self.resource
  end

  # POST /resource
  def create
    build_resource(sign_up_params)

    if resource.save
      if resource.active_for_authentication?
        set_flash_message :notice, :signed_up if is_navigational_format?
        sign_up(resource_name, resource)
        respond_with resource, :location => after_sign_up_path_for(resource)
      else
        set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format?
        expire_session_data_after_sign_in!
        respond_with resource, :location => after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords resource

      respond_to do |format|
        format.json { render :json => resource.errors, :status => :unprocessable_entity }
        format.html { respond_with resource }
      end
    end
  end

  # GET /resource/edit
  def edit
    render :edit
  end

  # PUT /resource
  # We need to use a copy of the resource because we don't want to change
  # the current user in place.
  def update
    self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
    prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)

    if update_resource(resource, account_update_params)
      if is_navigational_format?
        flash_key = update_needs_confirmation?(resource, prev_unconfirmed_email) ?
          :update_needs_confirmation : :updated
        set_flash_message :notice, flash_key
      end
      sign_in resource_name, resource, :bypass => true
      respond_with resource, :location => after_update_path_for(resource)
    else
      clean_up_passwords resource
      respond_with resource
    end
  end

  # DELETE /resource
  def destroy
    resource.destroy
    Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
    set_flash_message :notice, :destroyed if is_navigational_format?
    respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
  end

  # GET /resource/cancel
  # Forces the session data which is usually expired after sign
  # in to be expired now. This is useful if the user wants to
  # cancel oauth signing in/up in the middle of the process,
  # removing all OAuth session data.
  def cancel
    expire_session_data_after_sign_in!
    redirect_to new_registration_path(resource_name)
  end

  protected

  # Custom Fields
  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) do |u|
      u.permit(:first_name, :last_name,
        :email, :password, :password_confirmation)
    end
  end

  def update_needs_confirmation?(resource, previous)
    resource.respond_to?(:pending_reconfirmation?) &&
      resource.pending_reconfirmation? &&
      previous != resource.unconfirmed_email
  end

  # By default we want to require a password checks on update.
  # You can overwrite this method in your own RegistrationsController.
  def update_resource(resource, params)
    resource.update_with_password(params)
  end

  # Build a devise resource passing in the session. Useful to move
  # temporary session data to the newly created user.
  def build_resource(hash=nil)
    self.resource = resource_class.new_with_session(hash || {}, session)
  end

  # Signs in a user on sign up. You can overwrite this method in your own
  # RegistrationsController.
  def sign_up(resource_name, resource)
    sign_in(resource_name, resource)
  end

  # The path used after sign up. You need to overwrite this method
  # in your own RegistrationsController.
  def after_sign_up_path_for(resource)
    after_sign_in_path_for(resource)
  end

  # The path used after sign up for inactive accounts. You need to overwrite
  # this method in your own RegistrationsController.
  def after_inactive_sign_up_path_for(resource)
    respond_to?(:root_path) ? root_path : "/"
  end

  # The default url to be used after updating a resource. You need to overwrite
  # this method in your own RegistrationsController.
  def after_update_path_for(resource)
    signed_in_root_path(resource)
  end

  # Authenticates the current scope and gets the current resource from the session.
  def authenticate_scope!
    send(:"authenticate_#{resource_name}!", :force => true)
    self.resource = send(:"current_#{resource_name}")
  end

  def sign_up_params
    devise_parameter_sanitizer.sanitize(:sign_up)
  end

  def account_update_params
    devise_parameter_sanitizer.sanitize(:account_update)
  end
end

and this is my sessions controller

class SessionsController < DeviseController
  prepend_before_filter :require_no_authentication, :only => [ :new, :create ]
  prepend_before_filter :allow_params_authentication!, :only => :create
  prepend_before_filter { request.env["devise.skip_timeout"] = true }

  prepend_view_path 'app/views/devise'

  # GET /resource/sign_in
  def new
    self.resource = resource_class.new(sign_in_params)
    clean_up_passwords(resource)
    respond_with(resource, serialize_options(resource))
  end

  # POST /resource/sign_in
  def create
    self.resource = warden.authenticate!(auth_options)
    set_flash_message(:notice, :signed_in) if is_navigational_format?
    sign_in(resource_name, resource)

    respond_to do |format|
        format.json { render :json => {}, :status => :ok }
        format.html { respond_with resource, :location => after_sign_in_path_for(resource) } 
    end
  end

  # DELETE /resource/sign_out
  def destroy
    redirect_path = after_sign_out_path_for(resource_name)
    signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
    set_flash_message :notice, :signed_out if signed_out && is_navigational_format?

    # We actually need to hardcode this as Rails default responder doesn't
    # support returning empty response on GET request
    respond_to do |format|
      format.all { head :no_content }
      format.any(*navigational_formats) { redirect_to redirect_path }
    end
  end


  protected

  def sign_in_params
    devise_parameter_sanitizer.sanitize(:sign_in)
  end

  def serialize_options(resource)
    methods = resource_class.authentication_keys.dup
    methods = methods.keys if methods.is_a?(Hash)
    methods << :password if resource.respond_to?(:password)
    { :methods => methods, :only => [:password] }
  end

  def auth_options
    { :scope => resource_name, :recall => "#{controller_path}#new" }
  end
end

this is registration form

<%= form_for(:user, :html => {:id => 'register_form'}, :url => user_registration_path, :remote => :true, :format => :json) do |f| %>

    <div class="name_input_container">
        <div class="name_input_cell">


    <%= f.email_field :email, :placeholder => "email" %>


    <%= f.password_field :password, :placeholder => "password", :title => "8+ characters" %>


    <%= f.password_field :password_confirmation, :placeholder => "confirm password" %>


    <div class="option_buttons">
        <div class="already_registered">
            <%= link_to 'already registered?', '#', :class => 'already_registered', :id => 'already_registered', :view => 'login' %>
        </div>
        <%= image_submit_tag('modals/account/register_submit.png', :class => 'go') %>
        <div class="clear"></div>
    </div>
<% end %>
user3144005
  • 839
  • 1
  • 8
  • 13
  • [Related](https://stackoverflow.com/q/35181340/4575793) [questions](https://stackoverflow.com/q/66827240/4575793) [here](https://stackoverflow.com/q/63911493/4575793). For me, it helped to give an authenticityToken from the controller to the frontend and just give that back to the backend unchanged when the request is done. – Cadoiz Mar 15 '23 at 08:02

8 Answers8

119

Per the comments in the core application_controller.rb, set protect_from_forgery to the following:

protect_from_forgery with: :null_session

Alternatively, per the docs, simply declaring protect_from_forgery without a :with argument will utilize :null_session by default:

protect_from_forgery # Same as above

UPDATE:

This seems to be a documented bug in the behavior of Devise. The author of Devise suggests disabling protect_from_forgery on the particular controller action that's raising this exception:

# app/controllers/users/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
  skip_before_filter :verify_authenticity_token, :only => :create
end
zeantsoi
  • 25,857
  • 7
  • 69
  • 61
  • ya I have done that but now I was unable post on my terminal it was showing Started POST "/users" for 127.0.0.1 at 2014-01-02 04:42:11 +0100 Processing by RegistrationsController#create as HTML Parameters: {"utf8"=>"✓", "user"=>{"email"=>"example@gmail.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Sign up"} Can't verify CSRF token authenticity (0.6ms) BEGIN (0.6ms) ROLLBACK Completed 200 OK in 18ms (Views: 0.5ms | ActiveRecord: 1.1ms) – user3144005 Jan 02 '14 at 03:44
  • Thanks for the answer. It may be useful at this point to mention whether this has any security impact and whether any of this will behave differently in dev vs prod. – Å Stuart Jan 28 '14 at 17:35
  • 1
    @AshStuart, there's no functional difference in behavior in development versus production. There _is_ a security concern, as `protect_from_forgery` is the primary means of defending against CSRF attacks. Anyone attempting this fix may want to look into alternative security measures, including IP throttling. – zeantsoi Jan 28 '14 at 17:55
  • Also, you will need to add something like this to your routes file so that devise knows you have overridden the controller: ```devise_for :users, controllers: {registrations: "registrations"}``` – stebooks May 26 '14 at 18:08
  • `protect_from_forgery with: :null_session` is usable on API post. with Auth token of course. – Pavel Kalashnikov Feb 08 '16 at 15:28
  • 1
    Before trying this, make sure you didn't forget csrf_meta_tags as suggested in http://stackoverflow.com/a/24081738/25192 – thewoolleyman Nov 15 '16 at 05:44
  • Not sure I understand the rationale for the downvote as the respective answers address different issues. – zeantsoi Nov 16 '16 at 07:52
33

You have forgot to add <%= csrf_meta_tags %> in side your layout file.

e.g.:

<!DOCTYPE html>
<html>
<head>
<title>Sample</title>
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>
Benjamin Cheah
  • 1,401
  • 17
  • 23
Snm Maurya
  • 1,085
  • 10
  • 12
  • 3
    If you are seeing this issue and already have `csrf_meta_tags` in your layout, it could be due to the order in which the `protect_from_forgery` hook is being called – try making sure it's the first declared hook in your `ApplicationController` (as a disclaimer, this worked for me with Rails 5.1.3) – lshepstone Nov 26 '17 at 19:01
  • Thanks for the solution @lshepstone! This issue started popping up randomly and couldn't figure it out! – Justin Adkins Sep 29 '18 at 18:17
21

TLDR: You are probably seeing this issue because your form submits via XHR.

Few things first:

  1. Rails includes a CSRF token inside the head tag of your page.
  2. Rails evaluates this CSRF token anytime you perform a POST, PATCH or DELETE request.
  3. This token expires when you sign in or sign out

A bog standard HTTP sign-in will cause a full page refresh, and the old CSRF token will be flushed and replaced with the brand new one that Rails creates when you sign in.

An AJAX sign in will not refresh the page, so the crusty old, stale CSRF token, which is now invalid, is still present on your page.

The solution is to update the CSRF token inside your HEAD tag manually after AJAX sign in.


Some steps that I have shamelessly borrowed from a helpful thread on this matter.

Step 1: Add the new CSRF-token to the response headers which are sent after a successful sign in

class SessionsController < Devise::SessionsController

  after_action :set_csrf_headers, only: :create

  # ...

  protected
    def set_csrf_headers
      if request.xhr?
        # Add the newly created csrf token to the page headers
        # These values are sent on 1 request only
        response.headers['X-CSRF-Token'] = "#{form_authenticity_token}"
        response.headers['X-CSRF-Param'] = "#{request_forgery_protection_token}"
      end
    end
  end

Step2: Use jQuery to update the page with the new values when the ajaxComplete event fires:

$(document).on("ajaxComplete", function(event, xhr, settings) {
  var csrf_param = xhr.getResponseHeader('X-CSRF-Param');
  var csrf_token = xhr.getResponseHeader('X-CSRF-Token');

  if (csrf_param) {
    $('meta[name="csrf-param"]').attr('content', csrf_param);
  }
  if (csrf_token) {
    $('meta[name="csrf-token"]').attr('content', csrf_token);
  }
});

That's it. YMMV depending on your Devise configuration. I suspect though that this issue is ultimately caused by the fact that the old CSRF token is killing the request, and rails throws an exception.

Community
  • 1
  • 1
stephenmurdoch
  • 34,024
  • 29
  • 114
  • 189
14

If you're using just an API you should try:

class ApplicationController < ActionController::Base
  protect_from_forgery unless: -> { request.format.json? }
end

http://edgeapi.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html#method-i-protect_against_forgery-3F

Franzé Jr.
  • 1,194
  • 13
  • 20
  • Adding this, I now get a 406 error: `xhr.js:173 POST http://localhost:3000/users/sign_in.json 406 (Not Acceptable)` `ActionController::UnknownFormat in Devise::SessionsController#create` – Uj Corb Oct 29 '18 at 14:29
  • update: I then had to add: `respond_to :html, :json` in my Application controller. now it works fine ! – Uj Corb Oct 29 '18 at 14:37
14

For Rails 5 it could be due to the order in which protect_from_forgery and your before_actions are triggered.

I faced a similar situation recently, even though protect_from_forgery with: :exception was the first line in the ApplicationController, the before_action's were still interfering.

The solution was to change:

protect_from_forgery with: :exception

to:

protect_from_forgery prepend: true, with: :exception

There's a blog post about it

Cadoiz
  • 1,446
  • 21
  • 31
Chris Edwards
  • 3,514
  • 2
  • 33
  • 40
5

Just spent the entire morning debugging this, so I thought I should share this here in case someone faces a similar issue when updating rails to 5.2 or 6.

I had 2 problems

1) Can't verify CSRF token authenticity.

and, after added skipping verification,

2) request would go through but user still wasn't logged in.

I wasn’t caching in development

  if Rails.root.join('tmp', 'caching-dev.txt').exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true

    config.cache_store = :memory_store
    config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" }
  else
    config.action_controller.perform_caching = false

    config.cache_store = :null_store
  end

And in session_store

config.session_store :cache_store,  servers: ... 
    
    

I guess app was trying to store session in cache, but it was null - so it wasn’t logging in. 

after I ran

bin/rails dev:cache

which started caching - login started to work.

You may need to

  • Rotate master.key
  • Rotate credentials.yml.enc
  • remove secrets.yml
AndreiMotinga
  • 1,024
  • 17
  • 24
  • In my case simply turning on caching fixed the issue, without needing any verification skipping or custom protect_from_forgery lines (Rails 6) – Suan Aug 04 '20 at 21:22
  • Thank you! I had the same issue, cache disabled does not work with cache cookie store... – eikes Sep 28 '21 at 09:49
5

Browser Caching HTML Issue (2020)

If you've tried all the remedies on this page and you're still having an issue with InvalidAuthenticityToken exceptions, it may be related to the browser caching HTML. There's an issue on Github with 100s of comments along with some reproducible code. In a nutshell, here's what was happening to me as it relates to HTML caching:

  1. User browses to website. Rails sets a signed session cookie on the first GET request. See config/initializers/session_store.rb for config options. This session cookie stores useful information, including a CSRF token that is used to decrypt and validate the authenticity of the request. Important: By default, the session cookie will expire when the browser window closes.
  2. User browses to a page containing a form. For me, I was receiving the most exceptions on my login page.
  3. Rails embeds a hidden CSRF token in this form, and submits this token along with the form data. Important: This token is embedded in the HTML.
  4. ActionController grabs the CSRF token from the params object and validates it with the CSRF token from the cookie using the verified_request? method in Rails 4.2+.

Many browsers are now implementing HTML caching, so that when you open a page the HTML is loaded without a request. Unfortunately, when the browser is closed the session cookie is destroyed, so if the user closes the browser while on a form (such as a login page), then the first request will not contain a CSRF token thus throwing an InvalidAuthenticityError.

Two common solutions

  1. Extend the expiry of your session cookie beyond the browser window.
  2. Detect in the browser if the session cookie is missing (via a proxy cookie), and if it is missing refresh the page.

1. Extending the session cookie expiry

As noted in this Github comment, Django takes this approach:

Django puts adds the token in its own cookie called CSRF_COOKIE. This is a persistent cookie that expires in a year. If subsequent requests are made, the cookie's expiry is updated.

In Rails:

# config/initializers/session_store.rb 
Rails.application.config.session_store :cookie_store, expire_after: 14.days

With many things security related, there's concern that this could create vulnerabilities, but I have not been able to locate any examples of how an attacker could exploit this.

2. Using javascript to refresh a page

This approach involves setting a separate token that can be read by the browser, and if that token is not present, refreshing the page. Thus, when the browser loads the cached HTML (without the session cookie), executes the JS on the page, the user can be redirected or refresh the HTML.

For example, setting a cookie for each non-protected request:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  after_action :set_csrf_token

  def set_csrf_token
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end
end

Checking for this cookie in JS:

const hasCrossSiteReferenceToken = () => document.cookie.indexOf('XSRF-TOKEN') > -1;

if (!hasCrossSiteReferenceToken()) {
    location.reload();
}

This will force the browser to refresh.

Conclusion

I hope this helps some folks out there; this bug cost me days of work. If you're still having issues, consider reading up on:

Matt
  • 5,800
  • 1
  • 44
  • 40
2

You have to put protect_from_forgery right before the action for authenticating user. This is the right solution

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :authenticate_user!
end
  • @Vadim do you know what does? – Chris Edwards Jul 27 '17 at 16:04
  • 1
    No idea, @ChrisEdwards. Here's what I ended up doing: `skip_before_action :verify_authenticity_token, if: -> { controller_name == 'sessions' }`. It now allows me to log in AND log out. Before, both actions were unavailable to me. This is not ideal, of course, and a bit of a security concern. Not a solution - just a workaround. – Vadim Jul 27 '17 at 20:21
  • @Vadim It happened to me exactly the way you described it, thanks for this. – Guido Tarsia May 18 '18 at 19:01