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
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With