Rails 5.0.0.1
Doorkeeper 4.2.6
Devise 4.2.0
With reference to my comment
I was facing the same situation just recently when made my existing Rails 5 application as an OAuth Provider using Doorkeeper.
And to test my Provider-app I cloned the sample doorkeeper-devise-client and updated it to use Rails 5 to use it as a Client-app which allows my Provider users to connect their account on the Client-app then using the issued Access Token (stored in Client-app's DB against the user) to pull details from Provider in that Client-app.
As a front-end the Client-app had link to "Sign in with OAuth 2 provider" clicking which the OAuth workflow began and executed. On successful auth the Access Token
was issued by my Provider-app to the Client-app and on Client-app the user was signed-in and he could see a GET /me.json
and GET /logout`. Clicking this link the Client-app internally sends
request for that request data from the Provider-app using the access token issues and Provider-app responded with that data validating that the access token was valid.
After that was done I logged-out and again I was seeing the "Sign in with OAuth 2 provider" link. However whenever I clicked that link again on my Client-app I saw following error
Could not authorize you from Doorkeeper because "Invalid credentials".
Checking the server logs in Provider-app I saw following:
Started GET "/oauth/authorize?client_id=45af3dc5333eb9b55f6a19f8b73d233532a3c46869860812b0eeaac3c225ab2f&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fusers%2Fauth%2Fdoorkeeper%2Fcallback&response_type=code&state=f4865dd75b0a173b056ce67ee0af02440334abf38f3caf48" for 127.0.0.1 at 2017-08-22 18:48:56 +0530
Processing by Doorkeeper::AuthorizationsController#new as HTML
Parameters: {"client_id"=>"45af3dc5333eb9b55f6a19f8b73d233532a3c46869860812b0eeaac3c225ab2f", "redirect_uri"=>"http://localhost:5000/users/auth/doorkeeper/callback", "response_type"=>"code", "state"=>"f4865dd75b0a173b056ce67ee0af02440334abf38f3caf48"}
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
Doorkeeper::Application Load (0.1ms) SELECT "oauth_applications".* FROM "oauth_applications" WHERE "oauth_applications"."uid" = $1 LIMIT $2 [["uid", "45af3dc5333eb9b55f6a19f8b73d233532a3c46869860812b0eeaac3c225ab2f"], ["LIMIT", 1]]
Doorkeeper::AccessToken Load (0.3ms) SELECT "oauth_access_tokens".* FROM "oauth_access_tokens" WHERE "oauth_access_tokens"."application_id" = $1 AND "oauth_access_tokens"."resource_owner_id" = $2 AND "oauth_access_tokens"."revoked_at" IS NULL ORDER BY created_at desc LIMIT $3 [["application_id", 1], ["resource_owner_id", 1], ["LIMIT", 1]]
(0.1ms) BEGIN
Doorkeeper::AccessGrant Exists (0.2ms) SELECT 1 AS one FROM "oauth_access_grants" WHERE "oauth_access_grants"."token" = $1 LIMIT $2 [["token", "fa000709b9a4227ff8a35f423ea4dc7ec2899eecc0b96320b8fe183c37088bb4"], ["LIMIT", 1]]
SQL (0.3ms) INSERT INTO "oauth_access_grants" ("resource_owner_id", "application_id", "token", "expires_in", "redirect_uri", "created_at", "scopes") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id" [["resource_owner_id", 1], ["application_id", 1], ["token", "fa000709b9a4227ff8a35f423ea4dc7ec2899eecc0b96320b8fe183c37088bb4"], ["expires_in", 600], ["redirect_uri", "http://localhost:5000/users/auth/doorkeeper/callback"], ["created_at", 2017-08-22 13:18:56 UTC], ["scopes", ""]]
(23.9ms) COMMIT
Redirected to http://localhost:5000/users/auth/doorkeeper/callback?code=fa000709b9a4227ff8a35f423ea4dc7ec2899eecc0b96320b8fe183c37088bb4&state=f4865dd75b0a173b056ce67ee0af02440334abf38f3caf48
Completed 302 Found in 32ms (ActiveRecord: 25.2ms)
Started POST "/oauth/token" for 127.0.0.1 at 2017-08-22 18:48:56 +0530
Processing by Doorkeeper::TokensController#create as */*
Parameters: {"client_id"=>"45af3dc5333eb9b55f6a19f8b73d233532a3c46869860812b0eeaac3c225ab2f", "client_secret"=>"[FILTERED]", "code"=>"[FILTERED]", "grant_type"=>"authorization_code", "redirect_uri"=>"http://localhost:5000/users/auth/doorkeeper/callback"}
Doorkeeper::AccessGrant Load (0.3ms) SELECT "oauth_access_grants".* FROM "oauth_access_grants" WHERE "oauth_access_grants"."token" = $1 LIMIT $2 [["token", "fa000709b9a4227ff8a35f423ea4dc7ec2899eecc0b96320b8fe183c37088bb4"], ["LIMIT", 1]]
Doorkeeper::Application Load (0.2ms) SELECT "oauth_applications".* FROM "oauth_applications" WHERE "oauth_applications"."uid" = $1 AND "oauth_applications"."secret" = $2 LIMIT $3 [["uid", "45af3dc5333eb9b55f6a19f8b73d233532a3c46869860812b0eeaac3c225ab2f"], ["secret", "ba879bd3b7b2d10b6003ed3f653e6c7b3f5ac6b165383fc288b4099d9d1a7194"], ["LIMIT", 1]]
(0.1ms) BEGIN
Doorkeeper::AccessGrant Load (0.2ms) SELECT "oauth_access_grants".* FROM "oauth_access_grants" WHERE "oauth_access_grants"."id" = $1 LIMIT $2 FOR UPDATE [["id", 15], ["LIMIT", 1]]
SQL (0.3ms) UPDATE "oauth_access_grants" SET "revoked_at" = $1 WHERE "oauth_access_grants"."id" = $2 [["revoked_at", 2017-08-22 13:18:56 UTC], ["id", 15]]
Doorkeeper::Application Load (0.2ms) SELECT "oauth_applications".* FROM "oauth_applications" WHERE "oauth_applications"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
CACHE (0.0ms) SELECT "oauth_applications".* FROM "oauth_applications" WHERE "oauth_applications"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Doorkeeper::AccessToken Exists (0.2ms) SELECT 1 AS one FROM "oauth_access_tokens" WHERE "oauth_access_tokens"."token" = $1 LIMIT $2 [["token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VyIjp7ImlkIjoxLCJlbWFpbCI6ImFkbWluQHNpbXBseS1ob21lLmNvbSJ9fQ."], ["LIMIT", 1]]
(0.1ms) ROLLBACK
Completed 422 Unprocessable Entity in 8ms
ActiveRecord::RecordInvalid (Validation failed: Token has already been taken):
activerecord (5.0.0.1) lib/active_record/validations.rb:78:in `raise_validation_error'
activerecord (5.0.0.1) lib/active_record/validations.rb:50:in `save!'
activerecord (5.0.0.1) lib/active_record/attribute_methods/dirty.rb:30:in `save!'
activerecord (5.0.0.1) lib/active_record/transactions.rb:324:in `block in save!'
activerecord (5.0.0.1) lib/active_record/transactions.rb:395:in `block in with_transaction_returning_status'
activerecord (5.0.0.1) lib/active_record/connection_adapters/abstract/database_statements.rb:230:in `transaction'
activerecord (5.0.0.1) lib/active_record/transactions.rb:211:in `transaction'
activerecord (5.0.0.1) lib/active_record/transactions.rb:392:in `with_transaction_returning_status'
activerecord (5.0.0.1) lib/active_record/transactions.rb:324:in `save!'
activerecord (5.0.0.1) lib/active_record/suppressor.rb:45:in `save!'
activerecord (5.0.0.1) lib/active_record/persistence.rb:51:in `create!'
doorkeeper (4.2.6) lib/doorkeeper/models/access_token_mixin.rb:150:in `find_or_create_for'
doorkeeper (4.2.6) lib/doorkeeper/oauth/base_request.rb:35:in `find_or_create_access_token'
doorkeeper (4.2.6) lib/doorkeeper/oauth/authorization_code_request.rb:26:in `block in before_successful_response'
activerecord (5.0.0.1) lib/active_record/connection_adapters/abstract/database_statements.rb:232:in `block in transaction'
activerecord (5.0.0.1) lib/active_record/connection_adapters/abstract/transaction.rb:189:in `within_new_transaction'
activerecord (5.0.0.1) lib/active_record/connection_adapters/abstract/database_statements.rb:232:in `transaction'
activerecord (5.0.0.1) lib/active_record/transactions.rb:211:in `transaction'
activerecord (5.0.0.1) lib/active_record/transactions.rb:310:in `transaction'
doorkeeper (4.2.6) lib/doorkeeper/oauth/authorization_code_request.rb:21:in `before_successful_response'
doorkeeper (4.2.6) lib/doorkeeper/oauth/base_request.rb:9:in `authorize'
doorkeeper (4.2.6) lib/doorkeeper/request/strategy.rb:6:in `authorize'
doorkeeper (4.2.6) app/controllers/doorkeeper/tokens_controller.rb:78:in `authorize_response'
doorkeeper (4.2.6) app/controllers/doorkeeper/tokens_controller.rb:4:in `create'
actionpack (5.0.0.1) lib/abstract_controller/base.rb:188:in `process_action'
actionpack (5.0.0.1) lib/action_controller/metal/instrumentation.rb:32:in `block in process_action'
activesupport (5.0.0.1) lib/active_support/notifications.rb:164:in `block in instrument'
activesupport (5.0.0.1) lib/active_support/notifications/instrumenter.rb:21:in `instrument'
activesupport (5.0.0.1) lib/active_support/notifications.rb:164:in `instrument'
actionpack (5.0.0.1) lib/action_controller/metal/instrumentation.rb:30:in `process_action'
actionpack (5.0.0.1) lib/action_controller/metal/rendering.rb:30:in `process_action'
actionpack (5.0.0.1) lib/abstract_controller/base.rb:126:in `process'
actionpack (5.0.0.1) lib/action_controller/metal.rb:190:in `dispatch'
actionpack (5.0.0.1) lib/action_controller/metal.rb:262:in `dispatch'
actionpack (5.0.0.1) lib/action_dispatch/routing/route_set.rb:50:in `dispatch'
actionpack (5.0.0.1) lib/action_dispatch/routing/route_set.rb:32:in `serve'
actionpack (5.0.0.1) lib/action_dispatch/journey/router.rb:39:in `block in serve'
actionpack (5.0.0.1) lib/action_dispatch/journey/router.rb:26:in `each'
actionpack (5.0.0.1) lib/action_dispatch/journey/router.rb:26:in `serve'
actionpack (5.0.0.1) lib/action_dispatch/routing/route_set.rb:725:in `call'
warden (1.2.6) lib/warden/manager.rb:35:in `block in call'
warden (1.2.6) lib/warden/manager.rb:34:in `catch'
warden (1.2.6) lib/warden/manager.rb:34:in `call'
rack (2.0.1) lib/rack/etag.rb:25:in `call'
rack (2.0.1) lib/rack/conditional_get.rb:38:in `call'
rack (2.0.1) lib/rack/head.rb:12:in `call'
rack (2.0.1) lib/rack/session/abstract/id.rb:222:in `context'
rack (2.0.1) lib/rack/session/abstract/id.rb:216:in `call'
actionpack (5.0.0.1) lib/action_dispatch/middleware/cookies.rb:613:in `call'
activerecord (5.0.0.1) lib/active_record/migration.rb:552:in `call'
actionpack (5.0.0.1) lib/action_dispatch/middleware/callbacks.rb:38:in `block in call'
activesupport (5.0.0.1) lib/active_support/callbacks.rb:97:in `__run_callbacks__'
activesupport (5.0.0.1) lib/active_support/callbacks.rb:750:in `_run_call_callbacks'
activesupport (5.0.0.1) lib/active_support/callbacks.rb:90:in `run_callbacks'
actionpack (5.0.0.1) lib/action_dispatch/middleware/callbacks.rb:36:in `call'
actionpack (5.0.0.1) lib/action_dispatch/middleware/executor.rb:12:in `call'
actionpack (5.0.0.1) lib/action_dispatch/middleware/remote_ip.rb:79:in `call'
actionpack (5.0.0.1) lib/action_dispatch/middleware/debug_exceptions.rb:49:in `call'
web-console (2.3.0) lib/web_console/middleware.rb:28:in `block in call'
web-console (2.3.0) lib/web_console/middleware.rb:18:in `catch'
web-console (2.3.0) lib/web_console/middleware.rb:18:in `call'
actionpack (5.0.0.1) lib/action_dispatch/middleware/show_exceptions.rb:31:in `call'
railties (5.0.0.1) lib/rails/rack/logger.rb:36:in `call_app'
railties (5.0.0.1) lib/rails/rack/logger.rb:24:in `block in call'
activesupport (5.0.0.1) lib/active_support/tagged_logging.rb:70:in `block in tagged'
activesupport (5.0.0.1) lib/active_support/tagged_logging.rb:26:in `tagged'
activesupport (5.0.0.1) lib/active_support/tagged_logging.rb:70:in `tagged'
railties (5.0.0.1) lib/rails/rack/logger.rb:24:in `call'
sprockets-rails (3.2.0) lib/sprockets/rails/quiet_assets.rb:13:in `call'
request_store (1.3.1) lib/request_store/middleware.rb:9:in `call'
actionpack (5.0.0.1) lib/action_dispatch/middleware/request_id.rb:24:in `call'
rack (2.0.1) lib/rack/method_override.rb:22:in `call'
rack (2.0.1) lib/rack/runtime.rb:22:in `call'
activesupport (5.0.0.1) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call'
actionpack (5.0.0.1) lib/action_dispatch/middleware/executor.rb:12:in `call'
actionpack (5.0.0.1) lib/action_dispatch/middleware/static.rb:136:in `call'
rack (2.0.1) lib/rack/sendfile.rb:111:in `call'
railties (5.0.0.1) lib/rails/engine.rb:522:in `call'
My Provider-app uses JWT for generating tokens. And initially I was using following configuration:
Doorkeeper::JWT.configure do
token_payload do |opts|
user = User.find(opts[:resource_owner_id])
{
user: {
id: user.id,
email: user.email
}
}
end
secret_key Settings.oauth.doorkeeper.jwt.encryption_secret
end
Searching the web for that error I found the solution at
and updated my JWT config to following:
Doorkeeper::JWT.configure do
# Set the payload for the JWT token. This should contain unique information
# about the user.
# Defaults to a randomly generated token in a hash
# { token: "RANDOM-TOKEN" }
#
# Additional references to prevent
# ```
# 422 error
#
# ActiveRecord::RecordInvalid (Validation failed: Token has already been taken):
# ```
#
# https://stackoverflow.com/questions/31193369/repetitive-authorization-gives-error-422-with-doorkeeper-resource-owner-credent
token_payload do |opts|
user = User.find(opts[:resource_owner_id])
{
iss: Rails.application.class.parent.to_s.underscore,
iat: Time.now.utc.to_i,
jti: SecureRandom.uuid,
user: {
id: user.id,
email: user.email
}
}
end
# Optionally set additional headers for the JWT. See https://tools.ietf.org/html/rfc7515#section-4.1
# token_headers do |opts|
# {
# kid: opts[:application][:uid]
# }
# end
# Use the application secret specified in the Access Grant token
# Defaults to false
# If you specify `use_application_secret true`, both secret_key and secret_key_path will be ignored
# use_application_secret false
# Set the encryption secret. This would be shared with any other applications
# that should be able to read the payload of the token.
# Defaults to "secret"
secret_key Settings.oauth.doorkeeper.jwt.encryption_secret
# If you want to use RS* encoding specify the path to the RSA key
# to use for signing.
# If you specify a secret_key_path it will be used instead of secret_key
# secret_key_path "path/to/file.pem"
# Specify encryption type. Supports any algorithim in
# https://github.com/progrium/ruby-jwt
# defaults to nil
encryption_method :hs256
end
which resolved the error
ActiveRecord::RecordInvalid (Validation failed: Token has already been taken):
And afterwards I was able to use "Sign in with OAuth 2 provider" repeatedly without any errors. But I observed a strange thing that each time I used "Sign in with OAuth 2 provider" link to sign-in from my Client-app and the OAuth request was sent to my Provider-app Doorkeeper in Provider-app created a new entry in OAUTH_ACCESS_TOKENS table.
THAT WAS NOT INTENDED. We are avoiding using refresh_tokens and our access token expiry we have set to 1.year
. In such a case each time using
"Sign in with OAuth 2 provider" link would generate a new Access Token and hence previously issued Access Token would never be used and this token generation
on repeated basis would make it possible to obtain a new Access Token valid for 1.year.
First-attempt Sign in via OAuth and a token was issued for 1.year. This token was used for say 10 days. Second-attempt Sign in via OAuth and obtained a new token was issued for 1.year. This token was used for say another 10 days.
Subsequent attempts like this will always issue a fresh token for 1.year and which if continued it would let the users use a token which from usage perspective can never expire.
Searching on how to return existing access token I ended up finding doorkeeper-gem/doorkeeper#383 which narrated the scenario in exact manner I had. Thanks to @kenn for bringing that up. With his hard-efforts and the support he received from Doorkeeper maintainer team finally a configuration option
reuse_access_token
was introduced in PR doorkeeper-gem/doorkeeper#387 which is now available for use in Doorkeeper latest version.So adding the
reuse_access_token
in myDoorkeeper.configure
resolved the problem I had of how to return an existing access token, for a ResourceOwner/Application pair, to the Client-app.