Ember-simple-auth with REST API and session cookie


#1

Hello,

I’m trying to authenticate my app against a REST API (Drupal 7 Service module). With a REST client it looks like this:

Host: http://example.com/api/user/login
Header:
  content-type: application/json
content: {"username":"test","password":"test"}

Response:
Header:
  Set-Cookie: SESSxxx=xxxxx; expires=Thu, 02-Apr-2015 14:16:35 GMT; Max-Age=2000000; path=/; domain=.example.com; HttpOnly

Content:
{"sessid":"xxxx","session_name":"SESSxxx","token":"xxxxxx","user":{"uid":"8","name":"test","mail":"test@example.com"}}

For each further request I just have to set the Cookie Header:

Cookie: session_name=sessid

I thought this must be super simple but I’m totally lost. You need to know this is my first ember app.

What works:

  • Send the login request and return the session data
  • Showing Login/Logout buttons depending on what simple-auth thinks my status is

What does not work:

  • Setting the Cookie header for each subsequent request after successfull authentication (tested with the logout request which also just needs the cookie header)
  • In Ember Inspector I see an indefnite loop of popping <unknown Promise> with the authentication requests response as “Fullfillment/Rejection value”

My Code:

app/authenticators/custom.js:

import Ember from 'ember';
import Base from 'simple-auth/authenticators/base';

export default Base.extend({
  restore: function(data) {
    return new Ember.RSVP.Promise(function (resolve, reject) {
      if (!Ember.isEmpty(data.session_name)) {
        resolve(data);
      }
      else {
        reject();
      }
    });
  },

  authenticate: function(options) {
    return new Ember.RSVP.Promise(function(resolve, reject) {
      Ember.$.ajax({
        type: "POST",
        url: 'http://example.com/api/user/login',
        contentType: 'application/json;charset=utf-8',
        dataType: 'json',
        data: JSON.stringify({
          username: options.identification,
          password: options.password
        })
      }).then(function(response) {
        Ember.run(function() {
          resolve(response);
        });
      }, function(xhr, status, error) {
        Ember.run(function() {
          reject(xhr.responseJSON || xhr.responseText);
        });
      });
    });
  },

  invalidate: function() {
    console.log('invalidate...');
    Ember.$.ajax({
      type: "POST",
      url: 'http://example.com/api/user/logout',
      contentType: 'application/json;charset=utf-8',
      dataType: 'json',
    });
    
    return Ember.RSVP.resolve();
  }
});

app/authorizers/custom.js

import Base from 'simple-auth/authorizers/base';

export default Base.extend({
  authorize: function(jqXHR, requestOptions) {
    console.log('authorize...');
    jqXHR.setRequestHeader('Cookie', '1234lkj1sdfguihsidfugh');
  }
});

I guess the authorizer is the point where I have to create the Cookie header. But there are two major problems:

  1. jqXHR.setRequestHeader does nothing
  2. I can’t access the stored(?) session information. I tried import Session from 'simple-auth/session' and then console.log(Session.store); but nothing.

Sorry if the solution might be too obvious. I alread read dozens of tutorials (each looks different) and a lot of documentation but I can’t get my head around this.

Every help is much appreciated, thank you.

Regards, haggis


#2

I’m using cookie based auth with ember simple auth and did not use an authorizer. What I did was have the server set the cookie. From your response header it looks like your server side setup is doing the same.

my ajax has

Ember.$.ajax(
  url: url
  type: method
  dataType: 'json'
  data: data
  xhrFields: {
    withCredentials: true <-- important
  },
  crossDomain: true
  beforeSend: (xhr, settings) ->
    xhr.setRequestHeader('Accept', settings.accepts.json);
)

I don’t know if this bit is necessary, like you I ended up trying many things, but I have this in my application adapter.

Ember.$.ajaxSetup(
  crossDomain: true
  xhrFields:
    withCredentials: true
)

If you haven’t noticed I’m also doing crossDomain since my ember server and api server were on different localhost ports.

On the server side you have to make sure that you setup cors headers I’m using rails cors and allowing the origin of my ember server full access to resources + credentials = true.

Hope that helps.


#3

Thank you varblob!

Yes, this is working. I had to use the same domain (with another port) though. But that’s not a problem as this is supposed to be in production later.

I also tried to remove my authorizer but I get CSRF missing token errors when doing that. Just for the record, this is how it looks like:

import Ember from 'ember';
import Base from 'simple-auth/authorizers/base';

export default Base.extend({
  authorize: function(jqXHR, requestOptions) {
    var token = this.get('session.token');
    if (this.get('session.isAuthenticated') && !Ember.isEmpty(token)) {
      jqXHR.setRequestHeader('X-CSRF-Token', token);
      jqXHR.crossDomain = true;
      jqXHR.xhrFields = {withCredentials: true};
    }
  }
});

Now i can proceed with the next chapter :smile:


#4

I’m doing this stuff outside the authorizer and it’s totally dumb. I’m going to move that all in there as you did. So thank you @haggis


#5

