ruby-on-railsrubydeviseclient-serverimpersonation

Implementing user impersonation in Rails API + React client


So, I found gems like pretender, user_impersonate2 and switch_user. They all seem to accomplish the similar goal - switching current_user for systems like Devise, for "monolith" Rails apps.

I have a React client talking to a Rails app. The admin page is implemented directly in Rails (it's a view), and the client is separated. Currently, client makes POST requests to Devise routes that provide an access_token via devise-jwt, and saves the token in browser's localstorage.

Is there a recommended way on allowing administrators to log in as users and be redirected from the admin (Rails) page to the client (React) page? Preferably with minimum changes to the frontend code, but I can make do with that.

I thought about sharing cookies via a shared root domain, but that smells of security issues to me.

I'm not sure how can I make the client app to "listen" for the token change made in the Rails app, or any similar way of changing the current user from the client's perspective.


Solution

  • I won't go so far as to suggest that this is recommended, but I'll tell you the basic approach that I have taken.

    When building the JWT we include some key user information that allows the user to be identified from the token. Looking at the devise-jwt docs, the simplest possible solution would be something like this:

    def jwt_payload
      { uid: id }
    end
    

    Next, you've got to make sure that you can share the secret that you used to create the JWT. I am a 12-factor fan, so our solution uses environment variables for things like this.

    Finally, we read the JWT from the headers, decrypt, and lookup the user. This assumes that you either share a database of users OR replicate users. We built this into a module that is included in our ApplicationController. Adapting ours, it looks something like this for the payload above:

    module JwtTokenable
      private
    
      def token_user
        user_id = token_payload.dig(:uid)
        # CUSTOMIZE BELOW
        User.find(user_id) if user_id
      end
    
      def token_payload
        @payload ||= JWT.decode(access_token, ENV.fetch('JWT_SECRET', ''), true, { algorithm: 'HS512' })
                        .first
                        .deep_symbolize_keys
      rescue JWT::DecodeError
        return {}
      end
    
      def access_token
        @access_token ||= authorization_header.split.last
      end
    
      def authorization_header
        request.headers['Authorization'] || ''
      end
    end
    

    The last step for impersonation would be where the 'CUSTOMIZE BELOW' comment is. This code just looks up the user by ID. At that point, you might be able to leverage one of the gems that you mentioned to figure out if you need to return the user or whomever is being impersonated.