Repository: applicake/doorkeeper Branch: main Commit: df16e5553035 Files: 326 Total size: 843.7 KB Directory structure: gitextract_wns691gx/ ├── .codeclimate.yml ├── .coveralls.yml ├── .dockerignore ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── changelog.yml │ ├── ci.yml │ └── rubocop.yml ├── .gitignore ├── .hound.yml ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── AGENTS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Gemfile ├── MIT-LICENSE ├── NEWS.md ├── README.md ├── RELEASING.md ├── Rakefile ├── SECURITY.md ├── UPGRADE.md ├── app/ │ ├── assets/ │ │ └── stylesheets/ │ │ └── doorkeeper/ │ │ ├── admin/ │ │ │ └── application.css │ │ └── application.css │ ├── controllers/ │ │ └── doorkeeper/ │ │ ├── application_controller.rb │ │ ├── application_metal_controller.rb │ │ ├── applications_controller.rb │ │ ├── authorizations_controller.rb │ │ ├── authorized_applications_controller.rb │ │ ├── token_info_controller.rb │ │ └── tokens_controller.rb │ ├── helpers/ │ │ └── doorkeeper/ │ │ └── dashboard_helper.rb │ └── views/ │ ├── doorkeeper/ │ │ ├── applications/ │ │ │ ├── _delete_form.html.erb │ │ │ ├── _form.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── index.html.erb │ │ │ ├── new.html.erb │ │ │ └── show.html.erb │ │ ├── authorizations/ │ │ │ ├── error.html.erb │ │ │ ├── form_post.html.erb │ │ │ ├── new.html.erb │ │ │ └── show.html.erb │ │ └── authorized_applications/ │ │ ├── _delete_form.html.erb │ │ └── index.html.erb │ └── layouts/ │ └── doorkeeper/ │ ├── admin.html.erb │ └── application.html.erb ├── benchmark/ │ ├── ruby/ │ │ └── client_credentials.rb │ └── wrk/ │ └── .keep ├── bin/ │ └── console ├── config/ │ └── locales/ │ └── en.yml ├── doorkeeper.gemspec ├── gemfiles/ │ ├── rails_7_0.gemfile │ ├── rails_7_1.gemfile │ ├── rails_7_2.gemfile │ ├── rails_8_0.gemfile │ └── rails_edge.gemfile ├── lib/ │ ├── doorkeeper/ │ │ ├── config/ │ │ │ ├── abstract_builder.rb │ │ │ ├── option.rb │ │ │ └── validations.rb │ │ ├── config.rb │ │ ├── engine.rb │ │ ├── errors.rb │ │ ├── grant_flow/ │ │ │ ├── fallback_flow.rb │ │ │ ├── flow.rb │ │ │ └── registry.rb │ │ ├── grant_flow.rb │ │ ├── grape/ │ │ │ ├── authorization_decorator.rb │ │ │ └── helpers.rb │ │ ├── helpers/ │ │ │ └── controller.rb │ │ ├── models/ │ │ │ ├── access_grant_mixin.rb │ │ │ ├── access_token_mixin.rb │ │ │ ├── application_mixin.rb │ │ │ └── concerns/ │ │ │ ├── accessible.rb │ │ │ ├── expirable.rb │ │ │ ├── expiration_time_sql_math.rb │ │ │ ├── orderable.rb │ │ │ ├── ownership.rb │ │ │ ├── polymorphic_resource_owner.rb │ │ │ ├── resource_ownerable.rb │ │ │ ├── reusable.rb │ │ │ ├── revocable.rb │ │ │ ├── scopes.rb │ │ │ ├── secret_storable.rb │ │ │ └── write_to_primary.rb │ │ ├── oauth/ │ │ │ ├── authorization/ │ │ │ │ ├── code.rb │ │ │ │ ├── context.rb │ │ │ │ ├── token.rb │ │ │ │ └── uri_builder.rb │ │ │ ├── authorization_code_request.rb │ │ │ ├── base_request.rb │ │ │ ├── base_response.rb │ │ │ ├── client/ │ │ │ │ └── credentials.rb │ │ │ ├── client.rb │ │ │ ├── client_credentials/ │ │ │ │ ├── creator.rb │ │ │ │ ├── issuer.rb │ │ │ │ └── validator.rb │ │ │ ├── client_credentials_request.rb │ │ │ ├── code_request.rb │ │ │ ├── code_response.rb │ │ │ ├── error.rb │ │ │ ├── error_response.rb │ │ │ ├── forbidden_token_response.rb │ │ │ ├── helpers/ │ │ │ │ ├── scope_checker.rb │ │ │ │ ├── unique_token.rb │ │ │ │ └── uri_checker.rb │ │ │ ├── hooks/ │ │ │ │ └── context.rb │ │ │ ├── invalid_request_response.rb │ │ │ ├── invalid_token_response.rb │ │ │ ├── nonstandard.rb │ │ │ ├── password_access_token_request.rb │ │ │ ├── pre_authorization.rb │ │ │ ├── refresh_token_request.rb │ │ │ ├── scopes.rb │ │ │ ├── token.rb │ │ │ ├── token_introspection.rb │ │ │ ├── token_request.rb │ │ │ └── token_response.rb │ │ ├── oauth.rb │ │ ├── orm/ │ │ │ ├── active_record/ │ │ │ │ ├── access_grant.rb │ │ │ │ ├── access_token.rb │ │ │ │ ├── application.rb │ │ │ │ ├── mixins/ │ │ │ │ │ ├── access_grant.rb │ │ │ │ │ ├── access_token.rb │ │ │ │ │ └── application.rb │ │ │ │ ├── redirect_uri_validator.rb │ │ │ │ └── stale_records_cleaner.rb │ │ │ └── active_record.rb │ │ ├── rails/ │ │ │ ├── helpers.rb │ │ │ ├── routes/ │ │ │ │ ├── abstract_router.rb │ │ │ │ ├── mapper.rb │ │ │ │ ├── mapping.rb │ │ │ │ └── registry.rb │ │ │ └── routes.rb │ │ ├── rake/ │ │ │ ├── db.rake │ │ │ └── setup.rake │ │ ├── rake.rb │ │ ├── request/ │ │ │ ├── authorization_code.rb │ │ │ ├── client_credentials.rb │ │ │ ├── code.rb │ │ │ ├── password.rb │ │ │ ├── refresh_token.rb │ │ │ ├── strategy.rb │ │ │ └── token.rb │ │ ├── request.rb │ │ ├── revocable_tokens/ │ │ │ ├── revocable_access_token.rb │ │ │ └── revocable_refresh_token.rb │ │ ├── secret_storing/ │ │ │ ├── base.rb │ │ │ ├── bcrypt.rb │ │ │ ├── plain.rb │ │ │ └── sha256_hash.rb │ │ ├── server.rb │ │ ├── stale_records_cleaner.rb │ │ ├── validations.rb │ │ └── version.rb │ ├── doorkeeper.rb │ └── generators/ │ └── doorkeeper/ │ ├── application_owner_generator.rb │ ├── confidential_applications_generator.rb │ ├── enable_polymorphic_resource_owner_generator.rb │ ├── install_generator.rb │ ├── migration_generator.rb │ ├── pkce_generator.rb │ ├── previous_refresh_token_generator.rb │ ├── remove_applications_secret_not_null_constraint_generator.rb │ ├── templates/ │ │ ├── README │ │ ├── add_confidential_to_applications.rb.erb │ │ ├── add_owner_to_application_migration.rb.erb │ │ ├── add_previous_refresh_token_to_access_tokens.rb.erb │ │ ├── enable_pkce_migration.rb.erb │ │ ├── enable_polymorphic_resource_owner_migration.rb.erb │ │ ├── initializer.rb │ │ ├── migration.rb.erb │ │ └── remove_applications_secret_not_null_constraint.rb.erb │ └── views_generator.rb └── spec/ ├── controllers/ │ ├── application_controller_spec.rb │ ├── application_metal_controller_spec.rb │ ├── applications_controller_spec.rb │ ├── authorizations_controller_spec.rb │ ├── protected_resources_controller_spec.rb │ ├── token_info_controller_spec.rb │ └── tokens_controller_spec.rb ├── doorkeeper/ │ ├── redirect_uri_validator_spec.rb │ ├── server_spec.rb │ ├── stale_records_cleaner_spec.rb │ └── version_spec.rb ├── dummy/ │ ├── Rakefile │ ├── app/ │ │ ├── assets/ │ │ │ └── config/ │ │ │ └── manifest.js │ │ ├── controllers/ │ │ │ ├── application_controller.rb │ │ │ ├── custom_authorizations_controller.rb │ │ │ ├── full_protected_resources_controller.rb │ │ │ ├── home_controller.rb │ │ │ ├── metal_controller.rb │ │ │ └── semi_protected_resources_controller.rb │ │ ├── helpers/ │ │ │ └── application_helper.rb │ │ ├── models/ │ │ │ └── user.rb │ │ └── views/ │ │ ├── home/ │ │ │ └── index.html.erb │ │ └── layouts/ │ │ └── application.html.erb │ ├── config/ │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments/ │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers/ │ │ │ ├── backtrace_silencers.rb │ │ │ ├── doorkeeper.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales/ │ │ │ └── doorkeeper.en.yml │ │ └── routes.rb │ ├── config.ru │ ├── db/ │ │ ├── migrate/ │ │ │ ├── 20111122132257_create_users.rb │ │ │ ├── 20120312140401_add_password_to_users.rb │ │ │ ├── 20151223192035_create_doorkeeper_tables.rb │ │ │ ├── 20151223200000_add_owner_to_application.rb │ │ │ ├── 20160320211015_add_previous_refresh_token_to_access_tokens.rb │ │ │ ├── 20170822064514_enable_pkce.rb │ │ │ ├── 20180210183654_add_confidential_to_applications.rb │ │ │ └── 20230205064514_add_custom_attributes.rb │ │ └── schema.rb │ ├── public/ │ │ ├── 404.html │ │ ├── 422.html │ │ └── 500.html │ └── script/ │ └── rails ├── factories.rb ├── generators/ │ ├── application_owner_generator_spec.rb │ ├── confidential_applications_generator_spec.rb │ ├── enable_polymorphic_resource_owner_generator_spec.rb │ ├── install_generator_spec.rb │ ├── migration_generator_spec.rb │ ├── pkce_generator_spec.rb │ ├── previous_refresh_token_generator_spec.rb │ ├── remove_applications_secret_not_null_constraint_generator_spec.rb │ ├── templates/ │ │ └── routes.rb │ └── views_generator_spec.rb ├── grape/ │ └── grape_integration_spec.rb ├── helpers/ │ └── doorkeeper/ │ └── dashboard_helper_spec.rb ├── lib/ │ ├── config_spec.rb │ ├── doorkeeper/ │ │ └── orm/ │ │ └── active_record_spec.rb │ ├── doorkeeper_spec.rb │ ├── grant_flow/ │ │ └── flow_spec.rb │ ├── grant_flow_spec.rb │ ├── models/ │ │ ├── concerns/ │ │ │ └── write_to_primary_spec.rb │ │ ├── expirable_spec.rb │ │ ├── reusable_spec.rb │ │ ├── revocable_spec.rb │ │ ├── scopes_spec.rb │ │ └── secret_storable_spec.rb │ ├── oauth/ │ │ ├── authorization/ │ │ │ ├── code_spec.rb │ │ │ └── uri_builder_spec.rb │ │ ├── authorization_code_request_spec.rb │ │ ├── base_request_spec.rb │ │ ├── base_response_spec.rb │ │ ├── client/ │ │ │ └── credentials_spec.rb │ │ ├── client_credentials/ │ │ │ ├── creator_spec.rb │ │ │ ├── issuer_spec.rb │ │ │ └── validation_spec.rb │ │ ├── client_credentials_integration_spec.rb │ │ ├── client_credentials_request_spec.rb │ │ ├── client_spec.rb │ │ ├── code_request_spec.rb │ │ ├── code_response_spec.rb │ │ ├── error_response_spec.rb │ │ ├── error_spec.rb │ │ ├── forbidden_token_response_spec.rb │ │ ├── helpers/ │ │ │ ├── scope_checker_spec.rb │ │ │ ├── unique_token_spec.rb │ │ │ └── uri_checker_spec.rb │ │ ├── invalid_request_response_spec.rb │ │ ├── invalid_token_response_spec.rb │ │ ├── password_access_token_request_spec.rb │ │ ├── pre_authorization_spec.rb │ │ ├── refresh_token_request_spec.rb │ │ ├── scopes_spec.rb │ │ ├── token_request_spec.rb │ │ ├── token_response_spec.rb │ │ └── token_spec.rb │ ├── option_spec.rb │ ├── request/ │ │ └── strategy_spec.rb │ └── secret_storing/ │ ├── base_spec.rb │ ├── bcrypt_spec.rb │ ├── plain_spec.rb │ └── sha256_hash_spec.rb ├── models/ │ └── doorkeeper/ │ ├── access_grant_spec.rb │ ├── access_token_spec.rb │ └── application_spec.rb ├── requests/ │ ├── applications/ │ │ ├── applications_request_spec.rb │ │ └── authorized_applications_spec.rb │ ├── endpoints/ │ │ ├── authorization_spec.rb │ │ └── token_spec.rb │ ├── flows/ │ │ ├── authorization_code_errors_spec.rb │ │ ├── authorization_code_spec.rb │ │ ├── client_credentials_spec.rb │ │ ├── implicit_grant_errors_spec.rb │ │ ├── implicit_grant_spec.rb │ │ ├── password_spec.rb │ │ ├── refresh_token_spec.rb │ │ ├── revoke_token_spec.rb │ │ └── skip_authorization_spec.rb │ └── protected_resources/ │ ├── metal_spec.rb │ └── private_api_spec.rb ├── routing/ │ ├── custom_controller_routes_spec.rb │ ├── default_routes_spec.rb │ └── scoped_routes_spec.rb ├── spec_helper.rb ├── spec_helper_integration.rb └── support/ ├── dependencies/ │ └── factory_bot.rb ├── doorkeeper_rspec.rb ├── helpers/ │ ├── access_token_request_helper.rb │ ├── authorization_request_helper.rb │ ├── config_helper.rb │ ├── model_helper.rb │ ├── request_spec_helper.rb │ └── url_helper.rb ├── orm/ │ └── active_record.rb ├── render_with_matcher.rb └── shared/ ├── controllers_shared_context.rb ├── hashing_shared_context.rb └── models_shared_examples.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codeclimate.yml ================================================ exclude_patterns: - "lib/doorkeeper/config.rb" - "spec/" ================================================ FILE: .coveralls.yml ================================================ service_name: travis-ci ================================================ FILE: .dockerignore ================================================ Gemfile.lock ================================================ FILE: .editorconfig ================================================ root = true [*.{rb,json}] indent_style = space indent_size = 2 insert_final_newline = true ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms open_collective: doorkeeper-gem ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### Steps to reproduce What we need to do to see your problem or bug? The more detailed the issue, the more likely that we will fix it ASAP. Don't use GitHub issues for questions like "How can I do that?" — use [StackOverflow](https://stackoverflow.com/questions/tagged/doorkeeper) instead with the corresponding tag. ### Expected behavior Tell us what should happen ### Actual behavior Tell us what happens instead ### System configuration You can help us to understand your problem if you will share some very useful information about your project environment (don't forget to remove any confidential data if it exists). **Doorkeeper initializer**: ```ruby # config/initializers/doorkeeper.rb Doorkeeper.configure do # ... end ``` **Ruby version**: `` **Gemfile.lock**:
Gemfile.lock content ``` Place your Gemfile.lock content here ```
================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Summary Provide a general description of the code changes in your pull request... were there any bugs you had fixed? If so, mention them. If these bugs have open GitHub issues, be sure to tag them here as well, to keep the conversation linked together. ### Other Information If there's anything else that's important and relevant to your pull request, mention that information here. This could include benchmarks, or other information. If you are updating CHANGELOG.md file or are asked to update it by reviewers, please add the changelog entry at the top of the file. Thanks for contributing to Doorkeeper project! ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: bundler directory: "/" schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: github-actions directory: "/" schedule: interval: daily open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/changelog.yml ================================================ name: "Changelog verifier" on: pull_request: # The specific activity types are listed here to include "labeled" and "unlabeled" # (which are not included by default for the "pull_request" trigger). # This is needed to allow skipping enforcement of the changelog in PRs with specific labels, # as defined in the (optional) "skipLabels" property. types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] jobs: # Enforces the update of a changelog file on every pull request changelog: runs-on: ubuntu-latest steps: - uses: dangoslen/changelog-enforcer@v3 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: [push, pull_request] permissions: contents: read jobs: build: name: >- Ruby ${{ matrix.ruby }} (${{ matrix.gemfile }}) env: CI: true BUNDLE_GEMFILE: ${{ matrix.gemfile }} runs-on: ${{ matrix.os }} if: | !( contains(github.event.pull_request.title, '[ci skip]') || contains(github.event.pull_request.title, '[skip ci]')) strategy: fail-fast: true matrix: os: [ubuntu-latest] ruby: - "3.1" - "3.2" - "3.3" - "3.4" gemfile: - gemfiles/rails_7_0.gemfile - gemfiles/rails_7_1.gemfile - gemfiles/rails_7_2.gemfile - gemfiles/rails_8_0.gemfile exclude: - ruby: 3.1 gemfile: gemfiles/rails_8_0.gemfile steps: - name: Repo checkout uses: actions/checkout@v6.0.2 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true rubygems: latest - name: Run tests timeout-minutes: 10 run: bundle exec rake spec rails_edge: runs-on: ubuntu-latest env: BUNDLE_GEMFILE: gemfiles/rails_edge.gemfile steps: - uses: actions/checkout@v6.0.2 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" bundler-cache: true - run: bundle exec rake spec || echo "Rails edge test is done." ruby_edge: strategy: matrix: gemfile: - gemfiles/rails_7_0.gemfile - gemfiles/rails_7_1.gemfile - gemfiles/rails_7_2.gemfile - gemfiles/rails_8_0.gemfile runs-on: ubuntu-latest env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} steps: - uses: actions/checkout@v6.0.2 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: "ruby-head" bundler-cache: true - run: bundle exec rake spec || echo "Ruby edge test is done." ================================================ FILE: .github/workflows/rubocop.yml ================================================ name: rubocop on: pull_request: permissions: contents: read pull-requests: write jobs: rubocop: name: runner / rubocop runs-on: ubuntu-latest env: BUNDLE_ONLY: rubocop steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ruby/setup-ruby@1a615958ad9d422dd932dc1d5823942ee002799f # v1.227.0 with: ruby-version: '3.1' bundler-cache: true - uses: reviewdog/action-rubocop@b6d5e953a5fc0bf3ab65254e77730ea2174d6d6d # v2.22.0 with: reporter: github-pr-review # Default is github-pr-check skip_install: true use_bundler: true ================================================ FILE: .gitignore ================================================ .bundle/ vendor/bundle/ .rbx *.rbc log/*.log pkg/ spec/dummy/db/*.sqlite3 spec/dummy/log/*.log spec/dummy/tmp/ spec/generators/tmp Gemfile.lock gemfiles/*.lock .rvmrc *.swp .idea /.yardoc/ /_yardoc/ /doc/ /rdoc/ coverage *.gem gemfiles/vendor vendor/bundle/ ================================================ FILE: .hound.yml ================================================ rubocop: config_file: .rubocop.yml version: 1.5.2 ================================================ FILE: .rspec ================================================ --colour ================================================ FILE: .rubocop.yml ================================================ inherit_from: .rubocop_todo.yml plugins: - rubocop-capybara - rubocop-factory_bot - rubocop-performance - rubocop-rails - rubocop-rspec - rubocop-rspec_rails AllCops: TargetRubyVersion: 3.1 Exclude: - "spec/generators/tmp/**/*" - "spec/dummy/db/*" - "spec/dummy/config/*" - "Dangerfile" - "gemfiles/*.gemfile" Layout/MultilineMethodCallIndentation: EnforcedStyle: indented Layout/TrailingEmptyLines: Enabled: true Layout/DotPosition: EnforcedStyle: leading Layout/LineLength: Exclude: - spec/**/* Metrics/BlockLength: Exclude: - spec/**/* - lib/doorkeeper/rake/* - doorkeeper.gemspec Metrics/MethodLength: Exclude: - spec/dummy/db/**/* Style/CaseEquality: Exclude: - lib/doorkeeper/grant_flow/flow.rb Style/StringLiterals: EnforcedStyle: double_quotes Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes Style/FrozenStringLiteralComment: Enabled: true Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: consistent_comma Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: consistent_comma Style/TrailingCommaInArguments: EnforcedStyleForMultiline: consistent_comma Style/SymbolArray: MinSize: 3 Style/WordArray: MinSize: 3 Style/ClassAndModuleChildren: Enabled: false Style/NumericPredicate: Enabled: false Style/DoubleNegation: Enabled: false Style/HashEachMethods: Enabled: true Style/HashTransformKeys: Enabled: true Style/HashTransformValues: Enabled: true Rails/DynamicFindBy: Whitelist: - find_by_sql - find_by_plaintext_token - find_by_fallback_token Rails/HttpPositionalArguments: Exclude: - spec/grape/* Rails/HttpStatus: Enabled: false Rails/RakeEnvironment: Exclude: - Rakefile Rails/ReflectionClassName: Exclude: - "lib/doorkeeper/orm/active_record/mixins/access_grant.rb" - "lib/doorkeeper/orm/active_record/mixins/access_token.rb" - "lib/doorkeeper/orm/active_record/mixins/application.rb" Rails/SkipsModelValidations: Enabled: false RSpec/BeforeAfterAll: Exclude: - "spec/routing/scoped_routes_spec.rb" - "spec/routing/custom_controller_routes_spec.rb" RSpec/ContextWording: Exclude: - "spec/support/shared/controllers_shared_context.rb" RSpec/DescribeClass: Enabled: false RSpec/ExampleLength: Enabled: false RSpec/SpecFilePathFormat: Enabled: false RSpec/MultipleExpectations: Enabled: false RSpec/NestedGroups: Enabled: false RSpec/NoExpectationExample: Enabled: true Exclude: - "spec/requests/**/*" ================================================ FILE: .rubocop_todo.yml ================================================ # This configuration was generated by # `rubocop --auto-gen-config` # on 2020-06-04 00:15:49 +0300 using RuboCop version 0.84.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 13 # Configuration parameters: IgnoredMethods. Metrics/AbcSize: Max: 33 # Offense count: 2 # Configuration parameters: CountComments, ExcludedMethods. # ExcludedMethods: refine Metrics/BlockLength: Max: 85 # Offense count: 2 # Configuration parameters: CountComments. Metrics/ClassLength: Max: 242 # Offense count: 2 # Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: Max: 10 # Offense count: 24 # Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: Max: 29 # Offense count: 1 # Configuration parameters: CountComments. Metrics/ModuleLength: Max: 209 # Offense count: 2 # Configuration parameters: IgnoredMethods. Metrics/PerceivedComplexity: Max: 11 # Offense count: 1 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - 'lib/doorkeeper/config.rb' # Offense count: 10 RSpec/AnyInstance: Exclude: - 'spec/generators/previous_refresh_token_generator_spec.rb' - 'spec/lib/oauth/authorization_code_request_spec.rb' - 'spec/lib/oauth/client_credentials/creator_spec.rb' - 'spec/lib/oauth/password_access_token_request_spec.rb' - 'spec/lib/oauth/token_request_spec.rb' - 'spec/requests/flows/authorization_code_spec.rb' - 'spec/requests/flows/refresh_token_spec.rb' # Offense count: 2 RSpec/ExpectInHook: Exclude: - 'spec/controllers/protected_resources_controller_spec.rb' # Offense count: 300 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Enabled: false # Offense count: 22 RSpec/LeakyConstantDeclaration: Exclude: - 'spec/controllers/authorizations_controller_spec.rb' - 'spec/controllers/protected_resources_controller_spec.rb' - 'spec/lib/config_spec.rb' - 'spec/lib/option_spec.rb' - 'spec/models/doorkeeper/access_token_spec.rb' # Offense count: 7 RSpec/MessageChain: Exclude: - 'spec/controllers/authorizations_controller_spec.rb' - 'spec/controllers/tokens_controller_spec.rb' # Offense count: 98 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: EnforcedStyle: receive # Offense count: 39 RSpec/SubjectStub: Exclude: - 'spec/lib/models/expirable_spec.rb' - 'spec/lib/models/reusable_spec.rb' - 'spec/lib/models/revocable_spec.rb' - 'spec/lib/oauth/base_request_spec.rb' - 'spec/lib/oauth/client_credentials_request_spec.rb' - 'spec/support/shared/models_shared_examples.rb' # Offense count: 73 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Enabled: false # Offense count: 4 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: - 'lib/doorkeeper/config.rb' - 'lib/doorkeeper/helpers/controller.rb' - 'lib/doorkeeper/oauth/client/credentials.rb' - 'lib/doorkeeper/oauth/token.rb' ================================================ FILE: AGENTS.md ================================================ # Doorkeeper Codebase Guide for AI Coding Agents This is the code base of the OAuth 2 provider for Ruby web applications. ## Architecture Overview Doorkeeper is a Ruby gem which is a Rails engine. It provides a set of models, controllers, and views that can be mounted into a Rails application to handle OAuth 2 authorization flows. **Key principle**: all the changes should conform OAuth 2 published specifications such as RFC 6749, RFC 6819 and etc. ## Testing Commands From within the root directory (preferred method): ```bash bundle exec rake spec ``` ## Code Conventions ### Changelog Updates When fixing bugs or adding features: - Add an entry to the top of `CHANGELOG.md` - Format: `- [PR number] Brief description` - See existing entries for style ### Code Style - Run RuboCop: `bundle exec rubocop` (there's a project-wide `.rubocop.yml`) ## Documentation - API docs use YARD/RDoc format ================================================ FILE: CHANGELOG.md ================================================ # Changelog See https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions for upgrade guides. User-visible changes worth mentioning. ## main - [#1781] Honor `handle_auth_errors :raise` in `AuthorizationsController#authorize_response` - [#1795] Fix: detailed error 'insufficient_scope' in protected resources 403s - [#1797] Fix `doorkeeper:db:cleanup` rake task failure on PostgreSQL - [#1800] Set `@grant_type` in `ClientCredentialsRequest` and `RefreshTokenRequest` constructors so `request.grant_type` returns the correct value in hooks like `before_successful_strategy_response`. - [#1802] Fix `filter_parameters` not applied when `Doorkeeper.configure` is called inside to_prepare. - [#1804] Use `ActiveSupport.on_load(:active_record)` in ORM hooks to prevent loading ActiveRecord models too early - [#1806] Fix token revocation bypass for public clients (RFC 7009) - [#1815] Expose `current_resource_owner` as a view helper in `Doorkeeper::ApplicationController`. - [#1818] Fix token introspection returning `exp: 0` for non-expiring tokens. - [#1784] Remove hardcoded colons from view templates, move punctuation to i18n translation strings. **[IMPORTANT]**: if you have customized Doorkeeper views (`authorizations/new`, `authorizations/show`, `applications/show`) or overridden the default `en.yml` translations, you may need to update them. Colons are no longer hardcoded in the views — they are now part of the translation strings. Update the [doorkeeper-i18n](https://github.com/doorkeeper-gem/doorkeeper-i18n) gem to get the updated translations for all locales. ## 5.9.0 - [#1791] Add support for Rails read replicas with automatic role switching via `enable_multiple_database_roles` configuration option - [#1792] Consider expires_in when clear expired tokens with StaleRecordsCleaner. - [#1790] Fix race condition in refresh token revocation check by moving InvalidGrantReuse check inside the lock block - [#1788] Fix regex for basic auth to be case-insensitive - [#1775] Fix Applications Secret Not Null Constraint generator - [#1779] Only lock previous access token model when creating a new token from its refresh token if revoke_previous_refresh_token_on_use is false - [#1778] Ensure that token revocation is idempotent by checking that that token has not already been revoked before revoking. ## 5.8.2 - [#1755] Fix the error message for force_pkce - [#1761] Memoize authentication failure - [#1762] Allow missing client to trigger invalid client error when force_pkce is enabled - [#1767] Make sure error handling happens on a controller level opposed to action level to account for the controller being extended ## 5.8.1 - [#1752] Bump the range of supported Ruby and Rails versions - [#1747] Fix unknown pkce method error when configured - [#1744] Allow for expired refresh tokens to be revoked - [#1754] Fix refresh tokens with dynamic scopes ## 5.8.0 - [#1739] Add support for dynamic scopes - [#1715] Fix token introspection invalid request reason - [#1714] Fix `Doorkeeper::AccessToken.find_or_create_for` with empty scopes which raises NoMethodError - [#1712] Add `Pragma: no-cache` to token response - [#1726] Refactor token introspection class. - [#1727] Allow to set null secret value for Applications if they are public. - [#1735] Add `pkce_code_challenge_methods` config option. ## 5.7.1 - [#1705] Add `force_pkce` option that requires non-confidential clients to use PKCE when requesting an access_token using an authorization code ## 5.7.0 - [#1696] Add missing `#issued_token` method to `OAuth::TokenResponse` - [#1697] Allow a TokenResponse body to be customized (memoize response body). - [#1702] Fix bugs for error response in the form_post and error view - [#1660] Custom access token attributes are now considered when finding matching tokens (fixes #1665). Introduce `revoke_previous_client_credentials_token` configuration option. ## 5.6.9 - [#1691] Make new Doorkeeper errors backward compatible with older extensions. ## 5.6.8 - [#1680] Fix handle_auth_errors :raise NotImplementedError ## 5.6.7 - [#1662] Specify uri_redirect validation class explicitly. - [#1652] Add custom attributes support to token generator. - [#1667] Pass `client` instead of `grant.application` to `find_or_create_access_token`. - [#1673] Honor `custom_access_token_attributes` in client credentials grant flow. - [#1676] Improve AuthorizationsController error response handling - [#1677] Fix URIHelper.valid_for_authorization? breaking for non url URIs. ## 5.6.6 - [#1644] Update HTTP headers. - [#1646] Block public clients automatic authorization skip. - [#1648] Add custom token attributes to Refresh Token Request. - [#1649] Fixed custom_access_token_attributes related errors. ## 5.6.5 - [#1602] Allow custom data to be stored inside access grants/tokens. - [#1634] Code refactoring for custom token attributes. - [#1639] Add grant type validation to avoid Internal Server Error for DELETE /oauth/authorize endpoint. ## 5.6.4 - [#1633] Apply ORM configuration in #to_prepare block to avoid autoloading errors. ## 5.6.3 - [#1622] Drop support for Rubies 2.5 and 2.6 - [#1605] Fix URI validation for Ruby 3.2+. - [#1625] Exclude endless access tokens from `StaleRecordsCleaner`. - [#1626] Remove deprecated `active_record_options` config option. - [#1631] Fix regression with redirect behavior after token lookup optimizations (redirect to app URI when found). - [#1630] Special case unique index creation for refresh_token on SQL Server. - [#1627] Lazy evaluate Doorkeeper config when loading files and executing initializers. ## 5.6.2 - [#1604] Fix fetching of the application when custom application_class defined. ## 5.6.1 - [#1593] Add support for Trilogy ActiveRecord adapter. - [#1597] Add optional support to use the url path for the native authorization code flow. Ports forward [#1143] from 4.4.3 - [#1599] Remove unnecessarily re-fetch of application object when creating an access token. ## 5.6.0 - [#1581] Consider `token_type_hint` when searching for access token in TokensController to avoid extra database calls. ## 5.6.0.rc2 - [#1558] Fixed bug: able to obtain a token with default scopes even if they are not present in the application scopes when using client credentials. - [#1567] Only filter `code` parameter if authorization_code grant flow is enabled. ## 5.6.0.rc1 - [#1551] Change lazy loading for ORM to be Ruby standard autoload. - [#1552] Remove duplicate IDs on Auth form to improve accessibility. - [#1542] Improve performance of `Doorkeeper::AccessToken#matching_token_for` using database specific SQL time math. **[IMPORTANT]**: API of the `Doorkeeper::AccessToken#matching_token_for` method has changed and now it returns only **active** access tokens (previously they were just not revoked). Please remember that the idea of the `reuse_access_token` option is to check for existing _active_ token (see configuration option description). ## 5.5.4 - [#1535] Revert changes introduced in #1528 to allow query params in `redirect_uri` as per the spec. ## 5.5.3 - [#1528] Don't allow extra query params in redirect_uri. - [#1525] I18n source for forbidden token error is now `doorkeeper.errors.messages.forbidden_token.missing_scope`. - [#1531] Disable `strict-loading` for Doorkeeper models by default. - [#1532] Add support for Rails 7. ## 5.5.2 - [#1502] Drop support for Ruby 2.4 because of EOL. - [#1504] Updated the url fragment in the comment for code documentation. - [#1512] Fix form behavior when response mode is form_post. - [#1511] Fix that authorization code is returned by fragment if response_mode is fragment. ## 5.5.1 - [#1496] Revoke `old_refresh_token` if `previous_refresh_token` is present. - [#1495] Fix `respond_to` undefined in API-only mode - [#1488] Verify client authentication for Resource Owner Password Grant when `config.skip_client_authentication_for_password_grant` is set and the client credentials are sent in a HTTP Basic auth header. ## 5.5.0 - [#1482] Simplify `TokenInfoController` to be overridable (extract response rendering). - [#1478] Fix ownership association and Rake tasks when custom models configured. - [#1477] Respect `ActiveRecord::Base.pluralize_table_names` for Doorkeeper table names. ## 5.5.0.rc2 - [#1473] Enable `Applications` and `AuthorizedApplications` controllers in API mode. **[IMPORTANT]** you can still skip these controllers using `skip_controllers` in `use_doorkeeper` inside `routes.rb`. Please do it in case you don't need them. - [#1472] Fix `establish_connection` configuration for custom defined models. - [#1471] Add support for Ruby 3.0. - [#1469] Check if `redirect_uri` exists. - [#1465] Memoize nil doorkeeper_token. - [#1459] Use built-in Ruby option to remove padding in PKCE code challenge value. - [#1457] Make owner_id a bigint for newly-generated owner migrations - [#1452] Empty previous_refresh_token only if present. - [#1440] Validate empty host in redirect_uri. - [#1438] Add form post response mode. - [#1458] Make `config.skip_client_authentication_for_password_grant` a long term configuration option. ## 5.5.0.rc1 - [#1435] Make error response not redirectable when client is unauthorized - [#1426] Ensure ActiveRecord callbacks are executed on token revocation. - [#1407] Remove redundant and complex to support helpers froms tests (`should_have_json`, etc). - [#1416] Don't add introspection route if token introspection completely disabled. - [#1410] Properly memoize `current_resource_owner` value (consider `nil` and `false` values). - [#1415] Ignore PKCE params for non-PKCE grants. - [#1418] Add ability to register custom OAuth Grant Flows. - [#1420] Require client authentication for Resource Owner Password Grant as stated in OAuth RFC. **[IMPORTANT]** you need to create a new OAuth client (`Doorkeeper::Application`) if you didn't have it before and use client credentials in HTTP Basic auth if you previously used this grant flow without client authentication. To opt out of this you could set the `skip_client_authentication_for_password_grant` configuration option to `true`, but note that this is in violation of the OAuth spec and represents a security risk. All the users of your provider application now need to include client credentials when they use this grant flow. - [#1421] Add Resource Owner instance to authorization hook context for `custom_access_token_expires_in` configuration option to allow resource owner based Access Tokens TTL. ## 5.4.0 - [#1404] Make `Doorkeeper::Application#read_attribute_for_serialization` public. ## 5.4.0.rc2 - [#1371] Add `#as_json` method and attributes serialization restriction for Application model. Fixes information disclosure vulnerability (CVE-2020-10187). **[IMPORTANT]** you need to re-implement `#as_json` method for Doorkeeper Application model if you previously used `#to_json` serialization with custom options or attributes or rely on JSON response from /oauth/applications.json or /oauth/authorized_applications.json. This change is a breaking change which restricts serialized attributes to a very small set of columns. - [#1395] Fix `NameError: uninitialized constant Doorkeeper::AccessToken` for Rake tasks. - [#1397] Add `as: :doorkeeper_application` on Doorkeeper application form in order to support custom configured application model. - [#1400] Correctly yield the application instance to `allow_grant_flow_for_client?` config option (fixes #1398). - [#1402] Handle trying authorization with client credentials. ## 5.4.0.rc1 - [#1366] Sets expiry of token generated using `refresh_token` to that of original token. (Fixes #1364) - [#1354] Add `authorize_resource_owner_for_client` option to authorize the calling user to access an application. - [#1355] Allow to enable polymorphic Resource Owner association for Access Token & Grant models (`use_polymorphic_resource_owner` configuration option). **[IMPORTANT]** Review your custom patches or extensions for Doorkeeper internals if you have such - since now Doorkeeper passes Resource Owner instance to every objects and not just it's ID. See PR description for details. - [#1356] Remove duplicated scopes from Access Tokens and Grants on attribute assignment. - [#1357] Fix `Doorkeeper::OAuth::PreAuthorization#as_json` method causing `Stack level too deep` error with AMS (fix #1312). - [#1358] Deprecate `active_record_options` configuration option. - [#1359] Refactor Doorkeeper configuration options DSL to make it easy to reuse it in external extensions. - [#1360] Increase `matching_token_for` lookup size to 10 000 and make it configurable. - [#1371] Fix controllers to use valid classes in case Doorkeeper has custom models configured. - [#1370] Fix revocation response for invalid token and unauthorized requests to conform with RFC 7009 (fixes #1362). **[IMPORTANT]** now fully according to RFC 7009 nobody can do a revocation request without `client_id` (for public clients) and `client_secret` (for private clients). Please update your apps to include that info in the revocation request payload. - [#1373] Make Doorkeeper routes mapper reusable in extensions. - [#1374] Revoke and issue client credentials token in a transaction with a row lock. - [#1384] Add context object with auth/pre_auth and issued_token for authorization hooks. - [#1387] Add `AccessToken#create_for` and use in `RefreshTokenRequest`. - [#1392] Fix `enable_polymorphic_resource_owner` migration template to have proper index name. - [#1393] Improve Applications #show page with more informative data on client secret and scopes. - [#1394] Use Ruby `autoload` feature to load Doorkeeper files. ## 5.3.3 - [#1404] Backport: Make `Doorkeeper::Application#read_attribute_for_serialization` public. ## 5.3.2 - [#1371] Backport: add `#as_json` method and attributes serialization restriction for Application model. Fixes information disclosure vulnerability (CVE-2020-10187). ## 5.3.1 - [#1360] Backport: Increase `matching_token_for` batch lookup size to 10 000 and make it configurable. ## 5.3.0 - [#1339] Validate Resource Owner in `PasswordAccessTokenRequest` against `nil` and `false` values. - [#1341] Fix `refresh_token_revoked_on_use` with `hash_token_secrets` enabled. - [#1343] Fix ruby 2.7 kwargs warning in InvalidTokenResponse. - [#1345] Allow to set custom classes for Doorkeeper models, extract reusable AR mixins. - [#1346] Refactor `Doorkeeper::Application#to_json` into convenient `#as_json` (fix #1344). - [#1349] Fix `Doorkeeper::Application` AR associations using an incorrect foreign key name when using a custom class. - [#1318] Make existing token revocation for client credentials optional and disable it by default. **[IMPORTANT]** This is a change compared to the behaviour of version 5.2. If you were relying on access tokens being revoked once the same client requested a new access token, reenable it with `revoke_previous_client_credentials_token` in Doorkeeper initialization file. ## 5.2.6 - [#1404] Backport: Make `Doorkeeper::Application#read_attribute_for_serialization` public. ## 5.2.5 - [#1371] Backport: add `#as_json` method and attributes serialization restriction for Application model. Fixes information disclosure vulnerability (CVE-2020-10187). ## 5.2.4 - [#1360] Backport: Increase `matching_token_for` batch lookup size to 10 000 and make it configurable. ## 5.2.3 - [#1334] Remove `application_secret` flash helper and `redirect_to` keyword. - [#1331] Move redirect_uri_validator to where it is used (`Application` model). - [#1326] Move response_type check in pre_authorization to a method to be easily to override. - [#1329] Fix `find_in_batches` order warning. ## 5.2.2 - [#1320] Call configured `authenticate_resource_owner` method once per request. - [#1315] Allow generation of new secret with `Doorkeeper::Application#renew_secret`. - [#1309] Allow `Doorkeeper::Application#to_json` to work without arguments. ## 5.2.1 - [#1308] Fix flash types for `api_only` mode (no flashes for `ActionController::API`). - [#1306] Fix interpolation of `missing_param` I18n. ## 5.2.0 - [#1305] Make `Doorkeeper::ApplicationController` to inherit from `ActionController::API` in cases when `api_mode` enabled (fixes #1302). ## 5.2.0.rc3 - [#1298] Slice strong params so doesn't error with Rails forms. - [#1300] Limiting access to attributes of pre_authorization. - [#1296] Adding client_id to strong parameters. **[IMPORTANT]** `Doorkeeper::Server#client_via_uid` was removed. - [#1293] Move ar specific redirect uri validator to ar orm directory. - [#1288] Allow to pass attributes to the `Doorkeeper::OAuth::PreAuthorization#as_json` method to customize the PreAuthorization response. - [#1286] Add ability to customize grant flows per application (OAuth client) (#1245 , #1207) - [#1283] Allow to customize base class for `Doorkeeper::ApplicationMetalController` (new configuration option called `base_metal_controller` (fix #1273). - [#1277] Prevent requested scope be empty on authorization request, handle and add description for invalid request. ## 5.2.0.rc2 - [#1270] Find matching tokens in batches for `reuse_access_token` option (fix #1193). - [#1271] Reintroduce existing token revocation for client credentials. **[IMPORTANT]** If you rely on being able to fetch multiple access tokens from the same client using client credentials flow, you should skip to version 5.3, where this behaviour is deactivated by default. - [#1269] Update initializer template documentation. - [#1266] Use strong parameters within pre-authorization. - [#1264] Add :before_successful_authorization and :after_successful_authorization hooks in TokensController - [#1263] Response properly when introspection fails and fix configurations's user guide. ## 5.2.0.rc1 - [#1260], [#1262] Improve Token Introspection configuration option (access to tokens, client). - [#1257] Add constraint configuration when using client authentication on introspection endpoint. - [#1252] Returning `unauthorized` when the revocation of the token should not be performed due to wrong permissions. - [#1249] Specify case sensitive uniqueness to remove Rails 6 deprecation message - [#1248] Display the Application Secret in HTML after creating a new application even when `hash_application_secrets` is used. - [#1248] Return the unhashed Application Secret in the JSON response after creating new application even when `hash_application_secrets` is used. - [#1238] Better support for native app with support for custom scheme and localhost redirection. ## 5.1.2 - [#1404] Backport: Make `Doorkeeper::Application#read_attribute_for_serialization` public. ## 5.1.1 - [#1371] Backport: add `#as_json` method and attributes serialization restriction for Application model. Fixes information disclosure vulnerability (CVE-2020-10187). ## 5.1.0 - [#1243] Add nil check operator in token checking at token introspection. - [#1241] Explaining foreign key options for resource owner in a single place - [#1237] Allow to set blank redirect URI if Doorkeeper configured to use redirect URI-less grant flows. - [#1234] Fix `StaleRecordsCleaner` to properly work with big amount of records. - [#1228] Allow to explicitly set non-expiring tokens in `custom_access_token_expires_in` configuration option using `Float::INFINITY` return value. - [#1224] Do not try to store token if not found by fallback hashing strategy. - [#1223] Update Hound/Rubocop rules, correct Doorkeeper codebase to follow style-guides. - [#1220] Drop Rails 4.2 & Ruby < 2.4 support. ## 5.1.0.rc2 - [#1208] Unify hashing implementation into secret storing strategies **[IMPORTANT]** If you have been using the master branch of doorkeeper with bcrypt in your Gemfile.lock, your application secrets have been hashed using BCrypt. To restore this behavior, use the initializer option `hash_application_secrets using: 'Doorkeeper::SecretStoring::BCrypt`. - [#1216] Add nil check to `expires_at` method. - [#1215] Fix deprecates for Rails 6. - [#1214] Scopes field accepts array. - [#1209] Fix tokens validation for Token Introspection request. - [#1202] Use correct HTTP status codes for error responses. **[IMPORTANT]**: this change might break your application if you were relying on the previous 401 status codes, this is now a 400 by default, or a 401 for `invalid_client` and `invalid_token` errors. - [#1201] Fix custom TTL block `client` parameter to always be an `Doorkeeper::Application` instance. **[IMPORTANT]**: those who defined `custom_access_token_expires_in` configuration option need to check their block implementation: if you are using `oauth_client.application` to get `Doorkeeper::Application` instance, then you need to replace it with just `oauth_client`. - [#1200] Increase default Doorkeeper access token value complexity (`urlsafe_base64` instead of just `hex`) matching RFC6749/RFC6750. **[IMPORTANT]**: this change have possible side-effects in case you have custom database constraints for access token value, application secrets, refresh tokens or you patched Doorkeeper models and introduced token value validations, or you are using database with case-insensitive WHERE clause like MySQL (you can face some collisions). Before this change access token value matched `[a-f0-9]` regex, and now it matches `[a-zA-Z0-9\-_]`. In case you have such restrictions and your don't use custom token generator please change configuration option `default_generator_method` to `:hex`. - [#1195] Allow to customize Token Introspection response (fixes #1194). - [#1189] Option to set `token_reuse_limit`. - [#1191] Try to load bcrypt for hashing of application secrets, but add fallback. ## 5.1.0.rc1 - [#1188] Use `params` instead of `request.POST` in tokens controller (fixes #1183). - [#1182] Fix loopback IP redirect URIs to conform with RFC8252, p. 7.3 (fixes #1170). - [#1179] Authorization Code Grant Flow without client id returns invalid_client error. - [#1177] Allow to limit `scopes` for certain `grant_types` - [#1176] Fix test factory support for `factory_bot_rails` - [#1175] Internal refactor: use `scopes_string` inside `scopes`. - [#1168] Allow optional hashing of tokens and secrets. - [#1164] Fix error when `root_path` is not defined. - [#1162] Fix `enforce_content_type` for requests without body. ## 5.0.3 - [#1371] Backport: add `#as_json` method and attributes serialization restriction for Application model. Fixes information disclosure vulnerability (CVE-2020-10187). ## 5.0.2 - [#1158] Fix initializer template: change `handle_auth_errors` option - [#1157] Remove redundant index from migration template. ## 5.0.1 - [#1154] Refactor `StaleRecordsCleaner` to be ORM agnostic. - [#1152] Fix migration template: change resource owner data type from integer to Rails generic `references` - [#1151] Fix Refresh Token strategy: add proper validation of client credentials both for Public & Private clients. - [#1149] Fix for `URIChecker#valid_for_authorization?` false negative when query is blank, but `?` present. - [#1140] Allow rendering custom errors from exceptions (issue #844). Originally opened as [#944]. - [#1138] Revert regression bug (check for token expiration in Authorizations controller so authorization triggers every time) ## 5.0.0 - [#1127] Change the token_type initials of the Banner Token to uppercase to comply with the RFC6750 specification. ## 5.0.0.rc2 - [#1122] Fix AuthorizationsController#new error response to be in JSON format - [#1119] Fix token revocation for OAuth apps using "implicit" grant flow - [#1116] `AccessGrant`s will now be revoked along with `AccessToken`s when hitting the `AuthorizedApplicationController#destroy` route. - [#1114] Make token info endpoint's attributes consistent with token creation - [#1108] Simple formatting of callback URLs when listing oauth applications - [#1106] Restrict access to AdminController with 'Forbidden 403' if admin_authenticator is not configured by developers. ## 5.0.0.rc1 - [#1103] Allow customizing use_refresh_token - [#1089] Removed enable_pkce_without_secret configuration option - [#1102] Expiration time based on scopes - [#1099] All the configuration variables in `Doorkeeper.configuration` now always return a non-nil value (`true` or `false`) - [#1099] ORM / Query optimization: Do not revoke the refresh token if it is not enabled in `doorkeeper.rb` - [#996] Expiration Time Base On Grant Type - [#997] Allow PKCE authorization_code flow as specified in RFC7636 - [#907] Fix lookup for matching tokens in certain edge-cases - [#992] Add API option to use Doorkeeper without management views for API only Rails applications (`api_only`) - [#1045] Validate redirect_uri as the native URI when making authorization code requests - [#1048] Remove deprecated `Doorkeeper#configured?`, `Doorkeeper#database_installed?`, and `Doorkeeper#installed?` method - [#1031] Allow public clients to authenticate without `client_secret`. Define an app as either public or private/confidential **[IMPORTANT]**: all the applications (clients) now are considered as private by default. You need to manually change `confidential` column to `false` if you are using public clients, in other case your mobile (or other) applications will not be able to authorize. See [#1142](https://github.com/doorkeeper-gem/doorkeeper/issues/1142) for more details. - [#1010] Add configuration to enforce configured scopes (`default_scopes` and `optional_scopes`) for applications - [#1060] Ensure that the native redirect_uri parameter matches with redirect_uri of the client - [#1064] Add :before_successful_authorization and :after_successful_authorization hooks - [#1069] Upgrade Bootstrap to 4 for Admin - [#1068] Add rake task to cleanup databases that can become large over time - [#1072] AuthorizationsController: Memoize strategy.authorize_response result to enable subclasses to use the response object. - [#1075] Call `before_successful_authorization` and `after_successful_authorization` hooks on `create` action as well as `new` - [#1082] Fix #916: remember routes mapping and use it required places (fix error with customized Token Info route). - [#1086, #1088] Fix bug with receiving default scopes in the token even if they are not present in the application scopes (use scopes intersection). - [#1076] Add config to enforce content type to application/x-www-form-urlencoded - Fix bug with `force_ssl_in_redirect_uri` when it breaks existing applications with an SSL redirect_uri. ## 4.4.3 - [#1143] Adds a config option `opt_out_native_route_change` to opt out of the breaking api changed introduced in https://github.com/doorkeeper-gem/doorkeeper/pull/1003 ## 4.4.2 - [#1130] Backport fix for native redirect_uri from 5.x. ## 4.4.1 - [#1127] Backport token type to comply with the RFC6750 specification. - [#1125] Backport Quote surround I18n yes/no keys ## 4.4.0 - [#1120] Backport security fix from 5.x for token revocation when using public clients **[IMPORTANT]**: all the applications (clients) now are considered as private by default. You need to manually change `confidential` column to `false` if you are using public clients, in other case your mobile (or other) applications will not be able to authorize. See [#1142](https://github.com/doorkeeper-gem/doorkeeper/issues/1142) for more details. ## 4.3.2 - [#1053] Support authorizing with query params in the request `redirect_uri` if explicitly present in app's `Application#redirect_uri` ## 4.3.1 - Remove `BaseRecord` and introduce additional concern for ordering methods to fix braking changes for Doorkeeper models. - [#1032] Refactor BaseRequest callbacks into configurable lambdas - [#1040] Clear mixins from ActiveRecord DSL and save only overridable API. It allows to use this mixins in Doorkeeper ORM extensions with minimum code boilerplate. ## 4.3.0 - [#976] Fix to invalidate the second redirect URI when the first URI is the native URI - [#1035] Allow `Application#redirect_uri=` to handle array of URIs. - [#1036] Allow to forbid Application redirect URI's with specific rules. - [#1029] Deprecate `order_method` and introduce `ordered_by`. Sort applications by `created_at` in index action. - [#1033] Allow Doorkeeper configuration option #force_ssl_in_redirect_uri to be a callable object. - Fix Grape integration & add specs for it - [#913] Deferred ORM (ActiveRecord) models loading - [#943] Fix Access Token token generation when certain errors occur in custom token generators - [#1026] Implement RFC7662 - OAuth 2.0 Token Introspection - [#985] Generate valid migration files for Rails >= 5 - [#972] Replace Struct subclassing with block-form initialization - [#1003] Use URL query param to pass through native redirect auth code so automated apps can find it. **[IMPORTANT]**: Previously authorization code response route was `/oauth/authorize/`, now it is `oauth/authorize/native?code=` (in order to help applications to automatically find the code value). - [#868] `Scopes#&` and `Scopes#+` now take an array or any other enumerable object. - [#1019] Remove translation not in use: `invalid_resource_owner`. - Use Ruby 2 hash style syntax (min required Ruby version = 2.1) - [#948] Make Scopes.<=> work with any "other" value. - [#974] Redirect URI is checked without query params within AuthorizationCodeRequest. - [#1004] More explicit help text for `native_redirect_uri`. - [#1023] Update Ruby versions and test against 2.5.0 on Travis CI. - [#1024] Migrate from FactoryGirl to FactoryBot. - [#1025] Improve documentation for adding foreign keys - [#1028] Make it possible to have composite strategy names. ## 4.2.6 - [#970] Escape certain attributes in authorization forms. ## 4.2.5 - [#936] Deprecate `Doorkeeper#configured?`, `Doorkeeper#database_installed?`, and `Doorkeeper#installed?` - [#909] Add `InvalidTokenResponse#reason` reader method to allow read the kind of invalid token error. - [#928] Test against more recent Ruby versions - Small refactorings within the codebase - [#921] Switch to Appraisal, and test against Rails master - [#892] Add minimum Ruby version requirement ## 4.2.0 - Security fix: Address CVE-2016-6582, implement token revocation according to spec (tokens might not be revoked if client follows the spec). - [#873] Add hooks to Doorkeeper::ApplicationMetalController - [#871] Allow downstream users to better utilize doorkeeper spec factories by eliminating name conflict on `:user` factory. ## 4.1.0 - [#845] Allow customising the `Doorkeeper::ApplicationController` base controller ## 4.0.0 - [#834] Fix AssetNotPrecompiled error with Sprockets 4 - [#843] Revert "Fix validation error messages" - [#847] Specify Null option to timestamps ## 4.0.0.rc4 - [#777] Add support for public client in password grant flow - [#823] Make configuration and specs ORM independent - [#745] Add created_at timestamp to token generation options - [#838] Drop `Application#scopes` generator and warning, introduced for upgrading doorkeeper from v2 to v3. - [#801] Fix Rails 5 warning messages - Test against Rails 5 RC1 ## 4.0.0.rc3 - [#769] Revoke refresh token on access token use. To make use of the new config add `previous_refresh_token` column to `oauth_access_tokens`: ``` rails generate doorkeeper:previous_refresh_token ``` - [#811] Toughen parameters filter with exact match - [#813] Applications admin bugfix - [#799] Fix Ruby Warnings - Drop `attr_accessible` from models ### Backward incompatible changes - [#730] Force all timezones to use UTC to prevent comparison issues. - [#802] Remove `config.i18n.fallbacks` from engine ## 4.0.0.rc2 - Fix optional belongs_to for Rails 5 - Fix Ruby warnings ## 4.0.0.rc1 ### Backward incompatible changes - Drops support for Rails 4.1 and earlier - Drops support for Ruby 2.0 - [#778] Bug fix: use the remaining time that a token is still valid when building the redirect URI for the implicit grant flow ### Other changes - [#771] Validation error messages fixes - Adds foreign key constraints in generated migrations between tokens and grants, and applications - Support Rails 5 ## 3.1.0 - [#736] Existing valid tokens are now reused in client_credentials flow - [#749] Allow user to raise authorization error with custom messages. Under `resource_owner_authenticator` block a user can `raise Doorkeeper::Errors::DoorkeeperError.new('custom_message')` - [#762] Check doesn’t abort the actual migration, so it runs - [#722] `doorkeeper_forbidden_render_options` now supports returning a 404 by specifying `respond_not_found_when_forbidden: true` in the `doorkeeper_forbidden_render_options` method. - [#734] Simplify and remove duplication in request strategy classes ## 3.0.1 - [#712] Wrap exchange of grant token for access token and access token refresh in transactions - [#704] Allow applications scopes to be mass assigned - [#707] Fixed order of Mixin inclusion and table_name configuration in models - [#712] Wrap access token and refresh grants in transactions - Adds JRuby support - Specs, views and documentation adjustments ## 3.0.0 ### Other changes - [#693] Updates `en.yml`. ## 3.0.0 (rc2) ### Backward incompatible changes - [#678] Change application-specific scopes to take precedence over server-wide scopes. This removes the previous behavior where the intersection between application and server scopes was used. ### Other changes - [#671] Fixes `NoMethodError - undefined method 'getlocal'` when calling the /oauth/token path. Switch from using a DateTime object to update AR to using a Time object. (Issue #668) - [#677] Support editing application-specific scopes via the standard forms - [#682] Pass error hash to Grape `error!` - [#683] Generate application secret/UID if fields are blank strings ## 3.0.0 (rc1) ### Backward incompatible changes - [#648] Extracts mongodb ORMs to https://github.com/doorkeeper-gem/doorkeeper-mongodb. If you use ActiveRecord you don’t need to do any change, otherwise you will need to install the new plugin. - [#665] `doorkeeper_unauthorized_render_options(error:)` and `doorkeeper_forbidden_render_options(error:)` now accept `error` keyword argument. ### Removed deprecations - Removes `doorkeeper_for` deprecation notice. - Remove `applications.scopes` upgrade notice. ## 2.2.2 - [#541] Fixed `undefined method attr_accessible` problem on Rails 4 (happens only when ProtectedAttributes gem is used) in #599 ## 2.2.1 - [#636] `custom_access_token_expires_in` bugfixes - [#641] syntax error fix (Issue #612) - [#633] Send extra details to Custom Token Generator - [#628] Refactor: improve orm adapters to ease extension - [#637] Upgrade to rspec to 3.2 ## 2.2.0 - 2015-04-19 - [#611] Allow custom access token generators to be used - [#632] Properly fallback to `default_scopes` when no scope is specified - [#622] Clarify that there is a logical OR between scopes for authorizing - [#635] Upgrade to rspec 3 - [#627] i18n fallbacks to english - Moved CHANGELOG to NEWS.md ## 2.1.4 - 2015-03-27 - [#595] HTTP spec: Add `scope` for refresh token scope param - [#596] Limit scopes in app scopes for client credentials - [#567] Add Grape helpers for easier integration with Grape framework - [#606] Add custom access token expiration support for Client Credentials flow ## 2.1.3 - 2015-03-01 - [#588] Fixes scopes_match? bug that skipped authorization form in some cases ## 2.1.2 - 2015-02-25 - [#574] Remove unused update authorization route. - [#576] Filter out sensitive parameters from logs. - [#582] The Authorization HTTP header fields are now case insensitive. - [#583] Database connection bugfix in certain scenarios. - Testing improvements ## 2.1.1 - 2015-02-06 - Remove `wildcard_redirect_url` option - [#481] Customize token flow OAuth expirations with a config lambda - [#568] TokensController: Memoize strategy.authorize_response result to enable subclasses to use the response object. - [#571] Fix database initialization issues in some configurations. - Documentation improvements ## 2.1.0 - 2015-01-13 - [#540] Include `created_at` in response. - [#538] Check application-level scopes in client_credentials and password flow. - [5596227] Check application scopes in AccessToken when present. Fixes a bug in doorkeeper 2.0.0 and 2.0.1 referring to application specific scopes. - [#534] Internationalizes doorkeeper views. - [#545] Ensure there is a connection to the database before checking for missing columns - [#546] Use `Doorkeeper::` prefix when referencing `Application` to avoid possible application model name conflict. - [#538] Test with Rails ~> 4.2. ### Potentially backward incompatible changes - Enable by default `authorization_code` and `client_credentials` grant flows. Disables implicit and password grant flows by default. - [#510, #544, 722113f] Revoked refresh token response bugfix. ## 2.0.1 - 2014-12-17 - [#525, #526, #527] Fix `ActiveRecord::NoDatabaseError` on gem load. ## 2.0.0 - 2014-12-16 ### Backward incompatible changes - [#448] Removes `doorkeeper_for` helper. Now we use `before_action :doorkeeper_authorize!`. - [#469] Allow client applications to restrict the set of allowable scopes. Fixes #317. `oauth_applications` relation needs a new `scopes` string column, non nullable, which defaults to an empty string. To add the column run: ``` rails generate doorkeeper:application_scopes ``` If you’d rather do it by hand, your ActiveRecord migration should contain: ```ruby add_column :oauth_applications, :scopes, :string, null: false, default: ‘’ ``` ### Removed deprecations - Removes `test_redirect_uri` option. It is now called `native_redirect_uri`. - [#446] Removes `mount Doorkeeper::Engine`. Now we use `use_doorkeeper`. ### Others - [#484] Performance improvement - avoid performing order_by when not required. - [#450] When password is invalid in Password Credentials Grant, Doorkeeper returned 'invalid_resource_owner' instead of 'invalid_grant', as the spec declares. Fixes #444. - [#452] Allows `revoked_at` to be set in the future, for future expiry. Rationale: https://github.com/doorkeeper-gem/doorkeeper/pull/452#issuecomment-51431459 - [#480] For Implicit grant flow, access tokens can now be reused. Fixes #421. - [#491] Reworks of @jasl's #454 and #478. ORM refactor that allows doorkeeper to be extended more easily with unsupported ORMs. It also marks the boundaries between shared model code and ORM specifics inside of the gem. - [#496] Tests with Rails 4.2. - [#489] Adds `force_ssl_in_redirect_uri` to force the usage of the HTTPS protocol in non-native redirect uris. - [#516] SECURITY: Adds `protect_from_forgery` to `Doorkeeper::ApplicationController` - [#518] Fix random failures in mongodb. --- ## 1.4.2 - 2015-03-02 - [#576] Filter out sensitive parameters from logs ## 1.4.1 - 2014-12-17 - [#516] SECURITY: Adds `protect_from_forgery` to `Doorkeeper::ApplicationController` ## 1.4.0 - 2014-07-31 - internals - [#427] Adds specs expectations. - [#428] Error response refactor. - [#417] Moves token validation into Access Token class. - [#439] Removes redundant module includes. - [#443] TokensController and TokenInfoController inherit from ActionController::Metal - bug - [#418] fixes #243, requests with insufficient scope now respond 403 instead of 401. (API change) - [#438] fixes #398, native redirect for implicit token grant bug. - [#440] namespace fixes - enhancements - [#432] Keeps query parameters ## 1.3.1 - 2014-07-06 - enhancements - [#405] Adds facade to more easily get the token from a request in a route constraint. - [#415] Extend Doorkeeper TokenResponse with an `after_successful_response` callback that allows handling of `response` object. - internals - [#409] Deprecates `test_redirect_uri` in favor of `native_redirect_uri`. See discussion in: [#351]. - [#411] Clean rspec deprecations. General test improvements. - [#412] rspec line width can go longer than 80 (hound CI config). - bug - [#413] fixes #340, routing scope is now taken into account in redirect. - [#401] and [#425] application is not required any longer for access_token. ## 1.3.0 - 2014-05-23 - enhancements - [#387] Adds reuse_access_token configuration option. ## 1.2.0 - 2014-05-02 - enhancements - [#376] Allow users to enable basic header authorization for access tokens. - [#374] Token revocation implementation [RFC 7009] - [#295] Only enable specific grant flows. - internals - [#381] Locale source fix. - [#380] Renames `errors_for` to `doorkeeper_errors_for`. - [#390] Style adjustments in accordance with Ruby Style Guide form Thoughtbot. ## 1.1.0 - 2014-03-29 - enhancements - [#336] mongoid4 support. - [#372] Allow users to set ActiveRecord table_name_prefix/suffix options - internals - [#343] separate OAuth's admin and user end-point to different layouts, upgrade theme to Bootstrap 3.1. - [#348] Move render_options in filter after `@error` has been set ## 1.0.0 - 2014-01-13 - bug (spec) - [#228] token response `expires_in` value is now in seconds, relative to request time - [#296] client is optional for password grant type. - [#319] If client credentials are present on password grant type they are validated - [#326] If client credentials are present in refresh token they are validated - [#326] If authenticated client does not match original client that obtained a refresh token it responds `invalid_grant` instead of `invalid_client`. Previous usage was invalid according to Section 5.2 of the spec. - [#329] access tokens' `scopes` string wa being compared against `default_scopes` symbols, always unauthorizing. - [#318] Include "WWW-Authenticate" header with Unauthorized responses - enhancements - [#293] Adds ActionController::Instrumentation in TokensController - [#298] Support for multiple redirect_uris added. - [#313] `AccessToken.revoke_all_for` actually revokes all non-revoked tokens for an application/owner instead of deleting them. - [#333] Rails 4.1 support - internals - Removes jQuery dependency [fixes #300][pr #312 is related] - [#294] Client uid and secret will be generated only if not present. - [#316] Test warnings addressed. - [#338] Rspec 3 syntax. --- ## 0.7.4 - 2013-12-01 - bug - Symbols instead of strings for user input. ## 0.7.3 - 2013-10-04 - enhancements - [#204] Allow to overwrite scope in routes - internals - Returns only present keys in Token Response (may imply a backwards incompatible change). https://github.com/doorkeeper-gem/doorkeeper/issues/220 - bug - [#290] Support for Rails 4 when 'protected_attributes' gem is present. ## 0.7.2 - 2013-09-11 - enhancements - [#272] Allow issuing multiple access_tokens for one user/application for multiple devices - [#170] Increase length of allowed redirect URIs - [#239] Do not try to load unavailable Request class for the current phase. - [#273] Relax jquery-rails gem dependency ## 0.7.1 - 2013-08-30 - bug - [#269] Rails 3.2 raised `ActiveModel::MassAssignmentSecurity::Error`. ## 0.7.0 - 2013-08-21 - enhancements - [#229] Rails 4! - internals - [#203] Changing table name to be specific in column_names_with_table - [#215] README update - [#227] Use Rails.config.paths["config/routes"] instead of assuming "config/routes.rb" exists - [#262] Add jquery as gem dependency - [#263] Add a configuration for ActiveRecord.establish_connection - Deprecation and Ruby warnings (PRs merged outside of GitHub). ## 0.6.7 - 2013-01-13 - internals - [#188] Add IDs to the show views for integration testing [@egtann](https://github.com/egtann) ## 0.6.6 - 2013-01-04 - enhancements - [#187] Raise error if configuration is not set ## 0.6.5 - 2012-12-26 - enhancements - [#184] Vendor the Bootstrap CSS [@tylerhunt](https://github.com/tylerhunt) ## 0.6.4 - 2012-12-15 - bug - [#180] Add localization to authorized_applications destroy notice [@aalvarado](https://github.com/aalvarado) ## 0.6.3 - 2012-12-07 - bugfixes - [#163] Error response content-type header should be application/json [@ggayan](https://github.com/ggayan) - [#175] Make token.expires_in_seconds return nil when expires_in is nil [@miyagawa](https://github.com/miyagawa) - enhancements - [#166, #172, #174] Behavior to automatically authorize based on a configured proc - internals - [#168] Using expectation syntax for controller specs [@rdsoze](https://github.com/rdsoze) ## 0.6.2 - 2012-11-10 - bugfixes - [#162] Remove ownership columns from base migration template [@rdsoze](https://github.com/rdsoze) ## 0.6.1 - 2012-11-07 - bugfixes - [#160] Removed |routes| argument from initializer authenticator blocks - documentation - [#160] Fixed description of context of authenticator blocks ## 0.6.0 - 2012-11-05 - enhancements - Mongoid `orm` configuration accepts only :mongoid2 or :mongoid3 - Authorization endpoint does not redirect in #new action anymore. It wasn't specified by OAuth spec - TokensController now inherits from ActionController::Metal. There might be performance upgrades - Add link to authorization in Applications scaffold - [#116] MongoMapper support [@carols10cents](https://github.com/carols10cents) - [#122] Mongoid3 support [@petergoldstein](https://github.com/petergoldstein) - [#150] Introduce test redirect uri for applications - bugfixes - [#157] Response token status should be `:ok`, not `:success` [@theycallmeswift](https://github.com/theycallmeswift) - [#159] Remove ActionView::Base.field_error_proc override (fixes #145) - internals - Update development dependencies - Several refactorings - Rails/ORM are easily swichable with env vars (rails and orm) - Travis now tests against Mongoid v2 ## 0.5.0 - 2012-10-20 Official support for rubinius was removed. - enhancements - Configure the way access token is retrieved from request (default to bearer header) - Authorization Code expiration time is now configurable - Add support for mongoid - [#78, #128, #137, #138] Application Ownership - [#92] Allow users to skip controllers - [#99] Remove deprecated warnings for data-\* attributes [@towerhe](https://github.com/towerhe) - [#101] Return existing access_token for PasswordAccessTokenRequest [@benoist](https://github.com/benoist) - [#104] Changed access token scopes example code to default_scopes and optional_scopes [@amkirwan](https://github.com/amkirwan) - [#107] Fix typos in initializer - [#123] i18n for validator, flash messages [@petergoldstein](https://github.com/petergoldstein) - [#140] ActiveRecord is the default value for the ORM [@petergoldstein](https://github.com/petergoldstein) - internals - [#112, #120] Replacing update_attribute with update_column to eliminate deprecation warnings [@rmoriz](https://github.com/rmoriz), [@petergoldstein](https://github.com/petergoldstein) - [#121] Updating all development dependencies to recent versions. [@petergoldstein](https://github.com/petergoldstein) - [#144] Adding MongoDB dependency to .travis.yml [@petergoldstein](https://github.com/petergoldstein) - [#143] Displays errors for unconfigured error messages [@timgaleckas](https://github.com/timgaleckas) - bugfixes - [#102] Not returning 401 when access token generation fails [@cslew](https://github.com/cslew) - [#125] Doorkeeper is using ActiveRecord version of as_json in ORM agnostic code [@petergoldstein](https://github.com/petergoldstein) - [#142] Prevent double submission of password based authentication [@bdurand](https://github.com/bdurand) - documentation - [#141] Add rack-cors middleware to readme [@gottfrois](https://github.com/gottfrois) ## 0.4.2 - 2012-06-05 - bugfixes: - [#94] Uninitialized Constant in Password Flow ## 0.4.1 - 2012-06-02 - enhancements: - Backport: Move doorkeeper_for extension to Filter helper ## 0.4.0 - 2012-05-26 - deprecation - Deprecate authorization_scopes - database changes - AccessToken#resource_owner_id is not nullable - enhancements - [#83] Add Resource Owner Password Credentials flow [@jaimeiniesta](https://github.com/jaimeiniesta) - [#76] Allow token expiration to be disabled [@mattgreen](https://github.com/mattgreen) - [#89] Configure the way client credentials are retrieved from request - [#b6470a] Add Client Credentials flow - internals - [#2ece8d, #f93778] Introduce Client and ErrorResponse classes ## 0.3.4 - 2012-05-24 - Fix attr_accessible for rails 3.2.x ## 0.3.3 - 2012-05-07 - [#86] shrink gem package size ## 0.3.2 - 2012-04-29 - enhancements - [#54] Ignore Authorization: headers that are not Bearer [@miyagawa](https://github.com/miyagawa) - [#58, #64] Add destroy action to applications endpoint [@jaimeiniesta](https://github.com/jaimeiniesta), [@davidfrey](https://github.com/davidfrey) - [#63] TokensController responds with `401 unauthorized` [@jaimeiniesta](https://github.com/jaimeiniesta) - [#67, #72] Fix for mass-assignment [@cicloid](https://github.com/cicloid) - internals - [#49] Add Gemnasium status image to README [@laserlemon](https://github.com/laserlemon) - [#50] Fix typos [@tomekw](https://github.com/tomekw) - [#51] Updated the factory_girl_rails dependency, fix expires_in response which returned a float number instead of integer [@antekpiechnik](https://github.com/antekpiechnik) - [#62] Typos, .gitignore [@jaimeiniesta](https://github.com/jaimeiniesta) - [#65] Change \_path redirections to \_url redirections [@jaimeiniesta](https://github.com/jaimeiniesta) - [#75] Fix unknown method #authenticate_admin! [@mattgreen](https://github.com/mattgreen) - Remove application link in authorized app view ## 0.3.1 - 2012-02-17 - enhancements - [#48] Add if, else options to doorkeeper_for - Add views generator - internals - Namespace models ## 0.3.0 - 2012-02-11 - enhancements - [#17, #31] Add support for client credentials in basic auth header [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners) - [#28] Add indices to migration [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners) - [#29] Allow doorkeeper to run with rails 3.2 [@john-griffin](https://github.com/john-griffin) - [#30] Improve client's redirect uri validation [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners) - [#32] Add token (implicit grant) flow [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners) - [#34] Add support for custom unathorized responses [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners) - [#36] Remove repetitions from the Authorised Applications view [@carvil](https://github.com/carvil) - When user revoke an application, all tokens for that application are revoked - Error messages now can be translated - Install generator copies the error messages localization file - internals - Fix deprecation warnings in ActiveSupport::Base64 - Remove deprecation in doorkeeper_for that handles hash arguments - Depends on railties instead of whole rails framework - CI now integrates with rails 3.1 and 3.2 ## 0.2.0 - 2011-12-17 - enhancements - [#4] Add authorized applications endpoint - [#5, #11] Add access token scopes - [#10] Add access token expiration by default - [#9, #12] Add refresh token flow - internals - [#7] Improve configuration options with :default - Improve configuration options with :builder - Refactor config class - Improve coverage of authorization request integration - bug fixes - [#6, #20] Fix access token response headers - Fix issue with state parameter - deprecation - deprecate :only and :except options in doorkeeper_for ## 0.1.1 - 2011-11-30 - enhancements - [#3] Authorization code must be short lived and single use - [#2] Improve views provided by doorkeeper - [#1] Skips authorization form if the client has been authorized by the resource owner - Improve readme - bugfixes - Fix issue when creating the access token (wrong client id) ## 0.1.0 - 2011-11-25 - Authorization Code flow - OAuth applications endpoint ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team members or current maintainer email, specified in gemspec. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing We love pull requests from everyone. By participating in this project, you agree to abide by the [code of conduct](CODE_OF_CONDUCT.md). Fork, then clone the repo: git clone git@github.com:your-username/doorkeeper.git ### Docker Setup Build the container image with: `docker build --pull -t doorkeeper:test .` Run the tests with: `docker run -it --rm doorkeeper:test` ### Local Setup * Set up Ruby dependencies via Bundler bundle install * Make sure the tests pass: rake spec * Make your changes. * Write tests. * Follow our [style guides](.rubocop.yml). * Make the tests pass: rake spec * Add notes about your changes to the `CHANGELOG.md` file. * Write a [good commit message][commit]. * Push to your fork. * [Submit a pull request][pr]. [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [pr]: https://github.com/doorkeeper-gem/doorkeeper/compare/ * If [Rubocop] catches style violations, fix them. If our bot suggested changes — please add them. * Wait for us. We try to at least comment on pull requests within one business day. * We may suggest changes. * Please, squash your commits to a single one if you introduced a new changes or pushed more than one commit. Let's keep the history clean. Thank you for your contribution! :handshake: ================================================ FILE: Dockerfile ================================================ FROM ruby:3.3.4-alpine # Linux UID (user id) for the doorkeeper user, change with [--build-arg UID=1234] ARG UID="991" # Linux GID (group id) for the doorkeeper user, change with [--build-arg GID=1234] ARG GID="991" # Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin] ARG TZ="Etc/UTC" # Apply timezone ENV TZ=${TZ} RUN addgroup -g "${GID}" doorkeeper; \ adduser -u "${UID}" -G "doorkeeper" -h /srv doorkeeper; \ echo "${TZ}" > /etc/localtime; RUN apk add --no-cache \ ca-certificates \ wget \ openssl \ bash \ build-base \ git \ sqlite-dev \ tzdata ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV LC_ALL=en_US.UTF-8 ENV BUNDLER_VERSION=2.5.11 RUN gem install bundler -v ${BUNDLER_VERSION} -i /usr/local/lib/ruby/gems/$(ls /usr/local/lib/ruby/gems) --force WORKDIR /srv COPY Gemfile doorkeeper.gemspec /srv/ COPY lib/doorkeeper/version.rb /srv/lib/doorkeeper/version.rb # This is a fix for sqlite alpine issues RUN bundle config force_ruby_platform true RUN bundle install COPY . /srv/ RUN chown -R doorkeeper:doorkeeper /srv/coverage /srv/spec/dummy/tmp /srv/spec/generators/tmp # Set the running user for resulting container USER doorkeeper CMD ["rake"] ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } gemspec gem "rails", ">= 7.0", "< 8.1" gem "sprockets-rails" gem "rspec-core" gem "rspec-expectations" gem "rspec-mocks" gem "rspec-rails", "~> 8.0" gem "rspec-support" group :development, :rubocop do gem "rubocop", "~> 1.72" gem "rubocop-capybara", "~> 2.22", require: false gem "rubocop-factory_bot", "~> 2.27", require: false gem "rubocop-performance", "~> 1.24", require: false gem "rubocop-rails", "~> 2.30", require: false gem "rubocop-rspec", "~> 3.5", require: false gem "rubocop-rspec_rails", "~> 2.31", require: false end gem "bcrypt", "~> 3.1", require: false gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw] gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw] gem "timecop" gem 'irb', '~> 1.8' # Interactive Debugging tools gem 'debug', '~> 1.8' ================================================ FILE: MIT-LICENSE ================================================ Copyright 2011 Applicake. http://applicake.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NEWS.md ================================================ Document moved [here](CHANGELOG.md) ================================================ FILE: README.md ================================================ # Doorkeeper — awesome OAuth 2 provider for your Rails / Grape app. [![Gem Version](https://badge.fury.io/rb/doorkeeper.svg)](https://rubygems.org/gems/doorkeeper) [![CI](https://github.com/doorkeeper-gem/doorkeeper/actions/workflows/ci.yml/badge.svg)](https://github.com/doorkeeper-gem/doorkeeper/actions/workflows/ci.yml) [![Maintainability](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper/maintainability.svg)](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper) [![Coverage Status](https://coveralls.io/repos/github/doorkeeper-gem/doorkeeper/badge.svg?branch=main)](https://coveralls.io/github/doorkeeper-gem/doorkeeper?branch=main) [![GuardRails badge](https://api.guardrails.io/v2/badges/21183?token=66768ce8f6995814df81f65a2cff40f739f688492704f973e62809e15599bb62)](https://dashboard.guardrails.io/gh/doorkeeper-gem/repos/21183) [![Dependabot](https://img.shields.io/badge/dependabot-enabled-success.svg)](https://dependabot.com) Doorkeeper is a gem (Rails engine) that makes it easy to introduce OAuth 2 provider functionality to your Ruby on Rails or Grape application. Supported features: - [The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749) - [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) - [Access Token Scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3) - [Refresh token](https://datatracker.ietf.org/doc/html/rfc6749#section-1.5) - [Implicit grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.2) - [Resource Owner Password Credentials](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) - [Client Credentials](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) - [OAuth 2.0 Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009) - [OAuth 2.0 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662) - [OAuth 2.0 Threat Model and Security Considerations](https://datatracker.ietf.org/doc/html/rfc6819) - [OAuth 2.0 for Native Apps](https://datatracker.ietf.org/doc/html/rfc8252) - [Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636) ## Table of Contents - [Documentation](#documentation) - [Installation](#installation) - [Ruby on Rails](#ruby-on-rails) - [Grape](#grape) - [ORMs](#orms) - [Extensions](#extensions) - [Example Applications](#example-applications) - [Sponsors](#sponsors) - [Development](#development) - [Contributing](#contributing) - [Contributors](#contributors) - [License](#license) ## Documentation This documentation is valid for `main` branch. Please check the documentation for the version of doorkeeper you are using in: https://github.com/doorkeeper-gem/doorkeeper/releases. Additionally, other resources can be found on: - [Guides](https://doorkeeper.gitbook.io/guides/) with how-to get started and configuration documentation - See the [Wiki](https://github.com/doorkeeper-gem/doorkeeper/wiki) for articles on how to integrate with other solutions - Screencast from [railscasts.com](http://railscasts.com/): [#353 OAuth with Doorkeeper](http://railscasts.com/episodes/353-oauth-with-doorkeeper) - See [upgrade guides](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions) - For general questions, please post on [Stack Overflow](http://stackoverflow.com/questions/tagged/doorkeeper) - See [SECURITY.md](SECURITY.md) for this project's security disclose policy ## Installation Installation depends on the framework you're using. The first step is to add the following to your Gemfile: ```ruby gem 'doorkeeper' ``` And run `bundle install`. After this, check out the guide related to the framework you're using. ### Ruby on Rails Doorkeeper currently supports Ruby on Rails >= 5.0. See the guide [here](https://doorkeeper.gitbook.io/guides/ruby-on-rails/getting-started). ### Grape Guide for integration with Grape framework can be found [here](https://doorkeeper.gitbook.io/guides/grape/grape). ## ORMs Doorkeeper supports Active Record by default, but can be configured to work with the following ORMs: | ORM | Support via | | :--- | :--- | | Active Record | by default | | MongoDB | [doorkeeper-gem/doorkeeper-mongodb](https://github.com/doorkeeper-gem/doorkeeper-mongodb) | | Sequel | [nbulaj/doorkeeper-sequel](https://github.com/nbulaj/doorkeeper-sequel) | | Couchbase | [acaprojects/doorkeeper-couchbase](https://github.com/acaprojects/doorkeeper-couchbase) | | RethinkDB | [aca-labs/doorkeeper-rethinkdb](https://github.com/aca-labs/doorkeeper-rethinkdb) | ## Extensions Extensions that are not included by default and can be installed separately. | | Link | | :--- | :--- | | OpenID Connect extension | [doorkeeper-gem/doorkeeper-openid\_connect](https://github.com/doorkeeper-gem/doorkeeper-openid_connect) | | JWT Token support | [doorkeeper-gem/doorkeeper-jwt](https://github.com/doorkeeper-gem/doorkeeper-jwt) | | Assertion grant extension | [doorkeeper-gem/doorkeeper-grants\_assertion](https://github.com/doorkeeper-gem/doorkeeper-grants_assertion) | | I18n translations | [doorkeeper-gem/doorkeeper-i18n](https://github.com/doorkeeper-gem/doorkeeper-i18n) | | CIBA - Client Initiated Backchannel Authentication Flow extension | [doorkeeper-ciba](https://github.com/autoseg/doorkeeper-ciba) | | Device Authorization Grant | [doorkeeper-device_authorization_grant](https://github.com/exop-group/doorkeeper-device_authorization_grant) | ## Example Applications These applications show how Doorkeeper works and how to integrate with it. Start with the oAuth2 server and use the clients to connect with the server. | Application | Link | | :--- | :--- | | OAuth2 Server with Doorkeeper | [doorkeeper-gem/doorkeeper-provider-app](https://github.com/doorkeeper-gem/doorkeeper-provider-app) | | Sinatra Client connected to Provider App | [doorkeeper-gem/doorkeeper-sinatra-client](https://github.com/doorkeeper-gem/doorkeeper-sinatra-client) | | Devise + Omniauth Client | [doorkeeper-gem/doorkeeper-devise-client](https://github.com/doorkeeper-gem/doorkeeper-devise-client) | You may want to create a client application to test the integration. Check out these [client examples](https://github.com/doorkeeper-gem/doorkeeper/wiki/Example-Applications) in our wiki or follow this [tutorial here](https://github.com/doorkeeper-gem/doorkeeper/wiki/Testing-your-provider-with-OAuth2-gem). ## Sponsors [![OpenCollective](https://opencollective.com/doorkeeper-gem/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/doorkeeper-gem/sponsors/badge.svg)](#sponsors) Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/doorkeeper-gem#sponsor)] > Codecademy supports open source as part of its mission to democratize tech. Come help us build the education the world deserves: [https://codecademy.com/about/careers](https://codecademy.com/about/careers?utm_source=doorkeeper-gem)
> If you prefer not to deal with the gory details of OAuth 2, need dedicated customer support & consulting, try the cloud-based SaaS version: [https://oauth.io](https://oauth.io/?utm_source=doorkeeper-gem)
> Wealthsimple is a financial company on a mission to help everyone achieve financial freedom by providing products and advice that are accessible and affordable. Using smart technology, Wealthsimple takes financial services that are often confusing, opaque and expensive and makes them simple, transparent, and low-cost. See what Investing on Autopilot is all about: [https://www.wealthsimple.com](https://www.wealthsimple.com/?utm_source=doorkeeper-gem) ## Development To run the local engine server: ``` bundle install bundle exec rake doorkeeper:server ```` By default, it uses the latest Rails version with ActiveRecord. To run the tests with a specific Rails version: ``` BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake ``` You can also experiment with the changes using `bin/console`. It uses in-memory SQLite database and default Doorkeeper config, but you can reestablish connection or reconfigure the gem if you need. ## Contributing Want to contribute and don't know where to start? Check out [features we're missing](https://github.com/doorkeeper-gem/doorkeeper/wiki/Supported-Features), create [example apps](https://github.com/doorkeeper-gem/doorkeeper/wiki/Example-Applications), integrate the gem with your app and let us know! Also, check out our [contributing guidelines page](CONTRIBUTING.md). ## Contributors Thanks to all our [awesome contributors](https://github.com/doorkeeper-gem/doorkeeper/graphs/contributors)! ## License MIT License. Created in Applicake. Maintained by the community. ================================================ FILE: RELEASING.md ================================================ # Releasing Doorkeeper How to release Doorkeeper in five easy steps! 1. Update `lib/doorkeeper/version.rb` file accordingly. 2. Update `CHANGELOG.md` to reflect the changes since last release. 3. Commit changes: `git commit -am 'Bump to vVERSION'`. 4. Build and publish the gem. 4. Create GitHub release. 5. Announce the new release, making sure to say “thank you” to the contributors who helped shape this version! ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true require "bundler/setup" require "rspec/core/rake_task" desc "Default: run specs." task default: :spec desc "Run all specs" RSpec::Core::RakeTask.new(:spec) do |config| config.verbose = false end namespace :doorkeeper do desc "Install doorkeeper in dummy app" task :install do cd "spec/dummy" system "bundle exec rails g doorkeeper:install --force" end desc "Runs local test server" task :server do cd "spec/dummy" system "bundle exec rails server" end end Bundler::GemHelper.install_tasks ================================================ FILE: SECURITY.md ================================================ # Reporting security issues in Doorkeeper Hello! Thank you for wanting to disclose a possible security vulnerability within the Doorkeeper gem! Please follow our disclosure policy as outlined below: 1. Do NOT open up a GitHub issue with your report. Security reports should be kept private until a possible fix is determined. 2. Send an email to Nikita Bulai at bulaj.nikita AT gmail.com or one of the others Doorkeeper maintainers listed in gemspec. You should receive a prompt response. 3. Be patient. Since Doorkeeper is in a stable maintenance phase, we want to do as little as possible to rock the boat of the project. Thank you very much for adhering for these policies! ================================================ FILE: UPGRADE.md ================================================ See [Upgrade Guides](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions) in the project Wiki. ================================================ FILE: app/assets/stylesheets/doorkeeper/admin/application.css ================================================ /* *= require doorkeeper/bootstrap.min * *= require_self *= require_tree . */ .doorkeeper-admin .form-group > .field_with_errors { width: 16.66667%; } ================================================ FILE: app/assets/stylesheets/doorkeeper/application.css ================================================ /* *= require doorkeeper/bootstrap.min * *= require_self *= require_tree . */ body { background-color: #eee; font-size: 14px; } #container { background-color: #fff; border: 1px solid #999; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 6px; -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); box-shadow: 0 3px 20px rgba(0, 0, 0, 0.3); margin: 2em auto; max-width: 600px; outline: 0; padding: 1em; width: 80%; } .page-header { margin-top: 20px; } #oauth-permissions { width: 260px; } .actions { border-top: 1px solid #eee; margin-top: 1em; padding-top: 9px; } .actions > form > .btn { margin-top: 5px; } .separator { color: #eee; padding: 0 .5em; } .inline_block { display: inline-block; } #oauth { margin-bottom: 1em; } #oauth > .btn { width: 7em; } td { vertical-align: middle !important; } ================================================ FILE: app/controllers/doorkeeper/application_controller.rb ================================================ # frozen_string_literal: true module Doorkeeper class ApplicationController < Doorkeeper.config.resolve_controller(:base) include Helpers::Controller include ActionController::MimeResponds if Doorkeeper.config.api_only unless Doorkeeper.config.api_only protect_from_forgery with: :exception helper "doorkeeper/dashboard" end end end ================================================ FILE: app/controllers/doorkeeper/application_metal_controller.rb ================================================ # frozen_string_literal: true module Doorkeeper class ApplicationMetalController < Doorkeeper.config.resolve_controller(:base_metal) include Helpers::Controller before_action :enforce_content_type, if: -> { Doorkeeper.config.enforce_content_type } ActiveSupport.run_load_hooks(:doorkeeper_metal_controller, self) end end ================================================ FILE: app/controllers/doorkeeper/applications_controller.rb ================================================ # frozen_string_literal: true module Doorkeeper class ApplicationsController < Doorkeeper::ApplicationController layout "doorkeeper/admin" unless Doorkeeper.configuration.api_only before_action :authenticate_admin! before_action :set_application, only: %i[show edit update destroy] def index @applications = Doorkeeper.config.application_model.ordered_by(:created_at) respond_to do |format| format.html format.json { head :no_content } end end def show respond_to do |format| format.html format.json { render json: @application, as_owner: true } end end def new @application = Doorkeeper.config.application_model.new end def create @application = Doorkeeper.config.application_model.new(application_params) if @application.save flash[:notice] = I18n.t(:notice, scope: %i[doorkeeper flash applications create]) flash[:application_secret] = @application.plaintext_secret respond_to do |format| format.html { redirect_to oauth_application_url(@application) } format.json { render json: @application, as_owner: true } end else respond_to do |format| format.html { render :new } format.json do errors = @application.errors.full_messages render json: { errors: errors }, status: :unprocessable_entity end end end end def edit; end def update if @application.update(application_params) flash[:notice] = I18n.t(:notice, scope: i18n_scope(:update)) respond_to do |format| format.html { redirect_to oauth_application_url(@application) } format.json { render json: @application, as_owner: true } end else respond_to do |format| format.html { render :edit } format.json do errors = @application.errors.full_messages render json: { errors: errors }, status: :unprocessable_entity end end end end def destroy flash[:notice] = I18n.t(:notice, scope: i18n_scope(:destroy)) if @application.destroy respond_to do |format| format.html { redirect_to oauth_applications_url } format.json { head :no_content } end end private def set_application @application = Doorkeeper.config.application_model.find(params[:id]) end def application_params params.require(:doorkeeper_application) .permit(:name, :redirect_uri, :scopes, :confidential) end def i18n_scope(action) %i[doorkeeper flash applications] << action end end end ================================================ FILE: app/controllers/doorkeeper/authorizations_controller.rb ================================================ # frozen_string_literal: true module Doorkeeper class AuthorizationsController < Doorkeeper::ApplicationController before_action :authenticate_resource_owner! def new if pre_auth.authorizable? render_success else render_error end end def create redirect_or_render(authorize_response) end def destroy redirect_or_render(authorization.deny) rescue Doorkeeper::Errors::InvalidTokenStrategy => e error_response = get_error_response_from_exception(e) if Doorkeeper.configuration.api_only render json: error_response.body, status: :bad_request else render :error, locals: { error_response: error_response } end end private def render_success if skip_authorization? || can_authorize_response? redirect_or_render(authorize_response) elsif Doorkeeper.configuration.api_only render json: pre_auth else render :new end end def render_error pre_auth.error_response.raise_exception! if Doorkeeper.config.raise_on_errors? if Doorkeeper.configuration.redirect_on_errors? && pre_auth.error_response.redirectable? redirect_or_render(pre_auth.error_response) elsif Doorkeeper.configuration.api_only render json: pre_auth.error_response.body, status: pre_auth.error_response.status else render :error, locals: { error_response: pre_auth.error_response }, status: pre_auth.error_response.status end end def can_authorize_response? Doorkeeper.config.custom_access_token_attributes.empty? && pre_auth.client.application.confidential? && matching_token? end # Active access token issued for the same client and resource owner with # the same set of the scopes exists? def matching_token? # We don't match tokens on the custom attributes here - we're in the pre-auth here, # so they haven't been supplied yet (there are no custom attributes to match on yet) @matching_token ||= Doorkeeper.config.access_token_model.matching_token_for( pre_auth.client, current_resource_owner, pre_auth.scopes, ) end def redirect_or_render(auth) if auth.redirectable? if Doorkeeper.configuration.api_only if pre_auth.form_post_response? render( json: { status: :post, redirect_uri: pre_auth.redirect_uri, body: auth.body }, status: auth.status, ) else render( json: { status: :redirect, redirect_uri: auth.redirect_uri }, status: auth.status, ) end elsif pre_auth.form_post_response? render :form_post, locals: { auth: auth } else redirect_to auth.redirect_uri, allow_other_host: true end else render json: auth.body, status: auth.status end end def pre_auth @pre_auth ||= OAuth::PreAuthorization.new( Doorkeeper.configuration, pre_auth_params, current_resource_owner, ) end def pre_auth_params params.slice(*pre_auth_param_fields).permit(*pre_auth_param_fields) end def pre_auth_param_fields custom_access_token_attributes + %i[ client_id code_challenge code_challenge_method response_type response_mode redirect_uri scope state ] end def custom_access_token_attributes Doorkeeper.config.custom_access_token_attributes.map(&:to_sym) end def authorization @authorization ||= strategy.request end def strategy @strategy ||= server.authorization_request(pre_auth.response_type) end def authorize_response @authorize_response ||= begin unless pre_auth.authorizable? response = pre_auth.error_response response.raise_exception! if Doorkeeper.config.raise_on_errors? return response end context = build_context(pre_auth: pre_auth) before_successful_authorization(context) auth = strategy.authorize context = build_context(auth: auth) after_successful_authorization(context) auth end end def build_context(**attributes) Doorkeeper::OAuth::Hooks::Context.new(**attributes) end def before_successful_authorization(context = nil) Doorkeeper.config.before_successful_authorization.call(self, context) end def after_successful_authorization(context) Doorkeeper.config.after_successful_authorization.call(self, context) end end end ================================================ FILE: app/controllers/doorkeeper/authorized_applications_controller.rb ================================================ # frozen_string_literal: true module Doorkeeper class AuthorizedApplicationsController < Doorkeeper::ApplicationController before_action :authenticate_resource_owner! def index @applications = Doorkeeper.config.application_model.authorized_for(current_resource_owner) respond_to do |format| format.html format.json { render json: @applications, current_resource_owner: current_resource_owner } end end def destroy Doorkeeper.config.application_model.revoke_tokens_and_grants_for( params[:id], current_resource_owner, ) respond_to do |format| format.html do redirect_to oauth_authorized_applications_url, notice: I18n.t( :notice, scope: %i[doorkeeper flash authorized_applications destroy], ) end format.json { head :no_content } end end end end ================================================ FILE: app/controllers/doorkeeper/token_info_controller.rb ================================================ # frozen_string_literal: true module Doorkeeper class TokenInfoController < Doorkeeper::ApplicationMetalController def show if doorkeeper_token&.accessible? render json: doorkeeper_token_to_json, status: :ok else error = OAuth::InvalidTokenResponse.new response.headers.merge!(error.headers) render json: error_to_json(error), status: error.status end end protected def doorkeeper_token_to_json doorkeeper_token end def error_to_json(error) error.body end end end ================================================ FILE: app/controllers/doorkeeper/tokens_controller.rb ================================================ # frozen_string_literal: true module Doorkeeper class TokensController < Doorkeeper::ApplicationMetalController before_action :validate_presence_of_client, only: [:revoke] rescue_from Errors::DoorkeeperError do |e| handle_token_exception(e) end def create headers.merge!(authorize_response.headers) render json: authorize_response.body, status: authorize_response.status end # OAuth 2.0 Token Revocation - https://datatracker.ietf.org/doc/html/rfc7009 def revoke # The authorization server responds with HTTP status code 200 if the client # submitted an invalid token or the token has been revoked successfully. if token.blank? render json: {}, status: 200 # The authorization server validates [...] and whether the token # was issued to the client making the revocation request. If this # validation fails, the request is refused and the client is informed # of the error by the authorization server as described below. elsif authorized? revoke_token render json: {}, status: 200 else render json: revocation_error_response, status: :forbidden end end # OAuth 2.0 Token Introspection - https://datatracker.ietf.org/doc/html/rfc7662 def introspect introspection = OAuth::TokenIntrospection.new(server, token) if introspection.authorized? render json: introspection.to_json, status: 200 else error = introspection.error_response headers.merge!(error.headers) render json: error.body, status: error.status end end private def validate_presence_of_client return if Doorkeeper.config.skip_client_authentication_for_password_grant # @see 2.1. Revocation Request # # The client constructs the request by including the following # parameters using the "application/x-www-form-urlencoded" format in # the HTTP request entity-body: # token REQUIRED. # token_type_hint OPTIONAL. # # The client also includes its authentication credentials as described # in Section 2.3. of [RFC6749]. # # The authorization server first validates the client credentials (in # case of a confidential client) and then verifies whether the token # was issued to the client making the revocation request. return if server.client # If this validation [client credentials / token ownership] fails, the request is # refused and the client is informed of the error by the authorization server as # described below. # # @see 2.2.1. Error Response # # The error presentation conforms to the definition in Section 5.2 of [RFC6749]. render json: revocation_error_response, status: :forbidden end # OAuth 2.0 Section 2.1 defines two client types, "public" & "confidential". # # RFC7009 # Section 5. Security Considerations # A malicious client may attempt to guess valid tokens on this endpoint # by making revocation requests against potential token strings. # According to this specification, a client's request must contain a # valid client_id, in the case of a public client, or valid client # credentials, in the case of a confidential client. The token being # revoked must also belong to the requesting client. # # Once a client is authenticated, it must be authorized to # revoke the provided access or refresh token. This ensures one client # cannot revoke another's tokens. # # Doorkeeper checks token ownership for any token that has an # application_id set. Tokens issued without a client (application_id # is null) can be revoked without client authorization. # # https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 # https://datatracker.ietf.org/doc/html/rfc7009 def authorized? # Token belongs to specific client, so we need to check if # authenticated client could access it. if token.application_id? # We authorize client by comparing client and token application IDs server.client && server.client.id == token.application_id else # Token was issued without client, authorization unnecessary true end end def revoke_token # The authorization server responds with HTTP status code 200 if the token # has been revoked successfully or if the client submitted an invalid # token revocable_token.revoke if revocable_token.revocable? end def token revocable_token&.token end def revocable_token return @revocable_token if defined? @revocable_token @revocable_token = if params[:token_type_hint] == "refresh_token" refresh_token else access_token || refresh_token end end def refresh_token token = Doorkeeper.config.access_token_model.by_refresh_token(params["token"]) return unless token RevocableTokens::RevocableRefreshToken.new(token) end def access_token token = Doorkeeper.config.access_token_model.by_token(params["token"]) return unless token RevocableTokens::RevocableAccessToken.new(token) end def strategy @strategy ||= server.token_request(params[:grant_type]) end def authorize_response @authorize_response ||= begin before_successful_authorization auth = strategy.authorize context = build_context(auth: auth) after_successful_authorization(context) unless auth.is_a?(Doorkeeper::OAuth::ErrorResponse) auth end end def build_context(**attributes) Doorkeeper::OAuth::Hooks::Context.new(**attributes) end def before_successful_authorization(context = nil) Doorkeeper.config.before_successful_authorization.call(self, context) end def after_successful_authorization(context) Doorkeeper.config.after_successful_authorization.call(self, context) end def revocation_error_response error_description = I18n.t(:unauthorized, scope: %i[doorkeeper errors messages revoke]) { error: :unauthorized_client, error_description: error_description } end end end ================================================ FILE: app/helpers/doorkeeper/dashboard_helper.rb ================================================ # frozen_string_literal: true module Doorkeeper module DashboardHelper def doorkeeper_errors_for(object, method) return if object.errors[method].blank? output = object.errors[method].map do |msg| content_tag(:span, class: "invalid-feedback") do msg.capitalize end end safe_join(output) end def doorkeeper_submit_path(application) application.persisted? ? oauth_application_path(application) : oauth_applications_path end end end ================================================ FILE: app/views/doorkeeper/applications/_delete_form.html.erb ================================================ <%- submit_btn_css ||= 'btn btn-link' %> <%= form_tag oauth_application_path(application), method: :delete do %> <%= submit_tag t('doorkeeper.applications.buttons.destroy'), onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')", class: submit_btn_css %> <% end %> ================================================ FILE: app/views/doorkeeper/applications/_form.html.erb ================================================ <%= form_for application, url: doorkeeper_submit_path(application), as: :doorkeeper_application, html: { role: 'form' } do |f| %> <% if application.errors.any? %>

<%= t('doorkeeper.applications.form.error') %>

<% end %>
<%= f.label :name, class: 'col-sm-2 col-form-label font-weight-bold' %>
<%= f.text_field :name, class: "form-control #{ 'is-invalid' if application.errors[:name].present? }", required: true %> <%= doorkeeper_errors_for application, :name %>
<%= f.label :redirect_uri, class: 'col-sm-2 col-form-label font-weight-bold' %>
<%= f.text_area :redirect_uri, class: "form-control #{ 'is-invalid' if application.errors[:redirect_uri].present? }" %> <%= doorkeeper_errors_for application, :redirect_uri %> <%= t('doorkeeper.applications.help.redirect_uri') %> <% if Doorkeeper.configuration.allow_blank_redirect_uri?(application) %> <%= t('doorkeeper.applications.help.blank_redirect_uri') %> <% end %>
<%= f.label :confidential, class: 'col-sm-2 form-check-label font-weight-bold' %>
<%= f.check_box :confidential, class: "checkbox #{ 'is-invalid' if application.errors[:confidential].present? }" %> <%= doorkeeper_errors_for application, :confidential %> <%= t('doorkeeper.applications.help.confidential') %>
<%= f.label :scopes, class: 'col-sm-2 col-form-label font-weight-bold' %>
<%= f.text_field :scopes, class: "form-control #{ 'has-error' if application.errors[:scopes].present? }" %> <%= doorkeeper_errors_for application, :scopes %> <%= t('doorkeeper.applications.help.scopes') %>
<%= f.submit t('doorkeeper.applications.buttons.submit'), class: 'btn btn-primary' %> <%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path, class: 'btn btn-secondary' %>
<% end %> ================================================ FILE: app/views/doorkeeper/applications/edit.html.erb ================================================

<%= t('.title') %>

<%= render 'form', application: @application %> ================================================ FILE: app/views/doorkeeper/applications/index.html.erb ================================================

<%= t('.title') %>

<%= link_to t('.new'), new_oauth_application_path, class: 'btn btn-success' %>

<% @applications.each do |application| %> <% end %>
<%= t('.name') %> <%= t('.callback_url') %> <%= t('.confidential') %> <%= t('.actions') %>
<%= link_to application.name, oauth_application_path(application) %> <%= simple_format(application.redirect_uri) %> <%= application.confidential? ? t('doorkeeper.applications.index.confidentiality.yes') : t('doorkeeper.applications.index.confidentiality.no') %> <%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %> <%= render 'delete_form', application: application %>
================================================ FILE: app/views/doorkeeper/applications/new.html.erb ================================================

<%= t('.title') %>

<%= render 'form', application: @application %> ================================================ FILE: app/views/doorkeeper/applications/show.html.erb ================================================

<%= t('.title', name: @application.name) %>

<%= t('.application_id') %>

<%= @application.uid %>

<%= t('.secret') %>

<% secret = flash[:application_secret].presence || @application.plaintext_secret %> <% if secret.blank? && Doorkeeper.config.application_secret_hashed? %> <%= t('.secret_hashed') %> <% else %> <%= secret %> <% end %>

<%= t('.scopes') %>

<% if @application.scopes.present? %> <%= @application.scopes %> <% else %> <%= t('.not_defined') %> <% end %>

<%= t('.confidential') %>

<%= @application.confidential? %>

<%= t('.callback_urls') %>

<% if @application.redirect_uri.present? %> <% @application.redirect_uri.split.each do |uri| %> <% end %>
<%= uri %> <%= link_to t('doorkeeper.applications.buttons.authorize'), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes), class: 'btn btn-success', target: '_blank' %>
<% else %> <%= t('.not_defined') %> <% end %>

<%= t('.actions') %>

<%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(@application), class: 'btn btn-primary' %>

<%= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger' %>

================================================ FILE: app/views/doorkeeper/authorizations/error.html.erb ================================================

<%= t('doorkeeper.authorizations.error.title') %>

    <%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %>
  
================================================ FILE: app/views/doorkeeper/authorizations/form_post.html.erb ================================================ <%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false do %> <% auth.body.compact.each do |key, value| %> <%= hidden_field_tag key, value %> <% end %> <% end %> ================================================ FILE: app/views/doorkeeper/authorizations/new.html.erb ================================================

<%= raw t('.prompt', client_name: content_tag(:strong, class: 'text-info') { @pre_auth.client.name }) %>

<% if @pre_auth.scopes.count > 0 %>

<%= t('.able_to') %>

    <% @pre_auth.scopes.each do |scope| %>
  • <%= t scope, scope: [:doorkeeper, :scopes] %>
  • <% end %>
<% end %>
<%= form_tag oauth_authorization_path, method: :post do %> <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %> <%= hidden_field_tag :state, @pre_auth.state, id: nil %> <%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %> <%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %> <%= hidden_field_tag :scope, @pre_auth.scope, id: nil %> <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %> <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %> <%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %> <% end %> <%= form_tag oauth_authorization_path, method: :delete do %> <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %> <%= hidden_field_tag :state, @pre_auth.state, id: nil %> <%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %> <%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %> <%= hidden_field_tag :scope, @pre_auth.scope, id: nil %> <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %> <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %> <%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %> <% end %>
================================================ FILE: app/views/doorkeeper/authorizations/show.html.erb ================================================
<%= params[:code] %>
================================================ FILE: app/views/doorkeeper/authorized_applications/_delete_form.html.erb ================================================ <%- submit_btn_css ||= 'btn btn-link' %> <%= form_tag oauth_authorized_application_path(application), method: :delete do %> <%= submit_tag t('doorkeeper.authorized_applications.buttons.revoke'), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %> <% end %> ================================================ FILE: app/views/doorkeeper/authorized_applications/index.html.erb ================================================
<% @applications.each do |application| %> <% end %>
<%= t('doorkeeper.authorized_applications.index.application') %> <%= t('doorkeeper.authorized_applications.index.created_at') %>
<%= application.name %> <%= application.created_at.strftime(t('doorkeeper.authorized_applications.index.date_format')) %> <%= render 'delete_form', application: application %>
================================================ FILE: app/views/layouts/doorkeeper/admin.html.erb ================================================ <%= t('doorkeeper.layouts.admin.title') %> <%= stylesheet_link_tag "doorkeeper/admin/application" %> <%= csrf_meta_tags %>
<%- if flash[:notice].present? %>
<%= flash[:notice] %>
<% end -%> <%= yield %>
================================================ FILE: app/views/layouts/doorkeeper/application.html.erb ================================================ <%= t('doorkeeper.layouts.application.title') %> <%= stylesheet_link_tag "doorkeeper/application" %> <%= csrf_meta_tags %>
<%- if flash[:notice].present? %>
<%= flash[:notice] %>
<% end -%> <%= yield %>
================================================ FILE: benchmark/ruby/client_credentials.rb ================================================ # frozen_string_literal: true $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "..", "lib")) begin require "bundler/inline" rescue LoadError => e warn "Bundler version 1.10 or later is required. Please update your Bundler" raise e end gemfile(true) do source "https://rubygems.org" gem "sqlite3" gem "rails" gem "benchmark-ips" end require "benchmark/ips" require "ostruct" require "doorkeeper" require "active_support/all" require "active_record/railtie" ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.logger = ENV["LOGGER"].present? ? Logger.new(STDOUT) : nil # Load database schema load File.expand_path("../../spec/dummy/db/schema.rb", __dir__) Doorkeeper.configure do orm :active_record grant_flows %w[password authorization_code client_credentials] skip_authorization do true end end client = Doorkeeper::Application.create!( name: "test", uid: "123456789", secret: "987654321", redirect_uri: "https://doorkeeper.test", ) context = OpenStruct.new request = OpenStruct.new request.parameters = { client_id: client.uid, client_secret: client.secret, }.with_indifferent_access context.request = request Benchmark.ips do |ips| ips.report("Client credentials") do server = Doorkeeper::Server.new(context) strategy = server.token_request("client_credentials") strategy.authorize end end ================================================ FILE: benchmark/wrk/.keep ================================================ ================================================ FILE: bin/console ================================================ #!/usr/bin/env ruby # frozen_string_literal: true require "bundler/setup" require "rails/all" require "active_support/all" require "irb" require "debug" require "doorkeeper" Rails.logger = Logger.new(STDOUT) Rails.logger.info("Doorkeeper version: #{Doorkeeper::VERSION::STRING}") Rails.logger.info("Rails version: #{Rails::VERSION::STRING}") # Default Doorkeeper config Doorkeeper.configure do orm :active_record end # Generate in-memory database for testing ActiveRecord::Base.establish_connection( adapter: "sqlite3", database: ":memory:", ) # Load database schema load File.expand_path("../spec/dummy/db/schema.rb", __dir__) IRB.start(__FILE__) ================================================ FILE: config/locales/en.yml ================================================ en: activerecord: attributes: doorkeeper/application: name: 'Name' redirect_uri: 'Redirect URI' errors: models: doorkeeper/application: attributes: redirect_uri: fragment_present: 'cannot contain a fragment.' invalid_uri: 'must be a valid URI.' unspecified_scheme: 'must specify a scheme.' relative_uri: 'must be an absolute URI.' secured_uri: 'must be an HTTPS/SSL URI.' forbidden_uri: 'is forbidden by the server.' scopes: not_match_configured: "doesn't match those configured on the server." doorkeeper: applications: confirmations: destroy: 'Are you sure?' buttons: edit: 'Edit' destroy: 'Destroy' submit: 'Submit' cancel: 'Cancel' authorize: 'Authorize' form: error: 'Whoops! Check your form for possible errors' help: confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.' redirect_uri: 'Use one line per URI' blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require a redirect URI." scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' edit: title: 'Edit application' index: title: 'Your applications' new: 'New Application' name: 'Name' callback_url: 'Callback URL' confidential: 'Confidential?' actions: 'Actions' confidentiality: 'yes': 'Yes' 'no': 'No' new: title: 'New Application' show: title: 'Application: %{name}' application_id: 'UID:' secret: 'Secret:' secret_hashed: 'Secret hashed' scopes: 'Scopes:' confidential: 'Confidential:' callback_urls: 'Callback URLs:' actions: 'Actions' not_defined: 'Not defined' authorizations: buttons: authorize: 'Authorize' deny: 'Deny' error: title: 'An error has occurred' new: title: 'Authorization required' prompt: 'Authorize %{client_name} to use your account?' able_to: 'This application will be able to:' show: title: 'Authorization code:' form_post: title: 'Submit this form' authorized_applications: confirmations: revoke: 'Are you sure?' buttons: revoke: 'Revoke' index: title: 'Your authorized applications' application: 'Application' created_at: 'Created At' date_format: '%Y-%m-%d %H:%M:%S' pre_authorization: status: 'Pre-authorization' errors: messages: # Common error messages invalid_request: unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' missing_param: 'Missing required parameter: %{value}.' request_not_authorized: 'Request needs to be authorized. Required parameter for authorizing the request is missing or invalid.' invalid_code_challenge: 'Code challenge is required.' invalid_redirect_uri: "The requested redirect URI is malformed or doesn't match the client redirect URI." unauthorized_client: 'The client is not authorized to perform this request using this method.' access_denied: 'The resource owner or authorization server denied the request.' invalid_scope: 'The requested scope is invalid, unknown, or malformed.' invalid_code_challenge_method: zero: 'The authorization server does not support PKCE as there are no accepted code_challenge_method values.' one: 'The code_challenge_method must be %{challenge_methods}.' other: 'The code_challenge_method must be one of %{challenge_methods}.' server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' # Configuration error messages credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.' admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.' # Access grant errors unsupported_response_type: 'The authorization server does not support this response type.' unsupported_response_mode: 'The authorization server does not support this response mode.' # Access token errors invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' invalid_token: revoked: "The access token was revoked" expired: "The access token expired" unknown: "The access token is invalid" revoke: unauthorized: "You are not authorized to revoke this token" forbidden_token: missing_scope: 'Access to this resource requires scope "%{oauth_scopes}".' flash: applications: create: notice: 'Application created.' destroy: notice: 'Application deleted.' update: notice: 'Application updated.' authorized_applications: destroy: notice: 'Application revoked.' layouts: admin: title: 'Doorkeeper' nav: oauth2_provider: 'OAuth2 Provider' applications: 'Applications' home: 'Home' application: title: 'OAuth authorization required' ================================================ FILE: doorkeeper.gemspec ================================================ # frozen_string_literal: true $LOAD_PATH.unshift(File.expand_path("lib", __dir__)) require "doorkeeper/version" Gem::Specification.new do |gem| gem.name = "doorkeeper" gem.version = Doorkeeper::VERSION::STRING gem.authors = ["Felipe Elias Philipp", "Tute Costa", "Jon Moss", "Nikita Bulai"] gem.email = %w[bulaj.nikita@gmail.com] gem.homepage = "https://github.com/doorkeeper-gem/doorkeeper" gem.summary = "OAuth 2 provider for Rails and Grape" gem.description = "Doorkeeper is an OAuth 2 provider for Rails and Grape." gem.license = "MIT" gem.files = Dir[ "{app,config,lib,vendor}/**/*", "CHANGELOG.md", "MIT-LICENSE", "README.md", ] gem.require_paths = ["lib"] gem.metadata = { "homepage_uri" => "https://github.com/doorkeeper-gem/doorkeeper", "changelog_uri" => "https://github.com/doorkeeper-gem/doorkeeper/blob/main/CHANGELOG.md", "source_code_uri" => "https://github.com/doorkeeper-gem/doorkeeper", "bug_tracker_uri" => "https://github.com/doorkeeper-gem/doorkeeper/issues", "documentation_uri" => "https://doorkeeper.gitbook.io/guides/", "funding_uri" => "https://opencollective.com/doorkeeper-gem", } gem.add_dependency "railties", ">= 5" gem.required_ruby_version = ">= 2.7" gem.add_development_dependency "appraisal" gem.add_development_dependency "capybara" gem.add_development_dependency "coveralls_reborn" gem.add_development_dependency "database_cleaner", "~> 2.0" gem.add_development_dependency "factory_bot", "~> 6.0" gem.add_development_dependency "generator_spec", "~> 0.10.0" gem.add_development_dependency "grape" gem.add_development_dependency "rake", ">= 11.3.0" gem.add_development_dependency "rspec-rails" gem.add_development_dependency "timecop" end ================================================ FILE: gemfiles/rails_7_0.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "rails", "~> 7.0.0" gem "rspec-core" gem "rspec-expectations" gem "rspec-mocks" gem "rspec-rails", "~> 5.0" gem "rspec-support" gem "rubocop", "~> 1.4" gem "rubocop-performance", require: false gem "rubocop-rails", require: false gem "rubocop-rspec", require: false gem "bcrypt", "~> 3.1", require: false gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "sprockets-rails" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw] gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] gem "timecop" gem "base64" gem "drb" gem "mutex_m" gem "concurrent-ruby", "1.3.4" gemspec path: "../" ================================================ FILE: gemfiles/rails_7_1.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "rails", "~> 7.1.0" gem "rspec-core" gem "rspec-expectations" gem "rspec-mocks" gem "rspec-rails", "~> 7.1" gem "rspec-support" gem "rubocop", "~> 1.4" gem "rubocop-performance", require: false gem "rubocop-rails", require: false gem "rubocop-rspec", require: false gem "bcrypt", "~> 3.1", require: false gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "sprockets-rails" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw] gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] gem "timecop" gemspec path: "../" ================================================ FILE: gemfiles/rails_7_2.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "rails", "~> 7.2.0" gem "rspec-core" gem "rspec-expectations" gem "rspec-mocks" gem "rspec-rails", "~> 7.1" gem "rspec-support" gem "rubocop", "~> 1.4" gem "rubocop-performance", require: false gem "rubocop-rails", require: false gem "rubocop-rspec", require: false gem "bcrypt", "~> 3.1", require: false gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "sprockets-rails" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw] gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] gem "timecop" gemspec path: "../" ================================================ FILE: gemfiles/rails_8_0.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "rails", "~> 8.0.0" gem "rspec-core" gem "rspec-expectations" gem "rspec-mocks" gem "rspec-rails", "~> 7.1" gem "rspec-support" gem "rubocop", "~> 1.4" gem "rubocop-performance", require: false gem "rubocop-rails", require: false gem "rubocop-rspec", require: false gem "bcrypt", "~> 3.1", require: false gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "sprockets-rails" gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw] gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] gem "timecop" gemspec path: "../" ================================================ FILE: gemfiles/rails_edge.gemfile ================================================ # This file was generated by Appraisal source "https://rubygems.org" gem "rails", git: "https://github.com/rails/rails" gem "rspec-core" gem "rspec-expectations" gem "rspec-mocks" gem "rspec-rails", "~> 7.1" gem "rspec-support" gem "rubocop", "~> 1.4" gem "rubocop-performance", require: false gem "rubocop-rails", require: false gem "rubocop-rspec", require: false gem "bcrypt", "~> 3.1", require: false gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "sprockets-rails" gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw] gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] gem "timecop" gemspec path: "../" ================================================ FILE: lib/doorkeeper/config/abstract_builder.rb ================================================ # frozen_string_literal: true module Doorkeeper class Config # Abstract base class for Doorkeeper and it's extensions configuration # builder. Instantiates and validates gem configuration. # class AbstractBuilder attr_reader :config # @param [Class] config class # def initialize(config = Config.new, &block) @config = config instance_eval(&block) if block_given? end # Builds and validates configuration. # # @return [Doorkeeper::Config] config instance # def build @config.validate! if @config.respond_to?(:validate!) @config end end end end ================================================ FILE: lib/doorkeeper/config/option.rb ================================================ # frozen_string_literal: true module Doorkeeper class Config # Doorkeeper configuration option DSL module Option # Defines configuration option # # When you call option, it defines two methods. One method will take place # in the +Config+ class and the other method will take place in the # +Builder+ class. # # The +name+ parameter will set both builder method and config attribute. # If the +:as+ option is defined, the builder method will be the specified # option while the config attribute will be the +name+ parameter. # # If you want to introduce another level of config DSL you can # define +builder_class+ parameter. # Builder should take a block as the initializer parameter and respond to function +build+ # that returns the value of the config attribute. # # ==== Options # # * [:+as+] Set the builder method that goes inside +configure+ block # * [+:default+] The default value in case no option was set # * [+:builder_class+] Configuration option builder class # # ==== Examples # # option :name # option :name, as: :set_name # option :name, default: 'My Name' # option :scopes builder_class: ScopesBuilder # def option(name, options = {}) attribute = options[:as] || name attribute_builder = options[:builder_class] builder_class.instance_eval do if method_defined?(name) Kernel.warn "[DOORKEEPER] Option #{name} already defined and will be overridden" remove_method name end define_method name do |*args, &block| if (deprecation_opts = options[:deprecated]) warning = "[DOORKEEPER] #{name} has been deprecated and will soon be removed" warning = "#{warning}\n#{deprecation_opts.fetch(:message)}" if deprecation_opts.is_a?(Hash) Kernel.warn(warning) end value = if attribute_builder attribute_builder.new(&block).build else block || args.first end @config.instance_variable_set(:"@#{attribute}", value) end end define_method attribute do |*_args| if instance_variable_defined?(:"@#{attribute}") instance_variable_get(:"@#{attribute}") else options[:default] end end public attribute end def self.extended(base) return if base.respond_to?(:builder_class) raise Doorkeeper::MissingConfigurationBuilderClass, "Define `self.builder_class` method " \ "for #{base} that returns your custom Builder class to use options DSL!" end end end end ================================================ FILE: lib/doorkeeper/config/validations.rb ================================================ # frozen_string_literal: true module Doorkeeper class Config # Doorkeeper configuration validator. # module Validations # Validates configuration options to be set properly. # def validate! validate_reuse_access_token_value validate_token_reuse_limit validate_secret_strategies validate_pkce_code_challenge_methods end private # Determine whether +reuse_access_token+ and a non-restorable # +token_secret_strategy+ have both been activated. # # In that case, disable reuse_access_token value and warn the user. def validate_reuse_access_token_value strategy = token_secret_strategy return if !reuse_access_token || strategy.allows_restoring_secrets? ::Rails.logger.warn( "[DOORKEEPER] You have configured both reuse_access_token " \ "AND '#{strategy}' strategy which cannot restore tokens. " \ "This combination is unsupported. reuse_access_token will be disabled", ) @reuse_access_token = false end # Validate that the provided strategies are valid for # tokens and applications def validate_secret_strategies token_secret_strategy.validate_for(:token) application_secret_strategy.validate_for(:application) end def validate_token_reuse_limit return if !reuse_access_token || (token_reuse_limit > 0 && token_reuse_limit <= 100) ::Rails.logger.warn( "[DOORKEEPER] You have configured an invalid value for token_reuse_limit option. " \ "It will be set to default 100", ) @token_reuse_limit = 100 end def validate_pkce_code_challenge_methods return if pkce_code_challenge_methods.all? {|method| method =~ /^plain$|^S256$/ } ::Rails.logger.warn( "[DOORKEEPER] You have configured an invalid value for pkce_code_challenge_methods option. " \ "It will be set to default ['plain', 'S256']", ) @pkce_code_challenge_methods = ['plain', 'S256'] end end end end ================================================ FILE: lib/doorkeeper/config.rb ================================================ # frozen_string_literal: true require "doorkeeper/config/abstract_builder" require "doorkeeper/config/option" require "doorkeeper/config/validations" module Doorkeeper # Doorkeeper option DSL could be reused in extensions to build their own # configurations. To use the Option DSL gems need to define `builder_class` method # that returns configuration Builder class. This exception raises when they don't # define it. # class Config # Default Doorkeeper configuration builder class Builder < AbstractBuilder # Provide support for an owner to be assigned to each registered # application (disabled by default) # Optional parameter confirmation: true (default false) if you want # to enforce ownership of a registered application # # @param opts [Hash] the options to confirm if an application owner # is present # @option opts[Boolean] :confirmation (false) # Set confirm_application_owner variable def enable_application_owner(opts = {}) @config.instance_variable_set(:@enable_application_owner, true) confirm_application_owner if opts[:confirmation].present? && opts[:confirmation] end def confirm_application_owner @config.instance_variable_set(:@confirm_application_owner, true) end # Provide support for dynamic scopes (e.g. user:*) (disabled by default) # Optional parameter delimiter (default ":") if you want to customize # the delimiter separating the scope name and matching value. # # @param opts [Hash] the options to configure dynamic scopes def enable_dynamic_scopes(opts = {}) @config.instance_variable_set(:@enable_dynamic_scopes, true) @config.instance_variable_set(:@dynamic_scopes_delimiter, opts[:delimiter] || ':') end # Define default access token scopes for your provider # # @param scopes [Array] Default set of access (OAuth::Scopes.new) # token scopes def default_scopes(*scopes) @config.instance_variable_set(:@default_scopes, OAuth::Scopes.from_array(scopes)) end # Define default access token scopes for your provider # # @param scopes [Array] Optional set of access (OAuth::Scopes.new) # token scopes def optional_scopes(*scopes) @config.instance_variable_set(:@optional_scopes, OAuth::Scopes.from_array(scopes)) end # Define scopes_by_grant_type to limit certain scope to certain grant_type # @param { Hash } with grant_types as keys. # Default set to {} i.e. no limitation on scopes usage def scopes_by_grant_type(hash = {}) @config.instance_variable_set(:@scopes_by_grant_type, hash) end # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then # falls back to the `:client_id` and `:client_secret` params from the # `params` object. # # @param methods [Array] Define client credentials def client_credentials(*methods) @config.instance_variable_set(:@client_credentials_methods, methods) end # Change the way access token is authenticated from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then # falls back to the `:access_token` or `:bearer_token` params from the # `params` object. # # @param methods [Array] Define access token methods def access_token_methods(*methods) @config.instance_variable_set(:@access_token_methods, methods) end # Issue access tokens with refresh token (disabled if not set) def use_refresh_token(enabled = true, &block) @config.instance_variable_set( :@refresh_token_enabled, block || enabled, ) end # Reuse access token for the same resource owner within an application # (disabled by default) # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 def reuse_access_token @config.instance_variable_set(:@reuse_access_token, true) end # Enable support for multiple database configurations with read replicas. # When enabled, wraps database write operations to ensure they use the primary # (writable) database when automatic role switching is enabled. # # For ActiveRecord (Rails 6.1+), this uses `ActiveRecord::Base.connected_to(role: :writing)`. # Other ORM extensions can implement their own primary database targeting logic. # # This prevents `ActiveRecord::ReadOnlyError` when using read replicas with Rails # automatic role switching. Enable this if your application uses multiple databases # with automatic role switching for read replicas. # # See: https://guides.rubyonrails.org/active_record_multiple_databases.html#activating-automatic-role-switching def enable_multiple_database_roles @config.instance_variable_set(:@enable_multiple_database_roles, true) end # Choose to use the url path for native autorization codes # Enabling this flag sets the authorization code response route for # native redirect uris to oauth/authorize/. The default is # oauth/authorize/native?code=. # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1143 def use_url_path_for_native_authorization @config.instance_variable_set(:@use_url_path_for_native_authorization, true) end # TODO: maybe make it more generic for other flows too? # Only allow one valid access token obtained via client credentials # per client. If a new access token is obtained before the old one # expired, the old one gets revoked (disabled by default) def revoke_previous_client_credentials_token @config.instance_variable_set(:@revoke_previous_client_credentials_token, true) end # Only allow one valid access token obtained via authorization code # per client. If a new access token is obtained before the old one # expired, the old one gets revoked (disabled by default) def revoke_previous_authorization_code_token @config.instance_variable_set(:@revoke_previous_authorization_code_token, true) end # Require non-confidential apps to use PKCE (send a code_verifier) when requesting # an access_token using an authorization code (disabled by default) def force_pkce @config.instance_variable_set(:@force_pkce, true) end # Use an API mode for applications generated with --api argument # It will skip applications controller, disable forgery protection def api_only @config.instance_variable_set(:@api_only, true) end # Enables polymorphic Resource Owner association for Access Grant and # Access Token models. Requires additional database columns to be setup. def use_polymorphic_resource_owner @config.instance_variable_set(:@polymorphic_resource_owner, true) end # Forbids creating/updating applications with arbitrary scopes that are # not in configuration, i.e. `default_scopes` or `optional_scopes`. # (disabled by default) def enforce_configured_scopes @config.instance_variable_set(:@enforce_configured_scopes, true) end # Enforce request content type as the spec requires: # disabled by default for backward compatibility. def enforce_content_type @config.instance_variable_set(:@enforce_content_type, true) end # Allow optional hashing of input tokens before persisting them. # Will be used for hashing of input token and grants. # # @param using # Provide a different secret storage implementation class for tokens # @param fallback # Provide a fallback secret storage implementation class for tokens # or use :plain to fallback to plain tokens def hash_token_secrets(using: nil, fallback: nil) default = "::Doorkeeper::SecretStoring::Sha256Hash" configure_secrets_for :token, using: using || default, fallback: fallback end # Allow optional hashing of application secrets before persisting them. # Will be used for hashing of input token and grants. # # @param using # Provide a different secret storage implementation for applications # @param fallback # Provide a fallback secret storage implementation for applications # or use :plain to fallback to plain application secrets def hash_application_secrets(using: nil, fallback: nil) default = "::Doorkeeper::SecretStoring::Sha256Hash" configure_secrets_for :application, using: using || default, fallback: fallback end private # Configure the secret storing functionality def configure_secrets_for(type, using:, fallback:) raise ArgumentError, "Invalid type #{type}" if %i[application token].exclude?(type) @config.instance_variable_set(:"@#{type}_secret_strategy", using.constantize) if fallback.nil? return elsif fallback.to_sym == :plain fallback = "::Doorkeeper::SecretStoring::Plain" end @config.instance_variable_set(:"@#{type}_secret_fallback_strategy", fallback.constantize) end end # Replace with `default: Builder` when we drop support of Rails < 5.2 mattr_reader(:builder_class) { Builder } extend Option include Validations option :resource_owner_authenticator, as: :authenticate_resource_owner, default: (lambda do |_routes| ::Rails.logger.warn( I18n.t("doorkeeper.errors.messages.resource_owner_authenticator_not_configured"), ) nil end) option :admin_authenticator, as: :authenticate_admin, default: (lambda do |_routes| ::Rails.logger.warn( I18n.t("doorkeeper.errors.messages.admin_authenticator_not_configured"), ) head :forbidden end) option :resource_owner_from_credentials, default: (lambda do |_routes| ::Rails.logger.warn( I18n.t("doorkeeper.errors.messages.credential_flow_not_configured"), ) nil end) # Hooks for authorization option :before_successful_authorization, default: ->(_controller, _context = nil) {} option :after_successful_authorization, default: ->(_controller, _context = nil) {} # Hooks for strategies responses option :before_successful_strategy_response, default: ->(_request) {} option :after_successful_strategy_response, default: ->(_request, _response) {} # Allows to customize Token Introspection response option :custom_introspection_response, default: ->(_token, _context) { {} } option :skip_authorization, default: ->(_routes) {} option :access_token_expires_in, default: 7200 option :custom_access_token_expires_in, default: ->(_context) { nil } option :authorization_code_expires_in, default: 600 option :orm, default: :active_record option :native_redirect_uri, default: "urn:ietf:wg:oauth:2.0:oob", deprecated: true option :grant_flows, default: %w[authorization_code client_credentials] option :pkce_code_challenge_methods, default: %w[plain S256] option :handle_auth_errors, default: :render option :token_lookup_batch_size, default: 10_000 # Sets the token_reuse_limit # It will be used only when reuse_access_token option in enabled # By default it will be 100 # It will be used for token reusablity to some threshold percentage # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 option :token_reuse_limit, default: 100 # Don't require client authentication for password grants. If client credentials # are present they will still be validated, and the grant rejected if the credentials # are invalid. # # This is discouraged. Spec says that password grants always require a client. # # See https://github.com/doorkeeper-gem/doorkeeper/issues/1412#issuecomment-632750422 # and https://github.com/doorkeeper-gem/doorkeeper/pull/1420 # # Since many applications use this unsafe behavior in the wild, this is kept as a # not-recommended option. You should be aware that you are not following the OAuth # spec, and understand the security implications of doing so. option :skip_client_authentication_for_password_grant, default: false # Hook to allow arbitrary user-client authorization option :authorize_resource_owner_for_client, default: ->(_client, _resource_owner) { true } # Allows to customize OAuth grant flows that +each+ application support. # You can configure a custom block (or use a class respond to `#call`) that must # return `true` in case Application instance supports requested OAuth grant flow # during the authorization request to the server. This configuration +doesn't+ # set flows per application, it only allows to check if application supports # specific grant flow. # # For example you can add an additional database column to `oauth_applications` table, # say `t.array :grant_flows, default: []`, and store allowed grant flows that can # be used with this application there. Then when authorization requested Doorkeeper # will call this block to check if specific Application (passed with client_id and/or # client_secret) is allowed to perform the request for the specific grant type # (authorization, password, client_credentials, etc). # # Example of the block: # # ->(flow, client) { client.grant_flows.include?(flow) } # # In case this option invocation result is `false`, Doorkeeper server returns # :unauthorized_client error and stops the request. # # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call # @return [Boolean] `true` if allow or `false` if forbid the request # option :allow_grant_flow_for_client, default: ->(_grant_flow, _client) { true } # Allows to forbid specific Application redirect URI's by custom rules. # Doesn't forbid any URI by default. # # @param forbid_redirect_uri [Proc] Block or any object respond to #call # option :forbid_redirect_uri, default: ->(_uri) { false } # WWW-Authenticate Realm (default "Doorkeeper"). # # @param realm [String] ("Doorkeeper") Authentication realm # option :realm, default: "Doorkeeper" # Forces the usage of the HTTPS protocol in non-native redirect uris # (enabled by default in non-development environments). OAuth2 # delegates security in communication to the HTTPS protocol so it is # wise to keep this enabled. # # @param [Boolean] boolean_or_block value for the parameter, true by default in # non-development environment # # @yield [uri] Conditional usage of SSL redirect uris. # @yieldparam [URI] Redirect URI # @yieldreturn [Boolean] Indicates necessity of usage of the HTTPS protocol # in non-native redirect uris # option :force_ssl_in_redirect_uri, default: !Rails.env.development? # Use a custom class for generating the access token. # https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator # # @param access_token_generator [String] # the name of the access token generator class # option :access_token_generator, default: "Doorkeeper::OAuth::Helpers::UniqueToken" # Allows additional data to be received when granting access to an Application, and for this # additional data to be sent with subsequently generated access tokens. The access grant and # access token models will both need to respond to the specified attribute names. # # @param attributes [Array] The array of custom attribute names to be saved # option :custom_access_token_attributes, default: [] # Use a custom class for generating the application secret. # https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-application-secret-generator # # @param application_secret_generator [String] # the name of the application secret generator class # option :application_secret_generator, default: "Doorkeeper::OAuth::Helpers::UniqueToken" # Default access token generator is a SecureRandom class from Ruby stdlib. # This option defines which method will be used to generate a unique token value. # # @param default_generator_method [Symbol] # the method name of the default access token generator # option :default_generator_method, default: :urlsafe_base64 # The controller Doorkeeper::ApplicationController inherits from. # Defaults to ActionController::Base. # https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-controllers # # @param base_controller [String] the name of the base controller option :base_controller, default: (lambda do api_only ? "ActionController::API" : "ActionController::Base" end) # The controller Doorkeeper::ApplicationMetalController inherits from. # Defaults to ActionController::API. # # @param base_metal_controller [String] the name of the base controller option :base_metal_controller, default: "ActionController::API" option :access_token_class, default: "Doorkeeper::AccessToken" option :access_grant_class, default: "Doorkeeper::AccessGrant" option :application_class, default: "Doorkeeper::Application" # Allows to set blank redirect URIs for Applications in case # server configured to use URI-less grant flows. # option :allow_blank_redirect_uri, default: (lambda do |grant_flows, _application| grant_flows.exclude?("authorization_code") && grant_flows.exclude?("implicit") end) # Configure protection of token introspection request. # By default this configuration allows to introspect a token by # another token of the same application, or to introspect the token # that belongs to authorized client, or access token has been introspected # is a public one (doesn't belong to any client) # # You can define any custom rule you need or just disable token # introspection at all. # # @param token [Doorkeeper::AccessToken] # token to be introspected # # @param authorized_client [Doorkeeper::Application] # authorized client (if request is authorized using Basic auth with # Client Credentials for example) # # @param authorized_token [Doorkeeper::AccessToken] # Bearer token used to authorize the request # option :allow_token_introspection, default: (lambda do |token, authorized_client, authorized_token| if authorized_token authorized_token.application == token&.application elsif token&.application authorized_client == token.application else true end end) attr_reader :reuse_access_token, :enable_multiple_database_roles, :token_secret_fallback_strategy, :application_secret_fallback_strategy def clear_cache! %i[ application_model access_token_model access_grant_model ].each do |var| remove_instance_variable("@#{var}") if instance_variable_defined?("@#{var}") end end # Doorkeeper Access Token model class. # # @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model] # def access_token_model @access_token_model ||= access_token_class.constantize end # Doorkeeper Access Grant model class. # # @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model] # def access_grant_model @access_grant_model ||= access_grant_class.constantize end # Doorkeeper Application model class. # # @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model] # def application_model @application_model ||= application_class.constantize end def api_only @api_only ||= false end def enforce_content_type @enforce_content_type ||= false end def refresh_token_enabled? if defined?(@refresh_token_enabled) @refresh_token_enabled else false end end def resolve_controller(name) config_option = public_send(:"#{name}_controller") controller_name = if config_option.respond_to?(:call) instance_exec(&config_option) else config_option end controller_name.constantize end def revoke_previous_client_credentials_token? option_set? :revoke_previous_client_credentials_token end def revoke_previous_authorization_code_token? option_set? :revoke_previous_authorization_code_token end def force_pkce? option_set? :force_pkce end def enforce_configured_scopes? option_set? :enforce_configured_scopes end def enable_application_owner? option_set? :enable_application_owner end def enable_dynamic_scopes? option_set? :enable_dynamic_scopes end def dynamic_scopes_delimiter @dynamic_scopes_delimiter end def polymorphic_resource_owner? option_set? :polymorphic_resource_owner end def confirm_application_owner? option_set? :confirm_application_owner end def raise_on_errors? handle_auth_errors == :raise end def redirect_on_errors? handle_auth_errors == :redirect end def application_secret_hashed? instance_variable_defined?(:"@application_secret_strategy") end def token_secret_strategy @token_secret_strategy ||= ::Doorkeeper::SecretStoring::Plain end def application_secret_strategy @application_secret_strategy ||= ::Doorkeeper::SecretStoring::Plain end def default_scopes @default_scopes ||= OAuth::Scopes.new end def optional_scopes @optional_scopes ||= OAuth::Scopes.new end def scopes @scopes ||= default_scopes + optional_scopes end def scopes_by_grant_type @scopes_by_grant_type ||= {} end def pkce_code_challenge_methods_supported return [] unless access_grant_model.pkce_supported? pkce_code_challenge_methods end def client_credentials_methods @client_credentials_methods ||= %i[from_basic from_params] end def access_token_methods @access_token_methods ||= %i[ from_bearer_authorization from_access_token_param from_bearer_param ] end def enabled_grant_flows @enabled_grant_flows ||= calculate_grant_flows.map { |name| Doorkeeper::GrantFlow.get(name) }.compact end def authorization_response_flows @authorization_response_flows ||= enabled_grant_flows.select(&:handles_response_type?) + deprecated_authorization_flows end def token_grant_flows @token_grant_flows ||= calculate_token_grant_flows end def authorization_response_types authorization_response_flows.map(&:response_type_matches) end def token_grant_types token_grant_flows.map(&:grant_type_matches) end # [NOTE]: deprecated and will be removed soon def deprecated_token_grant_types_resolver @deprecated_token_grant_types ||= calculate_token_grant_types end def native_authorization_code_route @use_url_path_for_native_authorization = false unless defined?(@use_url_path_for_native_authorization) @use_url_path_for_native_authorization ? '/:code' : '/native' end # [NOTE]: deprecated and will be removed soon def deprecated_authorization_flows response_types = calculate_authorization_response_types if response_types.any? ::Kernel.warn <<~WARNING Please, don't patch Doorkeeper::Config#calculate_authorization_response_types method. Register your custom grant flows using the public API: `Doorkeeper::GrantFlow.register(grant_flow_name, **options)`. WARNING end response_types.map do |response_type| Doorkeeper::GrantFlow::FallbackFlow.new(response_type, response_type_matches: response_type) end end # [NOTE]: deprecated and will be removed soon def calculate_authorization_response_types [] end # [NOTE]: deprecated and will be removed soon def calculate_token_grant_types types = grant_flows - ["implicit"] types << "refresh_token" if refresh_token_enabled? types end # Calculates grant flows configured by the user in Doorkeeper # configuration considering registered aliases that is exposed # to single or multiple other flows. # def calculate_grant_flows configured_flows = grant_flows.map(&:to_s) aliases = Doorkeeper::GrantFlow.aliases.keys.map(&:to_s) flows = configured_flows - aliases aliases.each do |flow_alias| next unless configured_flows.include?(flow_alias) flows.concat(Doorkeeper::GrantFlow.expand_alias(flow_alias)) end flows.flatten.uniq end def allow_blank_redirect_uri?(application = nil) if allow_blank_redirect_uri.respond_to?(:call) allow_blank_redirect_uri.call(grant_flows, application) else allow_blank_redirect_uri end end def allow_grant_flow_for_client?(grant_flow, client) return true unless option_defined?(:allow_grant_flow_for_client) allow_grant_flow_for_client.call(grant_flow, client) end def option_defined?(name) instance_variable_defined?("@#{name}") end private # Helper to read boolearized configuration option def option_set?(instance_key) var = instance_variable_get("@#{instance_key}") !!(defined?(var) && var) end def calculate_token_grant_flows flows = enabled_grant_flows.select(&:handles_grant_type?) flows << Doorkeeper::GrantFlow.get("refresh_token") if refresh_token_enabled? flows end end end ================================================ FILE: lib/doorkeeper/engine.rb ================================================ # frozen_string_literal: true module Doorkeeper class Engine < Rails::Engine initializer "doorkeeper.params.filter", after: :load_config_initializers do |app| app.config.to_prepare do Doorkeeper.setup_filter_parameters end end initializer "doorkeeper.routes" do Doorkeeper::Rails::Routes.install! end initializer "doorkeeper.helpers" do ActiveSupport.on_load(:action_controller) do include Doorkeeper::Rails::Helpers end end config.to_prepare do Doorkeeper.run_orm_hooks end if defined?(Sprockets) && Sprockets::VERSION.chr.to_i >= 4 initializer "doorkeeper.assets.precompile" do |app| # Force users to use: # //= link doorkeeper/admin/application.css # in Doorkeeper 5 for Sprockets 4 instead of precompile. # Add note to official docs & Wiki app.config.assets.precompile += %w[ doorkeeper/application.css doorkeeper/admin/application.css ] end end end end ================================================ FILE: lib/doorkeeper/errors.rb ================================================ # frozen_string_literal: true module Doorkeeper module Errors class DoorkeeperError < StandardError def type message end def self.translate_options {} end end class InvalidGrantReuse < DoorkeeperError def type :invalid_grant end end class InvalidTokenStrategy < DoorkeeperError def type :unsupported_grant_type end end class MissingRequiredParameter < DoorkeeperError attr_reader :missing_param def initialize(missing_param) super @missing_param = missing_param end def type :invalid_request end end class BaseResponseError < DoorkeeperError attr_reader :response def initialize(response) @response = response end def self.name_for_response self.name.demodulize.underscore.to_sym end end class InvalidCodeChallengeMethod < BaseResponseError def self.translate_options challenge_methods = Doorkeeper.config.pkce_code_challenge_methods_supported { challenge_methods: challenge_methods.join(", "), count: challenge_methods.length } end end UnableToGenerateToken = Class.new(DoorkeeperError) TokenGeneratorNotFound = Class.new(DoorkeeperError) NoOrmCleaner = Class.new(DoorkeeperError) InvalidRequest = Class.new(BaseResponseError) InvalidToken = Class.new(BaseResponseError) InvalidClient = Class.new(BaseResponseError) InvalidScope = Class.new(BaseResponseError) InvalidRedirectUri = Class.new(BaseResponseError) InvalidGrant = Class.new(BaseResponseError) UnauthorizedClient = Class.new(BaseResponseError) UnsupportedResponseType = Class.new(BaseResponseError) UnsupportedResponseMode = Class.new(BaseResponseError) AccessDenied = Class.new(BaseResponseError) ServerError = Class.new(BaseResponseError) TokenExpired = Class.new(InvalidToken) TokenRevoked = Class.new(InvalidToken) TokenUnknown = Class.new(InvalidToken) TokenForbidden = Class.new(InvalidToken) end end ================================================ FILE: lib/doorkeeper/grant_flow/fallback_flow.rb ================================================ # frozen_string_literal: true module Doorkeeper module GrantFlow class FallbackFlow < Flow def handles_grant_type? false end def handles_response_type? false end end end end ================================================ FILE: lib/doorkeeper/grant_flow/flow.rb ================================================ # frozen_string_literal: true module Doorkeeper module GrantFlow class Flow attr_reader :name, :grant_type_matches, :grant_type_strategy, :response_type_matches, :response_type_strategy, :response_mode_matches def initialize(name, **options) @name = name @grant_type_matches = options[:grant_type_matches] @grant_type_strategy = options[:grant_type_strategy] @response_type_matches = options[:response_type_matches] @response_type_strategy = options[:response_type_strategy] @response_mode_matches = options[:response_mode_matches] end def handles_grant_type? grant_type_matches.present? end def handles_response_type? response_type_matches.present? end def matches_grant_type?(value) grant_type_matches === value end def matches_response_type?(value) response_type_matches === value end def default_response_mode response_mode_matches[0] end def matches_response_mode?(value) response_mode_matches.include?(value) end end end end ================================================ FILE: lib/doorkeeper/grant_flow/registry.rb ================================================ # frozen_string_literal: true module Doorkeeper module GrantFlow module Registry mattr_accessor :flows self.flows = {} mattr_accessor :aliases self.aliases = {} # Allows to register custom OAuth grant flow so that Doorkeeper # could recognize and process it. # def register(name_or_flow, **options) unless name_or_flow.is_a?(Doorkeeper::GrantFlow::Flow) name_or_flow = Flow.new(name_or_flow, **options) end flow_key = name_or_flow.name.to_sym if flows.key?(flow_key) ::Kernel.warn <<~WARNING [DOORKEEPER] '#{flow_key}' grant flow already registered and will be overridden in #{caller(1..1).first} WARNING end flows[flow_key] = name_or_flow end # Allows to register aliases that could be used in `grant_flows` # configuration option. It is possible to have aliases like 1:1 or # 1:N, i.e. "implicit_oidc" => ['token', 'id_token', 'id_token token']. # def register_alias(alias_name, **options) aliases[alias_name.to_sym] = Array.wrap(options.fetch(:as)) end def expand_alias(alias_name) aliases.fetch(alias_name.to_sym, []) end # [NOTE]: make it to use #fetch after removing fallbacks def get(name) flows[name.to_sym] end end end end ================================================ FILE: lib/doorkeeper/grant_flow.rb ================================================ # frozen_string_literal: true require "doorkeeper/grant_flow/flow" require "doorkeeper/grant_flow/fallback_flow" require "doorkeeper/grant_flow/registry" module Doorkeeper module GrantFlow extend Registry register( :implicit, response_type_matches: "token", response_mode_matches: %w[fragment form_post], response_type_strategy: Doorkeeper::Request::Token, ) register( :authorization_code, response_type_matches: "code", response_mode_matches: %w[query fragment form_post], response_type_strategy: Doorkeeper::Request::Code, grant_type_matches: "authorization_code", grant_type_strategy: Doorkeeper::Request::AuthorizationCode, ) register( :client_credentials, grant_type_matches: "client_credentials", grant_type_strategy: Doorkeeper::Request::ClientCredentials, ) register( :password, grant_type_matches: "password", grant_type_strategy: Doorkeeper::Request::Password, ) register( :refresh_token, grant_type_matches: "refresh_token", grant_type_strategy: Doorkeeper::Request::RefreshToken, ) end end ================================================ FILE: lib/doorkeeper/grape/authorization_decorator.rb ================================================ # frozen_string_literal: true module Doorkeeper module Grape class AuthorizationDecorator < SimpleDelegator def parameters params end def authorization env = __getobj__.env env["HTTP_AUTHORIZATION"] || env["X-HTTP_AUTHORIZATION"] || env["X_HTTP_AUTHORIZATION"] || env["REDIRECT_X_HTTP_AUTHORIZATION"] end end end end ================================================ FILE: lib/doorkeeper/grape/helpers.rb ================================================ # frozen_string_literal: true require "doorkeeper/grape/authorization_decorator" module Doorkeeper module Grape # Doorkeeper helpers for Grape applications. # Provides helpers for endpoints authorization based on defined set of scopes. module Helpers # These helpers are for grape >= 0.10 extend ::Grape::API::Helpers include Doorkeeper::Rails::Helpers # endpoint specific scopes > parameter scopes > default scopes def doorkeeper_authorize!(*scopes) endpoint_scopes = endpoint.route_setting(:scopes) || endpoint.options[:route_options][:scopes] scopes = if endpoint_scopes Doorkeeper::OAuth::Scopes.from_array(endpoint_scopes) elsif scopes.present? Doorkeeper::OAuth::Scopes.from_array(scopes) end super(*scopes) end def doorkeeper_render_error_with(error) status_code = error_status_codes[error.status] error!({ error: error.description }, status_code, error.headers) end private def endpoint env["api.endpoint"] end def doorkeeper_token @doorkeeper_token ||= OAuth::Token.authenticate( decorated_request, *Doorkeeper.config.access_token_methods, ) end def decorated_request AuthorizationDecorator.new(request) end def error_status_codes { unauthorized: 401, forbidden: 403, } end end end end ================================================ FILE: lib/doorkeeper/helpers/controller.rb ================================================ # frozen_string_literal: true # Define methods that can be called in any controller that inherits from # Doorkeeper::ApplicationMetalController or Doorkeeper::ApplicationController module Doorkeeper module Helpers # Rails controller helpers. # module Controller def self.included(base) base.helper_method :current_resource_owner if base.respond_to?(:helper_method) end private # :doc: def authenticate_resource_owner! current_resource_owner end # :doc: def current_resource_owner return @current_resource_owner if defined?(@current_resource_owner) @current_resource_owner ||= begin instance_eval(&Doorkeeper.config.authenticate_resource_owner) end end def resource_owner_from_credentials instance_eval(&Doorkeeper.config.resource_owner_from_credentials) end # :doc: def authenticate_admin! instance_eval(&Doorkeeper.config.authenticate_admin) end def server @server ||= Server.new(self) end # :doc: def doorkeeper_token return @doorkeeper_token if defined?(@doorkeeper_token) @doorkeeper_token ||= OAuth::Token.authenticate(request, *config_methods) end def config_methods @config_methods ||= Doorkeeper.config.access_token_methods end def get_error_response_from_exception(exception) if exception.respond_to?(:response) exception.response elsif exception.type == :invalid_request OAuth::InvalidRequestResponse.new( name: exception.type, state: params[:state], missing_param: exception.missing_param, ) else OAuth::ErrorResponse.new(name: exception.type, state: params[:state]) end end def handle_token_exception(exception) error = get_error_response_from_exception(exception) headers.merge!(error.headers) self.response_body = error.body.to_json self.status = error.status end def skip_authorization? !!instance_exec( [server.current_resource_owner, @pre_auth.client], &Doorkeeper.config.skip_authorization ) end def enforce_content_type if (request.put? || request.post? || request.patch?) && !x_www_form_urlencoded? render json: {}, status: :unsupported_media_type end end def x_www_form_urlencoded? request.media_type == "application/x-www-form-urlencoded" end end end end ================================================ FILE: lib/doorkeeper/models/access_grant_mixin.rb ================================================ # frozen_string_literal: true module Doorkeeper module AccessGrantMixin extend ActiveSupport::Concern include OAuth::Helpers include Models::Expirable include Models::Revocable include Models::Accessible include Models::Orderable include Models::SecretStorable include Models::Scopes include Models::ResourceOwnerable include Models::Concerns::WriteToPrimary include Models::ExpirationTimeSqlMath # Never uses PKCE if PKCE migrations were not generated def uses_pkce? self.class.pkce_supported? && code_challenge.present? end module ClassMethods # Searches for Doorkeeper::AccessGrant record with the # specific token value. # # @param token [#to_s] token value (any object that responds to `#to_s`) # # @return [Doorkeeper::AccessGrant, nil] # AccessGrant object or nil if there is no record with such token # def by_token(token) find_by_plaintext_token(:token, token) end # Revokes AccessGrant records that have not been revoked and associated # with the specific Application and Resource Owner. # # @param application_id [Integer] # ID of the Application # @param resource_owner [ActiveRecord::Base, Integer] # instance of the Resource Owner model or it's ID # def revoke_all_for(application_id, resource_owner, clock = Time) with_primary_role do by_resource_owner(resource_owner) .where( application_id: application_id, revoked_at: nil, ) .update_all(revoked_at: clock.now.utc) end end # Implements PKCE code_challenge encoding without base64 padding as described in the spec. # https://datatracker.ietf.org/doc/html/rfc7636#appendix-A # Appendix A. Notes on Implementing Base64url Encoding without Padding # # This appendix describes how to implement a base64url-encoding # function without padding, based upon the standard base64-encoding # function that uses padding. # # To be concrete, example C# code implementing these functions is shown # below. Similar code could be used in other languages. # # static string base64urlencode(byte [] arg) # { # string s = Convert.ToBase64String(arg); // Regular base64 encoder # s = s.Split('=')[0]; // Remove any trailing '='s # s = s.Replace('+', '-'); // 62nd char of encoding # s = s.Replace('/', '_'); // 63rd char of encoding # return s; # } # # An example correspondence between unencoded and encoded values # follows. The octet sequence below encodes into the string below, # which when decoded, reproduces the octet sequence. # # 3 236 255 224 193 # # A-z_4ME # # https://ruby-doc.org/stdlib-2.1.3/libdoc/base64/rdoc/Base64.html#method-i-urlsafe_encode64 # # urlsafe_encode64(bin) # Returns the Base64-encoded version of bin. This method complies with # "Base 64 Encoding with URL and Filename Safe Alphabet" in RFC 4648. # The alphabet uses '-' instead of '+' and '_' instead of '/'. # @param code_verifier [#to_s] a one time use value (any object that responds to `#to_s`) # # @return [#to_s] An encoded code challenge based on the provided verifier # suitable for PKCE validation # def generate_code_challenge(code_verifier) Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false) end def pkce_supported? column_names.include?("code_challenge") end ## # Determines the secret storing transformer # Unless configured otherwise, uses the plain secret strategy # # @return [Doorkeeper::SecretStoring::Base] # def secret_strategy ::Doorkeeper.config.token_secret_strategy end ## # Determine the fallback storing strategy # Unless configured, there will be no fallback # # @return [Doorkeeper::SecretStoring::Base] # def fallback_secret_strategy ::Doorkeeper.config.token_secret_fallback_strategy end end end end ================================================ FILE: lib/doorkeeper/models/access_token_mixin.rb ================================================ # frozen_string_literal: true module Doorkeeper module AccessTokenMixin extend ActiveSupport::Concern include OAuth::Helpers include Models::Expirable include Models::Reusable include Models::Revocable include Models::Accessible include Models::Orderable include Models::SecretStorable include Models::Scopes include Models::ResourceOwnerable include Models::ExpirationTimeSqlMath include Models::Concerns::WriteToPrimary module ClassMethods # Returns an instance of the Doorkeeper::AccessToken with # specific plain text token value. # # @param token [#to_s] # Plain text token value (any object that responds to `#to_s`) # # @return [Doorkeeper::AccessToken, nil] AccessToken object or nil # if there is no record with such token # def by_token(token) find_by_plaintext_token(:token, token) end # Returns an instance of the Doorkeeper::AccessToken # with specific token value. # # @param refresh_token [#to_s] # refresh token value (any object that responds to `#to_s`) # # @return [Doorkeeper::AccessToken, nil] AccessToken object or nil # if there is no record with such refresh token # def by_refresh_token(refresh_token) find_by_plaintext_token(:refresh_token, refresh_token) end # Returns an instance of the Doorkeeper::AccessToken # found by previous refresh token. Keep in mind that value # of the previous_refresh_token isn't encrypted using # secrets strategy. # # @param previous_refresh_token [#to_s] # previous refresh token value (any object that responds to `#to_s`) # # @return [Doorkeeper::AccessToken, nil] AccessToken object or nil # if there is no record with such refresh token # def by_previous_refresh_token(previous_refresh_token) find_by(refresh_token: previous_refresh_token) end # Revokes AccessToken records that have not been revoked and associated # with the specific Application and Resource Owner. # # @param application_id [Integer] # ID of the Application # @param resource_owner [ActiveRecord::Base, Integer] # instance of the Resource Owner model or it's ID # def revoke_all_for(application_id, resource_owner, clock = Time) with_primary_role do by_resource_owner(resource_owner) .where( application_id: application_id, revoked_at: nil, ) .update_all(revoked_at: clock.now.utc) end end # Looking for not revoked Access Token with a matching set of scopes # that belongs to specific Application and Resource Owner. # # @param application [Doorkeeper::Application] # Application instance # @param resource_owner [ActiveRecord::Base, Integer] # Resource Owner model instance or it's ID # @param scopes [String, Doorkeeper::OAuth::Scopes] # set of scopes # @param custom_attributes [Nilable Hash] # A nil value, or hash with keys corresponding to the custom attributes # configured with the `custom_access_token_attributes` config option. # A nil value will ignore custom attributes. # # @return [Doorkeeper::AccessToken, nil] Access Token instance or # nil if matching record was not found # def matching_token_for(application, resource_owner, scopes, custom_attributes: nil, include_expired: true) tokens = authorized_tokens_for(application&.id, resource_owner) tokens = tokens.not_expired unless include_expired find_matching_token(tokens, application, custom_attributes, scopes) end # Interface to enumerate access token records in batches in order not # to bloat the memory. Could be overloaded in any ORM extension. # def find_access_token_in_batches(relation, **args, &block) relation.find_in_batches(**args, &block) end # Enumerates AccessToken records in batches to find a matching token. # Batching is required in order not to pollute the memory if Application # has huge amount of associated records. # # ActiveRecord 5.x - 6.x ignores custom ordering so we can't perform a # database sort by created_at, so we need to load all the matching records, # sort them and find latest one. # # @param relation [ActiveRecord::Relation] # Access tokens relation # @param application [Doorkeeper::Application] # Application instance # @param scopes [String, Doorkeeper::OAuth::Scopes] # set of scopes # @param custom_attributes [Nilable Hash] # A nil value, or hash with keys corresponding to the custom attributes # configured with the `custom_access_token_attributes` config option. # A nil value will ignore custom attributes. # # @return [Doorkeeper::AccessToken, nil] Access Token instance or # nil if matching record was not found # def find_matching_token(relation, application, custom_attributes, scopes) return nil unless relation matching_tokens = [] batch_size = Doorkeeper.configuration.token_lookup_batch_size find_access_token_in_batches(relation, batch_size: batch_size) do |batch| tokens = batch.select do |token| scopes_match?(token.scopes, scopes, application&.scopes) && custom_attributes_match?(token, custom_attributes) end matching_tokens.concat(tokens) end matching_tokens.max_by(&:created_at) end # Checks whether the token scopes match the scopes from the parameters # # @param token_scopes [#to_s] # set of scopes (any object that responds to `#to_s`) # @param param_scopes [Doorkeeper::OAuth::Scopes] # scopes from params # @param app_scopes [Doorkeeper::OAuth::Scopes] # Application scopes # # @return [Boolean] true if the param scopes match the token scopes, # and all the param scopes are defined in the application (or in the # server configuration if the application doesn't define any scopes), # and false in other cases # def scopes_match?(token_scopes, param_scopes, app_scopes) return true if token_scopes.empty? && param_scopes.empty? (token_scopes.sort == param_scopes.sort) && Doorkeeper::OAuth::Helpers::ScopeChecker.valid?( scope_str: param_scopes.to_s, server_scopes: Doorkeeper.config.scopes, app_scopes: app_scopes, ) end # Checks whether the token custom attribute values match the custom # attributes from the parameters. # # @param token [Doorkeeper::AccessToken] # The access token whose custom attributes are being compared # to the custom_attributes. # # @param custom_attributes [Hash] # A hash of the attributes for which we want to determine whether # the token's custom attributes match. # # @return [Boolean] true if the token's custom attribute values # match those in the custom_attributes, or if both are empty/blank. # False otherwise. def custom_attributes_match?(token, custom_attributes) return true if custom_attributes.nil? token_attribs = token.custom_attributes return true if token_attribs.blank? && custom_attributes.blank? Doorkeeper.config.custom_access_token_attributes.all? do |attribute| token_attribs[attribute] == custom_attributes[attribute] end end # Looking for not expired AccessToken record with a matching set of # scopes that belongs to specific Application and Resource Owner. # If it doesn't exists - then creates it. # # @param application [Doorkeeper::Application] # Application instance # @param resource_owner [ActiveRecord::Base, Integer] # Resource Owner model instance or it's ID # @param scopes [#to_s] # set of scopes (any object that responds to `#to_s`) # @param token_attributes [Hash] # Additional attributes to use when creating a token # @option token_attributes [Integer] :expires_in # token lifetime in seconds # @option token_attributes [Boolean] :use_refresh_token # whether to use the refresh token # # @return [Doorkeeper::AccessToken] existing record or a new one # def find_or_create_for(application:, resource_owner:, scopes:, **token_attributes) scopes = Doorkeeper::OAuth::Scopes.from_string(scopes) if scopes.is_a?(String) if Doorkeeper.config.reuse_access_token custom_attributes = extract_custom_attributes(token_attributes).presence access_token = matching_token_for( application, resource_owner, scopes, custom_attributes: custom_attributes, include_expired: false) return access_token if access_token&.reusable? end create_for( application: application, resource_owner: resource_owner, scopes: scopes, **token_attributes, ) end # Creates a not expired AccessToken record with a matching set of # scopes that belongs to specific Application and Resource Owner. # # @param application [Doorkeeper::Application] # Application instance # @param resource_owner [ActiveRecord::Base, Integer] # Resource Owner model instance or it's ID # @param scopes [#to_s] # set of scopes (any object that responds to `#to_s`) # @param token_attributes [Hash] # Additional attributes to use when creating a token # @option token_attributes [Integer] :expires_in # token lifetime in seconds # @option token_attributes [Boolean] :use_refresh_token # whether to use the refresh token # # @return [Doorkeeper::AccessToken] new access token # def create_for(application:, resource_owner:, scopes:, **token_attributes) token_attributes[:application] = application token_attributes[:scopes] = scopes.to_s if Doorkeeper.config.polymorphic_resource_owner? token_attributes[:resource_owner] = resource_owner else token_attributes[:resource_owner_id] = resource_owner_id_for(resource_owner) end with_primary_role do create!(token_attributes) end end # Looking for not revoked Access Token records that belongs to specific # Application and Resource Owner. # # @param application_id [Integer] # ID of the Application model instance # @param resource_owner [ActiveRecord::Base, Integer] # Resource Owner model instance or it's ID # # @return [ActiveRecord::Relation] # collection of matching AccessToken objects # def authorized_tokens_for(application_id, resource_owner) by_resource_owner(resource_owner).where( application_id: application_id, revoked_at: nil, ) end # Convenience method for backwards-compatibility, return the last # matching token for the given Application and Resource Owner. # # @param application_id [Integer] # ID of the Application model instance # @param resource_owner [ActiveRecord::Base, Integer] # ID of the Resource Owner model instance # # @return [Doorkeeper::AccessToken, nil] matching AccessToken object or # nil if nothing was found # def last_authorized_token_for(application_id, resource_owner) authorized_tokens_for(application_id, resource_owner) .ordered_by(:created_at, :desc) .first end ## # Determines the secret storing transformer # Unless configured otherwise, uses the plain secret strategy # # @return [Doorkeeper::SecretStoring::Base] # def secret_strategy ::Doorkeeper.config.token_secret_strategy end ## # Determine the fallback storing strategy # Unless configured, there will be no fallback def fallback_secret_strategy ::Doorkeeper.config.token_secret_fallback_strategy end # Extracts the token's custom attributes (defined by the # custom_access_token_attributes config option) from the token's attributes. # # @param attributes [Hash] # A hash of the access token's attributes. # @return [Hash] # A hash containing only the custom access token attributes. def extract_custom_attributes(attributes) attributes.with_indifferent_access.slice( *Doorkeeper.configuration.custom_access_token_attributes) end end # Access Token type: Bearer. # @see https://datatracker.ietf.org/doc/html/rfc6750 # The OAuth 2.0 Authorization Framework: Bearer Token Usage # def token_type "Bearer" end def use_refresh_token? @use_refresh_token ||= false !!@use_refresh_token end # JSON representation of the Access Token instance. # # @return [Hash] hash with token data def as_json(_options = {}) { resource_owner_id: resource_owner_id, scope: scopes, expires_in: expires_in_seconds, application: { uid: application.try(:uid) }, created_at: created_at.to_i, }.tap do |json| if Doorkeeper.configuration.polymorphic_resource_owner? json[:resource_owner_type] = resource_owner_type end end end # The token's custom attributes, as defined by # the custom_access_token_attributes config option. # # @return [Hash] hash of custom access token attributes. def custom_attributes self.class.extract_custom_attributes(attributes) end # Indicates whether the token instance have the same credential # as the other Access Token. # # @param access_token [Doorkeeper::AccessToken] other token # # @return [Boolean] true if credentials are same of false in other cases # def same_credential?(access_token) application_id == access_token.application_id && same_resource_owner?(access_token) end # Indicates whether the token instance have the same credential # as the other Access Token. # # @param access_token [Doorkeeper::AccessToken] other token # # @return [Boolean] true if credentials are same of false in other cases # def same_resource_owner?(access_token) if Doorkeeper.configuration.polymorphic_resource_owner? resource_owner == access_token.resource_owner else resource_owner_id == access_token.resource_owner_id end end # Indicates if token is acceptable for specific scopes. # # @param scopes [Array] scopes # # @return [Boolean] true if record is accessible and includes scopes or # false in other cases # def acceptable?(scopes) accessible? && includes_scope?(*scopes) end # We keep a volatile copy of the raw refresh token for initial communication # The stored refresh_token may be mapped and not available in cleartext. def plaintext_refresh_token if secret_strategy.allows_restoring_secrets? secret_strategy.restore_secret(self, :refresh_token) else @raw_refresh_token end end # We keep a volatile copy of the raw token for initial communication # The stored refresh_token may be mapped and not available in cleartext. # # Some strategies allow restoring stored secrets (e.g. symmetric encryption) # while hashing strategies do not, so you cannot rely on this value # returning a present value for persisted tokens. def plaintext_token if secret_strategy.allows_restoring_secrets? secret_strategy.restore_secret(self, :token) else @raw_token end end # Revokes token with `:refresh_token` equal to `:previous_refresh_token` # and clears `:previous_refresh_token` attribute. # def revoke_previous_refresh_token! return if !self.class.refresh_token_revoked_on_use? || previous_refresh_token.blank? old_refresh_token&.revoke if self.class.respond_to?(:with_primary_role) self.class.with_primary_role { update_attribute(:previous_refresh_token, "") } else update_attribute(:previous_refresh_token, "") end end private # Searches for Access Token record with `:refresh_token` equal to # `:previous_refresh_token` value. # # @return [Doorkeeper::AccessToken, nil] # Access Token record or nil if nothing found # def old_refresh_token @old_refresh_token ||= self.class.by_previous_refresh_token(previous_refresh_token) end # Generates refresh token with UniqueToken generator. # # @return [String] refresh token value # def generate_refresh_token @raw_refresh_token = UniqueToken.generate secret_strategy.store_secret(self, :refresh_token, @raw_refresh_token) end # Generates and sets the token value with the # configured Generator class (see Doorkeeper.config). # # @return [String] generated token value # # @raise [Doorkeeper::Errors::UnableToGenerateToken] # custom class doesn't implement .generate method # @raise [Doorkeeper::Errors::TokenGeneratorNotFound] # custom class doesn't exist # def generate_token self.created_at ||= Time.now.utc @raw_token = token_generator.generate(attributes_for_token_generator) secret_strategy.store_secret(self, :token, @raw_token) @raw_token end # Set of attributes that would be passed to token generator to # generate unique token based on them. # # @return [Hash] set of attributes # def attributes_for_token_generator { resource_owner_id: resource_owner_id, scopes: scopes, application: application, expires_in: expires_in, created_at: created_at, }.tap do |attributes| if Doorkeeper.config.polymorphic_resource_owner? attributes[:resource_owner] = resource_owner end Doorkeeper.config.custom_access_token_attributes.each do |attribute_name| attributes[attribute_name] = public_send(attribute_name) end end end def token_generator generator_name = Doorkeeper.config.access_token_generator generator = generator_name.constantize return generator if generator.respond_to?(:generate) raise Errors::UnableToGenerateToken, "#{generator} does not respond to `.generate`." rescue NameError raise Errors::TokenGeneratorNotFound, "#{generator_name} not found" end end end ================================================ FILE: lib/doorkeeper/models/application_mixin.rb ================================================ # frozen_string_literal: true module Doorkeeper module ApplicationMixin extend ActiveSupport::Concern include OAuth::Helpers include Models::Orderable include Models::SecretStorable include Models::Scopes # :nodoc module ClassMethods # Returns an instance of the Doorkeeper::Application with # specific UID and secret. # # Public/Non-confidential applications will only find by uid if secret is # blank. # # @param uid [#to_s] UID (any object that responds to `#to_s`) # @param secret [#to_s] secret (any object that responds to `#to_s`) # # @return [Doorkeeper::Application, nil] # Application instance or nil if there is no record with such credentials # def by_uid_and_secret(uid, secret) app = by_uid(uid) return unless app return app if secret.blank? && !app.confidential? return unless app.secret_matches?(secret) app end # Returns an instance of the Doorkeeper::Application with specific UID. # # @param uid [#to_s] UID (any object that responds to `#to_s`) # # @return [Doorkeeper::Application, nil] Application instance or nil # if there is no record with such UID # def by_uid(uid) find_by(uid: uid.to_s) end ## # Determines the secret storing transformer # Unless configured otherwise, uses the plain secret strategy def secret_strategy ::Doorkeeper.config.application_secret_strategy end ## # Determine the fallback storing strategy # Unless configured, there will be no fallback def fallback_secret_strategy ::Doorkeeper.config.application_secret_fallback_strategy end end # Set an application's valid redirect URIs. # # @param uris [String, Array] Newline-separated string or array the URI(s) # # @return [String] The redirect URI(s) separated by newlines. # def redirect_uri=(uris) super(uris.is_a?(Array) ? uris.join("\n") : uris) end # Check whether the given plain text secret matches our stored secret # # @param input [#to_s] Plain secret provided by user # (any object that responds to `#to_s`) # # @return [Boolean] Whether the given secret matches the stored secret # of this application. # def secret_matches?(input) # return false if either is nil, since secure_compare depends on strings # but Application secrets MAY be nil depending on confidentiality. return false if input.nil? || secret.nil? # When matching the secret by comparer function, all is well. return true if secret_strategy.secret_matches?(input, secret) # When fallback lookup is enabled, ensure applications # with plain secrets can still be found if fallback_secret_strategy fallback_secret_strategy.secret_matches?(input, secret) else false end end end end ================================================ FILE: lib/doorkeeper/models/concerns/accessible.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module Accessible # Indicates whether the object is accessible (not expired and not revoked). # # @return [Boolean] true if object accessible or false in other case # def accessible? !expired? && !revoked? end end end end ================================================ FILE: lib/doorkeeper/models/concerns/expirable.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module Expirable # Indicates whether the object is expired (`#expires_in` present and # expiration time has come). # # @return [Boolean] true if object expired and false in other case def expired? !!(expires_in && Time.now.utc > expires_at) end # Calculates expiration time in seconds. # # @return [Integer, nil] number of seconds if object has expiration time # or nil if object never expires. def expires_in_seconds return nil if expires_in.nil? expires = expires_at - Time.now.utc expires_sec = expires.seconds.round(0) expires_sec > 0 ? expires_sec : 0 end # Expiration time (date time of creation + TTL). # # @return [Time, nil] expiration time in UTC # or nil if the object never expires. # def expires_at expires_in && created_at + expires_in.seconds end end end end ================================================ FILE: lib/doorkeeper/models/concerns/expiration_time_sql_math.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module ExpirationTimeSqlMath extend ::ActiveSupport::Concern WARNING_MESSAGE = <<~WARNING.squish [DOORKEEPER] Doorkeeper doesn't support expiration time math for your database adapter. Records with an individual expires_in value longer than the global TTL may be incorrectly processed. Please add a class method `custom_expiration_time_sql` to your AccessToken/AccessGrant models/mixins to provide a custom SQL expression to calculate access token expiration time. See lib/doorkeeper/orm/active_record/mixins/access_token.rb for more details. WARNING class ExpirationTimeSqlGenerator attr_reader :model delegate :table_name, to: :@model def initialize(model) @model = model end def generate_sql raise "`generate_sql` should be overridden for a #{self.class.name}!" end end class MySqlExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator def generate_sql Arel.sql("DATE_ADD(#{table_name}.created_at, INTERVAL #{table_name}.expires_in SECOND)") end end class SqlLiteExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator def generate_sql Arel.sql("DATETIME(#{table_name}.created_at, '+' || #{table_name}.expires_in || ' SECONDS')") end end class SqlServerExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator def generate_sql Arel.sql("DATEADD(second, #{table_name}.expires_in, #{table_name}.created_at) AT TIME ZONE 'UTC'") end end class OracleExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator def generate_sql Arel.sql("#{table_name}.created_at + INTERVAL to_char(#{table_name}.expires_in) second") end end class PostgresExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator def generate_sql Arel.sql("#{table_name}.created_at + #{table_name}.expires_in * INTERVAL '1 SECOND'") end end ADAPTERS_MAPPING = { "sqlite" => SqlLiteExpirationTimeSqlGenerator, "sqlite3" => SqlLiteExpirationTimeSqlGenerator, "postgis" => PostgresExpirationTimeSqlGenerator, "postgresql" => PostgresExpirationTimeSqlGenerator, "mysql" => MySqlExpirationTimeSqlGenerator, "mysql2" => MySqlExpirationTimeSqlGenerator, "trilogy" => MySqlExpirationTimeSqlGenerator, "sqlserver" => SqlServerExpirationTimeSqlGenerator, "oracleenhanced" => OracleExpirationTimeSqlGenerator, }.freeze module ClassMethods def supports_expiration_time_math? ADAPTERS_MAPPING.key?(adapter_name.downcase) || respond_to?(:custom_expiration_time_sql) end def expiration_time_sql if respond_to?(:custom_expiration_time_sql) custom_expiration_time_sql else expiration_time_sql_expression end end def expiration_time_sql_expression ADAPTERS_MAPPING.fetch(adapter_name.downcase).new(self).generate_sql end def adapter_name ActiveRecord::Base.connection.adapter_name end end end end end ================================================ FILE: lib/doorkeeper/models/concerns/orderable.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module Orderable extend ActiveSupport::Concern module ClassMethods def ordered_by(attribute, direction = :asc) order(attribute => direction) end end end end end ================================================ FILE: lib/doorkeeper/models/concerns/ownership.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module Ownership extend ActiveSupport::Concern included do belongs_to :owner, polymorphic: true, optional: true validates :owner, presence: true, if: :validate_owner? end def validate_owner? Doorkeeper.config.confirm_application_owner? end end end end ================================================ FILE: lib/doorkeeper/models/concerns/polymorphic_resource_owner.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module PolymorphicResourceOwner module ForAccessGrant extend ActiveSupport::Concern included do if Doorkeeper.config.polymorphic_resource_owner? belongs_to :resource_owner, polymorphic: true, optional: false else validates :resource_owner_id, presence: true end end end module ForAccessToken extend ActiveSupport::Concern included do if Doorkeeper.config.polymorphic_resource_owner? belongs_to :resource_owner, polymorphic: true, optional: true end end end end end end ================================================ FILE: lib/doorkeeper/models/concerns/resource_ownerable.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module ResourceOwnerable extend ActiveSupport::Concern module ClassMethods # Searches for record by Resource Owner considering Doorkeeper # configuration for resource owner association. # # @param resource_owner [ActiveRecord::Base, Integer] # resource owner # # @return [Doorkeeper::AccessGrant, Doorkeeper::AccessToken] # collection of records # def by_resource_owner(resource_owner) if Doorkeeper.configuration.polymorphic_resource_owner? where(resource_owner: resource_owner) else where(resource_owner_id: resource_owner_id_for(resource_owner)) end end protected # Backward compatible way to retrieve resource owner itself (if # polymorphic association enabled) or just it's ID. # # @param resource_owner [ActiveRecord::Base, Integer] # resource owner # # @return [ActiveRecord::Base, Integer] # instance of Resource Owner or it's ID # def resource_owner_id_for(resource_owner) if resource_owner.respond_to?(:to_key) resource_owner.id else resource_owner end end end end end end ================================================ FILE: lib/doorkeeper/models/concerns/reusable.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module Reusable # Indicates whether the object is reusable (i.e. It is not expired and # has not crossed reuse_limit). # # @return [Boolean] true if can be reused and false in other case def reusable? return false if expired? return true unless expires_in threshold_limit = 100 - Doorkeeper.config.token_reuse_limit expires_in_seconds >= threshold_limit * expires_in / 100 end end end end ================================================ FILE: lib/doorkeeper/models/concerns/revocable.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module Revocable # Revokes the object (updates `:revoked_at` attribute setting its value # to the specific time). # # @param clock [Time] time object # def revoke(clock = Time) return if revoked? # Wrap in with_primary_role if the model class supports it if self.class.respond_to?(:with_primary_role) self.class.with_primary_role { update_attribute(:revoked_at, clock.now.utc) } else update_attribute(:revoked_at, clock.now.utc) end end # Indicates whether the object has been revoked. # # @return [Boolean] true if revoked, false in other case # def revoked? !!(revoked_at && revoked_at <= Time.now.utc) end end end end ================================================ FILE: lib/doorkeeper/models/concerns/scopes.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module Scopes def scopes OAuth::Scopes.from_string(scopes_string) end def scopes=(value) if value.is_a?(Array) super(Doorkeeper::OAuth::Scopes.from_array(value).to_s) else super(Doorkeeper::OAuth::Scopes.from_string(value.to_s).to_s) end end def scopes_string self[:scopes] end def includes_scope?(*required_scopes) required_scopes.blank? || required_scopes.any? { |scope| scopes.exists?(scope.to_s) } end end end end ================================================ FILE: lib/doorkeeper/models/concerns/secret_storable.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models ## # Storable finder to provide lookups for input plaintext values which are # mapped to their stored versions (e.g., hashing, encryption) before lookup. module SecretStorable extend ActiveSupport::Concern delegate :secret_strategy, :fallback_secret_strategy, to: :class # :nodoc module ClassMethods # Compare the given plaintext with the secret # # @param input [String] # The plain input to compare. # # @param secret [String] # The secret value to compare with. # # @return [Boolean] # Whether input matches secret as per the secret strategy # delegate :secret_matches?, to: :secret_strategy # Returns an instance of the Doorkeeper::AccessToken with # specific token value. # # @param attr [Symbol] # The token attribute we're looking with. # # @param token [#to_s] # token value (any object that responds to `#to_s`) # # @return [Doorkeeper::AccessToken, nil] AccessToken object or nil # if there is no record with such token # def find_by_plaintext_token(attr, token) token = token.to_s find_by(attr => secret_strategy.transform_secret(token)) || find_by_fallback_token(attr, token) end # Allow looking up previously plain tokens as a fallback # IFF a fallback strategy has been defined # # @param attr [Symbol] # The token attribute we're looking with. # # @param plain_secret [#to_s] # plain secret value (any object that responds to `#to_s`) # # @return [Doorkeeper::AccessToken, nil] AccessToken object or nil # if there is no record with such token # def find_by_fallback_token(attr, plain_secret) return nil unless fallback_secret_strategy # Use the previous strategy to look up stored_token = fallback_secret_strategy.transform_secret(plain_secret) find_by(attr => stored_token).tap do |resource| return nil unless resource upgrade_fallback_value resource, attr, plain_secret end end # Allow implementations in ORMs to replace a plain # value falling back to to avoid it remaining as plain text. # # @param instance # An instance of this model with a plain value token. # # @param attr # The secret attribute name to upgrade. # # @param plain_secret # The plain secret to upgrade. # def upgrade_fallback_value(instance, attr, plain_secret) upgraded = secret_strategy.store_secret(instance, attr, plain_secret) instance.update(attr => upgraded) end ## # Determines the secret storing transformer # Unless configured otherwise, uses the plain secret strategy def secret_strategy ::Doorkeeper::SecretStoring::Plain end ## # Determine the fallback storing strategy # Unless configured, there will be no fallback def fallback_secret_strategy nil end end end end end ================================================ FILE: lib/doorkeeper/models/concerns/write_to_primary.rb ================================================ # frozen_string_literal: true module Doorkeeper module Models module Concerns # Provides support for Rails read replicas by ensuring write operations # use the primary database when automatic role switching is enabled. # # When Rails uses automatic role switching with read replicas, GET requests # are routed to read-only databases. However, Doorkeeper may need to write # to the database during GET requests (e.g., creating access tokens during # implicit grant flow). This concern wraps write operations with # `connected_to(role: :writing)` to ensure they use the primary database. # # This concern is only active when: # 1. ActiveRecord supports `connected_to` (Rails 6.1+) # 2. The configuration option is enabled # module WriteToPrimary extend ActiveSupport::Concern class_methods do # Executes the given block with a connection to the primary database # for writing, if read replica support is enabled and available. # # @yield Block to execute with write connection # @return The result of the block # def with_primary_role(&block) if should_use_primary_role? ::ActiveRecord::Base.connected_to(role: :writing, &block) else yield end end private # Determines if we should explicitly use the primary role for writes # # @return [Boolean] # def should_use_primary_role? # Guard clause: return false if ActiveRecord is not available return false unless defined?(::ActiveRecord::Base) # Only use primary role if: # 1. The enable_multiple_database_roles option is enabled in config # 2. ActiveRecord supports connected_to (Rails 6.1+) Doorkeeper.config.enable_multiple_database_roles && ::ActiveRecord::Base.respond_to?(:connected_to) end end end end end end ================================================ FILE: lib/doorkeeper/oauth/authorization/code.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth module Authorization class Code attr_reader :pre_auth, :resource_owner, :token def initialize(pre_auth, resource_owner) @pre_auth = pre_auth @resource_owner = resource_owner end def issue_token! return @token if defined?(@token) @token = Doorkeeper.config.access_grant_model.with_primary_role do Doorkeeper.config.access_grant_model.create!(access_grant_attributes) end end def oob_redirect { action: :show, code: token.plaintext_token } end def access_grant? true end private def authorization_code_expires_in Doorkeeper.config.authorization_code_expires_in end def access_grant_attributes attributes = { application_id: pre_auth.client.id, expires_in: authorization_code_expires_in, redirect_uri: pre_auth.redirect_uri, scopes: pre_auth.scopes.to_s, } if Doorkeeper.config.polymorphic_resource_owner? attributes[:resource_owner] = resource_owner else attributes[:resource_owner_id] = resource_owner.id end pkce_attributes.merge(attributes).merge(custom_attributes) end def custom_attributes # Custom access token attributes are saved into the access grant, # and then included in subsequently generated access tokens. @pre_auth.custom_access_token_attributes.to_h.with_indifferent_access end def pkce_attributes return {} unless pkce_supported? { code_challenge: pre_auth.code_challenge, code_challenge_method: pre_auth.code_challenge_method, } end # Ensures firstly, if migration with additional PKCE columns was # generated and migrated def pkce_supported? Doorkeeper.config.access_grant_model.pkce_supported? end end end end end ================================================ FILE: lib/doorkeeper/oauth/authorization/context.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth module Authorization class Context attr_reader :client, :grant_type, :resource_owner, :scopes def initialize(**attributes) attributes.each do |name, value| instance_variable_set(:"@#{name}", value) if respond_to?(name) end end end end end end ================================================ FILE: lib/doorkeeper/oauth/authorization/token.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth module Authorization class Token attr_reader :pre_auth, :resource_owner, :token class << self def build_context(pre_auth_or_oauth_client, grant_type, scopes, resource_owner) oauth_client = if pre_auth_or_oauth_client.respond_to?(:application) pre_auth_or_oauth_client.application elsif pre_auth_or_oauth_client.respond_to?(:client) pre_auth_or_oauth_client.client else pre_auth_or_oauth_client end Doorkeeper::OAuth::Authorization::Context.new( client: oauth_client, grant_type: grant_type, scopes: scopes, resource_owner: resource_owner, ) end def access_token_expires_in(configuration, context) if configuration.option_defined?(:custom_access_token_expires_in) expiration = configuration.custom_access_token_expires_in.call(context) return nil if expiration == Float::INFINITY expiration || configuration.access_token_expires_in else configuration.access_token_expires_in end end def refresh_token_enabled?(server, context) if server.refresh_token_enabled?.respond_to?(:call) server.refresh_token_enabled?.call(context) else !!server.refresh_token_enabled? end end end def initialize(pre_auth, resource_owner) @pre_auth = pre_auth @resource_owner = resource_owner end def issue_token! return @token if defined?(@token) context = self.class.build_context( pre_auth.client, Doorkeeper::OAuth::IMPLICIT, pre_auth.scopes, resource_owner, ) @token = Doorkeeper.config.access_token_model.find_or_create_for( application: application, resource_owner: resource_owner, scopes: pre_auth.scopes, expires_in: self.class.access_token_expires_in(Doorkeeper.config, context), use_refresh_token: false, ) end def application return unless pre_auth.client pre_auth.client.is_a?(Doorkeeper.config.application_model) ? pre_auth.client : pre_auth.client.application end def oob_redirect { controller: controller, action: :show, access_token: token.plaintext_token, } end def access_token? true end private def controller @controller ||= begin mapping = Doorkeeper::Rails::Routes.mapping[:token_info] || {} mapping[:controllers] || "doorkeeper/token_info" end end end end end end ================================================ FILE: lib/doorkeeper/oauth/authorization/uri_builder.rb ================================================ # frozen_string_literal: true require "rack/utils" module Doorkeeper module OAuth module Authorization class URIBuilder class << self def uri_with_query(url, parameters = {}) uri = URI.parse(url) original_query = Rack::Utils.parse_query(uri.query) uri.query = build_query(original_query.merge(parameters)) uri.to_s end def uri_with_fragment(url, parameters = {}) uri = URI.parse(url) uri.fragment = build_query(parameters) uri.to_s end private def build_query(parameters = {}) parameters.reject! { |_, value| value.blank? } Rack::Utils.build_query(parameters) end end end end end end ================================================ FILE: lib/doorkeeper/oauth/authorization_code_request.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class AuthorizationCodeRequest < BaseRequest validate :params, error: Errors::InvalidRequest validate :client, error: Errors::InvalidClient validate :grant, error: Errors::InvalidGrant # @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 validate :redirect_uri, error: Errors::InvalidGrant validate :code_verifier, error: Errors::InvalidGrant attr_reader :grant, :client, :redirect_uri, :access_token, :code_verifier, :invalid_request_reason, :missing_param def initialize(server, grant, client, parameters = {}) @server = server @client = client @grant = grant @grant_type = Doorkeeper::OAuth::AUTHORIZATION_CODE @redirect_uri = parameters[:redirect_uri] @code_verifier = parameters[:code_verifier] end private def before_successful_response grant.transaction do grant.lock! raise Errors::InvalidGrantReuse if grant.revoked? if Doorkeeper.config.revoke_previous_authorization_code_token? revoke_previous_tokens(grant.application, resource_owner) end grant.revoke find_or_create_access_token( client, resource_owner, grant.scopes, custom_token_attributes_with_data, server, ) end super end def resource_owner if Doorkeeper.config.polymorphic_resource_owner? grant.resource_owner else grant.resource_owner_id end end def pkce_supported? Doorkeeper.config.access_grant_model.pkce_supported? end def validate_params @missing_param = if grant&.uses_pkce? && code_verifier.blank? :code_verifier elsif client && !client.confidential && Doorkeeper.config.force_pkce? && code_verifier.blank? :code_verifier elsif redirect_uri.blank? :redirect_uri end @missing_param.nil? end def validate_client client.present? end def validate_grant return false unless grant && grant.application_id == client.id grant.accessible? end def validate_redirect_uri Helpers::URIChecker.valid_for_authorization?( redirect_uri, grant.redirect_uri, ) end # if either side (server or client) request PKCE, check the verifier # against the DB - if PKCE is supported def validate_code_verifier return true unless pkce_supported? return grant.code_challenge.blank? if code_verifier.blank? if grant.code_challenge_method == "S256" grant.code_challenge == generate_code_challenge(code_verifier) elsif grant.code_challenge_method == "plain" grant.code_challenge == code_verifier else false end end def generate_code_challenge(code_verifier) Doorkeeper.config.access_grant_model.generate_code_challenge(code_verifier) end def custom_token_attributes_with_data grant .attributes .with_indifferent_access .slice(*Doorkeeper.config.custom_access_token_attributes) .symbolize_keys end def revoke_previous_tokens(application, resource_owner) Doorkeeper.config.access_token_model.revoke_all_for(application.id, resource_owner) end end end end ================================================ FILE: lib/doorkeeper/oauth/base_request.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class BaseRequest include Validations attr_reader :grant_type, :server delegate :default_scopes, to: :server def authorize if valid? before_successful_response @response = TokenResponse.new(access_token) after_successful_response @response elsif error == Errors::InvalidRequest @response = InvalidRequestResponse.from_request(self) else @response = ErrorResponse.from_request(self) end end def scopes @scopes ||= build_scopes end def find_or_create_access_token(client, resource_owner, scopes, custom_attributes, server) context = Authorization::Token.build_context(client, grant_type, scopes, resource_owner) application = client.is_a?(Doorkeeper.config.application_model) ? client : client&.application token_attributes = { application: application, resource_owner: resource_owner, scopes: scopes, expires_in: Authorization::Token.access_token_expires_in(server, context), use_refresh_token: Authorization::Token.refresh_token_enabled?(server, context), } @access_token = Doorkeeper.config.access_token_model.find_or_create_for(**token_attributes.merge(custom_attributes)) end def before_successful_response Doorkeeper.config.before_successful_strategy_response.call(self) end def after_successful_response Doorkeeper.config.after_successful_strategy_response.call(self, @response) end private def build_scopes if @original_scopes.present? OAuth::Scopes.from_string(@original_scopes) else client_scopes = @client&.scopes return default_scopes if client_scopes.blank? # Avoid using Scope#& for dynamic scopes client_scopes.allowed(default_scopes) end end end end end ================================================ FILE: lib/doorkeeper/oauth/base_response.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class BaseResponse def body {} end def description "" end def headers {} end def redirectable? false end def redirect_uri "" end def status :ok end end end end ================================================ FILE: lib/doorkeeper/oauth/client/credentials.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class Client Credentials = Struct.new(:uid, :secret) do class << self def from_request(request, *credentials_methods) credentials_methods.inject(nil) do |_, method| method = self.method(method) if method.is_a?(Symbol) credentials = Credentials.new(*method.call(request)) break credentials if credentials.present? end end def from_params(request) request.parameters.values_at(:client_id, :client_secret) end def from_basic(request) authorization = request.authorization if authorization.present? && authorization =~ /^Basic (.*)/im Base64.decode64(Regexp.last_match(1)).split(/:/, 2) end end end # Public clients may have their secret blank, but "credentials" are # still present delegate :blank?, to: :uid end end end end ================================================ FILE: lib/doorkeeper/oauth/client.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class Client attr_reader :application delegate :id, :name, :uid, :redirect_uri, :scopes, :confidential, to: :@application def initialize(application) @application = application end def self.find(uid, method = Doorkeeper.config.application_model.method(:by_uid)) return unless (application = method.call(uid)) new(application) end def self.authenticate(credentials, method = Doorkeeper.config.application_model.method(:by_uid_and_secret)) return if credentials.blank? return unless (application = method.call(credentials.uid, credentials.secret)) new(application) end end end end ================================================ FILE: lib/doorkeeper/oauth/client_credentials/creator.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth module ClientCredentials class Creator def call(client, scopes, attributes = {}) existing_token = nil if lookup_existing_token? existing_token = find_active_existing_token_for(client, scopes, attributes) return existing_token if Doorkeeper.config.reuse_access_token && existing_token&.reusable? end with_revocation(existing_token: existing_token) do application = client.is_a?(Doorkeeper.config.application_model) ? client : client&.application Doorkeeper.config.access_token_model.create_for( application: application, resource_owner: nil, scopes: scopes, **attributes, ) end end private def with_revocation(existing_token:) if existing_token && Doorkeeper.config.revoke_previous_client_credentials_token? existing_token.with_lock do raise Errors::DoorkeeperError, :invalid_token_reuse if existing_token.revoked? existing_token.revoke yield end else yield end end def lookup_existing_token? Doorkeeper.config.reuse_access_token || Doorkeeper.config.revoke_previous_client_credentials_token? end def find_active_existing_token_for(client, scopes, attributes) custom_attributes = Doorkeeper.config.access_token_model. extract_custom_attributes(attributes).presence Doorkeeper.config.access_token_model.matching_token_for( client, nil, scopes, custom_attributes: custom_attributes, include_expired: false) end end end end end ================================================ FILE: lib/doorkeeper/oauth/client_credentials/issuer.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth module ClientCredentials class Issuer attr_reader :token, :validator, :error def initialize(server, validator) @server = server @validator = validator end def create(client, scopes, attributes = {}, creator = Creator.new) if validator.valid? @token = create_token(client, scopes, attributes, creator) @error = Errors::ServerError unless @token else @token = false @error = validator.error end @token end private def create_token(client, scopes, attributes, creator) context = Authorization::Token.build_context( client, Doorkeeper::OAuth::CLIENT_CREDENTIALS, scopes, nil, ) ttl = Authorization::Token.access_token_expires_in(@server, context) creator.call( client, scopes, use_refresh_token: false, expires_in: ttl, **attributes ) end end end end end ================================================ FILE: lib/doorkeeper/oauth/client_credentials/validator.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth module ClientCredentials class Validator include Validations include OAuth::Helpers validate :client, error: Errors::InvalidClient validate :client_supports_grant_flow, error: Errors::UnauthorizedClient validate :scopes, error: Errors::InvalidScope def initialize(server, request) @server = server @request = request @client = request.client validate end private def validate_client @client.present? end def validate_client_supports_grant_flow return if @client.blank? Doorkeeper.config.allow_grant_flow_for_client?( Doorkeeper::OAuth::CLIENT_CREDENTIALS, @client.application, ) end def validate_scopes application_scopes = if @client.present? @client.application.scopes else "" end return true if @request.scopes.blank? && application_scopes.blank? ScopeChecker.valid?( scope_str: @request.scopes.to_s, server_scopes: @server.scopes, app_scopes: application_scopes, grant_type: Doorkeeper::OAuth::CLIENT_CREDENTIALS, ) end end end end end ================================================ FILE: lib/doorkeeper/oauth/client_credentials_request.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class ClientCredentialsRequest < BaseRequest attr_reader :client, :original_scopes, :parameters, :response alias error_response response delegate :error, to: :issuer def initialize(server, client, parameters = {}) @client = client @server = server @response = nil @grant_type = Doorkeeper::OAuth::CLIENT_CREDENTIALS @original_scopes = parameters[:scope] @parameters = parameters.except(:scope) end def access_token issuer.token end def issuer @issuer ||= ClientCredentials::Issuer.new( server, ClientCredentials::Validator.new(server, self), ) end private def valid? issuer.create(client, scopes, custom_token_attributes_with_data) end def custom_token_attributes_with_data parameters .with_indifferent_access .slice(*Doorkeeper.config.custom_access_token_attributes) .symbolize_keys end end end end ================================================ FILE: lib/doorkeeper/oauth/code_request.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class CodeRequest attr_reader :pre_auth, :resource_owner def initialize(pre_auth, resource_owner) @pre_auth = pre_auth @resource_owner = resource_owner end def authorize auth = Authorization::Code.new(pre_auth, resource_owner) auth.issue_token! CodeResponse.new(pre_auth, auth, response_on_fragment: pre_auth.response_mode == "fragment") end def deny pre_auth.error = Errors::AccessDenied pre_auth.error_response end end end end ================================================ FILE: lib/doorkeeper/oauth/code_response.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class CodeResponse < BaseResponse include OAuth::Helpers attr_reader :pre_auth, :auth, :response_on_fragment def initialize(pre_auth, auth, options = {}) @pre_auth = pre_auth @auth = auth @response_on_fragment = options[:response_on_fragment] end def redirectable? true end def issued_token auth.token end def body if auth.try(:access_token?) { access_token: auth.token.plaintext_token, token_type: auth.token.token_type, expires_in: auth.token.expires_in_seconds, state: pre_auth.state, } elsif auth.try(:access_grant?) { code: auth.token.plaintext_token, state: pre_auth.state, } end end def redirect_uri if URIChecker.oob_uri?(pre_auth.redirect_uri) auth.oob_redirect elsif response_on_fragment Authorization::URIBuilder.uri_with_fragment(pre_auth.redirect_uri, body) else Authorization::URIBuilder.uri_with_query(pre_auth.redirect_uri, body) end end end end end ================================================ FILE: lib/doorkeeper/oauth/error.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth Error = Struct.new(:name, :state, :translate_options) do def description options = (translate_options || {}).merge( scope: %i[doorkeeper errors messages], default: :server_error, ) I18n.translate(name, **options) end end end end ================================================ FILE: lib/doorkeeper/oauth/error_response.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class ErrorResponse < BaseResponse include OAuth::Helpers NON_REDIRECTABLE_STATES = %i[invalid_redirect_uri invalid_client unauthorized_client].freeze def self.from_request(request, attributes = {}) new( attributes.merge( name: error_name_for(request.error), exception_class: exception_class_for(request.error), translate_options: request.error.try(:translate_options), state: request.try(:state), redirect_uri: request.try(:redirect_uri), ), ) end def self.error_name_for(error) error.respond_to?(:name_for_response) ? error.name_for_response : error end def self.exception_class_for(error) return error if error.respond_to?(:name_for_response) "Doorkeeper::Errors::#{error.to_s.classify}".safe_constantize end private_class_method :error_name_for, :exception_class_for delegate :name, :description, :state, to: :@error def initialize(attributes = {}) @error = OAuth::Error.new(*attributes.values_at(:name, :state, :translate_options)) @exception_class = attributes[:exception_class] @redirect_uri = attributes[:redirect_uri] @response_on_fragment = attributes[:response_on_fragment] end def body { error: name, error_description: description, state: state, }.reject { |_, v| v.blank? } end def status if name == :invalid_client || name == :unauthorized_client :unauthorized else :bad_request end end def redirectable? !NON_REDIRECTABLE_STATES.include?(name) && !URIChecker.oob_uri?(@redirect_uri) end def redirect_uri if @response_on_fragment Authorization::URIBuilder.uri_with_fragment(@redirect_uri, body) else Authorization::URIBuilder.uri_with_query(@redirect_uri, body) end end def headers { "Cache-Control" => "no-store, no-cache", "Content-Type" => "application/json; charset=utf-8", "WWW-Authenticate" => authenticate_info, } end def raise_exception! raise exception_class.new(self), description end protected def realm Doorkeeper.config.realm end def exception_class return @exception_class if @exception_class raise NotImplementedError, "error response must define #exception_class" end private def authenticate_info %(Bearer realm="#{realm}", error="#{sanitize_error_values(name)}", error_description="#{sanitize_error_values(description)}") end # This method removes any characters that are invalid in error # details per RFC6750. # # > Values for the "error" and "error_description" attributes # > (specified in Appendixes A.7 and A.8 of [RFC6749]) MUST NOT # > include characters outside the set %x20-21 (" " or "!") / %x23-5B / # > %x5D-7E (ascii "#" to "~" without "\"). def sanitize_error_values(string) string.to_s.each_char.map do |char| if char.in?("\x20".encode("utf-8").."\x21".encode("utf-8")) || char.in?("\x23".encode("utf-8").."\x5B".encode("utf-8")) || char.in?("\x5D".encode("utf-8").."\x7E".encode("utf-8")) char else "_" end end.join("") end end end end ================================================ FILE: lib/doorkeeper/oauth/forbidden_token_response.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class ForbiddenTokenResponse < ErrorResponse def self.from_scopes(scopes, attributes = {}) new(attributes.merge(scopes: scopes)) end def initialize(attributes = {}) super(attributes.merge(name: :insufficient_scope, state: :forbidden)) @scopes = attributes[:scopes] end def status :forbidden end def description @description ||= I18n.t("doorkeeper.errors.messages.forbidden_token.missing_scope", oauth_scopes: @scopes.map(&:to_s).join(" "),) end protected def exception_class Doorkeeper::Errors::TokenForbidden end end end end ================================================ FILE: lib/doorkeeper/oauth/helpers/scope_checker.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth module Helpers module ScopeChecker class Validator attr_reader :parsed_scopes, :scope_str def initialize(scope_str, server_scopes, app_scopes, grant_type) @parsed_scopes = OAuth::Scopes.from_string(scope_str) @scope_str = scope_str @valid_scopes = valid_scopes(server_scopes, app_scopes) @scopes_by_grant_type = Doorkeeper.config.scopes_by_grant_type[grant_type.to_sym] if grant_type end def valid? scope_str.present? && scope_str !~ /[\n\r\t]/ && @valid_scopes.has_scopes?(parsed_scopes) && permitted_to_grant_type? end private def valid_scopes(server_scopes, app_scopes) app_scopes.presence || server_scopes end def permitted_to_grant_type? return true unless @scopes_by_grant_type OAuth::Scopes.from_array(@scopes_by_grant_type) .has_scopes?(parsed_scopes) end end def self.valid?(scope_str:, server_scopes:, app_scopes: nil, grant_type: nil) Validator.new( scope_str, server_scopes, app_scopes, grant_type, ).valid? end end end end end ================================================ FILE: lib/doorkeeper/oauth/helpers/unique_token.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth module Helpers # Default Doorkeeper token generator. Follows OAuth RFC and # could be customized using `default_generator_method` in # configuration. module UniqueToken def self.generate(options = {}) # Access Token value must be 1*VSCHAR or # 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=" # # @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.12 # @see https://datatracker.ietf.org/doc/html/rfc6750#section-2.1 # generator = options.delete(:generator) || SecureRandom.method(default_generator_method) token_size = options.delete(:size) || 32 generator.call(token_size) end # Generator method for default generator class (SecureRandom) # def self.default_generator_method Doorkeeper.config.default_generator_method end end end end end ================================================ FILE: lib/doorkeeper/oauth/helpers/uri_checker.rb ================================================ # frozen_string_literal: true require "ipaddr" module Doorkeeper module OAuth module Helpers module URIChecker def self.valid?(url) return true if oob_uri?(url) uri = as_uri(url) valid_scheme?(uri) && iff_host?(uri) && uri.fragment.nil? && uri.opaque.nil? rescue URI::InvalidURIError false end def self.matches?(url, client_url) url = as_uri(url) client_url = as_uri(client_url) unless client_url.query.nil? return false unless query_matches?(url.query, client_url.query) # Clear out queries so rest of URI can be tested. This allows query # params to be in the request but order not mattering. client_url.query = nil end # RFC8252, Paragraph 7.3 # @see https://datatracker.ietf.org/doc/html/rfc8252#section-7.3 if loopback_uri?(url) && loopback_uri?(client_url) url.port = nil client_url.port = nil end url.query = nil url == client_url end def self.loopback_uri?(uri) IPAddr.new(uri.host).loopback? rescue IPAddr::Error, IPAddr::InvalidAddressError false end def self.valid_for_authorization?(url, client_url) valid?(url) && client_url.split.any? { |other_url| matches?(url, other_url) } end def self.as_uri(url) URI.parse(url) end def self.query_matches?(query, client_query) return true if client_query.blank? && query.blank? return false if client_query.nil? || query.nil? # Will return true independent of query order client_query.split("&").sort == query.split("&").sort end def self.valid_scheme?(uri) return false if uri.scheme.blank? %w[localhost].exclude?(uri.scheme) end def self.hypertext_scheme?(uri) %w[http https].include?(uri.scheme) end def self.iff_host?(uri) !(hypertext_scheme?(uri) && uri.host.blank?) end def self.oob_uri?(uri) NonStandard::IETF_WG_OAUTH2_OOB_METHODS.include?(uri) end end end end end ================================================ FILE: lib/doorkeeper/oauth/hooks/context.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth module Hooks class Context attr_reader :auth, :pre_auth def initialize(**attributes) attributes.each do |name, value| instance_variable_set(:"@#{name}", value) if respond_to?(name) end end def issued_token auth&.issued_token end end end end end ================================================ FILE: lib/doorkeeper/oauth/invalid_request_response.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class InvalidRequestResponse < ErrorResponse attr_reader :reason def self.from_request(request, attributes = {}) new( attributes.merge( state: request.try(:state), redirect_uri: request.try(:redirect_uri), missing_param: request.try(:missing_param), reason: request.try(:invalid_request_reason), ), ) end def initialize(attributes = {}) super(attributes.merge(name: :invalid_request)) @missing_param = attributes[:missing_param] @reason = @missing_param.nil? ? attributes[:reason] : :missing_param end def status :bad_request end def description I18n.translate( reason, scope: %i[doorkeeper errors messages invalid_request], default: :unknown, value: @missing_param, ) end def exception_class Doorkeeper::Errors::InvalidRequest end def redirectable? super && @missing_param != :client_id end end end end ================================================ FILE: lib/doorkeeper/oauth/invalid_token_response.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class InvalidTokenResponse < ErrorResponse attr_reader :reason def self.from_access_token(access_token, attributes = {}) reason = if access_token&.revoked? :revoked elsif access_token&.expired? :expired else :unknown end new(attributes.merge(reason: reason)) end def initialize(attributes = {}) super(attributes.merge(name: :invalid_token, state: :unauthorized)) @reason = attributes[:reason] || :unknown end def status :unauthorized end def description @description ||= I18n.translate( @reason, scope: %i[doorkeeper errors messages invalid_token], ) end protected def exception_class errors_mapping.fetch(reason) end private def errors_mapping { expired: Doorkeeper::Errors::TokenExpired, revoked: Doorkeeper::Errors::TokenRevoked, unknown: Doorkeeper::Errors::TokenUnknown, } end end end end ================================================ FILE: lib/doorkeeper/oauth/nonstandard.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class NonStandard # These are not part of the OAuth 2 specification but are still in use by Google # and in some other implementations. Native applications should use one of the # approaches discussed in RFC8252. OOB is 'Out of Band' # This value signals to the Google Authorization Server that the authorization # code should be returned in the title bar of the browser, with the page text # prompting the user to copy the code and paste it in the application. # This is useful when the client (such as a Windows application) cannot listen # on an HTTP port without significant client configuration. # When you use this value, your application can then detect that the page has loaded, and can # read the title of the HTML page to obtain the authorization code. It is then up to your # application to close the browser window if you want to ensure that the user never sees the # page that contains the authorization code. The mechanism for doing this varies from platform # to platform. # # If your platform doesn't allow you to detect that the page has loaded or read the title of # the page, you can have the user paste the code back to your application, as prompted by the # text in the confirmation page that the OAuth 2.0 server generates. IETF_WG_OAUTH2_OOB = "urn:ietf:wg:oauth:2.0:oob" # This is identical to urn:ietf:wg:oauth:2.0:oob, but the text in the confirmation page that # the OAuth 2.0 server generates won't instruct the user to copy the authorization code, but # instead will simply ask the user to close the window. # # This is useful when your application reads the title of the HTML page (by checking window # titles on the desktop, for example) to obtain the authorization code, but can't close the # page on its own. IETF_WG_OAUTH2_OOB_AUTO = "urn:ietf:wg:oauth:2.0:oob:auto" IETF_WG_OAUTH2_OOB_METHODS = [IETF_WG_OAUTH2_OOB, IETF_WG_OAUTH2_OOB_AUTO].freeze end end end ================================================ FILE: lib/doorkeeper/oauth/password_access_token_request.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class PasswordAccessTokenRequest < BaseRequest include OAuth::Helpers validate :client, error: Errors::InvalidClient validate :client_supports_grant_flow, error: Errors::UnauthorizedClient validate :resource_owner, error: Errors::InvalidGrant validate :scopes, error: Errors::InvalidScope attr_reader :client, :credentials, :resource_owner, :parameters, :access_token def initialize(server, client, credentials, resource_owner, parameters = {}) @server = server @resource_owner = resource_owner @client = client @credentials = credentials @parameters = parameters @original_scopes = parameters[:scope] @grant_type = Doorkeeper::OAuth::PASSWORD end private def before_successful_response find_or_create_access_token(client, resource_owner, scopes, {}, server) super end def validate_scopes return true if scopes.blank? ScopeChecker.valid?( scope_str: scopes.to_s, server_scopes: server.scopes, app_scopes: client.try(:scopes), grant_type: grant_type, ) end def validate_resource_owner resource_owner.present? end # Section 4.3.2. Access Token Request for Resource Owner Password Credentials Grant: # # If the client type is confidential or the client was issued client credentials (or assigned # other authentication requirements), the client MUST authenticate with the authorization # server as described in Section 3.2.1. # # The authorization server MUST: # # o require client authentication for confidential clients or for any client that was # issued client credentials (or with other authentication requirements) # # o authenticate the client if client authentication is included, # # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3 # def validate_client if Doorkeeper.config.skip_client_authentication_for_password_grant client.present? || (!parameters[:client_id] && credentials.blank?) else client.present? end end def validate_client_supports_grant_flow Doorkeeper.config.allow_grant_flow_for_client?(grant_type, client&.application) end end end end ================================================ FILE: lib/doorkeeper/oauth/pre_authorization.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class PreAuthorization include Validations validate :client_id, error: Errors::InvalidRequest validate :client, error: Errors::InvalidClient validate :client_supports_grant_flow, error: Errors::UnauthorizedClient validate :resource_owner_authorize_for_client, error: Errors::InvalidClient validate :redirect_uri, error: Errors::InvalidRedirectUri validate :params, error: Errors::InvalidRequest validate :response_type, error: Errors::UnsupportedResponseType validate :response_mode, error: Errors::UnsupportedResponseMode validate :scopes, error: Errors::InvalidScope validate :code_challenge, error: Errors::InvalidRequest validate :code_challenge_method, error: Errors::InvalidCodeChallengeMethod attr_reader :client, :code_challenge, :code_challenge_method, :missing_param, :redirect_uri, :resource_owner, :response_type, :state, :authorization_response_flow, :response_mode, :custom_access_token_attributes, :invalid_request_reason def initialize(server, parameters = {}, resource_owner = nil) @server = server @client_id = parameters[:client_id] @response_type = parameters[:response_type] @response_mode = parameters[:response_mode] @redirect_uri = parameters[:redirect_uri] @scope = parameters[:scope] @state = parameters[:state] @code_challenge = parameters[:code_challenge] @code_challenge_method = parameters[:code_challenge_method] @resource_owner = resource_owner @custom_access_token_attributes = parameters.slice(*Doorkeeper.config.custom_access_token_attributes).to_h end def authorizable? valid? end def scopes Scopes.from_string(scope) end def scope @scope.presence || (server.default_scopes.presence && build_scopes) end def error_response if error == Errors::InvalidRequest OAuth::InvalidRequestResponse.from_request( self, response_on_fragment: response_on_fragment?, ) else OAuth::ErrorResponse.from_request(self, response_on_fragment: response_on_fragment?) end end def as_json(_options = nil) pre_auth_hash end def form_post_response? response_mode == "form_post" end private attr_reader :client_id, :server def build_scopes client_scopes = client.scopes if client_scopes.blank? server.default_scopes.to_s else server.default_scopes.allowed(client_scopes).to_s end end def validate_client_id @missing_param = :client_id if client_id.blank? @missing_param.nil? end def validate_client @client = OAuth::Client.find(client_id) @client.present? end def validate_client_supports_grant_flow Doorkeeper.config.allow_grant_flow_for_client?(grant_type, client.application) end def validate_resource_owner_authorize_for_client # The `authorize_resource_owner_for_client` config option is used for this validation client.application.authorized_for_resource_owner?(@resource_owner) end def validate_redirect_uri return false if redirect_uri.blank? Helpers::URIChecker.valid_for_authorization?( redirect_uri, client.redirect_uri, ) end def validate_params @missing_param = if response_type.blank? :response_type elsif @scope.blank? && server.default_scopes.blank? :scope end @missing_param.nil? end def validate_response_type server.authorization_response_flows.any? do |flow| if flow.matches_response_type?(response_type) @authorization_response_flow = flow true end end end def validate_response_mode if response_mode.blank? @response_mode = authorization_response_flow.default_response_mode return true end authorization_response_flow.matches_response_mode?(response_mode) end def validate_scopes Helpers::ScopeChecker.valid?( scope_str: scope, server_scopes: server.scopes, app_scopes: client.scopes, grant_type: grant_type, ) end def validate_code_challenge return true unless Doorkeeper.config.force_pkce? return true if client.confidential return true if code_challenge.present? @invalid_request_reason = :invalid_code_challenge false end def validate_code_challenge_method return true unless Doorkeeper.config.access_grant_model.pkce_supported? code_challenge.blank? || (code_challenge_method.present? && Doorkeeper.config.pkce_code_challenge_methods_supported.include?(code_challenge_method)) end def response_on_fragment? return response_type == "token" if response_mode.nil? response_mode == "fragment" end def grant_type response_type == "code" ? AUTHORIZATION_CODE : IMPLICIT end def pre_auth_hash { client_id: client.uid, redirect_uri: redirect_uri, state: state, response_type: response_type, scope: scope, client_name: client.name, status: I18n.t("doorkeeper.pre_authorization.status"), } end end end end ================================================ FILE: lib/doorkeeper/oauth/refresh_token_request.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class RefreshTokenRequest < BaseRequest include OAuth::Helpers validate :token_presence, error: Errors::InvalidRequest validate :token, error: Errors::InvalidGrant validate :client, error: Errors::InvalidClient validate :client_match, error: Errors::InvalidGrant validate :scope, error: Errors::InvalidScope attr_reader :access_token, :client, :credentials, :refresh_token attr_reader :missing_param def initialize(server, refresh_token, credentials, parameters = {}) @server = server @refresh_token = refresh_token @credentials = credentials @grant_type = Doorkeeper::OAuth::REFRESH_TOKEN @original_scopes = parameters[:scope] || parameters[:scopes] @refresh_token_parameter = parameters[:refresh_token] @client = load_client(credentials) if credentials end private def load_client(credentials) Doorkeeper.config.application_model.by_uid_and_secret(credentials.uid, credentials.secret) end def before_successful_response if refresh_token_revoked_on_use? # No locking needed when refresh tokens are revoked on use # because the old token is revoked later when the new token is used. # This allows multiple concurrent refresh requests to succeed during the # transition period, after which the old refresh token will be revoked. raise Errors::InvalidGrantReuse if refresh_token.revoked? create_access_token else # Use locking when refresh tokens are revoked immediately # to prevent race conditions where multiple tokens could be created refresh_token.with_lock do raise Errors::InvalidGrantReuse if refresh_token.revoked? refresh_token.revoke create_access_token end end super end def refresh_token_revoked_on_use? Doorkeeper.config.access_token_model.refresh_token_revoked_on_use? end def default_scopes refresh_token.scopes end def create_access_token attributes = {}.merge(custom_token_attributes_with_data) resource_owner = if Doorkeeper.config.polymorphic_resource_owner? refresh_token.resource_owner else refresh_token.resource_owner_id end if refresh_token_revoked_on_use? attributes[:previous_refresh_token] = refresh_token.refresh_token end # RFC6749 # 1.5. Refresh Token # # Refresh tokens are issued to the client by the authorization server and are # used to obtain a new access token when the current access token # becomes invalid or expires, or to obtain additional access tokens # with identical or narrower scope (access tokens may have a shorter # lifetime and fewer permissions than authorized by the resource # owner). # # Here we assume that TTL of the token received after refreshing should be # the same as that of the original token. # @access_token = Doorkeeper.config.access_token_model.create_for( application: refresh_token.application, resource_owner: resource_owner, scopes: scopes, expires_in: refresh_token.expires_in, use_refresh_token: true, **attributes, ) end def validate_token_presence @missing_param = :refresh_token if refresh_token.blank? && @refresh_token_parameter.blank? @missing_param.nil? end def validate_token refresh_token.present? && !refresh_token.revoked? end def validate_client return true if credentials.blank? client.present? end # @see https://datatracker.ietf.org/doc/html/rfc6749#section-1.5 # def validate_client_match return true if refresh_token.application_id.blank? client && refresh_token.application_id == client.id end def validate_scope if @original_scopes.present? ScopeChecker.valid?( scope_str: @original_scopes, server_scopes: refresh_token.scopes, ) else true end end def custom_token_attributes_with_data refresh_token .attributes .with_indifferent_access .slice(*Doorkeeper.config.custom_access_token_attributes) .symbolize_keys end end end end ================================================ FILE: lib/doorkeeper/oauth/scopes.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class Scopes include Enumerable include Comparable DYNAMIC_SCOPE_WILDCARD = "*" def self.from_string(string) string ||= "" new.tap do |scope| scope.add(*string.split) end end def self.from_array(array) new.tap do |scope| scope.add(*array) end end delegate :each, :empty?, to: :@scopes def initialize @scopes = [] end def exists?(scope) scope = scope.to_s @scopes.any? do |allowed_scope| if dynamic_scopes_enabled? && dynamic_scopes_present?(allowed_scope, scope) dynamic_scope_match?(allowed_scope, scope) else allowed_scope == scope end end end def add(*scopes) @scopes.push(*scopes.map(&:to_s)) @scopes.uniq! end def all @scopes end def to_s @scopes.join(" ") end def scopes?(scopes) scopes.all? { |scope| exists?(scope) } end alias has_scopes? scopes? def +(other) self.class.from_array(all + to_array(other)) end def <=>(other) if other.respond_to?(:map) map(&:to_s).sort <=> other.map(&:to_s).sort else super end end # DEPRECATED: With dynamic scopes, #allowed should be called because # A & B doesn't really make sense with dynamic scopes. # # For example, if A = user:* and B is user:1, A & B = []. # If we modified this method to take dynamic scopes into an account, then order # becomes important, and this would violate the principle that A & B = B & A. def &(other) return allowed(other) if dynamic_scopes_enabled? self.class.from_array(all & to_array(other)) end # Returns a set of scopes that are allowed, taking dynamic # scopes into account. This instance's scopes is taken as the allowed set, # and the passed value is the set to filter. # # @param other The set of scopes to filter def allowed(other) filtered_scopes = other.select { |scope| self.exists?(scope) } self.class.from_array(filtered_scopes) end private def dynamic_scopes_enabled? Doorkeeper.config.enable_dynamic_scopes? end def dynamic_scope_delimiter return unless dynamic_scopes_enabled? @dynamic_scope_delimiter ||= Doorkeeper.config.dynamic_scopes_delimiter end def dynamic_scopes_present?(allowed, requested) allowed.include?(dynamic_scope_delimiter) && requested.include?(dynamic_scope_delimiter) end def dynamic_scope_match?(allowed, requested) allowed_pattern = allowed.split(dynamic_scope_delimiter, 2) request_pattern = requested.split(dynamic_scope_delimiter, 2) return false if allowed_pattern[0] != request_pattern[0] return false if allowed_pattern[1].blank? return false if request_pattern[1].blank? return true if allowed_pattern[1] == DYNAMIC_SCOPE_WILDCARD && allowed_pattern[1].present? allowed_pattern[1] == request_pattern[1] end def to_array(other) case other when Scopes other.all else other.to_a end end end end end ================================================ FILE: lib/doorkeeper/oauth/token.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class Token class << self def from_request(request, *methods) methods.inject(nil) do |_, method| method = self.method(method) if method.is_a?(Symbol) credentials = method.call(request) break credentials if credentials.present? end end def authenticate(request, *methods) if (token = from_request(request, *methods)) access_token = Doorkeeper.config.access_token_model.by_token(token) if access_token.present? && Doorkeeper.config.refresh_token_enabled? access_token.revoke_previous_refresh_token! end access_token end end def from_access_token_param(request) request.parameters[:access_token] end def from_bearer_param(request) request.parameters[:bearer_token] end def from_bearer_authorization(request) pattern = /^Bearer /i header = request.authorization token_from_header(header, pattern) if match?(header, pattern) end def from_basic_authorization(request) pattern = /^Basic /i header = request.authorization token_from_basic_header(header, pattern) if match?(header, pattern) end private def token_from_basic_header(header, pattern) encoded_header = token_from_header(header, pattern) decode_basic_credentials_token(encoded_header) end def decode_basic_credentials_token(encoded_header) Base64.decode64(encoded_header).split(/:/, 2).first end def token_from_header(header, pattern) header.gsub(pattern, "") end def match?(header, pattern) header&.match(pattern) end end end end end ================================================ FILE: lib/doorkeeper/oauth/token_introspection.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth # RFC7662 OAuth 2.0 Token Introspection # # @see https://datatracker.ietf.org/doc/html/rfc7662 class TokenIntrospection attr_reader :token, :error, :invalid_request_reason def initialize(server, token) @server = server @token = token end def authorized? authorize! @error.blank? end def error_response return if @error.blank? if @error == Errors::InvalidToken OAuth::InvalidTokenResponse.from_access_token(authorized_token) elsif @error == Errors::InvalidRequest OAuth::InvalidRequestResponse.from_request(self) else OAuth::ErrorResponse.from_request(self) end end def to_json(*) active? ? success_response : failure_response end private attr_reader :server # If the protected resource uses OAuth 2.0 client credentials to # authenticate to the introspection endpoint and its credentials are # invalid, the authorization server responds with an HTTP 401 # (Unauthorized) as described in Section 5.2 of OAuth 2.0 [RFC6749]. # # Endpoint must first validate the authentication. # If the authentication is invalid, the endpoint should respond with # an HTTP 401 status code and an invalid_client response. # # @see https://www.oauth.com/oauth2-servers/token-introspection-endpoint/ # # To prevent token scanning attacks, the endpoint MUST also require # some form of authorization to access this endpoint, such as client # authentication as described in OAuth 2.0 [RFC6749] or a separate # OAuth 2.0 access token such as the bearer token described in OAuth # 2.0 Bearer Token Usage [RFC6750]. # def authorize! # Requested client authorization if server.credentials authorize_using_basic_auth! elsif authorized_token authorize_using_bearer_token! else @error = Errors::InvalidRequest @invalid_request_reason = :request_not_authorized end end def authorize_using_basic_auth! # Note that a properly formed and authorized query for an inactive or # otherwise invalid token (or a token the protected resource is not # allowed to know about) is not considered an error response by this # specification. In these cases, the authorization server MUST instead # respond with an introspection response with the "active" field set to # "false" as described in Section 2.2. @error = Errors::InvalidClient unless authorized_client end def authorize_using_bearer_token! # Requested bearer token authorization # # If the protected resource uses an OAuth 2.0 bearer token to authorize # its call to the introspection endpoint and the token used for # authorization does not contain sufficient privileges or is otherwise # invalid for this request, the authorization server responds with an # HTTP 401 code as described in Section 3 of OAuth 2.0 Bearer Token # Usage [RFC6750]. # @error = Errors::InvalidToken unless valid_authorized_token? end # Client Authentication def authorized_client @authorized_client ||= server.credentials && server.client end # Bearer Token Authentication def authorized_token @authorized_token ||= Doorkeeper.authenticate(server.context.request) end # 2.2. Introspection Response def success_response response = { active: true, scope: @token.scopes_string, client_id: @token.try(:application).try(:uid), token_type: @token.token_type, iat: @token.created_at.to_i, } # `exp` is OPTIONAL per RFC 7662 §2.2; omit it for non-expiring tokens # so clients don't interpret `0` as "expired at 1970-01-01". response[:exp] = @token.expires_at.to_i if @token.expires_at customize_response(response) end # If the introspection call is properly authorized but the token is not # active, does not exist on this server, or the protected resource is # not allowed to introspect this particular token, then the # authorization server MUST return an introspection response with the # "active" field set to "false". Note that to avoid disclosing too # much of the authorization server's state to a third party, the # authorization server SHOULD NOT include any additional information # about an inactive token, including why the token is inactive. # # @see https://datatracker.ietf.org/doc/html/rfc7662 2.2. Introspection Response # def failure_response { active: false, } end # Boolean indicator of whether or not the presented token # is currently active. The specifics of a token's "active" state # will vary depending on the implementation of the authorization # server and the information it keeps about its tokens, but a "true" # value return for the "active" property will generally indicate # that a given token has been issued by this authorization server, # has not been revoked by the resource owner, and is within its # given time window of validity (e.g., after its issuance time and # before its expiration time). # # Any other error is considered an "inactive" token. # # * The token requested does not exist or is invalid # * The token expired # * The token was issued to a different client than is making this request # # Since resource servers using token introspection rely on the # authorization server to determine the state of a token, the # authorization server MUST perform all applicable checks against a # token's state. For instance, these tests include the following: # # o If the token can expire, the authorization server MUST determine # whether or not the token has expired. # o If the token can be issued before it is able to be used, the # authorization server MUST determine whether or not a token's valid # period has started yet. # o If the token can be revoked after it was issued, the authorization # server MUST determine whether or not such a revocation has taken # place. # o If the token has been signed, the authorization server MUST # validate the signature. # o If the token can be used only at certain resource servers, the # authorization server MUST determine whether or not the token can # be used at the resource server making the introspection call. # def active? if authorized_client valid_token? && token_introspection_allowed?(auth_client: authorized_client.application) else valid_token? end end # Token can be valid only if it is not expired or revoked. def valid_token? @token&.accessible? end def valid_authorized_token? !authorized_token_matches_introspected? && authorized_token.accessible? && token_introspection_allowed?(auth_token: authorized_token) end # RFC7662 Section 2.1 def authorized_token_matches_introspected? authorized_token.token == @token&.token end # Config constraints for introspection in Doorkeeper.config.allow_token_introspection def token_introspection_allowed?(auth_client: nil, auth_token: nil) allow_introspection = Doorkeeper.config.allow_token_introspection return allow_introspection unless allow_introspection.respond_to?(:call) allow_introspection.call(@token, auth_client, auth_token) end # Allows to customize introspection response. # Provides context (controller) and token for generating developer-specific # response. # # @see https://datatracker.ietf.org/doc/html/rfc7662#section-2.2 # def customize_response(response) customized_response = Doorkeeper.config.custom_introspection_response.call( token, server.context, ) return response if customized_response.blank? response.merge(customized_response) end end end end ================================================ FILE: lib/doorkeeper/oauth/token_request.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class TokenRequest attr_reader :pre_auth, :resource_owner def initialize(pre_auth, resource_owner) @pre_auth = pre_auth @resource_owner = resource_owner end def authorize auth = Authorization::Token.new(pre_auth, resource_owner) auth.issue_token! CodeResponse.new(pre_auth, auth, response_on_fragment: true) end def deny pre_auth.error = Errors::AccessDenied pre_auth.error_response end end end end ================================================ FILE: lib/doorkeeper/oauth/token_response.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth class TokenResponse attr_reader :token alias issued_token token def initialize(token) @token = token end def body @body ||= { "access_token" => token.plaintext_token, "token_type" => token.token_type, "expires_in" => token.expires_in_seconds, "refresh_token" => token.plaintext_refresh_token, "scope" => token.scopes_string, "created_at" => token.created_at.to_i, }.reject { |_, value| value.blank? } end def status :ok end def headers { "Cache-Control" => "no-store, no-cache", "Content-Type" => "application/json; charset=utf-8", "Pragma" => "no-cache", } end end end end ================================================ FILE: lib/doorkeeper/oauth.rb ================================================ # frozen_string_literal: true module Doorkeeper module OAuth GRANT_TYPES = [ AUTHORIZATION_CODE = "authorization_code", IMPLICIT = "implicit", PASSWORD = "password", CLIENT_CREDENTIALS = "client_credentials", REFRESH_TOKEN = "refresh_token", ].freeze end end ================================================ FILE: lib/doorkeeper/orm/active_record/access_grant.rb ================================================ # frozen_string_literal: true require "doorkeeper/orm/active_record/mixins/access_grant" module Doorkeeper class AccessGrant < ::ActiveRecord::Base include Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant end end ================================================ FILE: lib/doorkeeper/orm/active_record/access_token.rb ================================================ # frozen_string_literal: true require "doorkeeper/orm/active_record/mixins/access_token" module Doorkeeper class AccessToken < ::ActiveRecord::Base include Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken end end ================================================ FILE: lib/doorkeeper/orm/active_record/application.rb ================================================ # frozen_string_literal: true require "doorkeeper/orm/active_record/redirect_uri_validator" require "doorkeeper/orm/active_record/mixins/application" module Doorkeeper class Application < ::ActiveRecord::Base include ::Doorkeeper::Orm::ActiveRecord::Mixins::Application end end ================================================ FILE: lib/doorkeeper/orm/active_record/mixins/access_grant.rb ================================================ # frozen_string_literal: true module Doorkeeper::Orm::ActiveRecord::Mixins module AccessGrant extend ActiveSupport::Concern included do self.table_name = compute_doorkeeper_table_name self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default) include ::Doorkeeper::AccessGrantMixin belongs_to :application, class_name: Doorkeeper.config.application_class.to_s, optional: true, inverse_of: :access_grants validates :application_id, :token, :expires_in, :redirect_uri, presence: true validates :token, uniqueness: { case_sensitive: true } before_validation :generate_token, on: :create # We keep a volatile copy of the raw token for initial communication # The stored refresh_token may be mapped and not available in cleartext. # # Some strategies allow restoring stored secrets (e.g. symmetric encryption) # while hashing strategies do not, so you cannot rely on this value # returning a present value for persisted tokens. def plaintext_token if secret_strategy.allows_restoring_secrets? secret_strategy.restore_secret(self, :token) else @raw_token end end private # Generates token value with UniqueToken class. # # @return [String] token value # def generate_token @raw_token = Doorkeeper::OAuth::Helpers::UniqueToken.generate secret_strategy.store_secret(self, :token, @raw_token) end end module ClassMethods private def compute_doorkeeper_table_name table_name = "oauth_access_grant" table_name = table_name.pluralize if pluralize_table_names "#{table_name_prefix}#{table_name}#{table_name_suffix}" end end end end ================================================ FILE: lib/doorkeeper/orm/active_record/mixins/access_token.rb ================================================ # frozen_string_literal: true module Doorkeeper::Orm::ActiveRecord::Mixins module AccessToken extend ActiveSupport::Concern included do self.table_name = compute_doorkeeper_table_name self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default) include ::Doorkeeper::AccessTokenMixin belongs_to :application, class_name: Doorkeeper.config.application_class.to_s, inverse_of: :access_tokens, optional: true validates :token, presence: true, uniqueness: { case_sensitive: true } validates :refresh_token, uniqueness: { case_sensitive: true }, if: :use_refresh_token? # @attr_writer [Boolean, nil] use_refresh_token # indicates the possibility of using refresh token attr_writer :use_refresh_token before_validation :generate_token, on: :create before_validation :generate_refresh_token, on: :create, if: :use_refresh_token? end module ClassMethods # Searches for not revoked Access Tokens associated with the # specific Resource Owner. # # @param resource_owner [ActiveRecord::Base] # Resource Owner model instance # # @return [ActiveRecord::Relation] # active Access Tokens for Resource Owner # def active_for(resource_owner) by_resource_owner(resource_owner).where(revoked_at: nil) end # Determines if refresh tokens should be revoked only when the new access token is used, # rather than immediately upon refresh. This is based on the presence of the # `previous_refresh_token` column in the database. # # When true (column exists): # - Refresh tokens are NOT immediately revoked # - New access token stores the old refresh token value in `previous_refresh_token` # - Old refresh token is revoked later when the new access token is first used # - Multiple concurrent refresh requests can succeed (no database locks) # - Better database performance and lower latency # # When false (column does not exist): # - Refresh tokens are immediately revoked using database locks # - Only one concurrent refresh request can succeed # - May experience database lock contention under high load # # To enable the revoke-on-use feature and improve performance: # rails generate doorkeeper:previous_refresh_token # rails db:migrate # # @return [Boolean] true if previous_refresh_token column exists # def refresh_token_revoked_on_use? column_names.include?("previous_refresh_token") end # Returns non-expired and non-revoked access tokens def not_expired relation = where(revoked_at: nil) if supports_expiration_time_math? # have not reached the expiration time or it never expires relation.where("#{expiration_time_sql} > ?", Time.now.utc).or( relation.where(expires_in: nil) ) else ::Kernel.warn(::Doorkeeper::Models::ExpirationTimeSqlMath::WARNING_MESSAGE) relation end end private def compute_doorkeeper_table_name table_name = "oauth_access_token" table_name = table_name.pluralize if pluralize_table_names "#{table_name_prefix}#{table_name}#{table_name_suffix}" end end end end ================================================ FILE: lib/doorkeeper/orm/active_record/mixins/application.rb ================================================ # frozen_string_literal: true module Doorkeeper::Orm::ActiveRecord::Mixins module Application extend ActiveSupport::Concern included do self.table_name = compute_doorkeeper_table_name self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default) include ::Doorkeeper::ApplicationMixin has_many :access_grants, foreign_key: :application_id, dependent: :delete_all, class_name: Doorkeeper.config.access_grant_class.to_s has_many :access_tokens, foreign_key: :application_id, dependent: :delete_all, class_name: Doorkeeper.config.access_token_class.to_s validates :name, :uid, presence: true validates :secret, presence: true, if: -> { secret_required? } validates :uid, uniqueness: { case_sensitive: true } validates :confidential, inclusion: { in: [true, false] } validates_with Doorkeeper::RedirectUriValidator, attributes: [:redirect_uri] validate :scopes_match_configured, if: :enforce_scopes? before_validation :generate_uid, :generate_secret, on: :create has_many :authorized_tokens, -> { where(revoked_at: nil) }, foreign_key: :application_id, class_name: Doorkeeper.config.access_token_class.to_s has_many :authorized_applications, through: :authorized_tokens, source: :application # Generates a new secret for this application, intended to be used # for rotating the secret or in case of compromise. # # @return [String] new transformed secret value # def renew_secret @raw_secret = secret_generator.generate secret_strategy.store_secret(self, :secret, @raw_secret) end # We keep a volatile copy of the raw secret for initial communication # The stored refresh_token may be mapped and not available in cleartext. # # Some strategies allow restoring stored secrets (e.g. symmetric encryption) # while hashing strategies do not, so you cannot rely on this value # returning a present value for persisted tokens. def plaintext_secret if secret_strategy.allows_restoring_secrets? secret_strategy.restore_secret(self, :secret) else @raw_secret end end # Represents client as set of it's attributes in JSON format. # This is the right way how we want to override ActiveRecord #to_json. # # Respects privacy settings and serializes minimum set of attributes # for public/private clients and full set for authorized owners. # # @return [Hash] entity attributes for JSON # def as_json(options = {}) # if application belongs to some owner we need to check if it's the same as # the one passed in the options or check if we render the client as an owner if (respond_to?(:owner) && owner && owner == options[:current_resource_owner]) || options[:as_owner] # Owners can see all the client attributes, fallback to ActiveModel serialization super else # if application has no owner or it's owner doesn't match one from the options # we render only minimum set of attributes that could be exposed to a public only = extract_serializable_attributes(options) super(options.merge(only: only)) end end def authorized_for_resource_owner?(resource_owner) Doorkeeper.configuration.authorize_resource_owner_for_client.call(self, resource_owner) end # We need to hook into this method to allow serializing plan-text secrets # when secrets hashing enabled. # # @param key [String] attribute name # def read_attribute_for_serialization(key) return super unless key.to_s == "secret" plaintext_secret || secret end private def secret_generator generator_name = Doorkeeper.config.application_secret_generator generator = generator_name.constantize return generator if generator.respond_to?(:generate) raise Errors::UnableToGenerateToken, "#{generator} does not respond to `.generate`." rescue NameError raise Errors::TokenGeneratorNotFound, "#{generator_name} not found" end def generate_uid self.uid = Doorkeeper::OAuth::Helpers::UniqueToken.generate if uid.blank? end def generate_secret return if secret.present? || !secret_required? renew_secret end def scopes_match_configured if scopes.present? && !Doorkeeper::OAuth::Helpers::ScopeChecker.valid?( scope_str: scopes.to_s, server_scopes: Doorkeeper.config.scopes, ) errors.add(:scopes, :not_match_configured) end end def enforce_scopes? Doorkeeper.config.enforce_configured_scopes? end def secret_required? confidential? || !self.class.columns.detect { |column| column.name == "secret" }&.null end # Helper method to extract collection of serializable attribute names # considering serialization options (like `only`, `except` and so on). # # @param options [Hash] serialization options # # @return [Array] # collection of attributes to be serialized using #as_json # def extract_serializable_attributes(options = {}) opts = options.try(:dup) || {} only = Array.wrap(opts[:only]).map(&:to_s) only = if only.blank? client_serializable_attributes else only & client_serializable_attributes end only -= Array.wrap(opts[:except]).map(&:to_s) if opts.key?(:except) only.uniq end # Collection of attributes that could be serialized for public. # Override this method if you need additional attributes to be serialized. # # @return [Array] collection of serializable attributes # # NOTE: `serializable_attributes` method already taken by Rails >= 6 # def client_serializable_attributes attributes = %w[id name created_at] attributes << "uid" unless confidential? attributes end end module ClassMethods # Returns Applications associated with active (not revoked) Access Tokens # that are owned by the specific Resource Owner. # # @param resource_owner [ActiveRecord::Base] # Resource Owner model instance # # @return [ActiveRecord::Relation] # Applications authorized for the Resource Owner # def authorized_for(resource_owner) resource_access_tokens = Doorkeeper.config.access_token_model.active_for(resource_owner) where(id: resource_access_tokens.select(:application_id).distinct) end # Revokes AccessToken and AccessGrant records that have not been revoked and # associated with the specific Application and Resource Owner. # # @param resource_owner [ActiveRecord::Base] # instance of the Resource Owner model # def revoke_tokens_and_grants_for(id, resource_owner) Doorkeeper.config.access_token_model.revoke_all_for(id, resource_owner) Doorkeeper.config.access_grant_model.revoke_all_for(id, resource_owner) end private def compute_doorkeeper_table_name table_name = "oauth_application" table_name = table_name.pluralize if pluralize_table_names "#{table_name_prefix}#{table_name}#{table_name_suffix}" end end end end ================================================ FILE: lib/doorkeeper/orm/active_record/redirect_uri_validator.rb ================================================ # frozen_string_literal: true require "uri" module Doorkeeper # ActiveModel validator for redirect URI validation in according # to OAuth standards and Doorkeeper configuration. class RedirectUriValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) if value.blank? return if Doorkeeper.config.allow_blank_redirect_uri?(record) record.errors.add(attribute, :blank) else value.split.each do |val| next if oob_redirect_uri?(val) uri = ::URI.parse(val) record.errors.add(attribute, :forbidden_uri) if forbidden_uri?(uri) record.errors.add(attribute, :fragment_present) unless uri.fragment.nil? record.errors.add(attribute, :unspecified_scheme) if unspecified_scheme?(uri) record.errors.add(attribute, :relative_uri) if relative_uri?(uri) record.errors.add(attribute, :secured_uri) if invalid_ssl_uri?(uri) record.errors.add(attribute, :invalid_uri) if unspecified_host?(uri) end end rescue URI::InvalidURIError record.errors.add(attribute, :invalid_uri) end private def oob_redirect_uri?(uri) Doorkeeper::OAuth::NonStandard::IETF_WG_OAUTH2_OOB_METHODS.include?(uri) end def forbidden_uri?(uri) Doorkeeper.config.forbid_redirect_uri.call(uri) end def unspecified_scheme?(uri) return true if uri.opaque.present? %w[localhost].include?(uri.try(:scheme)) end def unspecified_host?(uri) uri.is_a?(URI::HTTP) && uri.host.blank? end def relative_uri?(uri) uri.scheme.nil? && uri.host.blank? end def invalid_ssl_uri?(uri) forces_ssl = Doorkeeper.config.force_ssl_in_redirect_uri non_https = uri.try(:scheme) == "http" if forces_ssl.respond_to?(:call) forces_ssl.call(uri) && non_https else forces_ssl && non_https end end end end ================================================ FILE: lib/doorkeeper/orm/active_record/stale_records_cleaner.rb ================================================ # frozen_string_literal: true module Doorkeeper module Orm module ActiveRecord # Helper class to clear stale and non-active tokens and grants. # Used by Doorkeeper Rake tasks. # class StaleRecordsCleaner def initialize(base_scope) @base_scope = base_scope end # Clears revoked records def clean_revoked table = @base_scope.arel_table @base_scope .where.not(revoked_at: nil) .where(table[:revoked_at].lt(Time.current)) .in_batches(&:delete_all) end # Clears expired records def clean_expired(ttl) table = @base_scope.arel_table model_class = @base_scope.is_a?(::ActiveRecord::Relation) ? @base_scope.klass : @base_scope scope = @base_scope .where.not(expires_in: nil) .where(table[:created_at].lt(Time.current - ttl)) if model_class.respond_to?(:supports_expiration_time_math?) && model_class.supports_expiration_time_math? scope = scope.where("#{model_class.expiration_time_sql} < ?", Time.current) else ::Kernel.warn(::Doorkeeper::Models::ExpirationTimeSqlMath::WARNING_MESSAGE) end scope.in_batches(&:delete_all) end end end end end ================================================ FILE: lib/doorkeeper/orm/active_record.rb ================================================ # frozen_string_literal: true module Doorkeeper autoload :AccessGrant, "doorkeeper/orm/active_record/access_grant" autoload :AccessToken, "doorkeeper/orm/active_record/access_token" autoload :Application, "doorkeeper/orm/active_record/application" autoload :RedirectUriValidator, "doorkeeper/orm/active_record/redirect_uri_validator" module Models autoload :Ownership, "doorkeeper/models/concerns/ownership" end # ActiveRecord ORM for Doorkeeper entity models. # Consists of three main OAuth entities: # * Access Token # * Access Grant # * Application (client) # # Do a lazy loading of all the required and configured stuff. # module Orm module ActiveRecord autoload :StaleRecordsCleaner, "doorkeeper/orm/active_record/stale_records_cleaner" module Mixins autoload :AccessGrant, "doorkeeper/orm/active_record/mixins/access_grant" autoload :AccessToken, "doorkeeper/orm/active_record/mixins/access_token" autoload :Application, "doorkeeper/orm/active_record/mixins/application" end def self.run_hooks initialize_configured_associations end def self.initialize_configured_associations # NOTE: on_load block is instance_exec'd on ActiveRecord::Base, # so use fully qualified references (e.g. Doorkeeper.config). ActiveSupport.on_load(:active_record) do if Doorkeeper.config.enable_application_owner? Doorkeeper.config.application_model.include ::Doorkeeper::Models::Ownership end Doorkeeper.config.access_grant_model.include ::Doorkeeper::Models::PolymorphicResourceOwner::ForAccessGrant Doorkeeper.config.access_token_model.include ::Doorkeeper::Models::PolymorphicResourceOwner::ForAccessToken end end end end end ================================================ FILE: lib/doorkeeper/rails/helpers.rb ================================================ # frozen_string_literal: true module Doorkeeper module Rails module Helpers def doorkeeper_authorize!(*scopes) @_doorkeeper_scopes = scopes.presence || Doorkeeper.config.default_scopes doorkeeper_render_error unless valid_doorkeeper_token? end def doorkeeper_unauthorized_render_options(**); end def doorkeeper_forbidden_render_options(**); end def valid_doorkeeper_token? doorkeeper_token&.acceptable?(@_doorkeeper_scopes) end private def doorkeeper_render_error error = doorkeeper_error error.raise_exception! if Doorkeeper.config.raise_on_errors? headers.merge!(error.headers.reject { |k| k == "Content-Type" }) doorkeeper_render_error_with(error) end def doorkeeper_render_error_with(error) options = doorkeeper_render_options(error) || {} status = doorkeeper_status_for_error( error, options.delete(:respond_not_found_when_forbidden), ) if options.blank? head status else options[:status] = status options[:layout] = false if options[:layout].nil? render options end end def doorkeeper_error if doorkeeper_invalid_token_response? OAuth::InvalidTokenResponse.from_access_token(doorkeeper_token) else OAuth::ForbiddenTokenResponse.from_scopes(@_doorkeeper_scopes) end end def doorkeeper_render_options(error) if doorkeeper_invalid_token_response? doorkeeper_unauthorized_render_options(error: error) else doorkeeper_forbidden_render_options(error: error) end end def doorkeeper_status_for_error(error, respond_not_found_when_forbidden) if respond_not_found_when_forbidden && error.status == :forbidden :not_found else error.status end end def doorkeeper_invalid_token_response? !doorkeeper_token || !doorkeeper_token.accessible? end def doorkeeper_token return @doorkeeper_token if defined?(@doorkeeper_token) @doorkeeper_token = OAuth::Token.authenticate( request, *Doorkeeper.config.access_token_methods, ) end end end end ================================================ FILE: lib/doorkeeper/rails/routes/abstract_router.rb ================================================ # frozen_string_literal: true module Doorkeeper module Rails # Abstract router module that implements base behavior # for generating and mapping Rails routes. # # Could be reused in Doorkeeper extensions. # module AbstractRouter extend ActiveSupport::Concern attr_reader :routes def initialize(routes, mapper = Mapper.new, &block) @routes = routes @mapping = mapper.map(&block) end def generate_routes!(**_options) raise NotImplementedError, "must be redefined for #{self.class.name}!" end private def map_route(name, method) return if @mapping.skipped?(name) send(method, @mapping[name]) mapping[name] = @mapping[name] end end end end ================================================ FILE: lib/doorkeeper/rails/routes/mapper.rb ================================================ # frozen_string_literal: true module Doorkeeper module Rails class Routes # :nodoc: class Mapper def initialize(mapping = Mapping.new) @mapping = mapping end def map(&block) instance_eval(&block) if block @mapping end def controllers(controller_names = {}) @mapping.controllers.merge!(controller_names) end def skip_controllers(*controller_names) @mapping.skips = controller_names end def as(alias_names = {}) @mapping.as.merge!(alias_names) end end end end end ================================================ FILE: lib/doorkeeper/rails/routes/mapping.rb ================================================ # frozen_string_literal: true module Doorkeeper module Rails class Routes # :nodoc: class Mapping attr_accessor :controllers, :as, :skips def initialize @controllers = { authorizations: "doorkeeper/authorizations", applications: "doorkeeper/applications", authorized_applications: "doorkeeper/authorized_applications", tokens: "doorkeeper/tokens", token_info: "doorkeeper/token_info", } @as = { authorizations: :authorization, tokens: :token, token_info: :token_info, } @skips = [] end def [](routes) { controllers: @controllers[routes], as: @as[routes], } end def skipped?(controller) @skips.include?(controller) end end end end end ================================================ FILE: lib/doorkeeper/rails/routes/registry.rb ================================================ # frozen_string_literal: true module Doorkeeper module Rails class Routes # Thread-safe registry of any Doorkeeper additional routes. # Used to allow implementing of Doorkeeper extensions that must # use their own routes. # module Registry ROUTES_ACCESS_LOCK = Mutex.new ROUTES_DEFINITION_LOCK = Mutex.new InvalidRouterClass = Class.new(StandardError) # Collection of additional registered routes for Doorkeeper. # # @return [Array] set of registered routes # def registered_routes ROUTES_DEFINITION_LOCK.synchronize do @registered_routes ||= Set.new end end # Registers additional routes in the Doorkeeper registry # # @param [Object] routes # routes class # def register_routes(routes) if !routes.is_a?(Module) || !(routes < AbstractRouter) raise InvalidRouterClass, "routes class must include Doorkeeper::Rails::AbstractRouter" end ROUTES_ACCESS_LOCK.synchronize do registered_routes << routes end end alias register register_routes end end end end ================================================ FILE: lib/doorkeeper/rails/routes.rb ================================================ # frozen_string_literal: true require "doorkeeper/rails/routes/mapping" require "doorkeeper/rails/routes/mapper" require "doorkeeper/rails/routes/abstract_router" require "doorkeeper/rails/routes/registry" module Doorkeeper module Rails class Routes # :nodoc: module Helper def use_doorkeeper(options = {}, &block) Doorkeeper::Rails::Routes.new(self, &block).generate_routes!(options) end end include AbstractRouter extend Registry mattr_reader :mapping do {} end def self.install! ActionDispatch::Routing::Mapper.include Doorkeeper::Rails::Routes::Helper registered_routes.each(&:install!) end def initialize(routes, mapper = Mapper.new, &block) super end def generate_routes!(options) routes.scope options[:scope] || "oauth", as: "oauth" do map_route(:authorizations, :authorization_routes) map_route(:tokens, :token_routes) map_route(:tokens, :revoke_routes) map_route(:tokens, :introspect_routes) if introspection_routes? map_route(:applications, :application_routes) map_route(:authorized_applications, :authorized_applications_routes) map_route(:token_info, :token_info_routes) end end private def authorization_routes(mapping) routes.resource( :authorization, path: "authorize", only: %i[create destroy], as: mapping[:as], controller: mapping[:controllers], ) do routes.get native_authorization_code_route, action: :show, on: :member routes.get '/', action: :new, on: :member end end def token_routes(mapping) routes.resource( :token, path: "token", only: [:create], as: mapping[:as], controller: mapping[:controllers], ) end def revoke_routes(mapping) routes.post "revoke", controller: mapping[:controllers], action: :revoke end def introspect_routes(mapping) routes.post "introspect", controller: mapping[:controllers], action: :introspect end def token_info_routes(mapping) routes.resource( :token_info, path: "token/info", only: [:show], as: mapping[:as], controller: mapping[:controllers], ) end def application_routes(mapping) routes.resources :doorkeeper_applications, controller: mapping[:controllers], as: :applications, path: "applications" end def authorized_applications_routes(mapping) routes.resources :authorized_applications, only: %i[index destroy], controller: mapping[:controllers] end def native_authorization_code_route Doorkeeper.configuration.native_authorization_code_route end def introspection_routes? Doorkeeper.configured? && !Doorkeeper.config.allow_token_introspection.is_a?(FalseClass) end end end end ================================================ FILE: lib/doorkeeper/rake/db.rake ================================================ # frozen_string_literal: true namespace :doorkeeper do namespace :db do desc "Removes stale data from doorkeeper related database tables" task cleanup: [ "doorkeeper:db:cleanup:revoked_tokens", "doorkeeper:db:cleanup:expired_tokens", "doorkeeper:db:cleanup:revoked_grants", "doorkeeper:db:cleanup:expired_grants", ] namespace :cleanup do desc "Removes stale access tokens" task revoked_tokens: "doorkeeper:setup" do cleaner = Doorkeeper::StaleRecordsCleaner.new(Doorkeeper.config.access_token_model) cleaner.clean_revoked end desc "Removes expired (TTL passed) access tokens" task expired_tokens: "doorkeeper:setup" do expirable_tokens = Doorkeeper.config.access_token_model.where(refresh_token: nil) cleaner = Doorkeeper::StaleRecordsCleaner.new(expirable_tokens) cleaner.clean_expired(Doorkeeper.config.access_token_expires_in) end desc "Removes stale access grants" task revoked_grants: "doorkeeper:setup" do cleaner = Doorkeeper::StaleRecordsCleaner.new(Doorkeeper.config.access_grant_model) cleaner.clean_revoked end desc "Removes expired (TTL passed) access grants" task expired_grants: "doorkeeper:setup" do cleaner = Doorkeeper::StaleRecordsCleaner.new(Doorkeeper.config.access_grant_model) cleaner.clean_expired(Doorkeeper.config.authorization_code_expires_in) end end end end ================================================ FILE: lib/doorkeeper/rake/setup.rake ================================================ # frozen_string_literal: true namespace :doorkeeper do task setup: :environment do end end ================================================ FILE: lib/doorkeeper/rake.rb ================================================ # frozen_string_literal: true module Doorkeeper module Rake class << self def load_tasks glob = File.join(File.absolute_path(__dir__), "rake", "*.rake") Dir[glob].each do |rake_file| load rake_file end end end end end ================================================ FILE: lib/doorkeeper/request/authorization_code.rb ================================================ # frozen_string_literal: true module Doorkeeper module Request class AuthorizationCode < Strategy delegate :client, :parameters, to: :server def request @request ||= OAuth::AuthorizationCodeRequest.new( Doorkeeper.config, grant, client, parameters, ) end private def grant raise Errors::MissingRequiredParameter, :code if parameters[:code].blank? Doorkeeper.config.access_grant_model.by_token(parameters[:code]) end end end end ================================================ FILE: lib/doorkeeper/request/client_credentials.rb ================================================ # frozen_string_literal: true module Doorkeeper module Request class ClientCredentials < Strategy delegate :client, :parameters, to: :server def request @request ||= OAuth::ClientCredentialsRequest.new( Doorkeeper.config, client, parameters, ) end end end end ================================================ FILE: lib/doorkeeper/request/code.rb ================================================ # frozen_string_literal: true module Doorkeeper module Request class Code < Strategy delegate :current_resource_owner, to: :server def pre_auth server.context.send(:pre_auth) end def request @request ||= OAuth::CodeRequest.new(pre_auth, current_resource_owner) end end end end ================================================ FILE: lib/doorkeeper/request/password.rb ================================================ # frozen_string_literal: true module Doorkeeper module Request class Password < Strategy delegate :credentials, :resource_owner, :parameters, :client, to: :server def request @request ||= OAuth::PasswordAccessTokenRequest.new( Doorkeeper.config, client, credentials, resource_owner, parameters, ) end end end end ================================================ FILE: lib/doorkeeper/request/refresh_token.rb ================================================ # frozen_string_literal: true module Doorkeeper module Request class RefreshToken < Strategy delegate :credentials, :parameters, to: :server def refresh_token Doorkeeper.config.access_token_model.by_refresh_token(parameters[:refresh_token]) end def request @request ||= OAuth::RefreshTokenRequest.new( Doorkeeper.config, refresh_token, credentials, parameters, ) end end end end ================================================ FILE: lib/doorkeeper/request/strategy.rb ================================================ # frozen_string_literal: true module Doorkeeper module Request class Strategy attr_reader :server delegate :authorize, to: :request def initialize(server) @server = server end def request raise NotImplementedError, "request strategies must define #request" end end end end ================================================ FILE: lib/doorkeeper/request/token.rb ================================================ # frozen_string_literal: true module Doorkeeper module Request class Token < Strategy delegate :current_resource_owner, to: :server def pre_auth server.context.send(:pre_auth) end def request @request ||= OAuth::TokenRequest.new(pre_auth, current_resource_owner) end end end end ================================================ FILE: lib/doorkeeper/request.rb ================================================ # frozen_string_literal: true module Doorkeeper module Request class << self def authorization_strategy(response_type) grant_flow = authorization_flows.detect do |flow| flow.matches_response_type?(response_type) end if grant_flow grant_flow.response_type_strategy else # [NOTE]: this will be removed in a newer versions of Doorkeeper. # For retro-compatibility only build_fallback_strategy_class(response_type) end end def token_strategy(grant_type) raise Errors::MissingRequiredParameter, :grant_type if grant_type.blank? grant_flow = token_flows.detect do |flow| flow.matches_grant_type?(grant_type) end if grant_flow grant_flow.grant_type_strategy else # [NOTE]: this will be removed in a newer versions of Doorkeeper. # For retro-compatibility only raise Errors::InvalidTokenStrategy unless available.include?(grant_type.to_s) strategy_class = build_fallback_strategy_class(grant_type) raise Errors::InvalidTokenStrategy unless strategy_class strategy_class end end private def authorization_flows Doorkeeper.configuration.authorization_response_flows end def token_flows Doorkeeper.configuration.token_grant_flows end # [NOTE]: this will be removed in a newer versions of Doorkeeper. # For retro-compatibility only def available Doorkeeper.config.deprecated_token_grant_types_resolver end def build_fallback_strategy_class(grant_or_request_type) strategy_class_name = grant_or_request_type.to_s.tr(" ", "_").camelize fallback_strategy = "Doorkeeper::Request::#{strategy_class_name}".constantize ::Kernel.warn <<~WARNING [DOORKEEPER] #{fallback_strategy} found using fallback, it must be registered using `Doorkeeper::GrantFlow.register(grant_flow_name, **options)`. This functionality will be removed in a newer versions of Doorkeeper. WARNING fallback_strategy rescue NameError raise Errors::InvalidTokenStrategy end end end end ================================================ FILE: lib/doorkeeper/revocable_tokens/revocable_access_token.rb ================================================ # frozen_string_literal: true module Doorkeeper module RevocableTokens class RevocableAccessToken attr_reader :token def initialize(token) @token = token end def revocable? token.accessible? end def revoke token.revoke end end end end ================================================ FILE: lib/doorkeeper/revocable_tokens/revocable_refresh_token.rb ================================================ # frozen_string_literal: true module Doorkeeper module RevocableTokens class RevocableRefreshToken attr_reader :token def initialize(token) @token = token end def revocable? !token.revoked? end def revoke token.revoke end end end end ================================================ FILE: lib/doorkeeper/secret_storing/base.rb ================================================ # frozen_string_literal: true module Doorkeeper module SecretStoring ## # Base class for secret storing, including common helpers class Base ## # Return the value to be stored by the database # used for looking up a database value. # @param plain_secret The plain secret input / generated def self.transform_secret(_plain_secret) raise NotImplementedError end ## # Transform and store the given secret attribute => value # pair used for safely storing the attribute # @param resource The model instance being modified # @param attribute The secret attribute # @param plain_secret The plain secret input / generated def self.store_secret(resource, attribute, plain_secret) transformed_value = transform_secret(plain_secret) resource.public_send(:"#{attribute}=", transformed_value) transformed_value end ## # Return the restored value from the database # @param resource The resource instance to act on # @param attribute The secret attribute to restore # as retrieved from the database. def self.restore_secret(_resource, _attribute) raise NotImplementedError end ## # Determines whether this strategy supports restoring # secrets from the database. This allows detecting users # trying to use a non-restorable strategy with +reuse_access_tokens+. def self.allows_restoring_secrets? false end ## # Determines what secrets this strategy is applicable for def self.validate_for(model) valid = %i[token application] return true if valid.include?(model.to_sym) raise ArgumentError, "'#{name}' can not be used for #{model}." end ## # Securely compare the given +input+ value with a +stored+ value # processed by +transform_secret+. def self.secret_matches?(input, stored) transformed_input = transform_secret(input) ActiveSupport::SecurityUtils.secure_compare transformed_input, stored end end end end ================================================ FILE: lib/doorkeeper/secret_storing/bcrypt.rb ================================================ # frozen_string_literal: true module Doorkeeper module SecretStoring ## # Plain text secret storing, which is the default # but also provides fallback lookup if # other secret storing mechanisms are enabled. class BCrypt < Base ## # Return the value to be stored by the database # @param plain_secret The plain secret input / generated def self.transform_secret(plain_secret) ::BCrypt::Password.create(plain_secret.to_s) end ## # Securely compare the given +input+ value with a +stored+ value # processed by +transform_secret+. def self.secret_matches?(input, stored) ::BCrypt::Password.new(stored.to_s) == input.to_s rescue ::BCrypt::Errors::InvalidHash false end ## # Determines whether this strategy supports restoring # secrets from the database. This allows detecting users # trying to use a non-restorable strategy with +reuse_access_tokens+. def self.allows_restoring_secrets? false end ## # Determines what secrets this strategy is applicable for def self.validate_for(model) unless model.to_sym == :application raise ArgumentError, "'#{name}' can only be used for storing application secrets." end unless bcrypt_present? raise ArgumentError, "'#{name}' requires the 'bcrypt' gem being loaded." end true end ## # Test if we can require the BCrypt gem def self.bcrypt_present? require "bcrypt" true rescue LoadError false end end end end ================================================ FILE: lib/doorkeeper/secret_storing/plain.rb ================================================ # frozen_string_literal: true module Doorkeeper module SecretStoring ## # Plain text secret storing, which is the default # but also provides fallback lookup if # other secret storing mechanisms are enabled. class Plain < Base ## # Return the value to be stored by the database # @param plain_secret The plain secret input / generated def self.transform_secret(plain_secret) plain_secret end ## # Return the restored value from the database # @param resource The resource instance to act on # @param attribute The secret attribute to restore # as retrieved from the database. def self.restore_secret(resource, attribute) resource.public_send(attribute) end ## # Plain values obviously allow restoring def self.allows_restoring_secrets? true end end end end ================================================ FILE: lib/doorkeeper/secret_storing/sha256_hash.rb ================================================ # frozen_string_literal: true module Doorkeeper module SecretStoring ## # Plain text secret storing, which is the default # but also provides fallback lookup if # other secret storing mechanisms are enabled. class Sha256Hash < Base ## # Return the value to be stored by the database # @param plain_secret The plain secret input / generated def self.transform_secret(plain_secret) ::Digest::SHA256.hexdigest plain_secret end ## # Determines whether this strategy supports restoring # secrets from the database. This allows detecting users # trying to use a non-restorable strategy with +reuse_access_tokens+. def self.allows_restoring_secrets? false end end end end ================================================ FILE: lib/doorkeeper/server.rb ================================================ # frozen_string_literal: true module Doorkeeper class Server attr_reader :context def initialize(context) @context = context end def authorization_request(strategy) klass = Request.authorization_strategy(strategy) klass.new(self) end def token_request(strategy) klass = Request.token_strategy(strategy) klass.new(self) end # TODO: context should be the request def parameters context.request.parameters end def client @client ||= OAuth::Client.authenticate(credentials) end def current_resource_owner context.send :current_resource_owner end # TODO: Use configuration and evaluate proper context on block def resource_owner context.send :resource_owner_from_credentials end def credentials methods = Doorkeeper.config.client_credentials_methods @credentials ||= OAuth::Client::Credentials.from_request(context.request, *methods) end end end ================================================ FILE: lib/doorkeeper/stale_records_cleaner.rb ================================================ # frozen_string_literal: true module Doorkeeper class StaleRecordsCleaner CLEANER_CLASS = "StaleRecordsCleaner" def self.for(base_scope) orm_adapter = "doorkeeper/orm/#{configured_orm}".classify orm_cleaner = "#{orm_adapter}::#{CLEANER_CLASS}".constantize orm_cleaner.new(base_scope) rescue NameError raise Doorkeeper::Errors::NoOrmCleaner, "'#{configured_orm}' ORM has no cleaner!" end def self.new(base_scope) self.for(base_scope) end def self.configured_orm Doorkeeper.config.orm end end end ================================================ FILE: lib/doorkeeper/validations.rb ================================================ # frozen_string_literal: true module Doorkeeper module Validations extend ActiveSupport::Concern attr_accessor :error def validate @error = nil self.class.validations.each do |validation| @error = validation[:options][:error] unless send("validate_#{validation[:attribute]}") break if @error end end def valid? validate @error.nil? end module ClassMethods def validate(attribute, options = {}) validations << { attribute: attribute, options: options } end def validations @validations ||= [] end end end end ================================================ FILE: lib/doorkeeper/version.rb ================================================ # frozen_string_literal: true module Doorkeeper module VERSION # Semantic versioning MAJOR = 5 MINOR = 9 TINY = 0 PRE = nil # Full version number STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end end ================================================ FILE: lib/doorkeeper.rb ================================================ # frozen_string_literal: true require "doorkeeper/config" require "doorkeeper/engine" # Main Doorkeeper namespace. # module Doorkeeper autoload :Errors, "doorkeeper/errors" autoload :GrantFlow, "doorkeeper/grant_flow" autoload :OAuth, "doorkeeper/oauth" autoload :Rake, "doorkeeper/rake" autoload :Request, "doorkeeper/request" autoload :Server, "doorkeeper/server" autoload :StaleRecordsCleaner, "doorkeeper/stale_records_cleaner" autoload :Validations, "doorkeeper/validations" autoload :VERSION, "doorkeeper/version" autoload :AccessGrantMixin, "doorkeeper/models/access_grant_mixin" autoload :AccessTokenMixin, "doorkeeper/models/access_token_mixin" autoload :ApplicationMixin, "doorkeeper/models/application_mixin" module Helpers autoload :Controller, "doorkeeper/helpers/controller" end module Request autoload :Strategy, "doorkeeper/request/strategy" autoload :AuthorizationCode, "doorkeeper/request/authorization_code" autoload :ClientCredentials, "doorkeeper/request/client_credentials" autoload :Code, "doorkeeper/request/code" autoload :Password, "doorkeeper/request/password" autoload :RefreshToken, "doorkeeper/request/refresh_token" autoload :Token, "doorkeeper/request/token" end module RevocableTokens autoload :RevocableAccessToken, "doorkeeper/revocable_tokens/revocable_access_token" autoload :RevocableRefreshToken, "doorkeeper/revocable_tokens/revocable_refresh_token" end module OAuth autoload :BaseRequest, "doorkeeper/oauth/base_request" autoload :AuthorizationCodeRequest, "doorkeeper/oauth/authorization_code_request" autoload :BaseResponse, "doorkeeper/oauth/base_response" autoload :CodeResponse, "doorkeeper/oauth/code_response" autoload :Client, "doorkeeper/oauth/client" autoload :ClientCredentialsRequest, "doorkeeper/oauth/client_credentials_request" autoload :CodeRequest, "doorkeeper/oauth/code_request" autoload :ErrorResponse, "doorkeeper/oauth/error_response" autoload :Error, "doorkeeper/oauth/error" autoload :InvalidTokenResponse, "doorkeeper/oauth/invalid_token_response" autoload :InvalidRequestResponse, "doorkeeper/oauth/invalid_request_response" autoload :ForbiddenTokenResponse, "doorkeeper/oauth/forbidden_token_response" autoload :NonStandard, "doorkeeper/oauth/nonstandard" autoload :PasswordAccessTokenRequest, "doorkeeper/oauth/password_access_token_request" autoload :PreAuthorization, "doorkeeper/oauth/pre_authorization" autoload :RefreshTokenRequest, "doorkeeper/oauth/refresh_token_request" autoload :Scopes, "doorkeeper/oauth/scopes" autoload :Token, "doorkeeper/oauth/token" autoload :TokenIntrospection, "doorkeeper/oauth/token_introspection" autoload :TokenRequest, "doorkeeper/oauth/token_request" autoload :TokenResponse, "doorkeeper/oauth/token_response" module Authorization autoload :Code, "doorkeeper/oauth/authorization/code" autoload :Context, "doorkeeper/oauth/authorization/context" autoload :Token, "doorkeeper/oauth/authorization/token" autoload :URIBuilder, "doorkeeper/oauth/authorization/uri_builder" end class Client autoload :Credentials, "doorkeeper/oauth/client/credentials" end module ClientCredentials autoload :Validator, "doorkeeper/oauth/client_credentials/validator" autoload :Creator, "doorkeeper/oauth/client_credentials/creator" autoload :Issuer, "doorkeeper/oauth/client_credentials/issuer" end module Helpers autoload :ScopeChecker, "doorkeeper/oauth/helpers/scope_checker" autoload :URIChecker, "doorkeeper/oauth/helpers/uri_checker" autoload :UniqueToken, "doorkeeper/oauth/helpers/unique_token" end module Hooks autoload :Context, "doorkeeper/oauth/hooks/context" end end module Models autoload :Accessible, "doorkeeper/models/concerns/accessible" autoload :Expirable, "doorkeeper/models/concerns/expirable" autoload :ExpirationTimeSqlMath, "doorkeeper/models/concerns/expiration_time_sql_math" autoload :Orderable, "doorkeeper/models/concerns/orderable" autoload :PolymorphicResourceOwner, "doorkeeper/models/concerns/polymorphic_resource_owner" autoload :Scopes, "doorkeeper/models/concerns/scopes" autoload :Reusable, "doorkeeper/models/concerns/reusable" autoload :ResourceOwnerable, "doorkeeper/models/concerns/resource_ownerable" autoload :Revocable, "doorkeeper/models/concerns/revocable" autoload :SecretStorable, "doorkeeper/models/concerns/secret_storable" module Concerns autoload :WriteToPrimary, "doorkeeper/models/concerns/write_to_primary" end end module Orm autoload :ActiveRecord, "doorkeeper/orm/active_record" end module Rails autoload :Helpers, "doorkeeper/rails/helpers" autoload :Routes, "doorkeeper/rails/routes" end module SecretStoring autoload :Base, "doorkeeper/secret_storing/base" autoload :Plain, "doorkeeper/secret_storing/plain" autoload :Sha256Hash, "doorkeeper/secret_storing/sha256_hash" autoload :BCrypt, "doorkeeper/secret_storing/bcrypt" end class << self attr_reader :orm_adapter def configure(&block) @config = Config::Builder.new(&block).build setup @config end # @return [Doorkeeper::Config] configuration instance # def configuration @config || configure end def configured? !@config.nil? end alias config configuration def setup setup_orm_adapter # Deprecated, will be removed soon unless configuration.orm == :active_record setup_orm_models setup_application_owner end end def setup_filter_parameters return unless defined?(::Rails) && ::Rails.application && configured? parameters = %w[client_secret authentication_token access_token refresh_token] parameters << "code" if configuration.grant_flows.include?("authorization_code") filter = /^(#{Regexp.union(parameters)})$/ filter_params = ::Rails.application.config.filter_parameters filter_params << filter unless filter_params.include?(filter) end def setup_orm_adapter @orm_adapter = "doorkeeper/orm/#{configuration.orm}".classify.constantize rescue NameError => e raise e, "ORM adapter not found (#{configuration.orm})", <<-ERROR_MSG.strip_heredoc [DOORKEEPER] ORM adapter not found (#{configuration.orm}), or there was an error trying to load it. You probably need to add the related gem for this adapter to work with doorkeeper. ERROR_MSG end def run_orm_hooks config.clear_cache! if @orm_adapter.respond_to?(:run_hooks) @orm_adapter.run_hooks else ::Kernel.warn <<~MSG.strip_heredoc [DOORKEEPER] ORM "#{configuration.orm}" should move all it's setup logic under `#run_hooks` method for the #{@orm_adapter.name}. Later versions of Doorkeeper will no longer support `setup_orm_models` and `setup_application_owner` API. MSG end end def setup_orm_models @orm_adapter.initialize_models! end def setup_application_owner @orm_adapter.initialize_application_owner! end def authenticate(request, methods = Doorkeeper.config.access_token_methods) OAuth::Token.authenticate(request, *methods) end def gem_version ::Gem::Version.new(::Doorkeeper::VERSION::STRING) end end end ================================================ FILE: lib/generators/doorkeeper/application_owner_generator.rb ================================================ # frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module Doorkeeper # Generates migration to add reference to owner of the # Doorkeeper application. # class ApplicationOwnerGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration source_root File.expand_path("templates", __dir__) desc "Provide support for client application ownership." def application_owner migration_template( "add_owner_to_application_migration.rb.erb", "db/migrate/add_owner_to_application.rb", migration_version: migration_version, ) end def self.next_migration_number(dirname) ActiveRecord::Generators::Base.next_migration_number(dirname) end private def migration_version "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" end end end ================================================ FILE: lib/generators/doorkeeper/confidential_applications_generator.rb ================================================ # frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module Doorkeeper # Generates migration to add confidential column to Doorkeeper # applications table. # class ConfidentialApplicationsGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration source_root File.expand_path("templates", __dir__) desc "Add confidential column to Doorkeeper applications" def confidential_applications migration_template( "add_confidential_to_applications.rb.erb", "db/migrate/add_confidential_to_applications.rb", migration_version: migration_version, ) end def self.next_migration_number(dirname) ActiveRecord::Generators::Base.next_migration_number(dirname) end private def migration_version "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" end end end ================================================ FILE: lib/generators/doorkeeper/enable_polymorphic_resource_owner_generator.rb ================================================ # frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module Doorkeeper # Generates migration with polymorphic resource owner required # database columns for Doorkeeper Access Token and Access Grant # models. # class EnablePolymorphicResourceOwnerGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration source_root File.expand_path("templates", __dir__) desc "Provide support for polymorphic Resource Owner." def enable_polymorphic_resource_owner migration_template( "enable_polymorphic_resource_owner_migration.rb.erb", "db/migrate/enable_polymorphic_resource_owner.rb", migration_version: migration_version, ) gsub_file( "config/initializers/doorkeeper.rb", "# use_polymorphic_resource_owner", "use_polymorphic_resource_owner", ) end def self.next_migration_number(dirname) ActiveRecord::Generators::Base.next_migration_number(dirname) end private def migration_version "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" end end end ================================================ FILE: lib/generators/doorkeeper/install_generator.rb ================================================ # frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module Doorkeeper # Setup doorkeeper into Rails application: locales, routes, etc. # class InstallGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration source_root File.expand_path("templates", __dir__) desc "Installs Doorkeeper." def install template "initializer.rb", "config/initializers/doorkeeper.rb" copy_file File.expand_path("../../../config/locales/en.yml", __dir__), "config/locales/doorkeeper.en.yml" route "use_doorkeeper" readme "README" end end end ================================================ FILE: lib/generators/doorkeeper/migration_generator.rb ================================================ # frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module Doorkeeper # Copies main Doorkeeper migration into parent Rails application. # class MigrationGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration source_root File.expand_path("templates", __dir__) desc "Installs Doorkeeper migration file." def install migration_template( "migration.rb.erb", "db/migrate/create_doorkeeper_tables.rb", migration_version: migration_version, ) end def self.next_migration_number(dirname) ActiveRecord::Generators::Base.next_migration_number(dirname) end private def migration_version "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" end end end ================================================ FILE: lib/generators/doorkeeper/pkce_generator.rb ================================================ # frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module Doorkeeper # Generates migration with PKCE required database columns for # Doorkeeper tables. # class PkceGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration source_root File.expand_path("templates", __dir__) desc "Provide support for PKCE." def pkce migration_template( "enable_pkce_migration.rb.erb", "db/migrate/enable_pkce.rb", migration_version: migration_version, ) end def self.next_migration_number(dirname) ActiveRecord::Generators::Base.next_migration_number(dirname) end private def migration_version "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" end end end ================================================ FILE: lib/generators/doorkeeper/previous_refresh_token_generator.rb ================================================ # frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module Doorkeeper # Generates migration to add previous refresh token column to the # database for Doorkeeper tables. # class PreviousRefreshTokenGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration source_root File.expand_path("templates", __dir__) desc "Support revoke refresh token on access token use" def self.next_migration_number(path) ActiveRecord::Generators::Base.next_migration_number(path) end def previous_refresh_token return unless no_previous_refresh_token_column? migration_template( "add_previous_refresh_token_to_access_tokens.rb.erb", "db/migrate/add_previous_refresh_token_to_access_tokens.rb", ) end private def migration_version "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" end def no_previous_refresh_token_column? !ActiveRecord::Base.connection.column_exists?( :oauth_access_tokens, :previous_refresh_token, ) end end end ================================================ FILE: lib/generators/doorkeeper/remove_applications_secret_not_null_constraint_generator.rb ================================================ # frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" module Doorkeeper # Generates migration with which drops NOT NULL constraint and allows not # to bloat the database with redundant secret value. # class RemoveApplicationsSecretNotNullConstraintGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration source_root File.expand_path("templates", __dir__) desc "Removes NOT NULL constraint for OAuth2 applications." def remove_applications_secret_not_null_constraint migration_template( "remove_applications_secret_not_null_constraint.rb.erb", "db/migrate/remove_applications_secret_not_null_constraint.rb", migration_version: migration_version, ) end def self.next_migration_number(dirname) ActiveRecord::Generators::Base.next_migration_number(dirname) end private def migration_version "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" end end end ================================================ FILE: lib/generators/doorkeeper/templates/README ================================================ =============================================================================== There is a setup that you need to do before you can use doorkeeper. Step 1. Go to config/initializers/doorkeeper.rb and configure resource_owner_authenticator block. Step 2. Choose the ORM: If you want to use ActiveRecord run: rails generate doorkeeper:migration And run rake db:migrate Step 3. That's it, that's all. Enjoy! =============================================================================== ================================================ FILE: lib/generators/doorkeeper/templates/add_confidential_to_applications.rb.erb ================================================ # frozen_string_literal: true class AddConfidentialToApplications < ActiveRecord::Migration<%= migration_version %> def change add_column( :oauth_applications, :confidential, :boolean, null: false, default: true ) end end ================================================ FILE: lib/generators/doorkeeper/templates/add_owner_to_application_migration.rb.erb ================================================ # frozen_string_literal: true class AddOwnerToApplication < ActiveRecord::Migration<%= migration_version %> def change add_column :oauth_applications, :owner_id, :bigint, null: true add_column :oauth_applications, :owner_type, :string, null: true add_index :oauth_applications, [:owner_id, :owner_type] end end ================================================ FILE: lib/generators/doorkeeper/templates/add_previous_refresh_token_to_access_tokens.rb.erb ================================================ # frozen_string_literal: true class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration<%= migration_version %> def change add_column( :oauth_access_tokens, :previous_refresh_token, :string, default: "", null: false ) end end ================================================ FILE: lib/generators/doorkeeper/templates/enable_pkce_migration.rb.erb ================================================ # frozen_string_literal: true class EnablePkce < ActiveRecord::Migration<%= migration_version %> def change add_column :oauth_access_grants, :code_challenge, :string, null: true add_column :oauth_access_grants, :code_challenge_method, :string, null: true end end ================================================ FILE: lib/generators/doorkeeper/templates/enable_polymorphic_resource_owner_migration.rb.erb ================================================ # frozen_string_literal: true class EnablePolymorphicResourceOwner < ActiveRecord::Migration<%= migration_version %> def change add_column :oauth_access_tokens, :resource_owner_type, :string add_column :oauth_access_grants, :resource_owner_type, :string change_column_null :oauth_access_grants, :resource_owner_type, false add_index :oauth_access_tokens, [:resource_owner_id, :resource_owner_type], name: 'polymorphic_owner_oauth_access_tokens' add_index :oauth_access_grants, [:resource_owner_id, :resource_owner_type], name: 'polymorphic_owner_oauth_access_grants' end end ================================================ FILE: lib/generators/doorkeeper/templates/initializer.rb ================================================ # frozen_string_literal: true Doorkeeper.configure do # Change the ORM that doorkeeper will use (requires ORM extensions installed). # Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms orm :active_record # Enable support for multiple database configurations with read replicas. # When enabled, Doorkeeper will wrap database write operations to ensure they # use the primary (writable) database when automatic role switching is enabled. # # For ActiveRecord (Rails 6.1+), this uses `ActiveRecord::Base.connected_to(role: :writing)`. # Other ORM extensions can implement their own primary database targeting logic. # # enable_multiple_database_roles # # This prevents `ActiveRecord::ReadOnlyError` when using read replicas with Rails # automatic role switching. Enable this if your application uses multiple databases # with automatic role switching for read replicas. # # See: https://guides.rubyonrails.org/active_record_multiple_databases.html#activating-automatic-role-switching # This block will be called to check whether the resource owner is authenticated or not. resource_owner_authenticator do raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}" # Put your resource owner authentication logic here. # Example implementation: # User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url) end # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb # file then you need to declare this block in order to restrict access to the web interface for # adding oauth authorized applications. In other case it will return 403 Forbidden response # every time somebody will try to access the admin web interface. # # admin_authenticator do # # Put your admin authentication logic here. # # Example implementation: # # if current_user # head :forbidden unless current_user.admin? # else # redirect_to sign_in_url # end # end # You can use your own model classes if you need to extend (or even override) default # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. # # By default Doorkeeper ActiveRecord ORM uses its own classes: # # access_token_class "Doorkeeper::AccessToken" # access_grant_class "Doorkeeper::AccessGrant" # application_class "Doorkeeper::Application" # # Don't forget to include Doorkeeper ORM mixins into your custom models: # # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - for access token # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant - for access grant # * ::Doorkeeper::Orm::ActiveRecord::Mixins::Application - for application (OAuth2 clients) # # For example: # # access_token_class "MyAccessToken" # # class MyAccessToken < ApplicationRecord # include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken # # self.table_name = "hey_i_wanna_my_name" # # def destroy_me! # destroy # end # end # Enables polymorphic Resource Owner association for Access Tokens and Access Grants. # By default this option is disabled. # # Make sure you properly setup you database and have all the required columns (run # `bundle exec rails generate doorkeeper:enable_polymorphic_resource_owner` and execute Rails # migrations). # # If this option enabled, Doorkeeper will store not only Resource Owner primary key # value, but also it's type (class name). See "Polymorphic Associations" section of # Rails guides: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations # # [NOTE] If you apply this option on already existing project don't forget to manually # update `resource_owner_type` column in the database and fix migration template as it will # set NOT NULL constraint for Access Grants table. # # use_polymorphic_resource_owner # If you are planning to use Doorkeeper in Rails 5 API-only application, then you might # want to use API mode that will skip all the views management and change the way how # Doorkeeper responds to a requests. # # api_only # Enforce token request content type to application/x-www-form-urlencoded. # It is not enabled by default to not break prior versions of the gem. # # enforce_content_type # Authorization Code expiration time (default: 10 minutes). # # authorization_code_expires_in 10.minutes # Access token expiration time (default: 2 hours). # If you set this to `nil` Doorkeeper will not expire the token and omit expires_in in response. # It is RECOMMENDED to set expiration time explicitly. # Prefer access_token_expires_in 100.years or similar, # which would be functionally equivalent and avoid the risk of unexpected behavior by callers. # # access_token_expires_in 2.hours # Assign custom TTL for access tokens. Will be used instead of access_token_expires_in # option if defined. In case the block returns `nil` value Doorkeeper fallbacks to # +access_token_expires_in+ configuration option value. If you really need to issue a # non-expiring access token (which is not recommended) then you need to return # Float::INFINITY from this block. # # `context` has the following properties available: # # * `client` - the OAuth client application (see Doorkeeper::OAuth::Client) # * `grant_type` - the grant type of the request (see Doorkeeper::OAuth) # * `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) # * `resource_owner` - authorized resource owner instance (if present) # # custom_access_token_expires_in do |context| # context.client.additional_settings.implicit_oauth_expiration # end # Use a custom class for generating the access token. # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator # # access_token_generator '::Doorkeeper::JWT' # The controller +Doorkeeper::ApplicationController+ inherits from. # Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to # +ActionController::API+. The return value of this option must be a stringified class name. # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-controllers # # base_controller 'ApplicationController' # Reuse access token for the same resource owner within an application (disabled by default). # # This option protects your application from creating new tokens before old **valid** one becomes # expired so your database doesn't bloat. Keep in mind that when this option is enabled Doorkeeper # doesn't update existing token expiration time, it will create a new token instead if no active matching # token found for the application, resources owner and/or set of scopes. # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 # # You can not enable this option together with +hash_token_secrets+. # # reuse_access_token # In case you enabled `reuse_access_token` option Doorkeeper will try to find matching # token using `matching_token_for` Access Token API that searches for valid records # in batches in order not to pollute the memory with all the database records. By default # Doorkeeper uses batch size of 10 000 records. You can increase or decrease this value # depending on your needs and server capabilities. # # token_lookup_batch_size 10_000 # Set a limit for token_reuse if using reuse_access_token option # # This option limits token_reusability to some extent. # If not set then access_token will be reused unless it expires. # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 # # This option should be a percentage(i.e. (0,100]) # # token_reuse_limit 100 # Only allow one valid access token obtained via client credentials # per client. If a new access token is obtained before the old one # expired, the old one gets revoked (disabled by default) # # When enabling this option, make sure that you do not expect multiple processes # using the same credentials at the same time (e.g. web servers spanning # multiple machines and/or processes). # # revoke_previous_client_credentials_token # Only allow one valid access token obtained via authorization code # per client. If a new access token is obtained before the old one # expired, the old one gets revoked (disabled by default) # # revoke_previous_authorization_code_token # Require non-confidential clients to use PKCE when using an authorization code # to obtain an access_token (disabled by default) # # force_pkce # Hash access and refresh tokens before persisting them. # This will disable the possibility to use +reuse_access_token+ # since plain values can no longer be retrieved. # # Note: If you are already a user of doorkeeper and have existing tokens # in your installation, they will be invalid without adding 'fallback: :plain'. # # hash_token_secrets # By default, token secrets will be hashed using the # +Doorkeeper::Hashing::SHA256+ strategy. # # If you wish to use another hashing implementation, you can override # this strategy as follows: # # hash_token_secrets using: '::Doorkeeper::Hashing::MyCustomHashImpl' # # Keep in mind that changing the hashing function will invalidate all existing # secrets, if there are any. # Hash application secrets before persisting them. # # hash_application_secrets # # By default, applications will be hashed # with the +Doorkeeper::SecretStoring::SHA256+ strategy. # # If you wish to use bcrypt for application secret hashing, uncomment # this line instead: # # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt' # When the above option is enabled, and a hashed token or secret is not found, # you can allow to fall back to another strategy. For users upgrading # doorkeeper and wishing to enable hashing, you will probably want to enable # the fallback to plain tokens. # # This will ensure that old access tokens and secrets # will remain valid even if the hashing above is enabled. # # This can be done by adding 'fallback: plain', e.g. : # # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt', fallback: :plain # Issue access tokens with refresh token (disabled by default), you may also # pass a block which accepts `context` to customize when to give a refresh # token or not. Similar to +custom_access_token_expires_in+, `context` has # the following properties: # # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) # # use_refresh_token # Provide support for an owner to be assigned to each registered application (disabled by default) # Optional parameter confirmation: true (default: false) if you want to enforce ownership of # a registered application # NOTE: you must also run the rails g doorkeeper:application_owner generator # to provide the necessary support # # enable_application_owner confirmation: false # Define access token scopes for your provider # For more information go to # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes # # default_scopes :public # optional_scopes :write, :update # Allows to restrict only certain scopes for grant_type. # By default, all the scopes will be available for all the grant types. # # Keys to this hash should be the name of grant_type and # values should be the array of scopes for that grant type. # Note: scopes should be from configured_scopes (i.e. default or optional) # # scopes_by_grant_type password: [:write], client_credentials: [:update] # Forbids creating/updating applications with arbitrary scopes that are # not in configuration, i.e. +default_scopes+ or +optional_scopes+. # (disabled by default) # # enforce_configured_scopes # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then # falls back to the `:client_id` and `:client_secret` params from the `params` object. # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated # for more information on customization # # client_credentials :from_basic, :from_params # Change the way access token is authenticated from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then # falls back to the `:access_token` or `:bearer_token` params from the `params` object. # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated # for more information on customization # # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled # by default in non-development environments). OAuth2 delegates security in # communication to the HTTPS protocol so it is wise to keep this enabled. # # Callable objects such as proc, lambda, block or any object that responds to # #call can be used in order to allow conditional checks (to allow non-SSL # redirects to localhost for example). # # force_ssl_in_redirect_uri !Rails.env.development? # # force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' } # Specify what redirect URI's you want to block during Application creation. # Any redirect URI is allowed by default. # # You can use this option in order to forbid URI's with 'javascript' scheme # for example. # # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } # Allows to set blank redirect URIs for Applications in case Doorkeeper configured # to use URI-less OAuth grant flows like Client Credentials or Resource Owner # Password Credentials. The option is on by default and checks configured grant # types, but you **need** to manually drop `NOT NULL` constraint from `redirect_uri` # column for `oauth_applications` database table. # # You can completely disable this feature with: # # allow_blank_redirect_uri false # # Or you can define your custom check: # # allow_blank_redirect_uri do |grant_flows, client| # client.superapp? # end # Specify how authorization errors should be handled. # By default, doorkeeper renders json errors when access token # is invalid, expired, revoked or has invalid scopes. # # If you want to render error response yourself (i.e. rescue exceptions), # set +handle_auth_errors+ to `:raise` and rescue Doorkeeper::Errors::InvalidToken # or following specific errors: # # Doorkeeper::Errors::TokenForbidden, Doorkeeper::Errors::TokenExpired, # Doorkeeper::Errors::TokenRevoked, Doorkeeper::Errors::TokenUnknown # # handle_auth_errors :raise # # If you want to redirect back to the client application in accordance with # https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1, you can set # +handle_auth_errors+ to :redirect # # handle_auth_errors :redirect # Customize token introspection response. # Allows to add your own fields to default one that are required by the OAuth spec # for the introspection response. It could be `sub`, `aud` and so on. # This configuration option can be a proc, lambda or any Ruby object responds # to `.call` method and result of it's invocation must be a Hash. # # custom_introspection_response do |token, context| # { # "sub": "Z5O3upPC88QrAjx00dis", # "aud": "https://protected.example.net/resource", # "username": User.find(token.resource_owner_id).username # } # end # # or # # custom_introspection_response CustomIntrospectionResponder # Specify what grant flows are enabled in array of Strings. The valid # strings and the flows they enable are: # # "authorization_code" => Authorization Code Grant Flow # "implicit" => Implicit Grant Flow # "password" => Resource Owner Password Credentials Grant Flow # "client_credentials" => Client Credentials Grant Flow # # If not specified, Doorkeeper enables authorization_code and # client_credentials. # # implicit and password grant flows have risks that you should understand # before enabling: # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.2 # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.3 # # grant_flows %w[authorization_code client_credentials] # Allows to customize OAuth grant flows that +each+ application support. # You can configure a custom block (or use a class respond to `#call`) that must # return `true` in case Application instance supports requested OAuth grant flow # during the authorization request to the server. This configuration +doesn't+ # set flows per application, it only allows to check if application supports # specific grant flow. # # For example you can add an additional database column to `oauth_applications` table, # say `t.array :grant_flows, default: []`, and store allowed grant flows that can # be used with this application there. Then when authorization requested Doorkeeper # will call this block to check if specific Application (passed with client_id and/or # client_secret) is allowed to perform the request for the specific grant type # (authorization, password, client_credentials, etc). # # Example of the block: # # ->(flow, client) { client.grant_flows.include?(flow) } # # In case this option invocation result is `false`, Doorkeeper server returns # :unauthorized_client error and stops the request. # # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call # @return [Boolean] `true` if allow or `false` if forbid the request # # allow_grant_flow_for_client do |grant_flow, client| # # `grant_flows` is an Array column with grant # # flows that application supports # # client.grant_flows.include?(grant_flow) # end # If you need arbitrary Resource Owner-Client authorization you can enable this option # and implement the check your need. Config option must respond to #call and return # true in case resource owner authorized for the specific application or false in other # cases. # # By default all Resource Owners are authorized to any Client (application). # # authorize_resource_owner_for_client do |client, resource_owner| # resource_owner.admin? || client.owners_allowlist.include?(resource_owner) # end # Allows additional data fields to be sent while granting access to an application, # and for this additional data to be included in subsequently generated access tokens. # The 'authorizations/new' page will need to be overridden to include this additional data # in the request params when granting access. The access grant and access token models # will both need to respond to these additional data fields, and have a database column # to store them in. # # Example: # You have a multi-tenanted platform and want to be able to grant access to a specific # tenant, rather than all the tenants a user has access to. You can use this config # option to specify that a ':tenant_id' will be passed when authorizing. This tenant_id # will be included in the access tokens. When a request is made with one of these access # tokens, you can check that the requested data belongs to the specified tenant. # # Default value is an empty Array: [] # custom_access_token_attributes [:tenant_id] # Hook into the strategies' request & response life-cycle in case your # application needs advanced customization or logging: # # before_successful_strategy_response do |request| # puts "BEFORE HOOK FIRED! #{request}" # end # # after_successful_strategy_response do |request, response| # puts "AFTER HOOK FIRED! #{request}, #{response}" # end # Hook into Authorization flow in order to implement Single Sign Out # or add any other functionality. Inside the block you have an access # to `controller` (authorizations controller instance) and `context` # (Doorkeeper::OAuth::Hooks::Context instance) which provides pre auth # or auth objects with issued token based on hook type (before or after). # # before_successful_authorization do |controller, context| # Rails.logger.info(controller.request.params.inspect) # # Rails.logger.info(context.pre_auth.inspect) # end # # after_successful_authorization do |controller, context| # controller.session[:logout_urls] << # Doorkeeper::Application # .find_by(controller.request.params.slice(:redirect_uri)) # .logout_uri # # Rails.logger.info(context.auth.inspect) # Rails.logger.info(context.issued_token) # end # Under some circumstances you might want to have applications auto-approved, # so that the user skips the authorization step. # For example if dealing with a trusted application. # # skip_authorization do |resource_owner, client| # client.superapp? or resource_owner.admin? # end # Configure custom constraints for the Token Introspection request. # By default this configuration option allows to introspect a token by another # token of the same application, OR to introspect the token that belongs to # authorized client (from authenticated client) OR when token doesn't # belong to any client (public token). Otherwise requester has no access to the # introspection and it will return response as stated in the RFC. # # Block arguments: # # @param token [Doorkeeper::AccessToken] # token to be introspected # # @param authorized_client [Doorkeeper::Application] # authorized client (if request is authorized using Basic auth with # Client Credentials for example) # # @param authorized_token [Doorkeeper::AccessToken] # Bearer token used to authorize the request # # In case the block returns `nil` or `false` introspection responses with 401 status code # when using authorized token to introspect, or you'll get 200 with { "active": false } body # when using authorized client to introspect as stated in the # RFC 7662 section 2.2. Introspection Response. # # Using with caution: # Keep in mind that these three parameters pass to block can be nil as following case: # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. # `token` will be nil if and only if `authorized_token` is present. # So remember to use `&` or check if it is present before calling method on # them to make sure you doesn't get NoMethodError exception. # # You can define your custom check: # # allow_token_introspection do |token, authorized_client, authorized_token| # if authorized_token # # customize: require `introspection` scope # authorized_token.application == token&.application || # authorized_token.scopes.include?("introspection") # elsif token.application # # `protected_resource` is a new database boolean column, for example # authorized_client == token.application || authorized_client.protected_resource? # else # # public token (when token.application is nil, token doesn't belong to any application) # true # end # end # # Or you can completely disable any token introspection: # # allow_token_introspection false # # If you need to block the request at all, then configure your routes.rb or web-server # like nginx to forbid the request. # WWW-Authenticate Realm (default: "Doorkeeper"). # # realm "Doorkeeper" end ================================================ FILE: lib/generators/doorkeeper/templates/migration.rb.erb ================================================ # frozen_string_literal: true class CreateDoorkeeperTables < ActiveRecord::Migration<%= migration_version %> def change create_table :oauth_applications do |t| t.string :name, null: false t.string :uid, null: false # Remove `null: false` or use conditional constraint if you are planning to use public clients. t.string :secret, null: false # Remove `null: false` if you are planning to use grant flows # that doesn't require redirect URI to be used during authorization # like Client Credentials flow or Resource Owner Password. t.text :redirect_uri, null: false t.string :scopes, null: false, default: '' t.boolean :confidential, null: false, default: true t.timestamps null: false end add_index :oauth_applications, :uid, unique: true create_table :oauth_access_grants do |t| t.references :resource_owner, null: false t.references :application, null: false t.string :token, null: false t.integer :expires_in, null: false t.text :redirect_uri, null: false t.string :scopes, null: false, default: '' t.datetime :created_at, null: false t.datetime :revoked_at end add_index :oauth_access_grants, :token, unique: true add_foreign_key( :oauth_access_grants, :oauth_applications, column: :application_id ) create_table :oauth_access_tokens do |t| t.references :resource_owner, index: true # Remove `null: false` if you are planning to use Password # Credentials Grant flow that doesn't require an application. t.references :application, null: false # If you use a custom token generator you may need to change this column # from string to text, so that it accepts tokens larger than 255 # characters. More info on custom token generators in: # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator # # t.text :token, null: false t.string :token, null: false t.string :refresh_token t.integer :expires_in t.string :scopes t.datetime :created_at, null: false t.datetime :revoked_at # The authorization server MAY issue a new refresh token, in which case # *the client MUST discard the old refresh token* and replace it with the # new refresh token. The authorization server MAY revoke the old # refresh token after issuing a new refresh token to the client. # @see https://datatracker.ietf.org/doc/html/rfc6749#section-6 # # Doorkeeper implementation: if there is a `previous_refresh_token` column, # refresh tokens will be revoked after a related access token is used. # If there is no `previous_refresh_token` column, previous tokens are # revoked as soon as a new access token is created. # # Comment out this line if you want refresh tokens to be instantly # revoked after use. t.string :previous_refresh_token, null: false, default: "" end add_index :oauth_access_tokens, :token, unique: true # See https://github.com/doorkeeper-gem/doorkeeper/issues/1592 if ActiveRecord::Base.connection.adapter_name == "SQLServer" execute <<~SQL.squish CREATE UNIQUE NONCLUSTERED INDEX index_oauth_access_tokens_on_refresh_token ON oauth_access_tokens(refresh_token) WHERE refresh_token IS NOT NULL SQL else add_index :oauth_access_tokens, :refresh_token, unique: true end add_foreign_key( :oauth_access_tokens, :oauth_applications, column: :application_id ) # Uncomment below to ensure a valid reference to the resource owner's table # add_foreign_key :oauth_access_grants, , column: :resource_owner_id # add_foreign_key :oauth_access_tokens, , column: :resource_owner_id end end ================================================ FILE: lib/generators/doorkeeper/templates/remove_applications_secret_not_null_constraint.rb.erb ================================================ # frozen_string_literal: true class RemoveApplicationsSecretNotNullConstraint < ActiveRecord::Migration<%= migration_version %> def change change_column_null :oauth_applications, :secret, true end end ================================================ FILE: lib/generators/doorkeeper/views_generator.rb ================================================ # frozen_string_literal: true module Doorkeeper module Generators # Generates doorkeeper views for Rails application # class ViewsGenerator < ::Rails::Generators::Base source_root File.expand_path("../../../app/views", __dir__) desc "Copies default Doorkeeper views and layouts to your application." def manifest directory "doorkeeper", "app/views/doorkeeper" directory "layouts/doorkeeper", "app/views/layouts/doorkeeper" end end end end ================================================ FILE: spec/controllers/application_controller_spec.rb ================================================ # frozen_string_literal: true require "spec_helper_integration" RSpec.describe Doorkeeper::ApplicationController, type: :controller do describe "current_resource_owner view helper" do controller(described_class) do def index render inline: "<%= current_resource_owner %>" end end it "is registered as a helper method" do expect(described_class._helper_methods).to include(:current_resource_owner) end it "is callable from views" do allow(controller).to receive(:current_resource_owner).and_return("owner-sentinel") get :index expect(response.body).to include("owner-sentinel") end end end ================================================ FILE: spec/controllers/application_metal_controller_spec.rb ================================================ # frozen_string_literal: true require "spec_helper_integration" RSpec.describe Doorkeeper::ApplicationMetalController, type: :controller do render_views controller(described_class) do def index render json: {}, status: 200 end def create render json: {}, status: 200 end end it "lacks `helper_method` so the included hook becomes a no-op" do expect(described_class).not_to respond_to(:helper_method) end it "lazy run hooks" do i = 0 ActiveSupport.on_load(:doorkeeper_metal_controller) { i += 1 } expect(i).to eq 1 end describe "enforce_content_type" do before { allow(Doorkeeper.config).to receive(:enforce_content_type).and_return(flag) } context "when enabled" do let(:flag) { true } it "returns a 200 for the requests without body" do get :index, params: {} expect(response).to have_http_status 200 end it "returns a 200 for the requests with body and correct media type" do post :create, params: {}, as: :url_encoded_form expect(response).to have_http_status 200 end it "returns a 415 for the requests with body and incorrect media type" do post :create, params: {}, as: :json expect(response).to have_http_status 415 end end context "when disabled" do let(:flag) { false } it "returns a 200 for the correct media type" do get :index, as: :url_encoded_form expect(response).to have_http_status 200 end it "returns a 200 for an incorrect media type" do get :index, as: :json expect(response).to have_http_status 200 end it "returns a 200 for the requests with body and incorrect media type" do post :create, params: {}, as: :json expect(response).to have_http_status 200 end end end end ================================================ FILE: spec/controllers/applications_controller_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::ApplicationsController, type: :controller do render_views context "when JSON API used" do before do allow(Doorkeeper.configuration).to receive(:api_only).and_return(true) allow(Doorkeeper.configuration).to receive(:authenticate_admin).and_return(->(*) { true }) end it "creates an application" do expect do post :create, params: { doorkeeper_application: { name: "Example", redirect_uri: "https://example.com", }, format: :json, } end.to(change { Doorkeeper::Application.count }) expect(response).to be_successful expect(json_response).to include("id", "name", "uid", "secret", "redirect_uri", "scopes") application = Doorkeeper::Application.last secret_from_response = json_response["secret"] expect(application).to be_secret_matches(secret_from_response) expect(json_response["name"]).to eq("Example") expect(json_response["redirect_uri"]).to eq("https://example.com") end it "returns validation errors on wrong create params" do expect do post :create, params: { doorkeeper_application: { name: "Example", }, format: :json, } end.not_to(change { Doorkeeper::Application.count }) expect(response).to have_http_status(422) expect(json_response).to include("errors") end it "returns validations on wrong create params (unspecified scheme)" do expect do post :create, params: { doorkeeper_application: { name: "Example", redirect_uri: "app.com:80", }, format: :json, } end.not_to(change { Doorkeeper::Application.count }) expect(response).to have_http_status(422) expect(json_response).to include("errors") end it "returns application info" do application = FactoryBot.create(:application, name: "Change me") get :show, params: { id: application.id, format: :json } expect(response).to be_successful expect(json_response).to include("id", "name", "uid", "secret", "redirect_uri", "scopes") end it "updates application" do application = FactoryBot.create(:application, name: "Change me") put :update, params: { id: application.id, doorkeeper_application: { name: "Example App", redirect_uri: "https://example.com", }, format: :json, } expect(application.reload.name).to eq "Example App" expect(json_response).to include("id", "name", "uid", "secret", "redirect_uri", "scopes") end it "returns validation errors on wrong update params" do application = FactoryBot.create(:application, name: "Change me") put :update, params: { id: application.id, doorkeeper_application: { name: "Example App", redirect_uri: "localhost:3000", }, format: :json, } expect(response).to have_http_status(422) expect(json_response).to include("errors") end it "destroys an application" do application = FactoryBot.create(:application) delete :destroy, params: { id: application.id, format: :json } expect(response).to have_http_status(204) expect(Doorkeeper::Application.count).to be_zero end end context "when admin is not authenticated" do before do allow(Doorkeeper.config).to receive(:authenticate_admin).and_return(proc do redirect_to main_app.root_url end) end it "redirects as set in Doorkeeper.authenticate_admin" do get :index expect(response).to redirect_to(controller.main_app.root_url) end it "does not create application" do expect do post :create, params: { doorkeeper_application: { name: "Example", redirect_uri: "https://example.com", }, } end.not_to(change { Doorkeeper::Application.count }) end end context "when admin is authenticated" do before do allow(Doorkeeper.configuration).to receive(:authenticate_admin).and_return(->(*) { true }) end context "when application secrets are hashed" do before do allow(Doorkeeper.configuration) .to receive(:application_secret_strategy).and_return(Doorkeeper::SecretStoring::Sha256Hash) end it "shows the application secret after creating a new application" do expect do post :create, params: { doorkeeper_application: { name: "Example", redirect_uri: "https://example.com", }, } end.to change { Doorkeeper::Application.count }.by(1) application = Doorkeeper::Application.last secret_from_flash = flash[:application_secret] expect(secret_from_flash).not_to be_empty expect(application).to be_secret_matches(secret_from_flash) expect(response).to redirect_to(controller.main_app.oauth_application_url(application.id)) get :show, params: { id: application.id, format: :html } # We don't know the application secret here (because its hashed) so we can not assert its text on the page # Instead, we read it from the page and then check if it matches the application secret code_element = /code.*id="secret">\s*\K([^<]*)/m.match(response.body) secret_from_page = code_element[1].strip expect(response.body).to have_selector("code#application_id", text: application.uid) expect(response.body).to have_selector("code#secret") expect(secret_from_page).not_to be_empty expect(application).to be_secret_matches(secret_from_page) end it "does not show an application secret when application did already exist" do application = FactoryBot.create(:application) get :show, params: { id: application.id, format: :html } expect(response.body).to have_selector("code#application_id", text: application.uid) expect(response.body).to have_selector("code#secret", text: "") end it "returns the application details in a json response" do expect do post :create, params: { doorkeeper_application: { name: "Example", redirect_uri: "https://example.com", }, format: :json, } end.to(change { Doorkeeper::Application.count }) expect(response).to be_successful expect(json_response).to include("id", "name", "uid", "secret", "redirect_uri", "scopes") application = Doorkeeper::Application.last secret_from_response = json_response["secret"] expect(application).to be_secret_matches(secret_from_response) expect(json_response["name"]).to eq("Example") expect(json_response["redirect_uri"]).to eq("https://example.com") end end it "sorts applications by created_at" do first_application = FactoryBot.create(:application) second_application = FactoryBot.create(:application) expect(Doorkeeper::Application).to receive(:ordered_by).and_call_original get :index expect(response.body).to have_selector("tbody tr:first-child#application_#{first_application.id}") expect(response.body).to have_selector("tbody tr:last-child#application_#{second_application.id}") end it "creates application" do expect do post :create, params: { doorkeeper_application: { name: "Example", redirect_uri: "https://example.com", }, } end.to change { Doorkeeper::Application.count }.by(1) expect(response).to be_redirect end it "shows application details" do application = FactoryBot.create(:application) get :show, params: { id: application.id, format: :html } expect(response.body).to have_selector("code#application_id", text: application.uid) expect(response.body).to have_selector("code#secret", text: application.plaintext_secret) end it "does not allow mass assignment of uid or secret" do application = FactoryBot.create(:application) put :update, params: { id: application.id, doorkeeper_application: { uid: "1A2B3C4D", secret: "1A2B3C4D", }, } expect(application.reload.uid).not_to eq "1A2B3C4D" end it "updates application" do application = FactoryBot.create(:application) put :update, params: { id: application.id, doorkeeper_application: { name: "Example", redirect_uri: "https://example.com", }, } expect(application.reload.name).to eq "Example" end end end ================================================ FILE: spec/controllers/authorizations_controller_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::AuthorizationsController, type: :controller do include AuthorizationRequestHelper render_views class ActionDispatch::TestResponse def query_params @query_params ||= begin fragment = URI.parse(location).fragment Rack::Utils.parse_query(fragment) end end end let(:client) { FactoryBot.create :application } let(:user) { User.create!(name: "Joe", password: "sekret") } let(:access_token) do FactoryBot.build :access_token, resource_owner_id: user.id, resource_owner_type: user.class.name, application_id: client.id, scopes: "default" end let(:response_json_body) { JSON.parse(response.body) } before do Doorkeeper.configure do orm DOORKEEPER_ORM default_scopes :default custom_access_token_expires_in(lambda do |context| context.grant_type == Doorkeeper::OAuth::IMPLICIT ? 1234 : nil end) end allow(Doorkeeper.config).to receive(:grant_flows).and_return(["implicit"]) allow(Doorkeeper.config).to receive(:authenticate_resource_owner).and_return(->(_) { authenticator_method }) allow(subject).to receive(:authenticator_method).and_return(user) end describe "POST #create" do context "without response_mode parameter" do before do post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri } end it "redirects after authorization" do expect(response).to be_redirect expect(subject).to receive(:authenticator_method).at_most(:once) end it "redirects to client redirect uri" do expect(response.location).to match(/^#{client.redirect_uri}/) end it "includes access token in fragment" do expect(response.query_params["access_token"]).to eq(Doorkeeper::AccessToken.first.token) end it "includes token type in fragment" do expect(response.query_params["token_type"]).to eq("Bearer") end it "includes token expiration in fragment" do expect(response.query_params["expires_in"].to_i).to eq(1234) end it "issues the token for the current client" do expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) end it "issues the token for the current resource owner" do expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) end end context "with 'form_post' as response_mode" do before do post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "renders 200 status" do expect(response.status).to eq 200 end it "issues a token" do expect(Doorkeeper::AccessToken.count).to eq(1) end it "issues the token for the current client" do expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) end it "issues the token for the current resource owner" do expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) end end end describe "POST #create in API mode" do context "without response_mode parameter" do before do allow(Doorkeeper.config).to receive(:api_only).and_return(true) post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri } end let(:redirect_uri) { response_json_body["redirect_uri"] } it "renders success after authorization" do expect(response).to be_successful end it "renders correct redirect uri" do expect(redirect_uri).to match(/^#{client.redirect_uri}/) end it "includes access token in fragment" do expect(redirect_uri.match(/access_token=([a-zA-Z0-9\-_]+)&?/)[1]).to eq(Doorkeeper::AccessToken.first.token) end it "includes token type in fragment" do expect(redirect_uri.match(/token_type=(\w+)&?/)[1]).to eq "Bearer" end it "includes token expiration in fragment" do expect(redirect_uri.match(/expires_in=(\d+)&?/)[1].to_i).to eq 1234 end it "issues the token for the current client" do expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) end it "issues the token for the current resource owner" do expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) end end context "with 'form_post' as response_mode" do before do allow(Doorkeeper.config).to receive(:api_only).and_return(true) post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "renders success after authorization" do expect(response).to be_successful end it "renders correct status" do expect(response_json_body["status"]).to eq "post" end it "renders correct redirect uri" do expect(response_json_body["redirect_uri"]).to eq(client.redirect_uri) end it "includes access token in fragment" do expect(response_json_body["body"]["access_token"]).to eq(Doorkeeper::AccessToken.first.token) end it "includes token type in fragment" do expect(response_json_body["body"]["token_type"]).to eq "Bearer" end it "includes token expiration in fragment" do expect(response_json_body["body"]["expires_in"]).to eq 1234 end it "issues the token for the current client" do expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) end it "issues the token for the current resource owner" do expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) end end end describe "POST #create with errors" do context "when missing client_id" do before do post :create, params: { client_id: "", response_type: "token", redirect_uri: client.redirect_uri, } end it "renders 400 error" do expect(response.status).to eq 400 end it "includes error name" do expect(response_json_body["error"]).to eq("invalid_request") end it "includes error description" do expect(response_json_body["error_description"]).to eq( translated_invalid_request_error_message(:missing_param, :client_id), ) end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end context "when client can not use grant flow" do before do config_is_set(:allow_grant_flow_for_client, ->(*_) { false }) post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "renders 401 error" do expect(response.status).to eq 401 end it "includes error name" do expect(response_json_body["error"]).to eq("unauthorized_client") end it "includes error description" do expect(response_json_body["error_description"]).to eq( translated_error_message(:unauthorized_client), ) end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end context "when user cannot access application" do before do allow(Doorkeeper.configuration).to receive(:authorize_resource_owner_for_client).and_return(->(*_) { false }) post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "renders 401 error" do expect(response.status).to eq 401 end it "includes error name" do expect(response_json_body["error"]).to eq("invalid_client") end it "includes error description" do expect(response_json_body["error_description"]).to eq( translated_error_message(:invalid_client), ) end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end context "when other error happens" do before do default_scopes_exist :public post :create, params: { client_id: client.uid, response_type: "token", scope: "invalid", redirect_uri: client.redirect_uri, } end it "redirects after authorization" do expect(response).to be_redirect end it "redirects to client redirect uri" do expect(response.location).to match(/^#{client.redirect_uri}/) end it "does not include access token in fragment" do expect(response.query_params["access_token"]).to be_nil end it "includes error in fragment" do expect(response.query_params["error"]).to eq("invalid_scope") end it "includes error description in fragment" do expect(response.query_params["error_description"]).to eq(translated_error_message(:invalid_scope)) end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end context "with 'form_post' as response_mode" do before do default_scopes_exist :public post :create, params: { client_id: client.uid, response_type: "token", scope: "invalid", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "redirects after authorization" do expect(response.status).to eq 200 end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end it "includes the error in the redirect post" do expect(response.body).to include("invalid_scope") end end end describe "POST #create in API mode with errors" do before { config_is_set(:api_only, true) } context "when missing client_id" do before do post :create, params: { client_id: "", response_type: "token", redirect_uri: client.redirect_uri, } end it "renders 400 error" do expect(response.status).to eq 400 end it "includes error name" do expect(response_json_body["error"]).to eq("invalid_request") end it "includes error description" do expect(response_json_body["error_description"]).to eq( translated_invalid_request_error_message(:missing_param, :client_id), ) end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end context "when client can not use grant flow" do before do config_is_set(:allow_grant_flow_for_client, ->(*_) { false }) post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "renders 401 error" do expect(response.status).to eq 401 end it "includes error name" do expect(response_json_body["error"]).to eq("unauthorized_client") end it "includes error description" do expect(response_json_body["error_description"]).to eq( translated_error_message(:unauthorized_client), ) end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end context "when user cannot access application" do before do allow(Doorkeeper.configuration).to receive(:authorize_resource_owner_for_client).and_return(->(*_) { false }) post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "renders 401 error" do expect(response.status).to eq 401 end it "includes error name" do expect(response_json_body["error"]).to eq("invalid_client") end it "includes error description" do expect(response_json_body["error_description"]).to eq( translated_error_message(:invalid_client), ) end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end context "when other error happens" do before do default_scopes_exist :public post :create, params: { client_id: client.uid, response_type: "token", scope: "invalid", redirect_uri: client.redirect_uri, } end let(:redirect_uri) { response_json_body["redirect_uri"] } it "renders 400 error" do expect(response.status).to eq 400 end it "includes correct redirect URI" do expect(redirect_uri).to match(/^#{client.redirect_uri}/) end it "does not include access token in fragment" do expect(redirect_uri.match(/access_token=([a-f0-9]+)&?/)).to be_nil end it "includes error in redirect uri" do expect(redirect_uri.match(/error=([a-z_]+)&?/)[1]).to eq "invalid_scope" end it "includes error description in redirect uri" do expect(redirect_uri.match(/error_description=(.+)&?/)[1]).not_to be_nil end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end context "with 'form_post' as response_mode" do before do default_scopes_exist :public post :create, params: { client_id: client.uid, response_type: "token", scope: "invalid", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "renders 400 error" do expect(response.status).to eq 400 end it "renders correct status" do expect(response_json_body["status"]).to eq "post" end it "renders correct redirect uri" do expect(response_json_body["redirect_uri"]).to eq(client.redirect_uri) end it "includes access token in fragment" do expect(response_json_body["body"]["access_token"]).to be_nil end it "includes token type in fragment" do expect(response_json_body["body"]["error"]).to eq "invalid_scope" end it "includes token expiration in fragment" do expect(response_json_body["body"]["error_description"]).not_to be_nil end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end end describe "POST #create with application already authorized" do before do allow(Doorkeeper.config).to receive(:reuse_access_token).and_return(true) access_token.save! post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "returns the existing access token in a fragment" do expect(response.query_params["access_token"]).to eq(access_token.token) end it "does not creates a new access token" do expect(Doorkeeper::AccessToken.count).to eq(1) end end describe "POST #create with callbacks" do after do client.update_attribute :redirect_uri, "urn:ietf:wg:oauth:2.0:oob" end describe "when successful" do after do post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "calls :before_successful_authorization callback" do expect(Doorkeeper.config) .to receive_message_chain(:before_successful_authorization, :call) .with(instance_of(described_class), instance_of(Doorkeeper::OAuth::Hooks::Context)) end it "calls :after_successful_authorization callback" do expect(Doorkeeper.config) .to receive_message_chain(:after_successful_authorization, :call) .with(instance_of(described_class), instance_of(Doorkeeper::OAuth::Hooks::Context)) end end describe "with errors" do after do post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: "bad_uri" } end it "does not call :before_successful_authorization callback" do expect(Doorkeeper.config).not_to receive(:before_successful_authorization) end it "does not call :after_successful_authorization callback" do expect(Doorkeeper.config).not_to receive(:after_successful_authorization) end end end describe "GET #new token request with native url and skip_authorization true" do before do allow(Doorkeeper.config).to receive(:skip_authorization).and_return(proc do true end) client.update_attribute :redirect_uri, "urn:ietf:wg:oauth:2.0:oob" get :new, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "redirects immediately" do expect(response).to be_redirect expect(response.location).to match(%r{/oauth/token/info\?access_token=}) end it "does not issue a grant" do expect(Doorkeeper::AccessGrant.count).to be 0 end it "issues a token" do expect(Doorkeeper::AccessToken.count).to be 1 end end describe "GET #new code request with native url and skip_authorization true" do before do allow(Doorkeeper.config).to receive(:grant_flows).and_return(%w[authorization_code]) allow(Doorkeeper.config).to receive(:skip_authorization).and_return(proc do true end) client.update_attribute :redirect_uri, "urn:ietf:wg:oauth:2.0:oob" get :new, params: { client_id: client.uid, response_type: "code", redirect_uri: client.redirect_uri, } end it "redirects immediately" do expect(response).to be_redirect expect(response.location) .to match(%r{/oauth/authorize/native\?code=#{Doorkeeper::AccessGrant.first.token}}) end it "issues a grant" do expect(Doorkeeper::AccessGrant.count).to be 1 end it "does not issue a token" do expect(Doorkeeper::AccessToken.count).to be 0 end context 'with use_url_path_for_native_authorization' do around(:each) do |example| Doorkeeper.configure do orm DOORKEEPER_ORM use_url_path_for_native_authorization end Rails.application.reload_routes! example.run Doorkeeper.configure do orm DOORKEEPER_ORM end Rails.application.reload_routes! end it 'should redirect immediately' do expect(response).to be_redirect expect(response.location).to match(/oauth\/authorize\/#{Doorkeeper::AccessGrant.first.token}/) end it 'should issue a grant' do expect(Doorkeeper::AccessGrant.count).to be 1 end it 'should not issue a token' do expect(Doorkeeper::AccessToken.count).to be 0 end end end describe "GET #new with skip_authorization true" do before do allow(Doorkeeper.config).to receive(:skip_authorization).and_return(proc do true end) get :new, params: params end context "without response_mode parameter" do let(:params) do { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "redirects immediately" do expect(response).to be_redirect expect(response.location).to match(/^#{client.redirect_uri}/) end it "issues a token" do expect(Doorkeeper::AccessToken.count).to be 1 end it "includes token type in fragment" do expect(response.query_params["token_type"]).to eq("Bearer") end it "includes token expiration in fragment" do expect(response.query_params["expires_in"].to_i).to eq(1234) end it "issues the token for the current client" do expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) end it "issues the token for the current resource owner" do expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) end end context "with 'form_post' as response_mode" do let(:params) do { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "renders 200 status" do expect(response.status).to eq 200 end it "issues a token" do expect(Doorkeeper::AccessToken.count).to be 1 end it "issues the token for the current client" do expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) end it "issues the token for the current resource owner" do expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) end end end describe "GET #new with skip_authorization false" do let(:params) do { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end before do allow(Doorkeeper.config.access_token_model).to receive(:matching_token_for).and_return(true) client.update_attribute :confidential, confidential_client get :new, params: params end context "with matching token and confidential application" do let(:confidential_client) { true } it "redirects immediately" do expect(subject).not_to receive(:render) expect(response).to be_redirect expect(response.location).to match(/^#{client.redirect_uri}/) end it "issues a token" do expect(Doorkeeper::AccessToken.count).to be 1 end end context "with matching token and non-confidential application" do let(:confidential_client) { false } it "renders the new view" do expect(response).to be_successful expect(subject).to render_with :new end it "doesn't issue a token" do expect(Doorkeeper::AccessToken.count).to be 0 end end end describe "GET #new in API mode" do before do allow(Doorkeeper.config).to receive(:api_only).and_return(true) get :new, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "renders success" do expect(response).to be_successful end it "sets status to pre-authorization" do expect(json_response["status"]).to eq(I18n.t("doorkeeper.pre_authorization.status")) end it "sets correct values" do expect(json_response["client_id"]).to eq(client.uid) expect(json_response["redirect_uri"]).to eq(client.redirect_uri) expect(json_response["state"]).to be_nil expect(json_response["response_type"]).to eq("token") expect(json_response["scope"]).to eq("default") end end describe "GET #new in API mode with skip_authorization true" do before do allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { true }) allow(Doorkeeper.configuration).to receive(:api_only).and_return(true) get :new, params: params end context "without response_mode parameter" do let(:params) do { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "renders success" do expect(response).to be_successful end it "issues a token" do expect(Doorkeeper::AccessToken.count).to be 1 end it "sets status to redirect" do expect(JSON.parse(response.body)["status"]).to eq("redirect") end it "sets redirect_uri to correct value" do redirect_uri = JSON.parse(response.body)["redirect_uri"] expect(redirect_uri).not_to be_nil expect(redirect_uri.match(/token_type=(\w+)&?/)[1]).to eq "Bearer" expect(redirect_uri.match(/expires_in=(\d+)&?/)[1].to_i).to eq 1234 expect( redirect_uri.match(/access_token=([a-zA-Z0-9\-_]+)&?/)[1], ).to eq Doorkeeper::AccessToken.first.token end it "issues the token for the current client" do expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) end it "issues the token for the current resource owner" do expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) end end context "with 'form_post' as response_mode" do let(:params) do { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "renders success" do expect(response).to be_successful end it "renders correct status" do expect(response_json_body["status"]).to eq "post" end it "renders correct redirect uri" do expect(response_json_body["redirect_uri"]).to eq(client.redirect_uri) end it "includes access token in fragment" do expect(response_json_body["body"]["access_token"]).to eq(Doorkeeper::AccessToken.first.token) end it "includes token type in fragment" do expect(response_json_body["body"]["token_type"]).to eq "Bearer" end it "includes token expiration in fragment" do expect(response_json_body["body"]["expires_in"]).to eq 1234 end it "issues a token" do expect(Doorkeeper::AccessToken.count).to be 1 end it "issues the token for the current client" do expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) end it "issues the token for the current resource owner" do expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) end end end describe "GET #new with errors" do context "without valid params" do before do default_scopes_exist :public get :new, params: { an_invalid: "request" } end it "does not redirect" do expect(response).not_to be_redirect end it "does not issue any token" do expect(Doorkeeper::AccessGrant.count).to eq 0 expect(Doorkeeper::AccessToken.count).to eq 0 end end context "when user cannot access application" do before do allow(Doorkeeper.configuration).to receive(:authorize_resource_owner_for_client).and_return(->(*_) { false }) get :new, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "does not redirect" do expect(response).not_to be_redirect end it "does not issue any token" do expect(Doorkeeper::AccessGrant.count).to eq 0 expect(Doorkeeper::AccessToken.count).to eq 0 end end end describe "GET #new in API mode with errors" do before do allow(Doorkeeper.configuration).to receive(:api_only).and_return(true) default_scopes_exist :public end context "without valid params" do before do get :new, params: { an_invalid: "request" } end it "renders bad request" do expect(response).to have_http_status(:bad_request) end it "includes error in body" do expect(response_json_body["error"]).to eq("invalid_request") end it "includes error description in body" do expect(response_json_body["error_description"]) .to eq(translated_invalid_request_error_message(:missing_param, :client_id)) end it "does not issue any token" do expect(Doorkeeper::AccessGrant.count).to eq 0 expect(Doorkeeper::AccessToken.count).to eq 0 end end context "when user cannot access application" do before do allow(Doorkeeper.configuration).to receive(:authorize_resource_owner_for_client).and_return(->(*_) { false }) get :new, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "renders unauthorized" do expect(response).to have_http_status(:unauthorized) end it "includes error in body" do expect(response_json_body["error"]).to eq("invalid_client") end it "includes error description in body" do expect(response_json_body["error_description"]) .to eq(translated_error_message(:invalid_client)) end it "does not issue any token" do expect(Doorkeeper::AccessGrant.count).to eq 0 expect(Doorkeeper::AccessToken.count).to eq 0 end end context "with 'form_post' as response_mode" do before do post :create, params: { client_id: client.uid, response_type: "token", scope: "invalid", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "renders 400 error" do expect(response.status).to eq 400 end it "renders correct status" do expect(response_json_body["status"]).to eq "post" end it "renders correct redirect uri" do expect(response_json_body["redirect_uri"]).to eq(client.redirect_uri) end it "includes access token in fragment" do expect(response_json_body["body"]["access_token"]).to be_nil end it "includes token type in fragment" do expect(response_json_body["body"]["error"]).to eq "invalid_scope" end it "includes token expiration in fragment" do expect(response_json_body["body"]["error_description"]).not_to be_nil end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end end end describe "GET #new with errors with handle_auth_errors :redirect" do before { config_is_set(:handle_auth_errors, :redirect) } context "without valid params" do before do default_scopes_exist :public get :new, params: { an_invalid: "request" } end it "does not redirect" do expect(response).not_to be_redirect end it "does not issue any token" do expect(Doorkeeper::AccessGrant.count).to eq 0 expect(Doorkeeper::AccessToken.count).to eq 0 end end context "invalid scope" do before do default_scopes_exist :public get :new, params: { client_id: client.uid, response_type: "token", scope: "invalid", redirect_uri: client.redirect_uri, state: "return-this", } end it "redirects to client redirect uri" do expect(response).to be_redirect expect(response.location).to match(/^#{client.redirect_uri}/) end it "includes error in fragment" do expect(response.query_params["error"]).to eq("invalid_scope") end it "includes error description in fragment" do expect(response.query_params["error_description"]).to eq(translated_error_message(:invalid_scope)) end it "includes state in fragment" do expect(response.query_params["state"]).to eq("return-this") end it "does not issue any token" do expect(Doorkeeper::AccessGrant.count).to eq 0 expect(Doorkeeper::AccessToken.count).to eq 0 end end context "invalid scope with form_post response mode" do before do default_scopes_exist :public get :new, params: { client_id: client.uid, response_type: "token", scope: "invalid", redirect_uri: client.redirect_uri, state: "return-this", response_mode: "form_post", } end it "renders the form_post page" do expect(response.status).to eq(200) end it "includes the error in the redirect post" do expect(response.body).to include("invalid_scope") end end context "invalid redirect_uri" do before do default_scopes_exist :public get :new, params: { client_id: client.uid, response_type: "token", redirect_uri: "invalid", } end it "does not redirect" do expect(response).not_to be_redirect end it "does not issue any token" do expect(Doorkeeper::AccessGrant.count).to eq 0 expect(Doorkeeper::AccessToken.count).to eq 0 end end context "with client_id and redirect_uri" do before do default_scopes_exist :public get :new, params: { client_id: client.uid, redirect_uri: client.redirect_uri, response_mode: "fragment" } end it "redirects to client redirect uri" do expect(response).to be_redirect expect(response.location).to match(/^#{client.redirect_uri}/) end it "includes error in fragment" do expect(response.query_params["error"]).to eq("invalid_request") end it "includes error description in fragment" do expect(response.query_params["error_description"]).to eq(translated_invalid_request_error_message(:missing_param, :response_type)) end it "does not issue any token" do expect(Doorkeeper::AccessGrant.count).to eq 0 expect(Doorkeeper::AccessToken.count).to eq 0 end end end describe "GET #new with errors with handle_auth_errors :raise" do before { config_is_set(:handle_auth_errors, :raise) } context "without valid params" do before do default_scopes_exist :public end it "raises InvalidRequest error" do expect { get :new, params: { an_invalid: "request" } }.to raise_error(Doorkeeper::Errors::InvalidRequest) end it "does not issue any token" do expect do get :new, params: { an_invalid: "request" } rescue Doorkeeper::Errors::InvalidRequest end.not_to change(Doorkeeper::AccessGrant, :count) expect do get :new, params: { an_invalid: "request" } rescue Doorkeeper::Errors::InvalidRequest end.not_to change(Doorkeeper::AccessToken, :count) end end context "invalid client_id" do before do default_scopes_exist :public end it "raises InvalidClient error" do expect { get :new, params: { client_id: "invalid" } }.to raise_error(Doorkeeper::Errors::InvalidClient) end end context "invalid scope" do before do default_scopes_exist :public end it "raises InvalidScope error" do expect do get :new, params: { client_id: client.uid, response_type: "token", scope: "invalid", redirect_uri: client.redirect_uri, state: "return-this", } end.to raise_error(Doorkeeper::Errors::InvalidScope) end end context "invalid redirect_uri" do before do default_scopes_exist :public end it "raises InvalidRedirectUri error" do expect do get :new, params: { client_id: client.uid, response_type: "token", redirect_uri: "invalid", } end.to raise_error(Doorkeeper::Errors::InvalidRedirectUri) end end end describe "POST #create with errors with handle_auth_errors :raise" do before { config_is_set(:handle_auth_errors, :raise) } context "without valid params" do before do default_scopes_exist :public end it "raises InvalidRequest error" do expect { post :create, params: { an_invalid: "request" } }.to raise_error(Doorkeeper::Errors::InvalidRequest) end it "does not issue any token" do expect do post :create, params: { an_invalid: "request" } rescue Doorkeeper::Errors::InvalidRequest end.not_to change(Doorkeeper::AccessGrant, :count) expect do post :create, params: { an_invalid: "request" } rescue Doorkeeper::Errors::InvalidRequest end.not_to change(Doorkeeper::AccessToken, :count) end end context "invalid client_id" do before do default_scopes_exist :public end it "raises InvalidClient error" do expect { post :create, params: { client_id: "invalid" } }.to raise_error(Doorkeeper::Errors::InvalidClient) end end context "invalid scope" do before do default_scopes_exist :public end it "raises InvalidScope error" do expect do post :create, params: { client_id: client.uid, response_type: "token", scope: "invalid", redirect_uri: client.redirect_uri, state: "return-this", } end.to raise_error(Doorkeeper::Errors::InvalidScope) end end context "invalid redirect_uri" do before do default_scopes_exist :public end it "raises InvalidRedirectUri error" do expect do post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: "invalid", } end.to raise_error(Doorkeeper::Errors::InvalidRedirectUri) end end end describe "GET #new with callbacks" do after do client.update_attribute :redirect_uri, "urn:ietf:wg:oauth:2.0:oob" get :new, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri } end describe "when authorizing" do before do allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { true }) end it "calls :before_successful_authorization callback" do expect(Doorkeeper.configuration) .to receive_message_chain(:before_successful_authorization, :call) .with(instance_of(described_class), instance_of(Doorkeeper::OAuth::Hooks::Context)) end it "calls :after_successful_authorization callback" do expect(Doorkeeper.configuration) .to receive_message_chain(:after_successful_authorization, :call) .with(instance_of(described_class), instance_of(Doorkeeper::OAuth::Hooks::Context)) end end describe "when not authorizing" do before do allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { false }) end it "does not call :before_successful_authorization callback" do expect(Doorkeeper.configuration).not_to receive(:before_successful_authorization) end it "does not call :after_successful_authorization callback" do expect(Doorkeeper.configuration).not_to receive(:after_successful_authorization) end end describe "when not authorizing in api mode" do before do allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { false }) allow(Doorkeeper.configuration).to receive(:api_only).and_return(true) end it "does not call :before_successful_authorization callback" do expect(Doorkeeper.configuration).not_to receive(:before_successful_authorization) end it "does not call :after_successful_authorization callback" do expect(Doorkeeper.configuration).not_to receive(:after_successful_authorization) end end end describe "authorize response memoization" do it "memoizes the result of the authorization" do pre_auth = double(:pre_auth, authorizable?: true) allow(subject).to receive(:pre_auth) { pre_auth } strategy = double(:strategy, authorize: true) expect(strategy).to receive(:authorize).once allow(subject).to receive(:strategy) { strategy } allow(subject).to receive(:create) do 2.times { subject.send :authorize_response } subject.render json: {}, status: :ok end post :create end end describe "strong parameters" do it "ignores non-scalar scope parameter" do get :new, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, scope: { "0" => "profile" }, } expect(response).to be_successful end end describe "DELETE #destroy" do context "without form_post response mode" do before do delete :destroy, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "redirects" do expect(response).to be_redirect end it "redirects to client redirect uri" do expect(response.location).to match(/^#{client.redirect_uri}/) end it "includes error in fragment" do expect(response.query_params["error"]).to eq("access_denied") end end context "with form_post response mode" do before do delete :destroy, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "redirects after authorization" do expect(response.status).to eq(200) end it "includes the error in the redirect post" do expect(response.body).to include("access_denied") end end context "with invalid params" do before do delete :destroy, params: { client_id: client.uid, response_type: "blabla", redirect_uri: client.redirect_uri, } end it "renders the error page correctly" do expect(response.status).to eq(200) end it "includes the error in the page" do expect(response.body).to include( translated_error_message(:unsupported_grant_type), ) end end end describe "DELETE #destroy in API mode" do before do allow(Doorkeeper.config).to receive(:api_only).and_return(true) end context "without form_post response mode" do before do delete :destroy, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, } end it "renders bad request" do expect(response).to have_http_status(:bad_request) end it "includes access_denied in the redirect uri" do expect(response_json_body["redirect_uri"].match(/error=(\w+)&?/)[1]).to eq("access_denied") end end context "with form_post response mode" do before do delete :destroy, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "renders bad request" do expect(response).to have_http_status(:bad_request) end it "includes the correct redirect uri" do expect(response_json_body["redirect_uri"]).to eq(client.redirect_uri) end it "includes access_denied in the body" do expect(response_json_body["body"]["error"]).to eq("access_denied") end end context "with invalid params" do before do delete :destroy, params: { client_id: client.uid, response_type: "blabla", redirect_uri: client.redirect_uri, response_mode: "form_post", } end it "renders bad request" do expect(response).to have_http_status(:bad_request) end it "includes error in body" do expect(response_json_body["error"]).to eq("unsupported_grant_type") end end end end ================================================ FILE: spec/controllers/protected_resources_controller_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" module ControllerActions def index render plain: "index" end def show render plain: "show" end def doorkeeper_unauthorized_render_options(*); end def doorkeeper_forbidden_render_options(*); end end RSpec.describe "doorkeeper authorize filter" do render_views context "when accepts token code specified as" do controller do before_action :doorkeeper_authorize! def index render plain: "index" end end let(:token_string) { "1A2BC3" } let(:token) do double( Doorkeeper::AccessToken, acceptable?: true, previous_refresh_token: "", revoke_previous_refresh_token!: true, ) end it "access_token param" do expect(Doorkeeper::AccessToken).to receive(:by_token).with(token_string).and_return(token) get :index, params: { access_token: token_string } end it "bearer_token param" do expect(Doorkeeper::AccessToken).to receive(:by_token).with(token_string).and_return(token) get :index, params: { bearer_token: token_string } end it "Authorization header" do expect(Doorkeeper::AccessToken).to receive(:by_token).with(token_string).and_return(token) request.env["HTTP_AUTHORIZATION"] = "Bearer #{token_string}" get :index end it "different kind of Authorization header" do expect(Doorkeeper::AccessToken).not_to receive(:by_token) request.env["HTTP_AUTHORIZATION"] = "MAC #{token_string}" get :index end it "does not change Authorization header value" do expect(Doorkeeper::AccessToken).to receive(:by_token).twice.and_return(token) request.env["HTTP_AUTHORIZATION"] = "Bearer #{token_string}" get :index controller.send(:remove_instance_variable, :@doorkeeper_token) get :index end end context "when defined for all actions" do controller do before_action :doorkeeper_authorize! include ControllerActions end context "with valid token", token: :valid do it "allows into index action" do get :index, params: { access_token: token_string } expect(response).to be_successful end it "allows into show action" do get :show, params: { id: "4", access_token: token_string } expect(response).to be_successful end end context "with invalid token", token: :invalid do it "does not allow into index action" do get :index, params: { access_token: token_string } expect(response.status).to eq 401 expect(response.header["WWW-Authenticate"]).to match(/^Bearer/) end it "does not allow into show action" do get :show, params: { id: "4", access_token: token_string } expect(response.status).to eq 401 expect(response.header["WWW-Authenticate"]).to match(/^Bearer/) end end end context "when defined with scopes" do controller do before_action -> { doorkeeper_authorize! :write } include ControllerActions end let(:token_string) { "1A2DUWE" } it "allows if the token has particular scopes" do token = double( Doorkeeper::AccessToken, accessible?: true, scopes: %w[write public], previous_refresh_token: "", revoke_previous_refresh_token!: true, ) expect(token).to receive(:acceptable?).with([:write]).and_return(true) expect( Doorkeeper::AccessToken, ).to receive(:by_token).with(token_string).and_return(token) get :index, params: { access_token: token_string } expect(response).to be_successful end it "does not allow if the token does not include given scope" do token = double( Doorkeeper::AccessToken, accessible?: true, scopes: ["public"], revoked?: false, expired?: false, previous_refresh_token: "", revoke_previous_refresh_token!: true, ) expect( Doorkeeper::AccessToken, ).to receive(:by_token).with(token_string).and_return(token) expect(token).to receive(:acceptable?).with([:write]).and_return(false) get :index, params: { access_token: token_string } expect(response.status).to eq 403 expect(response.header["WWW-Authenticate"]).to include('error="insufficient_scope"') end end context "when custom unauthorized render options are configured" do controller do before_action :doorkeeper_authorize! include ControllerActions end context "with a JSON custom render", token: :invalid do before do module ControllerActions remove_method :doorkeeper_unauthorized_render_options def doorkeeper_unauthorized_render_options(error: nil) { json: ActiveSupport::JSON.encode(error_message: error.description) } end end end after do module ControllerActions remove_method :doorkeeper_unauthorized_render_options def doorkeeper_unauthorized_render_options(error: nil); end end end it "renders a custom JSON response", token: :invalid do get :index, params: { access_token: token_string } expect(response.status).to eq 401 expect(response.content_type).to include("application/json") expect(response.header["WWW-Authenticate"]).to match(/^Bearer/) expect(json_response).not_to be_nil expect(json_response["error_message"]).to match("token is invalid") end end context "with a text custom render", token: :invalid do before do module ControllerActions remove_method :doorkeeper_unauthorized_render_options def doorkeeper_unauthorized_render_options(**) { plain: "Unauthorized" } end end end after do module ControllerActions remove_method :doorkeeper_unauthorized_render_options def doorkeeper_unauthorized_render_options(error: nil); end end end it "renders a custom text response", token: :invalid do get :index, params: { access_token: token_string } expect(response.status).to eq 401 expect(response.content_type).to include("text/plain") expect(response.header["WWW-Authenticate"]).to match(/^Bearer/) expect(response.body).to eq("Unauthorized") end end end context "when custom forbidden render options are configured" do before do expect(Doorkeeper::AccessToken).to receive(:by_token).with(token_string).and_return(token) expect(token).to receive(:acceptable?).with([:write]).and_return(false) end after do module ControllerActions remove_method :doorkeeper_forbidden_render_options def doorkeeper_forbidden_render_options(*); end end end controller do before_action -> { doorkeeper_authorize! :write } include ControllerActions end let(:token) do double( Doorkeeper::AccessToken, accessible?: true, scopes: ["public"], revoked?: false, expired?: false, previous_refresh_token: "", revoke_previous_refresh_token!: true, ) end let(:token_string) { "1A2DUWE" } context "with a JSON custom render" do before do module ControllerActions remove_method :doorkeeper_forbidden_render_options def doorkeeper_forbidden_render_options(*) { json: { error_message: "Forbidden" } } end end end it "renders a custom JSON response" do get :index, params: { access_token: token_string } expect(response.header["WWW-Authenticate"]).to include('error="insufficient_scope"') expect(response.content_type).to include("application/json") expect(response.status).to eq 403 expect(json_response).not_to be_nil expect(json_response["error_message"]).to match("Forbidden") end end context "with a status and JSON custom render" do before do module ControllerActions remove_method :doorkeeper_forbidden_render_options def doorkeeper_forbidden_render_options(*) { json: { error_message: "Not Found" }, respond_not_found_when_forbidden: true, } end end end it "overrides the default status code" do get :index, params: { access_token: token_string } expect(response.status).to eq 404 end end context "with a text custom render" do before do module ControllerActions remove_method :doorkeeper_forbidden_render_options def doorkeeper_forbidden_render_options(*) { plain: "Forbidden" } end end end it "renders a custom status code and text response" do get :index, params: { access_token: token_string } expect(response.header["WWW-Authenticate"]).to include('error="insufficient_scope"') expect(response.status).to eq 403 expect(response.body).to eq("Forbidden") end end context "with a status and text custom render" do before do module ControllerActions remove_method :doorkeeper_forbidden_render_options def doorkeeper_forbidden_render_options(*) { respond_not_found_when_forbidden: true, plain: "Not Found" } end end end it "overrides the default status code" do get :index, params: { access_token: token_string } expect(response.status).to eq 404 end end end context "when handle_auth_errors option is set to :raise" do subject(:request) { get :index, params: { access_token: token_string } } before do config_is_set(:handle_auth_errors, :raise) end controller do before_action :doorkeeper_authorize! include ControllerActions end context "when token is unknown" do it "raises Doorkeeper::Errors::TokenUnknown exception", token: :invalid do expect { request }.to raise_error(Doorkeeper::Errors::TokenUnknown) end end context "when token is expired" do it "raises Doorkeeper::Errors::TokenExpired exception", token: :expired do expect { request }.to raise_error(Doorkeeper::Errors::TokenExpired) end end context "when token is revoked" do it "raises Doorkeeper::Errors::TokenRevoked exception", token: :revoked do expect { request }.to raise_error(Doorkeeper::Errors::TokenRevoked) end end context "when token is forbidden" do it "raises Doorkeeper::Errors::TokenForbidden exception", token: :forbidden do expect { request }.to raise_error(Doorkeeper::Errors::TokenForbidden) end end context "when token is valid" do it "allows into index action", token: :valid do expect(response).to be_successful end end end end ================================================ FILE: spec/controllers/token_info_controller_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::TokenInfoController, type: :controller do render_views describe "when requesting token info with valid token" do let(:doorkeeper_token) { FactoryBot.create(:access_token) } describe "successful request" do it "responds with token info" do get :show, params: { access_token: doorkeeper_token.token } expect(response.body).to eq(doorkeeper_token.to_json) end it "responds with a 200 status" do get :show, params: { access_token: doorkeeper_token.token } expect(response.status).to eq 200 end end describe "invalid token response" do it "responds with 401 when doorkeeper_token is not valid" do get :show expect(response.status).to eq 401 expect(response.headers["WWW-Authenticate"]).to match(/^Bearer/) end it "responds with 401 when doorkeeper_token is invalid, expired or revoked" do allow(controller).to receive(:doorkeeper_token).and_return(doorkeeper_token) allow(doorkeeper_token).to receive(:accessible?).and_return(false) get :show expect(response.status).to eq 401 expect(response.headers["WWW-Authenticate"]).to match(/^Bearer/) end it "responds body message for error" do get :show expect(response.body).to eq( Doorkeeper::OAuth::InvalidTokenResponse.new.body.to_json, ) end end end end ================================================ FILE: spec/controllers/tokens_controller_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::TokensController, type: :controller do render_views subject(:json) { JSON.parse(response.body) } let(:client) { FactoryBot.create :application } let!(:user) { User.create!(name: "Joe", password: "sekret") } before do Doorkeeper.configure do orm DOORKEEPER_ORM resource_owner_from_credentials do User.first end end allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(["password"]) end describe "POST #create" do before do post :create, params: { client_id: client.uid, client_secret: client.secret, grant_type: "password", } end it "responds after authorization" do expect(response).to be_successful end it "includes access token in response" do expect(json["access_token"]).to eq(Doorkeeper::AccessToken.first.token) end it "includes token type in response" do expect(json["token_type"]).to eq("Bearer") end it "includes token expiration in response" do expect(json["expires_in"].to_i).to eq(Doorkeeper.configuration.access_token_expires_in) end it "issues the token for the current client" do expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) end it "issues the token for the current resource owner" do expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) end end describe "POST #create with errors" do let(:grant_type) { "password" } before do post :create, params: { client_id: client.uid, client_secret: "invalid", grant_type:, } end it "responds after authorization" do expect(response).to be_unauthorized end it "include error in response" do expect(json["error"]).to eq("invalid_client") end it "include error_description in response" do expect(json["error_description"]).to be_present end it "does not include access token in response" do expect(json["access_token"]).to be_nil end it "does not include token type in response" do expect(json["token_type"]).to be_nil end it "does not include token expiration in response" do expect(json["expires_in"]).to be_nil end it "does not issue any access token" do expect(Doorkeeper::AccessToken.all).to be_empty end context "when controller is extended" do controller(Doorkeeper::TokensController) do def create headers.merge!("Custom-Header" => authorize_response.headers) super end end let(:grant_type) { "refresh_token" } it "still handles errors" do expect(response).to be_bad_request end end end describe "POST #create with callbacks" do after do client.update_attribute :redirect_uri, "urn:ietf:wg:oauth:2.0:oob" end describe "when successful" do after do post :create, params: { client_id: client.uid, client_secret: client.secret, grant_type: "password", } end it "calls :before_successful_authorization callback" do expect(Doorkeeper.configuration) .to receive_message_chain(:before_successful_authorization, :call).with(instance_of(described_class), nil) end it "calls :after_successful_authorization callback" do expect(Doorkeeper.configuration) .to receive_message_chain(:after_successful_authorization, :call) .with(instance_of(described_class), instance_of(Doorkeeper::OAuth::Hooks::Context)) end end describe "with errors" do after do post :create, params: { client_id: client.uid, client_secret: "invalid", grant_type: "password", } end it "calls :before_successful_authorization callback" do expect(Doorkeeper.configuration) .to receive_message_chain(:before_successful_authorization, :call).with(instance_of(described_class), nil) end it "does not call :after_successful_authorization callback" do expect(Doorkeeper.configuration).not_to receive(:after_successful_authorization) end end end describe "POST #create with custom error" do it "returns the error response with a custom message" do # I18n looks for `doorkeeper.errors.messages.custom_message` in locale files custom_message = "my_message" allow(I18n).to receive(:translate) .with( custom_message, hash_including(scope: %i[doorkeeper errors messages]), ) .and_return("Authorization custom message") doorkeeper_error = Doorkeeper::Errors::DoorkeeperError.new(custom_message) strategy = double(:strategy) request = double(token_request: strategy) allow(strategy).to receive(:authorize).and_raise(doorkeeper_error) allow(controller).to receive(:server).and_return(request) post :create expected_response_body = { "error" => custom_message, "error_description" => "Authorization custom message", } expect(response.status).to eq 400 expect(response.headers["WWW-Authenticate"]).to match(/Bearer/) expect(JSON.parse(response.body)).to eq expected_response_body end end # https://datatracker.ietf.org/doc/html/rfc7009#section-2.2 describe "POST #revoke" do let(:client) { FactoryBot.create(:application) } let(:revoked_at) { nil } let(:access_token) do FactoryBot.create( :access_token, application: client, revoked_at: revoked_at, use_refresh_token: true ) end context "when associated app is public" do let(:client) { FactoryBot.create(:application, confidential: false) } it "returns 200" do post :revoke, params: { client_id: client.uid, token: access_token.token } expect(response.status).to eq 200 end it "does not revoke the access token when token_type_hint == refresh_token" do post :revoke, params: { client_id: client.uid, token: access_token.token, token_type_hint: "refresh_token", } expect(response.status).to eq 200 expect(access_token.reload).to have_attributes(revoked?: false) end it "revokes the refresh token when token_type_hint == refresh_token" do post :revoke, params: { client_id: client.uid, token: access_token.refresh_token, token_type_hint: "refresh_token", } expect(response.status).to eq 200 expect(access_token.reload).to have_attributes(revoked?: true) end it "revokes the refresh token when token_type_hint not passed" do post :revoke, params: { client_id: client.uid, token: access_token.refresh_token, } expect(response.status).to eq 200 expect(access_token.reload).to have_attributes(revoked?: true) end context "when access_token has already been revoked" do let(:revoked_at) { 1.day.ago.floor } it "does not update the revoked_at when the access token has already been revoked" do post :revoke, params: { client_id: client.uid, token: access_token.token, } expect(response.status).to eq 200 expect(access_token.reload).to have_attributes(revoked_at: revoked_at) end it "does not update the revoked_at when the refresh token has already been revoked" do post :revoke, params: { client_id: client.uid, token: access_token.refresh_token, } expect(response.status).to eq 200 expect(access_token.reload).to have_attributes(revoked_at: revoked_at) end end it "does not revoke when the access token has expired" do access_token.update!(created_at: access_token.created_at - access_token.expires_in - 1) post :revoke, params: { client_id: client.uid, token: access_token.token, } expect(response.status).to eq 200 expect(access_token.reload).to have_attributes(revoked?: false) end it "revokes the refresh token after the access token has expired" do access_token.update!(created_at: access_token.created_at - access_token.expires_in - 1) post :revoke, params: { client_id: client.uid, token: access_token.refresh_token, } expect(response.status).to eq 200 expect(access_token.reload).to have_attributes(revoked?: true) end end context "when associated app is confidential" do let(:client) { FactoryBot.create(:application, confidential: true) } let(:oauth_client) { Doorkeeper::OAuth::Client.new(client) } let(:server) { instance_double(Doorkeeper::Server) } before do allow(Doorkeeper::Server).to receive(:new).and_return(server) allow(server).to receive(:client).and_return(oauth_client) end it "returns 200" do post :revoke, params: { token: access_token.token } expect(response.status).to eq 200 end it "revokes the access token" do post :revoke, params: { token: access_token.token } expect(access_token.reload).to have_attributes(revoked?: true) end context "when authorization fails" do let(:some_other_client) { FactoryBot.create(:application, confidential: true) } let(:oauth_client) { Doorkeeper::OAuth::Client.new(some_other_client) } it "returns 403" do post :revoke, params: { token: access_token.token } expect(response.status).to eq 403 end it "does not revoke the access token" do post :revoke, params: { token: access_token.token } expect(access_token.reload).to have_attributes(revoked?: false) end end end end describe "POST #introspect" do let(:client) { FactoryBot.create(:application) } let(:access_token) { FactoryBot.create(:access_token, application: client) } let(:token_for_introspection) { FactoryBot.create(:access_token, application: client) } context "when authorized using valid Bearer token" do it "responds with full token introspection" do request.headers["Authorization"] = "Bearer #{access_token.token}" post :introspect, params: { token: token_for_introspection.token } expect(json_response).to include("active" => true) expect(json_response).to include("client_id", "token_type", "exp", "iat") end end context "when authorized using Client Credentials of the client that token is issued to" do it "responds with full token introspection" do request.headers["Authorization"] = basic_auth_header_for_client(client) post :introspect, params: { token: token_for_introspection.token } expect(json_response).to match( "active" => true, "client_id" => client.uid, "token_type" => "Bearer", "scope" => nil, "exp" => an_instance_of(Integer), "iat" => an_instance_of(Integer), ) end end context "when token introspection disabled" do before do Doorkeeper.configure do orm DOORKEEPER_ORM allow_token_introspection false end end it "responds with invalid_token error for bearer auth" do request.headers["Authorization"] = "Bearer #{access_token.token}" post :introspect, params: { token: token_for_introspection.token } response_status_should_be 401 expect(json_response).not_to include("active") expect(json_response).to include("error" => "invalid_token") end it "responds with access_denied error for basic auth" do request.headers["Authorization"] = basic_auth_header_for_client(client) post :introspect, params: { token: token_for_introspection.token } response_status_should_be 200 expect(json_response).to include("active" => false) end end context "when custom introspection response configured" do before do Doorkeeper.configure do orm DOORKEEPER_ORM custom_introspection_response do |_token, _context| { sub: "Z5O3upPC88QrAjx00dis", aud: "https://protected.example.net/resource", } end end end it "responds with full token introspection" do request.headers["Authorization"] = "Bearer #{access_token.token}" post :introspect, params: { token: token_for_introspection.token } expect(json_response).to match( "active" => true, "client_id" => client.uid, "token_type" => "Bearer", "scope" => nil, "exp" => an_instance_of(Integer), "iat" => an_instance_of(Integer), "aud" => "https://protected.example.net/resource", "sub" => "Z5O3upPC88QrAjx00dis", ) end end context "when access token is public" do let(:token_for_introspection) { FactoryBot.create(:access_token, application: nil) } it "responds with full token introspection" do request.headers["Authorization"] = basic_auth_header_for_client(client) post :introspect, params: { token: token_for_introspection.token } expect(json_response).to match( "active" => true, "client_id" => nil, "token_type" => "Bearer", "scope" => nil, "exp" => an_instance_of(Integer), "iat" => an_instance_of(Integer), ) end end context "when token never expires (expires_in is nil)" do let(:token_for_introspection) { FactoryBot.create(:access_token, application: client, expires_in: nil) } it "omits the exp field per RFC 7662" do request.headers["Authorization"] = basic_auth_header_for_client(client) post :introspect, params: { token: token_for_introspection.token } expect(json_response).to match( "active" => true, "client_id" => client.uid, "token_type" => "Bearer", "scope" => nil, "iat" => an_instance_of(Integer), ) expect(json_response).not_to have_key("exp") end end context "when token was issued to a different client than is making this request" do let(:different_client) { FactoryBot.create(:application) } it "responds with only active state" do request.headers["Authorization"] = basic_auth_header_for_client(different_client) post :introspect, params: { token: token_for_introspection.token } expect(response).to be_successful expect(json_response).to match("active" => false) end end context "when introspection request authorized by a client and allow_token_introspection is true" do let(:different_client) { FactoryBot.create(:application) } before do allow(Doorkeeper.configuration).to receive(:allow_token_introspection).and_return(proc do true end) end it "responds with full token introspection" do request.headers["Authorization"] = basic_auth_header_for_client(different_client) post :introspect, params: { token: token_for_introspection.token } expect(json_response).to match( "active" => true, "client_id" => client.uid, "token_type" => "Bearer", "scope" => nil, "exp" => an_instance_of(Integer), "iat" => an_instance_of(Integer), ) end end context "when allow_token_introspection requires authorized token with special scope" do let(:access_token) { FactoryBot.create(:access_token, scopes: "introspection") } before do allow(Doorkeeper.configuration).to receive(:allow_token_introspection).and_return(proc do |_token, _client, authorized_token| authorized_token.scopes.include?("introspection") end) end it "responds with full token introspection if authorized token has introspection scope" do request.headers["Authorization"] = "Bearer #{access_token.token}" post :introspect, params: { token: token_for_introspection.token } expect(json_response).to match( "active" => true, "client_id" => client.uid, "token_type" => "Bearer", "scope" => nil, "exp" => an_instance_of(Integer), "iat" => an_instance_of(Integer), ) end it "responds with invalid_token error if authorized token doesn't have introspection scope" do access_token.update(scopes: "read write") request.headers["Authorization"] = "Bearer #{access_token.token}" post :introspect, params: { token: token_for_introspection.token } response_status_should_be 401 expect(json_response).to match( "error" => "invalid_token", "error_description" => an_instance_of(String), "state" => "unauthorized", ) end end context "when authorized using invalid Bearer token" do let(:access_token) do FactoryBot.create(:access_token, application: client, revoked_at: 1.day.ago) end it "responds with invalid_token error" do request.headers["Authorization"] = "Bearer #{access_token.token}" post :introspect, params: { token: token_for_introspection.token } response_status_should_be 401 expect(json_response).to match( "error" => "invalid_token", "error_description" => an_instance_of(String), "state" => "unauthorized", ) end end context "when authorized using the Bearer token that need to be introspected" do it "responds with invalid token error" do request.headers["Authorization"] = "Bearer #{access_token.token}" post :introspect, params: { token: access_token.token } response_status_should_be 401 expect(json_response).to match( "error" => "invalid_token", "error_description" => an_instance_of(String), "state" => "unauthorized", ) end end context "when invalid credentials used to authorize" do let(:client) { double(uid: "123123", secret: "666999") } let(:access_token) { FactoryBot.create(:access_token) } it "responds with invalid_client error" do request.headers["Authorization"] = basic_auth_header_for_client(client) post :introspect, params: { token: access_token.token } expect(response).not_to be_successful response_status_should_be 401 expect(json_response).to match( "error" => "invalid_client", "error_description" => an_instance_of(String), ) end end context "when wrong token value used" do context "when authorized using client credentials" do it "responds with only active state" do request.headers["Authorization"] = basic_auth_header_for_client(client) post :introspect, params: { token: SecureRandom.hex(16) } expect(json_response).to match("active" => false) end end context "when authorized using valid Bearer token" do it "responds with invalid_token error" do request.headers["Authorization"] = "Bearer #{access_token.token}" post :introspect, params: { token: SecureRandom.hex(16) } response_status_should_be 401 expect(json_response).to match( "error" => "invalid_token", "error_description" => an_instance_of(String), "state" => "unauthorized", ) end end end context "when requested access token expired" do let(:token_for_introspection) do FactoryBot.create(:access_token, application: client, created_at: 1.year.ago) end it "responds with only active state" do request.headers["Authorization"] = basic_auth_header_for_client(client) post :introspect, params: { token: token_for_introspection.token } expect(json_response).to match("active" => false) end end context "when requested Access Token revoked" do let(:token_for_introspection) do FactoryBot.create(:access_token, application: client, revoked_at: 1.year.ago) end it "responds with only active state" do request.headers["Authorization"] = basic_auth_header_for_client(client) post :introspect, params: { token: token_for_introspection.token } expect(json_response).to match("active" => false) end end context "when unauthorized (no bearer token or client credentials)" do let(:token_for_introspection) { FactoryBot.create(:access_token) } it "responds with invalid_request error" do post :introspect, params: { token: token_for_introspection.token } expect(response).not_to be_successful response_status_should_be 400 expect(json_response).to match( "error" => "invalid_request", "error_description" => I18n.t("doorkeeper.errors.messages.invalid_request.request_not_authorized"), ) end end end end ================================================ FILE: spec/doorkeeper/redirect_uri_validator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::RedirectUriValidator do subject(:client) do FactoryBot.create(:application) end it "is valid when the uri is a uri" do client.redirect_uri = "https://example.com/callback" expect(client).to be_valid end # Most mobile and desktop operating systems allow apps to register a custom URL # scheme that will launch the app when a URL with that scheme is visited from # the system browser. # # @see https://www.oauth.com/oauth2-servers/redirect-uris/redirect-uris-native-apps/ it "is valid when the uri is custom native URI" do client.redirect_uri = "myapp:/callback" expect(client).to be_valid end it "is valid when the uri has a query parameter" do client.redirect_uri = "https://example.com/abcd?xyz=123" expect(client).to be_valid end it "accepts nonstandard oob redirect uri" do client.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" expect(client).to be_valid end it "accepts nonstandard oob:auto redirect uri" do client.redirect_uri = "urn:ietf:wg:oauth:2.0:oob:auto" expect(client).to be_valid end it "is invalid when the uri is not a uri" do client.redirect_uri = "]" expect(client).not_to be_valid expect(client.errors[:redirect_uri].first).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.invalid_uri")) end it "is invalid when the uri is relative" do client.redirect_uri = "/abcd" expect(client).not_to be_valid expect(client.errors[:redirect_uri].first).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.relative_uri")) end it "is invalid when the uri has a fragment" do client.redirect_uri = "https://example.com/abcd#xyz" expect(client).not_to be_valid expect(client.errors[:redirect_uri].first).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.fragment_present")) end it "is invalid when scheme resolves to localhost (needs an explict scheme)" do client.redirect_uri = "localhost:80" expect(client).to be_invalid expect(client.errors[:redirect_uri].first).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.unspecified_scheme")) end it "is invalid if an ip address" do client.redirect_uri = "127.0.0.1:8080" expect(client).to be_invalid end it "accepts an ip address based URI if a scheme is specified" do client.redirect_uri = "https://127.0.0.1:8080" expect(client).to be_valid end it "is invalid when host is not specified" do client.redirect_uri = "https://" expect(client).to be_invalid expect(client.errors[:redirect_uri].first).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.invalid_uri")) end context "when force secured uri configured" do it "accepts a valid uri" do client.redirect_uri = "https://example.com/callback" expect(client).to be_valid end it "accepts custom scheme redirect uri (as per rfc8252 section 7.1)" do client.redirect_uri = "com.example.app:/oauth/callback" expect(client).to be_valid end it "accepts custom scheme redirect uri (as per rfc8252 section 7.1) #2" do client.redirect_uri = "com.example.app:/test" expect(client).to be_valid end it "accepts custom scheme redirect uri (common misconfiguration we have decided to allow)" do client.redirect_uri = "com.example.app://oauth/callback" expect(client).to be_valid end it "accepts custom scheme redirect uri (common misconfiguration we have decided to allow) #2" do client.redirect_uri = "com.example.app://test" expect(client).to be_valid end it "accepts a non secured protocol when disabled" do client.redirect_uri = "http://example.com/callback" allow(Doorkeeper.configuration).to receive( :force_ssl_in_redirect_uri, ).and_return(false) expect(client).to be_valid end it "accepts a non secured protocol when conditional option defined" do Doorkeeper.configure do orm DOORKEEPER_ORM force_ssl_in_redirect_uri { |uri| uri.host != "localhost" } end application = FactoryBot.build(:application, redirect_uri: "http://localhost/callback") expect(application).to be_valid application = FactoryBot.build(:application, redirect_uri: "https://test.com/callback") expect(application).to be_valid application = FactoryBot.build(:application, redirect_uri: "http://localhost2/callback") expect(application).not_to be_valid application = FactoryBot.build(:application, redirect_uri: "https://test.com/callback") expect(application).to be_valid end it "forbids redirect uri if required" do client.redirect_uri = "javascript://document.cookie" Doorkeeper.configure do orm DOORKEEPER_ORM forbid_redirect_uri { |uri| uri.scheme == "javascript" } end expect(client).to be_invalid expect(client.errors[:redirect_uri].first).to eq("is forbidden by the server.") client.redirect_uri = "https://localhost/callback" expect(client).to be_valid end it "invalidates the uri when the uri does not use a secure protocol" do client.redirect_uri = "http://example.com/callback" expect(client).not_to be_valid error = client.errors[:redirect_uri].first expect(error).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.secured_uri")) end end context "with multiple redirect uri" do it "invalidates the second uri when the first uri is native uri" do client.redirect_uri = "urn:ietf:wg:oauth:2.0:oob\nexample.com/callback" expect(client).to be_invalid end end context "with blank redirect URI" do it "forbids blank redirect uri by default" do client.redirect_uri = "" expect(client).to be_invalid expect(client.errors[:redirect_uri]).not_to be_blank end it "forbids blank redirect uri by custom condition" do Doorkeeper.configure do orm DOORKEEPER_ORM allow_blank_redirect_uri do |_grant_flows, application| application.name == "admin app" end end client.name = "test app" client.redirect_uri = "" expect(client).to be_invalid expect(client.errors[:redirect_uri]).not_to be_blank client.name = "admin app" expect(client).to be_valid end end end ================================================ FILE: spec/doorkeeper/server_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Server do subject(:server) do described_class.new(context) end let(:fake_class) { double :fake_class } let(:context) { double :context } describe ".authorization_request" do it "raises error when strategy does not match phase" do expect do server.token_request(:code) end.to raise_error(Doorkeeper::Errors::InvalidTokenStrategy) end context "when only Authorization Code strategy is enabled" do before do allow(Doorkeeper.configuration) .to receive(:grant_flows) .and_return(["authorization_code"]) end it "raises error when using the disabled Client Credentials strategy" do expect do server.token_request(:client_credentials) end.to raise_error(Doorkeeper::Errors::InvalidTokenStrategy) end end it "builds the request with selected strategy" do stub_const "Doorkeeper::Request::Code", fake_class expect(fake_class).to receive(:new).with(server) expect(::Kernel).to receive(:warn) server.authorization_request :code end it "builds the request with composite strategy name" do Doorkeeper.configure do grant_flows ["id_token token"] end stub_const "Doorkeeper::Request::IdTokenToken", fake_class expect(fake_class).to receive(:new).with(server) expect(::Kernel).to receive(:warn) server.authorization_request "id_token token" end end end ================================================ FILE: spec/doorkeeper/stale_records_cleaner_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::StaleRecordsCleaner do let(:cleaner) { described_class.new(model) } let(:models_by_name) do { access_token: Doorkeeper::AccessToken, access_grant: Doorkeeper::AccessGrant, } end let(:resource_owner) { FactoryBot.create(:resource_owner) } context "when ORM has no cleaner class" do it "raises an error" do allow(Doorkeeper.configuration).to receive(:orm).and_return("hibernate") expect do described_class.for(Doorkeeper::AccessToken) end.to raise_error(Doorkeeper::Errors::NoOrmCleaner, /has no cleaner/) end end %i[access_token access_grant].each do |model_name| context "(#{model_name})" do let(:model) { models_by_name.fetch(model_name) } describe "#clean_revoked" do context "with revoked record" do before do FactoryBot.create model_name, revoked_at: Time.current - 1.minute, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "removes the record" do expect { cleaner.clean_revoked }.to change(model, :count).to(0) end end context "with record revoked in the future" do before do FactoryBot.create model_name, revoked_at: Time.current + 1.minute, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "keeps the record" do expect { cleaner.clean_revoked }.not_to(change(model, :count)) end end context "with unrevoked record" do before do FactoryBot.create model_name, revoked_at: nil, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "keeps the record" do expect { cleaner.clean_revoked }.not_to(change(model, :count)) end end end describe "#clean_expired" do let(:ttl) { 500 } let(:expiry_border) { ttl.seconds.ago } context "with record that is past the threshold and expired" do before do FactoryBot.create model_name, created_at: expiry_border - 1.minute, expires_in: ttl, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "removes the record" do expect { cleaner.clean_expired(ttl) }.to change(model, :count).to(0) end end context "with record that is past the threshold, but not expired" do before do FactoryBot.create model_name, created_at: expiry_border - 1.minute, expires_in: 2 * ttl, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "keeps the record" do expect { cleaner.clean_expired(ttl) }.not_to(change(model, :count)) end end context "with record that is within the threshold and expired" do before do FactoryBot.create model_name, created_at: expiry_border + 1.minute, expires_in: ttl, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "keeps the record" do expect { cleaner.clean_expired(ttl) }.not_to(change(model, :count)) end end context "with record that is within the threshold, but not expired" do before do FactoryBot.create model_name, created_at: expiry_border + 1.minute, expires_in: 2 * ttl, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "keeps the record" do expect { cleaner.clean_expired(ttl) }.not_to(change(model, :count)) end end context "when the model uses an unsupported database adapter" do before do allow(model).to receive(:adapter_name).and_return("unsupported_db") end it "emits a warning" do expect(Kernel).to receive(:warn).with(/\[DOORKEEPER\].*doesn't support expiration time math for your database adapter/) cleaner.clean_expired(ttl) end end if model_name == :access_token context "with record that is past the threshold, but never expires" do before do FactoryBot.create model_name, created_at: expiry_border - 1.minute, expires_in: nil, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "keeps the record" do expect { cleaner.clean_expired(ttl) }.not_to(change(model, :count)) end end context "with record that is within the threshold, but never expires" do before do FactoryBot.create model_name, created_at: expiry_border + 1.minute, expires_in: nil, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "keeps the record" do expect { cleaner.clean_expired(ttl) }.not_to(change(model, :count)) end end end end end end end ================================================ FILE: spec/doorkeeper/version_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::VERSION do describe "#gem_version" do it "returns Gem::Version instance" do expect(Doorkeeper.gem_version).to be_an_instance_of(Gem::Version) end end describe "VERSION" do it "returns gem version string" do expect(Doorkeeper::VERSION::STRING).to match(/^\d+\.\d+\.\d+(\.\w+)?$/) end end end ================================================ FILE: spec/dummy/Rakefile ================================================ #!/usr/bin/env rake # frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path("config/application", __dir__) Dummy::Application.load_tasks ================================================ FILE: spec/dummy/app/assets/config/manifest.js ================================================ // JS and CSS bundles // ================================================ FILE: spec/dummy/app/controllers/application_controller.rb ================================================ # frozen_string_literal: true class ApplicationController < ActionController::Base protect_from_forgery with: :exception end ================================================ FILE: spec/dummy/app/controllers/custom_authorizations_controller.rb ================================================ # frozen_string_literal: true class CustomAuthorizationsController < ::ApplicationController %w[index show new create edit update destroy].each do |action| define_method action do render nothing: true end end end ================================================ FILE: spec/dummy/app/controllers/full_protected_resources_controller.rb ================================================ # frozen_string_literal: true class FullProtectedResourcesController < ApplicationController before_action -> { doorkeeper_authorize! :write, :admin }, only: :show before_action :doorkeeper_authorize!, only: :index def index render plain: "index" end def show render plain: "show" end end ================================================ FILE: spec/dummy/app/controllers/home_controller.rb ================================================ # frozen_string_literal: true class HomeController < ApplicationController def index; end def sign_in session[:user_id] = if Rails.env.development? User.first || User.create!(name: "Joe", password: "sekret") else User.first end redirect_to "/" end def callback render plain: "ok" end end ================================================ FILE: spec/dummy/app/controllers/metal_controller.rb ================================================ # frozen_string_literal: true class MetalController < ActionController::Metal include AbstractController::Callbacks include ActionController::Head include Doorkeeper::Rails::Helpers before_action :doorkeeper_authorize! def index self.response_body = { ok: true }.to_json end end ================================================ FILE: spec/dummy/app/controllers/semi_protected_resources_controller.rb ================================================ # frozen_string_literal: true class SemiProtectedResourcesController < ApplicationController before_action :doorkeeper_authorize!, only: :index def index render plain: "protected index" end def show render plain: "non protected show" end end ================================================ FILE: spec/dummy/app/helpers/application_helper.rb ================================================ # frozen_string_literal: true module ApplicationHelper def current_user @current_user ||= User.find_by(id: session[:user_id]) end end ================================================ FILE: spec/dummy/app/models/user.rb ================================================ # frozen_string_literal: true class ApplicationRecord < ::ActiveRecord::Base self.abstract_class = true end class User < ApplicationRecord def self.authenticate!(name, password) User.where(name: name, password: password).first end end ================================================ FILE: spec/dummy/app/views/home/index.html.erb ================================================ ================================================ FILE: spec/dummy/app/views/layouts/application.html.erb ================================================ Dummy <%= csrf_meta_tags %> <%= link_to "Sign in", '/sign_in' %> <%= yield %> ================================================ FILE: spec/dummy/config/application.rb ================================================ require File.expand_path("boot", __dir__) require "rails" %w[ action_controller/railtie action_view/railtie action_cable/engine sprockets/railtie ].each do |railtie| begin require railtie rescue LoadError => e puts "Error loading '#{railtie}' (#{e.message})" end end Bundler.require(*Rails.groups) require "yaml" orm = if DOORKEEPER_ORM =~ /mongoid/ Mongoid.load!(File.join(File.dirname(File.expand_path(__FILE__)), "#{DOORKEEPER_ORM}.yml")) :mongoid else DOORKEEPER_ORM end require "#{orm}/railtie" module Dummy class Application < Rails::Application if Rails.gem_version < Gem::Version.new("5.1") config.action_controller.per_form_csrf_tokens = true config.action_controller.forgery_protection_origin_check = true ActiveSupport.to_time_preserves_timezone = true if DOORKEEPER_ORM =~ /active_record/ config.active_record.belongs_to_required_by_default = true end config.ssl_options = { hsts: { subdomains: true } } else config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" end # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. end end ================================================ FILE: spec/dummy/config/boot.rb ================================================ require "rubygems" require "bundler/setup" orm = ENV["BUNDLE_GEMFILE"].match(/Gemfile\.(.+)\.rb/) DOORKEEPER_ORM = (orm && orm[1]) || :active_record unless defined?(DOORKEEPER_ORM) $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) ================================================ FILE: spec/dummy/config/database.yml ================================================ development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000 test: adapter: sqlite3 database: ":memory:" timeout: 500 production: adapter: sqlite3 database: ":memory:" timeout: 500 ================================================ FILE: spec/dummy/config/environment.rb ================================================ # Load the rails application require File.expand_path("application", __dir__) # Initialize the rails application Rails.application.initialize! ================================================ FILE: spec/dummy/config/environments/development.rb ================================================ # frozen_string_literal: true Dummy::Application.configure do # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false # Don't care if the mailer can't send # config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger config.active_support.deprecation = :log # Only use best-standards-support built into browsers config.action_dispatch.best_standards_support = :builtin # Do not compress assets config.assets.compress = false # Expands the lines which load the assets config.assets.debug = true config.eager_load = false end ================================================ FILE: spec/dummy/config/environments/production.rb ================================================ # frozen_string_literal: true Dummy::Application.configure do # Settings specified here will take precedence over those in config/application.rb # Code is not reloaded between requests config.cache_classes = true # Full error reports are disabled and caching is turned on config.consider_all_requests_local = false config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) config.serve_static_assets = false # Compress JavaScripts and CSS config.assets.compress = true # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = false # Generate digests for assets URLs config.assets.digest = true # Defaults to Rails.root.join("public/assets") # config.assets.manifest = YOUR_PATH # Specifies the header that your server uses for sending files # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # See everything in the log (default is :info) # config.log_level = :debug # Use a different logger for distributed setups # config.logger = SyslogLogger.new # Use a different cache store in production # config.cache_store = :mem_cache_store # Enable serving of images, stylesheets, and JavaScripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) # config.assets.precompile += %w( search.js ) # Disable delivery errors, bad email addresses will be ignored # config.action_mailer.raise_delivery_errors = false # Enable threaded mode # config.threadsafe! # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found) config.i18n.fallbacks = true # Send deprecation notices to registered listeners config.active_support.deprecation = :notify config.eager_load = true end ================================================ FILE: spec/dummy/config/environments/test.rb ================================================ # frozen_string_literal: true Dummy::Application.configure do # Settings specified here will take precedence over those in config/application.rb # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true config.assets.enabled = true config.assets.version = "1.0" config.assets.digest = false # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates # Rails 7.1 deprecated false in favor of :none, but we need to use false for # backwards compatibility: https://github.com/rails/rails/pull/45867 config.action_dispatch.show_exceptions = Gem::Version.new(Rails.version) >= Gem::Version.new('7.1.0') ? :none : false # Disable request forgery protection in test environment config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. # config.action_mailer.delivery_method = :test # Use SQL instead of Active Record's schema dumper when creating the test database. # This is necessary if your schema can't be completely dumped by the schema dumper, # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql # Print deprecation notices to the stderr config.active_support.deprecation = :stderr config.eager_load = true end ================================================ FILE: spec/dummy/config/initializers/backtrace_silencers.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: spec/dummy/config/initializers/doorkeeper.rb ================================================ # frozen_string_literal: true Doorkeeper.configure do # Change the ORM that doorkeeper will use. orm DOORKEEPER_ORM # This block will be called to check whether the resource owner is authenticated or not. resource_owner_authenticator do # Put your resource owner authentication logic here. User.where(id: session[:user_id]).first || redirect_to(root_url, alert: "Needs sign in.") end # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb # file then you need to declare this block in order to restrict access to the web interface for # adding oauth authorized applications. In other case it will return 403 Forbidden response # every time somebody will try to access the admin web interface. # # admin_authenticator do # # Put your admin authentication logic here. # # Example implementation: # Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url) # end # Authorization Code expiration time (default 10 minutes). # authorization_code_expires_in 10.minutes # Access token expiration time (default 2 hours). # If you want to disable expiration, set this to nil. # access_token_expires_in 2.hours # Reuse access token for the same resource owner within an application (disabled by default) # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 # reuse_access_token # Issue access tokens with refresh token (disabled by default) use_refresh_token # Forbids creating/updating applications with arbitrary scopes that are # not in configuration, i.e. `default_scopes` or `optional_scopes`. # (disabled by default) # # enforce_configured_scopes # Use the url path for the native authorization code flow. Enabling this flag sets the authorization # code response route for native redirect uris to oauth/authorize/. The default is oauth/authorize/native?code=. # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1143 # use_url_path_for_native_authorization # Provide support for an owner to be assigned to each registered application (disabled by default) # Optional parameter confirmation: true (default false) if you want to enforce ownership of # a registered application # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support # enable_application_owner confirmation: false # Define access token scopes for your provider # For more information go to # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes default_scopes :public optional_scopes :write, :update # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then # falls back to the `:client_id` and `:client_secret` params from the `params` object. # Check out the wiki for more information on customization # client_credentials :from_basic, :from_params # Change the way access token is authenticated from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then # falls back to the `:access_token` or `:bearer_token` params from the `params` object. # Check out the wiki for more information on customization # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled # by default in non-development environments). OAuth2 delegates security in # communication to the HTTPS protocol so it is wise to keep this enabled. # # force_ssl_in_redirect_uri !Rails.env.development? # Specify what grant flows are enabled in array of Strings. The valid # strings and the flows they enable are: # # "authorization_code" => Authorization Code Grant Flow # "implicit" => Implicit Grant Flow # "password" => Resource Owner Password Credentials Grant Flow # "client_credentials" => Client Credentials Grant Flow # # If not specified, Doorkeeper enables authorization_code and # client_credentials. # # implicit and password grant flows have risks that you should understand # before enabling: # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.2 # https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.3 # # grant_flows %w[authorization_code client_credentials] # Hook into the strategies' request & response life-cycle in case your # application needs advanced customization or logging: # # before_successful_strategy_response do |request| # puts "BEFORE HOOK FIRED! #{request}" # end # # after_successful_strategy_response do |request, response| # puts "AFTER HOOK FIRED! #{request}, #{response}" # end # Under some circumstances you might want to have applications auto-approved, # so that the user skips the authorization step. # For example if dealing with a trusted application. # skip_authorization do |resource_owner, client| # client.superapp? or resource_owner.admin? # end # Configure custom constraints for the Token Introspection request. # By default this configuration option allows to introspect a token by another # token of the same application, OR to introspect the token that belongs to # authorized client (from authenticated client) OR when token doesn't # belong to any client (public token). Otherwise requester has no access to the # introspection and it will return response as stated in the RFC. # # Block arguments: # # @param token [Doorkeeper::AccessToken] # token to be introspected # # @param authorized_client [Doorkeeper::Application] # authorized client (if request is authorized using Basic auth with # Client Credentials for example) # # @param authorized_token [Doorkeeper::AccessToken] # Bearer token used to authorize the request # # In case the block returns `nil` or `false` introspection responses with 401 status code # when using authorized token to introspect, or you'll get 200 with { "active": false } body # when using authorized client to introspect as stated in the # RFC 7662 section 2.2. Introspection Response. # # Using with caution: # Keep in mind that these three parameters pass to block can be nil as following case: # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. # `token` will be nil if and only if `authorized_token` is present. # So remember to use `&` or check if it is present before calling method on # them to make sure you doesn't get NoMethodError exception. # # You can define your custom check: # # allow_token_introspection do |token, authorized_client, authorized_token| # if authorized_token # # customize: require `introspection` scope # authorized_token.application == token&.application || # authorized_token.scopes.include?("introspection") # elsif token.application # # `protected_resource` is a new database boolean column, for example # authorized_client == token.application || authorized_client.protected_resource? # else # # public token (when token.application is nil, token doesn't belong to any application) # true # end # end # # Or you can completely disable any token introspection: # # allow_token_introspection false # # If you need to block the request at all, then configure your routes.rb or web-server # like nginx to forbid the request. # WWW-Authenticate Realm (default "Doorkeeper"). realm "Doorkeeper" end ================================================ FILE: spec/dummy/config/initializers/secret_token.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. Dummy::Application.config.secret_key_base = "c00157b5a1bb6181792f0f4a8a080485de7bab9987e6cf159" ================================================ FILE: spec/dummy/config/initializers/session_store.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. Dummy::Application.config.session_store :cookie_store, key: "_dummy_session" # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rails generate session_migration") # Dummy::Application.config.session_store :active_record_store ================================================ FILE: spec/dummy/config/initializers/wrap_parameters.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end # Disable root element in JSON by default. ActiveSupport.on_load(:active_record) do self.include_root_in_json = false end ================================================ FILE: spec/dummy/config/locales/doorkeeper.en.yml ================================================ en: doorkeeper: scopes: public: "Access your public data" write: "Update your data" ================================================ FILE: spec/dummy/config/routes.rb ================================================ Rails.application.routes.draw do use_doorkeeper resources :semi_protected_resources resources :full_protected_resources get "metal.json" => "metal#index" get "/callback", to: "home#callback" get "/sign_in", to: "home#sign_in" root to: "home#index" end ================================================ FILE: spec/dummy/config.ru ================================================ # frozen_string_literal: true # This file is used by Rack-based servers to start the application. require ::File.expand_path('config/environment', __dir__) run Dummy::Application ================================================ FILE: spec/dummy/db/migrate/20111122132257_create_users.rb ================================================ # frozen_string_literal: true class CreateUsers < ActiveRecord::Migration[4.2] def change create_table :users do |t| t.string :name t.timestamps end end end ================================================ FILE: spec/dummy/db/migrate/20120312140401_add_password_to_users.rb ================================================ # frozen_string_literal: true class AddPasswordToUsers < ActiveRecord::Migration[4.2] def change add_column :users, :password, :string end end ================================================ FILE: spec/dummy/db/migrate/20151223192035_create_doorkeeper_tables.rb ================================================ # frozen_string_literal: true class CreateDoorkeeperTables < ActiveRecord::Migration[4.2] def change create_table :oauth_applications do |t| t.string :name, null: false t.string :uid, null: false t.string :secret # Remove `null: false` if you are planning to use grant flows # that doesn't require redirect URI to be used during authorization # like Client Credentials flow or Resource Owner Password. t.text :redirect_uri, null: false t.string :scopes, null: false, default: "" t.timestamps null: false end add_index :oauth_applications, :uid, unique: true create_table :oauth_access_grants do |t| t.references :resource_owner, null: false, polymorphic: true t.references :application, null: false t.string :token, null: false t.integer :expires_in, null: false t.text :redirect_uri, null: false t.datetime :created_at, null: false t.datetime :revoked_at t.string :scopes, null: false, default: "" end add_index :oauth_access_grants, :token, unique: true add_foreign_key( :oauth_access_grants, :oauth_applications, column: :application_id, ) create_table :oauth_access_tokens do |t| t.references :resource_owner, index: true, polymorphic: true t.references :application, null: false # If you use a custom token generator you may need to change this column # from string to text, so that it accepts tokens larger than 255 # characters. More info on custom token generators in: # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator # # t.text :token, null: false t.string :token, null: false t.string :refresh_token t.integer :expires_in t.datetime :revoked_at t.datetime :created_at, null: false t.string :scopes end add_index :oauth_access_tokens, :token, unique: true add_index :oauth_access_tokens, :refresh_token, unique: true add_foreign_key( :oauth_access_tokens, :oauth_applications, column: :application_id, ) # Uncomment below to ensure a valid reference to the resource owner's table add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id end end ================================================ FILE: spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb ================================================ # frozen_string_literal: true class AddOwnerToApplication < ActiveRecord::Migration[4.2] def change add_column :oauth_applications, :owner_id, :integer, null: true add_column :oauth_applications, :owner_type, :string, null: true add_index :oauth_applications, %i[owner_id owner_type] end end ================================================ FILE: spec/dummy/db/migrate/20160320211015_add_previous_refresh_token_to_access_tokens.rb ================================================ # frozen_string_literal: true class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration[4.2] def change add_column( :oauth_access_tokens, :previous_refresh_token, :string, default: "", null: false, ) end end ================================================ FILE: spec/dummy/db/migrate/20170822064514_enable_pkce.rb ================================================ # frozen_string_literal: true class EnablePkce < ActiveRecord::Migration[4.2] def change add_column :oauth_access_grants, :code_challenge, :string, null: true add_column :oauth_access_grants, :code_challenge_method, :string, null: true end end ================================================ FILE: spec/dummy/db/migrate/20180210183654_add_confidential_to_applications.rb ================================================ # frozen_string_literal: true class AddConfidentialToApplications < ActiveRecord::Migration[5.1] def change add_column( :oauth_applications, :confidential, :boolean, null: false, default: true, # maintaining backwards compatibility: require secrets ) end end ================================================ FILE: spec/dummy/db/migrate/20230205064514_add_custom_attributes.rb ================================================ # frozen_string_literal: true class AddCustomAttributes < ActiveRecord::Migration[4.2] def change add_column :oauth_access_grants, :tenant_name, :string add_column :oauth_access_tokens, :tenant_name, :string end end ================================================ FILE: spec/dummy/db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # Note that this schema.rb definition is the authoritative source for your # database schema. If you need to create the application database on another # system, you should be using db:schema:load, not running all the migrations # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 20230205064514) do create_table "oauth_access_grants", force: :cascade do |t| t.integer "resource_owner_id", null: false t.string "resource_owner_type" # [NOTE] null: false skipped to allow test pass t.integer "application_id", null: false t.string "token", null: false t.integer "expires_in", null: false t.text "redirect_uri", null: false t.datetime "created_at", null: false t.datetime "revoked_at" t.string "scopes" t.string "tenant_name" unless ENV["WITHOUT_PKCE"] t.string "code_challenge" t.string "code_challenge_method" end t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true end create_table "oauth_access_tokens", force: :cascade do |t| t.integer "resource_owner_id" t.string "resource_owner_type" t.integer "application_id" t.string "token", null: false t.string "refresh_token" t.integer "expires_in" t.datetime "revoked_at" t.datetime "created_at", null: false t.string "scopes" t.string "previous_refresh_token", default: "", null: false t.string "tenant_name" t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true end create_table "oauth_applications", force: :cascade do |t| t.string "name", null: false t.string "uid", null: false t.string "secret" t.text "redirect_uri" t.string "scopes", default: "", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "owner_id" t.string "owner_type" t.boolean "confidential", default: true, null: false t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end create_table "users", force: :cascade do |t| t.string "name" t.datetime "created_at" t.datetime "updated_at" t.string "password" end end ================================================ FILE: spec/dummy/public/404.html ================================================ The page you were looking for doesn't exist (404)

The page you were looking for doesn't exist.

You may have mistyped the address or the page may have moved.

================================================ FILE: spec/dummy/public/422.html ================================================ The change you wanted was rejected (422)

The change you wanted was rejected.

Maybe you tried to change something you didn't have access to.

================================================ FILE: spec/dummy/public/500.html ================================================ We're sorry, but something went wrong (500)

We're sorry, but something went wrong.

We've been notified about this issue and we'll take a look at it shortly.

================================================ FILE: spec/dummy/script/rails ================================================ #!/usr/bin/env ruby # frozen_string_literal: true # This command will automatically be run when you run "rails" with Rails 3 gems # installed from the root of your application. APP_PATH = File.expand_path("../config/application", __dir__) require File.expand_path("../config/boot", __dir__) require "rails/commands" ================================================ FILE: spec/factories.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :access_grant, class: "Doorkeeper::AccessGrant" do sequence(:resource_owner_id) { |n| n } application redirect_uri { "https://app.com/callback" } expires_in { 100 } scopes { "public write" } end factory :access_token, class: "Doorkeeper::AccessToken" do sequence(:resource_owner_id) { |n| n } application expires_in { 2.hours } factory :clientless_access_token do application { nil } end end factory :application, class: "Doorkeeper::Application" do sequence(:name) { |n| "Application #{n}" } redirect_uri { "https://app.com/callback" } end # do not name this factory :user, otherwise it will conflict with factories # from applications that use doorkeeper factories in their own tests factory :doorkeeper_testing_user, class: :user, aliases: [:resource_owner] end ================================================ FILE: spec/generators/application_owner_generator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "generators/doorkeeper/application_owner_generator" RSpec.describe Doorkeeper::ApplicationOwnerGenerator do include GeneratorSpec::TestCase tests described_class destination ::File.expand_path('tmp/dummy', __dir__) describe "after running the generator" do before do prepare_destination end it "creates a migration with a version specifier" do stub_const("ActiveRecord::VERSION::MAJOR", 5) stub_const("ActiveRecord::VERSION::MINOR", 0) run_generator assert_migration "db/migrate/add_owner_to_application.rb" do |migration| assert migration.include?("ActiveRecord::Migration[5.0]\n") end end end end ================================================ FILE: spec/generators/confidential_applications_generator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "generators/doorkeeper/confidential_applications_generator" RSpec.describe Doorkeeper::ConfidentialApplicationsGenerator do include GeneratorSpec::TestCase tests described_class destination ::File.expand_path('tmp/dummy', __dir__) describe "after running the generator" do before do prepare_destination end it "creates a migration with a version specifier" do stub_const("ActiveRecord::VERSION::MAJOR", 5) stub_const("ActiveRecord::VERSION::MINOR", 0) run_generator assert_migration "db/migrate/add_confidential_to_applications.rb" do |migration| assert migration.include?("ActiveRecord::Migration[5.0]\n") assert migration.include?(":confidential") end end end end ================================================ FILE: spec/generators/enable_polymorphic_resource_owner_generator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "generators/doorkeeper/enable_polymorphic_resource_owner_generator" RSpec.describe Doorkeeper::EnablePolymorphicResourceOwnerGenerator do include GeneratorSpec::TestCase tests described_class destination ::File.expand_path('tmp/dummy', __dir__) describe "after running the generator" do before do prepare_destination FileUtils.mkdir_p(::File.expand_path("config/initializers", Pathname(destination_root))) FileUtils.copy_file( ::File.expand_path("../../lib/generators/doorkeeper/templates/initializer.rb", __dir__), ::File.expand_path("config/initializers/doorkeeper.rb", Pathname.new(destination_root)), ) end it "creates a migration with a version specifier and changes the initializer" do stub_const("ActiveRecord::VERSION::MAJOR", 5) stub_const("ActiveRecord::VERSION::MINOR", 0) run_generator assert_migration "db/migrate/enable_polymorphic_resource_owner.rb" do |migration| assert migration.include?("ActiveRecord::Migration[5.0]\n") end # generator_spec gem requires such block definition :( # # rubocop:disable Style/BlockDelimiters expect(destination_root).to(have_structure { directory "config" do directory "initializers" do file "doorkeeper.rb" do contains " use_polymorphic_resource_owner" end end end }) # rubocop:enable Style/BlockDelimiters end end end ================================================ FILE: spec/generators/install_generator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "generators/doorkeeper/install_generator" RSpec.describe Doorkeeper::InstallGenerator do include GeneratorSpec::TestCase tests described_class destination ::File.expand_path('tmp/dummy', __dir__) describe "after running the generator" do before do prepare_destination FileUtils.mkdir(::File.expand_path("config", Pathname(destination_root))) FileUtils.mkdir(::File.expand_path("db", Pathname(destination_root))) FileUtils.copy_file( ::File.expand_path('templates/routes.rb', __dir__), ::File.expand_path("config/routes.rb", Pathname.new(destination_root)), ) run_generator end it "creates an initializer file" do assert_file "config/initializers/doorkeeper.rb" end it "copies the locale file" do assert_file "config/locales/doorkeeper.en.yml" end it "adds sample route" do assert_file "config/routes.rb", /use_doorkeeper/ end end end ================================================ FILE: spec/generators/migration_generator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "generators/doorkeeper/migration_generator" RSpec.describe Doorkeeper::MigrationGenerator do include GeneratorSpec::TestCase tests described_class destination ::File.expand_path('tmp/dummy', __dir__) describe "after running the generator" do before do prepare_destination end it "creates a migration with a version specifier" do stub_const("ActiveRecord::VERSION::MAJOR", 5) stub_const("ActiveRecord::VERSION::MINOR", 0) run_generator assert_migration "db/migrate/create_doorkeeper_tables.rb" do |migration| assert migration.include?("ActiveRecord::Migration[5.0]\n") end end end end ================================================ FILE: spec/generators/pkce_generator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "generators/doorkeeper/pkce_generator" RSpec.describe Doorkeeper::PkceGenerator do include GeneratorSpec::TestCase tests described_class destination ::File.expand_path('tmp/dummy', __dir__) describe "after running the generator" do before do prepare_destination end it "creates a migration with a version specifier" do stub_const("ActiveRecord::VERSION::MAJOR", 5) stub_const("ActiveRecord::VERSION::MINOR", 0) run_generator assert_migration "db/migrate/enable_pkce.rb" do |migration| assert migration.include?("ActiveRecord::Migration[5.0]\n") end end end end ================================================ FILE: spec/generators/previous_refresh_token_generator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "generators/doorkeeper/previous_refresh_token_generator" RSpec.describe Doorkeeper::PreviousRefreshTokenGenerator do include GeneratorSpec::TestCase tests described_class destination ::File.expand_path('tmp/dummy', __dir__) describe "after running the generator" do before do prepare_destination allow_any_instance_of(described_class).to( receive(:no_previous_refresh_token_column?).and_return(true), ) end it "creates a migration with a version specifier" do stub_const("ActiveRecord::VERSION::MAJOR", 5) stub_const("ActiveRecord::VERSION::MINOR", 0) run_generator assert_migration "db/migrate/add_previous_refresh_token_to_access_tokens.rb" do |migration| assert migration.include?("ActiveRecord::Migration[5.0]\n") end end context "when file already exist" do it "does not create a migration" do allow_any_instance_of(described_class).to( receive(:no_previous_refresh_token_column?).and_call_original, ) run_generator assert_no_migration "db/migrate/add_previous_refresh_token_to_access_tokens.rb" end end end end ================================================ FILE: spec/generators/remove_applications_secret_not_null_constraint_generator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "generators/doorkeeper/remove_applications_secret_not_null_constraint_generator" RSpec.describe Doorkeeper::RemoveApplicationsSecretNotNullConstraintGenerator do include GeneratorSpec::TestCase tests described_class destination ::File.expand_path('tmp/dummy', __dir__) describe "after running the generator" do before do prepare_destination end it "creates a migration with a version specifier" do run_generator assert_migration "db/migrate/remove_applications_secret_not_null_constraint.rb" do |migration| assert migration.include?("change_column_null :oauth_applications, :secret") end end end end ================================================ FILE: spec/generators/templates/routes.rb ================================================ # frozen_string_literal: true Rails.application.routes.draw do end ================================================ FILE: spec/generators/views_generator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "generators/doorkeeper/views_generator" RSpec.describe Doorkeeper::Generators::ViewsGenerator do include GeneratorSpec::TestCase tests described_class destination File.expand_path("tmp/dummy", __dir__) before do prepare_destination end it "create all views" do run_generator assert_file "app/views/doorkeeper/applications/_form.html.erb" assert_file "app/views/doorkeeper/applications/edit.html.erb" assert_file "app/views/doorkeeper/applications/index.html.erb" assert_file "app/views/doorkeeper/applications/new.html.erb" assert_file "app/views/doorkeeper/applications/show.html.erb" assert_file "app/views/doorkeeper/authorizations/error.html.erb" assert_file "app/views/doorkeeper/authorizations/new.html.erb" assert_file "app/views/doorkeeper/authorized_applications/index.html.erb" end end ================================================ FILE: spec/grape/grape_integration_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "grape" require "rack/test" require "doorkeeper/grape/helpers" # Test Grape API application module GrapeApp class API < Grape::API version "v1", using: :path format :json prefix :api helpers Doorkeeper::Grape::Helpers resource :protected do before do doorkeeper_authorize! end desc "Protected resource, requires token." get :status do { token: doorkeeper_token.token } end end resource :protected_with_endpoint_scopes do before do doorkeeper_authorize! end desc "Protected resource, requires token with scopes (defined in endpoint)." get :status, scopes: [:admin] do { response: "OK" } end end resource :protected_with_helper_scopes do before do doorkeeper_authorize! :admin end desc "Protected resource, requires token with scopes (defined in helper)." get :status do { response: "OK" } end end resource :public do desc "Public resource, no token required." get :status do { response: "OK" } end end end end RSpec.describe "Grape integration" do include Rack::Test::Methods def app GrapeApp::API end def json_body JSON.parse(last_response.body) end let(:client) { FactoryBot.create(:application) } let(:resource) { FactoryBot.create(:doorkeeper_testing_user, name: "Joe", password: "sekret") } let(:access_token) { client_is_authorized(client, resource) } context "with valid Access Token" do it "successfully requests protected resource" do get "api/v1/protected/status.json?access_token=#{access_token.token}" expect(last_response).to be_successful expect(json_body["token"]).to eq(access_token.token) end it "successfully requests protected resource with token that has required scopes (Grape endpoint)" do access_token = client_is_authorized(client, resource, scopes: "admin") get "api/v1/protected_with_endpoint_scopes/status.json?access_token=#{access_token.token}" expect(last_response).to be_successful expect(json_body).to have_key("response") end it "successfully requests protected resource with token that has required scopes (Doorkeeper helper)" do access_token = client_is_authorized(client, resource, scopes: "admin") get "api/v1/protected_with_helper_scopes/status.json?access_token=#{access_token.token}" expect(last_response).to be_successful expect(json_body).to have_key("response") end it "successfully requests public resource" do get "api/v1/public/status.json" expect(last_response).to be_successful expect(json_body).to have_key("response") end end context "with invalid Access Token" do it "fails without access token" do get "api/v1/protected/status.json" expect(last_response).not_to be_successful expect(json_body).to have_key("error") end it "fails for access token without scopes" do get "api/v1/protected_with_endpoint_scopes/status.json?access_token=#{access_token.token}" expect(last_response).not_to be_successful expect(json_body).to have_key("error") end it "fails for access token with invalid scopes" do access_token = client_is_authorized(client, resource, scopes: "read write") get "api/v1/protected_with_endpoint_scopes/status.json?access_token=#{access_token.token}" expect(last_response).not_to be_successful expect(json_body).to have_key("error") end end end ================================================ FILE: spec/helpers/doorkeeper/dashboard_helper_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::DashboardHelper do describe "#doorkeeper_errors_for" do let(:object) { double errors: { method: messages } } let(:messages) { ["first message", "second message"] } context "when object has errors" do it "returns error messages" do messages.each do |message| expect(helper.doorkeeper_errors_for(object, :method)).to include( message.capitalize, ) end end end context "when object has no errors" do it "returns nil" do expect(helper.doorkeeper_errors_for(object, :amonter_method)).to be_nil end end end end ================================================ FILE: spec/lib/config_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Config do subject(:config) { Doorkeeper.config } describe "resource_owner_authenticator" do it "sets the block that is accessible via authenticate_resource_owner" do block = proc {} Doorkeeper.configure do orm DOORKEEPER_ORM resource_owner_authenticator(&block) end expect(config.authenticate_resource_owner).to eq(block) end it "prints warning message by default" do Doorkeeper.configure do orm DOORKEEPER_ORM end expect(Rails.logger).to receive(:warn).with( I18n.t("doorkeeper.errors.messages.resource_owner_authenticator_not_configured"), ) config.authenticate_resource_owner.call(nil) end end describe "resource_owner_from_credentials" do it "sets the block that is accessible via authenticate_resource_owner" do block = proc {} Doorkeeper.configure do orm DOORKEEPER_ORM resource_owner_from_credentials(&block) end expect(config.resource_owner_from_credentials).to eq(block) end it "prints warning message by default" do Doorkeeper.configure do orm DOORKEEPER_ORM end expect(Rails.logger).to receive(:warn).with( I18n.t("doorkeeper.errors.messages.credential_flow_not_configured"), ) config.resource_owner_from_credentials.call(nil) end end describe "setup_orm" do it "adds specific error message to NameError exception" do expect do Doorkeeper.configure { orm "hibernate" } Doorkeeper.setup end.to raise_error(NameError, /ORM adapter not found \(hibernate\)/) end it "does not change other exceptions" do allow(Doorkeeper).to receive(:setup_orm_adapter) { raise NoMethodError } expect do Doorkeeper.configure { orm "hibernate" } Doorkeeper.setup end.to raise_error(NoMethodError) end end describe "admin_authenticator" do it "sets the block that is accessible via authenticate_admin" do default_behaviour = "default behaviour" allow(described_class).to receive(:head).and_return(default_behaviour) Doorkeeper.configure do orm DOORKEEPER_ORM end expect(config.authenticate_admin.call({})).to eq(default_behaviour) end it "could be customized with a block" do block = proc {} Doorkeeper.configure do orm DOORKEEPER_ORM admin_authenticator(&block) end expect(config.authenticate_admin).to eq(block) end end describe "access_token_expires_in" do it "has 2 hours by default" do expect(config.access_token_expires_in).to eq(2.hours) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM access_token_expires_in 4.hours end expect(config.access_token_expires_in).to eq(4.hours) end it "can be set to nil" do Doorkeeper.configure do orm DOORKEEPER_ORM access_token_expires_in nil end expect(config.access_token_expires_in).to be_nil end end describe "scopes" do it "has default scopes" do Doorkeeper.configure do orm DOORKEEPER_ORM default_scopes :public end expect(config.default_scopes).to include("public") end it "has optional scopes" do Doorkeeper.configure do orm DOORKEEPER_ORM optional_scopes :write, :update end expect(config.optional_scopes).to include("write", "update") end it "has all scopes" do Doorkeeper.configure do orm DOORKEEPER_ORM default_scopes :normal optional_scopes :admin end expect(config.scopes).to include("normal", "admin") end end describe "scopes_by_grant_type" do it "is {} by default" do expect(config.scopes_by_grant_type).to eq({}) end it "has hash value" do hash = {} Doorkeeper.configure do orm DOORKEEPER_ORM scopes_by_grant_type hash end expect(config.scopes_by_grant_type).to eq(hash) end end describe "use_refresh_token" do it "is false by default" do expect(config.refresh_token_enabled?).to eq(false) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM use_refresh_token end expect(config.refresh_token_enabled?).to eq(true) end it "can accept a boolean parameter" do Doorkeeper.configure do orm DOORKEEPER_ORM use_refresh_token false end expect(config.refresh_token_enabled?).to eq(false) end it "can accept a block parameter" do Doorkeeper.configure do orm DOORKEEPER_ORM use_refresh_token { |_context| nil } end expect(config.refresh_token_enabled?).to be_a(Proc) end it "does not includes 'refresh_token' in token_grant_flows" do expect(config.token_grant_flows).not_to include Doorkeeper::GrantFlow.get("refresh_token") end context "when enabled" do before do Doorkeeper.configure do orm DOORKEEPER_ORM use_refresh_token end end it "includes 'refresh_token' in token_grant_flows" do expect(config.token_grant_flows).to include Doorkeeper::GrantFlow.get("refresh_token") end end end describe "token_reuse_limit" do it "is 100 by default" do expect(config.token_reuse_limit).to eq(100) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM token_reuse_limit 90 end expect(config.token_reuse_limit).to eq(90) end it "sets the value to 100 if invalid value is being set" do expect(Rails.logger).to receive(:warn).with(/will be set to default 100/) Doorkeeper.configure do orm DOORKEEPER_ORM reuse_access_token token_reuse_limit 110 end expect(config.token_reuse_limit).to eq(100) end end describe "enforce_configured_scopes" do it "is false by default" do expect(config.enforce_configured_scopes?).to eq(false) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM enforce_configured_scopes end expect(config.enforce_configured_scopes?).to eq(true) end end describe 'use_url_path_for_native_authorization' do around(:each) do |example| Doorkeeper.configure do orm DOORKEEPER_ORM use_url_path_for_native_authorization end Rails.application.reload_routes! subject { Doorkeeper.configuration } example.run Doorkeeper.configure do orm DOORKEEPER_ORM end Rails.application.reload_routes! end it 'sets the native authorization code route /:code' do expect(subject.native_authorization_code_route).to eq('/:code') end end describe "client_credentials" do it "has defaults order" do expect(config.client_credentials_methods) .to eq(%i[from_basic from_params]) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM client_credentials :from_digest, :from_params end expect(config.client_credentials_methods) .to eq(%i[from_digest from_params]) end end describe "force_ssl_in_redirect_uri" do it "is true by default in non-development environments" do expect(config.force_ssl_in_redirect_uri).to eq(true) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM force_ssl_in_redirect_uri(false) end expect(config.force_ssl_in_redirect_uri).to eq(false) end it "can be a callable object" do block = proc { false } Doorkeeper.configure do orm DOORKEEPER_ORM force_ssl_in_redirect_uri(&block) end expect(config.force_ssl_in_redirect_uri).to eq(block) expect(config.force_ssl_in_redirect_uri.call).to eq(false) end end describe "access_token_methods" do it "has defaults order" do expect(config.access_token_methods) .to eq(%i[from_bearer_authorization from_access_token_param from_bearer_param]) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM access_token_methods :from_access_token_param, :from_bearer_param end expect(config.access_token_methods) .to eq(%i[from_access_token_param from_bearer_param]) end end describe "forbid_redirect_uri" do it "is false by default" do expect(config.forbid_redirect_uri.call(URI.parse("https://localhost"))).to eq(false) end it "can be a callable object" do block = proc { true } Doorkeeper.configure do orm DOORKEEPER_ORM forbid_redirect_uri(&block) end expect(config.forbid_redirect_uri).to eq(block) expect(config.forbid_redirect_uri.call).to eq(true) end end describe "enable_dynamic_scopes" do it "is disabled by default" do expect(Doorkeeper.config.enable_dynamic_scopes?).not_to be(true) end context "when enabled with default delimiter" do before do Doorkeeper.configure do enable_dynamic_scopes end end it 'returns true' do expect(Doorkeeper.config.enable_dynamic_scopes?).to be(true) expect(Doorkeeper.config.dynamic_scopes_delimiter).to eq(":") end end context "when enabled with custom delimiter" do before do Doorkeeper.configure do enable_dynamic_scopes(delimiter: "-") end end it 'returns true' do expect(Doorkeeper.config.enable_dynamic_scopes?).to be(true) expect(Doorkeeper.config.dynamic_scopes_delimiter).to eq("-") end end end describe "enable_application_owner" do it "is disabled by default" do expect(Doorkeeper.config.enable_application_owner?).not_to be(true) end if DOORKEEPER_ORM == :active_record context "when enabled without confirmation", active_record: true do class ApplicationWithOwner < ActiveRecord::Base include Doorkeeper::Orm::ActiveRecord::Mixins::Application end before do Doorkeeper.configure do orm DOORKEEPER_ORM enable_application_owner application_class "ApplicationWithOwner" end Doorkeeper.run_orm_hooks end it "adds support for application owner" do instance = ApplicationWithOwner.new(FactoryBot.attributes_for(:application)) expect(instance).to respond_to :owner expect(instance).to be_valid end it "Doorkeeper.configuration.confirm_application_owner? returns false" do expect(Doorkeeper.config.confirm_application_owner?).not_to be(true) end end end if DOORKEEPER_ORM == :active_record context "when enabled with confirmation set to true", active_record: true do class ApplicationWithOwner < ActiveRecord::Base include Doorkeeper::Orm::ActiveRecord::Mixins::Application end before do Doorkeeper.configure do orm DOORKEEPER_ORM enable_application_owner confirmation: true application_class "ApplicationWithOwner" end Doorkeeper.run_orm_hooks end it "adds support for application owner" do instance = ApplicationWithOwner.new(FactoryBot.attributes_for(:application)) expect(instance).to respond_to :owner expect(instance).not_to be_valid expect(instance.errors[:owner]).to be_present end it "Doorkeeper.configuration.confirm_application_owner? returns true" do expect(Doorkeeper.config.confirm_application_owner?).to be(true) end end end end describe "realm" do it "is 'Doorkeeper' by default" do expect(Doorkeeper.config.realm).to eq("Doorkeeper") end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM realm "Example" end expect(config.realm).to eq("Example") end end describe "grant_flows" do it "is set to all grant flows by default" do expect(Doorkeeper.config.grant_flows) .to eq(%w[authorization_code client_credentials]) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM grant_flows %w[authorization_code implicit] end expect(config.grant_flows).to eq %w[authorization_code implicit] end context "when including 'authorization_code'" do before do Doorkeeper.configure do orm DOORKEEPER_ORM grant_flows ["authorization_code"] end end it "includes 'authorization_code' in authorization_response_flows" do expect(config.authorization_response_flows).to include Doorkeeper::GrantFlow.get("authorization_code") end it "includes 'authorization_code' in token_grant_flows" do expect(config.token_grant_flows).to include Doorkeeper::GrantFlow.get("authorization_code") end end context "when including 'implicit'" do before do Doorkeeper.configure do orm DOORKEEPER_ORM grant_flows ["implicit"] end end it "includes 'implicit' in authorization_response_flows" do expect(config.authorization_response_flows).to include Doorkeeper::GrantFlow.get("implicit") end end context "when including 'password'" do before do Doorkeeper.configure do orm DOORKEEPER_ORM grant_flows ["password"] end end it "includes 'password' in token_grant_flows" do expect(config.token_grant_flows).to include Doorkeeper::GrantFlow.get("password") end end context "when including 'client_credentials'" do before do Doorkeeper.configure do orm DOORKEEPER_ORM grant_flows ["client_credentials"] end end it "includes 'client_credentials' in token_grant_flows" do expect(config.token_grant_flows).to include Doorkeeper::GrantFlow.get("client_credentials") end end end describe "access_token_generator" do it "is 'Doorkeeper::OAuth::Helpers::UniqueToken' by default" do expect(Doorkeeper.configuration.access_token_generator).to( eq("Doorkeeper::OAuth::Helpers::UniqueToken"), ) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Example" end expect(config.access_token_generator).to eq("Example") end end describe "custom_access_token_attributes" do it "is '[]' by default" do expect(Doorkeeper.configuration.custom_access_token_attributes).to(eq([])) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM custom_access_token_attributes [:tenant_name] end expect(config.custom_access_token_attributes).to eq([:tenant_name]) end end describe "application_secret_generator" do it "is 'Doorkeeper::OAuth::Helpers::UniqueToken' by default" do expect(Doorkeeper.configuration.application_secret_generator).to( eq("Doorkeeper::OAuth::Helpers::UniqueToken"), ) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM application_secret_generator "Example" end expect(config.application_secret_generator).to eq("Example") end end describe "default_generator_method" do it "is :urlsafe_base64 by default" do expect(Doorkeeper.configuration.default_generator_method) .to eq(:urlsafe_base64) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM default_generator_method :hex end expect(config.default_generator_method).to eq(:hex) end end describe "base_controller" do context "when default value set" do it { expect(Doorkeeper.configuration.base_controller).to be_an_instance_of(Proc) } it "resolves to a ApplicationController::Base in default mode" do expect(Doorkeeper.configuration.resolve_controller(:base)) .to eq(ActionController::Base) end it "resolves to a ApplicationController::API in api_only mode" do Doorkeeper.configure do orm DOORKEEPER_ORM api_only end expect(Doorkeeper.configuration.resolve_controller(:base)) .to eq(ActionController::API) end end context "when custom value set" do before do Doorkeeper.configure do orm DOORKEEPER_ORM base_controller "ApplicationController" end end it { expect(Doorkeeper.config.base_controller).to eq("ApplicationController") } end end describe "base_metal_controller" do context "when default value set" do it { expect(Doorkeeper.config.base_metal_controller).to eq("ActionController::API") } end context "when custom value set" do before do Doorkeeper.configure do orm DOORKEEPER_ORM base_metal_controller { "ApplicationController" } end end it { expect(Doorkeeper.configuration.resolve_controller(:base_metal)).to eq(ApplicationController) } end end if DOORKEEPER_ORM == :active_record class FakeCustomModel < ::ActiveRecord::Base end describe "access_token_class" do it "uses default doorkeeper value" do expect(config.access_token_class).to eq("Doorkeeper::AccessToken") expect(config.access_token_model).to be(Doorkeeper::AccessToken) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM access_token_class "FakeCustomModel" end expect(config.access_token_class).to eq("FakeCustomModel") expect(config.access_token_model).to be(FakeCustomModel) end end describe "access_grant_class" do it "uses default doorkeeper value" do expect(config.access_grant_class).to eq("Doorkeeper::AccessGrant") expect(config.access_grant_model).to be(Doorkeeper::AccessGrant) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM access_grant_class "FakeCustomModel" end expect(config.access_grant_class).to eq("FakeCustomModel") expect(config.access_grant_model).to be(FakeCustomModel) end end describe "application_class" do it "uses default doorkeeper value" do expect(config.application_class).to eq("Doorkeeper::Application") expect(config.application_model).to be(Doorkeeper::Application) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM application_class "FakeCustomModel" end expect(config.application_class).to eq("FakeCustomModel") expect(config.application_model).to be(FakeCustomModel) end end end describe "api_only" do it "is false by default" do expect(config.api_only).to eq(false) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM api_only end expect(config.api_only).to eq(true) end end describe "token_lookup_batch_size" do it "uses default doorkeeper value" do expect(config.token_lookup_batch_size).to eq(10_000) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM token_lookup_batch_size 100_000 end expect(config.token_lookup_batch_size).to eq(100_000) end end describe "strict_content_type" do it "is false by default" do expect(config.enforce_content_type).to eq(false) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM enforce_content_type end expect(config.enforce_content_type).to eq(true) end end describe "handle_auth_errors" do it "is set to render by default" do expect(Doorkeeper.config.handle_auth_errors).to eq(:render) end it "can change the value" do Doorkeeper.configure do orm DOORKEEPER_ORM handle_auth_errors :raise end expect(config.handle_auth_errors).to eq(:raise) end end describe "token_secret_strategy" do it "is plain by default" do expect(config.token_secret_strategy).to eq(Doorkeeper::SecretStoring::Plain) expect(config.token_secret_fallback_strategy).to eq(nil) end context "when provided" do before do Doorkeeper.configure do orm DOORKEEPER_ORM hash_token_secrets end end it "will enable hashing for applications" do expect(config.token_secret_strategy).to eq(Doorkeeper::SecretStoring::Sha256Hash) expect(config.token_secret_fallback_strategy).to eq(nil) end end context "when manually provided with invalid constant" do it "raises an exception" do expect do Doorkeeper.configure do orm DOORKEEPER_ORM hash_token_secrets using: "does not exist" end end.to raise_error(NameError) end end context "when manually provided with invalid option" do it "raises an exception" do expect do Doorkeeper.configure do orm DOORKEEPER_ORM hash_token_secrets using: "Doorkeeper::SecretStoring::BCrypt" end end.to raise_error( ArgumentError, /can only be used for storing application secrets/, ) end end context "when provided with fallback" do before do Doorkeeper.configure do orm DOORKEEPER_ORM hash_token_secrets fallback: :plain end end it "will enable hashing for applications" do expect(config.token_secret_strategy).to eq(Doorkeeper::SecretStoring::Sha256Hash) expect(config.token_secret_fallback_strategy).to eq(Doorkeeper::SecretStoring::Plain) end end describe "hash_token_secrets together with reuse_access_token" do it "will disable reuse_access_token" do expect(Rails.logger).to receive(:warn).with(/reuse_access_token will be disabled/) Doorkeeper.configure do orm DOORKEEPER_ORM reuse_access_token hash_token_secrets end expect(config.reuse_access_token).to eq(false) end end end describe "application_secret_strategy" do it "is plain by default" do expect(config.application_secret_strategy).to eq(Doorkeeper::SecretStoring::Plain) expect(config.application_secret_fallback_strategy).to eq(nil) end context "when provided" do before do Doorkeeper.configure do orm DOORKEEPER_ORM hash_application_secrets end end it "will enable hashing for applications" do expect(config.application_secret_strategy).to eq(Doorkeeper::SecretStoring::Sha256Hash) expect(config.application_secret_fallback_strategy).to eq(nil) end end context "when manually provided with invalid constant" do it "raises an exception" do expect do Doorkeeper.configure do orm DOORKEEPER_ORM hash_application_secrets using: "does not exist" end end.to raise_error(NameError) end end context "when provided with fallback" do before do Doorkeeper.configure do orm DOORKEEPER_ORM hash_application_secrets fallback: :plain end end it "will enable hashing for applications" do expect(config.application_secret_strategy).to eq(Doorkeeper::SecretStoring::Sha256Hash) expect(config.application_secret_fallback_strategy).to eq(Doorkeeper::SecretStoring::Plain) end end end describe "options deprecation" do it "prints a warning message when an option is deprecated" do expect(Kernel).to receive(:warn).with( /\[DOORKEEPER\] native_redirect_uri has been deprecated and will soon be removed/, ) Doorkeeper.configure do orm DOORKEEPER_ORM native_redirect_uri "urn:ietf:wg:oauth:2.0:oob" end end end end ================================================ FILE: spec/lib/doorkeeper/orm/active_record_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" if DOORKEEPER_ORM == :active_record RSpec.describe Doorkeeper::Orm::ActiveRecord do describe ".initialize_configured_associations" do it "uses ActiveSupport.on_load(:active_record) to defer model loading" do expect(ActiveSupport).to receive(:on_load).with(:active_record) described_class.initialize_configured_associations end end # Reproduction test for https://github.com/doorkeeper-gem/doorkeeper/issues/1703 # # Without the on_load wrapper, calling initialize_configured_associations # eagerly triggers constantize on model class names (via access_token_model, # access_grant_model, etc.), which forces ActiveRecord to load before # config.active_record.* settings have been applied. This test verifies # that the on_load block does NOT execute eagerly at registration time. # # See also: https://github.com/ngan/doorkeeper-activerecord-load-issue describe "deferral of model loading (issue #1703)" do it "does not call config model accessors at registration time" do # Stub on_load to capture the block WITHOUT executing it, # simulating the state before ActiveRecord is fully initialized. allow(ActiveSupport).to receive(:on_load).with(:active_record) expect(Doorkeeper.config).not_to receive(:application_model) expect(Doorkeeper.config).not_to receive(:access_token_model) expect(Doorkeeper.config).not_to receive(:access_grant_model) described_class.initialize_configured_associations end it "calls config model accessors only when the on_load hook fires" do deferred_block = nil allow(ActiveSupport).to receive(:on_load).with(:active_record) do |*, &block| deferred_block = block end described_class.initialize_configured_associations # Block was captured but not yet executed expect(deferred_block).not_to be_nil # Now simulate ActiveRecord finishing initialization by executing the block expect(Doorkeeper.config).to receive(:enable_application_owner?).and_return(false) expect(Doorkeeper.config).to receive(:access_grant_model).and_return(Doorkeeper::AccessGrant) expect(Doorkeeper.config).to receive(:access_token_model).and_return(Doorkeeper::AccessToken) deferred_block.call end end describe "STI (Single Table Inheritance) support" do # Ensure STI subclasses work correctly with the ActiveSupport.on_load hook. # See: https://github.com/doorkeeper-gem/doorkeeper/issues/1703 # https://github.com/doorkeeper-gem/doorkeeper/issues/1513 context "when application_class is a STI subclass of Doorkeeper::Application" do let!(:custom_application_class) do Class.new(Doorkeeper::Application) do def self.name "CustomStiApplication" end end end before do stub_const("CustomStiApplication", custom_application_class) Doorkeeper.configure do orm DOORKEEPER_ORM enable_application_owner application_class "CustomStiApplication" end Doorkeeper.run_orm_hooks end it "includes Ownership module in the STI subclass" do expect(CustomStiApplication.ancestors).to include(Doorkeeper::Models::Ownership) end it "STI subclass responds to owner association" do instance = CustomStiApplication.new expect(instance).to respond_to(:owner) end end context "when access_token_class is a STI subclass of Doorkeeper::AccessToken" do let!(:custom_token_class) do Class.new(Doorkeeper::AccessToken) do def self.name "CustomStiAccessToken" end end end before do stub_const("CustomStiAccessToken", custom_token_class) Doorkeeper.configure do orm DOORKEEPER_ORM access_token_class "CustomStiAccessToken" end Doorkeeper.run_orm_hooks end it "includes PolymorphicResourceOwner::ForAccessToken in the STI subclass" do expect(CustomStiAccessToken.ancestors).to include( Doorkeeper::Models::PolymorphicResourceOwner::ForAccessToken, ) end end context "when access_grant_class is a STI subclass of Doorkeeper::AccessGrant" do let!(:custom_grant_class) do Class.new(Doorkeeper::AccessGrant) do def self.name "CustomStiAccessGrant" end end end before do stub_const("CustomStiAccessGrant", custom_grant_class) Doorkeeper.configure do orm DOORKEEPER_ORM access_grant_class "CustomStiAccessGrant" end Doorkeeper.run_orm_hooks end it "includes PolymorphicResourceOwner::ForAccessGrant in the STI subclass" do expect(CustomStiAccessGrant.ancestors).to include( Doorkeeper::Models::PolymorphicResourceOwner::ForAccessGrant, ) end end end end end ================================================ FILE: spec/lib/doorkeeper_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper do describe "#authenticate" do let(:request) { double } it "calls OAuth::Token#authenticate" do token_strategies = described_class.config.access_token_methods expect(Doorkeeper::OAuth::Token).to receive(:authenticate) .with(request, *token_strategies) described_class.authenticate(request) end it "accepts custom token strategies" do token_strategies = %i[first_way second_way] expect(Doorkeeper::OAuth::Token).to receive(:authenticate) .with(request, *token_strategies) described_class.authenticate(request, token_strategies) end end describe "#setup_filter_parameters" do let(:original_filter_parameters) { Rails.application.config.filter_parameters.dup } before { original_filter_parameters } after do Rails.application.config.filter_parameters.replace(original_filter_parameters) end it "adds OAuth sensitive parameters to filter_parameters" do Rails.application.config.filter_parameters.clear described_class.configure do orm DOORKEEPER_ORM resource_owner_authenticator { nil } end described_class.setup_filter_parameters filter_params = Rails.application.config.filter_parameters expect(filter_params).to include( a_kind_of(Regexp).and(match("client_secret")) ) expect(filter_params).to include( a_kind_of(Regexp).and(match("access_token")) ) expect(filter_params).to include( a_kind_of(Regexp).and(match("refresh_token")) ) expect(filter_params).to include( a_kind_of(Regexp).and(match("authentication_token")) ) end it "includes code parameter when authorization_code flow is enabled" do Rails.application.config.filter_parameters.clear described_class.configure do orm DOORKEEPER_ORM resource_owner_authenticator { nil } grant_flows %w[authorization_code] end described_class.setup_filter_parameters filter_params = Rails.application.config.filter_parameters expect(filter_params).to include( a_kind_of(Regexp).and(match("code")) ) end it "does not include code parameter when authorization_code flow is not enabled" do Rails.application.config.filter_parameters.clear described_class.configure do orm DOORKEEPER_ORM resource_owner_authenticator { nil } grant_flows %w[client_credentials] end described_class.setup_filter_parameters filter_params = Rails.application.config.filter_parameters expect(filter_params.any? { |f| f.is_a?(Regexp) && f.match?("code") }).to be(false) end it "does not add duplicate filters when called multiple times" do Rails.application.config.filter_parameters.clear described_class.configure do orm DOORKEEPER_ORM resource_owner_authenticator { nil } end 2.times { described_class.setup_filter_parameters } filters = Rails.application.config.filter_parameters.select { |f| f.is_a?(Regexp) && f.match?("access_token") } expect(filters.size).to eq(1) end end end ================================================ FILE: spec/lib/grant_flow/flow_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::GrantFlow::Flow do subject(:flow) { described_class.new(name, **options) } let(:name) { "secret_handshake" } let(:options) { {} } it "reflects the given name" do expect(flow.name).to eq name end context "with neither grant_type nor response_type" do it "does not handle grant_type" do expect(flow.handles_grant_type?).to be false end it "does not handle response_type" do expect(flow.handles_response_type?).to be false end end context "when given a grant_type to match" do let(:grant_type_matches) { "secret_handshake" } let(:options) { { grant_type_matches: grant_type_matches } } it "handles grant_type" do expect(flow.handles_grant_type?).to be true end context "when grant_type_matches is a string" do it "matches grant_type values" do expect(flow.matches_grant_type?(grant_type_matches)).to be true end end context "when grant_type_matches is a regular expression" do let(:grant_type_matches) { /^secret_(.*)$/ } it "matches grant_type values" do expect(flow.matches_grant_type?("secret_boogie")).to be true end end end context "when given a response_type to match" do let(:response_type_matches) { "secret_handshake" } let(:options) { { response_type_matches: response_type_matches } } it "handles response_type" do expect(flow.handles_response_type?).to be true end context "when response_type_matches is a string" do it "matches response_type values" do expect(flow.matches_response_type?(response_type_matches)).to be true end end context "when response_type_matches is a regular expression" do let(:response_type_matches) { /^secret_(.*)$/ } it "matches response_type values" do expect(flow.matches_response_type?("secret_boogie")).to be true end end end context "when given a response_mode to match" do let(:response_mode_matches) { %w[secret_handshake_1 secret_handshake_2] } let(:options) { { response_mode_matches: response_mode_matches } } it "default response_mode value" do expect(flow.default_response_mode).to eq "secret_handshake_1" end it "matches response_mode values" do expect(flow.matches_response_mode?("secret_handshake_2")).to be true end end end ================================================ FILE: spec/lib/grant_flow_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::GrantFlow do # Avoid global side effects before do @origin_grant_flows = Doorkeeper::GrantFlow::Registry.flows.deep_dup end after do Doorkeeper::GrantFlow::Registry.flows = @origin_grant_flows end describe "#register" do context "with a name and options" do subject(:the_registered_flow) { described_class.get(name) } let(:name) { "puzzle_box" } let(:grant_type_matches) { "tile_position" } let(:grant_type_strategy) { double } before do described_class.register( name, grant_type_matches: grant_type_matches, grant_type_strategy: grant_type_strategy, ) end it "creates a new Flow" do expect(the_registered_flow).to be_a(Doorkeeper::GrantFlow::Flow) end it "passes on the given name" do expect(the_registered_flow.name).to eq name end it "sets the options" do expect(the_registered_flow.grant_type_matches).to eq grant_type_matches expect(the_registered_flow.grant_type_strategy).to eq grant_type_strategy end it "shows a warning when trying to register already existing flow" do expect(::Kernel).to receive(:warn).with(/already registered/) described_class.register( name, grant_type_matches: grant_type_matches, grant_type_strategy: grant_type_strategy, ) end end context "with an existing flow" do let(:existing_flow) { Doorkeeper::GrantFlow::Flow.new("light") } before do described_class.register(existing_flow) end it "records the existing Flow using its name" do expect(described_class.get(existing_flow.name)).to eq existing_flow end end end describe "#register_alias" do it "allows to alias multiple grant flows" do described_class.register_alias("implicit_oidc", as: ["token", "id_token", "id_token token"]) expect(described_class.aliases).to include(implicit_oidc: ["token", "id_token", "id_token token"]) end it "allows to alias a single grant flow" do described_class.register_alias("implicit_oidc", as: :id_token) expect(described_class.aliases).to include(implicit_oidc: [:id_token]) end end end ================================================ FILE: spec/lib/models/concerns/write_to_primary_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Models::Concerns::WriteToPrimary do let(:test_class) do Class.new do include Doorkeeper::Models::Concerns::WriteToPrimary def self.create_record with_primary_role do "created" end end end end describe ".with_primary_role" do context "when ActiveRecord is not defined" do before(:each) do # Override the global before hook by skipping DatabaseCleaner for this context # This is needed because removing ActiveRecord causes DatabaseCleaner to fail end after(:each) do # Override the global after hook - don't try to clean database end around do |example| # Save original ActiveRecord original_active_record = Object.const_get("ActiveRecord") begin # Temporarily hide ActiveRecord constant Object.send(:remove_const, "ActiveRecord") # Run the test Doorkeeper.configure do orm :active_record enable_multiple_database_roles end example.run ensure # Restore ActiveRecord for cleanup Object.const_set("ActiveRecord", original_active_record) end end it "executes block without connected_to when ActiveRecord is not available" do expect(test_class.create_record).to eq("created") end end context "when enable_multiple_database_roles is disabled" do before do Doorkeeper.configure do orm :active_record # enable_multiple_database_roles is disabled by default end end it "executes block without connected_to" do expect(ActiveRecord::Base).not_to receive(:connected_to) expect(test_class.create_record).to eq("created") end end context "when enable_multiple_database_roles is enabled" do before do Doorkeeper.configure do orm :active_record enable_multiple_database_roles end end context "when ActiveRecord supports connected_to" do before do allow(ActiveRecord::Base).to receive(:respond_to?) .with(:connected_to) .and_return(true) end it "wraps block in connected_to with writing role" do expect(ActiveRecord::Base).to receive(:connected_to) .with(role: :writing) .and_yield expect(test_class.create_record).to eq("created") end end context "when ActiveRecord does not support connected_to" do before do allow(ActiveRecord::Base).to receive(:respond_to?) .with(:connected_to) .and_return(false) end it "executes block without connected_to" do expect(ActiveRecord::Base).not_to receive(:connected_to) expect(test_class.create_record).to eq("created") end end end end end ================================================ FILE: spec/lib/models/expirable_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Models::Expirable do subject(:fake_object) do Class.new do include Doorkeeper::Models::Expirable end.new end before do allow(fake_object).to receive(:created_at).and_return(1.minute.ago) end describe "#expired?" do it "is not expired if time has not passed" do allow(fake_object).to receive(:expires_in).and_return(2.minutes) expect(fake_object).not_to be_expired end it "is expired if time has passed" do allow(fake_object).to receive(:expires_in).and_return(10.seconds) expect(fake_object).to be_expired end it "is not expired if expires_in is not set" do allow(fake_object).to receive(:expires_in).and_return(nil) expect(fake_object).not_to be_expired end end describe "#expires_in_seconds" do it "returns the amount of time remaining until the token is expired" do allow(fake_object).to receive(:expires_in).and_return(2.minutes) expect(fake_object.expires_in_seconds).to eq(60) end it "returns 0 when expired" do allow(fake_object).to receive(:expires_in).and_return(30.seconds) expect(fake_object.expires_in_seconds).to eq(0) end it "returns nil when expires_in is nil" do allow(fake_object).to receive(:expires_in).and_return(nil) expect(fake_object.expires_in_seconds).to be_nil end end describe "#expires_at" do it "returns the expiration time of the token" do allow(fake_object).to receive(:expires_in).and_return(2.minutes) expect(fake_object.expires_at).to be_a(Time) end it "returns nil when expires_in is nil" do allow(fake_object).to receive(:expires_in).and_return(nil) expect(fake_object.expires_at).to be_nil end end end ================================================ FILE: spec/lib/models/reusable_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Models::Reusable do subject(:fake_object) do Class.new do include Doorkeeper::Models::Reusable end.new end describe "#reusable?" do it "is reusable if its expires_in is nil" do allow(fake_object).to receive(:expired?).and_return(false) allow(fake_object).to receive(:expires_in).and_return(nil) expect(fake_object).to be_reusable end it "is reusable if its expiry has crossed reusable limit" do allow(fake_object).to receive(:expired?).and_return(false) allow(Doorkeeper.configuration).to receive(:token_reuse_limit).and_return(90) allow(fake_object).to receive(:expires_in).and_return(100.seconds) allow(fake_object).to receive(:expires_in_seconds).and_return(20.seconds) expect(fake_object).to be_reusable end it "is not reusable if its expiry has crossed reusable limit" do allow(fake_object).to receive(:expired?).and_return(false) allow(Doorkeeper.configuration).to receive(:token_reuse_limit).and_return(90) allow(fake_object).to receive(:expires_in).and_return(100.seconds) allow(fake_object).to receive(:expires_in_seconds).and_return(5.seconds) expect(fake_object).not_to be_reusable end it "is not reusable if it is already expired" do allow(fake_object).to receive(:expired?).and_return(true) expect(fake_object).not_to be_reusable end end end ================================================ FILE: spec/lib/models/revocable_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Models::Revocable do subject(:fake_object) do Class.new do include Doorkeeper::Models::Revocable end.new end describe "#revoke" do let(:revoked_at) { nil } before do allow(fake_object).to receive(:revoked_at).and_return(revoked_at) end it "updates :revoked_at attribute with current time" do utc = double utc: double clock = double now: utc expect(fake_object).to receive(:update_attribute).with(:revoked_at, clock.now.utc) fake_object.revoke(clock) end context "when the object is already revoked" do let(:revoked_at) { Time.now.utc - 1000 } it "does not update :revoked_at attribute" do expect(fake_object).not_to receive(:update_attribute) end end end describe "#revoked?" do it "is revoked if :revoked_at has passed" do allow(fake_object).to receive(:revoked_at).and_return(Time.now.utc - 1000) expect(fake_object).to be_revoked end it "is not revoked if :revoked_at has not passed" do allow(fake_object).to receive(:revoked_at).and_return(Time.now.utc + 1000) expect(fake_object).not_to be_revoked end it "is not revoked if :revoked_at is not set" do allow(fake_object).to receive(:revoked_at).and_return(nil) expect(fake_object).not_to be_revoked end end describe "#revoke_previous_refresh_token!" do it "revokes the previous token if exists and resets the `previous_refresh_token` attribute" do previous_token = FactoryBot.create( :access_token, refresh_token: "refresh_token", ) current_token = FactoryBot.create( :access_token, previous_refresh_token: previous_token.refresh_token, ) current_token.revoke_previous_refresh_token! expect(current_token.previous_refresh_token).to be_empty expect(previous_token.reload).to be_revoked end end end ================================================ FILE: spec/lib/models/scopes_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Models::Scopes do subject(:fake_object) do Class.new(Struct.new(:scopes)) do include Doorkeeper::Models::Scopes end.new end before do fake_object[:scopes] = "public admin" end describe "#scopes" do it "is a `Scopes` class" do expect(fake_object.scopes).to be_a(Doorkeeper::OAuth::Scopes) end it "includes scopes" do expect(fake_object.scopes).to include("public") end end describe "#scopes=" do it "accepts String" do fake_object.scopes = "private admin" expect(fake_object.scopes_string).to eq("private admin") end it "accepts Array" do fake_object.scopes = %w[private admin] expect(fake_object.scopes_string).to eq("private admin") end it "ignores duplicated scopes" do fake_object.scopes = %w[private admin admin] expect(fake_object.scopes_string).to eq("private admin") fake_object.scopes = "private admin admin" expect(fake_object.scopes_string).to eq("private admin") end end describe "#scopes_string" do it "is a `Scopes` class" do expect(fake_object.scopes_string).to eq("public admin") end end describe "#includes_scope?" do it "returns true if at least one scope is included" do expect(fake_object.includes_scope?("public", "private")).to be true end it "returns false if no scopes are included" do expect(fake_object.includes_scope?("teacher", "student")).to be false end end end ================================================ FILE: spec/lib/models/secret_storable_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Models::SecretStorable do let(:clazz) do Class.new do include Doorkeeper::Models::SecretStorable def self.find_by(*) raise "stub this" end def update_column(*) raise "stub this" end def token raise "stub this" end end end let(:strategy) { clazz.secret_strategy } describe ".find_by_plaintext_token" do subject(:result) { clazz.send(:find_by_plaintext_token, "attr", "input") } it "forwards to the secret_strategy" do expect(strategy) .to receive(:transform_secret) .with("input") .and_return "found" expect(clazz) .to receive(:find_by) .with("attr" => "found") .and_return "result" expect(result).to eq "result" end it "calls find_by_fallback_token if not found" do expect(clazz) .to receive(:find_by) .with("attr" => "input") .and_return nil expect(clazz) .to receive(:find_by_fallback_token) .with("attr", "input") .and_return "fallback" expect(result).to eq "fallback" end end describe ".find_by_fallback_token" do subject(:result) { clazz.send(:find_by_fallback_token, "attr", "input") } let(:fallback) { double(::Doorkeeper::SecretStoring::Plain) } it "returns nil if none defined" do expect(clazz.fallback_secret_strategy).to eq nil expect(result).to eq nil end context "when a fallback strategy is defined" do before do allow(clazz).to receive(:fallback_secret_strategy).and_return(fallback) end context "when resource is defined" do let(:resource) { double("Token model") } it "calls the strategy for lookup" do expect(clazz) .to receive(:find_by) .with("attr" => "fallback") .and_return(resource) expect(fallback) .to receive(:transform_secret) .with("input") .and_return("fallback") # store_secret will call the resource expect(resource) .to receive(:attr=) .with("new value") # It will upgrade the secret automatically using the current strategy expect(strategy) .to receive(:transform_secret) .with("input") .and_return("new value") expect(resource).to receive(:update).with("attr" => "new value") expect(result).to eq resource end end context "when resource is not defined" do before do allow(clazz).to receive(:fallback_secret_strategy).and_return(fallback) end it "returns nil" do expect(clazz) .to receive(:find_by) .with("attr" => "fallback") .and_return(nil) expect(fallback) .to receive(:transform_secret) .with("input") .and_return("fallback") # It does not find a token even with the fallback method expect(result).to be_nil end end end end describe ".secret_strategy" do it "defaults to plain strategy" do expect(strategy).to eq Doorkeeper::SecretStoring::Plain end end describe ".fallback_secret_strategy" do it "defaults to nil" do expect(clazz.fallback_secret_strategy).to eq nil end end end ================================================ FILE: spec/lib/oauth/authorization/code_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::Authorization::Code do let(:pre_auth) do double( :pre_auth, client: application, redirect_uri: "https://example.com/callback", scopes: Doorkeeper::OAuth::Scopes.from_string("public"), code_challenge: nil, code_challenge_method: nil, custom_access_token_attributes: {}, ) end let(:resource_owner) { FactoryBot.create(:resource_owner) } let(:application) { FactoryBot.create(:application) } let(:authorization) { described_class.new(pre_auth, resource_owner) } describe "#issue_token! with read replica support" do context "when enable_multiple_database_roles is enabled" do before do Doorkeeper.configure do orm :active_record enable_multiple_database_roles end end it "creates access grant using primary database role" do expect(ActiveRecord::Base).to receive(:connected_to).with(role: :writing).and_call_original token = authorization.issue_token! expect(token).to be_persisted expect(token.application_id).to eq(application.id) end end context "when enable_multiple_database_roles is disabled" do before do Doorkeeper.configure do orm :active_record # enable_multiple_database_roles is disabled by default end end it "creates access grant without explicit role switching" do expect(ActiveRecord::Base).not_to receive(:connected_to) token = authorization.issue_token! expect(token).to be_persisted end end end end ================================================ FILE: spec/lib/oauth/authorization/uri_builder_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::Authorization::URIBuilder do describe ".uri_with_query" do it "returns the uri with query" do uri = described_class.uri_with_query "http://example.com/", parameter: "value" expect(uri).to eq("http://example.com/?parameter=value") end it "rejects nil values" do uri = described_class.uri_with_query "http://example.com/", parameter: "" expect(uri).to eq("http://example.com/?") end it "preserves original query parameters" do uri = described_class.uri_with_query "http://example.com/?query1=value", parameter: "value" expect(uri).to match(/query1=value/) expect(uri).to match(/parameter=value/) end end describe ".uri_with_fragment" do it "returns uri with parameters as fragments" do uri = described_class.uri_with_fragment "http://example.com/", parameter: "value" expect(uri).to eq("http://example.com/#parameter=value") end it "preserves original query parameters" do uri = described_class.uri_with_fragment "http://example.com/?query1=value1", parameter: "value" expect(uri).to eq("http://example.com/?query1=value1#parameter=value") end end end ================================================ FILE: spec/lib/oauth/authorization_code_request_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::AuthorizationCodeRequest do subject(:request) do described_class.new(server, grant, client, params) end let(:server) do double :server, access_token_expires_in: 2.days, refresh_token_enabled?: false, custom_access_token_expires_in: lambda { |context| context.grant_type == Doorkeeper::OAuth::AUTHORIZATION_CODE ? 1234 : nil } end let(:resource_owner) { FactoryBot.create :resource_owner } let(:grant) do FactoryBot.create :access_grant, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end let(:client) { grant.application } let(:redirect_uri) { client.redirect_uri } let(:params) { { redirect_uri: redirect_uri } } before do allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) end it "issues a new token for the client" do expect do request.authorize end.to change { client.reload.access_tokens.count }.by(1) expect(client.reload.access_tokens.max_by(&:created_at).expires_in).to eq(1234) end it "issues the token with same grant's scopes" do request.authorize expect(Doorkeeper::AccessToken.last.scopes).to eq(grant.scopes) end it "revokes the grant" do expect { request.authorize }.to(change { grant.reload.accessible? }) end it "requires the grant to be accessible" do grant.revoke request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end it "requires the grant" do request = described_class.new(server, nil, client, params) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end it "requires the client" do request = described_class.new(server, grant, nil, params) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidClient) end it "requires the redirect_uri" do request = described_class.new(server, grant, nil, params.except(:redirect_uri)) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidRequest) expect(request.missing_param).to eq(:redirect_uri) end it "matches the redirect_uri with grant's one" do request = described_class.new(server, grant, client, params.merge(redirect_uri: "http://other.com")) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end it "matches the client with grant's one" do other_client = FactoryBot.create :application request = described_class.new(server, grant, other_client, params) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end it "skips token creation if there is a matching one reusable" do scopes = grant.scopes Doorkeeper.configure do orm DOORKEEPER_ORM reuse_access_token default_scopes(*scopes) end FactoryBot.create( :access_token, application_id: client.id, resource_owner_id: grant.resource_owner_id, resource_owner_type: grant.resource_owner_type, scopes: grant.scopes.to_s, ) expect { request.authorize }.not_to(change { Doorkeeper::AccessToken.count }) end it "creates token if there is a matching one but non reusable" do scopes = grant.scopes Doorkeeper.configure do orm DOORKEEPER_ORM reuse_access_token default_scopes(*scopes) end FactoryBot.create( :access_token, application_id: client.id, resource_owner_id: grant.resource_owner_id, resource_owner_type: grant.resource_owner_type, scopes: grant.scopes.to_s, ) allow_any_instance_of(Doorkeeper::AccessToken).to receive(:reusable?).and_return(false) expect { request.authorize }.to change { Doorkeeper::AccessToken.count }.by(1) end it "calls configured request callback methods" do expect(Doorkeeper.configuration.before_successful_strategy_response) .to receive(:call).with(request).once expect(Doorkeeper.configuration.after_successful_strategy_response) .to receive(:call).with(request, instance_of(Doorkeeper::OAuth::TokenResponse)).once request.authorize end context "when redirect_uri contains some query params" do let(:redirect_uri) { "#{client.redirect_uri}?query=q" } it "allows query params" do request.validate expect(request.error).to eq(nil) end end context "when redirect_uri is not an URI" do let(:redirect_uri) { "123d#!s" } it "responds with invalid_grant" do request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end end context "when redirect_uri is the native one" do let(:redirect_uri) { "urn:ietf:wg:oauth:2.0:oob" } it "invalidates when redirect_uri of the grant is not native" do request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end it "validates when redirect_uri of the grant is also native" do allow(grant).to receive(:redirect_uri) { redirect_uri } request.validate expect(request.error).to eq(nil) end end context "when using PKCE params" do context "when force_pkce is enabled" do before do allow_any_instance_of(Doorkeeper::Config).to receive(:force_pkce?).and_return(true) end context "when the app is confidential" do it "issues a new token for the client" do expect do request.authorize end.to change { client.reload.access_tokens.count }.by(1) end end context "when the app is not confidential" do before do client.update(confidential: false) end it "does not issue a token" do expect do request.authorize end.not_to change { client.reload.access_tokens.count } end end context "when the app is missing" do it "does not assume non-confidential and forcibly validate pkce params" do request = described_class.new(server, grant, nil, params) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidClient) end end end context "when PKCE is supported" do before do allow(Doorkeeper::AccessGrant).to receive(:pkce_supported?).and_return(true) grant.code_challenge = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" grant.code_challenge_method = "plain" end it "validates when code_verifier is present" do params[:code_verifier] = grant.code_challenge request.validate expect(request.error).to eq(nil) end it "validates when both code_verifier and code_challenge are blank" do params[:code_verifier] = grant.code_challenge = "" request.validate expect(request.error).to eq(nil) end it "invalidates when code_verifier is missing" do request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidRequest) expect(request.missing_param).to eq(:code_verifier) end it "invalidates when code_verifier is the wrong value" do params[:code_verifier] = "foobar" request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end context "when PKCE code challenge methods is set to only S256" do before do Doorkeeper.configure do pkce_code_challenge_methods ["S256"] end end it "validates when code_verifier is S256" do params[:code_verifier] = grant.code_challenge = "S256" request.validate expect(request.error).to eq(nil) end it "invalidates when code_verifier is plain" do params[:code_verifier] = "plain" request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end end end context "when PKCE is not supported" do before do allow(Doorkeeper::AccessGrant).to receive(:pkce_supported?).and_return(false) end it "validates when code_verifier is present" do params[:code_verifier] = "foobar" request.validate expect(request.error).to be_nil end end end context "when revoke_previous_authorization_code_token is false" do before do allow(Doorkeeper.config).to receive(:revoke_previous_authorization_code_token?).and_return(false) end it "does not revoke the previous token" do previous_token = FactoryBot.create( :access_token, application_id: client.id, resource_owner_id: grant.resource_owner_id, resource_owner_type: grant.resource_owner_type, scopes: grant.scopes.to_s, ) expect { request.authorize }.not_to(change { previous_token.reload.revoked_at }) end end context "when revoke_previous_authorization_code_token is true" do before do allow(Doorkeeper.config).to receive(:revoke_previous_authorization_code_token?).and_return(true) end it "revokes the previous token" do previous_token = FactoryBot.create( :access_token, application_id: client.id, resource_owner_id: grant.resource_owner_id, resource_owner_type: grant.resource_owner_type, scopes: grant.scopes.to_s, ) expect { request.authorize }.to(change { previous_token.reload.revoked_at }) end end end ================================================ FILE: spec/lib/oauth/base_request_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::BaseRequest do subject(:request) do described_class.new end let(:access_token) do double :access_token, plaintext_token: "some-token", expires_in: "3600", expires_in_seconds: "300", scopes_string: "two scopes", plaintext_refresh_token: "some-refresh-token", token_type: "Bearer", created_at: 0 end let(:client) { Doorkeeper::Application.new(id: "1") } let(:scopes_array) { %w[public write] } let(:server) do double :server, access_token_expires_in: 100, custom_access_token_expires_in: ->(_context) { nil }, refresh_token_enabled?: false end before do allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) end describe "#authorize" do before do allow(request).to receive(:access_token).and_return(access_token) end it "validates itself" do expect(request).to receive(:validate).once request.authorize end context "when valid" do before do allow(request).to receive(:valid?).and_return(true) end it "calls callback methods" do expect(request).to receive(:before_successful_response).once expect(request).to receive(:after_successful_response).once request.authorize end it "returns a TokenResponse object" do result = request.authorize expect(result).to be_an_instance_of(Doorkeeper::OAuth::TokenResponse) expect(result.body).to eq( Doorkeeper::OAuth::TokenResponse.new(access_token).body, ) end end context "when invalid" do context "with error other than invalid_request" do before do allow(request).to receive(:valid?).and_return(false) allow(request).to receive(:error).and_return(Doorkeeper::Errors::ServerError) allow(request).to receive(:state).and_return("hello") end it "returns an ErrorResponse object" do result = request.authorize expect(result).to be_an_instance_of(Doorkeeper::OAuth::ErrorResponse) expect(result.body).to eq( error: :server_error, error_description: translated_error_message(:server_error), state: "hello", ) end end context "with invalid_request error" do before do allow(request).to receive(:valid?).and_return(false) allow(request).to receive(:error).and_return(Doorkeeper::Errors::InvalidRequest) allow(request).to receive(:state).and_return("hello") end it "returns an InvalidRequestResponse object" do result = request.authorize expect(result).to be_an_instance_of(Doorkeeper::OAuth::InvalidRequestResponse) expect(result.body).to eq( error: :invalid_request, error_description: translated_invalid_request_error_message(:unknown, :unknown), state: "hello", ) end end end end describe "#default_scopes" do it "delegates to the server" do expect(request).to receive(:server).and_return(server).once expect(server).to receive(:default_scopes).once request.default_scopes end end describe "#find_or_create_access_token" do let(:resource_owner) { FactoryBot.build_stubbed(:resource_owner) } it "returns an instance of AccessToken" do result = request.find_or_create_access_token( client, resource_owner, "public", {}, server, ) expect(result).to be_an_instance_of(Doorkeeper::AccessToken) end it "respects custom_access_token_expires_in" do server = double( :server, access_token_expires_in: 100, custom_access_token_expires_in: ->(context) { context.scopes == "public" ? 500 : nil }, refresh_token_enabled?: false, ) allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) result = request.find_or_create_access_token( client, resource_owner, "public", {}, server, ) expect(result.expires_in).to be(500) end it "respects use_refresh_token with a block" do server = double( :server, access_token_expires_in: 100, custom_access_token_expires_in: ->(_context) { nil }, refresh_token_enabled?: lambda { |context| context.scopes == "public" }, ) allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) result = request.find_or_create_access_token( client, resource_owner, "public", {}, server, ) expect(result.refresh_token).not_to be_nil result = request.find_or_create_access_token( client, resource_owner, "private", {}, server, ) expect(result.refresh_token).to be_nil end end describe "#scopes" do context "when @original_scopes is present" do before do request.instance_variable_set(:@original_scopes, "public write") end it "returns array of @original_scopes" do result = request.scopes expect(result).to eq(scopes_array) end end context "when @original_scopes is blank" do before do request.instance_variable_set(:@original_scopes, "") end it "calls #default_scopes" do allow(request).to receive(:server).and_return(server).once allow(server).to receive(:default_scopes).and_return(scopes_array).once result = request.scopes expect(result).to eq(scopes_array) end end end end ================================================ FILE: spec/lib/oauth/base_response_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::BaseResponse do subject(:response) do described_class.new end describe "#body" do it "returns an empty Hash" do expect(response.body).to eq({}) end end describe "#description" do it "returns an empty String" do expect(response.description).to eq("") end end describe "#headers" do it "returns an empty Hash" do expect(response.headers).to eq({}) end end describe "#redirectable?" do it "returns false" do expect(response.redirectable?).to eq(false) end end describe "#redirect_uri" do it "returns an empty String" do expect(response.redirect_uri).to eq("") end end describe "#status" do it "returns :ok" do expect(response.status).to eq(:ok) end end end ================================================ FILE: spec/lib/oauth/client/credentials_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" class Doorkeeper::OAuth::Client describe Credentials do let(:client_id) { "some-uid" } let(:client_secret) { "some-secret" } it "is blank when the uid in credentials is blank" do expect(described_class.new(nil, nil)).to be_blank expect(described_class.new(nil, "something")).to be_blank expect(described_class.new("something", nil)).to be_present expect(described_class.new("something", "something")).to be_present end describe ".from_request" do let(:request) { double.as_null_object } let(:method) do ->(_request) { %w[uid secret] } end it "accepts anything that responds to #call" do expect(method).to receive(:call).with(request) described_class.from_request request, method end it "delegates methods received as symbols to Credentials class" do expect(described_class).to receive(:from_params).with(request) described_class.from_request request, :from_params end it "stops at the first credentials found" do not_called_method = double expect(not_called_method).not_to receive(:call) described_class.from_request request, ->(_) {}, method, not_called_method end it "returns new Credentials" do credentials = described_class.from_request request, method expect(credentials).to be_a(described_class) end it "returns uid and secret from extractor method" do credentials = described_class.from_request request, method expect(credentials.uid).to eq("uid") expect(credentials.secret).to eq("secret") end end describe ".from_params" do it "returns credentials from parameters when Authorization header is not available" do request = double parameters: { client_id: client_id, client_secret: client_secret } uid, secret = described_class.from_params(request) expect(uid).to eq("some-uid") expect(secret).to eq("some-secret") end it "is blank when there are no credentials" do request = double parameters: {} uid, secret = described_class.from_params(request) expect(uid).to be_blank expect(secret).to be_blank end end describe ".from_basic" do let(:credentials) { Base64.encode64("#{client_id}:#{client_secret}") } it "decodes the credentials" do request = double authorization: "Basic #{credentials}" uid, secret = described_class.from_basic(request) expect(uid).to eq("some-uid") expect(secret).to eq("some-secret") end it "is blank if Authorization is not Basic" do request = double authorization: credentials.to_s uid, secret = described_class.from_basic(request) expect(uid).to be_blank expect(secret).to be_blank end it "decodes credentials with lowercase 'basic' prefix" do request = double authorization: "basic #{credentials}" uid, secret = described_class.from_basic(request) expect(uid).to eq("some-uid") expect(secret).to eq("some-secret") end it "decodes credentials with mixed case 'BaSiC' prefix" do request = double authorization: "BaSiC #{credentials}" uid, secret = described_class.from_basic(request) expect(uid).to eq("some-uid") expect(secret).to eq("some-secret") end end end end ================================================ FILE: spec/lib/oauth/client_credentials/creator_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::ClientCredentials::Creator do subject(:creator) { described_class.new } let(:client) { FactoryBot.create :application } let(:scopes) { Doorkeeper::OAuth::Scopes.from_string("public") } before do default_scopes_exist :public end it "creates a new token" do expect do creator.call(client, scopes) end.to change { Doorkeeper::AccessToken.count }.by(1) end context "when reuse_access_token is true" do before do allow(Doorkeeper.config).to receive(:reuse_access_token).and_return(true) end context "when expiration is disabled" do it "returns the existing valid token" do existing_token = creator.call(client, scopes) result = creator.call(client, scopes) expect(Doorkeeper::AccessToken.count).to eq(1) expect(result).to eq(existing_token) end end context "when existing token has not crossed token_reuse_limit" do let!(:existing_token) { creator.call(client, scopes, expires_in: 1000) } before do allow(Doorkeeper.config).to receive(:token_reuse_limit).and_return(50) allow_any_instance_of(Doorkeeper::AccessToken).to receive(:expires_in_seconds).and_return(600) end it "returns the existing valid token" do result = creator.call(client, scopes, expires_in: 1000) expect(Doorkeeper::AccessToken.count).to eq(1) expect(result).to eq(existing_token) end context "when revoke_previous_client_credentials_token is false" do before do allow(Doorkeeper.config).to receive(:revoke_previous_client_credentials_token).and_return(false) end it "does not revoke the existing valid token" do creator.call(client, scopes, expires_in: 1000) expect(existing_token.reload).not_to be_revoked end end end context "when existing token has crossed token_reuse_limit" do it "returns a new token" do allow(Doorkeeper.config).to receive(:token_reuse_limit).and_return(50) existing_token = creator.call(client, scopes, expires_in: 1000) allow_any_instance_of(Doorkeeper::AccessToken).to receive(:expires_in_seconds).and_return(400) result = creator.call(client, scopes, expires_in: 1000) expect(Doorkeeper::AccessToken.count).to eq(2) expect(result).not_to eq(existing_token) end end context "when existing token has been expired" do it "returns a new token" do allow(Doorkeeper.configuration).to receive(:token_reuse_limit).and_return(50) existing_token = creator.call(client, scopes, expires_in: 1000) allow_any_instance_of(Doorkeeper::AccessToken).to receive(:expired?).and_return(true) result = creator.call(client, scopes, expires_in: 1000) expect(Doorkeeper::AccessToken.count).to eq(2) expect(result).not_to eq(existing_token) end end end context "when reuse_access_token is false" do before do allow(Doorkeeper.config).to receive(:reuse_access_token).and_return(false) end it "returns a new token" do existing_token = creator.call(client, scopes) result = creator.call(client, scopes) expect(Doorkeeper::AccessToken.count).to eq(2) expect(result).not_to eq(existing_token) end end context "when revoke_previous_client_credentials_token is true" do let!(:existing_token) { creator.call(client, scopes, expires_in: 1000) } before do allow(Doorkeeper.configuration).to receive(:revoke_previous_client_credentials_token?).and_return(true) end it "revokes the existing token" do creator.call(client, scopes, expires_in: 1000) expect(existing_token.reload).to be_revoked end end context "when revoke_previous_client_credentials_token is false" do let!(:existing_token) { creator.call(client, scopes, expires_in: 1000) } before do allow(Doorkeeper.configuration).to receive(:revoke_previous_client_credentials_token?).and_return(false) end it "does not revoke the existing token" do creator.call(client, scopes, expires_in: 1000) expect(existing_token.reload).not_to be_revoked end end end ================================================ FILE: spec/lib/oauth/client_credentials/issuer_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::ClientCredentials::Issuer do subject(:issuer) { described_class.new(server, validator) } let(:creator) { double :access_token_creator } let(:server) do double( :server, access_token_expires_in: 100, ) end let(:validator) { double :validator, valid?: true } before do allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(false) end describe "#create" do let(:client) { double :client, id: "some-id" } let(:scopes) { "some scope" } it "creates and sets the token" do expect(creator).to receive(:call).and_return("token") issuer.create client, scopes, {}, creator expect(issuer.token).to eq("token") end it "creates with correct token parameters" do expect(creator).to receive(:call).with( client, scopes, expires_in: 100, use_refresh_token: false, ) issuer.create client, scopes, {}, creator end it "creates with custom token parameters" do expect(creator).to receive(:call).with( client, scopes, expires_in: 100, use_refresh_token: false, tenant_id: 9000 ) issuer.create client, scopes, { tenant_id: 9000 }, creator end it "has error set to :server_error if creator fails" do expect(creator).to receive(:call).and_return(false) issuer.create client, scopes, {}, creator expect(issuer.error).to eq(Doorkeeper::Errors::ServerError) end context "when validator fails" do before do allow(validator).to receive(:valid?).and_return(false) allow(validator).to receive(:error).and_return(:validation_error) end it "has error set from validator" do expect(creator).not_to receive(:create) issuer.create client, scopes, {}, creator expect(issuer.error).to eq(:validation_error) end it "returns false" do expect(issuer.create(client, scopes, {}, creator)).to be_falsey end end context "with custom expiration" do let(:custom_ttl_grant) { 1234 } let(:custom_ttl_scope) { 1235 } let(:custom_scope) { "special" } let(:server) do double( :server, custom_access_token_expires_in: lambda { |context| # scopes is normally an object but is a string in this test if context.scopes == custom_scope custom_ttl_scope elsif context.grant_type == Doorkeeper::OAuth::CLIENT_CREDENTIALS custom_ttl_grant end }, ) end before do allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) end it "respects grant based rules" do expect(creator).to receive(:call).with( client, scopes, expires_in: custom_ttl_grant, use_refresh_token: false, ) issuer.create client, scopes, {}, creator end it "respects scope based rules" do expect(creator).to receive(:call).with( client, custom_scope, expires_in: custom_ttl_scope, use_refresh_token: false, ) issuer.create client, custom_scope, {}, creator end end end end ================================================ FILE: spec/lib/oauth/client_credentials/validation_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::ClientCredentials::Validator do subject(:validator) { described_class.new(server, request) } let(:server) { double :server, scopes: nil } let(:application) { double scopes: nil } let(:client) { double application: application } let(:request) { double :request, client: client, scopes: nil } it "is valid with valid request" do expect(validator).to be_valid end it "is invalid when client is not present" do allow(request).to receive(:client).and_return(nil) expect(validator).not_to be_valid end context "when a grant flow check is configured" do let(:callback) { double("callback") } before do allow(Doorkeeper.config).to receive(:option_defined?).with(:allow_grant_flow_for_client).and_return(true) allow(Doorkeeper.config).to receive(:allow_grant_flow_for_client).and_return(callback) end context "when the callback rejects the grant flow" do let(:callback_response) { false } it "is invalid" do expect(callback).to receive(:call).twice.with( Doorkeeper::OAuth::CLIENT_CREDENTIALS, application, ).and_return(callback_response) expect(validator).not_to be_valid end end context "when the callback allows the grant flow" do let(:callback_response) { true } it "is invalid" do expect(callback).to receive(:call).twice.with( Doorkeeper::OAuth::CLIENT_CREDENTIALS, application, ).and_return(callback_response) expect(validator).to be_valid end end end context "with scopes" do it "is invalid when scopes are not included in the server" do server_scopes = Doorkeeper::OAuth::Scopes.from_string "email" allow(request).to receive(:grant_type).and_return(Doorkeeper::OAuth::CLIENT_CREDENTIALS) allow(server).to receive(:scopes).and_return(server_scopes) allow(request).to receive(:scopes).and_return( Doorkeeper::OAuth::Scopes.from_string("invalid"), ) expect(validator).not_to be_valid end context "with application scopes" do it "is valid when scopes are included in the application" do application_scopes = Doorkeeper::OAuth::Scopes.from_string "app" server_scopes = Doorkeeper::OAuth::Scopes.from_string "email app" allow(application).to receive(:scopes).and_return(application_scopes) allow(server).to receive(:scopes).and_return(server_scopes) allow(request).to receive(:grant_type).and_return(Doorkeeper::OAuth::CLIENT_CREDENTIALS) allow(request).to receive(:scopes).and_return(application_scopes) expect(validator).to be_valid end it "is invalid when scopes are not included in the application" do application_scopes = Doorkeeper::OAuth::Scopes.from_string "app" server_scopes = Doorkeeper::OAuth::Scopes.from_string "email app" allow(application).to receive(:scopes).and_return(application_scopes) allow(request).to receive(:grant_type).and_return(Doorkeeper::OAuth::CLIENT_CREDENTIALS) allow(server).to receive(:scopes).and_return(server_scopes) allow(request).to receive(:scopes).and_return( Doorkeeper::OAuth::Scopes.from_string("email"), ) expect(validator).not_to be_valid end end end end ================================================ FILE: spec/lib/oauth/client_credentials_integration_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::ClientCredentialsRequest do let(:server) { Doorkeeper.configuration } context "with a valid request" do let(:client) { Doorkeeper::OAuth::Client.new(FactoryBot.build_stubbed(:application)) } it "issues an access token" do request = described_class.new(server, client, {}) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) end end describe "with an invalid request" do it "does not issue an access token" do request = described_class.new(server, nil, {}) expect do request.authorize end.not_to(change { Doorkeeper::AccessToken.count }) end end end ================================================ FILE: spec/lib/oauth/client_credentials_request_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::ClientCredentialsRequest do subject(:request) { described_class.new(server, client) } let(:server) do double( default_scopes: nil, access_token_expires_in: 2.hours, custom_access_token_expires_in: ->(_context) { nil }, ) end let(:application) { FactoryBot.create(:application, scopes: "") } let(:client) { double :client, application: application, scopes: "" } let(:token_creator) { double :issuer, create: true, token: double } before do allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) allow(request).to receive(:issuer).and_return(token_creator) end it "returns :grant_type as client_credentials" do expect(request.grant_type).to eq(Doorkeeper::OAuth::CLIENT_CREDENTIALS) end it "issues an access token for the current client" do expect(token_creator).to receive(:create).with(client, nil, {}) request.authorize end it "has successful response when issue was created" do request.authorize expect(request.response).to be_a(Doorkeeper::OAuth::TokenResponse) end context "when issue was not created" do before do issuer = double create: false, error: Doorkeeper::Errors::UnsupportedResponseType allow(request).to receive(:issuer).and_return(issuer) end it "has an error response" do request.authorize expect(request.response).to be_a(Doorkeeper::OAuth::ErrorResponse) end it "delegates the error to issuer" do request.authorize expect(request.error).to eq(Doorkeeper::Errors::UnsupportedResponseType) end end context "with scopes" do let(:default_scopes) { Doorkeeper::OAuth::Scopes.from_string("public email") } before do allow(server).to receive(:default_scopes).and_return(default_scopes) end it "issues an access token with default scopes if none was requested" do expect(token_creator).to receive(:create).with(client, default_scopes, {}) request.authorize end it "issues an access token with requested scopes" do request = described_class.new(server, client, scope: "email") allow(request).to receive(:issuer).and_return(token_creator) expect(token_creator).to receive(:create).with(client, Doorkeeper::OAuth::Scopes.from_string("email"), {}) request.authorize end end context "with custom_access_token_attributes configured" do before do Doorkeeper.configure do custom_access_token_attributes [:tenant_id] end end it "issues an access token with the custom access token attributes" do request = described_class.new(server, client, scope: "email", tenant_id: 9000) allow(request).to receive(:issuer).and_return(token_creator) expect(token_creator).to receive(:create).with(client, Doorkeeper::OAuth::Scopes.from_string("email"), { tenant_id: 9000 }) request.authorize end end context "with restricted client" do let(:default_scopes) do Doorkeeper::OAuth::Scopes.from_string("public email") end let(:server_scopes) do Doorkeeper::OAuth::Scopes.from_string("public email phone") end let(:client_scopes) do Doorkeeper::OAuth::Scopes.from_string("public phone") end before do allow(server).to receive(:default_scopes).and_return(default_scopes) allow(server).to receive(:scopes).and_return(server_scopes) allow(server).to receive(:access_token_expires_in).and_return(100) allow(application).to receive(:scopes).and_return(client_scopes) allow(client).to receive(:id).and_return(nil) end it "delegates the error to issuer if no scope was requested" do request = described_class.new(server, client) request.authorize expect(request.response).to be_a(Doorkeeper::OAuth::ErrorResponse) expect(request.error).to eq(Doorkeeper::Errors::InvalidScope) end it "issues an access token with requested scopes" do request = described_class.new(server, client, scope: "phone") request.authorize expect(request.response).to be_a(Doorkeeper::OAuth::TokenResponse) expect(request.response.token.scopes_string).to eq("phone") end end end ================================================ FILE: spec/lib/oauth/client_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::Client do describe ".find" do let(:method) { double } it "finds the client via uid" do client = double expect(method).to receive(:call).with("uid").and_return(client) expect(described_class.find("uid", method)) .to be_a(described_class) end it "returns nil if client was not found" do expect(method).to receive(:call).with("uid").and_return(nil) expect(described_class.find("uid", method)).to be_nil end end describe ".authenticate" do it "returns the authenticated client via credentials" do credentials = Doorkeeper::OAuth::Client::Credentials.new("some-uid", "some-secret") authenticator = double expect(authenticator).to receive(:call).with("some-uid", "some-secret").and_return(double) expect(described_class.authenticate(credentials, authenticator)) .to be_a(described_class) end it "returns nil if client was not authenticated" do credentials = Doorkeeper::OAuth::Client::Credentials.new("some-uid", "some-secret") authenticator = double expect(authenticator).to receive(:call).with("some-uid", "some-secret").and_return(nil) expect(described_class.authenticate(credentials, authenticator)).to be_nil end end end ================================================ FILE: spec/lib/oauth/code_request_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::CodeRequest do subject(:request) do described_class.new(pre_auth, owner) end let(:pre_auth) do allow(Doorkeeper.config) .to receive(:default_scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public")) allow(Doorkeeper.config) .to receive(:grant_flows).and_return(Doorkeeper::OAuth::Scopes.from_string("authorization_code")) application = FactoryBot.create(:application, scopes: "public") client = Doorkeeper::OAuth::Client.new(application) attributes = { client_id: client.uid, response_type: "code", redirect_uri: "https://app.com/callback", response_mode: response_mode, }.compact pre_auth = Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.config, attributes) pre_auth.authorizable? pre_auth end let(:response_mode) { nil } let(:owner) { FactoryBot.create(:resource_owner) } context "when pre_auth is authorized" do it "creates an access grant and returns a code response" do expect { request.authorize }.to change { Doorkeeper::AccessGrant.count }.by(1) expect(request.authorize).to be_a(Doorkeeper::OAuth::CodeResponse) expect(request.authorize.response_on_fragment).to be false end context "with 'fragment' as response_mode" do let(:response_mode) { "fragment" } it "returns a code response with response_on_fragment set to true" do expect(request.authorize.response_on_fragment).to be true end end end context "when pre_auth is denied" do it "does not create access grant and returns a error response" do expect { request.deny }.not_to(change { Doorkeeper::AccessGrant.count }) expect(request.deny).to be_a(Doorkeeper::OAuth::ErrorResponse) end end end ================================================ FILE: spec/lib/oauth/code_response_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::CodeResponse do let(:application) { FactoryBot.create(:application, scopes: "") } let(:owner) { FactoryBot.build_stubbed(:resource_owner) } let(:pre_auth) do double( :pre_auth, client: application, redirect_uri: "http://tst.com/cb", state: "state", scopes: Doorkeeper::OAuth::Scopes.from_string("public"), custom_access_token_attributes: {}, ) end describe "#body" do subject(:body) { described_class.new(pre_auth, auth).body } context "when auth object is for authorization code flow" do let(:auth) do Doorkeeper::OAuth::Authorization::Code.new(pre_auth, owner).tap(&:issue_token!) end before do allow(pre_auth).to receive(:code_challenge).and_return("code_challenge") allow(pre_auth).to receive(:code_challenge_method).and_return("plain") end it "return body response for authorization code" do expect(body).to eq({ code: auth.token.plaintext_token, state: pre_auth.state }) end end context "when auth object is for implicit grant flow" do let(:auth) do Doorkeeper::OAuth::Authorization::Token.new(pre_auth, owner).tap do |c| c.issue_token! allow(c.token).to receive(:expires_in_seconds).and_return(3600) end end it "return body response for access token" do expect(body).to eq( { access_token: auth.token.plaintext_token, token_type: auth.token.token_type, expires_in: auth.token.expires_in_seconds, state: pre_auth.state, }, ) end end end describe "#redirect_uri" do subject(:redirect_uri) do described_class.new(pre_auth, auth, response_on_fragment: response_on_fragment).redirect_uri end context "when generating the redirect URI for an authorization code grant" do let(:response_on_fragment) { false } let(:auth) do Doorkeeper::OAuth::Authorization::Code.new(pre_auth, owner).tap(&:issue_token!) end before do allow(pre_auth).to receive(:code_challenge).and_return("code_challenge") allow(pre_auth).to receive(:code_challenge_method).and_return("plain") end it "includes the authorization code was generated and state" do expect(redirect_uri).to eq("#{pre_auth.redirect_uri}?code=#{auth.token.plaintext_token}&state=#{pre_auth.state}") end end context "when generating the redirect URI for an implicit grant" do let(:response_on_fragment) { true } let(:auth) do Doorkeeper::OAuth::Authorization::Token.new(pre_auth, owner).tap do |c| c.issue_token! allow(c.token).to receive(:expires_in_seconds).and_return(3600) end end it "includes info of the token was generated and state" do expect(redirect_uri).to include("#{pre_auth.redirect_uri}#access_token=#{auth.token.plaintext_token}&" \ "token_type=#{auth.token.token_type}&expires_in=3600&state=#{pre_auth.state}") end end end end ================================================ FILE: spec/lib/oauth/error_response_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::ErrorResponse do describe "#status" do it "has a status of bad_request" do expect(described_class.new.status).to eq(:bad_request) end it "has a status of unauthorized for an invalid_client error" do subject = described_class.new(name: :invalid_client) expect(subject.status).to eq(:unauthorized) end it "has a status of unauthorized for an unauthorized_client error" do subject = described_class.new(name: :unauthorized_client) expect(subject.status).to eq(:unauthorized) end end describe ".from_request" do it "has the error from request" do error = described_class.from_request double(error: Doorkeeper::Errors::InvalidClient) expect(error.name).to eq(:invalid_client) end it "ignores state if request does not respond to state" do error = described_class.from_request double(error: Doorkeeper::Errors::InvalidClient) expect(error.state).to be_nil end it "has state if request responds to state" do error = described_class.from_request double(error: Doorkeeper::Errors::InvalidClient, state: :hello) expect(error.state).to eq(:hello) end it "supports old extensions" do error = described_class.from_request double(error: :invalid_client) expect(error.name).to eq(:invalid_client) expect { error.raise_exception! }.to raise_error(Doorkeeper::Errors::InvalidClient) end end it "ignores empty error values" do subject = described_class.new(error: Doorkeeper::Errors::InvalidClient, state: nil) expect(subject.body).not_to have_key(:state) end describe ".body" do subject(:body) { described_class.new(name: Doorkeeper::Errors::InvalidClient, state: :some_state).body } describe "#body" do it { expect(body).to have_key(:error) } it { expect(body).to have_key(:error_description) } it { expect(body).to have_key(:state) } end end describe ".headers" do subject(:headers) { error_response.headers } let(:error_response) { described_class.new(name: Doorkeeper::Errors::InvalidClient, state: :some_state) } it { expect(headers).to include "WWW-Authenticate" } describe "WWW-Authenticate header" do subject(:headers) { error_response.headers["WWW-Authenticate"] } it { expect(headers).to include("realm=\"#{error_response.send(:realm)}\"") } it { expect(headers).to include("error=\"#{error_response.name}\"") } it { expect(headers).to include("error_description=\"#{error_response.description}\"") } context "with error description containing forbidden characters (\\ or \")" do it "sanitize the value per RFC 6750 Section 3.1" do error = double(:error, name: "backslash\\", description:"\"quotes\"") allow(Doorkeeper::OAuth::Error).to receive(:new).and_return(error) expect(headers).to include("error=\"backslash_\"") expect(headers).to include("error_description=\"_quotes_\"") end end end end describe ".redirectable?" do it "not redirectable when error name is invalid_redirect_uri" do subject = described_class.new(name: :invalid_redirect_uri, redirect_uri: "https://example.com") expect(subject.redirectable?).to be false end it "not redirectable when error name is invalid_client" do subject = described_class.new(name: :invalid_client, redirect_uri: "https://example.com") expect(subject.redirectable?).to be false end it "not redirectable when error name is unauthorized_client" do subject = described_class.new(name: :unauthorized_client, redirect_uri: "https://example.com") expect(subject.redirectable?).to be false end it "not redirectable when redirect_uri is oob uri" do subject = described_class.new(name: :other_error, redirect_uri: Doorkeeper::OAuth::NonStandard::IETF_WG_OAUTH2_OOB) expect(subject.redirectable?).to be false end it "is redirectable when error is not related to client or redirect_uri, and redirect_uri is not oob uri" do subject = described_class.new(name: :other_error, redirect_uri: "https://example.com") expect(subject.redirectable?).to be true end end end ================================================ FILE: spec/lib/oauth/error_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::Error do subject(:error) { described_class.new(:some_error, :some_state, nil) } it { expect(error).to respond_to(:name) } it { expect(error).to respond_to(:state) } it { expect(error).to respond_to(:translate_options) } describe "#description" do it "is translated from translation messages" do expect(I18n).to receive(:translate).with( :some_error, scope: %i[doorkeeper errors messages], default: :server_error, ) error.description end context "when there are variables" do subject(:error) do described_class.new( :invalid_code_challenge_method, :some_state, { challenge_methods: "foo, bar", count: 2, } ) end it "is translated from translation messages with variables" do expect(error.description).to eq("The code_challenge_method must be one of foo, bar.") end end end end ================================================ FILE: spec/lib/oauth/forbidden_token_response_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::ForbiddenTokenResponse do subject(:response) { described_class.new } describe "#name" do it { expect(response.name).to eq(:insufficient_scope) } end describe "#status" do it { expect(response.status).to eq(:forbidden) } end describe "#headers" do subject(:response) { described_class.from_scopes(["public"]) } it "includes a WWW-Authenticate header per RFC 6750 Section 3.1" do www_authenticate = response.headers["WWW-Authenticate"] expect(www_authenticate).to include('error="insufficient_scope"') expect(www_authenticate).to include('error_description="Access to this resource requires scope _public_."') end end describe ".from_scopes" do subject(:response) { described_class.from_scopes(["public"]) } it "includes a list of acceptable scopes" do expect(response.description).to include("public") end it "explains that the problem is due to a missing scope" do expect(response.description).to match(/requires scope/i) end it "does not use the scope description from authorize page" do expect(response.description).not_to eql("Access your public data") end end end ================================================ FILE: spec/lib/oauth/helpers/scope_checker_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::Helpers::ScopeChecker do describe ".valid?" do let(:server_scopes) { Doorkeeper::OAuth::Scopes.new } it "is valid if scope is present" do server_scopes.add :scope expect(described_class.valid?(scope_str: "scope", server_scopes: server_scopes)).to be(true) end it "is invalid if includes tabs space" do expect(described_class.valid?(scope_str: "\tsomething", server_scopes: server_scopes)).to be(false) end it "is invalid if scope is not present" do expect(described_class.valid?(scope_str: nil, server_scopes: server_scopes)).to be(false) end it "is invalid if scope is blank" do expect(described_class.valid?(scope_str: " ", server_scopes: server_scopes)).to be(false) end it "is invalid if includes return space" do expect(described_class.valid?(scope_str: "scope\r", server_scopes: server_scopes)).to be(false) end it "is invalid if includes new lines" do expect(described_class.valid?(scope_str: "scope\nanother", server_scopes: server_scopes)).to be(false) end it "is invalid if any scope is not included in server scopes" do expect(described_class.valid?(scope_str: "scope another", server_scopes: server_scopes)).to be(false) end context "with application_scopes" do let(:server_scopes) { Doorkeeper::OAuth::Scopes.from_string "common svr" } let(:application_scopes) { Doorkeeper::OAuth::Scopes.from_string "app123" } it "is valid if scope is included in the application scope list" do expect(described_class.valid?(scope_str: "app123", server_scopes: server_scopes, app_scopes: application_scopes)) .to be(true) end it "is invalid if any scope is not included in the application" do expect(described_class.valid?(scope_str: "svr", server_scopes: server_scopes, app_scopes: application_scopes)) .to be(false) end end context "with grant_type" do let(:server_scopes) { Doorkeeper::OAuth::Scopes.from_string "scope1 scope2" } context "with scopes_by_grant_type not configured for grant_type" do it "is valid if the scope is in server scopes" do expect(described_class.valid?(scope_str: "scope1", server_scopes: server_scopes, grant_type: Doorkeeper::OAuth::PASSWORD)) .to be(true) end it "is invalid if the scope is not in server scopes" do expect(described_class.valid?(scope_str: "unknown", server_scopes: server_scopes, grant_type: Doorkeeper::OAuth::PASSWORD)) .to be(false) end end context "when scopes_by_grant_type configured for grant_type" do before do allow(Doorkeeper.configuration).to receive(:scopes_by_grant_type) .and_return(password: [:scope1]) end it "is valid if the scope is permitted for grant_type" do expect(described_class.valid?(scope_str: "scope1", server_scopes: server_scopes, grant_type: Doorkeeper::OAuth::PASSWORD)) .to be(true) end it "is invalid if the scope is permitted for grant_type" do expect(described_class.valid?(scope_str: "scope2", server_scopes: server_scopes, grant_type: Doorkeeper::OAuth::PASSWORD)) .to be(false) end end end end end ================================================ FILE: spec/lib/oauth/helpers/unique_token_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" module Doorkeeper::OAuth::Helpers describe UniqueToken do let :generator do ->(size) { "a" * size } end it "is able to customize the generator method" do token = described_class.generate(generator: generator) expect(token).to eq("a" * 32) end it "is able to customize the size of the token" do token = described_class.generate(generator: generator, size: 2) expect(token).to eq("aa") end end end ================================================ FILE: spec/lib/oauth/helpers/uri_checker_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" describe Doorkeeper::OAuth::Helpers::URIChecker do describe ".valid?" do it "is valid for valid uris" do uri = "http://app.co" expect(described_class).to be_valid(uri) end it "is valid if include path param" do uri = "http://app.co/path" expect(described_class).to be_valid(uri) end it "is valid if include query param" do uri = "http://app.co/?query=1" expect(described_class).to be_valid(uri) end it "is invalid if uri includes fragment" do uri = "http://app.co/test#fragment" expect(described_class).not_to be_valid(uri) end it "is invalid if scheme is missing" do uri = "app.co" expect(described_class).not_to be_valid(uri) end it "is invalid if is a relative uri" do uri = "/abc/123" expect(described_class).not_to be_valid(uri) end it "is invalid if is not a url" do uri = "http://" expect(described_class).not_to be_valid(uri) end it "is invalid if localhost is resolved as as scheme (no scheme specified)" do uri = "localhost:8080" expect(described_class).not_to be_valid(uri) end it "is invalid if scheme is missing #2" do uri = "app.co:80" expect(described_class).not_to be_valid(uri) end it "is invalid if is not an uri" do uri = " " expect(described_class).not_to be_valid(uri) end it "is valid for custom schemes" do uri = "com.example.app:/test" expect(described_class).to be_valid(uri) end it "is valid for custom schemes with authority marker (common misconfiguration)" do uri = "com.example.app://test" expect(described_class).to be_valid(uri) end end describe ".matches?" do it "is true if both url matches" do uri = client_uri = "http://app.co/aaa" expect(described_class).to be_matches(uri, client_uri) end it "allows additional query parameters" do uri = "http://app.co/?query=hello" client_uri = "http://app.co" expect(described_class).to be_matches(uri, client_uri) end it "doesn't allow non-matching domains through" do uri = "http://app.abc/?query=hello" client_uri = "http://app.co" expect(described_class).not_to be_matches(uri, client_uri) end it "doesn't allow non-matching domains that don't start at the beginning" do uri = "http://app.co/?query=hello" client_uri = "http://example.com?app.co=test" expect(described_class).not_to be_matches(uri, client_uri) end context "when loopback IP redirect URIs" do it "ignores port for same URIs" do uri = "http://127.0.0.1:5555/auth/callback" client_uri = "http://127.0.0.1:48599/auth/callback" expect(described_class).to be_matches(uri, client_uri) uri = "http://[::1]:5555/auth/callback" client_uri = "http://[::1]:5555/auth/callback" expect(described_class).to be_matches(uri, client_uri) end it "doesn't ignore port for URIs with different queries" do uri = "http://127.0.0.1:5555/auth/callback" client_uri = "http://127.0.0.1:48599/auth/callback2" expect(described_class).not_to be_matches(uri, client_uri) end end context "when client registered query params" do it "doesn't allow query being absent" do uri = "http://app.co" client_uri = "http://app.co/?vendorId=AJ4L7XXW9" expect(described_class).not_to be_matches(uri, client_uri) end it "is false if query values differ but key same" do uri = "http://app.co/?vendorId=pancakes" client_uri = "http://app.co/?vendorId=waffles" expect(described_class).not_to be_matches(uri, client_uri) end it "is false if query values same but key differs" do uri = "http://app.co/?foo=pancakes" client_uri = "http://app.co/?bar=pancakes" expect(described_class).not_to be_matches(uri, client_uri) end it "is false if query present and match, but unknown queries present" do uri = "http://app.co/?vendorId=pancakes&unknown=query" client_uri = "http://app.co/?vendorId=waffles" expect(described_class).not_to be_matches(uri, client_uri) end it "is true if queries are present and match" do uri = "http://app.co/?vendorId=AJ4L7XXW9&foo=bar" client_uri = "http://app.co/?vendorId=AJ4L7XXW9&foo=bar" expect(described_class).to be_matches(uri, client_uri) end it "is true if queries are present, match and in different order" do uri = "http://app.co/?bing=bang&foo=bar" client_uri = "http://app.co/?foo=bar&bing=bang" expect(described_class).to be_matches(uri, client_uri) end end end describe ".valid_for_authorization?" do it "is true if valid and matches" do uri = client_uri = "http://app.co/aaa" expect(described_class).to be_valid_for_authorization(uri, client_uri) uri = client_uri = "http://app.co/aaa?b=c" expect(described_class).to be_valid_for_authorization(uri, client_uri) end it "is true if uri includes blank query" do uri = client_uri = "http://app.co/aaa?" expect(described_class).to be_valid_for_authorization(uri, client_uri) uri = "http://app.co/aaa?" client_uri = "http://app.co/aaa" expect(described_class).to be_valid_for_authorization(uri, client_uri) uri = "http://app.co/aaa" client_uri = "http://app.co/aaa?" expect(described_class).to be_valid_for_authorization(uri, client_uri) end it "is false if valid and mismatches" do uri = "http://app.co/aaa" client_uri = "http://app.co/bbb" expect(described_class).not_to be_valid_for_authorization(uri, client_uri) end it "is true if valid and included in array" do uri = "http://app.co/aaa" client_uri = "http://example.com/bbb\nhttp://app.co/aaa" expect(described_class).to be_valid_for_authorization(uri, client_uri) end it "is false if valid and not included in array" do uri = "http://app.co/aaa" client_uri = "http://example.com/bbb\nhttp://app.co/cc" expect(described_class).not_to be_valid_for_authorization(uri, client_uri) end it "is false if queries does not match" do uri = "http://app.co/aaa?pankcakes=abc" client_uri = "http://app.co/aaa?waffles=abc" expect(described_class.valid_for_authorization?(uri, client_uri)).to be false end it "calls .matches?" do uri = "http://app.co/aaa?pankcakes=abc" client_uri = "http://app.co/aaa?waffles=abc" expect(described_class).to receive(:matches?).with(uri, client_uri).once described_class.valid_for_authorization?(uri, client_uri) end it "calls .valid?" do uri = "http://app.co/aaa?pankcakes=abc" client_uri = "http://app.co/aaa?waffles=abc" expect(described_class).to receive(:valid?).with(uri).once described_class.valid_for_authorization?(uri, client_uri) end end describe ".query_matches?" do it "is true if no queries" do expect(described_class).to be_query_matches("", "") expect(described_class).to be_query_matches(nil, nil) end it "is true if same query" do expect(described_class).to be_query_matches("foo", "foo") end it "is false if different query" do expect(described_class).not_to be_query_matches("foo", "bar") end it "is true if same queries" do expect(described_class).to be_query_matches("foo&bar", "foo&bar") end it "is true if same queries, different order" do expect(described_class).to be_query_matches("foo&bar", "bar&foo") end it "is false if one different query" do expect(described_class).not_to be_query_matches("foo&bang", "foo&bing") end it "is true if same query with same value" do expect(described_class).to be_query_matches("foo=bar", "foo=bar") end it "is true if same queries with same values" do expect(described_class).to be_query_matches("foo=bar&bing=bang", "foo=bar&bing=bang") end it "is true if same queries with same values, different order" do expect(described_class).to be_query_matches("foo=bar&bing=bang", "bing=bang&foo=bar") end it "is false if same query with different value" do expect(described_class).not_to be_query_matches("foo=bar", "foo=bang") end it "is false if some queries missing" do expect(described_class).not_to be_query_matches("foo=bar", "foo=bar&bing=bang") end it "is false if some queries different value" do expect(described_class).not_to be_query_matches("foo=bar&bing=bang", "foo=bar&bing=banana") end end describe ".loopback_uri?" do it "is true if loopback IP" do expect(described_class).to be_loopback_uri(URI.parse("http://127.0.0.1")) end it 'is false if not loopback IP' do expect(described_class).not_to be_loopback_uri(URI.parse("http://example.com")) end it 'is false for non URL' do expect(described_class).not_to be_loopback_uri(URI.parse("vscode://file/home/user/.vimrc")) end end end ================================================ FILE: spec/lib/oauth/invalid_request_response_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::InvalidRequestResponse do subject(:response) { described_class.new } describe "#name" do it { expect(response.name).to eq(:invalid_request) } end describe "#status" do it { expect(response.status).to eq(:bad_request) } end describe ".from_request" do let(:response) { described_class.from_request(request) } context "when param missed" do let(:request) { double(missing_param: "some_param") } it "sets a description" do expect(response.description).to eq( I18n.t(:missing_param, scope: %i[doorkeeper errors messages invalid_request], value: "some_param"), ) end it "sets the reason" do expect(response.reason).to eq(:missing_param) end end context "when request is not authorized" do let(:request) { double(invalid_request_reason: :request_not_authorized) } it "sets a description" do expect(response.description).to eq( I18n.t(:request_not_authorized, scope: %i[doorkeeper errors messages invalid_request]), ) end it "sets the reason" do expect(response.reason).to eq(:request_not_authorized) end end context "when unknown reason" do let(:request) { double(invalid_request_reason: :unknown_reason) } it "sets a description" do expect(response.description).to eq( I18n.t(:unknown, scope: %i[doorkeeper errors messages invalid_request]), ) end it "sets the reason to unknown" do expect(response.reason).to eq(:unknown_reason) end end end describe ".redirectable?" do it "not redirectable when missing_param is client_id" do subject = described_class.new(missing_param: :client_id) expect(subject.redirectable?).to be false end it "is redirectable when missing_param is other than client_id" do subject = described_class.new(missing_param: :code_verifier) expect(subject.redirectable?).to be true end end end ================================================ FILE: spec/lib/oauth/invalid_token_response_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::InvalidTokenResponse do let(:response) { described_class.new } describe "#name" do it { expect(response.name).to eq(:invalid_token) } end describe "#status" do it { expect(response.status).to eq(:unauthorized) } end describe ".from_access_token" do let(:response) { described_class.from_access_token(access_token) } context "when token revoked" do let(:access_token) { double(revoked?: true, expired?: true) } it "sets a description" do expect(response.description).to include("revoked") end it "sets the reason" do expect(response.reason).to eq(:revoked) end end context "when token expired" do let(:access_token) { double(revoked?: false, expired?: true) } it "sets a description" do expect(response.description).to include("expired") end it "sets the reason" do expect(response.reason).to eq(:expired) end end context "when unknown" do let(:access_token) { double(revoked?: false, expired?: false) } it "sets a description" do expect(response.description).to include("invalid") end it "sets the reason" do expect(response.reason).to eq(:unknown) end end end end ================================================ FILE: spec/lib/oauth/password_access_token_request_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::PasswordAccessTokenRequest do subject(:request) do described_class.new(server, client, credentials, owner) end let(:server) do double( :server, default_scopes: Doorkeeper::OAuth::Scopes.new, access_token_expires_in: 2.hours, refresh_token_enabled?: false, custom_access_token_expires_in: lambda { |context| context.grant_type == Doorkeeper::OAuth::PASSWORD ? 1234 : nil }, ) end let(:client) { Doorkeeper::OAuth::Client.new(FactoryBot.create(:application)) } let(:credentials) { Doorkeeper::OAuth::Client::Credentials.new("uid", "secret") } let(:application) { client.application } let(:owner) { FactoryBot.build_stubbed(:resource_owner) } before do allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) end it "issues a new token for the client" do expect do request.authorize end.to change { application.reload.access_tokens.count }.by(1) expect(application.reload.access_tokens.max_by(&:created_at).expires_in).to eq(1234) end it "doesn't issue a new token without client authentication" do request = described_class.new(server, nil, nil, owner) expect do request.authorize end.not_to(change { Doorkeeper::AccessToken.count }) expect(request.error).to eq(Doorkeeper::Errors::InvalidClient) end context "when skip_client_authentication_for_password_grant is true" do before do Doorkeeper.configure do orm DOORKEEPER_ORM skip_client_authentication_for_password_grant true end end it "issues a new token for the client without client authentication" do request = described_class.new(server, nil, nil, owner) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) expect(Doorkeeper::AccessToken.all.max_by(&:created_at).expires_in).to eq(1234) end end it "doesn't issue a new token with an invalid client" do request = described_class.new(server, nil, credentials, owner, { client_id: "bad_id" }) expect do request.authorize end.not_to(change { Doorkeeper::AccessToken.count }) expect(request.error).to eq(Doorkeeper::Errors::InvalidClient) end it "requires the owner" do request = described_class.new(server, client, credentials, nil) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end it "creates token even when there is already one (default)" do FactoryBot.create( :access_token, application_id: client.id, resource_owner_id: owner.id, resource_owner_type: owner.class.name, ) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) end it "skips token creation if there is already one reusable" do allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) FactoryBot.create( :access_token, application_id: client.id, resource_owner_id: owner.id, resource_owner_type: owner.class.name, ) expect do request.authorize end.not_to(change { Doorkeeper::AccessToken.count }) end it "creates token when there is already one but non reusable" do allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) FactoryBot.create( :access_token, application_id: client.id, resource_owner_id: owner.id, resource_owner_type: owner.class.name, ) allow_any_instance_of(Doorkeeper::AccessToken).to receive(:reusable?).and_return(false) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) end it "calls configured request callback methods" do expect(Doorkeeper.configuration.before_successful_strategy_response) .to receive(:call).with(request).once expect(Doorkeeper.configuration.after_successful_strategy_response) .to receive(:call).with(request, instance_of(Doorkeeper::OAuth::TokenResponse)).once request.authorize end describe "with scopes" do subject(:request) do described_class.new(server, client, credentials, owner, scope: "public") end context "when scopes_by_grant_type is not configured for grant_type" do it "returns error when scopes are invalid" do allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("another")) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidScope) end it "creates the token with scopes if scopes are valid" do allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public")) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) expect(Doorkeeper::AccessToken.last.scopes).to include("public") end end context "when scopes_by_grant_type is configured for grant_type" do it "returns error when scopes are valid but not permitted for grant_type" do allow(server) .to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public")) allow(Doorkeeper.configuration) .to receive(:scopes_by_grant_type).and_return(password: "another") request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidScope) end it "creates the token with scopes if scopes are valid and permitted for grant_type" do allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public")) allow(Doorkeeper.configuration) .to receive(:scopes_by_grant_type).and_return(password: [:public]) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) expect(Doorkeeper::AccessToken.last.scopes).to include("public") end end end describe "with custom expiry" do let(:server) do double( :server, default_scopes: Doorkeeper::OAuth::Scopes.new, access_token_expires_in: 2.hours, refresh_token_enabled?: false, custom_access_token_expires_in: lambda { |context| if context.scopes.exists?("public") 222 elsif context.scopes.exists?("magic") Float::INFINITY end }, ) end before do allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) end it "checks scopes" do request = described_class.new(server, client, credentials, owner, scope: "public") allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public")) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) expect(Doorkeeper::AccessToken.last.expires_in).to eq(222) end it "falls back to the default otherwise" do request = described_class.new(server, client, credentials, owner, scope: "private") allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("private")) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) expect(Doorkeeper::AccessToken.last.expires_in).to eq(2.hours) end end end ================================================ FILE: spec/lib/oauth/pre_authorization_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::PreAuthorization do subject(:pre_auth) do described_class.new(server, attributes) end let(:server) do server = Doorkeeper.configuration allow(server).to receive(:default_scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("default")) allow(server).to receive(:optional_scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public profile")) server end let(:application) { FactoryBot.create(:application, redirect_uri: "https://app.com/callback") } let(:client) { Doorkeeper::OAuth::Client.find(application.uid) } let(:attributes) do { client_id: client.uid, response_type: "code", redirect_uri: "https://app.com/callback", state: "save-this", current_resource_owner: Object.new, } end it "must call the validations on client and redirect_uri before other validations because they are not redirectable" do validation_attributes = described_class.validations.map { |validation| validation[:attribute] } expect(validation_attributes).to eq(%i[ client_id client client_supports_grant_flow resource_owner_authorize_for_client redirect_uri params response_type response_mode scopes code_challenge code_challenge_method ]) end it "is authorizable when request is valid" do expect(pre_auth).to be_authorizable end context "when using default grant flows" do it 'accepts "code" as response type' do attributes[:response_type] = "code" expect(pre_auth).to be_authorizable end it 'accepts "token" as response type' do allow(server).to receive(:grant_flows).and_return(["implicit"]) attributes[:response_type] = "token" expect(pre_auth).to be_authorizable end end context "when authorization code grant flow is disabled" do before do allow(server).to receive(:grant_flows).and_return(["implicit"]) end it 'does not accept "code" as response type' do attributes[:response_type] = "code" expect(pre_auth).not_to be_authorizable end end context "when implicit grant flow is disabled" do before do allow(server).to receive(:grant_flows).and_return(["authorization_code"]) end it 'does not accept "token" as response type' do attributes[:response_type] = "token" expect(pre_auth).not_to be_authorizable end end context "with response_mode parameter is provided" do context "when response_type is 'code'" do before { attributes[:response_type] = "code" } it "sets response_mode as 'query' when it is not provided" do attributes[:response_mode] = "" expect(pre_auth).to be_authorizable expect(pre_auth.response_mode).to eq("query") end it 'accepts "query" as response_mode' do attributes[:response_mode] = "query" expect(pre_auth).to be_authorizable end it 'accepts "fragment" as response_mode' do attributes[:response_mode] = "fragment" expect(pre_auth).to be_authorizable end it 'accepts "form_post" as response_mode' do attributes[:response_mode] = "form_post" expect(pre_auth).to be_authorizable end it "does not accept response_mode other than query, fragment, form_post" do attributes[:response_mode] = "other response_mode" expect(pre_auth).not_to be_authorizable end end context "when response_type is 'token'" do before do allow(server).to receive(:grant_flows).and_return(["implicit"]) attributes[:response_type] = "token" end it "sets response_mode as 'fragment' when it is not provided" do attributes[:response_mode] = "" expect(pre_auth).to be_authorizable expect(pre_auth.response_mode).to eq("fragment") end it 'accepts "fragment" as response_mode' do attributes[:response_mode] = "fragment" expect(pre_auth).to be_authorizable end it 'accepts "form_post" as response_mode' do attributes[:response_mode] = "form_post" expect(pre_auth).to be_authorizable end it 'does not accept "query" response_mode when response_type is "token"' do attributes[:response_mode] = "query" expect(pre_auth).not_to be_authorizable end end end context "when client application does not restrict valid scopes" do it "accepts valid scopes" do attributes[:scope] = "public" expect(pre_auth).to be_authorizable end it "rejects (globally) non-valid scopes" do attributes[:scope] = "invalid" expect(pre_auth).not_to be_authorizable end it "accepts scopes which are permitted for grant_type" do allow(server).to receive(:scopes_by_grant_type).and_return(authorization_code: [:public]) attributes[:scope] = "public" expect(pre_auth).to be_authorizable end it "rejects scopes which are not permitted for grant_type" do allow(server).to receive(:scopes_by_grant_type).and_return(authorization_code: [:profile]) attributes[:scope] = "public" expect(pre_auth).not_to be_authorizable end end context "when client application restricts valid scopes" do let(:application) do FactoryBot.create(:application, scopes: Doorkeeper::OAuth::Scopes.from_string("public nonsense")) end it "accepts valid scopes" do attributes[:scope] = "public" expect(pre_auth).to be_authorizable end it "rejects (globally) non-valid scopes" do attributes[:scope] = "invalid" expect(pre_auth).not_to be_authorizable end it "rejects (application level) non-valid scopes" do attributes[:scope] = "profile" expect(pre_auth).not_to be_authorizable end it "accepts scopes which are permitted for grant_type" do allow(server).to receive(:scopes_by_grant_type).and_return(authorization_code: [:public]) attributes[:scope] = "public" expect(pre_auth).to be_authorizable end it "rejects scopes which are not permitted for grant_type" do allow(server).to receive(:scopes_by_grant_type).and_return(authorization_code: [:profile]) attributes[:scope] = "public" expect(pre_auth).not_to be_authorizable end end context "when scope is not provided to pre_authorization" do before { attributes[:scope] = nil } context "when default scopes is provided" do it "uses default scopes" do allow(server).to receive(:default_scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("default_scope")) expect(pre_auth).to be_authorizable expect(pre_auth.scope).to eq("default_scope") expect(pre_auth.scopes).to eq(Doorkeeper::OAuth::Scopes.from_string("default_scope")) end end context "when default scopes is none" do it "not be authorizable when none default scope" do allow(server).to receive(:default_scopes).and_return(Doorkeeper::OAuth::Scopes.new) expect(pre_auth).not_to be_authorizable end end end it "matches the redirect uri against client's one" do attributes[:redirect_uri] = "http://nothesame.com" expect(pre_auth).not_to be_authorizable end it "stores the state" do expect(pre_auth.state).to eq("save-this") end it "rejects if response type is not allowed" do attributes[:response_type] = "whops" expect(pre_auth).not_to be_authorizable end it "requires an existing client" do attributes[:client_id] = nil expect(pre_auth).not_to be_authorizable end it "requires a redirect uri" do attributes[:redirect_uri] = nil expect(pre_auth).not_to be_authorizable end context "when resource_owner cannot access client application" do before { allow(Doorkeeper.configuration).to receive(:authorize_resource_owner_for_client).and_return(->(*_) { false }) } it "is not authorizable" do expect(pre_auth).not_to be_authorizable end end describe "as_json" do before { pre_auth.authorizable? } it { is_expected.to respond_to :as_json } shared_examples "returns the pre authorization" do it "returns the pre authorization" do expect(json[:client_id]).to eq client.uid expect(json[:redirect_uri]).to eq pre_auth.redirect_uri expect(json[:state]).to eq pre_auth.state expect(json[:response_type]).to eq pre_auth.response_type expect(json[:scope]).to eq pre_auth.scope expect(json[:client_name]).to eq client.name expect(json[:status]).to eq I18n.t("doorkeeper.pre_authorization.status") end end context "when called without params" do let(:json) { pre_auth.as_json } include_examples "returns the pre authorization" end context "when called with params" do let(:json) { pre_auth.as_json(foo: "bar") } include_examples "returns the pre authorization" end end describe "#form_post_response?" do it { is_expected.to respond_to(:form_post_response?) } it "return true when response_mode is form_post" do attributes[:response_mode] = "form_post" expect(pre_auth.form_post_response?).to be true end it "when response_mode is other than form_post" do attributes[:response_mode] = "fragment" expect(pre_auth.form_post_response?).to be false end end context "when using PKCE params" do context "when PKCE is supported" do before do allow(Doorkeeper::AccessGrant).to receive(:pkce_supported?).and_return(true) end it "accepts a blank code_challenge" do attributes[:code_challenge] = " " expect(pre_auth).to be_authorizable end it "accepts a code_challenge with a known code_challenge_method" do attributes[:code_challenge] = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" attributes[:code_challenge_method] = "plain" expect(pre_auth).to be_authorizable attributes[:code_challenge_method] = "S256" expect(pre_auth).to be_authorizable end it "rejects unknown values for code_challenge_method" do attributes[:code_challenge] = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" attributes[:code_challenge_method] = "unknown" expect(pre_auth).not_to be_authorizable end context "when pkce_code_challenge_methods is set to none" do before do Doorkeeper.configure do pkce_code_challenge_methods [] end end it "rejects plain as a code_challenge_method" do attributes[:code_challenge] = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" attributes[:code_challenge_method] = "plain" expect(pre_auth).to_not be_authorizable expect(pre_auth.error_response.description).to eq( "The authorization server does not support PKCE as there are no accepted code_challenge_method values." ) end end context "when pkce_code_challenge_methods is set to only S256" do before do Doorkeeper.configure do pkce_code_challenge_methods ["S256"] end end it "accepts S256 as a code_challenge_method" do attributes[:code_challenge] = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" attributes[:code_challenge_method] = "S256" expect(pre_auth).to be_authorizable end it "rejects plain as a code_challenge_method" do attributes[:code_challenge] = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" attributes[:code_challenge_method] = "plain" expect(pre_auth).to_not be_authorizable expect(pre_auth.error_response.description).to eq("The code_challenge_method must be S256.") end end it "rejects unknown as a code_challenge_method" do attributes[:code_challenge] = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" attributes[:code_challenge_method] = "unknown" expect(pre_auth).to_not be_authorizable expect(pre_auth.error_response.description).to eq("The code_challenge_method must be one of plain, S256.") end end context "when PKCE is not supported" do before do allow(Doorkeeper::AccessGrant).to receive(:pkce_supported?).and_return(false) end it "accepts unknown values for code_challenge_method" do attributes[:code_challenge] = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" attributes[:code_challenge_method] = "unknown" expect(pre_auth).to be_authorizable end end context "when force_pkce is enabled" do before do allow_any_instance_of(Doorkeeper::Config).to receive(:force_pkce?).and_return(true) end context "when the app is confidential" do before do application.update(confidential: true) end it "accepts a blank code_challenge" do attributes[:code_challenge] = " " expect(pre_auth).to be_authorizable end it "accepts a code challenge" do attributes[:code_challenge] = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" attributes[:code_challenge_method] = "plain" expect(pre_auth).to be_authorizable end end context "when the app is not confidential" do before do application.update(confidential: false) end it "does not accept a blank code_challenge" do attributes[:code_challenge] = " " expect(pre_auth).not_to be_authorizable expect(pre_auth.error_response.description).to eq(translated_invalid_request_error_message(:invalid_code_challenge, nil)) end it "accepts a code challenge" do attributes[:code_challenge] = "a45a9fea-0676-477e-95b1-a40f72ac3cfb" attributes[:code_challenge_method] = "plain" expect(pre_auth).to be_authorizable end end end end end ================================================ FILE: spec/lib/oauth/refresh_token_request_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::RefreshTokenRequest do subject(:request) { described_class.new(server, refresh_token, credentials) } let(:server) do double :server, access_token_expires_in: 2.minutes end let(:refresh_token) do FactoryBot.create(:access_token, use_refresh_token: true) end let(:client) { refresh_token.application } let(:credentials) { Doorkeeper::OAuth::Client::Credentials.new(client.uid, client.secret) } before do allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false) allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(false) end it "returns :grant_type as refresh_token" do expect(request.grant_type).to eq(Doorkeeper::OAuth::REFRESH_TOKEN) end it "issues a new token for the client" do expect { request.authorize }.to change { client.reload.access_tokens.count }.by(1) # #sort_by used for MongoDB ORM extensions for valid ordering expect(client.reload.access_tokens.max_by(&:created_at).expires_in).to eq(refresh_token.expires_in) end it "issues a new token for the client with the same expiry as of original token" do allow(server).to receive(:option_defined?).with(:custom_access_token_expires_in).and_return(true) allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false) described_class.new(server, refresh_token, credentials).authorize # #sort_by used for MongoDB ORM extensions for valid ordering expect(client.reload.access_tokens.max_by(&:created_at).expires_in).to eq(refresh_token.expires_in) end it "revokes the previous token" do expect { request.authorize }.to change(refresh_token, :revoked?).from(false).to(true) end it "calls configured request callback methods" do expect(Doorkeeper.configuration.before_successful_strategy_response) .to receive(:call).with(request).once expect(Doorkeeper.configuration.after_successful_strategy_response) .to receive(:call).with(request, instance_of(Doorkeeper::OAuth::TokenResponse)).once request.authorize end it "requires the refresh token" do request = described_class.new(server, nil, credentials) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidRequest) expect(request.missing_param).to eq(:refresh_token) end it "requires credentials to be valid if provided" do credentials = Doorkeeper::OAuth::Client::Credentials.new("invalid", "invalid") request = described_class.new(server, refresh_token, credentials) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidClient) end it "requires the token's client and current client to match" do other_app = FactoryBot.create(:application) credentials = Doorkeeper::OAuth::Client::Credentials.new(other_app.uid, other_app.secret) request = described_class.new(server, refresh_token, credentials) request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end it "rejects revoked tokens" do refresh_token.revoke request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidGrant) end it "accepts expired tokens" do refresh_token.expires_in = -1 refresh_token.save request.validate expect(request).to be_valid end context "when refresh token gets revoked between validation and authorization" do before do allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false) end it "raises InvalidGrantReuse error inside the lock block to prevent race condition" do # This test verifies that the InvalidGrantReuse check inside the lock block # properly detects when a token has been revoked by a concurrent request. # Set up the token to be revoked inside the lock allow(refresh_token).to receive(:with_lock) do |&block| # Mark token as revoked before executing the block allow(refresh_token).to receive(:revoked?).and_return(true) block.call end # Validation should pass (we haven't set up the mock yet) expect(request).to be_valid # Authorization should raise error when it checks revoked status inside lock expect { request.authorize }.to raise_error(Doorkeeper::Errors::InvalidGrantReuse) end end context "when refresh tokens expire on access token use" do before do allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(true) end it "issues a new token for the client" do expect { request.authorize }.to change { client.reload.access_tokens.count }.by(1) end it "does not revoke the previous token" do request.authorize expect(refresh_token).not_to be_revoked end it "sets the previous refresh token in the new access token" do request.authorize expect( # #sort_by used for MongoDB ORM extensions for valid ordering client.access_tokens.max_by(&:created_at).previous_refresh_token, ).to eq(refresh_token.refresh_token) end it "does not lock the previous token model" do expect(refresh_token).not_to receive(:lock!) request.authorize end end context "with clientless access tokens" do subject(:request) { described_class.new(server, refresh_token, nil) } let!(:refresh_token) { FactoryBot.create(:clientless_access_token, use_refresh_token: true) } it "issues a new token without a client" do expect { request.authorize }.to change { Doorkeeper::AccessToken.count }.by(1) end end context "with scopes" do subject(:request) { described_class.new(server, refresh_token, credentials, parameters) } let(:refresh_token) do FactoryBot.create :access_token, use_refresh_token: true, scopes: "public write" end let(:parameters) { {} } it "transfers scopes from the old token to the new token" do request.authorize expect(Doorkeeper::AccessToken.last.scopes).to eq(%i[public write]) end it "reduces scopes to the provided scopes" do parameters[:scopes] = "public" request.authorize expect(Doorkeeper::AccessToken.last.scopes).to eq(%i[public]) end it "validates that scopes are included in the original access token" do parameters[:scopes] = "public update" request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidScope) end it "uses params[:scope] in favor of scopes if present (valid)" do parameters[:scopes] = "public update" parameters[:scope] = "public" request.authorize expect(Doorkeeper::AccessToken.last.scopes).to eq(%i[public]) end it "uses params[:scope] in favor of scopes if present (invalid)" do parameters[:scopes] = "public" parameters[:scope] = "public update" request.validate expect(request.error).to eq(Doorkeeper::Errors::InvalidScope) end end context "with dynamic scopes enabled" do subject(:request) { described_class.new(server, refresh_token, credentials, parameters) } let(:application_scopes) { "public write user:*" } let(:application) { FactoryBot.create(:application, scopes: application_scopes) } let(:token_scopes) { "public write user:1" } let(:refresh_token) do FactoryBot.create :access_token, use_refresh_token: true, scopes: token_scopes, application: application end let(:parameters) { {} } before do Doorkeeper.configure do enable_dynamic_scopes end end it "transfers scopes from the old token to the new token" do request.authorize expect(Doorkeeper::AccessToken.last.scopes).to eq(%i[public write user:1]) end it "returns an error with invalid scope" do parameters[:scopes] = "public garbage:*" response = request.authorize expect(response).to be_a(Doorkeeper::OAuth::ErrorResponse) expect(response.status).to eq(:bad_request) end it "reduces scopes to the dynamic scope" do parameters[:scopes] = "user:1" request.authorize expect(Doorkeeper::AccessToken.last.scopes).to eq(%i[user:1]) end it "reduces scopes to the public scope" do parameters[:scopes] = "public" request.authorize expect(Doorkeeper::AccessToken.last.scopes).to eq(%i[public]) end end end ================================================ FILE: spec/lib/oauth/scopes_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::Scopes do subject(:scopes) { described_class.new } describe "#add" do it "allows you to add scopes with symbols" do scopes.add :public expect(scopes.all).to eq(["public"]) end it "allows you to add scopes with strings" do scopes.add "public" expect(scopes.all).to eq(["public"]) end it "do not add already included scopes" do scopes.add :public scopes.add :public expect(scopes.all).to eq(["public"]) end end describe "#exists" do before do scopes.add :public end it "returns true if scope with given name is present" do expect(scopes).to exist("public") end it "returns false if scope with given name does not exist" do expect(scopes).not_to exist("other") end it "handles symbols" do expect(scopes).to exist(:public) expect(scopes).not_to exist(:other) end end describe ".from_string" do subject(:scopes) { described_class.from_string(string) } let(:string) { "public write" } it { expect(scopes).to be_a(described_class) } describe "#all" do it "is an array of the expected scopes" do scopes_array = scopes.all expect(scopes_array.size).to eq(2) expect(scopes_array).to include("public") expect(scopes_array).to include("write") end end end describe "#+" do it "can add to another scope object" do scopes = described_class.from_string("public") + described_class.from_string("admin") expect(scopes.all).to eq(%w[public admin]) end it "does not change the existing object" do origin = described_class.from_string("public") expect(origin.to_s).to eq("public") end it "can add an array to a scope object" do scopes = described_class.from_string("public") + ["admin"] expect(scopes.all).to eq(%w[public admin]) end it "raises an error if cannot handle addition" do expect do described_class.from_string("public") + "admin" end.to raise_error(NoMethodError) end end describe "#&" do it "can get intersection with another scope object" do scopes = described_class.from_string("public admin") & described_class.from_string("write admin") expect(scopes.all).to eq(%w[admin]) end it "does not change the existing object" do origin = described_class.from_string("public admin") origin & described_class.from_string("write admin") expect(origin.to_s).to eq("public admin") end it "can get intersection with an array" do scopes = described_class.from_string("public admin") & %w[write admin] expect(scopes.all).to eq(%w[admin]) end end describe "#==" do it "is equal to another set of scopes" do expect(described_class.from_string("public")).to eq(described_class.from_string("public")) end it "is equal to another set of scopes with no particular order" do expect(described_class.from_string("public write")).to eq(described_class.from_string("write public")) end it "differs from another set of scopes when scopes are not the same" do expect(described_class.from_string("public write")).not_to eq(described_class.from_string("write")) end it "does not raise an error when compared to a non-enumerable object" do expect { described_class.from_string("public") == false }.not_to raise_error end end describe "#allowed" do it "can get intersection with another scope object" do scopes = described_class.from_string("public admin").allowed(described_class.from_string("write admin")) expect(scopes.all).to eq(%w[admin]) end end describe "#has_scopes?" do subject(:scopes) { described_class.from_string("public admin") } it "returns true when at least one scope is included" do expect(scopes).to have_scopes(described_class.from_string("public")) end it "returns true when all scopes are included" do expect(scopes).to have_scopes(described_class.from_string("public admin")) end it "is true if all scopes are included in any order" do expect(scopes).to have_scopes(described_class.from_string("admin public")) end it "is false if no scopes are included" do expect(scopes).not_to have_scopes(described_class.from_string("notexistent")) end it "returns false when any scope is not included" do expect(scopes).not_to have_scopes(described_class.from_string("public nope")) end it "is false if no scopes are included even for existing ones" do expect(scopes).not_to have_scopes(described_class.from_string("public admin notexistent")) end context "with dynamic scopes disabled" do context "with wildcard dynamic scope" do before do scopes.add "user:*" end it "returns false with specific user" do expect(scopes).not_to have_scopes(described_class.from_string("public user:1")) end it "returns true with wildcard user" do expect(scopes).to have_scopes(described_class.from_string("public user:*")) end it "returns false if requested scope missing parameter" do expect(scopes).not_to have_scopes(described_class.from_string("public user:")) end end end context "with dynamic scopes enabled" do before do Doorkeeper.configure do enable_dynamic_scopes end end context "with wildcard dynamic scope" do before do scopes.add "user:*" end it "returns true with specific user" do expect(scopes).to have_scopes(described_class.from_string("public user:1")) end it "returns true with wildcard user" do expect(scopes).to have_scopes(described_class.from_string("public user:*")) end it "returns false if requested scope missing parameter" do expect(scopes).not_to have_scopes(described_class.from_string("public user:")) end it "returns false if dynamic scope does not match" do expect(scopes).not_to have_scopes(described_class.from_string("public userA:1")) end describe "#&" do it "allows user:1 scope" do scopes = described_class.from_string("public user:*") & (described_class.from_string("public user:1")) expect(scopes.all).to eq(%w[public user:1]) end it "does not allow user:2 scope" do scopes = described_class.from_string("public user:1") & (described_class.from_string("public user:2")) expect(scopes.all).to eq(%w[public]) end it "does not allow user:* scope" do scopes = described_class.from_string("public user:1") & (described_class.from_string("public user:*")) expect(scopes.all).to eq(%w[public]) end end describe "#allowed" do it "allows user:1 scope" do scopes = described_class.from_string("public user:*").allowed(described_class.from_string("public user:1")) expect(scopes.all).to eq(%w[public user:1]) end it "does not allow user:2 scope" do scopes = described_class.from_string("public user:1").allowed(described_class.from_string("public user:2")) expect(scopes.all).to eq(%w[public]) end it "does not allow user:* scope" do scopes = described_class.from_string("public user:1").allowed(described_class.from_string("public user:*")) expect(scopes.all).to eq(%w[public]) end end end context "with specific dynamic scope" do before do scopes.add "user:1" end it "returns true with specific user" do expect(scopes).to have_scopes(described_class.from_string("public user:1")) end it "returns false with wildcard user" do expect(scopes).not_to have_scopes(described_class.from_string("public user:*")) end it "returns false for disallowed user" do expect(scopes).not_to have_scopes(described_class.from_string("public user:2")) end context "with custom delimiter" do before do Doorkeeper.configure do enable_dynamic_scopes(delimiter: "-") end scopes.add "user-1" end it "returns true with specific user" do expect(scopes).to have_scopes(described_class.from_string("public user-1")) end it "returns false with wildcard user" do expect(scopes).not_to have_scopes(described_class.from_string("public user-*")) end it "returns false for disallowed user" do expect(scopes).not_to have_scopes(described_class.from_string("public user-2")) end end end end end end ================================================ FILE: spec/lib/oauth/token_request_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::TokenRequest do subject(:request) do described_class.new(pre_auth, owner) end let(:application) do FactoryBot.create(:application, scopes: "public") end let(:pre_auth) do server = Doorkeeper.config allow(server).to receive(:default_scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public")) allow(server).to receive(:grant_flows).and_return(Doorkeeper::OAuth::Scopes.from_string("implicit")) client = Doorkeeper::OAuth::Client.new(application) attributes = { client_id: client.uid, response_type: "token", redirect_uri: "https://app.com/callback", } pre_auth = Doorkeeper::OAuth::PreAuthorization.new(server, attributes) pre_auth.authorizable? pre_auth end let(:owner) do FactoryBot.create(:doorkeeper_testing_user, name: "John") end it "creates an access token" do expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) end it "returns a code response" do expect(request.authorize).to be_a(Doorkeeper::OAuth::CodeResponse) end context "when pre_auth is denied" do it "does not create token and returns a error response" do expect { request.deny }.not_to(change { Doorkeeper::AccessToken.count }) expect(request.deny).to be_a(Doorkeeper::OAuth::ErrorResponse) end end describe "with custom expiration" do context "when proper TTL returned" do before do Doorkeeper.configure do orm DOORKEEPER_ORM custom_access_token_expires_in do |context| context.grant_type == Doorkeeper::OAuth::IMPLICIT ? 1234 : nil end end end it "uses the custom ttl" do request.authorize token = Doorkeeper::AccessToken.first expect(token.expires_in).to eq(1234) end end context "when nil TTL returned" do before do Doorkeeper.configure do orm DOORKEEPER_ORM access_token_expires_in 654 custom_access_token_expires_in do |_context| nil end end end it "fallbacks to access_token_expires_in" do request.authorize token = Doorkeeper::AccessToken.first expect(token.expires_in).to eq(654) end end context "when infinite TTL returned" do before do Doorkeeper.configure do orm DOORKEEPER_ORM access_token_expires_in 654 custom_access_token_expires_in do |_context| Float::INFINITY end end end it "fallbacks to access_token_expires_in" do request.authorize token = Doorkeeper::AccessToken.first expect(token.expires_in).to be_nil end end context "when custom_access_token_expires_in uses resource_owner condition" do before do Doorkeeper.configure do orm DOORKEEPER_ORM custom_access_token_expires_in do |context| if context.resource_owner&.name == "John" 10_000 else 500 end end end end it "uses configured values for TTL" do request = described_class.new(pre_auth, owner) request.authorize token = Doorkeeper::AccessToken.last expect(token.expires_in).to eq(10_000) request = described_class.new(pre_auth, nil) request.authorize token = Doorkeeper::AccessToken.last expect(token.expires_in).to eq(500) end end end context "when reuse_access_token enabled" do it "creates a new token if there are no matching tokens" do allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) end it "creates a new token if scopes do not match" do allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) FactoryBot.create( :access_token, application_id: pre_auth.client.id, resource_owner_id: owner.id, resource_owner_type: owner.class.name, scopes: "", ) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) end it "skips token creation if there is a matching one reusable" do allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) allow(application.scopes).to receive(:has_scopes?).and_return(true) allow(application.scopes).to receive(:all?).and_return(true) FactoryBot.create( :access_token, application_id: pre_auth.client.id, resource_owner_id: owner.id, resource_owner_type: owner.class.name, scopes: "public", ) expect { request.authorize }.not_to(change { Doorkeeper::AccessToken.count }) end it "creates new token if there is a matching one but non reusable" do allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) allow(application.scopes).to receive(:has_scopes?).and_return(true) allow(application.scopes).to receive(:all?).and_return(true) FactoryBot.create( :access_token, application_id: pre_auth.client.id, resource_owner_id: owner.id, resource_owner_type: owner.class.name, scopes: "public", ) allow_any_instance_of(Doorkeeper::AccessToken).to receive(:reusable?).and_return(false) expect do request.authorize end.to change { Doorkeeper::AccessToken.count }.by(1) end end end ================================================ FILE: spec/lib/oauth/token_response_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::OAuth::TokenResponse do subject(:response) { described_class.new(double.as_null_object) } it "includes access token response headers" do headers = response.headers expect(headers.fetch("Cache-Control")).to eq("no-store, no-cache") expect(headers.fetch("Pragma")).to eq("no-cache") end it "status is ok" do expect(response.status).to eq(:ok) end describe ".body" do subject(:body) { described_class.new(access_token).body } let(:access_token) do double :access_token, plaintext_token: "some-token", expires_in: "3600", expires_in_seconds: "300", scopes_string: "two scopes", plaintext_refresh_token: "some-refresh-token", token_type: "Bearer", created_at: 0 end it "includes :access_token" do expect(body["access_token"]).to eq("some-token") end it "includes :token_type" do expect(body["token_type"]).to eq("Bearer") end # expires_in_seconds is returned as `expires_in` in order to match # the OAuth spec (section 4.2.2) it "includes :expires_in" do expect(body["expires_in"]).to eq("300") end it "includes :scope" do expect(body["scope"]).to eq("two scopes") end it "includes :refresh_token" do expect(body["refresh_token"]).to eq("some-refresh-token") end it "includes :created_at" do expect(body["created_at"]).to eq(0) end end describe ".body attributes" do subject(:token_response) { described_class.new(access_token) } let(:access_token) do double :access_token, plaintext_token: "some-token", expires_in: "3600", expires_in_seconds: "300", scopes_string: "two scopes", plaintext_refresh_token: "some-refresh-token", token_type: "Bearer", custom_parameter: "custom_value", created_at: 0 end it "can be augmented" do token_response.body["custom_parameter"] = access_token.custom_parameter expect(token_response.body["custom_parameter"]).to eq("custom_value") end end describe ".body filters out empty values" do subject(:body) { described_class.new(access_token).body } let(:access_token) do double :access_token, plaintext_token: "some-token", expires_in_seconds: "", scopes_string: "", plaintext_refresh_token: "", token_type: "Bearer", created_at: 0 end it "includes :expires_in" do expect(body["expires_in"]).to be_nil end it "includes :scope" do expect(body["scope"]).to be_nil end it "includes :refresh_token" do expect(body["refresh_token"]).to be_nil end end end ================================================ FILE: spec/lib/oauth/token_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" module Doorkeeper unless defined?(AccessToken) class AccessToken end end end RSpec.describe Doorkeeper::OAuth::Token do describe ".from_request" do let(:request) { double.as_null_object } let(:method) do ->(*) { "token-value" } end it "accepts anything that responds to #call" do expect(method).to receive(:call).with(request) described_class.from_request request, method end it "delegates methods received as symbols to described_class class" do expect(described_class).to receive(:from_params).with(request) described_class.from_request request, :from_params end it "stops at the first credentials found" do not_called_method = double expect(not_called_method).not_to receive(:call) described_class.from_request request, ->(_r) {}, method, not_called_method end it "returns the credential from extractor method" do credentials = described_class.from_request request, method expect(credentials).to eq("token-value") end end describe ".from_access_token_param" do it "returns token from access_token parameter" do request = double parameters: { access_token: "some-token" } token = described_class.from_access_token_param(request) expect(token).to eq("some-token") end end describe ".from_bearer_param" do it "returns token from bearer_token parameter" do request = double parameters: { bearer_token: "some-token" } token = described_class.from_bearer_param(request) expect(token).to eq("some-token") end end describe ".from_bearer_authorization" do it "returns token from capitalized authorization bearer" do request = double authorization: "Bearer SomeToken" token = described_class.from_bearer_authorization(request) expect(token).to eq("SomeToken") end it "returns token from lowercased authorization bearer" do request = double authorization: "bearer SomeToken" token = described_class.from_bearer_authorization(request) expect(token).to eq("SomeToken") end it "does not return token if authorization is not bearer" do request = double authorization: "MAC SomeToken" token = described_class.from_bearer_authorization(request) expect(token).to be_blank end end describe ".from_basic_authorization" do it "returns token from capitalized authorization basic" do request = double authorization: "Basic #{Base64.encode64 "SomeToken:"}" token = described_class.from_basic_authorization(request) expect(token).to eq("SomeToken") end it "returns token from lowercased authorization basic" do request = double authorization: "basic #{Base64.encode64 "SomeToken:"}" token = described_class.from_basic_authorization(request) expect(token).to eq("SomeToken") end it "does not return token if authorization is not basic" do request = double authorization: "MAC #{Base64.encode64 "SomeToken:"}" token = described_class.from_basic_authorization(request) expect(token).to be_blank end end describe ".authenticate" do context "when refresh tokens are disabled (default)" do context "when refresh tokens are enabled" do it "does not revoke previous refresh_token if token was found" do token = ->(_r) { "token" } expect( Doorkeeper::AccessToken, ).to receive(:by_token).with("token").and_return(token) expect(token).not_to receive(:revoke_previous_refresh_token!) described_class.authenticate double, token end end it "calls the finder if token was returned" do token = ->(_r) { "token" } expect(Doorkeeper::AccessToken).to receive(:by_token).with("token") described_class.authenticate double, token end end context "when token hashing is enabled" do include_context "with token hashing enabled" let(:hashed_token) { hashed_or_plain_token_func.call("token") } let(:token) { ->(_r) { "token" } } it "searches with the hashed token" do expect( Doorkeeper::AccessToken, ).to receive(:find_by).with(token: hashed_token).and_return(token) described_class.authenticate double, token end end context "when refresh tokens are enabled" do before do Doorkeeper.configure do orm DOORKEEPER_ORM use_refresh_token end end it "revokes previous refresh_token if token was found" do token = ->(_r) { "token" } expect( Doorkeeper::AccessToken, ).to receive(:by_token).with("token").and_return(token) expect(token).to receive(:revoke_previous_refresh_token!) described_class.authenticate double, token end it "calls the finder if token was returned" do token = ->(_r) { "token" } expect(Doorkeeper::AccessToken).to receive(:by_token).with("token") described_class.authenticate double, token end end end end ================================================ FILE: spec/lib/option_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Config::Option do class Extension def self.configure(&block) @config = Config::Builder.new(Config.new, &block).build end def self.configuration @config || (raise Errors::MissingConfiguration) end class Config class Builder < Doorkeeper::Config::AbstractBuilder def enforce_something @config.instance_variable_set(:@enforce_something, true) end end def enforce_something? if defined?(@enforce_something) @enforce_something else false end end def self.builder_class Config::Builder end extend Doorkeeper::Config::Option end end it "allows to define custom options in extensions" do expect do Extension::Config.option(:some_option, default: 1) end.not_to raise_error Extension.configure do some_option 20 enforce_something end expect(Extension.configuration.some_option).to eq(20) expect(Extension.configuration.enforce_something?).to be(true) end end ================================================ FILE: spec/lib/request/strategy_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::Request::Strategy do subject(:strategy) { described_class.new(server) } let(:server) { double } describe "#initialize" do it "sets the server attribute" do expect(strategy.server).to eq server end end describe "#request" do it "requires an implementation" do expect { strategy.request }.to raise_exception NotImplementedError end end describe "a sample Strategy subclass" do subject(:strategy) { strategy_class.new(server) } let(:fake_request) { double } let(:strategy_class) do subclass = Class.new(described_class) do class << self attr_accessor :fake_request end def request self.class.fake_request end end subclass.fake_request = fake_request subclass end it "provides a request implementation" do expect(strategy.request).to eq fake_request end it "authorizes the request" do expect(fake_request).to receive :authorize strategy.authorize end end end ================================================ FILE: spec/lib/secret_storing/base_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe ::Doorkeeper::SecretStoring::Base do let(:instance) { double("instance", token: "foo") } describe "#transform_secret" do it "raises" do expect { described_class.transform_secret("foo") } .to raise_error(NotImplementedError) end end describe "#store_secret" do it "sends to response of #transform_secret to the instance" do expect(described_class) .to receive(:transform_secret).with("bar") .and_return "bar+transform" expect(instance).to receive(:token=).with "bar+transform" result = described_class.store_secret instance, :token, "bar" expect(result).to eq "bar+transform" end end describe "#restore_secret" do it "raises" do expect { described_class.restore_secret(described_class, :token) } .to raise_error(NotImplementedError) end end describe "#allows_restoring_secrets?" do it "does not allow it" do expect(described_class.allows_restoring_secrets?).to eq false end end describe "validate_for" do it "allows for valid model" do expect(described_class.validate_for(:application)).to eq true expect(described_class.validate_for(:token)).to eq true end it "raises for invalid model" do expect { described_class.validate_for(:wat) } .to raise_error(ArgumentError, /can not be used for wat/) end end describe "secret_matches?" do before do allow(described_class).to receive(:transform_secret) { |input| "transformed: #{input}" } end it "compares input with #transform_secret" do expect(described_class.secret_matches?("input", "input")).to eq false expect(described_class.secret_matches?("a", "transformed: a")).to eq true end end end ================================================ FILE: spec/lib/secret_storing/bcrypt_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "bcrypt" RSpec.describe ::Doorkeeper::SecretStoring::BCrypt do let(:instance) { double("instance", token: "foo") } describe "#transform_secret" do it "creates a bcrypt password" do expect(described_class.transform_secret("foo")).to be_a BCrypt::Password end end describe "#restore_secret" do it "raises" do expect { described_class.restore_secret(instance, :token) } .to raise_error(NotImplementedError) end end describe "#allows_restoring_secrets?" do it "does not allow it" do expect(described_class.allows_restoring_secrets?).to be(false) end end describe "validate_for" do it "allows for valid model" do expect(described_class.validate_for(:application)).to eq(true) end it "raises for invalid model" do expect { described_class.validate_for(:wat) } .to raise_error(ArgumentError, /can only be used for storing application secrets/) expect { described_class.validate_for(:token) } .to raise_error(ArgumentError, /can only be used for storing application secrets/) end end describe "secret_matches?" do it "compares input with #transform_secret" do expect(described_class.secret_matches?("input", "input")).to eq(false) password = BCrypt::Password.create("foobar") expect(described_class.secret_matches?("foobar", password.to_s)).to eq(true) end end end ================================================ FILE: spec/lib/secret_storing/plain_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe ::Doorkeeper::SecretStoring::Plain do let(:instance) { double("instance", token: "foo") } describe "#transform_secret" do it "raises" do expect(described_class.transform_secret("foo")).to eq "foo" end end describe "#restore_secret" do it "raises" do expect(described_class.restore_secret(instance, :token)).to eq "foo" end end describe "#allows_restoring_secrets?" do it "does allow it" do expect(described_class.allows_restoring_secrets?).to eq true end end describe "validate_for" do it "allows for valid model" do expect(described_class.validate_for(:application)).to eq true expect(described_class.validate_for(:token)).to eq true end it "raises for invalid model" do expect { described_class.validate_for(:wat) } .to raise_error(ArgumentError, /can not be used for wat/) end end describe "secret_matches?" do it "compares input with #transform_secret" do expect(described_class.secret_matches?("input", "input")).to eq true expect(described_class.secret_matches?("a", "b")).to eq false end end end ================================================ FILE: spec/lib/secret_storing/sha256_hash_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe ::Doorkeeper::SecretStoring::Sha256Hash do let(:instance) { double("instance") } let(:hash_function) do ->(input) { ::Digest::SHA256.hexdigest(input) } end describe "#transform_secret" do it "raises" do expect(described_class.transform_secret("foo")).to eq hash_function.call("foo") end end describe "#restore_secret" do it "raises" do expect { described_class.restore_secret(instance, :token) }.to raise_error(NotImplementedError) end end describe "#allows_restoring_secrets?" do it "does not allow it" do expect(described_class.allows_restoring_secrets?).to eq false end end describe "validate_for" do it "allows for valid model" do expect(described_class.validate_for(:application)).to eq true expect(described_class.validate_for(:token)).to eq true end it "raises for invalid model" do expect { described_class.validate_for(:wat) }.to raise_error(ArgumentError, /can not be used for wat/) end end describe "secret_matches?" do it "compares input with #transform_secret" do expect(described_class.secret_matches?("input", "input")).to eq false expect(described_class.secret_matches?("a", hash_function.call("a"))).to eq true end end end ================================================ FILE: spec/models/doorkeeper/access_grant_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::AccessGrant do subject(:access_grant) do FactoryBot.build( :access_grant, application: client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, ) end let(:resource_owner) { FactoryBot.build_stubbed(:resource_owner) } let(:client) { FactoryBot.build_stubbed(:application) } it { expect(access_grant).to be_valid } it_behaves_like "an accessible token" it_behaves_like "a revocable token" it_behaves_like "a unique token" do let(:factory_name) { :access_grant } end context "with hashing enabled" do let(:grant) do FactoryBot.create :access_grant, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end include_context "with token hashing enabled" it "holds a volatile plaintext token when created" do expect(grant.plaintext_token).to be_a(String) expect(grant.token) .to eq(hashed_or_plain_token_func.call(grant.plaintext_token)) # Finder method only finds the hashed token loaded = described_class.find_by(token: grant.token) expect(loaded).to eq(grant) expect(loaded.plaintext_token).to be_nil expect(loaded.token).to eq(grant.token) end it "does not find_by plain text tokens" do expect(described_class.find_by(token: grant.plaintext_token)).to be_nil end describe "with having a plain text token" do let(:plain_text_token) { "plain text token" } before do # Assume we have a plain text token from before activating the option grant.update_column(:token, plain_text_token) end context "without fallback lookup" do it "does not provide lookups with either through by_token" do expect(described_class.by_token(plain_text_token)).to eq(nil) expect(described_class.by_token(grant.token)).to eq(nil) # And it does not touch the token grant.reload expect(grant.token).to eq(plain_text_token) end end context "with fallback lookup" do include_context "with token hashing and fallback lookup enabled" it "upgrades a plain token when falling back to it" do # Side-effect: This will automatically upgrade the token expect(described_class).to receive(:upgrade_fallback_value).and_call_original expect(described_class.by_token(plain_text_token)) .to have_attributes( resource_owner_id: grant.resource_owner_id, application_id: grant.application_id, redirect_uri: grant.redirect_uri, expires_in: grant.expires_in, scopes: grant.scopes, ) # Will find subsequently by hashing the token expect(described_class.by_token(plain_text_token)) .to have_attributes( resource_owner_id: grant.resource_owner_id, application_id: grant.application_id, redirect_uri: grant.redirect_uri, expires_in: grant.expires_in, scopes: grant.scopes, ) # Not all the ORM support :id PK if grant.respond_to?(:id) expect(described_class.by_token(plain_text_token).id).to eq(grant.id) end # And it modifies the token value grant.reload expect(grant.token).not_to eq(plain_text_token) expect(described_class.find_by(token: plain_text_token)).to eq(nil) expect(described_class.find_by(token: grant.token)).not_to be_nil end end end end describe "validations" do it "is invalid without resource_owner_id" do access_grant.resource_owner_id = nil expect(access_grant).not_to be_valid end it "is invalid without application_id" do access_grant.application_id = nil expect(access_grant).not_to be_valid end it "is invalid without token" do access_grant.save access_grant.token = nil expect(access_grant).not_to be_valid end it "is invalid without expires_in" do access_grant.expires_in = nil expect(access_grant).not_to be_valid end end describe ".revoke_all_for" do let(:application) { FactoryBot.create :application } let(:default_attributes) do { application: application, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, } end it "revokes all tokens for given application and resource owner" do FactoryBot.create :access_grant, default_attributes described_class.revoke_all_for(application.id, resource_owner) expect(described_class.all).to all(be_revoked) end it "matches application" do access_grant_for_different_app = FactoryBot.create( :access_grant, default_attributes.merge(application: FactoryBot.create(:application)), ) described_class.revoke_all_for(application.id, resource_owner) expect(access_grant_for_different_app.reload).not_to be_revoked end it "matches resource owner" do other_resource_owner = FactoryBot.create(:resource_owner) access_grant_for_different_owner = FactoryBot.create( :access_grant, default_attributes.merge(resource_owner_id: other_resource_owner.id), ) described_class.revoke_all_for(application.id, resource_owner) expect(access_grant_for_different_owner.reload).not_to be_revoked end end describe ".revoke_all_for with read replica support" do let(:application) { FactoryBot.create(:application) } let(:resource_owner) { FactoryBot.create(:resource_owner) } before do FactoryBot.create(:access_grant, application: application, resource_owner_id: resource_owner.id) end context "when enable_multiple_database_roles is enabled" do before do Doorkeeper.configure do orm :active_record enable_multiple_database_roles end end it "revokes grants using primary database role" do expect(ActiveRecord::Base).to receive(:connected_to).with(role: :writing).and_call_original described_class.revoke_all_for(application.id, resource_owner) end end end end ================================================ FILE: spec/models/doorkeeper/access_token_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe Doorkeeper::AccessToken do subject(:access_token) { FactoryBot.build(:access_token) } it { expect(access_token).to be_valid } it_behaves_like "an accessible token" it_behaves_like "a revocable token" it_behaves_like "a unique token" do let(:factory_name) { :access_token } end module CustomGeneratorArgs def self.generate; end end describe "#generate_token" do it "generates a token using the default method" do FactoryBot.create :access_token token = FactoryBot.create :access_token expect(token.token).to be_a(String) end context "with hashing enabled" do let(:token) { FactoryBot.create :access_token } include_context "with token hashing enabled" it "holds a volatile plaintext token when created" do expect(token.plaintext_token).to be_a(String) expect(token.token) .to eq(hashed_or_plain_token_func.call(token.plaintext_token)) # Finder method only finds the hashed token loaded = described_class.find_by(token: token.token) expect(loaded).to eq(token) expect(loaded.plaintext_token).to be_nil expect(loaded.token).to eq(token.token) end it "does not find_by plain text tokens" do expect(described_class.find_by(token: token.plaintext_token)).to be_nil end describe "with having a plain text token" do let(:plain_text_token) { "plain text token" } let(:access_token) { FactoryBot.create :access_token } before do # Assume we have a plain text token from before activating the option access_token.update_column(:token, plain_text_token) end context "without fallback lookup" do it "does not provide lookups with either through by_token" do expect(described_class.by_token(plain_text_token)).to eq(nil) expect(described_class.by_token(access_token.token)).to eq(nil) # And it does not touch the token access_token.reload expect(access_token.token).to eq(plain_text_token) end end context "with fallback lookup" do include_context "with token hashing and fallback lookup enabled" it "upgrades a plain token when falling back to it" do # Side-effect: This will automatically upgrade the token expect(described_class).to receive(:upgrade_fallback_value).and_call_original expect(described_class.by_token(plain_text_token)) .to have_attributes( resource_owner_id: access_token.resource_owner_id, application_id: access_token.application_id, scopes: access_token.scopes, ) # Will find subsequently by hashing the token expect(described_class.by_token(plain_text_token)) .to have_attributes( resource_owner_id: access_token.resource_owner_id, application_id: access_token.application_id, scopes: access_token.scopes, ) # Not all the ORM support :id PK if access_token.respond_to?(:id) expect(described_class.by_token(plain_text_token).id).to eq(access_token.id) end # And it modifies the token value access_token.reload expect(access_token.token).not_to eq(plain_text_token) expect(described_class.find_by(token: plain_text_token)).to eq(nil) expect(described_class.find_by(token: access_token.token)).not_to be_nil end end end end it "generates a token using a custom object" do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(opts = {}) id = opts[:resource_owner_id] || opts[:resource_owner]&.id "custom_generator_token_#{id}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "CustomGeneratorArgs" end owner = FactoryBot.create :resource_owner token = FactoryBot.create :access_token, resource_owner_id: owner.id, resource_owner_type: owner.class.name expect(token.token).to match(/custom_generator_token_\d+/) end it "allows the custom generator to access the application details" do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:application].name}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "CustomGeneratorArgs" end token = FactoryBot.create :access_token expect(token.token).to match(/custom_generator_token_Application \d+/) end it "allows the custom generator to access the scopes" do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:scopes].count}_#{opts[:scopes]}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "CustomGeneratorArgs" end token = FactoryBot.create :access_token, scopes: "public write" expect(token.token).to eq "custom_generator_token_2_public write" end it "allows the custom generator to access the expiry length" do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:expires_in]}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "CustomGeneratorArgs" end token = FactoryBot.create :access_token expect(token.token).to eq "custom_generator_token_7200" end it "allows the custom generator to access the created time" do module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:created_at].to_i}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "CustomGeneratorArgs" end token = FactoryBot.create :access_token created_at = token.created_at expect(token.token).to eq "custom_generator_token_#{created_at.to_i}" end it "allows the custom generator to access the custom attributes" do module CustomGeneratorArgs def self.generate(opts = {}) "custom_generator_token_#{opts[:tenant_name]}" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "CustomGeneratorArgs" custom_access_token_attributes [:tenant_name] end token = FactoryBot.create :access_token, tenant_name: "Tenant 1" expect(token.token).to eq "custom_generator_token_Tenant 1" end it "raises an error if the custom object does not support generate" do module NoGenerate end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "NoGenerate" end expect { FactoryBot.create :access_token }.to( raise_error(Doorkeeper::Errors::UnableToGenerateToken), ) end it "raises original error if something went wrong in custom generator" do eigenclass = class << CustomGeneratorArgs; self; end eigenclass.class_eval do remove_method :generate end module CustomGeneratorArgs def self.generate(_opts = {}) raise LoadError, "custom behaviour" end end Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "CustomGeneratorArgs" end expect { FactoryBot.create :access_token }.to( raise_error(LoadError), ) end it "raises an error if the custom object does not exist" do Doorkeeper.configure do orm DOORKEEPER_ORM access_token_generator "Doorkeeper::NotReal" end expect { FactoryBot.create :access_token }.to( raise_error(Doorkeeper::Errors::TokenGeneratorNotFound, /NotReal/), ) end end describe "refresh_token" do it "has empty refresh token if it was not required" do token = FactoryBot.create :access_token expect(token.refresh_token).to be_nil end it "generates a refresh token if it was requested" do token = FactoryBot.create :access_token, use_refresh_token: true expect(token.refresh_token).not_to be_nil end it "is not valid if token exists" do token1 = FactoryBot.create :access_token, use_refresh_token: true token2 = FactoryBot.create :access_token, use_refresh_token: true token2.refresh_token = token1.refresh_token expect(token2).not_to be_valid end it "expects database to raise an error if refresh tokens are the same" do token1 = FactoryBot.create :access_token, use_refresh_token: true token2 = FactoryBot.create :access_token, use_refresh_token: true expect do token2.refresh_token = token1.refresh_token token2.save(validate: false) end.to raise_error(uniqueness_error) end context "with hashing enabled" do include_context "with token hashing enabled" let(:token) { FactoryBot.create :access_token, use_refresh_token: true } it "holds a volatile refresh token when created" do expect(token.plaintext_refresh_token).to be_a(String) expect(token.refresh_token) .to eq(hashed_or_plain_token_func.call(token.plaintext_refresh_token)) # Finder method only finds the hashed token loaded = described_class.find_by(refresh_token: token.refresh_token) expect(loaded).to eq(token) expect(loaded.plaintext_refresh_token).to be_nil expect(loaded.refresh_token).to eq(token.refresh_token) end it "does not find_by plain text refresh tokens" do expect(described_class.find_by(refresh_token: token.plaintext_refresh_token)).to be_nil end describe "with having a plain text token" do let(:plain_refresh_token) { "plain refresh token" } let(:access_token) { FactoryBot.create :access_token } before do # Assume we have a plain text token from before activating the option access_token.update_column(:refresh_token, plain_refresh_token) end context "without fallback lookup" do it "does not provide lookups with either through by_token" do expect(described_class.by_refresh_token(plain_refresh_token)).to eq(nil) expect(described_class.by_refresh_token(access_token.refresh_token)).to eq(nil) # And it does not touch the token access_token.reload expect(access_token.refresh_token).to eq(plain_refresh_token) end end context "with fallback lookup" do include_context "with token hashing and fallback lookup enabled" it "upgrades a plain token when falling back to it" do # Side-effect: This will automatically upgrade the token expect(described_class).to receive(:upgrade_fallback_value).and_call_original expect(described_class.by_refresh_token(plain_refresh_token)) .to have_attributes( token: access_token.token, resource_owner_id: access_token.resource_owner_id, application_id: access_token.application_id, ) # Will find subsequently by hashing the token expect(described_class.by_refresh_token(plain_refresh_token)) .to have_attributes( token: access_token.token, resource_owner_id: access_token.resource_owner_id, application_id: access_token.application_id, ) # Not all the ORM support :id PK if access_token.respond_to?(:id) expect(described_class.by_refresh_token(plain_refresh_token).id).to eq(access_token.id) end # And it modifies the token value access_token.reload expect(access_token.refresh_token).not_to eq(plain_refresh_token) expect(described_class.find_by(refresh_token: plain_refresh_token)).to eq(nil) expect(described_class.find_by(refresh_token: access_token.refresh_token)).not_to be_nil end end end end end describe "validations" do it "is valid without resource_owner_id" do # For client credentials flow access_token.resource_owner_id = nil expect(access_token).to be_valid end it "is valid without application_id" do # For resource owner credentials flow access_token.application_id = nil expect(access_token).to be_valid end end describe "#same_credential?" do context "with default parameters" do let(:resource_owner) { FactoryBot.create(:resource_owner) } let(:resource_owner_id) { resource_owner.id } let(:application) { FactoryBot.create :application } let(:default_attributes) do { application: application, resource_owner_id: resource_owner_id, resource_owner_type: resource_owner.class.name, } end let(:access_token1) { FactoryBot.create :access_token, default_attributes } context "when the second token has the same owner and same app" do let(:access_token2) { FactoryBot.create :access_token, default_attributes } it "success" do expect(access_token1).to be_same_credential(access_token2) end end context "when the second token has same owner and different app" do let(:other_application) { FactoryBot.create :application } let(:access_token2) do FactoryBot.create :access_token, application: other_application, resource_owner_id: resource_owner_id, resource_owner_type: resource_owner.class.name end it "fails" do expect(access_token1).not_to be_same_credential(access_token2) end end context "when the second token has different owner and different app" do let(:other_application) { FactoryBot.create :application } let(:access_token2) do FactoryBot.create :access_token, application: other_application, resource_owner_id: resource_owner.id + 1 end it "fails" do expect(access_token1).not_to be_same_credential(access_token2) end end context "when the second token has different owner and same app" do let(:access_token2) do FactoryBot.create :access_token, application: application, resource_owner_id: resource_owner.id + 1 end it "fails" do expect(access_token1).not_to be_same_credential(access_token2) end end end end describe "#acceptable?" do context "when token is not accessible" do let(:token) { FactoryBot.create(:access_token, created_at: 6.hours.ago) } it "returns false" do expect(token.acceptable?(nil)).to be false end end context "when token has the incorrect scopes" do let(:token) { FactoryBot.create(:access_token) } it "returns false" do expect(token.acceptable?(["public"])).to be false end end context "when token is acceptable with the correct scopes" do let(:token) do token = FactoryBot.create(:access_token) token[:scopes] = "public" token end it "returns true" do expect(token.acceptable?(["public"])).to be true end end end describe ".revoke_all_for" do let(:resource_owner) { FactoryBot.create :resource_owner } let(:application) { FactoryBot.create :application } let(:default_attributes) do { application: application, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, } end it "revokes all tokens for given application and resource owner" do FactoryBot.create :access_token, default_attributes described_class.revoke_all_for(application.id, resource_owner) expect(described_class.all).to all(be_revoked) end it "matches application" do access_token_for_different_app = FactoryBot.create( :access_token, default_attributes.merge(application: FactoryBot.create(:application)), ) described_class.revoke_all_for(application.id, resource_owner) expect(access_token_for_different_app.reload).not_to be_revoked end it "matches resource owner" do access_token_for_different_owner = FactoryBot.create( :access_token, default_attributes.merge(resource_owner_id: resource_owner.id + 1), ) described_class.revoke_all_for(application.id, resource_owner) expect(access_token_for_different_owner.reload).not_to be_revoked end end describe ".matching_token_for" do let(:resource_owner) { FactoryBot.create :resource_owner } let(:resource_owner_id) { resource_owner.id } let(:application) { FactoryBot.create :application } let(:scopes) { Doorkeeper::OAuth::Scopes.from_string("public write") } let(:default_attributes) do { application: application, resource_owner_id: resource_owner_id, resource_owner_type: resource_owner.class.name, scopes: scopes.to_s, } end before do default_scopes_exist(*scopes.all) end it "returns only one token" do token = FactoryBot.create :access_token, default_attributes last_token = described_class.matching_token_for(application, resource_owner, scopes) expect(last_token).to eq(token) end it "accepts nil as resource owner" do token = FactoryBot.create :access_token, default_attributes.merge(resource_owner_id: nil, resource_owner_type: nil) last_token = described_class.matching_token_for(application, nil, scopes) expect(last_token).to eq(token) end it "excludes revoked tokens" do FactoryBot.create :access_token, default_attributes.merge(revoked_at: 1.day.ago) last_token = described_class.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it "excludes tokens with a different application" do FactoryBot.create :access_token, default_attributes.merge(application: FactoryBot.create(:application)) last_token = described_class.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it "excludes tokens with a different resource owner" do FactoryBot.create :access_token, default_attributes.merge(resource_owner_id: resource_owner.id + 1) last_token = described_class.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it "excludes tokens with fewer scopes" do FactoryBot.create :access_token, default_attributes.merge(scopes: "public") last_token = described_class.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to be_nil end it "excludes tokens with different scopes" do FactoryBot.create :access_token, default_attributes.merge(scopes: "public email") last_token = described_class.matching_token_for(application, resource_owner, scopes) expect(last_token).to be_nil end it "excludes tokens with additional scopes" do FactoryBot.create :access_token, default_attributes.merge(scopes: "public write email") last_token = described_class.matching_token_for(application, resource_owner, scopes) expect(last_token).to be_nil end it "excludes tokens with scopes that are not present in server scopes" do FactoryBot.create :access_token, default_attributes.merge( application: application, scopes: "public read", ) last_token = described_class.matching_token_for(application, resource_owner, scopes) expect(last_token).to be_nil end it "excludes tokens with scopes that are not present in application scopes" do application = FactoryBot.create :application, scopes: "private read" FactoryBot.create :access_token, default_attributes.merge( application: application, ) last_token = described_class.matching_token_for(application, resource_owner, scopes) expect(last_token).to be_nil end it "does not match token if empty scope requested and token/app scopes present" do application = FactoryBot.create :application, scopes: "sample:scope" app_params = { application_id: application.id, scopes: "sample:scope", resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, } FactoryBot.create :access_token, app_params empty_scopes = Doorkeeper::OAuth::Scopes.from_string("") last_token = described_class.matching_token_for(application, resource_owner.id, empty_scopes) expect(last_token).to be_nil end it "matches token if empty scope requested and no token scopes present" do empty_scopes = Doorkeeper::OAuth::Scopes.from_string("") token = FactoryBot.create :access_token, default_attributes.merge(scopes: empty_scopes) last_token = described_class.matching_token_for(application, resource_owner.id, empty_scopes) expect(last_token).to eq(token) end it "returns the last matching token" do FactoryBot.create :access_token, default_attributes.merge(created_at: 1.day.ago) matching_token = FactoryBot.create :access_token, default_attributes FactoryBot.create :access_token, default_attributes.merge(scopes: "public") last_token = described_class.matching_token_for(application, resource_owner_id, scopes) expect(last_token).to eq(matching_token) end context "when custom access token attributes are used" do before do Doorkeeper.configure do orm DOORKEEPER_ORM custom_access_token_attributes [:tenant_name] end default_scopes_exist(*scopes.all) end let(:custom_attributes) { { tenant_name: "Me" } } it "returns a token when attributes match" do token = FactoryBot.create :access_token, default_attributes.merge(custom_attributes) last_token = described_class.matching_token_for( application, resource_owner, scopes, custom_attributes: custom_attributes) expect(last_token).to eq(token) end it "does not return a token if attributes don't match" do token = FactoryBot.create :access_token, default_attributes.merge(custom_attributes) last_token = described_class.matching_token_for(application, resource_owner, scopes, custom_attributes: { tenant_id: 'different' }) expect(last_token).to eq(nil) end it "ignores custom attributes if a nil value is passed" do token = FactoryBot.create :access_token, default_attributes.merge(custom_attributes) last_token = described_class.matching_token_for(application, resource_owner, scopes, custom_attributes: nil) expect(last_token).to eq(token) end end end describe "#as_json" do let(:token) { FactoryBot.create(:access_token) } let(:token_hash) do { resource_owner_id: token.resource_owner_id, scope: token.scopes, expires_in: token.expires_in_seconds, application: { uid: token.application.uid }, created_at: token.created_at.to_i, } end it "returns as_json hash" do hash = token_hash if Doorkeeper.configuration.polymorphic_resource_owner? hash[:resource_owner_type] = token.resource_owner_type end expect(token.as_json).to match(hash) end describe "#not_expired" do let(:resource_owner) { FactoryBot.create(:doorkeeper_testing_user) } let(:application) { FactoryBot.create(:application) } let(:attrs) { { resource_owner_id: resource_owner.id, application_id: application.id } } let!(:active_token1) { FactoryBot.create(:access_token, attrs.merge(expires_in: 2000)) } let!(:active_token2) { FactoryBot.create(:access_token, attrs.merge(expires_in: 2)) } let!(:active_token3) { FactoryBot.create(:access_token, attrs.merge(expires_in: 10, created_at: Time.current - 5.seconds)) } let!(:active_token4) { FactoryBot.create(:access_token, attrs.merge(expires_in: nil)) } let!(:not_active_token1) { FactoryBot.create(:access_token, attrs.merge(expires_in: 2, created_at: Time.current - 2.seconds)) } let!(:not_active_token2) { FactoryBot.create(:access_token, attrs.merge(expires_in: 10, created_at: Time.current - 12.seconds)) } let!(:not_active_token3) { FactoryBot.create(:access_token, attrs.merge(expires_in: 10_000, revoked_at: Time.current)) } before do Timecop.freeze(Time.current) end after do Timecop.return end it "returns only non expired tokens" do expired_tokens = described_class.not_expired expect(expired_tokens.size).to be(4) expect(expired_tokens).to match_array([active_token1, active_token2, active_token3, active_token4]) end end end describe ".create_for with read replica support" do let(:application) { FactoryBot.create(:application) } let(:resource_owner) { FactoryBot.create(:resource_owner) } let(:scopes) { Doorkeeper::OAuth::Scopes.from_string("public") } context "when handle_read_write_roles is enabled" do before do Doorkeeper.configure do orm :active_record enable_multiple_database_roles end end it "creates token using primary database role" do expect(ActiveRecord::Base).to receive(:connected_to).with(role: :writing).and_call_original token = described_class.create_for( application: application, resource_owner: resource_owner, scopes: scopes, ) expect(token).to be_persisted expect(token.application).to eq(application) expect(token.resource_owner_id).to eq(resource_owner.id) end end context "when enable_multiple_database_roles is disabled" do before do Doorkeeper.configure do orm :active_record # enable_multiple_database_roles is disabled by default end end it "creates token without explicit role switching" do expect(ActiveRecord::Base).not_to receive(:connected_to) token = described_class.create_for( application: application, resource_owner: resource_owner, scopes: scopes, ) expect(token).to be_persisted end end end describe ".revoke_all_for with read replica support" do let(:application) { FactoryBot.create(:application) } let(:resource_owner) { FactoryBot.create(:resource_owner) } before do FactoryBot.create(:access_token, application: application, resource_owner_id: resource_owner.id) end context "when handle_read_write_roles is enabled" do before do Doorkeeper.configure do orm :active_record enable_multiple_database_roles end end it "revokes tokens using primary database role" do expect(ActiveRecord::Base).to receive(:connected_to).with(role: :writing).and_call_original described_class.revoke_all_for(application.id, resource_owner) end end end describe "#revoke with read replica support" do let(:token) { FactoryBot.create(:access_token) } context "when handle_read_write_roles is enabled" do before do Doorkeeper.configure do orm :active_record enable_multiple_database_roles end end it "revokes token using primary database role" do expect(ActiveRecord::Base).to receive(:connected_to).with(role: :writing).and_call_original token.revoke expect(token).to be_revoked end end context "when enable_multiple_database_roles is disabled" do before do Doorkeeper.configure do orm :active_record # enable_multiple_database_roles is disabled by default end end it "revokes token without explicit role switching" do expect(ActiveRecord::Base).not_to receive(:connected_to) token.revoke expect(token).to be_revoked end end end end ================================================ FILE: spec/models/doorkeeper/application_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" require "bcrypt" RSpec.describe Doorkeeper::Application do let(:new_application) { FactoryBot.build(:application) } let(:owner) { FactoryBot.build_stubbed(:doorkeeper_testing_user) } let(:uid) { SecureRandom.hex(8) } let(:secret) { SecureRandom.hex(8) } it "is invalid without a name" do new_application.name = nil expect(new_application).not_to be_valid end it "is invalid without determining confidentiality" do new_application.confidential = nil expect(new_application).not_to be_valid end it "generates uid on create" do expect(new_application.uid).to be_nil new_application.save expect(new_application.uid).not_to be_nil end it "generates uid on create if an empty string" do new_application.uid = "" new_application.save expect(new_application.uid).not_to be_blank end it "generates uid on create unless one is set" do new_application.uid = uid new_application.save expect(new_application.uid).to eq(uid) end it "is invalid without uid" do new_application.save new_application.uid = nil expect(new_application).not_to be_valid end it "checks uniqueness of uid" do app1 = FactoryBot.create(:application) app2 = FactoryBot.create(:application) app2.uid = app1.uid expect(app2).not_to be_valid end it "expects database to throw an error when uids are the same" do app1 = FactoryBot.create(:application) app2 = FactoryBot.create(:application) app2.uid = app1.uid expect { app2.save!(validate: false) }.to raise_error(uniqueness_error) end it "generate secret on create" do expect(new_application.secret).to be_nil new_application.save expect(new_application.secret).not_to be_nil end it "generate secret on create if is blank string" do new_application.secret = "" new_application.save expect(new_application.secret).not_to be_blank end it "generate secret on create unless one is set" do new_application.secret = secret new_application.save expect(new_application.secret).to eq(secret) end it "is invalid without secret" do new_application.save new_application.secret = nil expect(new_application).not_to be_valid end it "is valid without secret if client is public" do new_application.confidential = false new_application.secret = nil expect(new_application).to be_valid end it "generates a secret using a custom object" do module CustomGeneratorArgs def self.generate "custom_application_secret" end end Doorkeeper.configure do orm DOORKEEPER_ORM application_secret_generator "CustomGeneratorArgs" end expect(new_application.secret).to be_nil new_application.save expect(new_application.secret).to eq("custom_application_secret") end context "when application_owner is enabled" do context "when application owner is not required" do before do Doorkeeper.configure do orm DOORKEEPER_ORM enable_application_owner end Doorkeeper.run_orm_hooks end it "is valid given valid attributes" do expect(new_application).to be_valid end end context "when application owner is required" do before do Doorkeeper.configure do orm DOORKEEPER_ORM enable_application_owner confirmation: true end Doorkeeper.run_orm_hooks end it "is invalid without an owner" do expect(new_application).not_to be_valid end it "is valid with an owner" do new_application.owner = owner expect(new_application).to be_valid end end end describe "redirect URI" do context "when grant flows allow blank redirect URI" do before do Doorkeeper.configure do orm DOORKEEPER_ORM grant_flows %w[password client_credentials] end end it "is valid without redirect_uri" do new_application.save new_application.redirect_uri = nil expect(new_application).to be_valid end end context "when grant flows require redirect URI" do before do Doorkeeper.configure do orm DOORKEEPER_ORM grant_flows %w[password client_credentials authorization_code] end end it "is invalid without redirect_uri" do new_application.save new_application.redirect_uri = nil expect(new_application).not_to be_valid end end context "when blank URI option disabled" do before do Doorkeeper.configure do orm DOORKEEPER_ORM grant_flows %w[password client_credentials] allow_blank_redirect_uri false end end it "is invalid without redirect_uri" do new_application.save new_application.redirect_uri = nil expect(new_application).not_to be_valid end end end context "with hashing enabled" do include_context "with application hashing enabled" let(:app) { FactoryBot.create :application } let(:default_strategy) { Doorkeeper::SecretStoring::Sha256Hash } it "uses SHA256 to avoid additional dependencies" do # Ensure token was generated app.validate expect(app.secret).to eq(default_strategy.transform_secret(app.plaintext_secret)) end context "when bcrypt strategy is configured" do # In this text context, we have bcrypt loaded so `bcrypt_present?` # will always be true before do Doorkeeper.configure do orm DOORKEEPER_ORM hash_application_secrets using: "Doorkeeper::SecretStoring::BCrypt" end end it "holds a volatile plaintext and BCrypt secret" do expect(app.secret_strategy).to eq Doorkeeper::SecretStoring::BCrypt expect(app.plaintext_secret).to be_a(String) expect(app.secret).not_to eq(app.plaintext_secret) expect { ::BCrypt::Password.create(app.secret) }.not_to raise_error end end it "does not fallback to plain lookup by default" do lookup = described_class.by_uid_and_secret(app.uid, app.secret) expect(lookup).to eq(nil) lookup = described_class.by_uid_and_secret(app.uid, app.plaintext_secret) expect(lookup).to eq(app) end context "with fallback enabled" do include_context "with token hashing and fallback lookup enabled" it "provides plain and hashed lookup" do lookup = described_class.by_uid_and_secret(app.uid, app.secret) expect(lookup).to eq(app) lookup = described_class.by_uid_and_secret(app.uid, app.plaintext_secret) expect(lookup).to eq(app) end end it "does not provide access to secret after loading" do lookup = described_class.by_uid_and_secret(app.uid, app.plaintext_secret) expect(lookup.plaintext_secret).to be_nil end end describe "destroy related models on cascade" do before do new_application.save end let(:resource_owner) { FactoryBot.create(:resource_owner) } it "destroys its access grants" do FactoryBot.create( :access_grant, application: new_application, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, ) expect { new_application.destroy }.to change { Doorkeeper::AccessGrant.count }.by(-1) end it "destroys its access tokens" do FactoryBot.create(:access_token, application: new_application) FactoryBot.create(:access_token, application: new_application, revoked_at: Time.now.utc) expect do new_application.destroy end.to change { Doorkeeper::AccessToken.count }.by(-2) end end describe "#ordered_by" do let(:applications) { FactoryBot.create_list(:application, 5) } context "when a direction is not specified" do it "calls order with a default order of asc" do names = applications.map(&:name).sort expect(described_class.ordered_by(:name).map(&:name)).to eq(names) end end context "when a direction is specified" do it "calls order with specified direction" do names = applications.map(&:name).sort.reverse expect(described_class.ordered_by(:name, :desc).map(&:name)).to eq(names) end end end describe "#redirect_uri=" do context "when array of valid redirect_uris" do it "joins by newline" do new_application.redirect_uri = ["http://localhost/callback1", "http://localhost/callback2"] expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2") end end context "when string of valid redirect_uris" do it "stores as-is" do new_application.redirect_uri = "http://localhost/callback1\nhttp://localhost/callback2" expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2") end end end describe "#renew_secret" do let(:app) { FactoryBot.create :application } it "generates a new secret" do old_secret = app.secret app.renew_secret expect(old_secret).not_to eq(app.secret) end end describe "#authorized_for" do let(:resource_owner) { FactoryBot.create(:resource_owner) } let(:other_resource_owner) { FactoryBot.create(:resource_owner) } it "is empty if the application is not authorized for anyone" do expect(described_class.authorized_for(resource_owner)).to be_empty end it "returns only application for a specific resource owner" do FactoryBot.create( :access_token, resource_owner_id: other_resource_owner.id, resource_owner_type: other_resource_owner.class.name, ) token = FactoryBot.create( :access_token, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, ) expect(described_class.authorized_for(resource_owner)).to eq([token.application]) end it "excludes revoked tokens" do FactoryBot.create( :access_token, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, revoked_at: 2.days.ago, ) expect(described_class.authorized_for(resource_owner)).to be_empty end it "returns all applications that have been authorized" do token1 = FactoryBot.create( :access_token, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, ) token2 = FactoryBot.create( :access_token, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, ) expect(described_class.authorized_for(resource_owner)) .to eq([token1.application, token2.application]) end it "returns only one application even if it has been authorized twice" do application = FactoryBot.create(:application) FactoryBot.create( :access_token, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, application: application, ) FactoryBot.create( :access_token, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, application: application, ) expect(described_class.authorized_for(resource_owner)).to eq([application]) end end describe "#revoke_tokens_and_grants_for" do it "revokes all access tokens and access grants" do application_id = 42 resource_owner = double expect(Doorkeeper::AccessToken) .to receive(:revoke_all_for).with(application_id, resource_owner) expect(Doorkeeper::AccessGrant) .to receive(:revoke_all_for).with(application_id, resource_owner) described_class.revoke_tokens_and_grants_for(application_id, resource_owner) end end describe "#by_uid_and_secret" do context "when application is private/confidential" do it "finds the application via uid/secret" do app = FactoryBot.create :application authenticated = described_class.by_uid_and_secret(app.uid, app.secret) expect(authenticated).to eq(app) end context "when secret is wrong" do it "does not find the application" do app = FactoryBot.create :application authenticated = described_class.by_uid_and_secret(app.uid, "bad") expect(authenticated).to eq(nil) end end end context "when application is public/non-confidential" do context "when secret is blank" do it "finds the application" do app = FactoryBot.create :application, confidential: false authenticated = described_class.by_uid_and_secret(app.uid, nil) expect(authenticated).to eq(app) end end context "when secret is wrong" do it "does not find the application" do app = FactoryBot.create :application, confidential: false authenticated = described_class.by_uid_and_secret(app.uid, "bad") expect(authenticated).to eq(nil) end end end end describe "#confidential?" do let(:app) do FactoryBot.create(:application, confidential: confidential) end context "when application is private/confidential" do let(:confidential) { true } it { expect(app).to be_confidential } end context "when application is public/non-confidential" do let(:confidential) { false } it { expect(app).not_to be_confidential } end end describe "#as_json" do let(:app) { FactoryBot.create :application, secret: "123123123" } before do allow(Doorkeeper.configuration) .to receive(:application_secret_strategy).and_return(Doorkeeper::SecretStoring::Plain) end # AR specific feature if DOORKEEPER_ORM == :active_record it "correctly works with #to_json" do ActiveRecord::Base.include_root_in_json = true expect(app.to_json(include_root_in_json: true)).to match(/application.+?:\{/) ActiveRecord::Base.include_root_in_json = false end end context "when called without authorized resource owner" do it "includes minimal set of attributes" do expect(app.as_json).to match( "id" => app.id, "name" => app.name, "created_at" => anything, ) end it "includes application UID if it's public" do app = FactoryBot.create :application, secret: "123123123", confidential: false expect(app.as_json).to match( "id" => app.id, "name" => app.name, "created_at" => anything, "uid" => app.uid, ) end it "respects custom options" do expect(app.as_json(except: :id)).not_to include("id") expect(app.as_json(only: %i[name created_at secret])) .to match( "name" => app.name, "created_at" => anything, ) end end context "when called with authorized resource owner" do let(:other_owner) { FactoryBot.create(:doorkeeper_testing_user) } let(:app) { FactoryBot.create(:application, secret: "123123123", owner: owner) } before do Doorkeeper.configure do orm DOORKEEPER_ORM enable_application_owner confirmation: false end Doorkeeper.run_orm_hooks end it "includes all the attributes" do expect(app.as_json(current_resource_owner: owner)) .to include( "secret" => "123123123", "redirect_uri" => app.redirect_uri, "uid" => app.uid, ) end it "doesn't include unsafe attributes if current owner isn't the same as owner" do expect(app.as_json(current_resource_owner: other_owner)) .not_to include("redirect_uri") end end end if DOORKEEPER_ORM == :active_record context "when custom model class configured", active_record: true do class CustomApp < ::ActiveRecord::Base include Doorkeeper::Orm::ActiveRecord::Mixins::Application end let(:new_application) { CustomApp.new(FactoryBot.attributes_for(:application)) } context "without confirmation" do before do Doorkeeper.configure do orm DOORKEEPER_ORM application_class "CustomApp" enable_application_owner confirmation: false end Doorkeeper.run_orm_hooks end it "is valid given valid attributes" do expect(new_application).to be_valid end end context "without confirmation" do before do Doorkeeper.configure do orm DOORKEEPER_ORM application_class "CustomApp" enable_application_owner confirmation: true end Doorkeeper.run_orm_hooks end it "is invalid without owner" do expect(new_application).not_to be_valid new_application.owner = owner expect(new_application).to be_valid end end end end end ================================================ FILE: spec/requests/applications/applications_request_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" feature "Adding applications in application form" do background do i_am_logged_in visit "/oauth/applications/new" end scenario "adding a valid app" do fill_in "doorkeeper_application[name]", with: "My Application" fill_in "doorkeeper_application[redirect_uri]", with: "https://example.com" click_button "Submit" i_should_see "Application created" i_should_see "My Application" end scenario "adding invalid app" do click_button "Submit" i_should_see "Whoops! Check your form for possible errors" end scenario "adding app ignoring bad scope" do config_is_set("enforce_configured_scopes", false) fill_in "doorkeeper_application[name]", with: "My Application" fill_in "doorkeeper_application[redirect_uri]", with: "https://example.com" fill_in "doorkeeper_application[scopes]", with: "blahblah" click_button "Submit" i_should_see "Application created" i_should_see "My Application" end scenario "adding app validating bad scope" do config_is_set("enforce_configured_scopes", true) fill_in "doorkeeper_application[name]", with: "My Application" fill_in "doorkeeper_application[redirect_uri]", with: "https://example.com" fill_in "doorkeeper_application[scopes]", with: "blahblah" click_button "Submit" i_should_see "Whoops! Check your form for possible errors" end scenario "adding app validating scope, blank scope is accepted" do config_is_set("enforce_configured_scopes", true) fill_in "doorkeeper_application[name]", with: "My Application" fill_in "doorkeeper_application[redirect_uri]", with: "https://example.com" fill_in "doorkeeper_application[scopes]", with: "" click_button "Submit" i_should_see "Application created" i_should_see "My Application" end scenario "adding app validating scope, multiple scopes configured" do config_is_set("enforce_configured_scopes", true) scopes = Doorkeeper::OAuth::Scopes.from_array(%w[read write admin]) config_is_set("optional_scopes", scopes) fill_in "doorkeeper_application[name]", with: "My Application" fill_in "doorkeeper_application[redirect_uri]", with: "https://example.com" fill_in "doorkeeper_application[scopes]", with: "read write" click_button "Submit" i_should_see "Application created" i_should_see "My Application" end scenario "adding app validating scope, bad scope with multiple scopes configured" do config_is_set("enforce_configured_scopes", true) scopes = Doorkeeper::OAuth::Scopes.from_array(%w[read write admin]) config_is_set("optional_scopes", scopes) fill_in "doorkeeper_application[name]", with: "My Application" fill_in "doorkeeper_application[redirect_uri]", with: "https://example.com" fill_in "doorkeeper_application[scopes]", with: "read blah" click_button "Submit" i_should_see "Whoops! Check your form for possible errors" i_should_see Regexp.new( I18n.t("activerecord.errors.models.doorkeeper/application.attributes.scopes.not_match_configured"), true, ) end context "with blank redirect URI" do scenario "adding app with blank redirect URI when configured flows requires redirect uri" do config_is_set("grant_flows", %w[authorization_code implicit client_credentials]) fill_in "doorkeeper_application[name]", with: "My Application" fill_in "doorkeeper_application[redirect_uri]", with: "" click_button "Submit" i_should_see "Whoops! Check your form for possible errors" end scenario "adding app with blank redirect URI when configured flows without redirect uri" do config_is_set("grant_flows", %w[client_credentials password]) # Visit it once again to consider grant flows visit "/oauth/applications/new" i_should_see I18n.t("doorkeeper.applications.help.blank_redirect_uri") fill_in "doorkeeper_application[name]", with: "My Application" fill_in "doorkeeper_application[redirect_uri]", with: "" click_button "Submit" i_should_see "Application created" i_should_see "My Application" end end end feature "Listing applications" do background do i_am_logged_in FactoryBot.create :application, name: "Oauth Dude" FactoryBot.create :application, name: "Awesome App" end scenario "application list" do visit "/oauth/applications" i_should_see "Awesome App" i_should_see "Oauth Dude" end end feature "Renders assets" do scenario "admin stylesheets" do visit "/assets/doorkeeper/admin/application.css" i_should_see "Bootstrap" i_should_see ".doorkeeper-admin" end scenario "application stylesheets" do visit "/assets/doorkeeper/application.css" i_should_see "Bootstrap" i_should_see "#oauth-permissions" i_should_see "#container" end end feature "Show application" do given :app do i_am_logged_in FactoryBot.create :application, name: "Just another oauth app" end scenario "visiting application page" do visit "/oauth/applications/#{app.id}" i_should_see "Just another oauth app" end end feature "Edit application" do let :app do FactoryBot.create :application, name: "OMG my app" end background do i_am_logged_in visit "/oauth/applications/#{app.id}/edit" end scenario "updating a valid app" do fill_in "doorkeeper_application[name]", with: "Serious app" click_button "Submit" i_should_see "Application updated" i_should_see "Serious app" i_should_not_see "OMG my app" end scenario "updating an invalid app" do fill_in "doorkeeper_application[name]", with: "" click_button "Submit" i_should_see "Whoops! Check your form for possible errors" end end feature "Remove application" do background do i_am_logged_in @app = FactoryBot.create :application end scenario "deleting an application from list" do visit "/oauth/applications" i_should_see @app.name within(:css, "tr#application_#{@app.id}") do click_button "Destroy" end i_should_see "Application deleted" i_should_not_see @app.name end scenario "deleting an application from show" do visit "/oauth/applications/#{@app.id}" click_button "Destroy" i_should_see "Application deleted" end end context "when admin authenticator block is default" do let(:app) { FactoryBot.create :application, name: "app" } feature "application list" do scenario "fails with forbidden" do visit "/oauth/applications" should_have_status 403 end end feature "adding an app" do scenario "fails with forbidden" do visit "/oauth/applications/new" should_have_status 403 end end feature "editing an app" do scenario "fails with forbidden" do visit "/oauth/applications/#{app.id}/edit" should_have_status 403 end end end ================================================ FILE: spec/requests/applications/authorized_applications_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" feature "Authorized applications" do background do @user = User.create!(name: "Joe", password: "sekret") @client = client_exists(name: "Amazing Client App") resource_owner_is_authenticated @user client_is_authorized @client, @user end scenario "display user's authorized applications" do visit "/oauth/authorized_applications" i_should_see "Amazing Client App" end scenario "do not display other user's authorized applications" do client = client_exists(name: "Another Client App") client_is_authorized client, User.create!(name: "Joe", password: "sekret") visit "/oauth/authorized_applications" i_should_not_see "Another Client App" end scenario "user revoke access to application" do visit "/oauth/authorized_applications" i_should_see "Amazing Client App" click_on "Revoke" i_should_see "Application revoked" i_should_not_see "Amazing Client App" end end ================================================ FILE: spec/requests/endpoints/authorization_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" feature "Authorization endpoint" do background do default_scopes_exist :default config_is_set(:authenticate_resource_owner) { User.first || redirect_to("/sign_in") } client_exists(name: "MyApp") end scenario "requires resource owner to be authenticated" do visit authorization_endpoint_url(client: @client) i_should_see "Sign in" i_should_be_on "/" end context "with authenticated resource owner" do background do create_resource_owner sign_in end scenario "displays the authorization form" do visit authorization_endpoint_url(client: @client) i_should_see "Authorize MyApp to use your account?" end scenario "displays all requested scopes" do default_scopes_exist :public optional_scopes_exist :write visit authorization_endpoint_url(client: @client, scope: "public write") i_should_see "Access your public data" i_should_see "Update your data" end end context "with a invalid request's param" do background do create_resource_owner sign_in end context "when missing required param" do scenario "displays invalid_request error when missing client" do visit authorization_endpoint_url(client: nil, response_type: "code") i_should_not_see "Authorize" i_should_see_translated_invalid_request_error_message :missing_param, :client_id end scenario "displays invalid_request error when missing response_type param" do visit authorization_endpoint_url(client: @client, response_type: "") i_should_not_see "Authorize" i_should_see_translated_invalid_request_error_message :missing_param, :response_type end scenario "displays invalid_request error when missing scope param and authorization server has no default scopes" do config_is_set(:default_scopes, []) visit authorization_endpoint_url(client: @client, response_type: "code", scope: "") i_should_not_see "Authorize" i_should_see_translated_invalid_request_error_message :missing_param, :scope end end scenario "displays unsupported_response_type error when using a disabled response type" do config_is_set(:grant_flows, ["implicit"]) visit authorization_endpoint_url(client: @client, response_type: "code") i_should_not_see "Authorize" i_should_see_translated_error_message :unsupported_response_type end scenario "displays unsupported_response_mode error when using an invalid response mode" do visit authorization_endpoint_url(client: @client, response_mode: "invalid_response_mode") i_should_not_see "Authorize" i_should_see_translated_error_message :unsupported_response_mode end end context "when forgery protection enabled" do background do create_resource_owner sign_in end scenario "raises exception on forged requests" do allowing_forgery_protection do expect do page.driver.post authorization_endpoint_url( client_id: @client.uid, redirect_uri: @client.redirect_uri, response_type: "code", ) end.to raise_error(ActionController::InvalidAuthenticityToken) end end end end ================================================ FILE: spec/requests/endpoints/token_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "Token endpoint" do before do client_exists create_resource_owner authorization_code_exists application: @client, scopes: "public", resource_owner_id: @resource_owner.id, resource_owner_type: @resource_owner.class.name end it "respond with correct headers" do post token_endpoint_url(code: @authorization.token, client: @client) expect(headers["Cache-Control"]).to be_in(["no-store", "no-cache, no-store", "private, no-store"]) expect(headers["Content-Type"]).to eq("application/json; charset=utf-8") expect(headers["Pragma"]).to eq("no-cache") end it "accepts client credentials with basic auth header" do post token_endpoint_url, params: { code: @authorization.token, redirect_uri: @client.redirect_uri, }, headers: { "HTTP_AUTHORIZATION" => basic_auth_header_for_client(@client) } expect(json_response).to include("access_token" => Doorkeeper::AccessToken.first.token) end it "returns null for expires_in when a permanent token is set" do config_is_set(:access_token_expires_in, nil) post token_endpoint_url(code: @authorization.token, client: @client) expect(json_response).to include("access_token" => Doorkeeper::AccessToken.first.token) expect(json_response).not_to include("expires_in") end it "returns unsupported_grant_type for invalid grant_type param" do post token_endpoint_url(code: @authorization.token, client: @client, grant_type: "nothing") expect(json_response).to match( "error" => "unsupported_grant_type", "error_description" => translated_error_message("unsupported_grant_type"), ) end it "returns unsupported_grant_type for disabled grant flows" do config_is_set(:grant_flows, ["implicit"]) post token_endpoint_url(code: @authorization.token, client: @client, grant_type: "authorization_code") expect(json_response).to match( "error" => "unsupported_grant_type", "error_description" => translated_error_message("unsupported_grant_type"), ) end it "returns unsupported_grant_type when refresh_token is not in use" do post token_endpoint_url(code: @authorization.token, client: @client, grant_type: "refresh_token") expect(json_response).to match( "error" => "unsupported_grant_type", "error_description" => translated_error_message("unsupported_grant_type"), ) end it "returns invalid_request if grant_type is missing" do post token_endpoint_url(code: @authorization.token, client: @client, grant_type: "") expect(json_response).to match( "error" => "invalid_request", "error_description" => translated_invalid_request_error_message(:missing_param, :grant_type), ) end end ================================================ FILE: spec/requests/flows/authorization_code_errors_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" feature "Authorization Code Flow Errors" do let(:client_params) { {} } background do default_scopes_exist :default config_is_set(:authenticate_resource_owner) { User.first || redirect_to("/sign_in") } client_exists client_params create_resource_owner sign_in end after do access_grant_should_not_exist end context "with a client trying to xss resource owner" do let(:client_name) { "
XSS
" } let(:client_params) { { name: client_name } } scenario "resource owner visit authorization endpoint" do visit authorization_endpoint_url(client: @client) expect(page).not_to have_css("#xss") end end context "when access was denied" do scenario "redirects with error" do visit authorization_endpoint_url(client: @client) click_on "Deny" i_should_be_on_client_callback @client url_should_not_have_param "code" url_should_have_param "error", "access_denied" url_should_have_param "error_description", translated_error_message(:access_denied) end scenario "redirects with state parameter" do visit authorization_endpoint_url(client: @client, state: "return-this") click_on "Deny" i_should_be_on_client_callback @client url_should_not_have_param "code" url_should_have_param "state", "return-this" end end end RSpec.describe "Authorization Code Flow Errors after authorization" do before do client_exists create_resource_owner authorization_code_exists application: @client, resource_owner_id: @resource_owner.id, resource_owner_type: @resource_owner.class.name end it "returns :invalid_grant error when posting an already revoked grant code" do # First successful request post token_endpoint_url(code: @authorization.token, client: @client) # Second attempt with same token expect do post token_endpoint_url(code: @authorization.token, client: @client) end.not_to(change { Doorkeeper::AccessToken.count }) expect(json_response).to match( "error" => "invalid_grant", "error_description" => translated_error_message("invalid_grant"), ) end it "returns :invalid_grant error for invalid grant code" do post token_endpoint_url(code: "invalid", client: @client) access_token_should_not_exist expect(json_response).to match( "error" => "invalid_grant", "error_description" => translated_error_message("invalid_grant"), ) end end ================================================ FILE: spec/requests/flows/authorization_code_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" feature "Authorization Code Flow" do background do default_scopes_exist :default config_is_set(:authenticate_resource_owner) { User.first || redirect_to("/sign_in") } client_exists create_resource_owner sign_in end scenario "resource owner authorizes the client" do visit authorization_endpoint_url(client: @client) click_on "Authorize" access_grant_should_exist_for(@client, @resource_owner) i_should_be_on_client_callback(@client) url_should_have_param("code", Doorkeeper::AccessGrant.first.token) url_should_not_have_param("state") url_should_not_have_param("error") end context "when configured to check application supported grant flow" do before do config_is_set(:allow_grant_flow_for_client, ->(_grant_flow, client) { client.name == "admin" }) end scenario "forbids the request when doesn't satisfy condition" do @client.update(name: "sample app") visit authorization_endpoint_url(client: @client) i_should_see_translated_error_message("unauthorized_client") end scenario "allows the request when satisfies condition" do @client.update(name: "admin") visit authorization_endpoint_url(client: @client) i_should_not_see_translated_error_message("unauthorized_client") click_on "Authorize" authorization_code = Doorkeeper::AccessGrant.first.token create_access_token authorization_code, @client access_token_should_exist_for(@client, @resource_owner) expect(json_response).to match( "access_token" => Doorkeeper::AccessToken.first.token, "token_type" => "Bearer", "expires_in" => 7200, "scope" => "default", "created_at" => an_instance_of(Integer), ) end end context "with grant hashing enabled" do background do config_is_set(:token_secret_strategy, ::Doorkeeper::SecretStoring::Sha256Hash) end def authorize(redirect_url) @client.redirect_uri = redirect_url @client.save! visit authorization_endpoint_url(client: @client) click_on "Authorize" access_grant_should_exist_for(@client, @resource_owner) code = current_params["code"] expect(code).not_to be_nil hashed_code = Doorkeeper::AccessGrant.secret_strategy.transform_secret code expect(hashed_code).to eq Doorkeeper::AccessGrant.first.token [code, hashed_code] end scenario "using redirect_url urn:ietf:wg:oauth:2.0:oob" do code, hashed_code = authorize("urn:ietf:wg:oauth:2.0:oob") expect(code).not_to eq(hashed_code) i_should_see "Authorization code:" i_should_see code i_should_not_see hashed_code end scenario "using redirect_url urn:ietf:wg:oauth:2.0:oob:auto" do code, hashed_code = authorize("urn:ietf:wg:oauth:2.0:oob:auto") expect(code).not_to eq(hashed_code) i_should_see "Authorization code:" i_should_see code i_should_not_see hashed_code end end scenario "resource owner authorizes using oob url" do @client.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" @client.save! visit authorization_endpoint_url(client: @client) click_on "Authorize" access_grant_should_exist_for(@client, @resource_owner) url_should_have_param("code", Doorkeeper::AccessGrant.first.token) i_should_see "Authorization code:" i_should_see Doorkeeper::AccessGrant.first.token end scenario "resource owner authorizes the client with state parameter set" do visit authorization_endpoint_url(client: @client, state: "return-me") click_on "Authorize" url_should_have_param("code", Doorkeeper::AccessGrant.first.token) url_should_have_param("state", "return-me") url_should_not_have_param("code_challenge_method") end scenario "resource owner requests an access token without authorization code" do create_access_token "", @client access_token_should_not_exist expect(Doorkeeper::AccessToken.count).to be_zero expect(json_response).to match( "error" => "invalid_request", "error_description" => translated_invalid_request_error_message(:missing_param, :code), ) end scenario "resource owner requests an access token with authorization code" do visit authorization_endpoint_url(client: @client) click_on "Authorize" authorization_code = Doorkeeper::AccessGrant.first.token create_access_token authorization_code, @client access_token_should_exist_for(@client, @resource_owner) expect(json_response).to match( "access_token" => Doorkeeper::AccessToken.first.token, "token_type" => "Bearer", "expires_in" => 7200, "scope" => "default", "created_at" => an_instance_of(Integer), ) end scenario "resource owner requests an access token with authorization code but without secret" do visit authorization_endpoint_url(client: @client) click_on "Authorize" authorization_code = Doorkeeper::AccessGrant.first.token page.driver.post token_endpoint_url( code: authorization_code, client_id: @client.uid, redirect_uri: @client.redirect_uri, ) expect(Doorkeeper::AccessToken.count).to be_zero expect(json_response).to match( "error" => "invalid_client", "error_description" => translated_error_message(:invalid_client), ) end scenario "resource owner requests an access token with authorization code but without client id" do visit authorization_endpoint_url(client: @client) click_on "Authorize" authorization_code = Doorkeeper::AccessGrant.first.token page.driver.post token_endpoint_url( code: authorization_code, client_secret: @client.secret, redirect_uri: @client.redirect_uri, ) expect(Doorkeeper::AccessToken.count).to be_zero expect(json_response).to match( "error" => "invalid_client", "error_description" => translated_error_message(:invalid_client), ) end scenario "silently authorizes if active matching token exists" do default_scopes_exist :public, :write access_token_exists application: @client, expires_in: 10_000, resource_owner_id: @resource_owner.id, resource_owner_type: @resource_owner.class.name, scopes: "public write" visit authorization_endpoint_url(client: @client, scope: "public write") response_status_should_be 200 i_should_not_see "Authorize" end context "with PKCE" do context "when plain" do let(:code_challenge) { "a45a9fea-0676-477e-95b1-a40f72ac3cfb" } let(:code_verifier) { "a45a9fea-0676-477e-95b1-a40f72ac3cfb" } scenario "resource owner authorizes the client with code_challenge parameter set" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "plain", ) click_on "Authorize" url_should_have_param("code", Doorkeeper::AccessGrant.first.token) url_should_not_have_param("code_challenge_method") url_should_not_have_param("code_challenge") end scenario "mobile app requests an access token with authorization code but not pkce token" do visit authorization_endpoint_url(client: @client) click_on "Authorize" url_should_have_param("code", Doorkeeper::AccessGrant.first.token) end scenario "mobile app requests an access token with authorization code and plain code challenge method" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "plain", ) click_on "Authorize" authorization_code = current_params["code"] create_access_token authorization_code, @client, code_verifier access_token_should_exist_for(@client, @resource_owner) expect(json_response).to match( "access_token" => Doorkeeper::AccessToken.first.token, "token_type" => "Bearer", "expires_in" => 7200, "scope" => "default", "created_at" => an_instance_of(Integer), ) end scenario "mobile app requests an access token with authorization code but without code_verifier" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "plain", ) click_on "Authorize" authorization_code = current_params["code"] create_access_token authorization_code, @client, nil expect(json_response).to match( "error" => "invalid_request", "error_description" => translated_invalid_request_error_message(:missing_param, :code_verifier), ) end scenario "mobile app requests an access token with authorization code with wrong code_verifier" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "plain", ) click_on "Authorize" authorization_code = current_params["code"] create_access_token authorization_code, @client, "wrong_code_verifier" expect(json_response).not_to include("access_token") expect(json_response).to match( "error" => "invalid_grant", "error_description" => translated_error_message(:invalid_grant), ) end end context "when S256" do let(:code_challenge) { "Oz733NtQ0rJP8b04fgZMJMwprn6Iw8sMCT_9bR1q4tA" } let(:code_verifier) { "a45a9fea-0676-477e-95b1-a40f72ac3cfb" } scenario "resource owner authorizes the client with code_challenge parameter set" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "S256", ) click_on "Authorize" url_should_have_param("code", Doorkeeper::AccessGrant.first.token) url_should_not_have_param("code_challenge_method") url_should_not_have_param("code_challenge") end scenario "mobile app requests an access token with authorization code and S256 code challenge method" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "S256", ) click_on "Authorize" authorization_code = current_params["code"] create_access_token authorization_code, @client, code_verifier access_token_should_exist_for(@client, @resource_owner) expect(json_response).to match( "access_token" => Doorkeeper::AccessToken.first.token, "token_type" => "Bearer", "expires_in" => 7200, "scope" => "default", "created_at" => an_instance_of(Integer), ) end scenario "mobile app requests an access token with authorization code and without secret" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "S256", ) click_on "Authorize" authorization_code = current_params["code"] page.driver.post token_endpoint_url( code: authorization_code, client_id: @client.uid, redirect_uri: @client.redirect_uri, code_verifier: code_verifier, ) expect(json_response).to match( "error" => "invalid_client", "error_description" => translated_error_message(:invalid_client), ) end scenario "mobile app requests an access token with authorization code and without secret but is marked as not confidential" do @client.update_attribute :confidential, false visit authorization_endpoint_url(client: @client, code_challenge: code_challenge, code_challenge_method: "S256") click_on "Authorize" authorization_code = current_params["code"] page.driver.post token_endpoint_url( code: authorization_code, client_id: @client.uid, redirect_uri: @client.redirect_uri, code_verifier: code_verifier, ) expect(json_response).to match( "access_token" => Doorkeeper::AccessToken.first.token, "token_type" => "Bearer", "expires_in" => 7200, "scope" => "default", "created_at" => an_instance_of(Integer), ) end scenario "mobile app requests an access token with authorization code but no code verifier" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "S256", ) click_on "Authorize" authorization_code = current_params["code"] create_access_token authorization_code, @client expect(json_response).not_to include("access_token") expect(json_response).to match( "error" => "invalid_request", "error_description" => translated_invalid_request_error_message(:missing_param, :code_verifier), ) end scenario "mobile app requests an access token with authorization code with wrong verifier" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "S256", ) click_on "Authorize" authorization_code = current_params["code"] create_access_token authorization_code, @client, "incorrect-code-verifier" expect(json_response).to match( "error" => "invalid_grant", "error_description" => translated_error_message(:invalid_grant), ) end scenario "code_challenge_methhod in token request is totally ignored" do visit authorization_endpoint_url( client: @client, code_challenge: code_challenge, code_challenge_method: "S256", ) click_on "Authorize" authorization_code = current_params["code"] page.driver.post token_endpoint_url( code: authorization_code, client: @client, code_verifier: code_challenge, code_challenge_method: "plain", ) expect(json_response).to match( "error" => "invalid_grant", "error_description" => translated_error_message(:invalid_grant), ) end scenario "expects to set code_challenge_method explicitly without fallback" do visit authorization_endpoint_url(client: @client, code_challenge: code_challenge) expect(page).to have_content("The code_challenge_method must be one of plain, S256.") end end end context "when application scopes are present and no scope is passed" do background do @client.update(scopes: "public write read default") end scenario "scope is invalid because default scope is different from application scope" do default_scopes_exist :admin visit authorization_endpoint_url(client: @client) response_status_should_be 400 i_should_not_see "Authorize" i_should_see_translated_error_message :invalid_scope end scenario "access grant have scopes which are common in application scopees and default scopes" do default_scopes_exist :public, :write visit authorization_endpoint_url(client: @client) click_on "Authorize" access_grant_should_exist_for(@client, @resource_owner) access_grant_should_have_scopes :public, :write end end context "with scopes" do background do default_scopes_exist :public optional_scopes_exist :write end scenario "resource owner authorizes the client with default scopes" do visit authorization_endpoint_url(client: @client) click_on "Authorize" access_grant_should_exist_for(@client, @resource_owner) access_grant_should_have_scopes :public end scenario "resource owner authorizes the client with required scopes" do visit authorization_endpoint_url(client: @client, scope: "public write") click_on "Authorize" access_grant_should_have_scopes :public, :write end scenario "resource owner authorizes the client with required scopes (without defaults)" do visit authorization_endpoint_url(client: @client, scope: "write") click_on "Authorize" access_grant_should_have_scopes :write end scenario "new access token matches required scopes" do visit authorization_endpoint_url(client: @client, scope: "public write") click_on "Authorize" authorization_code = Doorkeeper::AccessGrant.first.token create_access_token authorization_code, @client access_token_should_exist_for(@client, @resource_owner) access_token_should_have_scopes :public, :write end scenario "returns new token if scopes have changed" do client_is_authorized(@client, @resource_owner, scopes: "public write") visit authorization_endpoint_url(client: @client, scope: "public") click_on "Authorize" authorization_code = Doorkeeper::AccessGrant.first.token create_access_token authorization_code, @client expect(Doorkeeper::AccessToken.count).to be(2) expect(json_response).to include("access_token" => Doorkeeper::AccessToken.last.token) end scenario "resource owner authorizes the client with extra scopes" do client_is_authorized(@client, @resource_owner, scopes: "public") visit authorization_endpoint_url(client: @client, scope: "public write") click_on "Authorize" authorization_code = Doorkeeper::AccessGrant.first.token create_access_token authorization_code, @client expect(Doorkeeper::AccessToken.count).to be(2) expect(json_response).to include("access_token" => Doorkeeper::AccessToken.last.token) access_token_should_have_scopes :public, :write end end context "when two requests sent" do before do Doorkeeper.configure do orm DOORKEEPER_ORM use_refresh_token end client_exists end describe "issuing a refresh token" do let(:resource_owner) { FactoryBot.create(:resource_owner) } before do authorization_code_exists application: @client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "second of simultaneous client requests get an error for revoked access token" do authorization_code = Doorkeeper::AccessGrant.first.token allow_any_instance_of(Doorkeeper::AccessGrant) .to receive(:revoked?).and_return(false, true) page.driver.post token_endpoint_url(code: authorization_code, client: @client) expect(json_response).to match( "error" => "invalid_grant", "error_description" => translated_error_message(:invalid_grant), ) end end end context "when custom_access_token_attributes are configured" do let(:resource_owner) { FactoryBot.create(:resource_owner) } let(:client) { client_exists } let(:grant) do authorization_code_exists( application: client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, tenant_name: "Tenant 1", ) end before do Doorkeeper.configure do orm DOORKEEPER_ORM custom_access_token_attributes [:tenant_name] end end it "copies custom attributes from the grant into the token" do page.driver.post token_endpoint_url(code: grant.token, client: client) access_token = Doorkeeper::AccessToken.find_by(token: json_response["access_token"]) expect(access_token.tenant_name).to eq("Tenant 1") end end end ================================================ FILE: spec/requests/flows/client_credentials_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "Client Credentials Request" do let(:client) { FactoryBot.create :application } context "with a valid request" do it "authorizes the client and returns the token response" do headers = authorization client.uid, client.secret params = { grant_type: "client_credentials" } post "/oauth/token", params: params, headers: headers expect(json_response).to match( "access_token" => Doorkeeper::AccessToken.first.token, "token_type" => "Bearer", "expires_in" => Doorkeeper.configuration.access_token_expires_in, "created_at" => an_instance_of(Integer), ) end context "with scopes" do before do optional_scopes_exist :write default_scopes_exist :public end it "adds the scope to the token an returns in the response" do headers = authorization client.uid, client.secret params = { grant_type: "client_credentials", scope: "write" } post "/oauth/token", params: params, headers: headers expect(json_response).to include( "access_token" => Doorkeeper::AccessToken.first.token, "scope" => "write", ) end context "when scopes are default" do it "adds the scope to the token an returns in the response" do headers = authorization client.uid, client.secret params = { grant_type: "client_credentials", scope: "public" } post "/oauth/token", params: params, headers: headers expect(json_response).to include( "access_token" => Doorkeeper::AccessToken.first.token, "scope" => "public", ) end end context "when scopes are invalid" do it "does not authorize the client and returns the error" do headers = authorization client.uid, client.secret params = { grant_type: "client_credentials", scope: "random" } post "/oauth/token", params: params, headers: headers expect(response.status).to eq(400) expect(json_response).to match( "error" => "invalid_scope", "error_description" => translated_error_message(:invalid_scope), ) end end end end context "when configured to check application supported grant flow" do before do Doorkeeper.configuration.instance_variable_set( :@allow_grant_flow_for_client, ->(_grant_flow, client) { client.name == "admin" }, ) end scenario "forbids the request when doesn't satisfy condition" do client.update(name: "sample app") headers = authorization client.uid, client.secret params = { grant_type: "client_credentials" } post "/oauth/token", params: params, headers: headers expect(json_response).to match( "error" => "unauthorized_client", "error_description" => translated_error_message(:unauthorized_client), ) end scenario "allows the request when satisfies condition" do client.update(name: "admin") headers = authorization client.uid, client.secret params = { grant_type: "client_credentials" } post "/oauth/token", params: params, headers: headers expect(json_response).to match( "access_token" => Doorkeeper::AccessToken.first.token, "token_type" => "Bearer", "expires_in" => 7200, "created_at" => an_instance_of(Integer), ) end end context "when application scopes contain some of the default scopes and no scope is passed" do before do client.update(scopes: "read write public") end it "issues new token with one default scope that are present in application scopes" do default_scopes_exist :public headers = authorization client.uid, client.secret params = { grant_type: "client_credentials" } expect do post "/oauth/token", params: params, headers: headers end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq client.id expect(json_response).to include( "access_token" => token.token, "scope" => "public", ) end it "issues new token with multiple default scopes that are present in application scopes" do default_scopes_exist :public, :read, :update headers = authorization client.uid, client.secret params = { grant_type: "client_credentials" } expect do post "/oauth/token", params: params, headers: headers end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq client.id expect(json_response).to include( "access_token" => token.token, "scope" => "public read", ) end it "forbids the request if the public scope is not present in the application scopes" do default_scopes_exist :default headers = authorization client.uid, client.secret params = { grant_type: "client_credentials" } post "/oauth/token", params: params, headers: headers expect(json_response).to match( "error" => "invalid_scope", "error_description" => translated_error_message(:invalid_scope), ) end end context "when request is invalid" do it "does not authorize the client and returns the error" do headers = {} params = { grant_type: "client_credentials" } post "/oauth/token", params: params, headers: headers expect(response.status).to eq(401) expect(json_response).to match( "error" => "invalid_client", "error_description" => translated_error_message(:invalid_client), ) end end context "when revoke_previous_client_credentials_token is true" do before do allow(Doorkeeper.config).to receive(:reuse_access_token).and_return(false) allow(Doorkeeper.config).to receive(:revoke_previous_client_credentials_token?).and_return(true) end it "revokes the previous token" do headers = authorization client.uid, client.secret params = { grant_type: "client_credentials" } post "/oauth/token", params: params, headers: headers expect(json_response).to include("access_token" => Doorkeeper::AccessToken.first.token) token = Doorkeeper::AccessToken.first post "/oauth/token", params: params, headers: headers expect(json_response).to include("access_token" => Doorkeeper::AccessToken.last.token) expect(token.reload).to be_revoked expect(Doorkeeper::AccessToken.last).not_to be_revoked end context "with a simultaneous request" do let!(:access_token) { FactoryBot.create :access_token, resource_owner_id: nil } before do allow(Doorkeeper.config.access_token_model).to receive(:matching_token_for) { access_token } allow(access_token).to receive(:revoked?).and_return(true) end it "returns an error" do headers = authorization client.uid, client.secret params = { grant_type: "client_credentials" } post "/oauth/token", params: params, headers: headers expect(json_response).to match( "error" => "invalid_token_reuse", "error_description" => translated_error_message(:server_error), ) end end end def authorization(username, password) credentials = ActionController::HttpAuthentication::Basic.encode_credentials username, password { "HTTP_AUTHORIZATION" => credentials } end end ================================================ FILE: spec/requests/flows/implicit_grant_errors_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" feature "Implicit Grant Flow Errors" do background do default_scopes_exist :default config_is_set(:authenticate_resource_owner) { User.first || redirect_to("/sign_in") } config_is_set(:grant_flows, ["implicit"]) client_exists create_resource_owner sign_in end after do access_token_should_not_exist end context "when validate client_id param" do scenario "displays invalid_client error for invalid client_id" do visit authorization_endpoint_url(client_id: "invalid", response_type: "token") i_should_not_see "Authorize" i_should_see_translated_error_message :invalid_client end scenario "displays invalid_request error when client_id is missing" do visit authorization_endpoint_url(client_id: "", response_type: "token") i_should_not_see "Authorize" i_should_see_translated_invalid_request_error_message :missing_param, :client_id end end context "when validate redirect_uri param" do scenario "displays invalid_redirect_uri error for invalid redirect_uri" do visit authorization_endpoint_url(client: @client, redirect_uri: "invalid", response_type: "token") i_should_not_see "Authorize" i_should_see_translated_error_message :invalid_redirect_uri end scenario "displays invalid_redirect_uri error when redirect_uri is missing" do visit authorization_endpoint_url(client: @client, redirect_uri: "", response_type: "token") i_should_not_see "Authorize" i_should_see_translated_error_message :invalid_redirect_uri end end context "when validate response_mode param" do scenario "displays unsupported_response_mode error when using 'query' response mode" do visit authorization_endpoint_url(client: @client, response_type: "token", response_mode: "query") i_should_not_see "Authorize" i_should_see_translated_error_message :unsupported_response_mode end end end ================================================ FILE: spec/requests/flows/implicit_grant_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" feature "Implicit Grant Flow (feature spec)" do background do default_scopes_exist :default config_is_set(:authenticate_resource_owner) { User.first || redirect_to("/sign_in") } config_is_set(:grant_flows, ["implicit"]) client_exists create_resource_owner sign_in end scenario "resource owner authorizes the client" do visit authorization_endpoint_url(client: @client, response_type: "token") click_on "Authorize" access_token_should_exist_for @client, @resource_owner i_should_be_on_client_callback @client end context "when application scopes are present and no scope is passed" do background do @client.update(scopes: "public write read") end scenario "scope is invalid because default scope is different from application scope" do default_scopes_exist :admin visit authorization_endpoint_url(client: @client, response_type: "token") response_status_should_be 400 i_should_not_see "Authorize" i_should_see_translated_error_message :invalid_scope end scenario "access token has scopes which are common in application scopes and default scopes" do default_scopes_exist :public, :write visit authorization_endpoint_url(client: @client, response_type: "token") click_on "Authorize" access_token_should_exist_for @client, @resource_owner access_token_should_have_scopes :public, :write end end end RSpec.describe "Implicit Grant Flow (request spec)" do before do default_scopes_exist :default config_is_set(:authenticate_resource_owner) { User.first || redirect_to("/sign_in") } config_is_set(:grant_flows, ["implicit"]) client_exists create_resource_owner end context "when reuse_access_token enabled" do it "returns a new token each request" do allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(false) token = client_is_authorized(@client, @resource_owner, scopes: "default") post "/oauth/authorize", params: { client_id: @client.uid, state: "", redirect_uri: @client.redirect_uri, response_type: "token", commit: "Authorize", } expect(response.location).not_to include(token.token) end it "returns the same token if it is still accessible" do allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) token = client_is_authorized(@client, @resource_owner, scopes: "default") post "/oauth/authorize", params: { client_id: @client.uid, state: "", redirect_uri: @client.redirect_uri, response_type: "token", commit: "Authorize", } expect(response.location).to include(token.token) end end end ================================================ FILE: spec/requests/flows/password_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "Resource Owner Password Credentials Flow" do context "when not setup properly" do before do client_exists create_resource_owner end context "with valid user credentials" do it "does not issue new token" do expect do post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) end.not_to(change { Doorkeeper::AccessToken.count }) end end end context "when grant type configured" do let(:client_attributes) { { redirect_uri: nil } } before do config_is_set(:grant_flows, ["password"]) config_is_set(:resource_owner_from_credentials) { User.authenticate! params[:username], params[:password] } client_exists(client_attributes) create_resource_owner end context "with valid user credentials" do context "with confidential client authorized using Basic auth" do it "issues a new token" do expect do post password_token_endpoint_url( resource_owner: @resource_owner, ), headers: { "HTTP_AUTHORIZATION" => basic_auth_header_for_client(@client) } end.to(change { Doorkeeper::AccessToken.count }) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq(@client.id) expect(json_response).to match( "access_token" => token.token, "expires_in" => an_instance_of(Integer), "token_type" => "Bearer", "created_at" => an_instance_of(Integer), ) end end context "with non-confidential/public client" do let(:client_attributes) { { confidential: false } } context "when configured to check application supported grant flow" do before do Doorkeeper.configuration.instance_variable_set( :@allow_grant_flow_for_client, ->(_grant_flow, client) { client.name == "admin" }, ) end scenario "forbids the request when doesn't satisfy condition" do @client.update(name: "sample app") expect do post password_token_endpoint_url( client_id: @client.uid, client_secret: "foobar", resource_owner: @resource_owner, ) end.not_to(change { Doorkeeper::AccessToken.count }) expect(response.status).to eq(401) expect(json_response).to match( "error" => "invalid_client", "error_description" => an_instance_of(String), ) end scenario "allows the request when satisfies condition" do @client.update(name: "admin") expect do post password_token_endpoint_url(client_id: @client.uid, resource_owner: @resource_owner) end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq(@client.id) expect(json_response).to include("access_token" => token.token) end end context "when client_secret absent" do it "issues a new token" do expect do post password_token_endpoint_url(client_id: @client.uid, resource_owner: @resource_owner) end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq(@client.id) expect(json_response).to include("access_token" => token.token) end end context "when client_secret present" do it "issues a new token" do expect do post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq(@client.id) expect(json_response).to include("access_token" => token.token) end context "when client_secret incorrect" do it "doesn't issue new token" do expect do post password_token_endpoint_url( client_id: @client.uid, client_secret: "foobar", resource_owner: @resource_owner, ) end.not_to(change { Doorkeeper::AccessToken.count }) expect(response.status).to eq(401) expect(json_response).to include( "error" => "invalid_client", "error_description" => an_instance_of(String), ) end end end end context "with confidential/private client" do it "issues a new token" do expect do post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq(@client.id) expect(json_response).to include("access_token" => token.token) end context "when client_secret absent" do it "doesn't issue new token" do expect do post password_token_endpoint_url(client_id: @client.uid, resource_owner: @resource_owner) end.not_to(change { Doorkeeper::AccessToken.count }) expect(response.status).to eq(401) expect(json_response).to match( "error" => "invalid_client", "error_description" => an_instance_of(String), ) end end end it "issues a refresh token if enabled" do config_is_set(:refresh_token_enabled, true) post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) token = Doorkeeper::AccessToken.first expect(json_response).to include("refresh_token" => token.refresh_token) end it "returns the same token if it is still accessible" do allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) client_is_authorized(@client, @resource_owner) post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) expect(Doorkeeper::AccessToken.count).to be(1) expect(json_response).to include("access_token" => Doorkeeper::AccessToken.first.token) end context "with valid, default scope" do before do default_scopes_exist :public end it "issues new token" do expect do post password_token_endpoint_url(client: @client, resource_owner: @resource_owner, scope: "public") end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq(@client.id) expect(json_response).to include( "access_token" => token.token, "scope" => "public", ) end end end context "with skip_client_authentication_for_password_grant config option" do context "when enabled" do before do allow(Doorkeeper.config) .to receive(:skip_client_authentication_for_password_grant).and_return(true) end it "issues a new token without client credentials" do expect do post password_token_endpoint_url(resource_owner: @resource_owner) end.to(change { Doorkeeper::AccessToken.count }.by(1)) token = Doorkeeper::AccessToken.first expect(token.application_id).to be_nil expect(json_response).to include("access_token" => token.token) end it "doesn't issue a new token with invalid client credentials in query string" do expect do post password_token_endpoint_url( resource_owner: @resource_owner, client_id: "invalid", client_secret: "invalid", ) end.not_to(change { Doorkeeper::AccessToken.count }) expect(response.status).to eq(401) expect(json_response).to match( "error" => "invalid_client", "error_description" => an_instance_of(String), ) end it "doesn't issue a new token with invalid client credentials in Basic auth" do invalid_client = Doorkeeper::OAuth::Client::Credentials.new("invalid", "invalid") expect do post password_token_endpoint_url( resource_owner: @resource_owner, ), headers: { "HTTP_AUTHORIZATION" => basic_auth_header_for_client(invalid_client) } end.not_to(change { Doorkeeper::AccessToken.count }) expect(response.status).to eq(401) expect(json_response).to match( "error" => "invalid_client", "error_description" => an_instance_of(String), ) end end context "when disabled" do before do allow(Doorkeeper.config) .to receive(:skip_client_authentication_for_password_grant).and_return(false) end it "doesn't issue a new token without client credentials" do expect do post password_token_endpoint_url(resource_owner: @resource_owner) end.not_to(change { Doorkeeper::AccessToken.count }) expect(response.status).to eq(401) expect(json_response).to match( "error" => "invalid_client", "error_description" => an_instance_of(String), ) end end end context "when application scopes are present and differs from configured default scopes and no scope is passed" do before do default_scopes_exist :public @client.update(scopes: "abc") end it "issues new token without any scope" do expect do post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq(@client.id) expect(token.scopes).to be_empty expect(json_response).to include("access_token" => token.token) expect(json_response).not_to include("scope") end end context "when application scopes contain some of the default scopes and no scope is passed" do before do @client.update(scopes: "read write public") end it "issues new token with one default scope that are present in application scopes" do default_scopes_exist :public, :admin expect do post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq(@client.id) expect(json_response).to include( "access_token" => token.token, "scope" => "public", ) end it "issues new token with multiple default scopes that are present in application scopes" do default_scopes_exist :public, :read, :update expect do post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) end.to change { Doorkeeper::AccessToken.count }.by(1) token = Doorkeeper::AccessToken.first expect(token.application_id).to eq(@client.id) expect(json_response).to include( "access_token" => token.token, "scope" => "public read", ) end end context "with invalid scopes" do it "doesn't issue new token" do expect do post password_token_endpoint_url( client: @client, resource_owner: @resource_owner, scope: "random", ) end.not_to(change { Doorkeeper::AccessToken.count }) end it "returns invalid_scope error" do post password_token_endpoint_url( client: @client, resource_owner: @resource_owner, scope: "random", ) expect(response.status).to eq(400) expect(json_response).to match( "error" => "invalid_scope", "error_description" => translated_error_message(:invalid_scope), ) end end context "with invalid user credentials" do it "doesn't issue new token with bad password" do expect do post password_token_endpoint_url( client: @client, resource_owner_username: @resource_owner.name, resource_owner_password: "wrongpassword", ) end.not_to(change { Doorkeeper::AccessToken.count }) end it "doesn't issue new token without credentials" do expect do post password_token_endpoint_url(client: @client) end.not_to(change { Doorkeeper::AccessToken.count }) end it "doesn't issue new token if resource_owner_from_credentials returned false or nil" do config_is_set(:resource_owner_from_credentials) { false } expect do post password_token_endpoint_url(client: @client) end.not_to(change { Doorkeeper::AccessToken.count }) config_is_set(:resource_owner_from_credentials) { nil } expect do post password_token_endpoint_url(client: @client) end.not_to(change { Doorkeeper::AccessToken.count }) end end context "with invalid confidential client credentials" do it "doesn't issue new token with bad client credentials" do expect do post password_token_endpoint_url( client_id: @client.uid, client_secret: "bad_secret", resource_owner: @resource_owner, ) end.not_to(change { Doorkeeper::AccessToken.count }) end end context "with invalid public client id" do it "doesn't issue new token with bad client id" do expect do post password_token_endpoint_url(client_id: "bad_id", resource_owner: @resource_owner) end.not_to(change { Doorkeeper::AccessToken.count }) end end end end ================================================ FILE: spec/requests/flows/refresh_token_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "Refresh Token Flow" do before do Doorkeeper.configure do orm DOORKEEPER_ORM use_refresh_token end client_exists end let(:resource_owner) { FactoryBot.create(:resource_owner) } describe "issuing a refresh token" do before do authorization_code_exists application: @client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name end it "client gets the refresh token and refreshes it" do post token_endpoint_url(code: @authorization.token, client: @client) token = Doorkeeper::AccessToken.first expect(json_response).to include( "access_token" => token.token, "refresh_token" => token.refresh_token, ) expect(@authorization.reload).to be_revoked post refresh_token_endpoint_url(client: @client, refresh_token: token.refresh_token) new_token = Doorkeeper::AccessToken.last expect(json_response).to include( "access_token" => new_token.token, "refresh_token" => new_token.refresh_token, ) expect(token.token).not_to eq(new_token.token) expect(token.refresh_token).not_to eq(new_token.refresh_token) end end describe "refreshing the token" do before do @token = FactoryBot.create( :access_token, application: @client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) end context "when refresh_token revoked on use" do it "client requests a token with refresh token" do post refresh_token_endpoint_url( client: @client, refresh_token: @token.refresh_token, ) expect(json_response).to include( "refresh_token" => Doorkeeper::AccessToken.last.refresh_token, ) expect(@token.reload).not_to be_revoked end it "client requests a token with expired access token" do @token.update_attribute :expires_in, -100 post refresh_token_endpoint_url( client: @client, refresh_token: @token.refresh_token, ) expect(json_response).to include( "refresh_token" => Doorkeeper::AccessToken.last.refresh_token, ) expect(@token.reload).not_to be_revoked end end context "when refresh_token revoked on refresh_token request" do before do allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false) end it "client request a token with refresh token" do post refresh_token_endpoint_url( client: @client, refresh_token: @token.refresh_token, ) expect(json_response).to include( "refresh_token" => Doorkeeper::AccessToken.last.refresh_token, ) expect(@token.reload).to be_revoked end it "client request a token with expired access token" do @token.update_attribute :expires_in, -100 post refresh_token_endpoint_url( client: @client, refresh_token: @token.refresh_token, ) expect(json_response).to include( "refresh_token" => Doorkeeper::AccessToken.last.refresh_token, ) expect(@token.reload).to be_revoked end end context "with public & private clients" do let(:public_client) do FactoryBot.create( :application, confidential: false, ) end let(:token_for_private_client) do FactoryBot.create( :access_token, application: @client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) end let(:token_for_public_client) do FactoryBot.create( :access_token, application: public_client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) end it "issues a new token without client_secret when refresh token was issued to a public client" do post refresh_token_endpoint_url( client_id: public_client.uid, refresh_token: token_for_public_client.refresh_token, ) new_token = Doorkeeper::AccessToken.last expect(json_response).to include( "access_token" => new_token.token, "refresh_token" => new_token.refresh_token, ) end it "returns an error without credentials" do post refresh_token_endpoint_url(refresh_token: token_for_private_client.refresh_token) expect(json_response).to include("error" => "invalid_grant") end it "returns an error with wrong credentials" do post refresh_token_endpoint_url( client_id: "1", client_secret: "1", refresh_token: token_for_private_client.refresh_token, ) expect(json_response).to match( "error" => "invalid_client", "error_description" => an_instance_of(String), ) end end it "client gets an error for invalid refresh token" do post refresh_token_endpoint_url(client: @client, refresh_token: "invalid") expect(json_response).to match( "error" => "invalid_grant", "error_description" => an_instance_of(String), ) end it "client gets an error for revoked access token" do @token.revoke post refresh_token_endpoint_url(client: @client, refresh_token: @token.refresh_token) expect(json_response).to match( "error" => "invalid_grant", "error_description" => an_instance_of(String), ) end it "second of simultaneous client requests get an error for revoked access token" do allow_any_instance_of(Doorkeeper::AccessToken).to receive(:revoked?).and_return(false, true) post refresh_token_endpoint_url(client: @client, refresh_token: @token.refresh_token) expect(json_response).to match( "error" => "invalid_grant", "error_description" => an_instance_of(String), ) end end context "when refreshing the token with multiple sessions (devices)" do before do # enable password auth to simulate other devices config_is_set(:grant_flows, ["password"]) config_is_set(:resource_owner_from_credentials) do User.authenticate! params[:username], params[:password] end create_resource_owner _another_token = post password_token_endpoint_url( client: @client, resource_owner: resource_owner, ) last_token.update(created_at: 5.seconds.ago) @token = FactoryBot.create( :access_token, application: @client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) @token.update_attribute :expires_in, -100 end context "when refresh_token revoked on use" do it "client request a token after creating another token with the same user" do post refresh_token_endpoint_url( client: @client, refresh_token: @token.refresh_token, ) expect(json_response).to include("refresh_token" => last_token.refresh_token) expect(@token.reload).not_to be_revoked end end context "when refresh_token revoked on refresh_token request" do before do allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false) end it "client request a token after creating another token with the same user" do post refresh_token_endpoint_url( client: @client, refresh_token: @token.refresh_token, ) expect(json_response).to include("refresh_token" => last_token.refresh_token) expect(@token.reload).to be_revoked end end context "when custom_access_token_attributes are configured" do before do Doorkeeper.configure do orm DOORKEEPER_ORM custom_access_token_attributes [:tenant_name] end @token = FactoryBot.create( :access_token, application: @client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, tenant_name: "Tenant 1", ) end it "copies custom attributes from the previous token into the new token" do post refresh_token_endpoint_url( client: @client, refresh_token: @token.refresh_token, ) new_token = Doorkeeper::AccessToken.last expect(new_token.tenant_name).to eq("Tenant 1") end end def last_token Doorkeeper::AccessToken.last_authorized_token_for( @client.id, resource_owner, ) end end end ================================================ FILE: spec/requests/flows/revoke_token_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "Revoke Token Flow" do before do Doorkeeper.configure { orm DOORKEEPER_ORM } end let(:private_client_application) { FactoryBot.create :application } let(:public_client_application) { FactoryBot.create :application, confidential: false } let(:resource_owner) { User.create!(name: "John", password: "sekret") } context "with authenticated, confidential OAuth 2.0 client/application" do let(:access_token) do FactoryBot.create( :access_token, application: private_client_application, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) end let(:headers) do client_id = private_client_application.uid client_secret = private_client_application.secret credentials = Base64.encode64("#{client_id}:#{client_secret}") { "HTTP_AUTHORIZATION" => "Basic #{credentials}" } end it "revokes the access token provided" do post revocation_token_endpoint_url, params: { token: access_token.token }, headers: headers expect(response).to be_successful expect(access_token.reload).to be_revoked end it "revokes the refresh token provided" do post revocation_token_endpoint_url, params: { token: access_token.refresh_token, token_type_hint: "refresh_token" }, headers: headers expect(response).to be_successful expect(access_token.reload).to be_revoked end context "with invalid token to revoke" do it "does not revoke any tokens and must respond with success" do expect do post revocation_token_endpoint_url, params: { token: "I_AM_AN_INVALID_TOKEN" }, headers: headers end.not_to(change { Doorkeeper::AccessToken.where(revoked_at: nil).count }) expect(response).to be_successful end end context "with bad credentials and a valid token" do let(:headers) do client_id = private_client_application.uid credentials = Base64.encode64("#{client_id}:poop") { "HTTP_AUTHORIZATION" => "Basic #{credentials}" } end it "does not revoke any tokens and respond with forbidden" do post revocation_token_endpoint_url, params: { token: access_token.token }, headers: headers expect(response).to be_forbidden expect(response.body).to include("unauthorized_client") expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized")) expect(access_token.reload).not_to be_revoked end end context "with no credentials and a valid token" do it "does not revoke any tokens and respond with forbidden" do post revocation_token_endpoint_url, params: { token: access_token.token } expect(response).to be_forbidden expect(response.body).to include("unauthorized_client") expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized")) expect(access_token.reload).not_to be_revoked end end context "with valid token for another client application" do let(:other_client_application) { FactoryBot.create :application } let(:headers) do client_id = other_client_application.uid client_secret = other_client_application.secret credentials = Base64.encode64("#{client_id}:#{client_secret}") { "HTTP_AUTHORIZATION" => "Basic #{credentials}" } end it "does not revoke the token as it's unauthorized" do post revocation_token_endpoint_url, params: { token: access_token.token }, headers: headers expect(response).to be_forbidden expect(response.body).to include("unauthorized_client") expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized")) expect(access_token.reload).not_to be_revoked end end end context "with authenticated public OAuth 2.0 client/application" do let(:access_token) do FactoryBot.create( :access_token, application: nil, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) end it "revokes the access token provided" do post revocation_token_endpoint_url, params: { client_id: public_client_application.uid, token: access_token.token }, headers: headers expect(response).to be_successful expect(access_token.reload).to be_revoked end it "revokes the refresh token provided" do post revocation_token_endpoint_url, params: { client_id: public_client_application.uid, token: access_token.refresh_token }, headers: headers expect(response).to be_successful expect(access_token.reload).to be_revoked end it "responses with success even for invalid token" do post revocation_token_endpoint_url, params: { client_id: public_client_application.uid, token: "dont_exist" }, headers: headers expect(response).to be_successful end context "with a valid token issued for a confidential client" do let(:access_token) do FactoryBot.create( :access_token, application: private_client_application, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) end it "does not revoke the access token provided" do post revocation_token_endpoint_url, params: { client_id: public_client_application.uid, token: access_token.token } expect(response).to be_forbidden expect(response.body).to include("unauthorized_client") expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized")) expect(access_token.reload).not_to be_revoked end it "does not revoke the refresh token provided" do post revocation_token_endpoint_url, params: { client_id: public_client_application.uid, token: access_token.refresh_token } expect(response).to be_forbidden expect(response.body).to include("unauthorized_client") expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized")) expect(access_token.reload).not_to be_revoked end end end context "with a token issued to a public client" do let(:access_token) do FactoryBot.create( :access_token, application: public_client_application, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) end it "revokes the token when the requesting client matches" do post revocation_token_endpoint_url, params: { client_id: public_client_application.uid, token: access_token.token } expect(response).to be_successful expect(access_token.reload).to be_revoked end it "revokes the refresh token when the requesting client matches" do post revocation_token_endpoint_url, params: { client_id: public_client_application.uid, token: access_token.refresh_token, token_type_hint: "refresh_token" } expect(response).to be_successful expect(access_token.reload).to be_revoked end it "does not revoke the token when the requesting client is a different public client" do other_public_client = FactoryBot.create(:application, confidential: false) post revocation_token_endpoint_url, params: { client_id: other_public_client.uid, token: access_token.token } expect(response).to be_forbidden expect(response.body).to include("unauthorized_client") expect(access_token.reload).not_to be_revoked end it "does not revoke the refresh token when the requesting client is a different public client" do other_public_client = FactoryBot.create(:application, confidential: false) post revocation_token_endpoint_url, params: { client_id: other_public_client.uid, token: access_token.refresh_token, token_type_hint: "refresh_token" } expect(response).to be_forbidden expect(response.body).to include("unauthorized_client") expect(access_token.reload).not_to be_revoked end end context "without client authentication, when skip_client_authentication_for_password_grant is false (the default)" do before do Doorkeeper.configure do orm DOORKEEPER_ORM skip_client_authentication_for_password_grant false end end let(:access_token) do FactoryBot.create( :access_token, application: nil, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) end it "does not remove the token and responses with an error" do post revocation_token_endpoint_url, params: { token: access_token.token }, headers: headers expect(response).not_to be_successful expect(access_token.reload).not_to be_revoked end end context "without client authentication, when skip_client_authentication_for_password_grant is true" do before do Doorkeeper.configure do orm DOORKEEPER_ORM skip_client_authentication_for_password_grant true end end let(:access_token) do FactoryBot.create( :access_token, application: nil, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, use_refresh_token: true, ) end it "revokes the access token provided" do post revocation_token_endpoint_url, params: { client_id: public_client_application.uid, token: access_token.token }, headers: headers expect(response).to be_successful expect(access_token.reload).to be_revoked end end end ================================================ FILE: spec/requests/flows/skip_authorization_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" feature "Skip authorization form" do background do config_is_set(:authenticate_resource_owner) { User.first || redirect_to("/sign_in") } client_exists default_scopes_exist :public optional_scopes_exist :write end context "with previously authorized clients" do background do create_resource_owner sign_in end scenario "skips the authorization and return a new grant code" do client_is_authorized(@client, @resource_owner, scopes: "public") visit authorization_endpoint_url(client: @client, scope: "public") i_should_not_see "Authorize" client_should_be_authorized @client i_should_be_on_client_callback @client url_should_have_param "code", Doorkeeper::AccessGrant.first.token end scenario "skips the authorization if other scopes are not requested" do client_exists scopes: "public read write" client_is_authorized(@client, @resource_owner, scopes: "public") visit authorization_endpoint_url(client: @client, scope: "public") i_should_not_see "Authorize" client_should_be_authorized @client i_should_be_on_client_callback @client url_should_have_param "code", Doorkeeper::AccessGrant.first.token end scenario "does not skip authorization when scopes differ (new request has fewer scopes)" do client_is_authorized(@client, @resource_owner, scopes: "public write") visit authorization_endpoint_url(client: @client, scope: "public") i_should_see "Authorize" end scenario "does not skip authorization when scopes differ (new request has more scopes)" do client_is_authorized(@client, @resource_owner, scopes: "public write") visit authorization_endpoint_url(client: @client, scopes: "public write email") i_should_see "Authorize" end scenario "creates grant with new scope when scopes differ" do client_is_authorized(@client, @resource_owner, scopes: "public write") visit authorization_endpoint_url(client: @client, scope: "public") click_on "Authorize" access_grant_should_have_scopes :public end scenario "creates grant with new scope when scopes are greater" do client_is_authorized(@client, @resource_owner, scopes: "public") visit authorization_endpoint_url(client: @client, scope: "public write") click_on "Authorize" access_grant_should_have_scopes :public, :write end end end ================================================ FILE: spec/requests/protected_resources/metal_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "ActionController::Metal API" do before do @client = FactoryBot.create(:application) @resource = User.create!(name: "Joe", password: "sekret") @token = client_is_authorized(@client, @resource) end it "client requests protected resource with valid token" do get "/metal.json?access_token=#{@token.token}" expect(json_response).to include("ok" => true) end end ================================================ FILE: spec/requests/protected_resources/private_api_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" feature "Private API" do background do @client = FactoryBot.create(:application) @resource = User.create!(name: "Joe", password: "sekret") @token = client_is_authorized(@client, @resource) end scenario "client requests protected resource with valid token" do with_access_token_header @token.token visit "/full_protected_resources" expect(page.body).to have_content("index") end scenario "client requests protected resource with disabled header authentication" do config_is_set :access_token_methods, [:from_access_token_param] with_access_token_header @token.token visit "/full_protected_resources" response_status_should_be 401 end scenario "client attempts to request protected resource with invalid token" do with_access_token_header "invalid" visit "/full_protected_resources" response_status_should_be 401 end scenario "client attempts to request protected resource with expired token" do @token.update_attribute :expires_in, -100 # expires token with_access_token_header @token.token visit "/full_protected_resources" response_status_should_be 401 end scenario "client requests protected resource with permanent token" do @token.update_attribute :expires_in, nil # never expires with_access_token_header @token.token visit "/full_protected_resources" expect(page.body).to have_content("index") end scenario "access token with no default scopes" do Doorkeeper.configuration.instance_eval do @default_scopes = Doorkeeper::OAuth::Scopes.from_array([:public]) @scopes = default_scopes + optional_scopes end @token.update_attribute :scopes, "dummy" with_access_token_header @token.token visit "/full_protected_resources" response_status_should_be 403 end scenario "access token with no allowed scopes" do @token.update_attribute :scopes, nil with_access_token_header @token.token visit "/full_protected_resources/1.json" response_status_should_be 403 end scenario "access token with one of allowed scopes" do @token.update_attribute :scopes, "admin" with_access_token_header @token.token visit "/full_protected_resources/1.json" expect(page.body).to have_content("show") end scenario "access token with another of allowed scopes" do @token.update_attribute :scopes, "write" with_access_token_header @token.token visit "/full_protected_resources/1.json" expect(page.body).to have_content("show") end scenario "access token with both allowed scopes" do @token.update_attribute :scopes, "write admin" with_access_token_header @token.token visit "/full_protected_resources/1.json" expect(page.body).to have_content("show") end end ================================================ FILE: spec/routing/custom_controller_routes_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "Custom controller for routes" do before :all do Doorkeeper.configure do orm DOORKEEPER_ORM end Rails.application.routes.disable_clear_and_finalize = true Rails.application.routes.draw do scope "inner_space" do use_doorkeeper scope: "scope" do controllers authorizations: "custom_authorizations", tokens: "custom_authorizations", applications: "custom_authorizations", token_info: "custom_authorizations" as authorizations: "custom_auth", tokens: "custom_token", token_info: "custom_token_info" end end scope "space" do use_doorkeeper do controllers authorizations: "custom_authorizations", tokens: "custom_authorizations", applications: "custom_authorizations", token_info: "custom_authorizations" as authorizations: "custom_auth", tokens: "custom_token", token_info: "custom_token_info" end end scope "outer_space" do use_doorkeeper do controllers authorizations: "custom_authorizations", tokens: "custom_authorizations", token_info: "custom_authorizations" as authorizations: "custom_auth", tokens: "custom_token", token_info: "custom_token_info" skip_controllers :tokens, :applications, :token_info end end end end after :all do Rails.application.routes.clear! load File.expand_path("../dummy/config/routes.rb", __dir__) end it "GET /inner_space/scope/authorize routes to custom authorizations controller" do expect(get("/inner_space/scope/authorize")).to route_to("custom_authorizations#new") end it "POST /inner_space/scope/authorize routes to custom authorizations controller" do expect(post("/inner_space/scope/authorize")).to route_to("custom_authorizations#create") end it "DELETE /inner_space/scope/authorize routes to custom authorizations controller" do expect(delete("/inner_space/scope/authorize")).to route_to("custom_authorizations#destroy") end it "POST /inner_space/scope/token routes to tokens controller" do expect(post("/inner_space/scope/token")).to route_to("custom_authorizations#create") end it "GET /inner_space/scope/applications routes to applications controller" do expect(get("/inner_space/scope/applications")).to route_to("custom_authorizations#index") end it "GET /inner_space/scope/token/info routes to the token_info controller" do expect(get("/inner_space/scope/token/info")).to route_to("custom_authorizations#show") end it "GET /space/oauth/authorize routes to custom authorizations controller" do expect(get("/space/oauth/authorize")).to route_to("custom_authorizations#new") end it "POST /space/oauth/authorize routes to custom authorizations controller" do expect(post("/space/oauth/authorize")).to route_to("custom_authorizations#create") end it "DELETE /space/oauth/authorize routes to custom authorizations controller" do expect(delete("/space/oauth/authorize")).to route_to("custom_authorizations#destroy") end it "POST /space/oauth/token routes to tokens controller" do expect(post("/space/oauth/token")).to route_to("custom_authorizations#create") end it "POST /space/oauth/revoke routes to tokens controller" do expect(post("/space/oauth/revoke")).to route_to("custom_authorizations#revoke") end it "POST /space/oauth/introspect routes to tokens controller" do expect(post("/space/oauth/introspect")).to route_to("custom_authorizations#introspect") end it "GET /space/oauth/applications routes to applications controller" do expect(get("/space/oauth/applications")).to route_to("custom_authorizations#index") end it "GET /space/oauth/token/info routes to the token_info controller" do expect(get("/space/oauth/token/info")).to route_to("custom_authorizations#show") end it "POST /outer_space/oauth/token is not be routable" do expect(post("/outer_space/oauth/token")).not_to be_routable end it "GET /outer_space/oauth/authorize routes to custom authorizations controller" do expect(get("/outer_space/oauth/authorize")).to be_routable end it "GET /outer_space/oauth/applications is not routable" do expect(get("/outer_space/oauth/applications")).not_to be_routable end it "GET /outer_space/oauth/token_info is not routable" do expect(get("/outer_space/oauth/token/info")).not_to be_routable end end ================================================ FILE: spec/routing/default_routes_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "Default routes" do it "GET /oauth/authorize routes to authorizations controller" do expect(get("/oauth/authorize")).to route_to("doorkeeper/authorizations#new") end it "POST /oauth/authorize routes to authorizations controller" do expect(post("/oauth/authorize")).to route_to("doorkeeper/authorizations#create") end it "DELETE /oauth/authorize routes to authorizations controller" do expect(delete("/oauth/authorize")).to route_to("doorkeeper/authorizations#destroy") end it "POST /oauth/token routes to tokens controller" do expect(post("/oauth/token")).to route_to("doorkeeper/tokens#create") end it "POST /oauth/revoke routes to tokens controller" do expect(post("/oauth/revoke")).to route_to("doorkeeper/tokens#revoke") end it "POST /oauth/introspect routes to tokens controller" do expect(post("/oauth/introspect")).to route_to("doorkeeper/tokens#introspect") end it "GET /oauth/applications routes to applications controller" do expect(get("/oauth/applications")).to route_to("doorkeeper/applications#index") end it "GET /oauth/authorized_applications routes to authorized applications controller" do expect(get("/oauth/authorized_applications")).to route_to("doorkeeper/authorized_applications#index") end it "GET /oauth/token/info route to authorized TokenInfo controller" do expect(get("/oauth/token/info")).to route_to("doorkeeper/token_info#show") end end ================================================ FILE: spec/routing/scoped_routes_spec.rb ================================================ # frozen_string_literal: true require "spec_helper" RSpec.describe "Scoped routes" do before :all do Doorkeeper.configure do orm DOORKEEPER_ORM allow_token_introspection false end Rails.application.routes.disable_clear_and_finalize = true Rails.application.routes.draw do use_doorkeeper scope: "scope" end end after :all do Rails.application.routes.clear! load File.expand_path("../dummy/config/routes.rb", __dir__) end it "GET /scope/authorize routes to authorizations controller" do expect(get("/scope/authorize")).to route_to("doorkeeper/authorizations#new") end it "POST /scope/authorize routes to authorizations controller" do expect(post("/scope/authorize")).to route_to("doorkeeper/authorizations#create") end it "DELETE /scope/authorize routes to authorizations controller" do expect(delete("/scope/authorize")).to route_to("doorkeeper/authorizations#destroy") end it "POST /scope/token routes to tokens controller" do expect(post("/scope/token")).to route_to("doorkeeper/tokens#create") end it "GET /scope/applications routes to applications controller" do expect(get("/scope/applications")).to route_to("doorkeeper/applications#index") end it "GET /scope/authorized_applications routes to authorized applications controller" do expect(get("/scope/authorized_applications")).to route_to("doorkeeper/authorized_applications#index") end it "GET /scope/token/info route to authorized TokenInfo controller" do expect(get("/scope/token/info")).to route_to("doorkeeper/token_info#show") end it "POST /scope/introspect routes not to exist" do expect(post("/scope/introspect")).not_to be_routable end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true require "coveralls" Coveralls.wear!("rails") do add_filter("/spec/") add_filter("/lib/generators/doorkeeper/templates/") end ENV["RAILS_ENV"] ||= "test" $LOAD_PATH.unshift File.dirname(__FILE__) require "#{File.dirname(__FILE__)}/support/doorkeeper_rspec.rb" DOORKEEPER_ORM = Doorkeeper::RSpec.detect_orm require "dummy/config/environment" require "rspec/rails" require "capybara/rspec" require "database_cleaner" require "generator_spec/test_case" # Load JRuby SQLite3 if in that platform if defined? JRUBY_VERSION require "jdbc/sqlite3" Jdbc::SQLite3.load_driver end Doorkeeper::RSpec.print_configuration_info require "support/orm/#{DOORKEEPER_ORM}" require "support/render_with_matcher" Dir["#{File.dirname(__FILE__)}/support/{dependencies,helpers,shared}/*.rb"].sort.each { |file| require file } RSpec.configure do |config| config.infer_spec_type_from_file_location! config.mock_with :rspec config.infer_base_class_for_anonymous_controllers = false config.include RSpec::Rails::RequestExampleGroup, type: :request config.before do begin DatabaseCleaner.start rescue NameError # ActiveRecord might not be defined in some tests end Doorkeeper.configure { orm DOORKEEPER_ORM } end config.after do begin DatabaseCleaner.clean rescue NameError # ActiveRecord might not be defined in some tests end end config.order = "random" end ================================================ FILE: spec/spec_helper_integration.rb ================================================ # frozen_string_literal: true # For compatibility only require "spec_helper" ================================================ FILE: spec/support/dependencies/factory_bot.rb ================================================ # frozen_string_literal: true require "factory_bot" FactoryBot.find_definitions ================================================ FILE: spec/support/doorkeeper_rspec.rb ================================================ # frozen_string_literal: true module Doorkeeper class RSpec # Print's useful information about env: Ruby / Rails versions, # Doorkeeper configuration, etc. def self.print_configuration_info puts <<-INFO.strip_heredoc ====> Doorkeeper ORM: '#{Doorkeeper.configuration.orm}' ====> Doorkeeper version: #{Doorkeeper.gem_version} ====> Rails version: #{::Rails.version} ====> Ruby version: #{RUBY_VERSION} on #{RUBY_PLATFORM} INFO end # Tries to find ORM from the Gemfile used to run test suite def self.detect_orm orm = (ENV["BUNDLE_GEMFILE"] || "").match(/Gemfile\.(.+)\.rb/) (orm && orm[1] || ENV["ORM"] || :active_record).to_sym end end end ================================================ FILE: spec/support/helpers/access_token_request_helper.rb ================================================ # frozen_string_literal: true module AccessTokenRequestHelper def client_is_authorized(client, resource_owner, access_token_attributes = {}) attributes = { application: client, resource_owner_id: resource_owner.id, resource_owner_type: resource_owner.class.name, }.merge(access_token_attributes) FactoryBot.create(:access_token, attributes) end end RSpec.configuration.send :include, AccessTokenRequestHelper ================================================ FILE: spec/support/helpers/authorization_request_helper.rb ================================================ # frozen_string_literal: true module AuthorizationRequestHelper def resource_owner_is_authenticated(resource_owner = nil) resource_owner ||= User.create!(name: "Joe", password: "sekret") Doorkeeper.config.instance_variable_set(:@authenticate_resource_owner, proc { resource_owner }) end def resource_owner_is_not_authenticated Doorkeeper.config.instance_variable_set(:@authenticate_resource_owner, proc { redirect_to("/sign_in") }) end def default_scopes_exist(*scopes) Doorkeeper.config.instance_variable_set(:@default_scopes, Doorkeeper::OAuth::Scopes.from_array(scopes)) end def optional_scopes_exist(*scopes) Doorkeeper.config.instance_variable_set(:@optional_scopes, Doorkeeper::OAuth::Scopes.from_array(scopes)) end def client_should_be_authorized(client) expect(client.access_grants.size).to eq(1) end def client_should_not_be_authorized(client) expect(client.size).to eq(0) end def i_should_be_on_client_callback(client) expect(client.redirect_uri).to eq("#{current_uri.scheme}://#{current_uri.host}#{current_uri.path}") end def allowing_forgery_protection(&_block) original_value = ActionController::Base.allow_forgery_protection ActionController::Base.allow_forgery_protection = true yield ensure ActionController::Base.allow_forgery_protection = original_value end end RSpec.configuration.send :include, AuthorizationRequestHelper ================================================ FILE: spec/support/helpers/config_helper.rb ================================================ # frozen_string_literal: true module ConfigHelper def config_is_set(setting, value = nil, &block) setting_ivar = "@#{setting}" value = block_given? ? block : value Doorkeeper.config.instance_variable_set(setting_ivar, value) end end RSpec.configuration.send :include, ConfigHelper ================================================ FILE: spec/support/helpers/model_helper.rb ================================================ # frozen_string_literal: true module ModelHelper def client_exists(client_attributes = {}) @client = FactoryBot.create(:application, client_attributes) end def create_resource_owner @resource_owner = User.create!(name: "Joe", password: "sekret") end def authorization_code_exists(options = {}) @authorization = FactoryBot.create(:access_grant, options) end def access_token_exists(options = {}) @access_token = FactoryBot.create(:access_token, options) end def access_grant_should_exist_for(client, resource_owner) grant = Doorkeeper::AccessGrant.first expect(grant.application).to have_attributes(id: client.id) .and(be_instance_of(Doorkeeper::Application)) expect(grant.resource_owner_id).to eq(resource_owner.id) end def access_token_should_exist_for(client, resource_owner) token = Doorkeeper::AccessToken.first expect(token.application).to have_attributes(id: client.id) .and(be_instance_of(Doorkeeper::Application)) expect(token.resource_owner_id).to eq(resource_owner.id) end def access_grant_should_not_exist expect(Doorkeeper::AccessGrant.all).to be_empty end def access_token_should_not_exist expect(Doorkeeper::AccessToken.all).to be_empty end def access_grant_should_have_scopes(*args) grant = Doorkeeper::AccessGrant.first expect(grant.scopes).to eq(Doorkeeper::OAuth::Scopes.from_array(args)) end def access_token_should_have_scopes(*args) grant = Doorkeeper::AccessToken.last expect(grant.scopes).to eq(Doorkeeper::OAuth::Scopes.from_array(args)) end def uniqueness_error case DOORKEEPER_ORM when :active_record ActiveRecord::RecordNotUnique when :sequel error_classes = [Sequel::UniqueConstraintViolation, Sequel::ValidationFailed] proc { |error| expect(error.class).to be_in(error_classes) } when /mongoid/ error_classes = [Mongoid::Errors::Validations] error_classes << Moped::Errors::OperationFailure if defined?(::Moped) # Mongoid 4 error_classes << Mongo::Error::OperationFailure if defined?(::Mongo) # Mongoid 5 proc { |error| expect(error.class).to be_in(error_classes) } else raise "'#{DOORKEEPER_ORM}' ORM is not supported!" end end end RSpec.configuration.send :include, ModelHelper ================================================ FILE: spec/support/helpers/request_spec_helper.rb ================================================ # frozen_string_literal: true module RequestSpecHelper def i_am_logged_in allow(Doorkeeper.configuration).to receive(:authenticate_admin).and_return(->(*) {}) end def i_should_see(content) expect(page).to have_content(content) end def i_should_not_see(content) expect(page).to have_no_content(content) end def i_should_be_on(path) expect(page).to have_current_path(path, ignore_query: true) end def url_should_have_param(param, value) expect(current_params[param]).to eq(value) end def url_should_not_have_param(param) expect(current_params).not_to have_key(param) end def current_params Rack::Utils.parse_query(current_uri.query) end def current_uri URI.parse(page.current_url) end def request_response respond_to?(:response) ? response : page.driver.response end def json_response JSON.parse(request_response.body) end def should_have_status(status) expect(page.driver.response.status).to eq(status) end def with_access_token_header(token) with_header "Authorization", "Bearer #{token}" end def with_header(header, value) page.driver.header(header, value) end def basic_auth_header_for_client(client) ActionController::HttpAuthentication::Basic.encode_credentials client.uid, client.secret end def sign_in visit "/" click_on "Sign in" end def create_access_token(authorization_code, client, code_verifier = nil) page.driver.post token_endpoint_url(code: authorization_code, client: client, code_verifier: code_verifier) end def i_should_see_translated_error_message(key) i_should_see translated_error_message(key) end def i_should_not_see_translated_error_message(key) i_should_not_see translated_error_message(key) end def translated_error_message(key) I18n.translate(key, scope: %i[doorkeeper errors messages]) end def i_should_see_translated_invalid_request_error_message(key, value) i_should_see translated_invalid_request_error_message(key, value) end def translated_invalid_request_error_message(key, value) I18n.translate key, scope: %i[doorkeeper errors messages invalid_request], value: value end def response_status_should_be(status) expect(request_response.status.to_i).to eq(status) end end RSpec.configuration.send :include, RequestSpecHelper ================================================ FILE: spec/support/helpers/url_helper.rb ================================================ # frozen_string_literal: true module UrlHelper def token_endpoint_url(options = {}) parameters = { code: options[:code], client_id: options[:client_id] || options[:client].try(:uid), client_secret: options[:client_secret] || options[:client].try(:secret), redirect_uri: options[:redirect_uri] || options[:client].try(:redirect_uri), grant_type: options[:grant_type] || "authorization_code", code_verifier: options[:code_verifier], code_challenge_method: options[:code_challenge_method], }.reject { |_, v| v.blank? } "/oauth/token?#{build_query(parameters)}" end def password_token_endpoint_url(options = {}) parameters = { code: options[:code], client_id: options[:client_id] || options[:client].try(:uid), client_secret: options[:client_secret] || options[:client].try(:secret), username: options[:resource_owner_username] || options[:resource_owner].try(:name), password: options[:resource_owner_password] || options[:resource_owner].try(:password), scope: options[:scope], grant_type: "password", }.reject { |_, v| v.blank? } "/oauth/token?#{build_query(parameters)}" end def authorization_endpoint_url(options = {}) parameters = { client_id: options[:client_id] || options[:client].try(:uid), redirect_uri: options[:redirect_uri] || options[:client].try(:redirect_uri), response_type: options[:response_type] || "code", response_mode: options[:response_mode] || "", scope: options[:scope], state: options[:state], code_challenge: options[:code_challenge], code_challenge_method: options[:code_challenge_method], }.reject { |_, v| v.blank? } "/oauth/authorize?#{build_query(parameters)}" end def refresh_token_endpoint_url(options = {}) parameters = { refresh_token: options[:refresh_token], client_id: options[:client_id] || options[:client].try(:uid), client_secret: options[:client_secret] || options[:client].try(:secret), grant_type: options[:grant_type] || "refresh_token", }.reject { |_, v| v.blank? } "/oauth/token?#{build_query(parameters)}" end def revocation_token_endpoint_url "/oauth/revoke" end def build_query(hash) Rack::Utils.build_query(hash) end end RSpec.configuration.send :include, UrlHelper ================================================ FILE: spec/support/orm/active_record.rb ================================================ # frozen_string_literal: true # load schema to in memory sqlite ActiveRecord::Migration.verbose = false load Rails.root + "db/schema.rb" ================================================ FILE: spec/support/render_with_matcher.rb ================================================ # frozen_string_literal: true # Adds the `render_with` matcher. # Ex: # expect(controller).to render_with(template: :show, locals: { alpha: "beta" }) # module RenderWithMatcher def self.included(base) # Setup spying for our "render_with" matcher base.before do allow(controller).to receive(:render).and_wrap_original do |original, *args, **kwargs, &block| original.call(*args, **kwargs, &block) end end end RSpec::Matchers.define :render_with do |expected| match do |actual| have_received(:render).with(expected).matches?(actual) end end end RSpec.configure do |config| config.include RenderWithMatcher, type: :controller end ================================================ FILE: spec/support/shared/controllers_shared_context.rb ================================================ # frozen_string_literal: true shared_context "valid token", token: :valid do let(:token_string) { "1A2B3C4D" } let :token do double( Doorkeeper::AccessToken, accessible?: true, includes_scope?: true, acceptable?: true, previous_refresh_token: "", revoke_previous_refresh_token!: true, ) end before do allow( Doorkeeper::AccessToken, ).to receive(:by_token).with(token_string).and_return(token) end end shared_context "invalid token", token: :invalid do let(:token_string) { "1A2B3C4D" } let :token do double( Doorkeeper::AccessToken, accessible?: false, revoked?: false, expired?: false, includes_scope?: false, acceptable?: false, previous_refresh_token: "", revoke_previous_refresh_token!: true, ) end before do allow( Doorkeeper::AccessToken, ).to receive(:by_token).with(token_string).and_return(token) end end shared_context "expired token", token: :expired do let :token_string do "1A2B3C4DEXP" end let :token do double( Doorkeeper::AccessToken, accessible?: false, revoked?: false, expired?: true, includes_scope?: false, acceptable?: false, previous_refresh_token: "", revoke_previous_refresh_token!: true, ) end before do allow( Doorkeeper::AccessToken, ).to receive(:by_token).with(token_string).and_return(token) end end shared_context "revoked token", token: :revoked do let :token_string do "1A2B3C4DREV" end let :token do double( Doorkeeper::AccessToken, accessible?: false, revoked?: true, expired?: false, includes_scope?: false, acceptable?: false, previous_refresh_token: "", revoke_previous_refresh_token!: true, ) end before do allow( Doorkeeper::AccessToken, ).to receive(:by_token).with(token_string).and_return(token) end end shared_context "forbidden token", token: :forbidden do let :token_string do "1A2B3C4DFORB" end let :token do double( Doorkeeper::AccessToken, accessible?: true, includes_scope?: true, acceptable?: false, previous_refresh_token: "", revoke_previous_refresh_token!: true, ) end before do allow( Doorkeeper::AccessToken, ).to receive(:by_token).with(token_string).and_return(token) end end ================================================ FILE: spec/support/shared/hashing_shared_context.rb ================================================ # frozen_string_literal: true shared_context "with token hashing enabled" do let(:hashed_or_plain_token_func) do Doorkeeper::SecretStoring::Sha256Hash.method(:transform_secret) end before do Doorkeeper.configure do orm DOORKEEPER_ORM hash_token_secrets end end end shared_context "with token hashing and fallback lookup enabled" do let(:hashed_or_plain_token_func) do Doorkeeper::SecretStoring::Sha256Hash.method(:transform_secret) end before do Doorkeeper.configure do orm DOORKEEPER_ORM hash_token_secrets fallback: :plain end end end shared_context "with application hashing enabled" do let(:hashed_or_plain_token_func) do Doorkeeper::SecretStoring::Sha256Hash.method(:transform_secret) end before do Doorkeeper.configure do orm DOORKEEPER_ORM hash_application_secrets end end end ================================================ FILE: spec/support/shared/models_shared_examples.rb ================================================ # frozen_string_literal: true shared_examples "an accessible token" do describe "#accessible?" do it "is accessible if token is not expired" do allow(subject).to receive(:expired?).and_return(false) expect(subject).to be_accessible end it "is not accessible if token is expired" do allow(subject).to receive(:expired?).and_return(true) expect(subject).not_to be_accessible end end end shared_examples "a revocable token" do describe "#accessible?" do before { subject.save! } it "is accessible if token is not revoked" do expect(subject).to be_accessible end it "is not accessible if token is revoked" do subject.revoke expect(subject).not_to be_accessible end end end shared_examples "a unique token" do describe "#token" do let(:owner) { FactoryBot.create(:resource_owner) } it "is generated before validation" do expect { subject.valid? }.to change(subject, :token).from(nil) end it "is not valid if token exists" do token1 = FactoryBot.create factory_name, resource_owner_id: owner.id, resource_owner_type: owner.class.name token2 = FactoryBot.create factory_name, resource_owner_id: owner.id, resource_owner_type: owner.class.name token2.token = token1.token expect(token2).not_to be_valid end it "expects database to throw an error when tokens are the same" do token1 = FactoryBot.create factory_name, resource_owner_id: owner.id, resource_owner_type: owner.class.name token2 = FactoryBot.create factory_name, resource_owner_id: owner.id, resource_owner_type: owner.class.name token2.token = token1.token expect do token2.save!(validate: false) end.to raise_error(uniqueness_error) end end end