0
votes

I am setting up an ember app that is backed by ruby on rails. I am running into issues with my sign in action using simple-auth and simple-auth-devise. I successfully retrieve the sessions authentication token and username when I submit a correct username and password, but I am still given a 401 access denied error and I can't figure out why. I suspect that it may have to do with the naming of email versus user_email and token vs user_token business. I am taking this code mostly from dayjot, so you'd think it would be trivial to track down this bug but I am having tons of issues finding the exact issue. Thanks for any help you can give me!

The exact error I get in the rails server is:

Started GET "/users/me" for 127.0.0.1 at 2015-02-17 10:25:31 -0600
Processing by UsersController#me as JSON
  Parameters: {"user"=>{}}
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 5ms (Views: 4.1ms | ActiveRecord: 0.0ms)
In rails, this is my application controller:

This is my application controller:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  # protect_from_forgery with: :null_session

  before_action :authenticate_user_from_token!, :handle_html
  around_action :user_time_zone, if: :current_user

  def index
    render file: 'public/index.html'
  end

  protected

  def authenticate_user!
    render(json: {}, status: 401) unless current_user
  end

  private

  def authenticate_user_from_token!
    authenticate_with_http_token do |token, options|
      user_email = options[:user_email].presence
      user       = user_email && User.find_by_email(user_email)

      if user && Devise.secure_compare(user.authentication_token, token)
        request.env['devise.skip_trackable'] = true
        sign_in user, store: false
      end
    end
  end

  def user_time_zone(&block)
    Time.use_zone(current_user.time_zone, &block)
  end

  # If this is a get request for HTML, just render the ember app.
  def handle_html
    render 'public/index.html' if request.method == 'GET' && request.headers['Accept'].match(/html/)
  end
end

My sessions controller looks like this:

class SessionsController < Devise::SessionsController
  def create
    self.resource = warden.authenticate!(auth_options)
    sign_in(resource_name, resource)
    data = {
      user_token: self.resource.authentication_token,
      user_email: self.resource.email
    }
    render json: data, status: 201
  end

  def destroy
    sign_out :user
    render json: {}, status: :accepted
  end
end

My serializers are these:

class UserSerializer < ActiveModel::Serializer
  attributes :id, :password, :user_email, :email, :user_token, :passwordConfirmation
end

class UserSerializer < ActiveModel::Serializer
  attributes :id, :email, :email_times, :last_export_time, :plan, 
             :plan_started, :plan_canceled, :plan_status, :trial_end, 
             :time_zone, :status, :created_at, :include_email_memory
end

My route is:

Rails.application.routes.draw do

  # PLANS
  post 'update_plan' => 'plans#update_plan', as: :update_plan
  post 'update_card' => 'plans#update_card', as: :update_card
  post 'cancel_plan' => 'plans#cancel_plan', as: :cancel_plan

  # PASSWORDS
  post 'start_password_reset' => 'users#start_password_reset'
  put  'finish_password_reset' => 'users#finish_password_reset'
  get  'password-reset' => 'application#index', as: :edit_user_password

  # USERS
  devise_for :users, controllers: { sessions: 'sessions' }, :skip => [:passwords]
  resources :users, only: [:create, :update] do 
    get 'me' => 'users#me', on: :collection
  end

  # background processing admin
  match "/delayed_job" => DelayedJobWeb, :anchor => false, via: [:get, :post]    

  # catch-all for ember app
  get '*path' => 'application#index', :constraints => { :format => 'html' }


end

In the ember-cli app itself, my login controller is:

import Ember from "ember";

export default Ember.Controller.extend({
  authenticator: 'simple-auth-authenticator:devise',

  identification: null,
  password: null,
  error: null,
  working: false,

  actions: {
    authenticate: function() {
      var _this = this,
          data = this.getProperties('identification', 'password');

      this.setProperties({
        working: true,
        password: null,
        error: null
      });

      this.get('session').authenticate('simple-auth-authenticator:devise', data).then(function() {
        // authentication was successful
      }, function(data) {
        _this.set('working', false);
        _this.set('error', data.error);
      });
    }
  }
});

