Рабочий процесс для Ember-simple-auth, Torii и Facebook Oauth2

После моего предыдущего вопроса о ember-simple-auth и torii, я успешно аутентифицирую своих пользователей с помощью их учетных записей Facebook.

Но в настоящее время провайдер torii facebook-oauth2 возвращает код авторизации от Facebook; когда обещание выполняется, я отправляю этот код авторизации на свой сервер, где я выполняю запрос к Facebook, чтобы получить идентификатор пользователя и адрес электронной почты: затем я аутентифицирую пользователя на своем сервере, генерируя определенный токен доступа и отправляя его обратно в мое приложение ember.

Код клиента:

// 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);
                }
            );
        }
    }
});

Проблема в том, что сеанс ember-simple-auth помечается как аутентифицированный, когда обещание аутентификации разрешается, а затем приложение перенаправляется на конкретный аутентифицированный маршрут. Но в этом случае сеанс должен быть аутентифицирован, когда мой бэкэнд возвращает «настоящий» access_token.

Есть ли способ управлять этим рабочим процессом с помощью ember-simple-auth-torii или я должен написать свой собственный аутентификатор?


person obo    schedule 13.09.2014    source источник
comment
Не могли бы вы рассказать мне, как проверить токен, полученный facebook на вашем собственном сервере?   -  person Ajey    schedule 12.01.2015
comment
См. Принятый ответ: в tokens_controller # create я использую драгоценный камень Koala, чтобы получить access_token с auth_code, полученным Facebook.   -  person obo    schedule 13.01.2015
comment
Я использую узел на своем сервере. Любая библиотека узлов для этого?   -  person Ajey    schedule 13.01.2015
comment
npmjs.com/search?q=facebook   -  person obo    schedule 13.01.2015


Ответы (3)


Наконец, я написал свой собственный аутентификатор, как предложил Бирлингтон. Но также я даю своим пользователям способ аутентификации с использованием логина / пароля, поэтому я переопределил аутентификатор ember-simple-auth-oauth2, изменив только метод аутентификации и использовал ember-simple-auth-torii.

Теперь я могу использовать Torii, чтобы получить код авторизации из учетной записи пользователя Facebook, отправить этот код на мой сервер, аутентифицировать пользователя и сгенерировать токен доступа, которым будет управлять ember-simple-auth, как токен oauth2.

Вот код:

// 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
        });
    }
};

Мой бэкэнд находится в Rails и использует Doorkeeper для управления access_token и Devise. Я переопределил Doorkeeper :: TokensController, чтобы передать user_id с токеном и управлять кодом авторизации facebook, если он есть (этот код следует отредактировать):

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

Вот код, который я использую в инициализаторе doorkeeper.rb для аутентификации пользователя.

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
person obo    schedule 15.09.2014
comment
Нужен ли здесь sign_in (: user, user)? Что он делает, если вы все равно возвращаете действующий токен доступа? Я пробовал точно такой же метод, используя devise / doorkeeper на моем сервере, но метод sign_in всегда выдает исключение и вызывает ответ 401 Unauthenticated - person cjroebuck; 10.10.2014
comment
Этот метод используется для добавления информации пользователю, такой как last_sign_in_ip, last_sign_in_at, sign_in_count и т. Д. Я отредактировал свой ответ конфигурацией, которую использую для Doorkeeper. - person obo; 10.10.2014
comment
Ага, круто, спасибо за обновление. Я просто понял, что причина, по которой я получал 401, заключалась в том, что я использовал: confirmable в конфигурации devise! - person cjroebuck; 10.10.2014
comment
Означает ли это, что мы также должны переопределить действие revoke и вызвать метод devise sign_out? - person cjroebuck; 10.10.2014
comment
Я думаю, нам следует ; но это также означает, что мы должны переопределить конкретный метод simple-auth, чтобы вызвать бэкэнд. На данный момент он только аннулирует сеанс на стороне клиента. - person obo; 10.10.2014
comment
Хорошая реализация. Сталкивались ли вы с какими-либо проблемами, когда для свойства serverTokenEndpoint по умолчанию задано значение '/ token'? У меня проблемы с установкой его на конечную точку рельсов. - person Bobby Hazel; 07.11.2014
comment
У меня не было проблем с установкой моей конечной точки rails с версией ember-simple-auth, которую я использовал в то время. Возможно, с последним релизом дела обстоят немного иначе, я какое-то время не работал над своим проектом и не следил за новостями о ember, ember-cli и ember-simple-auth. - person obo; 07.11.2014
comment
@obo Этот код отлично работает !. Но когда я обновляю страницу после входа в систему с помощью facebook, сеанс пропадает. Любая идея? - person Soundar Rathinasamy; 18.03.2015
comment
@obo, это решение выглядит красиво, прежде чем я запачкаю его руками, не могли бы вы рассказать мне, как бы я поступил, если бы я хотел использовать несколько провайдеров одновременно? Большое спасибо - person Moh; 23.04.2015
comment
@Moh Я думаю, вы могли бы создать конкретный аутентификатор в соответствии с поведением каждого провайдера ... - person obo; 23.04.2015
comment
@obo ты имеешь в виду, что я должен создавать аутентификаторы для каждого провайдера? Я действительно думал передать поставщика на сервер, тогда он сделает всю работу, я открыл здесь вопрос с пробным решением, если бы вы могли дать обратную связь, было бы очень признательно stackoverflow.com/questions/29851214/ - person Moh; 27.04.2015

Я потратил несколько дней, пытаясь понять, как заставить его работать с torii, и в итоге отказался от него в пользу собственного аутентификатора. Это смесь кода из torii и ember-simple-auth, поэтому он не самый чистый и, вероятно, не обрабатывает все крайние случаи. Он в основном расширяет аутентификатор ember-simple-auth oauth2 и добавляет настраиваемый код для передачи токена доступа в API.

приложение / lib / facebook-аутентификатор.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')
});
person Peter Brown    schedule 13.09.2014
comment
Хороший метод. Я также написал свой собственный аутентификатор, который расширяет Oauth2Authenticator, но я все еще использую код ToriiAuthenticator для получения кода авторизации от провайдера (см. Мой ответ). - person obo; 15.09.2014

Я использовал это:

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);
      });
    });
  }
});
person Dave Goulash    schedule 04.12.2016