17

After my previous question about ember-simple-auth and torii, I successfully authenticate my users with their Facebook accounts.

But currently, torii's provider facebook-oauth2 is returning an authorization code from Facebook ; when the promise resolves, I send this authorization code to my backend where I perform a request against Facebook to get the user's id and email : then I authenticate the user on my backend, generating a specific access token and sending back to my ember application.

Client code :

// app/controllers/login.js
import Ember from 'ember';
import LoginControllerMixin from 'simple-auth/mixins/login-controller-mixin';

export
default Ember.Controller.extend(LoginControllerMixin, {
    // This authenticator for a simple login/password authentication.
    authenticator: 'simple-auth-authenticator:oauth2-password-grant',
    actions: {
        // This method for login with Facebook.
        authenticateWithFacebook: function() {
            var _this = this;
            this.get('session').authenticate(
                'simple-auth-authenticator:torii',
                "facebook-oauth2"
            ).then(
                function() {
                    var authCode = _this.get('session.authorizationCode');
                    Ember.$.ajax({
                            type: "POST",
                            url: window.ENV.host + "/facebook/auth.json",
                            data: JSON.stringify({
                                    auth_code: authCode
                            }),
                            contentType: "application/json; charset=utf-8",
                            dataType: "json",
                            success: function(data) {
                                    // TODO : manage access_token and save it to the session
                            },
                            failure: function(errMsg) {
                                    // TODO : manage error
                            }
                    });
                },
                function(error) {
                    alert('There was an error when trying to sign you in: ' + error);
                }
            );
        }
    }
});

The problem is : the ember-simple-auth's session is marked as authenticated when the authenticate's promise resolves and then the app redirects to the specific authenticated route. But in this case the session should be authenticated when my backend returns the "real" access_token.

Is there a way to manage this workflow with ember-simple-auth-torii or should I write my own authenticator ?

Community
  • 1
  • 1
obo
  • 1,652
  • 2
  • 26
  • 50
  • Can you please tell me how to you validate the token received by facebook on your custom server ? – Ajey Jan 12 '15 at 19:14
  • See the accepted answer : in the tokens_controller#create I use the gem Koala to get the access_token with the auth_code received by Facebook. – obo Jan 13 '15 at 09:19
  • I am using node on my backend. Any node library to do this ? – Ajey Jan 13 '15 at 09:28
  • https://www.npmjs.com/search?q=facebook – obo Jan 13 '15 at 12:40

3 Answers3

17

I finally wrote my own authenticator as Beerlington suggested. But also I give to my users a way to authenticate using login/password, so I overrode the ember-simple-auth-oauth2 authenticator, changing only the "authenticate" method and used ember-simple-auth-torii.

Now I can use Torii to get the authorization code from the user's Facebook account, send this code to my backend, authentify the user and generate an access token that will be managed by ember-simple-auth like an oauth2 token.

Here is the code :

// initializers/simple-auth-config.js
import Ember from 'ember';
import Oauth2 from 'simple-auth-oauth2/authenticators/oauth2';

/**
  Authenticator that extends simple-auth-oauth2 and wraps the
  [Torii library](https://github.com/Vestorly/torii)'s facebook-oauth2 provider.

    It is a mix between ember-simple-auth-torii and ember-simple-auth-oauth2.

    First it uses Torii to get the facebook access token or the authorization code.

    Then it performs a request to the backend's API in order to authenticate the
    user (fetching personnal information from Facebook, creating account, login,
    generate session and access token). Then it uses simple-auth's
    oauth2 authenticator to maintain the session.

    _The factory for this authenticator is registered as
    `'authenticator:facebook'` in Ember's container._

    @class Facebook
    @namespace Authenticators
    @extends Oauth2
*/
var FacebookAuthenticator = Oauth2.extend({
    /**
    @property torii
    @private
    */
    torii: null,

    /**
    @property provider
    @private
    */
    provider: "facebook-oauth2",

    /**
    Authenticates the session by opening the torii provider. For more
    documentation on torii, see the
    [project's README](https://github.com/Vestorly/torii#readme). Then it makes a
    request to the backend's token endpoint and manage the result to create
    the session.

    @method authenticate
    @return {Ember.RSVP.Promise} A promise that resolves when the provider successfully 
    authenticates a user and rejects otherwise
    */
    authenticate: function() {
        var _this = this;
        return new Ember.RSVP.Promise(function(resolve, reject) {
            _this.torii.open(_this.provider).then(function(data) {
                var data = {
                    facebook_auth_code: data.authorizationCode
                };
                _this.makeRequest(_this.serverTokenEndpoint, data).then(function(response) {
                    Ember.run(function() {
                        var expiresAt = _this.absolutizeExpirationTime(response.expires_in);
                        _this.scheduleAccessTokenRefresh(response.expires_in, expiresAt, response.refresh_token);
                        if (!Ember.isEmpty(expiresAt)) {
                            response = Ember.merge(response, {
                            expires_at: expiresAt
                        });
                        }
                        resolve(response);
                    });
                }, function(xhr, status, error) {
                    Ember.run(function() {
                            reject(xhr.responseJSON || xhr.responseText);
                    });
                });
            }, reject);
        });
    },
});

