Skip to content

Instantly share code, notes, and snippets.

@jiggneshhgohel
Last active October 17, 2018 09:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jiggneshhgohel/193c013ea7ac8cead4c77cef1127c55c to your computer and use it in GitHub Desktop.
Save jiggneshhgohel/193c013ea7ac8cead4c77cef1127c55c to your computer and use it in GitHub Desktop.
Doorkeeper-based OAuth Provider (aka OAuth Server), JWT Token reuse

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

https://stackoverflow.com/questions/31193369/repetitive-authorization-gives-error-422-with-doorkeeper-resource-owner-credent

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.

@jiggneshhgohel
Copy link
Author

jiggneshhgohel commented Aug 23, 2017

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 my Doorkeeper.configure resolved the problem I had of how to return an existing access token, for a ResourceOwner/Application pair, to the Client-app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment