Повторная авторизация дает ошибку 422 с Doorkeeper (поток учетных данных владельца ресурса)

Я новичок в Rails и webdev. Попытка реализовать простой API для мобильного приложения с помощью Rails + Devise + Doorkeeper (как в https://github.com/doorkeeper-gem/doorkeeper-provider-app).

Столкнулся с проблемой, что пользователь не может сделать запрос на авторизацию (POST /oauth/token), если он уже получил токен. То есть:

curl -F grant_type=password -F [email protected] -F password=12345678 -X POST http://api.to_the_trip.dev/oauth/token

Получение впервые:

{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7ImlkIjoyLCJlbWFpbCI6IjFAdG90aGV0cmlwLmNvbSJ9fQ.dYai6nH_KYb9YbDltqwFuzCO3i0igR_gw2T7u_TeVcI","token_type":"bearer","expires_in":7200,"created_at":1435864812}

Токен попадает в таблицу oauth_access_tokens (что для JWT не обязательно, но не проблема).

Если я повторю этот запрос, я получу ошибку 422 и страницу рельсов с чем-то вроде

    ActiveRecord::RecordInvalid in Doorkeeper::TokensController#create
Validation failed: Token has already been taken

activerecord (4.2.3) lib/active_record/validations.rb:79:in `raise_record_invalid'
activerecord (4.2.3) lib/active_record/validations.rb:43:in `save!'
activerecord (4.2.3) lib/active_record/attribute_methods/dirty.rb:29:in `save!'
activerecord (4.2.3) lib/active_record/transactions.rb:291:in `block in save!'
activerecord (4.2.3) lib/active_record/transactions.rb:351:in `block in with_transaction_returning_status'
activerecord (4.2.3) lib/active_record/connection_adapters/abstract/database_statements.rb:213:in `block in transaction'
activerecord (4.2.3) lib/active_record/connection_adapters/abstract/transaction.rb:184:in `within_new_transaction'
activerecord (4.2.3) lib/active_record/connection_adapters/abstract/database_statements.rb:213:in `transaction'
activerecord (4.2.3) lib/active_record/transactions.rb:220:in `transaction'
activerecord (4.2.3) lib/active_record/transactions.rb:348:in `with_transaction_returning_status'
activerecord (4.2.3) lib/active_record/transactions.rb:291:in `save!'
activerecord (4.2.3) lib/active_record/persistence.rb:51:in `create!'
doorkeeper (2.2.1) lib/doorkeeper/models/access_token_mixin.rb:76:in `find_or_create_for'
doorkeeper (2.2.1) lib/doorkeeper/oauth/request_concern.rb:33:in `find_or_create_access_token'
doorkeeper (2.2.1) lib/doorkeeper/oauth/password_access_token_request.rb:30:in `before_successful_response'
doorkeeper (2.2.1) lib/doorkeeper/oauth/request_concern.rb:7:in `authorize'
doorkeeper (2.2.1) lib/doorkeeper/request/password.rb:19:in `authorize'
doorkeeper (2.2.1) app/controllers/doorkeeper/tokens_controller.rb:42:in `authorize_response'
doorkeeper (2.2.1) app/controllers/doorkeeper/tokens_controller.rb:4:in `create'

Даже если я отзову токен с помощью POST /oauth/revoke, все будет по-прежнему, за исключением отзыва метки времени в oauth_access_tokens. И это очень странно.

Я немного исследую это и нахожу фрагмент кода в геме привратника (access_token_mixin.rb):

def find_or_create_for(application, resource_owner_id, scopes, expires_in, use_refresh_token)
        if Doorkeeper.configuration.reuse_access_token
          access_token = matching_token_for(application, resource_owner_id, scopes)
          if access_token && !access_token.expired?
            return access_token
          end
        end
        create!(
          application_id:    application.try(:id),
          resource_owner_id: resource_owner_id,
          scopes:            scopes.to_s,
          expires_in:        expires_in,
          use_refresh_token: use_refresh_token
        )
      end

Итак, ошибка в create! метод, который говорит, что мы пытались добавить дубликат (в трассировке стека). И если я установил reuse_access_token в Doorkeeper.configure, то все в порядке. Но я буду получать один и тот же токен после каждой авторизации, что очень небезопасно, как я понимаю. И да, если я вручную удалю токен из oauth_access_tokens, тогда я смогу авторизоваться.

Так что же не так?

Конфигурация моего привратника:

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

  resource_owner_authenticator do
    current_user || env['warden'].authenticate!(:scope => :user)
  end

  resource_owner_from_credentials do |routes|
    request.params[:user] = {:email => request.params[:username], :password => request.params[:password]}
    request.env["devise.allow_params_authentication"] = true
    user = request.env['warden'].authenticate!(:scope => :user)
    env['warden'].logout
    user
  end

  access_token_generator "Doorkeeper::JWT"
end

Doorkeeper.configuration.token_grant_types << "password"

Doorkeeper::JWT.configure do
#JWT config
end

Маршруты:

require 'api_constraints'

Rails.application.routes.draw do
  use_doorkeeper
  devise_for :users
  namespace :api, defaults: {format: :json}, constraints: { subdomain: 'api' }, path: '/' do
    scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do
      resources :users, :only => [:show, :create, :update]

      get '/me' => "credentials#me"
    end
  end
end

person cheyuriy    schedule 02.07.2015    source источник


Ответы (2)


Ну, если вы хотите найти ответ, просто сформулируйте вопрос.

Проблема заключалась в реализации токена Doorkeeper::JWT по умолчанию. У него нет никакой случайности в полезной нагрузке, поэтому он всегда был одинаковым для аутентификации каждого пользователя. Поэтому я добавил:

Doorkeeper::JWT.configure do
  token_payload do |opts|
    user = User.find(opts[:resource_owner_id])
    {
      iss: "myapp",  #this
      iat: DateTime.current.utc.to_i,   #this
      rnd: SecureRandom.hex,   #and this

      user: {
        id: user.id,
        email: user.email
      }
    }
  end

  secret_key "key"

  encryption_method :hs256
end

И это работает нормально.

person cheyuriy    schedule 02.07.2015

У меня недостаточно репутации, чтобы прокомментировать выбранный ответ, поэтому я добавлю еще один ответ, чтобы предложить улучшение.

Вместо создания утверждения rnd, которое подвержено конфликтам имен, используйте зарезервированное утверждение jti, поскольку оно предназначено для предоставления уникального идентификатора для JWT. Я также рекомендую использовать UUID вместо Hex для значения jti.

Doorkeeper::JWT.configure do
  token_payload do |opts|
    user = User.find(opts[:resource_owner_id])
    {
      iss: "myapp",
      iat: DateTime.current.utc.to_i,
      jti: SecureRandom.uuid,

      user: {
        id: user.id,
        email: user.email
      }
    }
  end

  secret_key "key"

  encryption_method :hs256
end

Подробнее о зарезервированных утверждениях JWT можно прочитать здесь.< /а>

person kevinsapp    schedule 09.12.2015