Skip to content

How To: Redirect back to current page after sign in, sign out, sign up, update

ssshhhooota edited this page Dec 21, 2020 · 62 revisions

The following guides are already implemented in Devise 4.7 version

The guide below reflects current versions of Devise (written as of 4.3 tested on 4.2).

Redirecting back to the "current page" involves saving the current url in the session and then retrieving the url from the session after the user is authenticated / signed out. This should only really be done for GET requests as the other http methods (POST, PUT, PATCH, DELETE) are not idempotent and should not be repeated automatically.

Why not use request.referer?

request.referer is set by the value of the HTTP referer header. Many browsers do not send this header. Therefore the only robust cross-browser way to implement this functionality is by using the session.

StoreLocation to the rescue

The Devise::Controllers::StoreLocation helper module defines the store_location_for and stored_location_for helpers which store and retrieve the values from the session - and which are used internally by Devise.

To store the location for your whole application use before_action to set a callback (Use before_filter in Rails versions before 4.0).

# This example assumes that you have setup devise to authenticate a class named User.
class ApplicationController < ActionController::Base
  before_action :store_user_location!, if: :storable_location?
  # The callback which stores the current location must be added before you authenticate the user 
  # as `authenticate_user!` (or whatever your resource is) will halt the filter chain and redirect 
  # before the location can be stored.
  before_action :authenticate_user!

  private
    # Its important that the location is NOT stored if:
    # - The request method is not GET (non idempotent)
    # - The request is handled by a Devise controller such as Devise::SessionsController as that could cause an 
    #    infinite redirect loop.
    # - The request is an Ajax request as this can lead to very unexpected behaviour.
    def storable_location?
      request.get? && is_navigational_format? && !devise_controller? && !request.xhr? 
    end

    def store_user_location!
      # :user is the scope we are authenticating
      store_location_for(:user, request.fullpath)
    end
end

To redirect to the stored location after the user signs in you would override the after_sign_in_path_for method:

def after_sign_in_path_for(resource_or_scope)
  stored_location_for(resource_or_scope) || super
end

2nd Approach

Imagine we have the following devise user User.

Setting up the controller for sign in

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  after_action :store_action
  
  def store_action
    return unless request.get? 
    if (request.path != "/users/sign_in" &&
        request.path != "/users/sign_up" &&
        request.path != "/users/password/new" &&
        request.path != "/users/password/edit" &&
        request.path != "/users/confirmation" &&
        request.path != "/users/sign_out" &&
        !request.xhr?) # don't store ajax calls
      store_location_for(:user, request.fullpath)
    end
  end
end

The stored location is used by the after_sign_in_path_for helper. By setting it the helper will use it.

In this case the user will be redirected to the last viewed page before he is authenticated. You can replace request.fullpath by anything you want.

Legacy versions of Devise prior to 3.2.1

You can provide your own storage mechanism or backport StoreLocation (upgrading is recommended though):

module StoreLocationBackport
  def store_location_for(resource_or_scope, location)
    session_key = stored_location_key_for(resource_or_scope)
    session[session_key] = location
  end
  
  def stored_location_for(resource_or_scope)
    session_key = stored_location_key_for(resource_or_scope)
    if is_navigational_format?
      session.delete(session_key)
    else
      session[session_key]
    end
  end

  def after_sign_in_path_for(resource_or_scope)
    stored_location_for(resource_or_scope) || super
  end
  
  private 
    def stored_location_key_for(resource_or_scope)
      scope = Devise::Mapping.find_scope!(resource_or_scope)
      "#{scope}_return_to"
    end
end

# This example assumes that you have setup devise to authenticate a class named User.
class ApplicationController < ActionController::Base
  # @todo Update Devise!
  # Remove this after updating Devise 
  include StoreLocationBackport

  before_action :store_user_location!, if: :storable_location?
  # The callback which stores the current location must be added before you authenticate the user 
  # as `authenticate_user!` (or whatever your resource is) will halt the filter chain and redirect 
  # before the location can be stored.
  before_action :authenticate_user!

  private 
    # Its important that the location is NOT stored if:
    # - The request method is not GET (non idempotent)
    # - The request is handled by a Devise controller such as Devise::SessionsController as that could cause an 
    #    infinite redirect loop.
    # - The request is an Ajax request as this can lead to very unexpected behaviour.
    def storable_location?
      request.get? && !devise_controller? && !request.xhr? 
    end

    def store_user_location
      # :user is the scope we are authenticating
      store_location_for(:user, request.fullpath)
    end
end
Clone this wiki locally