Labels

tools (7) programming (6) rails (5) cluj (4) productivity (4) misc (3) startups (3) emberjs (2) internet (2) software (2) hack (1) meetup (1) os x (1) science (1)

Tuesday, February 12, 2019

Impersonation in EmberJS

Our Rails 5 application needed a way for admins to impersonate regular users. We're using Ember (2.14) together with Ember Simple Auth, which supports multiple ways of session authentication using authenticators. I'm quite pleased with the result, the solution is elegant and doesn't feel like a hack.

We start off from the impersonation route:

  1. The impersonation route handler receives the email and authorization token as query parameters (from the admin panel) and processes them in the beforeModel() hook
  2. The authentication mechanism is initiated using the impersonation authenticator *but*
  3. the session data is changed to reflect what is normally used as a session authenticator (DeviseAuthenticator, in our case)
  4. Finally, sessionDataUpdated event is triggered which will eventually call DeviseAuthenticator's restore() without breaking the flow.


// app/routes/impersonate.js
export default Ember.Route.extend({
  beforeModel(transition) { //   (1)
    let email = transition.queryParams.email,
      token = transition.queryParams.token;

    this.get('session').authenticate('authenticator:impersonate', email, token).then(() => { //   (2)
      let authenticated = this.get('session.data.authenticated');
      authenticated.authenticator = 'authenticator:devise'; // (3)
      getOwner(this).lookup('authenticator:impersonate').trigger('sessionDataUpdated', authenticated); //    (4)
    }, () => {});
  }
});

The session service will invoke the authenticate() method in order to authenticate itself.

// app/authenticators/impersonate.js
import Devise from 'ember-simple-auth/authenticators/devise';

export default Devise.extend({
  authenticate(identification, token) {
    return new Promise((resolve, reject) => {
      const { resourceName, identificationAttributeName } = this.getProperties('resourceName', 'identificationAttributeName');
      const data         = {};
      data[resourceName] = { token };
      data[resourceName][identificationAttributeName] = identification;

      this.makeRequest(data, { url: 'users/impersonate' }).then((response) => {
        run(null, resolve, response);
      }).catch((error) => run(null, reject, error));
    });
  }
});

The backend controller validates the authorization token and a JSON containing an authorization token and an identifier.

class Users::SessionsController < Devise::SessionsController

  def impersonate
    user_email = params[:user][:email].presence
    user = user_email && User.find_by_email(user_email)
    token = params[:user][:token]

    if user && Devise.secure_compare(user.authorization_token, token)
      render json: return_session(user), status: 201
    end
  end
end

Finally, the promise is resolved and its data is stored in the session.



1 comment:

  1. Never put secrets in queryparams. A couple quick googles will explain why.

    ReplyDelete