Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Repetitive authorization gives error 422 with Doorkeeper (Resource owner credentials flow)

I'm new in Rails and webdev. Trying to implement simple API for mobile app with Rails + Devise + Doorkeeper (like in https://github.com/doorkeeper-gem/doorkeeper-provider-app ).

Faced the problem that user can't make authorization request (POST /oauth/token) if he has already received token. I.e.:

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

First time receiving:

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

Token goes to oauth_access_tokens table (what's not necessary for JWT, but not a problem).

If I repeat this request, i'll receive 422 error and rails' page with something like

    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'

Even if i revoke token with POST /oauth/revoke, everything will be the same, except revoking timestamp in oauth_access_tokens. And that's very strange.

I investigate it a bit and find piece of code in doorkeeper gem (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

So, the error is in create! method, which says that we tried to add duplicate (in stacktrace). And if i set reuse_access_token in Doorkeeper.configure, then it's ok. But i'll receive the same token after each authorization, what is very unsecure, as i understand. And yes, if i manually delete token from oauth_access_tokens, then i'll be able to auth.

So what's wrong?

My Doorkeeper config:

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

Routes:

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
like image 692
cheyuriy Avatar asked Jul 02 '15 19:07

cheyuriy


2 Answers

Well, if u want to find an answer then just formulate a question.

Problem was in default implementation of Doorkeeper::JWT token. It doesn't have any randomness in payload, so it was always the same for every user's authentication. So i added:

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

And it works fine.

like image 185
cheyuriy Avatar answered Nov 03 '22 00:11

cheyuriy


I don't have enough reputation to comment on the selected answer, so I'll add another answer to suggest an improvement.

Instead of creating the rnd claim, which is subject to name collisions, use the jti reserved claim since it's meant to provide a unique identifier for the JWT. I also recommend using a UUID instead of Hex for the jti value.

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

You can read more about JWT reserved claims here.

like image 30
kevinsapp Avatar answered Nov 03 '22 00:11

kevinsapp