0

The Problem

I have a multipage react application & rails backend that I am using as an API.

After a user logs out of the app, rails will throw CSRF errors when receiving any subsequent POST or DELETE requests until I perform a full page refresh in my browser.

e.g. After logging out, the login form POST request will cause 422 errors until I refresh the page. Then when logged in, post login POST / DELETE requests will randomly also trigger 422 errors until I refresh the page

Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms | Allocations: 775)

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
  
actionpack (6.0.3.4) lib/action_controller/metal/request_forgery_protection.rb:215:in `handle_unverified_request'
actionpack (6.0.3.4) lib/action_controller/metal/request_forgery_protection.rb:247:in `handle_unverified_request'

The CSRF token is being set in the application.html.erb file

<%= csrf_meta_tags %>

I have created an axios component to parse this token

const token = document.querySelector('[name="csrf-token"]') || {content: 'no-csrf-token'}
const axiosPost = axios.create({
  headers: {
    common: {
      'X-CSRF-Token': token.content
    }
  }
})
export default axiosPost

And I am importing this in various other components & using it to make the requests e.g.

import AuthContext from './AuthContext'

const Logout = () => {

  const {authState, setAuthState} = useContext(AuthContext)
  const handleLogout = () => {
    axiosPost.delete('/users/sign_out', {}, { withCredentials: true })
    .then((resp) => {
      setAuthState(false)
    })

When the setAuthState value is set to false, my App.js component will re-render the page & display the Login component only.

Interesting, if I replace this logout / state driven re-render with

axiosPost.delete('/users/sign_out', {}, { withCredentials: true })
    .then((resp) => {
      setAuthState(false)
      window.location.href = '/login'
    })

It triggers a page refresh and I don't get the CSRF error on the backend (at least not as frequently).

I am starting to think that the CSRF token in the layout needs to be reloaded / refreshed after a user logs out of the application but maybe this is just another red herring.

One other thing, I did overwrite the destroy method in the devise sessions controller with

def destroy
  signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
  if signed_out
    head :no_content
  end
end

I don't see how this method would cause the CSRF issue, but adding it for additional context.

Application Stack

  • Rails 6.0.3.4 on the backend (full stack, not just api)
  • CSRF protection enabled
  • Devise for user authentication & to authorize access to protected controllers
  • React on the front-end.
  • My react app is initialized once through application.html.erb

Any help / guidance is appreciated

Jason
  • 22,645
  • 5
  • 29
  • 51
  • This thread https://stackoverflow.com/questions/50159847/single-page-application-and-csrf-token mentions that the CSRF token is per session. The easiest would be a reload after sign-out or maybe a server side generate and passing to the front. – Maxence Feb 13 '21 at 22:48
  • 2
    For API controllers you need `protect_from_forgery with: :null_session`. This eliminates the session issue that @Maxence mentioned. Don't put this in controllers that are served by Rails, though. – sam Feb 14 '21 at 16:21

0 Answers0