My application route is:

// ember-simple-auth
import Ember from "ember";
import ApplicationRouteMixin from 'simple-auth/mixins/application-route-mixin';
import Notify from 'ember-notify';
import ENV from 'front-end/config/environment';

export default Ember.Route.extend(ApplicationRouteMixin, {
  beforeModel: function(transition) {
    this._super(transition);    
    return this.setCurrentUser();
  },
  actions: {
    sessionAuthenticationFailed: function(data) {
      this.controllerFor('login').set('working', false);
      this.controllerFor('login').set('loginErrorMessage', data.message);
    },
    sessionInvalidationSucceeded: function() {
      this.transitionTo('index');
    },
    sessionAuthenticationSucceeded: function() {
      var _this = this;
      this.controllerFor('login').set('working', false);

      this.setCurrentUser().then(function() {
        if (_this.get('session.currentUser.mustSubscribe')) {
          _this.transitionTo('plans');
        } else {
          _this.transitionTo('courses');
        }
      });      
    },
    authorizationFailed: function() {
      Notify.error("Could not be authenticated.. signing out.", {closeAfter: 5000});
      this.get('session').invalidate();
    }
  },

  setCurrentUser: function() {

    var _this = this,
        adapter = this.get('store').adapterFor('user');

    if (this.get('session.isAuthenticated')) {
      return new Ember.RSVP.Promise(function(resolve) {
        adapter.ajax(ENV.APP.API_HOST + "/users/me", "GET", {}).then(
          function(response){
            _this.store.pushPayload(response);
            var user = _this.store.find('user', response.user.id);
            resolve(user);
          },
          function(response){
            resolve(response);
          }
        );
      }).then(function(user) {
        _this.set('session.currentUser', user);  
      }, function() {
        Notify.error("Could not be authenticated.. signing out.", {closeAfter: 5000});
        _this.get('session').invalidate();
      });
    } else {
      return new Ember.RSVP.Promise(function(resolve){ resolve(); });
    }
  }
});

Finally my login route is:

import Ember from "ember";

export default Ember.Route.extend({
  activate: function() {
    if (this.get('session').isAuthenticated) {
      this.transitionTo('courses');
    }
  }
});

And Template is:

<form {{action 'register' on='submit'}} class='d-auth-form fade-in'>

  {{#each errors}}
    <div class="d-error">
      {{this}}
    </div>
  {{/each}}

  {{input placeholder='Email' type='email' value=email autocomplete='off' autocapitalize="none"}}
  {{input placeholder='Password' type='password' value=password autocomplete='off'}}
  <button type="submit" class='d-btn d-btn--success' {{bind-attr disabled=working}}>
    {{#if working}}
      Registering..
    {{else}}
      Sign up for DayJot for free
    {{/if}}
  </button>

  <ul class='d-links'>
    <li>{{#link-to 'login'}}Login to existing account{{/link-to}}</li>
  </ul>
</form>

The important parts of environment.js are:

    'simple-auth': {
      crossOriginWhitelist: ['http://localhost:3000','http://localhost:4202','https://api.dayjot.com'],
      authorizer: 'simple-auth-authorizer:devise',
      authenticationRoute: 'index'
    } 

and

  ENV['simple-auth-devise'] = {
    serverTokenEndpoint: ENV.APP.API_HOST+'/users/sign_in',
    identificationAttributeName: 'email'

  }
1
I went through the readme and drastically simplified the Ember application and login route and controllers to be what is recommended in the Readme. It now signs in, which is awesome, but the page refreshses itself and the page is no longer logged in. What causes these hard refreshes and is there a way to fix it? Also, I think that I'd want to store the token even if the person did a hard refresh in their browser so they don't have to log in. Is there a way to store that? I enabled Crsf tokens and that seems to be fine.Coherent

1 Answers

2
votes

Checkout the README - Ember Simple Auth Devise expects the token to be returned as token, you're using user_token however. Thus, the session will never actually be authenticated in Ember and the token won't be included in requests which leads to the 401 response.