export
default {
    name: 'simple-auth-config',
    before: 'simple-auth',
    after: 'torii',
    initialize: function(container, application) {
        window.ENV = window.ENV || {};
        window.ENV['simple-auth-oauth2'] = {
            serverTokenEndpoint: window.ENV.host + "/oauth/token",
            refreshAccessTokens: true
        };

        var torii = container.lookup('torii:main');
        var authenticator = FacebookAuthenticator.create({
            torii: torii
        });
        container.register('authenticator:facebook', authenticator, {
            instantiate: false
        });
    }
};

My backend is in Rails and uses Doorkeeper to manage the access_token and Devise. I overrode Doorkeeper::TokensController to pass the user_id with the token and manage the facebook's authorization code if any (that code should be refactored) :

class TokensController < Doorkeeper::TokensController
    include Devise::Controllers::SignInOut # Include helpers to sign_in

    # The main accessor for the warden proxy instance
    # Used by Devise::Controllers::SignInOut::sign_in
    #
    def warden
        request.env['warden']
    end

    # Override this method in order to manage facebook authorization code and
    # add resource_owner_id in the token's response as
    # user_id.
    #
    def create
        if params[:facebook_auth_code]
            # Login with Facebook.
            oauth = Koala::Facebook::OAuth.new("app_id", "app_secret", "redirect_url")

            access_token = oauth.get_access_token params[:facebook_auth_code]
            graph = Koala::Facebook::API.new(access_token, "app_secret")
            facebook_user = graph.get_object("me", {}, api_version: "v2.1")

            user = User.find_or_create_by(email: facebook_user["email"]).tap do |u|
                u.facebook_id = facebook_user["id"]
                u.gender = facebook_user["gender"]
                u.username = "#{facebook_user["first_name"]} #{facebook_user["last_name"]}"
                u.password = Devise.friendly_token.first(8)
                u.save!
            end

            access_token = Doorkeeper::AccessToken.create!(application_id: nil, :resource_owner_id => user.id, expires_in: 7200)
            sign_in(:user, user)

            token_data = {
                access_token: access_token.token,
                token_type: "bearer",
                expires_in: access_token.expires_in,
                user_id: user.id.to_s
            }

            render json: token_data.to_json, status: :ok

        else
            # Doorkeeper's defaut behaviour when the user signs in with login/password.
            begin
                response = strategy.authorize
                self.headers.merge! response.headers
                self.response_body = response.body.merge(user_id: (response.token.resource_owner_id && response.token.resource_owner_id.to_s)).to_json
                self.status        = response.status
            rescue Doorkeeper::Errors::DoorkeeperError => e
                handle_token_exception e
            end

        end
    end
end

Here is the code I use in the initializer doorkeeper.rb to authentify the user

Doorkeeper.configure do
  # Change the ORM that doorkeeper will use.
  # Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper
  orm :mongoid4

  resource_owner_from_credentials do |routes|
    request.params[:user] = {:email => request.params[:username], :password => request.params[:password]}
    request.env["devise.allow_params_authentication"] = true
    request.env["warden"].authenticate!(:scope => :user)
  end
  # This block will be called to check whether the resource owner is authenticated or not.
  resource_owner_authenticator do
    # Put your resource owner authentication logic here.
    # Example implementation:
    #   User.find_by_id(session[:user_id]) || redirect_to(new_user_session_url)
    #
    # USING DEVISE IS THE FOLLOWING WAY TO RETRIEVE THE USER
    current_user || warden.authenticate!(:scope => :user)
  end

  # Under some circumstances you might want to have applications auto-approved,
  # so that the user skips the authorization step.
  # For example if dealing with trusted a application.
  skip_authorization do |resource_owner, client|
     # client.superapp? or resource_owner.admin?
     true
  end
