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