重复授权给 Doorkeeper 错误 422(资源所有者凭据流)
Repetitive authorization gives error 422 with Doorkeeper (Resource owner credentials flow)
我是 Rails 和 webdev 的新手。尝试使用 Rails + Devise + Doorkeeper(如 https://github.com/doorkeeper-gem/doorkeeper-provider-app)为移动应用程序实现简单的 API。
遇到了如果用户已经收到令牌就无法进行授权请求(POST /oauth/token)的问题。即:
curl -F grant_type=password -F username=1@tothetrip.com -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 table(JWT 不需要什么,但不是问题)。
如果我重复此请求,我将收到 422 错误和 rails' 页面,其中包含类似
的内容
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 中撤销时间戳外,一切都将相同。这很奇怪。
我调查了一下,在 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
所以,错误在创建!方法,它表示我们尝试添加重复项(在堆栈跟踪中)。如果我在 Doorkeeper.configure 中设置 reuse_access_token,那么就可以了。但据我所知,每次授权后我都会收到相同的令牌,这是非常不安全的。是的,如果我从 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
嗯,如果你想找到答案,那就提出一个问题。
问题出在 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
而且效果很好。
我没有足够的声誉来评论所选答案,因此我将添加另一个答案以提出改进建议。
不要创建受名称冲突影响的 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
我是 Rails 和 webdev 的新手。尝试使用 Rails + Devise + Doorkeeper(如 https://github.com/doorkeeper-gem/doorkeeper-provider-app)为移动应用程序实现简单的 API。
遇到了如果用户已经收到令牌就无法进行授权请求(POST /oauth/token)的问题。即:
curl -F grant_type=password -F username=1@tothetrip.com -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 table(JWT 不需要什么,但不是问题)。
如果我重复此请求,我将收到 422 错误和 rails' 页面,其中包含类似
的内容 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 中撤销时间戳外,一切都将相同。这很奇怪。
我调查了一下,在 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
所以,错误在创建!方法,它表示我们尝试添加重复项(在堆栈跟踪中)。如果我在 Doorkeeper.configure 中设置 reuse_access_token,那么就可以了。但据我所知,每次授权后我都会收到相同的令牌,这是非常不安全的。是的,如果我从 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
嗯,如果你想找到答案,那就提出一个问题。
问题出在 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
而且效果很好。
我没有足够的声誉来评论所选答案,因此我将添加另一个答案以提出改进建议。
不要创建受名称冲突影响的 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