end
obo
  • 1,652
  • 2
  • 26
  • 50
  • Is there a need for sign_in(:user,user) here? What is it doing if you are anyway returning a valid access token? I tried this exact same method using devise/doorkeeper on my server, but sign_in method always throws an exception and causes a 401 Unauthenticated response – cjroebuck Oct 10 '14 at 00:10
  • The method is used to add information to the user such as last_sign_in_ip, last_sign_in_at, sign_in_count, etc... I edited my answer with the configuration I use for Doorkeeper. – obo Oct 10 '14 at 08:55
  • Yeh, that's cool thanks for the update. I just realised the reason I was getting 401's was because I was using :confirmable in devise config! – cjroebuck Oct 10 '14 at 11:37
  • does that mean we should also be overriding the revoke action and calling devise's sign_out method? – cjroebuck Oct 10 '14 at 11:42
  • I think we should ; but it also means we should override the specific simple-auth-method's to make the call to the backend. For now it only invalidates the session on the client's side. – obo Oct 10 '14 at 12:43
  • Nice implementation. Have you come across any issues with the serverTokenEndpoint property defaulting to '/token'? I'm having trouble setting it to my rails endpoint. – Bobby Hazel Nov 07 '14 at 05:22
  • I had no problem to set my rails endpoint with the version of ember-simple-auth I used at this time. Maybe things are a bit different with the latest release, I did not work on my project for a while and did not follow the news about ember, ember-cli and ember-simple-auth. – obo Nov 07 '14 at 09:13
  • @obo This code works great!. But when I refresh the page after login with facebook the session gone. Any idea? – Soundar Rathinasamy Mar 18 '15 at 07:01
  • @obo this solution looks nice, before I get my hands dirty into it, could you please tell me how I would do if I want to use multiple providers at the same time?? Thanks a lot – Moh Apr 23 '15 at 12:48
  • @Moh I think you could create a specific authenticator according to the behavior of each provider... – obo Apr 23 '15 at 15:07
  • @obo you mean I must create authenticators for each provider? I actually was thinking to pass the provider to the server then it does the whole work, I opened a question here with a try solution if you could give a feedback would be very appreciated http://stackoverflow.com/questions/29851214/ember-cli-torii-and-multiple-providers – Moh Apr 27 '15 at 07:29
1

I spent a few days trying to figure out how to make it work with torii and ended up ditching it for my own authenticator. This is a mix of code from torii and ember-simple-auth so it's not the cleanest, and probably doesn't handle all the edge cases. It basically extends the ember-simple-auth oauth2 authenticator and adds the custom code to pass the access token to the API.

app/lib/facebook-authenticator.js

/* global FB */

import OAuth2Authenticator from 'simple-auth-oauth2/authenticators/oauth2';
import ajax from 'ic-ajax';

var fbPromise;

var settings = {
  appId: '1234567890',
  version: 'v2.1'
};

function fbLoad(){
  if (fbPromise) { return fbPromise; }

  fbPromise = new Ember.RSVP.Promise(function(resolve){
    FB.init(settings);
    Ember.run(null, resolve);
  });

  return fbPromise;
}

function fblogin() {
  return new Ember.RSVP.Promise(function(resolve, reject){
    FB.login(function(response){
      if (response.authResponse) {
        Ember.run(null, resolve, response.authResponse);
      } else {
        Ember.run(null, reject, response.status);
      }
    }, {scope: 'email'});
  });
}

export default OAuth2Authenticator.extend({
  authenticate: function() {
    var _this = this;

    return new Ember.RSVP.Promise(function(resolve, reject) {
      fbLoad().then(fblogin).then(function(response) {
        ajax(MyApp.API_NAMESPACE + '/oauth/facebook', {
          type: 'POST',
          data: {
            auth_token: response.accessToken,
            user_id: response.userId
          }
        }).then(function(response) {
          Ember.run(function() {
            var expiresAt = _this.absolutizeExpirationTime(response.expires_in);
            _this.scheduleAccessTokenRefresh(response.expires_in, expiresAt, response.refresh_token);
            if (!Ember.isEmpty(expiresAt)) {
              response = Ember.merge(response, { expires_at: expiresAt });
            }
            resolve(response);
          });
        }).catch(function(xhr) {
          Ember.run(function() {
            reject(xhr.textStatus);
          });
        });
      });
    });
  },

  loadFbLogin: function(){
    fbLoad();
  }.on('init')
});
Peter Brown
  • 50,956
  • 18
  • 113
  • 146
  • Nice method. I also wrote my own authenticator that extends Oauth2Authenticator but I still use code from ToriiAuthenticator to fetch the authorization code from the provider (see my answer). – obo Sep 15 '14 at 09:29
0

I used this:

import Ember from 'ember';
import Torii from 'ember-simple-auth/authenticators/torii';
import ENV from "../config/environment";

const { inject: { service } } = Ember;

export default Torii.extend({
  torii: service(),
  ajax: service(),

  authenticate() {
    const ajax = this.get('ajax');

    return this._super(...arguments).then((data) => {
      return ajax.request(ENV.APP.API_HOST + "/oauth/token", {
        type:     'POST',
        dataType: 'json',
        data:     { 'grant_type': 'assertion', 'auth_code': data.authorizationCode, 'data': data }
      }).then((response) => {
        return {
          access_token: response.access_token,
          provider: data.provider,
          data: data
        };
      }).catch((error) => {
        console.log(error);
      });
    });
  }
});
Dave Goulash
  • 125
  • 10