I learned something new and realized I did it totaly wrong. So here’s a brief example of how simple auth, routes, models and a customized RESTAdapter work together. I’ll expand the example with serialization stuff once I stumbled through it.

Scenario:

/config/environment.js:

ENV['simple-auth'] = {
  authorizer: 'authorizer:custom',
  store: 'simple-auth-session-store:cookie', // optional
  crossOriginWhitelist: ['http://example.com'],
  routeAfterAuthentication: '/events'
};

/app/controllers/login.js:

import Ember from 'ember';
import LoginControllerMixin from 'simple-auth/mixins/login-controller-mixin';

export default Ember.Controller.extend(LoginControllerMixin, {
  authenticator: 'authenticator:custom'
});

/app/authenticators/custom.js:

import Ember from 'ember';
import Base from 'simple-auth/authenticators/base';

export default Base.extend({  
  restore: function(data) {
    return new Ember.RSVP.Promise(function (resolve, reject) {
      if (!Ember.isEmpty(data.session_name)) {
        resolve(data);
      }
      else {
        reject();
      }
    });
  },

  authenticate: function(options) {
    return new Ember.RSVP.Promise(function(resolve, reject) {
      Ember.$.ajax({
        type: "POST",
        url: 'http://example.com/api/user/login',
        data: JSON.stringify({
          username: options.identification,
          password: options.password
        })
      }).then(function(response) {
        Ember.run(function() {
          resolve(response);
        });
      }, function(xhr, status, error) {
        Ember.run(function() {
          reject(xhr.responseJSON || xhr.responseText);
        });
      });
    });
  },

  invalidate: function() {
    console.log('invalidate...');

    return new Ember.RSVP.Promise(function(resolve, reject) {
      Ember.$.ajax({
        type: "POST",
        url: 'http://example.com/api/user/logout',
      }).then(function(response) {
        Ember.run(function() {
          resolve(response);
        });
      }, function(xhr, status, error) {
        Ember.run(function() {
          reject(xhr.responseJSON || xhr.responseText);
        });
      });
    });
  },
});

/app/authorizers/custom.js: (this touches all local XHR requests + to hosts defined in crossOriginWhitelist)

import Ember from 'ember';
import Base from 'simple-auth/authorizers/base';

export default Base.extend({
  authorize: function(jqXHR, requestOptions) {
    requestOptions.contentType = 'application/json;charset=utf-8';
    requestOptions.crossDomain = true;
    requestOptions.xhrFields = {
      withCredentials: true
    };

    var token = this.get('session.token');
    if (this.get('session.isAuthenticated') && !Ember.isEmpty(token)) {
      jqXHR.setRequestHeader('X-CSRF-Token', token);
    }
  }
});

/app/router.js:

import Ember from 'ember';
import config from './config/environment';

var Router = Ember.Router.extend({
  location: config.locationType
});

Router.map(function() {
  this.route('login');
  this.resource('events', function() {
    this.route('show', { path: '/:event_id'});
  });
});

export default Router;

/app/routes/events.js:

import Ember from 'ember';
import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin';

export default Ember.Route.extend(AuthenticatedRouteMixin, {
  model: function(params) {
    return this.store.find('event');
  },

  setupController: function(controller, events) {
    controller.set('events', events);
  }
});

/app/adapters/application.js:

import DS from "ember-data";

var ApplicationAdapter = DS.RESTAdapter.extend({
  namespace: 'api',
  host: 'http://example.com',
  pathForType: function(type) {
    return type + '.json';
  }
});

export default ApplicationAdapter;

Ember+Rails devs: what do you do for CORS/OPTIONS responses?
Fileupload to remote server
#6

Thanks, your post was really helpful for my project.


#8

What is the file location of the code where you extended the base authorizer?


#9

This one: app/authorizers/custom.js


#10

Please be aware with this line. It will always convert the content type and it will stop you from uploading the files It is here explained But if I comment it, i am not able to login


#11

Hi aamirrajpoot,

nice catch. That makes perfectly sense. I’ve got no beautiful solution ready as I didn’t need to upload files via Ember so far. For a quick workaround I’d try to conditionally implement that header based on a white/blacklist of path (-patterns).


#12

This is a great resource to get started with Ember authentication. However, with the update of ember simple auth to 1.0, the invalidate function above doesn’t work. Does anyone has any trouble with this update?


#13

What part doesn’t work/what errors are you seeing? The ESA jj-abrams branch (which is Ember 2.0+ compat) has a functioning dummy app with examples: https://github.com/simplabs/ember-simple-auth/tree/jj-abrams/tests/dummy/app/authenticators


#14

What about this:

Ember Simple Auth’s old auto-authorization mechanism was complex (especially when working with cross origin requests where configuring the crossOriginWhitelist was causing big problems for many people) and has been removed in 1.0.0. Instead authorization is now explicit. To authorize a block of code use the session service’s authorise method. http://simplabs.com/blog/2015/11/27/updating-to-ember-simple-auth-1.0.html

I´m getting an authorize undefined error, using the session service method