├── .codeclimate.yml ├── .coveralls.yml ├── .dockerignore ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── check_orm_changes.yml │ └── ci.yml ├── .gitignore ├── .hound.yml ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── 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.rb ├── doorkeeper │ ├── config.rb │ ├── config │ │ ├── abstract_builder.rb │ │ ├── option.rb │ │ └── validations.rb │ ├── engine.rb │ ├── errors.rb │ ├── grant_flow.rb │ ├── grant_flow │ │ ├── fallback_flow.rb │ │ ├── flow.rb │ │ └── registry.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 │ ├── oauth.rb │ ├── oauth │ │ ├── authorization │ │ │ ├── code.rb │ │ │ ├── context.rb │ │ │ ├── token.rb │ │ │ └── uri_builder.rb │ │ ├── authorization_code_request.rb │ │ ├── base_request.rb │ │ ├── base_response.rb │ │ ├── client.rb │ │ ├── client │ │ │ └── credentials.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 │ ├── orm │ │ ├── active_record.rb │ │ └── 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 │ ├── rails │ │ ├── helpers.rb │ │ ├── routes.rb │ │ └── routes │ │ │ ├── abstract_router.rb │ │ │ ├── mapper.rb │ │ │ ├── mapping.rb │ │ │ └── registry.rb │ ├── rake.rb │ ├── rake │ │ ├── db.rake │ │ └── setup.rake │ ├── request.rb │ ├── request │ │ ├── authorization_code.rb │ │ ├── client_credentials.rb │ │ ├── code.rb │ │ ├── password.rb │ │ ├── refresh_token.rb │ │ ├── strategy.rb │ │ └── token.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 └── 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_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.ru │ ├── 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 │ ├── 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 │ │ └── favicon.ico │ └── 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_spec.rb │ ├── grant_flow │ │ └── flow_spec.rb │ ├── grant_flow_spec.rb │ ├── models │ │ ├── expirable_spec.rb │ │ ├── reusable_spec.rb │ │ ├── revocable_spec.rb │ │ ├── scopes_spec.rb │ │ └── secret_storable_spec.rb │ ├── oauth │ │ ├── authorization │ │ │ └── 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 └── vendor └── assets └── stylesheets └── doorkeeper └── bootstrap.min.css /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_patterns: 2 | - "lib/doorkeeper/config.rb" 3 | - "spec/" 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{rb,json}] 4 | indent_style = space 5 | indent_size = 2 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: doorkeeper-gem 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | What we need to do to see your problem or bug? 3 | 4 | The more detailed the issue, the more likely that we will fix it ASAP. 5 | 6 | Don't use GitHub issues for questions like "How can I do that?" — 7 | use [StackOverflow](https://stackoverflow.com/questions/tagged/doorkeeper) 8 | instead with the corresponding tag. 9 | 10 | ### Expected behavior 11 | Tell us what should happen 12 | 13 | ### Actual behavior 14 | Tell us what happens instead 15 | 16 | ### System configuration 17 | You can help us to understand your problem if you will share some very 18 | useful information about your project environment (don't forget to 19 | remove any confidential data if it exists). 20 | 21 | **Doorkeeper initializer**: 22 | 23 | ```ruby 24 | # config/initializers/doorkeeper.rb 25 | Doorkeeper.configure do 26 | # ... 27 | end 28 | ``` 29 | 30 | **Ruby version**: `` 31 | 32 | **Gemfile.lock**: 33 | 34 |
35 | Gemfile.lock content 36 | 37 | ``` 38 | Place your Gemfile.lock content here 39 | ``` 40 |
41 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | Provide a general description of the code changes in your pull 4 | request... were there any bugs you had fixed? If so, mention them. If 5 | these bugs have open GitHub issues, be sure to tag them here as well, 6 | to keep the conversation linked together. 7 | 8 | ### Other Information 9 | 10 | If there's anything else that's important and relevant to your pull 11 | request, mention that information here. This could include 12 | benchmarks, or other information. 13 | 14 | If you are updating CHANGELOG.md file or are asked to update it by reviewers, 15 | please add the changelog entry at the top of the file. 16 | 17 | Thanks for contributing to Doorkeeper project! 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/check_orm_changes.yml: -------------------------------------------------------------------------------- 1 | name: Check ORM changes 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | check_orm_changes_and_notify: 8 | name: Notify ORM extensions about required code updates 9 | runs-on: ubuntu-latest 10 | if: | 11 | !contains(github.event.head_commit.message, 'ga skip') && 12 | !contains(github.event.head_commit.message, 'skip ga') 13 | env: 14 | ISSUE_TITLE: "ORM requires update due to latest Doorkeeper changes" 15 | ISSUE_BODY: | 16 | Doorkeeper has ORM changes merged to `main` branch. 17 | 18 | See https://github.com/doorkeeper-gem/doorkeeper/commit/${{ github.sha }} for the details. 19 | ISSUE_LABELS: ORM 20 | steps: 21 | - name: List & cache git modified files 22 | id: changed_files 23 | uses: lots0logs/gh-action-get-changed-files@2.2.2 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | - name: Check if ORM files were changed 27 | id: check_orm_files 28 | run: | 29 | changed_files=$(cat ${HOME}/files.json) 30 | 31 | if [[ $changed_files == *"lib/doorkeeper/orm/active_record"* ]] || \ 32 | [[ $changed_files == *"lib/doorkeeper/models/"* ]]; 33 | then 34 | echo "ORM files changes detected!" 35 | echo "ORM_CHANGES=yes" >> $GITHUB_ENV 36 | else 37 | echo "No changes in ORM files, exiting" 38 | fi 39 | - name: Create an issue for Mongo extension 40 | id: create_mongodb_issue 41 | if: env.ORM_CHANGES == 'yes' 42 | uses: dacbd/create-issue-action@main 43 | with: 44 | token: ${{ github.token }} 45 | title: ${{ env.ISSUE_TITLE }} 46 | body: ${{ env.ISSUE_BODY }} 47 | org: doorkeeper 48 | repo: doorkeeper-mongodb 49 | labels: ${{ env.ISSUE_LABELS}} 50 | - name: Create an issue for Sequel extension 51 | id: create_sequel_issue 52 | if: env.ORM_CHANGES == 'yes' 53 | uses: dacbd/create-issue-action@main 54 | with: 55 | token: ${{ github.token }} 56 | title: ${{ env.ISSUE_TITLE }} 57 | body: ${{ env.ISSUE_BODY }} 58 | repo: doorkeeper-sequel 59 | labels: ${{ env.ISSUE_LABELS}} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | .rbx 3 | *.rbc 4 | log/*.log 5 | pkg/ 6 | spec/dummy/db/*.sqlite3 7 | spec/dummy/log/*.log 8 | spec/dummy/tmp/ 9 | spec/generators/tmp 10 | Gemfile.lock 11 | gemfiles/*.lock 12 | .rvmrc 13 | *.swp 14 | .idea 15 | /.yardoc/ 16 | /_yardoc/ 17 | /doc/ 18 | /rdoc/ 19 | coverage 20 | *.gem 21 | gemfiles/vendor 22 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | rubocop: 2 | config_file: .rubocop.yml 3 | version: 1.5.2 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests from everyone. By participating in this project, you agree 4 | to abide by the [code of conduct](CODE_OF_CONDUCT.md). 5 | 6 | Fork, then clone the repo: 7 | 8 | git clone git@github.com:your-username/doorkeeper.git 9 | 10 | ### Docker Setup 11 | 12 | Build the container image with: `docker build --pull -t doorkeeper:test .` 13 | Run the tests with: `docker run -it --rm doorkeeper:test` 14 | 15 | ### Local Setup 16 | 17 | * Set up Ruby dependencies via Bundler 18 | 19 | bundle install 20 | 21 | * Make sure the tests pass: 22 | 23 | rake spec 24 | 25 | * Make your changes. 26 | * Write tests. 27 | * Follow our [style guides](.rubocop.yml). 28 | * Make the tests pass: 29 | 30 | rake spec 31 | 32 | * Add notes about your changes to the `CHANGELOG.md` file. 33 | 34 | * Write a [good commit message][commit]. 35 | * Push to your fork. 36 | * [Submit a pull request][pr]. 37 | 38 | [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 39 | [pr]: https://github.com/doorkeeper-gem/doorkeeper/compare/ 40 | 41 | * If [Hound] catches style violations, fix them. If our bot suggested changes — please add them. 42 | 43 | [hound]: https://houndci.com 44 | 45 | * Wait for us. We try to at least comment on pull requests within one business day. 46 | * We may suggest changes. 47 | * Please, squash your commits to a single one if you introduced a new changes or pushed more than 48 | one commit. Let's keep the history clean. 49 | 50 | Thank you for your contribution! :handshake: 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.3.4-alpine 2 | 3 | # Linux UID (user id) for the doorkeeper user, change with [--build-arg UID=1234] 4 | ARG UID="991" 5 | # Linux GID (group id) for the doorkeeper user, change with [--build-arg GID=1234] 6 | ARG GID="991" 7 | # Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin] 8 | ARG TZ="Etc/UTC" 9 | 10 | # Apply timezone 11 | ENV TZ=${TZ} 12 | 13 | RUN addgroup -g "${GID}" doorkeeper; \ 14 | adduser -u "${UID}" -G "doorkeeper" -h /srv doorkeeper; \ 15 | echo "${TZ}" > /etc/localtime; 16 | 17 | RUN apk add --no-cache \ 18 | ca-certificates \ 19 | wget \ 20 | openssl \ 21 | bash \ 22 | build-base \ 23 | git \ 24 | sqlite-dev \ 25 | tzdata 26 | 27 | ENV LANG=en_US.UTF-8 28 | ENV LANGUAGE=en_US:en 29 | ENV LC_ALL=en_US.UTF-8 30 | 31 | ENV BUNDLER_VERSION=2.5.11 32 | RUN gem install bundler -v ${BUNDLER_VERSION} -i /usr/local/lib/ruby/gems/$(ls /usr/local/lib/ruby/gems) --force 33 | 34 | WORKDIR /srv 35 | 36 | COPY Gemfile doorkeeper.gemspec /srv/ 37 | COPY lib/doorkeeper/version.rb /srv/lib/doorkeeper/version.rb 38 | 39 | # This is a fix for sqlite alpine issues 40 | RUN bundle config force_ruby_platform true 41 | RUN bundle install 42 | 43 | COPY . /srv/ 44 | 45 | RUN chown -R doorkeeper:doorkeeper /srv/coverage /srv/spec/dummy/tmp /srv/spec/generators/tmp 46 | 47 | # Set the running user for resulting container 48 | USER doorkeeper 49 | 50 | CMD ["rake"] 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | gemspec 7 | 8 | gem "rails", ">= 7.0", "< 8.1" 9 | 10 | gem "sprockets-rails" 11 | 12 | gem "rspec-core" 13 | gem "rspec-expectations" 14 | gem "rspec-mocks" 15 | gem "rspec-rails", "~> 8.0" 16 | gem "rspec-support" 17 | 18 | gem "rubocop", "~> 1.4" 19 | gem "rubocop-performance", require: false 20 | gem "rubocop-rails", require: false 21 | gem "rubocop-rspec", require: false 22 | 23 | gem "bcrypt", "~> 3.1", require: false 24 | 25 | gem "activerecord-jdbcsqlite3-adapter", platform: :jruby 26 | gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw] 27 | 28 | gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw] 29 | gem "timecop" 30 | 31 | gem 'irb', '~> 1.8' 32 | 33 | # Interactive Debugging tools 34 | gem 'debug', '~> 1.8' 35 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Applicake. http://applicake.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | Document moved [here](CHANGELOG.md) 2 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing Doorkeeper 2 | 3 | How to release Doorkeeper in five easy steps! 4 | 5 | 1. Update `lib/doorkeeper/version.rb` file accordingly. 6 | 2. Update `CHANGELOG.md` to reflect the changes since last release. 7 | 3. Commit changes: `git commit -am 'Bump to vVERSION'`. 8 | 4. Build and publish the gem. 9 | 4. Create GitHub release. 10 | 5. Announce the new release, making sure to say “thank you” to the contributors 11 | who helped shape this version! 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "rspec/core/rake_task" 5 | 6 | desc "Default: run specs." 7 | task default: :spec 8 | 9 | desc "Run all specs" 10 | RSpec::Core::RakeTask.new(:spec) do |config| 11 | config.verbose = false 12 | end 13 | 14 | namespace :doorkeeper do 15 | desc "Install doorkeeper in dummy app" 16 | task :install do 17 | cd "spec/dummy" 18 | system "bundle exec rails g doorkeeper:install --force" 19 | end 20 | 21 | desc "Runs local test server" 22 | task :server do 23 | cd "spec/dummy" 24 | system "bundle exec rails server" 25 | end 26 | end 27 | 28 | Bundler::GemHelper.install_tasks 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security issues in Doorkeeper 2 | 3 | Hello! Thank you for wanting to disclose a possible security 4 | vulnerability within the Doorkeeper gem! Please follow our disclosure 5 | policy as outlined below: 6 | 7 | 1. Do NOT open up a GitHub issue with your report. Security reports 8 | should be kept private until a possible fix is determined. 9 | 2. Send an email to Nikita Bulai at bulaj.nikita AT gmail.com or one of 10 | the others Doorkeeper maintainers listed in gemspec. You should receive 11 | a prompt response. 12 | 3. Be patient. Since Doorkeeper is in a stable maintenance phase, we want to 13 | do as little as possible to rock the boat of the project. 14 | 15 | Thank you very much for adhering for these policies! 16 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | See [Upgrade Guides](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions) 2 | in the project Wiki. 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/doorkeeper/admin/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require doorkeeper/bootstrap.min 3 | * 4 | *= require_self 5 | *= require_tree . 6 | */ 7 | 8 | .doorkeeper-admin .form-group > .field_with_errors { 9 | width: 16.66667%; 10 | } 11 | -------------------------------------------------------------------------------- /app/assets/stylesheets/doorkeeper/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require doorkeeper/bootstrap.min 3 | * 4 | *= require_self 5 | *= require_tree . 6 | */ 7 | 8 | body { 9 | background-color: #eee; 10 | font-size: 14px; 11 | } 12 | 13 | #container { 14 | background-color: #fff; 15 | border: 1px solid #999; 16 | border: 1px solid rgba(0, 0, 0, 0.2); 17 | border-radius: 6px; 18 | -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); 19 | box-shadow: 0 3px 20px rgba(0, 0, 0, 0.3); 20 | margin: 2em auto; 21 | max-width: 600px; 22 | outline: 0; 23 | padding: 1em; 24 | width: 80%; 25 | } 26 | 27 | .page-header { 28 | margin-top: 20px; 29 | } 30 | 31 | #oauth-permissions { 32 | width: 260px; 33 | } 34 | 35 | .actions { 36 | border-top: 1px solid #eee; 37 | margin-top: 1em; 38 | padding-top: 9px; 39 | } 40 | 41 | .actions > form > .btn { 42 | margin-top: 5px; 43 | } 44 | 45 | .separator { 46 | color: #eee; 47 | padding: 0 .5em; 48 | } 49 | 50 | .inline_block { 51 | display: inline-block; 52 | } 53 | 54 | #oauth { 55 | margin-bottom: 1em; 56 | } 57 | 58 | #oauth > .btn { 59 | width: 7em; 60 | } 61 | 62 | td { 63 | vertical-align: middle !important; 64 | } 65 | -------------------------------------------------------------------------------- /app/controllers/doorkeeper/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class ApplicationController < 5 | Doorkeeper.config.resolve_controller(:base) 6 | include Helpers::Controller 7 | include ActionController::MimeResponds if Doorkeeper.config.api_only 8 | 9 | unless Doorkeeper.config.api_only 10 | protect_from_forgery with: :exception 11 | helper "doorkeeper/dashboard" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/doorkeeper/application_metal_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class ApplicationMetalController < 5 | Doorkeeper.config.resolve_controller(:base_metal) 6 | include Helpers::Controller 7 | 8 | before_action :enforce_content_type, 9 | if: -> { Doorkeeper.config.enforce_content_type } 10 | 11 | ActiveSupport.run_load_hooks(:doorkeeper_metal_controller, self) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/doorkeeper/authorized_applications_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class AuthorizedApplicationsController < Doorkeeper::ApplicationController 5 | before_action :authenticate_resource_owner! 6 | 7 | def index 8 | @applications = Doorkeeper.config.application_model.authorized_for(current_resource_owner) 9 | 10 | respond_to do |format| 11 | format.html 12 | format.json { render json: @applications, current_resource_owner: current_resource_owner } 13 | end 14 | end 15 | 16 | def destroy 17 | Doorkeeper.config.application_model.revoke_tokens_and_grants_for( 18 | params[:id], 19 | current_resource_owner, 20 | ) 21 | 22 | respond_to do |format| 23 | format.html do 24 | redirect_to oauth_authorized_applications_url, notice: I18n.t( 25 | :notice, scope: %i[doorkeeper flash authorized_applications destroy], 26 | ) 27 | end 28 | 29 | format.json { head :no_content } 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/controllers/doorkeeper/token_info_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class TokenInfoController < Doorkeeper::ApplicationMetalController 5 | def show 6 | if doorkeeper_token&.accessible? 7 | render json: doorkeeper_token_to_json, status: :ok 8 | else 9 | error = OAuth::InvalidTokenResponse.new 10 | response.headers.merge!(error.headers) 11 | render json: error_to_json(error), status: error.status 12 | end 13 | end 14 | 15 | protected 16 | 17 | def doorkeeper_token_to_json 18 | doorkeeper_token 19 | end 20 | 21 | def error_to_json(error) 22 | error.body 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/helpers/doorkeeper/dashboard_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module DashboardHelper 5 | def doorkeeper_errors_for(object, method) 6 | return if object.errors[method].blank? 7 | 8 | output = object.errors[method].map do |msg| 9 | content_tag(:span, class: "invalid-feedback") do 10 | msg.capitalize 11 | end 12 | end 13 | 14 | safe_join(output) 15 | end 16 | 17 | def doorkeeper_submit_path(application) 18 | application.persisted? ? oauth_application_path(application) : oauth_applications_path 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/doorkeeper/applications/_delete_form.html.erb: -------------------------------------------------------------------------------- 1 | <%- submit_btn_css ||= 'btn btn-link' %> 2 | <%= form_tag oauth_application_path(application), method: :delete do %> 3 | <%= submit_tag t('doorkeeper.applications.buttons.destroy'), 4 | onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')", 5 | class: submit_btn_css %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/doorkeeper/applications/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

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

3 |
4 | 5 | <%= render 'form', application: @application %> 6 | -------------------------------------------------------------------------------- /app/views/doorkeeper/applications/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

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

3 |
4 | 5 |

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

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <% @applications.each do |application| %> 19 | 20 | 23 | 26 | 29 | 32 | 35 | 36 | <% end %> 37 | 38 |
<%= t('.name') %><%= t('.callback_url') %><%= t('.confidential') %><%= t('.actions') %>
21 | <%= link_to application.name, oauth_application_path(application) %> 22 | 24 | <%= simple_format(application.redirect_uri) %> 25 | 27 | <%= application.confidential? ? t('doorkeeper.applications.index.confidentiality.yes') : t('doorkeeper.applications.index.confidentiality.no') %> 28 | 30 | <%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %> 31 | 33 | <%= render 'delete_form', application: application %> 34 |
39 | -------------------------------------------------------------------------------- /app/views/doorkeeper/applications/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

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

3 |
4 | 5 | <%= render 'form', application: @application %> 6 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorizations/error.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

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

3 |
4 | 5 |
6 |
 7 |     <%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %>
 8 |   
9 |
10 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorizations/form_post.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 5 | <%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false do %> 6 | <% auth.body.compact.each do |key, value| %> 7 | <%= hidden_field_tag key, value %> 8 | <% end %> 9 | <% end %> 10 | 11 | 16 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorizations/new.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

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

9 | 10 | <% if @pre_auth.scopes.count > 0 %> 11 |
12 |

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

13 | 14 | 19 |
20 | <% end %> 21 | 22 |
23 | <%= form_tag oauth_authorization_path, method: :post do %> 24 | <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> 25 | <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %> 26 | <%= hidden_field_tag :state, @pre_auth.state, id: nil %> 27 | <%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %> 28 | <%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %> 29 | <%= hidden_field_tag :scope, @pre_auth.scope, id: nil %> 30 | <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %> 31 | <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %> 32 | <%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %> 33 | <% end %> 34 | <%= form_tag oauth_authorization_path, method: :delete do %> 35 | <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> 36 | <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %> 37 | <%= hidden_field_tag :state, @pre_auth.state, id: nil %> 38 | <%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %> 39 | <%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %> 40 | <%= hidden_field_tag :scope, @pre_auth.scope, id: nil %> 41 | <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %> 42 | <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %> 43 | <%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %> 44 | <% end %> 45 |
46 |
47 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorizations/show.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | <%= params[:code] %> 7 |
8 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorized_applications/_delete_form.html.erb: -------------------------------------------------------------------------------- 1 | <%- submit_btn_css ||= 'btn btn-link' %> 2 | <%= form_tag oauth_authorized_application_path(application), method: :delete do %> 3 | <%= submit_tag t('doorkeeper.authorized_applications.buttons.revoke'), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /app/views/doorkeeper/authorized_applications/index.html.erb: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% @applications.each do |application| %> 16 | 17 | 18 | 19 | 20 | 21 | <% end %> 22 | 23 |
<%= 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 %>
24 |
25 | -------------------------------------------------------------------------------- /app/views/layouts/doorkeeper/admin.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= t('doorkeeper.layouts.admin.title') %> 8 | <%= stylesheet_link_tag "doorkeeper/admin/application" %> 9 | <%= csrf_meta_tags %> 10 | 11 | 12 | 28 | 29 |
30 | <%- if flash[:notice].present? %> 31 |
32 | <%= flash[:notice] %> 33 |
34 | <% end -%> 35 | 36 | <%= yield %> 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /app/views/layouts/doorkeeper/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= t('doorkeeper.layouts.application.title') %> 5 | 6 | 7 | 8 | 9 | <%= stylesheet_link_tag "doorkeeper/application" %> 10 | <%= csrf_meta_tags %> 11 | 12 | 13 |
14 | <%- if flash[:notice].present? %> 15 |
16 | <%= flash[:notice] %> 17 |
18 | <% end -%> 19 | 20 | <%= yield %> 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /benchmark/ruby/client_credentials.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "..", "lib")) 4 | 5 | begin 6 | require "bundler/inline" 7 | rescue LoadError => e 8 | warn "Bundler version 1.10 or later is required. Please update your Bundler" 9 | raise e 10 | end 11 | 12 | gemfile(true) do 13 | source "https://rubygems.org" 14 | 15 | gem "sqlite3" 16 | gem "rails" 17 | gem "benchmark-ips" 18 | end 19 | 20 | require "benchmark/ips" 21 | require "ostruct" 22 | require "doorkeeper" 23 | require "active_support/all" 24 | require "active_record/railtie" 25 | 26 | ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") 27 | ActiveRecord::Base.logger = ENV["LOGGER"].present? ? Logger.new(STDOUT) : nil 28 | 29 | # Load database schema 30 | load File.expand_path("../../spec/dummy/db/schema.rb", __dir__) 31 | 32 | Doorkeeper.configure do 33 | orm :active_record 34 | 35 | grant_flows %w[password authorization_code client_credentials] 36 | 37 | skip_authorization do 38 | true 39 | end 40 | end 41 | 42 | client = Doorkeeper::Application.create!( 43 | name: "test", 44 | uid: "123456789", 45 | secret: "987654321", 46 | redirect_uri: "https://doorkeeper.test", 47 | ) 48 | 49 | context = OpenStruct.new 50 | request = OpenStruct.new 51 | request.parameters = { 52 | client_id: client.uid, 53 | client_secret: client.secret, 54 | }.with_indifferent_access 55 | context.request = request 56 | 57 | Benchmark.ips do |ips| 58 | ips.report("Client credentials") do 59 | server = Doorkeeper::Server.new(context) 60 | strategy = server.token_request("client_credentials") 61 | strategy.authorize 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /benchmark/wrk/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorkeeper-gem/doorkeeper/9218cdec8eb295366ffe8ec9129c377b30c00bea/benchmark/wrk/.keep -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "rails/all" 6 | require "active_support/all" 7 | require "irb" 8 | require "debug" 9 | require "doorkeeper" 10 | 11 | Rails.logger = Logger.new(STDOUT) 12 | 13 | Rails.logger.info("Doorkeeper version: #{Doorkeeper::VERSION::STRING}") 14 | Rails.logger.info("Rails version: #{Rails::VERSION::STRING}") 15 | 16 | # Default Doorkeeper config 17 | Doorkeeper.configure do 18 | orm :active_record 19 | end 20 | 21 | # Generate in-memory database for testing 22 | ActiveRecord::Base.establish_connection( 23 | adapter: "sqlite3", 24 | database: ":memory:", 25 | ) 26 | 27 | # Load database schema 28 | load File.expand_path("../spec/dummy/db/schema.rb", __dir__) 29 | 30 | IRB.start(__FILE__) 31 | -------------------------------------------------------------------------------- /gemfiles/rails_7_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 7.0.0" 6 | gem "rspec-core" 7 | gem "rspec-expectations" 8 | gem "rspec-mocks" 9 | gem "rspec-rails", "~> 5.0" 10 | gem "rspec-support" 11 | gem "rubocop", "~> 1.4" 12 | gem "rubocop-performance", require: false 13 | gem "rubocop-rails", require: false 14 | gem "rubocop-rspec", require: false 15 | gem "bcrypt", "~> 3.1", require: false 16 | gem "activerecord-jdbcsqlite3-adapter", platform: :jruby 17 | gem "sprockets-rails" 18 | gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw] 19 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] 20 | gem "timecop" 21 | gem "base64" 22 | gem "drb" 23 | gem "mutex_m" 24 | gem "concurrent-ruby", "1.3.4" 25 | 26 | gemspec path: "../" 27 | -------------------------------------------------------------------------------- /gemfiles/rails_7_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 7.1.0" 6 | gem "rspec-core" 7 | gem "rspec-expectations" 8 | gem "rspec-mocks" 9 | gem "rspec-rails", "~> 7.1" 10 | gem "rspec-support" 11 | gem "rubocop", "~> 1.4" 12 | gem "rubocop-performance", require: false 13 | gem "rubocop-rails", require: false 14 | gem "rubocop-rspec", require: false 15 | gem "bcrypt", "~> 3.1", require: false 16 | gem "activerecord-jdbcsqlite3-adapter", platform: :jruby 17 | gem "sprockets-rails" 18 | gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw] 19 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] 20 | gem "timecop" 21 | 22 | gemspec path: "../" 23 | -------------------------------------------------------------------------------- /gemfiles/rails_7_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 7.2.0" 6 | gem "rspec-core" 7 | gem "rspec-expectations" 8 | gem "rspec-mocks" 9 | gem "rspec-rails", "~> 7.1" 10 | gem "rspec-support" 11 | gem "rubocop", "~> 1.4" 12 | gem "rubocop-performance", require: false 13 | gem "rubocop-rails", require: false 14 | gem "rubocop-rspec", require: false 15 | gem "bcrypt", "~> 3.1", require: false 16 | gem "activerecord-jdbcsqlite3-adapter", platform: :jruby 17 | gem "sprockets-rails" 18 | gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw] 19 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] 20 | gem "timecop" 21 | 22 | gemspec path: "../" 23 | -------------------------------------------------------------------------------- /gemfiles/rails_8_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 8.0.0" 6 | gem "rspec-core" 7 | gem "rspec-expectations" 8 | gem "rspec-mocks" 9 | gem "rspec-rails", "~> 7.1" 10 | gem "rspec-support" 11 | gem "rubocop", "~> 1.4" 12 | gem "rubocop-performance", require: false 13 | gem "rubocop-rails", require: false 14 | gem "rubocop-rspec", require: false 15 | gem "bcrypt", "~> 3.1", require: false 16 | gem "activerecord-jdbcsqlite3-adapter", platform: :jruby 17 | gem "sprockets-rails" 18 | gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw] 19 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] 20 | gem "timecop" 21 | 22 | gemspec path: "../" 23 | -------------------------------------------------------------------------------- /gemfiles/rails_edge.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", git: "https://github.com/rails/rails" 6 | gem "rspec-core" 7 | gem "rspec-expectations" 8 | gem "rspec-mocks" 9 | gem "rspec-rails", "~> 7.1" 10 | gem "rspec-support" 11 | gem "rubocop", "~> 1.4" 12 | gem "rubocop-performance", require: false 13 | gem "rubocop-rails", require: false 14 | gem "rubocop-rspec", require: false 15 | gem "bcrypt", "~> 3.1", require: false 16 | gem "activerecord-jdbcsqlite3-adapter", platform: :jruby 17 | gem "sprockets-rails" 18 | gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw] 19 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] 20 | gem "timecop" 21 | 22 | gemspec path: "../" 23 | -------------------------------------------------------------------------------- /lib/doorkeeper/config/abstract_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class Config 5 | # Abstract base class for Doorkeeper and it's extensions configuration 6 | # builder. Instantiates and validates gem configuration. 7 | # 8 | class AbstractBuilder 9 | attr_reader :config 10 | 11 | # @param [Class] config class 12 | # 13 | def initialize(config = Config.new, &block) 14 | @config = config 15 | instance_eval(&block) if block_given? 16 | end 17 | 18 | # Builds and validates configuration. 19 | # 20 | # @return [Doorkeeper::Config] config instance 21 | # 22 | def build 23 | @config.validate! if @config.respond_to?(:validate!) 24 | @config 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/doorkeeper/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class Engine < Rails::Engine 5 | initializer "doorkeeper.params.filter", after: :load_config_initializers do |app| 6 | if Doorkeeper.configured? 7 | parameters = %w[client_secret authentication_token access_token refresh_token] 8 | parameters << "code" if Doorkeeper.config.grant_flows.include?("authorization_code") 9 | app.config.filter_parameters << /^(#{Regexp.union(parameters)})$/ 10 | end 11 | end 12 | 13 | initializer "doorkeeper.routes" do 14 | Doorkeeper::Rails::Routes.install! 15 | end 16 | 17 | initializer "doorkeeper.helpers" do 18 | ActiveSupport.on_load(:action_controller) do 19 | include Doorkeeper::Rails::Helpers 20 | end 21 | end 22 | 23 | config.to_prepare do 24 | Doorkeeper.run_orm_hooks 25 | end 26 | 27 | if defined?(Sprockets) && Sprockets::VERSION.chr.to_i >= 4 28 | initializer "doorkeeper.assets.precompile" do |app| 29 | # Force users to use: 30 | # //= link doorkeeper/admin/application.css 31 | # in Doorkeeper 5 for Sprockets 4 instead of precompile. 32 | # Add note to official docs & Wiki 33 | app.config.assets.precompile += %w[ 34 | doorkeeper/application.css 35 | doorkeeper/admin/application.css 36 | ] 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/doorkeeper/grant_flow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "doorkeeper/grant_flow/flow" 4 | require "doorkeeper/grant_flow/fallback_flow" 5 | require "doorkeeper/grant_flow/registry" 6 | 7 | module Doorkeeper 8 | module GrantFlow 9 | extend Registry 10 | 11 | register( 12 | :implicit, 13 | response_type_matches: "token", 14 | response_mode_matches: %w[fragment form_post], 15 | response_type_strategy: Doorkeeper::Request::Token, 16 | ) 17 | 18 | register( 19 | :authorization_code, 20 | response_type_matches: "code", 21 | response_mode_matches: %w[query fragment form_post], 22 | response_type_strategy: Doorkeeper::Request::Code, 23 | grant_type_matches: "authorization_code", 24 | grant_type_strategy: Doorkeeper::Request::AuthorizationCode, 25 | ) 26 | 27 | register( 28 | :client_credentials, 29 | grant_type_matches: "client_credentials", 30 | grant_type_strategy: Doorkeeper::Request::ClientCredentials, 31 | ) 32 | 33 | register( 34 | :password, 35 | grant_type_matches: "password", 36 | grant_type_strategy: Doorkeeper::Request::Password, 37 | ) 38 | 39 | register( 40 | :refresh_token, 41 | grant_type_matches: "refresh_token", 42 | grant_type_strategy: Doorkeeper::Request::RefreshToken, 43 | ) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/doorkeeper/grant_flow/fallback_flow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module GrantFlow 5 | class FallbackFlow < Flow 6 | def handles_grant_type? 7 | false 8 | end 9 | 10 | def handles_response_type? 11 | false 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/doorkeeper/grant_flow/flow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module GrantFlow 5 | class Flow 6 | attr_reader :name, :grant_type_matches, :grant_type_strategy, 7 | :response_type_matches, :response_type_strategy, 8 | :response_mode_matches 9 | 10 | def initialize(name, **options) 11 | @name = name 12 | @grant_type_matches = options[:grant_type_matches] 13 | @grant_type_strategy = options[:grant_type_strategy] 14 | @response_type_matches = options[:response_type_matches] 15 | @response_type_strategy = options[:response_type_strategy] 16 | @response_mode_matches = options[:response_mode_matches] 17 | end 18 | 19 | def handles_grant_type? 20 | grant_type_matches.present? 21 | end 22 | 23 | def handles_response_type? 24 | response_type_matches.present? 25 | end 26 | 27 | def matches_grant_type?(value) 28 | grant_type_matches === value 29 | end 30 | 31 | def matches_response_type?(value) 32 | response_type_matches === value 33 | end 34 | 35 | def default_response_mode 36 | response_mode_matches[0] 37 | end 38 | 39 | def matches_response_mode?(value) 40 | response_mode_matches.include?(value) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/doorkeeper/grant_flow/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module GrantFlow 5 | module Registry 6 | mattr_accessor :flows 7 | self.flows = {} 8 | 9 | mattr_accessor :aliases 10 | self.aliases = {} 11 | 12 | # Allows to register custom OAuth grant flow so that Doorkeeper 13 | # could recognize and process it. 14 | # 15 | def register(name_or_flow, **options) 16 | unless name_or_flow.is_a?(Doorkeeper::GrantFlow::Flow) 17 | name_or_flow = Flow.new(name_or_flow, **options) 18 | end 19 | 20 | flow_key = name_or_flow.name.to_sym 21 | 22 | if flows.key?(flow_key) 23 | ::Kernel.warn <<~WARNING 24 | [DOORKEEPER] '#{flow_key}' grant flow already registered and will be overridden 25 | in #{caller(1..1).first} 26 | WARNING 27 | end 28 | 29 | flows[flow_key] = name_or_flow 30 | end 31 | 32 | # Allows to register aliases that could be used in `grant_flows` 33 | # configuration option. It is possible to have aliases like 1:1 or 34 | # 1:N, i.e. "implicit_oidc" => ['token', 'id_token', 'id_token token']. 35 | # 36 | def register_alias(alias_name, **options) 37 | aliases[alias_name.to_sym] = Array.wrap(options.fetch(:as)) 38 | end 39 | 40 | def expand_alias(alias_name) 41 | aliases.fetch(alias_name.to_sym, []) 42 | end 43 | 44 | # [NOTE]: make it to use #fetch after removing fallbacks 45 | def get(name) 46 | flows[name.to_sym] 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/doorkeeper/grape/authorization_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Grape 5 | class AuthorizationDecorator < SimpleDelegator 6 | def parameters 7 | params 8 | end 9 | 10 | def authorization 11 | env = __getobj__.env 12 | env["HTTP_AUTHORIZATION"] || 13 | env["X-HTTP_AUTHORIZATION"] || 14 | env["X_HTTP_AUTHORIZATION"] || 15 | env["REDIRECT_X_HTTP_AUTHORIZATION"] 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/doorkeeper/grape/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "doorkeeper/grape/authorization_decorator" 4 | 5 | module Doorkeeper 6 | module Grape 7 | # Doorkeeper helpers for Grape applications. 8 | # Provides helpers for endpoints authorization based on defined set of scopes. 9 | module Helpers 10 | # These helpers are for grape >= 0.10 11 | extend ::Grape::API::Helpers 12 | include Doorkeeper::Rails::Helpers 13 | 14 | # endpoint specific scopes > parameter scopes > default scopes 15 | def doorkeeper_authorize!(*scopes) 16 | endpoint_scopes = endpoint.route_setting(:scopes) || 17 | endpoint.options[:route_options][:scopes] 18 | 19 | scopes = if endpoint_scopes 20 | Doorkeeper::OAuth::Scopes.from_array(endpoint_scopes) 21 | elsif scopes.present? 22 | Doorkeeper::OAuth::Scopes.from_array(scopes) 23 | end 24 | 25 | super(*scopes) 26 | end 27 | 28 | def doorkeeper_render_error_with(error) 29 | status_code = error_status_codes[error.status] 30 | error!({ error: error.description }, status_code, error.headers) 31 | end 32 | 33 | private 34 | 35 | def endpoint 36 | env["api.endpoint"] 37 | end 38 | 39 | def doorkeeper_token 40 | @doorkeeper_token ||= OAuth::Token.authenticate( 41 | decorated_request, 42 | *Doorkeeper.config.access_token_methods, 43 | ) 44 | end 45 | 46 | def decorated_request 47 | AuthorizationDecorator.new(request) 48 | end 49 | 50 | def error_status_codes 51 | { 52 | unauthorized: 401, 53 | forbidden: 403, 54 | } 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/doorkeeper/models/concerns/accessible.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Models 5 | module Accessible 6 | # Indicates whether the object is accessible (not expired and not revoked). 7 | # 8 | # @return [Boolean] true if object accessible or false in other case 9 | # 10 | def accessible? 11 | !expired? && !revoked? 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/doorkeeper/models/concerns/expirable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Models 5 | module Expirable 6 | # Indicates whether the object is expired (`#expires_in` present and 7 | # expiration time has come). 8 | # 9 | # @return [Boolean] true if object expired and false in other case 10 | def expired? 11 | !!(expires_in && Time.now.utc > expires_at) 12 | end 13 | 14 | # Calculates expiration time in seconds. 15 | # 16 | # @return [Integer, nil] number of seconds if object has expiration time 17 | # or nil if object never expires. 18 | def expires_in_seconds 19 | return nil if expires_in.nil? 20 | 21 | expires = expires_at - Time.now.utc 22 | expires_sec = expires.seconds.round(0) 23 | expires_sec > 0 ? expires_sec : 0 24 | end 25 | 26 | # Expiration time (date time of creation + TTL). 27 | # 28 | # @return [Time, nil] expiration time in UTC 29 | # or nil if the object never expires. 30 | # 31 | def expires_at 32 | expires_in && created_at + expires_in.seconds 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/doorkeeper/models/concerns/orderable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Models 5 | module Orderable 6 | extend ActiveSupport::Concern 7 | 8 | module ClassMethods 9 | def ordered_by(attribute, direction = :asc) 10 | order(attribute => direction) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/doorkeeper/models/concerns/ownership.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Models 5 | module Ownership 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | belongs_to :owner, polymorphic: true, optional: true 10 | validates :owner, presence: true, if: :validate_owner? 11 | end 12 | 13 | def validate_owner? 14 | Doorkeeper.config.confirm_application_owner? 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/doorkeeper/models/concerns/polymorphic_resource_owner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Models 5 | module PolymorphicResourceOwner 6 | module ForAccessGrant 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | if Doorkeeper.config.polymorphic_resource_owner? 11 | belongs_to :resource_owner, polymorphic: true, optional: false 12 | else 13 | validates :resource_owner_id, presence: true 14 | end 15 | end 16 | end 17 | 18 | module ForAccessToken 19 | extend ActiveSupport::Concern 20 | 21 | included do 22 | if Doorkeeper.config.polymorphic_resource_owner? 23 | belongs_to :resource_owner, polymorphic: true, optional: true 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | 31 | -------------------------------------------------------------------------------- /lib/doorkeeper/models/concerns/resource_ownerable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Models 5 | module ResourceOwnerable 6 | extend ActiveSupport::Concern 7 | 8 | module ClassMethods 9 | # Searches for record by Resource Owner considering Doorkeeper 10 | # configuration for resource owner association. 11 | # 12 | # @param resource_owner [ActiveRecord::Base, Integer] 13 | # resource owner 14 | # 15 | # @return [Doorkeeper::AccessGrant, Doorkeeper::AccessToken] 16 | # collection of records 17 | # 18 | def by_resource_owner(resource_owner) 19 | if Doorkeeper.configuration.polymorphic_resource_owner? 20 | where(resource_owner: resource_owner) 21 | else 22 | where(resource_owner_id: resource_owner_id_for(resource_owner)) 23 | end 24 | end 25 | 26 | protected 27 | 28 | # Backward compatible way to retrieve resource owner itself (if 29 | # polymorphic association enabled) or just it's ID. 30 | # 31 | # @param resource_owner [ActiveRecord::Base, Integer] 32 | # resource owner 33 | # 34 | # @return [ActiveRecord::Base, Integer] 35 | # instance of Resource Owner or it's ID 36 | # 37 | def resource_owner_id_for(resource_owner) 38 | if resource_owner.respond_to?(:to_key) 39 | resource_owner.id 40 | else 41 | resource_owner 42 | end 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/doorkeeper/models/concerns/reusable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Models 5 | module Reusable 6 | # Indicates whether the object is reusable (i.e. It is not expired and 7 | # has not crossed reuse_limit). 8 | # 9 | # @return [Boolean] true if can be reused and false in other case 10 | def reusable? 11 | return false if expired? 12 | return true unless expires_in 13 | 14 | threshold_limit = 100 - Doorkeeper.config.token_reuse_limit 15 | expires_in_seconds >= threshold_limit * expires_in / 100 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/doorkeeper/models/concerns/revocable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Models 5 | module Revocable 6 | # Revokes the object (updates `:revoked_at` attribute setting its value 7 | # to the specific time). 8 | # 9 | # @param clock [Time] time object 10 | # 11 | def revoke(clock = Time) 12 | update_attribute(:revoked_at, clock.now.utc) 13 | end 14 | 15 | # Indicates whether the object has been revoked. 16 | # 17 | # @return [Boolean] true if revoked, false in other case 18 | # 19 | def revoked? 20 | !!(revoked_at && revoked_at <= Time.now.utc) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/doorkeeper/models/concerns/scopes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Models 5 | module Scopes 6 | def scopes 7 | OAuth::Scopes.from_string(scopes_string) 8 | end 9 | 10 | def scopes=(value) 11 | if value.is_a?(Array) 12 | super(Doorkeeper::OAuth::Scopes.from_array(value).to_s) 13 | else 14 | super(Doorkeeper::OAuth::Scopes.from_string(value.to_s).to_s) 15 | end 16 | end 17 | 18 | def scopes_string 19 | self[:scopes] 20 | end 21 | 22 | def includes_scope?(*required_scopes) 23 | required_scopes.blank? || required_scopes.any? { |scope| scopes.exists?(scope.to_s) } 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | GRANT_TYPES = [ 6 | AUTHORIZATION_CODE = "authorization_code", 7 | IMPLICIT = "implicit", 8 | PASSWORD = "password", 9 | CLIENT_CREDENTIALS = "client_credentials", 10 | REFRESH_TOKEN = "refresh_token", 11 | ].freeze 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/authorization/code.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | module Authorization 6 | class Code 7 | attr_reader :pre_auth, :resource_owner, :token 8 | 9 | def initialize(pre_auth, resource_owner) 10 | @pre_auth = pre_auth 11 | @resource_owner = resource_owner 12 | end 13 | 14 | def issue_token! 15 | return @token if defined?(@token) 16 | 17 | @token = Doorkeeper.config.access_grant_model.create!(access_grant_attributes) 18 | end 19 | 20 | def oob_redirect 21 | { action: :show, code: token.plaintext_token } 22 | end 23 | 24 | def access_grant? 25 | true 26 | end 27 | 28 | private 29 | 30 | def authorization_code_expires_in 31 | Doorkeeper.config.authorization_code_expires_in 32 | end 33 | 34 | def access_grant_attributes 35 | attributes = { 36 | application_id: pre_auth.client.id, 37 | expires_in: authorization_code_expires_in, 38 | redirect_uri: pre_auth.redirect_uri, 39 | scopes: pre_auth.scopes.to_s, 40 | } 41 | 42 | if Doorkeeper.config.polymorphic_resource_owner? 43 | attributes[:resource_owner] = resource_owner 44 | else 45 | attributes[:resource_owner_id] = resource_owner.id 46 | end 47 | 48 | pkce_attributes.merge(attributes).merge(custom_attributes) 49 | end 50 | 51 | def custom_attributes 52 | # Custom access token attributes are saved into the access grant, 53 | # and then included in subsequently generated access tokens. 54 | @pre_auth.custom_access_token_attributes.to_h.with_indifferent_access 55 | end 56 | 57 | def pkce_attributes 58 | return {} unless pkce_supported? 59 | 60 | { 61 | code_challenge: pre_auth.code_challenge, 62 | code_challenge_method: pre_auth.code_challenge_method, 63 | } 64 | end 65 | 66 | # Ensures firstly, if migration with additional PKCE columns was 67 | # generated and migrated 68 | def pkce_supported? 69 | Doorkeeper.config.access_grant_model.pkce_supported? 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/authorization/context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | module Authorization 6 | class Context 7 | attr_reader :client, :grant_type, :resource_owner, :scopes 8 | 9 | def initialize(**attributes) 10 | attributes.each do |name, value| 11 | instance_variable_set(:"@#{name}", value) if respond_to?(name) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/authorization/uri_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rack/utils" 4 | 5 | module Doorkeeper 6 | module OAuth 7 | module Authorization 8 | class URIBuilder 9 | class << self 10 | def uri_with_query(url, parameters = {}) 11 | uri = URI.parse(url) 12 | original_query = Rack::Utils.parse_query(uri.query) 13 | uri.query = build_query(original_query.merge(parameters)) 14 | uri.to_s 15 | end 16 | 17 | def uri_with_fragment(url, parameters = {}) 18 | uri = URI.parse(url) 19 | uri.fragment = build_query(parameters) 20 | uri.to_s 21 | end 22 | 23 | private 24 | 25 | def build_query(parameters = {}) 26 | parameters.reject! { |_, value| value.blank? } 27 | Rack::Utils.build_query(parameters) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/base_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class BaseRequest 6 | include Validations 7 | 8 | attr_reader :grant_type, :server 9 | 10 | delegate :default_scopes, to: :server 11 | 12 | def authorize 13 | if valid? 14 | before_successful_response 15 | @response = TokenResponse.new(access_token) 16 | after_successful_response 17 | @response 18 | elsif error == Errors::InvalidRequest 19 | @response = InvalidRequestResponse.from_request(self) 20 | else 21 | @response = ErrorResponse.from_request(self) 22 | end 23 | end 24 | 25 | def scopes 26 | @scopes ||= build_scopes 27 | end 28 | 29 | def find_or_create_access_token(client, resource_owner, scopes, custom_attributes, server) 30 | context = Authorization::Token.build_context(client, grant_type, scopes, resource_owner) 31 | application = client.is_a?(Doorkeeper.config.application_model) ? client : client&.application 32 | 33 | token_attributes = { 34 | application: application, 35 | resource_owner: resource_owner, 36 | scopes: scopes, 37 | expires_in: Authorization::Token.access_token_expires_in(server, context), 38 | use_refresh_token: Authorization::Token.refresh_token_enabled?(server, context), 39 | } 40 | 41 | @access_token = 42 | Doorkeeper.config.access_token_model.find_or_create_for(**token_attributes.merge(custom_attributes)) 43 | end 44 | 45 | def before_successful_response 46 | Doorkeeper.config.before_successful_strategy_response.call(self) 47 | end 48 | 49 | def after_successful_response 50 | Doorkeeper.config.after_successful_strategy_response.call(self, @response) 51 | end 52 | 53 | private 54 | 55 | def build_scopes 56 | if @original_scopes.present? 57 | OAuth::Scopes.from_string(@original_scopes) 58 | else 59 | client_scopes = @client&.scopes 60 | return default_scopes if client_scopes.blank? 61 | 62 | # Avoid using Scope#& for dynamic scopes 63 | client_scopes.allowed(default_scopes) 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/base_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class BaseResponse 6 | def body 7 | {} 8 | end 9 | 10 | def description 11 | "" 12 | end 13 | 14 | def headers 15 | {} 16 | end 17 | 18 | def redirectable? 19 | false 20 | end 21 | 22 | def redirect_uri 23 | "" 24 | end 25 | 26 | def status 27 | :ok 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class Client 6 | attr_reader :application 7 | 8 | delegate :id, :name, :uid, :redirect_uri, :scopes, :confidential, to: :@application 9 | 10 | def initialize(application) 11 | @application = application 12 | end 13 | 14 | def self.find(uid, method = Doorkeeper.config.application_model.method(:by_uid)) 15 | return unless (application = method.call(uid)) 16 | 17 | new(application) 18 | end 19 | 20 | def self.authenticate(credentials, method = Doorkeeper.config.application_model.method(:by_uid_and_secret)) 21 | return if credentials.blank? 22 | return unless (application = method.call(credentials.uid, credentials.secret)) 23 | 24 | new(application) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/client/credentials.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class Client 6 | Credentials = Struct.new(:uid, :secret) do 7 | class << self 8 | def from_request(request, *credentials_methods) 9 | credentials_methods.inject(nil) do |_, method| 10 | method = self.method(method) if method.is_a?(Symbol) 11 | credentials = Credentials.new(*method.call(request)) 12 | break credentials if credentials.present? 13 | end 14 | end 15 | 16 | def from_params(request) 17 | request.parameters.values_at(:client_id, :client_secret) 18 | end 19 | 20 | def from_basic(request) 21 | authorization = request.authorization 22 | if authorization.present? && authorization =~ /^Basic (.*)/m 23 | Base64.decode64(Regexp.last_match(1)).split(/:/, 2) 24 | end 25 | end 26 | end 27 | 28 | # Public clients may have their secret blank, but "credentials" are 29 | # still present 30 | delegate :blank?, to: :uid 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/client_credentials/creator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | module ClientCredentials 6 | class Creator 7 | def call(client, scopes, attributes = {}) 8 | existing_token = nil 9 | 10 | if lookup_existing_token? 11 | existing_token = find_active_existing_token_for(client, scopes, attributes) 12 | return existing_token if Doorkeeper.config.reuse_access_token && existing_token&.reusable? 13 | end 14 | 15 | with_revocation(existing_token: existing_token) do 16 | application = client.is_a?(Doorkeeper.config.application_model) ? client : client&.application 17 | Doorkeeper.config.access_token_model.create_for( 18 | application: application, 19 | resource_owner: nil, 20 | scopes: scopes, 21 | **attributes, 22 | ) 23 | end 24 | end 25 | 26 | private 27 | 28 | def with_revocation(existing_token:) 29 | if existing_token && Doorkeeper.config.revoke_previous_client_credentials_token? 30 | existing_token.with_lock do 31 | raise Errors::DoorkeeperError, :invalid_token_reuse if existing_token.revoked? 32 | 33 | existing_token.revoke 34 | 35 | yield 36 | end 37 | else 38 | yield 39 | end 40 | end 41 | 42 | def lookup_existing_token? 43 | Doorkeeper.config.reuse_access_token || 44 | Doorkeeper.config.revoke_previous_client_credentials_token? 45 | end 46 | 47 | def find_active_existing_token_for(client, scopes, attributes) 48 | custom_attributes = Doorkeeper.config.access_token_model. 49 | extract_custom_attributes(attributes).presence 50 | Doorkeeper.config.access_token_model.matching_token_for( 51 | client, nil, scopes, custom_attributes: custom_attributes, include_expired: false) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/client_credentials/issuer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | module ClientCredentials 6 | class Issuer 7 | attr_reader :token, :validator, :error 8 | 9 | def initialize(server, validator) 10 | @server = server 11 | @validator = validator 12 | end 13 | 14 | def create(client, scopes, attributes = {}, creator = Creator.new) 15 | if validator.valid? 16 | @token = create_token(client, scopes, attributes, creator) 17 | @error = Errors::ServerError unless @token 18 | else 19 | @token = false 20 | @error = validator.error 21 | end 22 | 23 | @token 24 | end 25 | 26 | private 27 | 28 | def create_token(client, scopes, attributes, creator) 29 | context = Authorization::Token.build_context( 30 | client, 31 | Doorkeeper::OAuth::CLIENT_CREDENTIALS, 32 | scopes, 33 | nil, 34 | ) 35 | ttl = Authorization::Token.access_token_expires_in(@server, context) 36 | 37 | creator.call( 38 | client, 39 | scopes, 40 | use_refresh_token: false, 41 | expires_in: ttl, 42 | **attributes 43 | ) 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/client_credentials/validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | module ClientCredentials 6 | class Validator 7 | include Validations 8 | include OAuth::Helpers 9 | 10 | validate :client, error: Errors::InvalidClient 11 | validate :client_supports_grant_flow, error: Errors::UnauthorizedClient 12 | validate :scopes, error: Errors::InvalidScope 13 | 14 | def initialize(server, request) 15 | @server = server 16 | @request = request 17 | @client = request.client 18 | 19 | validate 20 | end 21 | 22 | private 23 | 24 | def validate_client 25 | @client.present? 26 | end 27 | 28 | def validate_client_supports_grant_flow 29 | return if @client.blank? 30 | 31 | Doorkeeper.config.allow_grant_flow_for_client?( 32 | Doorkeeper::OAuth::CLIENT_CREDENTIALS, 33 | @client.application, 34 | ) 35 | end 36 | 37 | def validate_scopes 38 | application_scopes = if @client.present? 39 | @client.application.scopes 40 | else 41 | "" 42 | end 43 | return true if @request.scopes.blank? && application_scopes.blank? 44 | 45 | ScopeChecker.valid?( 46 | scope_str: @request.scopes.to_s, 47 | server_scopes: @server.scopes, 48 | app_scopes: application_scopes, 49 | grant_type: Doorkeeper::OAuth::CLIENT_CREDENTIALS, 50 | ) 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/client_credentials_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class ClientCredentialsRequest < BaseRequest 6 | attr_reader :client, :original_scopes, :parameters, :response 7 | 8 | alias error_response response 9 | 10 | delegate :error, to: :issuer 11 | 12 | def initialize(server, client, parameters = {}) 13 | @client = client 14 | @server = server 15 | @response = nil 16 | @original_scopes = parameters[:scope] 17 | @parameters = parameters.except(:scope) 18 | end 19 | 20 | def access_token 21 | issuer.token 22 | end 23 | 24 | def issuer 25 | @issuer ||= ClientCredentials::Issuer.new( 26 | server, 27 | ClientCredentials::Validator.new(server, self), 28 | ) 29 | end 30 | 31 | private 32 | 33 | def valid? 34 | issuer.create(client, scopes, custom_token_attributes_with_data) 35 | end 36 | 37 | def custom_token_attributes_with_data 38 | parameters 39 | .with_indifferent_access 40 | .slice(*Doorkeeper.config.custom_access_token_attributes) 41 | .symbolize_keys 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/code_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class CodeRequest 6 | attr_reader :pre_auth, :resource_owner 7 | 8 | def initialize(pre_auth, resource_owner) 9 | @pre_auth = pre_auth 10 | @resource_owner = resource_owner 11 | end 12 | 13 | def authorize 14 | auth = Authorization::Code.new(pre_auth, resource_owner) 15 | auth.issue_token! 16 | CodeResponse.new(pre_auth, auth, response_on_fragment: pre_auth.response_mode == "fragment") 17 | end 18 | 19 | def deny 20 | pre_auth.error = Errors::AccessDenied 21 | pre_auth.error_response 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/code_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class CodeResponse < BaseResponse 6 | include OAuth::Helpers 7 | 8 | attr_reader :pre_auth, :auth, :response_on_fragment 9 | 10 | def initialize(pre_auth, auth, options = {}) 11 | @pre_auth = pre_auth 12 | @auth = auth 13 | @response_on_fragment = options[:response_on_fragment] 14 | end 15 | 16 | def redirectable? 17 | true 18 | end 19 | 20 | def issued_token 21 | auth.token 22 | end 23 | 24 | def body 25 | if auth.try(:access_token?) 26 | { 27 | access_token: auth.token.plaintext_token, 28 | token_type: auth.token.token_type, 29 | expires_in: auth.token.expires_in_seconds, 30 | state: pre_auth.state, 31 | } 32 | elsif auth.try(:access_grant?) 33 | { 34 | code: auth.token.plaintext_token, 35 | state: pre_auth.state, 36 | } 37 | end 38 | end 39 | 40 | def redirect_uri 41 | if URIChecker.oob_uri?(pre_auth.redirect_uri) 42 | auth.oob_redirect 43 | elsif response_on_fragment 44 | Authorization::URIBuilder.uri_with_fragment(pre_auth.redirect_uri, body) 45 | else 46 | Authorization::URIBuilder.uri_with_query(pre_auth.redirect_uri, body) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | Error = Struct.new(:name, :state, :translate_options) do 6 | def description 7 | options = (translate_options || {}).merge( 8 | scope: %i[doorkeeper errors messages], 9 | default: :server_error, 10 | ) 11 | 12 | I18n.translate(name, **options) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/forbidden_token_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class ForbiddenTokenResponse < ErrorResponse 6 | def self.from_scopes(scopes, attributes = {}) 7 | new(attributes.merge(scopes: scopes)) 8 | end 9 | 10 | def initialize(attributes = {}) 11 | super(attributes.merge(name: :invalid_scope, state: :forbidden)) 12 | @scopes = attributes[:scopes] 13 | end 14 | 15 | def status 16 | :forbidden 17 | end 18 | 19 | def headers 20 | headers = super 21 | headers.delete "WWW-Authenticate" 22 | headers 23 | end 24 | 25 | def description 26 | @description ||= I18n.t("doorkeeper.errors.messages.forbidden_token.missing_scope", 27 | oauth_scopes: @scopes.map(&:to_s).join(" "),) 28 | end 29 | 30 | protected 31 | 32 | def exception_class 33 | Doorkeeper::Errors::TokenForbidden 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/helpers/scope_checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | module Helpers 6 | module ScopeChecker 7 | class Validator 8 | attr_reader :parsed_scopes, :scope_str 9 | 10 | def initialize(scope_str, server_scopes, app_scopes, grant_type) 11 | @parsed_scopes = OAuth::Scopes.from_string(scope_str) 12 | @scope_str = scope_str 13 | @valid_scopes = valid_scopes(server_scopes, app_scopes) 14 | 15 | @scopes_by_grant_type = Doorkeeper.config.scopes_by_grant_type[grant_type.to_sym] if grant_type 16 | end 17 | 18 | def valid? 19 | scope_str.present? && 20 | scope_str !~ /[\n\r\t]/ && 21 | @valid_scopes.has_scopes?(parsed_scopes) && 22 | permitted_to_grant_type? 23 | end 24 | 25 | private 26 | 27 | def valid_scopes(server_scopes, app_scopes) 28 | app_scopes.presence || server_scopes 29 | end 30 | 31 | def permitted_to_grant_type? 32 | return true unless @scopes_by_grant_type 33 | 34 | OAuth::Scopes.from_array(@scopes_by_grant_type) 35 | .has_scopes?(parsed_scopes) 36 | end 37 | end 38 | 39 | def self.valid?(scope_str:, server_scopes:, app_scopes: nil, grant_type: nil) 40 | Validator.new( 41 | scope_str, 42 | server_scopes, 43 | app_scopes, 44 | grant_type, 45 | ).valid? 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/helpers/unique_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | module Helpers 6 | # Default Doorkeeper token generator. Follows OAuth RFC and 7 | # could be customized using `default_generator_method` in 8 | # configuration. 9 | module UniqueToken 10 | def self.generate(options = {}) 11 | # Access Token value must be 1*VSCHAR or 12 | # 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=" 13 | # 14 | # @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.12 15 | # @see https://datatracker.ietf.org/doc/html/rfc6750#section-2.1 16 | # 17 | generator = options.delete(:generator) || SecureRandom.method(default_generator_method) 18 | token_size = options.delete(:size) || 32 19 | generator.call(token_size) 20 | end 21 | 22 | # Generator method for default generator class (SecureRandom) 23 | # 24 | def self.default_generator_method 25 | Doorkeeper.config.default_generator_method 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/hooks/context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | module Hooks 6 | class Context 7 | attr_reader :auth, :pre_auth 8 | 9 | def initialize(**attributes) 10 | attributes.each do |name, value| 11 | instance_variable_set(:"@#{name}", value) if respond_to?(name) 12 | end 13 | end 14 | 15 | def issued_token 16 | auth&.issued_token 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/invalid_request_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class InvalidRequestResponse < ErrorResponse 6 | attr_reader :reason 7 | 8 | def self.from_request(request, attributes = {}) 9 | new( 10 | attributes.merge( 11 | state: request.try(:state), 12 | redirect_uri: request.try(:redirect_uri), 13 | missing_param: request.try(:missing_param), 14 | reason: request.try(:invalid_request_reason), 15 | ), 16 | ) 17 | end 18 | 19 | def initialize(attributes = {}) 20 | super(attributes.merge(name: :invalid_request)) 21 | @missing_param = attributes[:missing_param] 22 | @reason = @missing_param.nil? ? attributes[:reason] : :missing_param 23 | end 24 | 25 | def status 26 | :bad_request 27 | end 28 | 29 | def description 30 | I18n.translate( 31 | reason, 32 | scope: %i[doorkeeper errors messages invalid_request], 33 | default: :unknown, 34 | value: @missing_param, 35 | ) 36 | end 37 | 38 | def exception_class 39 | Doorkeeper::Errors::InvalidRequest 40 | end 41 | 42 | def redirectable? 43 | super && @missing_param != :client_id 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/invalid_token_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class InvalidTokenResponse < ErrorResponse 6 | attr_reader :reason 7 | 8 | def self.from_access_token(access_token, attributes = {}) 9 | reason = if access_token&.revoked? 10 | :revoked 11 | elsif access_token&.expired? 12 | :expired 13 | else 14 | :unknown 15 | end 16 | 17 | new(attributes.merge(reason: reason)) 18 | end 19 | 20 | def initialize(attributes = {}) 21 | super(attributes.merge(name: :invalid_token, state: :unauthorized)) 22 | @reason = attributes[:reason] || :unknown 23 | end 24 | 25 | def status 26 | :unauthorized 27 | end 28 | 29 | def description 30 | @description ||= 31 | I18n.translate( 32 | @reason, 33 | scope: %i[doorkeeper errors messages invalid_token], 34 | ) 35 | end 36 | 37 | protected 38 | 39 | def exception_class 40 | errors_mapping.fetch(reason) 41 | end 42 | 43 | private 44 | 45 | def errors_mapping 46 | { 47 | expired: Doorkeeper::Errors::TokenExpired, 48 | revoked: Doorkeeper::Errors::TokenRevoked, 49 | unknown: Doorkeeper::Errors::TokenUnknown, 50 | } 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/nonstandard.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class NonStandard 6 | # These are not part of the OAuth 2 specification but are still in use by Google 7 | # and in some other implementations. Native applications should use one of the 8 | # approaches discussed in RFC8252. OOB is 'Out of Band' 9 | 10 | # This value signals to the Google Authorization Server that the authorization 11 | # code should be returned in the title bar of the browser, with the page text 12 | # prompting the user to copy the code and paste it in the application. 13 | # This is useful when the client (such as a Windows application) cannot listen 14 | # on an HTTP port without significant client configuration. 15 | 16 | # When you use this value, your application can then detect that the page has loaded, and can 17 | # read the title of the HTML page to obtain the authorization code. It is then up to your 18 | # application to close the browser window if you want to ensure that the user never sees the 19 | # page that contains the authorization code. The mechanism for doing this varies from platform 20 | # to platform. 21 | # 22 | # If your platform doesn't allow you to detect that the page has loaded or read the title of 23 | # the page, you can have the user paste the code back to your application, as prompted by the 24 | # text in the confirmation page that the OAuth 2.0 server generates. 25 | IETF_WG_OAUTH2_OOB = "urn:ietf:wg:oauth:2.0:oob" 26 | 27 | # This is identical to urn:ietf:wg:oauth:2.0:oob, but the text in the confirmation page that 28 | # the OAuth 2.0 server generates won't instruct the user to copy the authorization code, but 29 | # instead will simply ask the user to close the window. 30 | # 31 | # This is useful when your application reads the title of the HTML page (by checking window 32 | # titles on the desktop, for example) to obtain the authorization code, but can't close the 33 | # page on its own. 34 | IETF_WG_OAUTH2_OOB_AUTO = "urn:ietf:wg:oauth:2.0:oob:auto" 35 | 36 | IETF_WG_OAUTH2_OOB_METHODS = [IETF_WG_OAUTH2_OOB, IETF_WG_OAUTH2_OOB_AUTO].freeze 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class Token 6 | class << self 7 | def from_request(request, *methods) 8 | methods.inject(nil) do |_, method| 9 | method = self.method(method) if method.is_a?(Symbol) 10 | credentials = method.call(request) 11 | break credentials if credentials.present? 12 | end 13 | end 14 | 15 | def authenticate(request, *methods) 16 | if (token = from_request(request, *methods)) 17 | access_token = Doorkeeper.config.access_token_model.by_token(token) 18 | if access_token.present? && Doorkeeper.config.refresh_token_enabled? 19 | access_token.revoke_previous_refresh_token! 20 | end 21 | access_token 22 | end 23 | end 24 | 25 | def from_access_token_param(request) 26 | request.parameters[:access_token] 27 | end 28 | 29 | def from_bearer_param(request) 30 | request.parameters[:bearer_token] 31 | end 32 | 33 | def from_bearer_authorization(request) 34 | pattern = /^Bearer /i 35 | header = request.authorization 36 | token_from_header(header, pattern) if match?(header, pattern) 37 | end 38 | 39 | def from_basic_authorization(request) 40 | pattern = /^Basic /i 41 | header = request.authorization 42 | token_from_basic_header(header, pattern) if match?(header, pattern) 43 | end 44 | 45 | private 46 | 47 | def token_from_basic_header(header, pattern) 48 | encoded_header = token_from_header(header, pattern) 49 | decode_basic_credentials_token(encoded_header) 50 | end 51 | 52 | def decode_basic_credentials_token(encoded_header) 53 | Base64.decode64(encoded_header).split(/:/, 2).first 54 | end 55 | 56 | def token_from_header(header, pattern) 57 | header.gsub(pattern, "") 58 | end 59 | 60 | def match?(header, pattern) 61 | header&.match(pattern) 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/token_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class TokenRequest 6 | attr_reader :pre_auth, :resource_owner 7 | 8 | def initialize(pre_auth, resource_owner) 9 | @pre_auth = pre_auth 10 | @resource_owner = resource_owner 11 | end 12 | 13 | def authorize 14 | auth = Authorization::Token.new(pre_auth, resource_owner) 15 | auth.issue_token! 16 | CodeResponse.new(pre_auth, auth, response_on_fragment: true) 17 | end 18 | 19 | def deny 20 | pre_auth.error = Errors::AccessDenied 21 | pre_auth.error_response 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/doorkeeper/oauth/token_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module OAuth 5 | class TokenResponse 6 | attr_reader :token 7 | 8 | alias issued_token token 9 | 10 | def initialize(token) 11 | @token = token 12 | end 13 | 14 | def body 15 | @body ||= { 16 | "access_token" => token.plaintext_token, 17 | "token_type" => token.token_type, 18 | "expires_in" => token.expires_in_seconds, 19 | "refresh_token" => token.plaintext_refresh_token, 20 | "scope" => token.scopes_string, 21 | "created_at" => token.created_at.to_i, 22 | }.reject { |_, value| value.blank? } 23 | end 24 | 25 | def status 26 | :ok 27 | end 28 | 29 | def headers 30 | { 31 | "Cache-Control" => "no-store, no-cache", 32 | "Content-Type" => "application/json; charset=utf-8", 33 | "Pragma" => "no-cache", 34 | } 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/doorkeeper/orm/active_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | autoload :AccessGrant, "doorkeeper/orm/active_record/access_grant" 5 | autoload :AccessToken, "doorkeeper/orm/active_record/access_token" 6 | autoload :Application, "doorkeeper/orm/active_record/application" 7 | autoload :RedirectUriValidator, "doorkeeper/orm/active_record/redirect_uri_validator" 8 | 9 | module Models 10 | autoload :Ownership, "doorkeeper/models/concerns/ownership" 11 | end 12 | 13 | # ActiveRecord ORM for Doorkeeper entity models. 14 | # Consists of three main OAuth entities: 15 | # * Access Token 16 | # * Access Grant 17 | # * Application (client) 18 | # 19 | # Do a lazy loading of all the required and configured stuff. 20 | # 21 | module Orm 22 | module ActiveRecord 23 | autoload :StaleRecordsCleaner, "doorkeeper/orm/active_record/stale_records_cleaner" 24 | 25 | module Mixins 26 | autoload :AccessGrant, "doorkeeper/orm/active_record/mixins/access_grant" 27 | autoload :AccessToken, "doorkeeper/orm/active_record/mixins/access_token" 28 | autoload :Application, "doorkeeper/orm/active_record/mixins/application" 29 | end 30 | 31 | def self.run_hooks 32 | initialize_configured_associations 33 | end 34 | 35 | def self.initialize_configured_associations 36 | if Doorkeeper.config.enable_application_owner? 37 | Doorkeeper.config.application_model.include ::Doorkeeper::Models::Ownership 38 | end 39 | 40 | Doorkeeper.config.access_grant_model.include ::Doorkeeper::Models::PolymorphicResourceOwner::ForAccessGrant 41 | Doorkeeper.config.access_token_model.include ::Doorkeeper::Models::PolymorphicResourceOwner::ForAccessToken 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/doorkeeper/orm/active_record/access_grant.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "doorkeeper/orm/active_record/mixins/access_grant" 4 | 5 | module Doorkeeper 6 | class AccessGrant < ::ActiveRecord::Base 7 | include Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/doorkeeper/orm/active_record/access_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "doorkeeper/orm/active_record/mixins/access_token" 4 | 5 | module Doorkeeper 6 | class AccessToken < ::ActiveRecord::Base 7 | include Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/doorkeeper/orm/active_record/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "doorkeeper/orm/active_record/redirect_uri_validator" 4 | require "doorkeeper/orm/active_record/mixins/application" 5 | 6 | module Doorkeeper 7 | class Application < ::ActiveRecord::Base 8 | include ::Doorkeeper::Orm::ActiveRecord::Mixins::Application 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/doorkeeper/orm/active_record/mixins/access_grant.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper::Orm::ActiveRecord::Mixins 4 | module AccessGrant 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | self.table_name = compute_doorkeeper_table_name 9 | self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default) 10 | 11 | include ::Doorkeeper::AccessGrantMixin 12 | 13 | belongs_to :application, class_name: Doorkeeper.config.application_class.to_s, 14 | optional: true, 15 | inverse_of: :access_grants 16 | 17 | validates :application_id, 18 | :token, 19 | :expires_in, 20 | :redirect_uri, 21 | presence: true 22 | 23 | validates :token, uniqueness: { case_sensitive: true } 24 | 25 | before_validation :generate_token, on: :create 26 | 27 | # We keep a volatile copy of the raw token for initial communication 28 | # The stored refresh_token may be mapped and not available in cleartext. 29 | # 30 | # Some strategies allow restoring stored secrets (e.g. symmetric encryption) 31 | # while hashing strategies do not, so you cannot rely on this value 32 | # returning a present value for persisted tokens. 33 | def plaintext_token 34 | if secret_strategy.allows_restoring_secrets? 35 | secret_strategy.restore_secret(self, :token) 36 | else 37 | @raw_token 38 | end 39 | end 40 | 41 | private 42 | 43 | # Generates token value with UniqueToken class. 44 | # 45 | # @return [String] token value 46 | # 47 | def generate_token 48 | @raw_token = Doorkeeper::OAuth::Helpers::UniqueToken.generate 49 | secret_strategy.store_secret(self, :token, @raw_token) 50 | end 51 | end 52 | 53 | module ClassMethods 54 | private 55 | 56 | def compute_doorkeeper_table_name 57 | table_name = "oauth_access_grant" 58 | table_name = table_name.pluralize if pluralize_table_names 59 | "#{table_name_prefix}#{table_name}#{table_name_suffix}" 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/doorkeeper/orm/active_record/redirect_uri_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "uri" 4 | 5 | module Doorkeeper 6 | # ActiveModel validator for redirect URI validation in according 7 | # to OAuth standards and Doorkeeper configuration. 8 | class RedirectUriValidator < ActiveModel::EachValidator 9 | def validate_each(record, attribute, value) 10 | if value.blank? 11 | return if Doorkeeper.config.allow_blank_redirect_uri?(record) 12 | 13 | record.errors.add(attribute, :blank) 14 | else 15 | value.split.each do |val| 16 | next if oob_redirect_uri?(val) 17 | 18 | uri = ::URI.parse(val) 19 | record.errors.add(attribute, :forbidden_uri) if forbidden_uri?(uri) 20 | record.errors.add(attribute, :fragment_present) unless uri.fragment.nil? 21 | record.errors.add(attribute, :unspecified_scheme) if unspecified_scheme?(uri) 22 | record.errors.add(attribute, :relative_uri) if relative_uri?(uri) 23 | record.errors.add(attribute, :secured_uri) if invalid_ssl_uri?(uri) 24 | record.errors.add(attribute, :invalid_uri) if unspecified_host?(uri) 25 | end 26 | end 27 | rescue URI::InvalidURIError 28 | record.errors.add(attribute, :invalid_uri) 29 | end 30 | 31 | private 32 | 33 | def oob_redirect_uri?(uri) 34 | Doorkeeper::OAuth::NonStandard::IETF_WG_OAUTH2_OOB_METHODS.include?(uri) 35 | end 36 | 37 | def forbidden_uri?(uri) 38 | Doorkeeper.config.forbid_redirect_uri.call(uri) 39 | end 40 | 41 | def unspecified_scheme?(uri) 42 | return true if uri.opaque.present? 43 | 44 | %w[localhost].include?(uri.try(:scheme)) 45 | end 46 | 47 | def unspecified_host?(uri) 48 | uri.is_a?(URI::HTTP) && uri.host.blank? 49 | end 50 | 51 | def relative_uri?(uri) 52 | uri.scheme.nil? && uri.host.blank? 53 | end 54 | 55 | def invalid_ssl_uri?(uri) 56 | forces_ssl = Doorkeeper.config.force_ssl_in_redirect_uri 57 | non_https = uri.try(:scheme) == "http" 58 | 59 | if forces_ssl.respond_to?(:call) 60 | forces_ssl.call(uri) && non_https 61 | else 62 | forces_ssl && non_https 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/doorkeeper/orm/active_record/stale_records_cleaner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Orm 5 | module ActiveRecord 6 | # Helper class to clear stale and non-active tokens and grants. 7 | # Used by Doorkeeper Rake tasks. 8 | # 9 | class StaleRecordsCleaner 10 | def initialize(base_scope) 11 | @base_scope = base_scope 12 | end 13 | 14 | # Clears revoked records 15 | def clean_revoked 16 | table = @base_scope.arel_table 17 | 18 | @base_scope 19 | .where.not(revoked_at: nil) 20 | .where(table[:revoked_at].lt(Time.current)) 21 | .in_batches(&:delete_all) 22 | end 23 | 24 | # Clears expired records 25 | def clean_expired(ttl) 26 | table = @base_scope.arel_table 27 | 28 | @base_scope 29 | .where.not(expires_in: nil) 30 | .where(table[:created_at].lt(Time.current - ttl)) 31 | .in_batches(&:delete_all) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/doorkeeper/rails/routes/abstract_router.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Rails 5 | # Abstract router module that implements base behavior 6 | # for generating and mapping Rails routes. 7 | # 8 | # Could be reused in Doorkeeper extensions. 9 | # 10 | module AbstractRouter 11 | extend ActiveSupport::Concern 12 | 13 | attr_reader :routes 14 | 15 | def initialize(routes, mapper = Mapper.new, &block) 16 | @routes = routes 17 | @mapping = mapper.map(&block) 18 | end 19 | 20 | def generate_routes!(**_options) 21 | raise NotImplementedError, "must be redefined for #{self.class.name}!" 22 | end 23 | 24 | private 25 | 26 | def map_route(name, method) 27 | return if @mapping.skipped?(name) 28 | 29 | send(method, @mapping[name]) 30 | 31 | mapping[name] = @mapping[name] 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/doorkeeper/rails/routes/mapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Rails 5 | class Routes # :nodoc: 6 | class Mapper 7 | def initialize(mapping = Mapping.new) 8 | @mapping = mapping 9 | end 10 | 11 | def map(&block) 12 | instance_eval(&block) if block 13 | @mapping 14 | end 15 | 16 | def controllers(controller_names = {}) 17 | @mapping.controllers.merge!(controller_names) 18 | end 19 | 20 | def skip_controllers(*controller_names) 21 | @mapping.skips = controller_names 22 | end 23 | 24 | def as(alias_names = {}) 25 | @mapping.as.merge!(alias_names) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/doorkeeper/rails/routes/mapping.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Rails 5 | class Routes # :nodoc: 6 | class Mapping 7 | attr_accessor :controllers, :as, :skips 8 | 9 | def initialize 10 | @controllers = { 11 | authorizations: "doorkeeper/authorizations", 12 | applications: "doorkeeper/applications", 13 | authorized_applications: "doorkeeper/authorized_applications", 14 | tokens: "doorkeeper/tokens", 15 | token_info: "doorkeeper/token_info", 16 | } 17 | 18 | @as = { 19 | authorizations: :authorization, 20 | tokens: :token, 21 | token_info: :token_info, 22 | } 23 | 24 | @skips = [] 25 | end 26 | 27 | def [](routes) 28 | { 29 | controllers: @controllers[routes], 30 | as: @as[routes], 31 | } 32 | end 33 | 34 | def skipped?(controller) 35 | @skips.include?(controller) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/doorkeeper/rails/routes/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Rails 5 | class Routes 6 | # Thread-safe registry of any Doorkeeper additional routes. 7 | # Used to allow implementing of Doorkeeper extensions that must 8 | # use their own routes. 9 | # 10 | module Registry 11 | ROUTES_ACCESS_LOCK = Mutex.new 12 | ROUTES_DEFINITION_LOCK = Mutex.new 13 | 14 | InvalidRouterClass = Class.new(StandardError) 15 | 16 | # Collection of additional registered routes for Doorkeeper. 17 | # 18 | # @return [Array] set of registered routes 19 | # 20 | def registered_routes 21 | ROUTES_DEFINITION_LOCK.synchronize do 22 | @registered_routes ||= Set.new 23 | end 24 | end 25 | 26 | # Registers additional routes in the Doorkeeper registry 27 | # 28 | # @param [Object] routes 29 | # routes class 30 | # 31 | def register_routes(routes) 32 | if !routes.is_a?(Module) || !(routes < AbstractRouter) 33 | raise InvalidRouterClass, "routes class must include Doorkeeper::Rails::AbstractRouter" 34 | end 35 | 36 | ROUTES_ACCESS_LOCK.synchronize do 37 | registered_routes << routes 38 | end 39 | end 40 | 41 | alias register register_routes 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/doorkeeper/rake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Rake 5 | class << self 6 | def load_tasks 7 | glob = File.join(File.absolute_path(__dir__), "rake", "*.rake") 8 | Dir[glob].each do |rake_file| 9 | load rake_file 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/doorkeeper/rake/db.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :doorkeeper do 4 | namespace :db do 5 | desc "Removes stale data from doorkeeper related database tables" 6 | task cleanup: [ 7 | "doorkeeper:db:cleanup:revoked_tokens", 8 | "doorkeeper:db:cleanup:expired_tokens", 9 | "doorkeeper:db:cleanup:revoked_grants", 10 | "doorkeeper:db:cleanup:expired_grants", 11 | ] 12 | 13 | namespace :cleanup do 14 | desc "Removes stale access tokens" 15 | task revoked_tokens: "doorkeeper:setup" do 16 | cleaner = Doorkeeper::StaleRecordsCleaner.new(Doorkeeper.config.access_token_model) 17 | cleaner.clean_revoked 18 | end 19 | 20 | desc "Removes expired (TTL passed) access tokens" 21 | task expired_tokens: "doorkeeper:setup" do 22 | expirable_tokens = Doorkeeper.config.access_token_model.where(refresh_token: nil) 23 | cleaner = Doorkeeper::StaleRecordsCleaner.new(expirable_tokens) 24 | cleaner.clean_expired(Doorkeeper.config.access_token_expires_in) 25 | end 26 | 27 | desc "Removes stale access grants" 28 | task revoked_grants: "doorkeeper:setup" do 29 | cleaner = Doorkeeper::StaleRecordsCleaner.new(Doorkeeper.config.access_grant_model) 30 | cleaner.clean_revoked 31 | end 32 | 33 | desc "Removes expired (TTL passed) access grants" 34 | task expired_grants: "doorkeeper:setup" do 35 | cleaner = Doorkeeper::StaleRecordsCleaner.new(Doorkeeper.config.access_grant_model) 36 | cleaner.clean_expired(Doorkeeper.config.authorization_code_expires_in) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/doorkeeper/rake/setup.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :doorkeeper do 4 | task setup: :environment do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/doorkeeper/request/authorization_code.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Request 5 | class AuthorizationCode < Strategy 6 | delegate :client, :parameters, to: :server 7 | 8 | def request 9 | @request ||= OAuth::AuthorizationCodeRequest.new( 10 | Doorkeeper.config, 11 | grant, 12 | client, 13 | parameters, 14 | ) 15 | end 16 | 17 | private 18 | 19 | def grant 20 | raise Errors::MissingRequiredParameter, :code if parameters[:code].blank? 21 | 22 | Doorkeeper.config.access_grant_model.by_token(parameters[:code]) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/doorkeeper/request/client_credentials.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Request 5 | class ClientCredentials < Strategy 6 | delegate :client, :parameters, to: :server 7 | 8 | def request 9 | @request ||= OAuth::ClientCredentialsRequest.new( 10 | Doorkeeper.config, 11 | client, 12 | parameters, 13 | ) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/doorkeeper/request/code.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Request 5 | class Code < Strategy 6 | delegate :current_resource_owner, to: :server 7 | 8 | def pre_auth 9 | server.context.send(:pre_auth) 10 | end 11 | 12 | def request 13 | @request ||= OAuth::CodeRequest.new(pre_auth, current_resource_owner) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/doorkeeper/request/password.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Request 5 | class Password < Strategy 6 | delegate :credentials, :resource_owner, :parameters, :client, to: :server 7 | 8 | def request 9 | @request ||= OAuth::PasswordAccessTokenRequest.new( 10 | Doorkeeper.config, 11 | client, 12 | credentials, 13 | resource_owner, 14 | parameters, 15 | ) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/doorkeeper/request/refresh_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Request 5 | class RefreshToken < Strategy 6 | delegate :credentials, :parameters, to: :server 7 | 8 | def refresh_token 9 | Doorkeeper.config.access_token_model.by_refresh_token(parameters[:refresh_token]) 10 | end 11 | 12 | def request 13 | @request ||= OAuth::RefreshTokenRequest.new( 14 | Doorkeeper.config, 15 | refresh_token, 16 | credentials, 17 | parameters, 18 | ) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/doorkeeper/request/strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Request 5 | class Strategy 6 | attr_reader :server 7 | 8 | delegate :authorize, to: :request 9 | 10 | def initialize(server) 11 | @server = server 12 | end 13 | 14 | def request 15 | raise NotImplementedError, "request strategies must define #request" 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/doorkeeper/request/token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Request 5 | class Token < Strategy 6 | delegate :current_resource_owner, to: :server 7 | 8 | def pre_auth 9 | server.context.send(:pre_auth) 10 | end 11 | 12 | def request 13 | @request ||= OAuth::TokenRequest.new(pre_auth, current_resource_owner) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/doorkeeper/revocable_tokens/revocable_access_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module RevocableTokens 5 | class RevocableAccessToken 6 | attr_reader :token 7 | 8 | def initialize(token) 9 | @token = token 10 | end 11 | 12 | def revocable? 13 | token.accessible? 14 | end 15 | 16 | def revoke 17 | token.revoke 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/doorkeeper/revocable_tokens/revocable_refresh_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module RevocableTokens 5 | class RevocableRefreshToken 6 | attr_reader :token 7 | 8 | def initialize(token) 9 | @token = token 10 | end 11 | 12 | def revocable? 13 | !token.revoked? 14 | end 15 | 16 | def revoke 17 | token.revoke 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/doorkeeper/secret_storing/bcrypt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module SecretStoring 5 | ## 6 | # Plain text secret storing, which is the default 7 | # but also provides fallback lookup if 8 | # other secret storing mechanisms are enabled. 9 | class BCrypt < Base 10 | ## 11 | # Return the value to be stored by the database 12 | # @param plain_secret The plain secret input / generated 13 | def self.transform_secret(plain_secret) 14 | ::BCrypt::Password.create(plain_secret.to_s) 15 | end 16 | 17 | ## 18 | # Securely compare the given +input+ value with a +stored+ value 19 | # processed by +transform_secret+. 20 | def self.secret_matches?(input, stored) 21 | ::BCrypt::Password.new(stored.to_s) == input.to_s 22 | rescue ::BCrypt::Errors::InvalidHash 23 | false 24 | end 25 | 26 | ## 27 | # Determines whether this strategy supports restoring 28 | # secrets from the database. This allows detecting users 29 | # trying to use a non-restorable strategy with +reuse_access_tokens+. 30 | def self.allows_restoring_secrets? 31 | false 32 | end 33 | 34 | ## 35 | # Determines what secrets this strategy is applicable for 36 | def self.validate_for(model) 37 | unless model.to_sym == :application 38 | raise ArgumentError, 39 | "'#{name}' can only be used for storing application secrets." 40 | end 41 | 42 | unless bcrypt_present? 43 | raise ArgumentError, 44 | "'#{name}' requires the 'bcrypt' gem being loaded." 45 | end 46 | 47 | true 48 | end 49 | 50 | ## 51 | # Test if we can require the BCrypt gem 52 | def self.bcrypt_present? 53 | require "bcrypt" 54 | true 55 | rescue LoadError 56 | false 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/doorkeeper/secret_storing/plain.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module SecretStoring 5 | ## 6 | # Plain text secret storing, which is the default 7 | # but also provides fallback lookup if 8 | # other secret storing mechanisms are enabled. 9 | class Plain < Base 10 | ## 11 | # Return the value to be stored by the database 12 | # @param plain_secret The plain secret input / generated 13 | def self.transform_secret(plain_secret) 14 | plain_secret 15 | end 16 | 17 | ## 18 | # Return the restored value from the database 19 | # @param resource The resource instance to act on 20 | # @param attribute The secret attribute to restore 21 | # as retrieved from the database. 22 | def self.restore_secret(resource, attribute) 23 | resource.public_send(attribute) 24 | end 25 | 26 | ## 27 | # Plain values obviously allow restoring 28 | def self.allows_restoring_secrets? 29 | true 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/doorkeeper/secret_storing/sha256_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module SecretStoring 5 | ## 6 | # Plain text secret storing, which is the default 7 | # but also provides fallback lookup if 8 | # other secret storing mechanisms are enabled. 9 | class Sha256Hash < Base 10 | ## 11 | # Return the value to be stored by the database 12 | # @param plain_secret The plain secret input / generated 13 | def self.transform_secret(plain_secret) 14 | ::Digest::SHA256.hexdigest plain_secret 15 | end 16 | 17 | ## 18 | # Determines whether this strategy supports restoring 19 | # secrets from the database. This allows detecting users 20 | # trying to use a non-restorable strategy with +reuse_access_tokens+. 21 | def self.allows_restoring_secrets? 22 | false 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/doorkeeper/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class Server 5 | attr_reader :context 6 | 7 | def initialize(context) 8 | @context = context 9 | end 10 | 11 | def authorization_request(strategy) 12 | klass = Request.authorization_strategy(strategy) 13 | klass.new(self) 14 | end 15 | 16 | def token_request(strategy) 17 | klass = Request.token_strategy(strategy) 18 | klass.new(self) 19 | end 20 | 21 | # TODO: context should be the request 22 | def parameters 23 | context.request.parameters 24 | end 25 | 26 | def client 27 | @client ||= OAuth::Client.authenticate(credentials) 28 | end 29 | 30 | def current_resource_owner 31 | context.send :current_resource_owner 32 | end 33 | 34 | # TODO: Use configuration and evaluate proper context on block 35 | def resource_owner 36 | context.send :resource_owner_from_credentials 37 | end 38 | 39 | def credentials 40 | methods = Doorkeeper.config.client_credentials_methods 41 | @credentials ||= OAuth::Client::Credentials.from_request(context.request, *methods) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/doorkeeper/stale_records_cleaner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class StaleRecordsCleaner 5 | CLEANER_CLASS = "StaleRecordsCleaner" 6 | 7 | def self.for(base_scope) 8 | orm_adapter = "doorkeeper/orm/#{configured_orm}".classify 9 | 10 | orm_cleaner = "#{orm_adapter}::#{CLEANER_CLASS}".constantize 11 | orm_cleaner.new(base_scope) 12 | rescue NameError 13 | raise Doorkeeper::Errors::NoOrmCleaner, "'#{configured_orm}' ORM has no cleaner!" 14 | end 15 | 16 | def self.new(base_scope) 17 | self.for(base_scope) 18 | end 19 | 20 | def self.configured_orm 21 | Doorkeeper.config.orm 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/doorkeeper/validations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Validations 5 | extend ActiveSupport::Concern 6 | 7 | attr_accessor :error 8 | 9 | def validate 10 | @error = nil 11 | 12 | self.class.validations.each do |validation| 13 | @error = validation[:options][:error] unless send("validate_#{validation[:attribute]}") 14 | break if @error 15 | end 16 | end 17 | 18 | def valid? 19 | validate 20 | @error.nil? 21 | end 22 | 23 | module ClassMethods 24 | def validate(attribute, options = {}) 25 | validations << { attribute: attribute, options: options } 26 | end 27 | 28 | def validations 29 | @validations ||= [] 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/doorkeeper/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module VERSION 5 | # Semantic versioning 6 | MAJOR = 5 7 | MINOR = 8 8 | TINY = 2 9 | PRE = nil 10 | 11 | # Full version number 12 | STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/application_owner_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | require "rails/generators/active_record" 5 | 6 | module Doorkeeper 7 | # Generates migration to add reference to owner of the 8 | # Doorkeeper application. 9 | # 10 | class ApplicationOwnerGenerator < ::Rails::Generators::Base 11 | include ::Rails::Generators::Migration 12 | source_root File.expand_path("templates", __dir__) 13 | desc "Provide support for client application ownership." 14 | 15 | def application_owner 16 | migration_template( 17 | "add_owner_to_application_migration.rb.erb", 18 | "db/migrate/add_owner_to_application.rb", 19 | migration_version: migration_version, 20 | ) 21 | end 22 | 23 | def self.next_migration_number(dirname) 24 | ActiveRecord::Generators::Base.next_migration_number(dirname) 25 | end 26 | 27 | private 28 | 29 | def migration_version 30 | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/confidential_applications_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | require "rails/generators/active_record" 5 | 6 | module Doorkeeper 7 | # Generates migration to add confidential column to Doorkeeper 8 | # applications table. 9 | # 10 | class ConfidentialApplicationsGenerator < ::Rails::Generators::Base 11 | include ::Rails::Generators::Migration 12 | source_root File.expand_path("templates", __dir__) 13 | desc "Add confidential column to Doorkeeper applications" 14 | 15 | def confidential_applications 16 | migration_template( 17 | "add_confidential_to_applications.rb.erb", 18 | "db/migrate/add_confidential_to_applications.rb", 19 | migration_version: migration_version, 20 | ) 21 | end 22 | 23 | def self.next_migration_number(dirname) 24 | ActiveRecord::Generators::Base.next_migration_number(dirname) 25 | end 26 | 27 | private 28 | 29 | def migration_version 30 | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/enable_polymorphic_resource_owner_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | require "rails/generators/active_record" 5 | 6 | module Doorkeeper 7 | # Generates migration with polymorphic resource owner required 8 | # database columns for Doorkeeper Access Token and Access Grant 9 | # models. 10 | # 11 | class EnablePolymorphicResourceOwnerGenerator < ::Rails::Generators::Base 12 | include ::Rails::Generators::Migration 13 | source_root File.expand_path("templates", __dir__) 14 | desc "Provide support for polymorphic Resource Owner." 15 | 16 | def enable_polymorphic_resource_owner 17 | migration_template( 18 | "enable_polymorphic_resource_owner_migration.rb.erb", 19 | "db/migrate/enable_polymorphic_resource_owner.rb", 20 | migration_version: migration_version, 21 | ) 22 | gsub_file( 23 | "config/initializers/doorkeeper.rb", 24 | "# use_polymorphic_resource_owner", 25 | "use_polymorphic_resource_owner", 26 | ) 27 | end 28 | 29 | def self.next_migration_number(dirname) 30 | ActiveRecord::Generators::Base.next_migration_number(dirname) 31 | end 32 | 33 | private 34 | 35 | def migration_version 36 | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | require "rails/generators/active_record" 5 | 6 | module Doorkeeper 7 | # Setup doorkeeper into Rails application: locales, routes, etc. 8 | # 9 | class InstallGenerator < ::Rails::Generators::Base 10 | include ::Rails::Generators::Migration 11 | source_root File.expand_path("templates", __dir__) 12 | desc "Installs Doorkeeper." 13 | 14 | def install 15 | template "initializer.rb", "config/initializers/doorkeeper.rb" 16 | copy_file File.expand_path("../../../config/locales/en.yml", __dir__), 17 | "config/locales/doorkeeper.en.yml" 18 | route "use_doorkeeper" 19 | readme "README" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/migration_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | require "rails/generators/active_record" 5 | 6 | module Doorkeeper 7 | # Copies main Doorkeeper migration into parent Rails application. 8 | # 9 | class MigrationGenerator < ::Rails::Generators::Base 10 | include ::Rails::Generators::Migration 11 | source_root File.expand_path("templates", __dir__) 12 | desc "Installs Doorkeeper migration file." 13 | 14 | def install 15 | migration_template( 16 | "migration.rb.erb", 17 | "db/migrate/create_doorkeeper_tables.rb", 18 | migration_version: migration_version, 19 | ) 20 | end 21 | 22 | def self.next_migration_number(dirname) 23 | ActiveRecord::Generators::Base.next_migration_number(dirname) 24 | end 25 | 26 | private 27 | 28 | def migration_version 29 | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/pkce_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | require "rails/generators/active_record" 5 | 6 | module Doorkeeper 7 | # Generates migration with PKCE required database columns for 8 | # Doorkeeper tables. 9 | # 10 | class PkceGenerator < ::Rails::Generators::Base 11 | include ::Rails::Generators::Migration 12 | source_root File.expand_path("templates", __dir__) 13 | desc "Provide support for PKCE." 14 | 15 | def pkce 16 | migration_template( 17 | "enable_pkce_migration.rb.erb", 18 | "db/migrate/enable_pkce.rb", 19 | migration_version: migration_version, 20 | ) 21 | end 22 | 23 | def self.next_migration_number(dirname) 24 | ActiveRecord::Generators::Base.next_migration_number(dirname) 25 | end 26 | 27 | private 28 | 29 | def migration_version 30 | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/previous_refresh_token_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | require "rails/generators/active_record" 5 | 6 | module Doorkeeper 7 | # Generates migration to add previous refresh token column to the 8 | # database for Doorkeeper tables. 9 | # 10 | class PreviousRefreshTokenGenerator < ::Rails::Generators::Base 11 | include ::Rails::Generators::Migration 12 | source_root File.expand_path("templates", __dir__) 13 | desc "Support revoke refresh token on access token use" 14 | 15 | def self.next_migration_number(path) 16 | ActiveRecord::Generators::Base.next_migration_number(path) 17 | end 18 | 19 | def previous_refresh_token 20 | return unless no_previous_refresh_token_column? 21 | 22 | migration_template( 23 | "add_previous_refresh_token_to_access_tokens.rb.erb", 24 | "db/migrate/add_previous_refresh_token_to_access_tokens.rb", 25 | ) 26 | end 27 | 28 | private 29 | 30 | def migration_version 31 | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" 32 | end 33 | 34 | def no_previous_refresh_token_column? 35 | !ActiveRecord::Base.connection.column_exists?( 36 | :oauth_access_tokens, 37 | :previous_refresh_token, 38 | ) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/remove_applications_secret_not_null_constraint_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | require "rails/generators/active_record" 5 | 6 | module Doorkeeper 7 | # Generates migration with which drops NOT NULL constraint and allows not 8 | # to bloat the database with redundant secret value. 9 | # 10 | class RemoveApplicationSecretNotNullConstraint < ::Rails::Generators::Base 11 | include ::Rails::Generators::Migration 12 | source_root File.expand_path("templates", __dir__) 13 | desc "Removes NOT NULL constraint for OAuth2 applications." 14 | 15 | def enable_polymorphic_resource_owner 16 | migration_template( 17 | "remove_applications_secret_not_null_constraint.rb.erb", 18 | "db/migrate/remove_applications_secret_not_null_constraint.rb", 19 | migration_version: migration_version, 20 | ) 21 | end 22 | 23 | def self.next_migration_number(dirname) 24 | ActiveRecord::Generators::Base.next_migration_number(dirname) 25 | end 26 | 27 | private 28 | 29 | def migration_version 30 | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/templates/README: -------------------------------------------------------------------------------- 1 | =============================================================================== 2 | 3 | There is a setup that you need to do before you can use doorkeeper. 4 | 5 | Step 1. 6 | Go to config/initializers/doorkeeper.rb and configure 7 | resource_owner_authenticator block. 8 | 9 | Step 2. 10 | Choose the ORM: 11 | 12 | If you want to use ActiveRecord run: 13 | 14 | rails generate doorkeeper:migration 15 | 16 | And run 17 | 18 | rake db:migrate 19 | 20 | Step 3. 21 | That's it, that's all. Enjoy! 22 | 23 | =============================================================================== 24 | 25 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/templates/add_confidential_to_applications.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddConfidentialToApplications < ActiveRecord::Migration<%= migration_version %> 4 | def change 5 | add_column( 6 | :oauth_applications, 7 | :confidential, 8 | :boolean, 9 | null: false, 10 | default: true 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/templates/add_owner_to_application_migration.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOwnerToApplication < ActiveRecord::Migration<%= migration_version %> 4 | def change 5 | add_column :oauth_applications, :owner_id, :bigint, null: true 6 | add_column :oauth_applications, :owner_type, :string, null: true 7 | add_index :oauth_applications, [:owner_id, :owner_type] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/templates/add_previous_refresh_token_to_access_tokens.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration<%= migration_version %> 4 | def change 5 | add_column( 6 | :oauth_access_tokens, 7 | :previous_refresh_token, 8 | :string, 9 | default: "", 10 | null: false 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/templates/enable_pkce_migration.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EnablePkce < ActiveRecord::Migration<%= migration_version %> 4 | def change 5 | add_column :oauth_access_grants, :code_challenge, :string, null: true 6 | add_column :oauth_access_grants, :code_challenge_method, :string, null: true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/templates/enable_polymorphic_resource_owner_migration.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EnablePolymorphicResourceOwner < ActiveRecord::Migration<%= migration_version %> 4 | def change 5 | add_column :oauth_access_tokens, :resource_owner_type, :string 6 | add_column :oauth_access_grants, :resource_owner_type, :string 7 | change_column_null :oauth_access_grants, :resource_owner_type, false 8 | 9 | add_index :oauth_access_tokens, 10 | [:resource_owner_id, :resource_owner_type], 11 | name: 'polymorphic_owner_oauth_access_tokens' 12 | 13 | add_index :oauth_access_grants, 14 | [:resource_owner_id, :resource_owner_type], 15 | name: 'polymorphic_owner_oauth_access_grants' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/templates/remove_applications_secret_not_null_constraint.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveApplicationsSecretNotNullConstraint < ActiveRecord::Migration<%= migration_version %> 4 | def change 5 | change_column_null :oauth_applications, :secret, true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/doorkeeper/views_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Generators 5 | # Generates doorkeeper views for Rails application 6 | # 7 | class ViewsGenerator < ::Rails::Generators::Base 8 | source_root File.expand_path("../../../app/views", __dir__) 9 | 10 | desc "Copies default Doorkeeper views and layouts to your application." 11 | 12 | def manifest 13 | directory "doorkeeper", "app/views/doorkeeper" 14 | directory "layouts/doorkeeper", "app/views/layouts/doorkeeper" 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/controllers/application_metal_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper_integration" 4 | 5 | RSpec.describe Doorkeeper::ApplicationMetalController, type: :controller do 6 | render_views 7 | 8 | controller(described_class) do 9 | def index 10 | render json: {}, status: 200 11 | end 12 | 13 | def create 14 | render json: {}, status: 200 15 | end 16 | end 17 | 18 | it "lazy run hooks" do 19 | i = 0 20 | ActiveSupport.on_load(:doorkeeper_metal_controller) { i += 1 } 21 | 22 | expect(i).to eq 1 23 | end 24 | 25 | describe "enforce_content_type" do 26 | before { allow(Doorkeeper.config).to receive(:enforce_content_type).and_return(flag) } 27 | 28 | context "when enabled" do 29 | let(:flag) { true } 30 | 31 | it "returns a 200 for the requests without body" do 32 | get :index, params: {} 33 | expect(response).to have_http_status 200 34 | end 35 | 36 | it "returns a 200 for the requests with body and correct media type" do 37 | post :create, params: {}, as: :url_encoded_form 38 | expect(response).to have_http_status 200 39 | end 40 | 41 | it "returns a 415 for the requests with body and incorrect media type" do 42 | post :create, params: {}, as: :json 43 | expect(response).to have_http_status 415 44 | end 45 | end 46 | 47 | context "when disabled" do 48 | let(:flag) { false } 49 | 50 | it "returns a 200 for the correct media type" do 51 | get :index, as: :url_encoded_form 52 | expect(response).to have_http_status 200 53 | end 54 | 55 | it "returns a 200 for an incorrect media type" do 56 | get :index, as: :json 57 | expect(response).to have_http_status 200 58 | end 59 | 60 | it "returns a 200 for the requests with body and incorrect media type" do 61 | post :create, params: {}, as: :json 62 | expect(response).to have_http_status 200 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/controllers/token_info_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::TokenInfoController, type: :controller do 6 | render_views 7 | 8 | describe "when requesting token info with valid token" do 9 | let(:doorkeeper_token) { FactoryBot.create(:access_token) } 10 | 11 | describe "successful request" do 12 | it "responds with token info" do 13 | get :show, params: { access_token: doorkeeper_token.token } 14 | 15 | expect(response.body).to eq(doorkeeper_token.to_json) 16 | end 17 | 18 | it "responds with a 200 status" do 19 | get :show, params: { access_token: doorkeeper_token.token } 20 | 21 | expect(response.status).to eq 200 22 | end 23 | end 24 | 25 | describe "invalid token response" do 26 | it "responds with 401 when doorkeeper_token is not valid" do 27 | get :show 28 | 29 | expect(response.status).to eq 401 30 | expect(response.headers["WWW-Authenticate"]).to match(/^Bearer/) 31 | end 32 | 33 | it "responds with 401 when doorkeeper_token is invalid, expired or revoked" do 34 | allow(controller).to receive(:doorkeeper_token).and_return(doorkeeper_token) 35 | allow(doorkeeper_token).to receive(:accessible?).and_return(false) 36 | 37 | get :show 38 | 39 | expect(response.status).to eq 401 40 | expect(response.headers["WWW-Authenticate"]).to match(/^Bearer/) 41 | end 42 | 43 | it "responds body message for error" do 44 | get :show 45 | 46 | expect(response.body).to eq( 47 | Doorkeeper::OAuth::InvalidTokenResponse.new.body.to_json, 48 | ) 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/doorkeeper/server_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::Server do 6 | subject(:server) do 7 | described_class.new(context) 8 | end 9 | 10 | let(:fake_class) { double :fake_class } 11 | let(:context) { double :context } 12 | 13 | describe ".authorization_request" do 14 | it "raises error when strategy does not match phase" do 15 | expect do 16 | server.token_request(:code) 17 | end.to raise_error(Doorkeeper::Errors::InvalidTokenStrategy) 18 | end 19 | 20 | context "when only Authorization Code strategy is enabled" do 21 | before do 22 | allow(Doorkeeper.configuration) 23 | .to receive(:grant_flows) 24 | .and_return(["authorization_code"]) 25 | end 26 | 27 | it "raises error when using the disabled Client Credentials strategy" do 28 | expect do 29 | server.token_request(:client_credentials) 30 | end.to raise_error(Doorkeeper::Errors::InvalidTokenStrategy) 31 | end 32 | end 33 | 34 | it "builds the request with selected strategy" do 35 | stub_const "Doorkeeper::Request::Code", fake_class 36 | expect(fake_class).to receive(:new).with(server) 37 | expect(::Kernel).to receive(:warn) 38 | server.authorization_request :code 39 | end 40 | 41 | it "builds the request with composite strategy name" do 42 | Doorkeeper.configure do 43 | grant_flows ["id_token token"] 44 | end 45 | 46 | stub_const "Doorkeeper::Request::IdTokenToken", fake_class 47 | expect(fake_class).to receive(:new).with(server) 48 | expect(::Kernel).to receive(:warn) 49 | server.authorization_request "id_token token" 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/doorkeeper/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::VERSION do 6 | describe "#gem_version" do 7 | it "returns Gem::Version instance" do 8 | expect(Doorkeeper.gem_version).to be_an_instance_of(Gem::Version) 9 | end 10 | end 11 | 12 | describe "VERSION" do 13 | it "returns gem version string" do 14 | expect(Doorkeeper::VERSION::STRING).to match(/^\d+\.\d+\.\d+(\.\w+)?$/) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # frozen_string_literal: true 3 | 4 | # Add your own tasks in files placed in lib/tasks ending in .rake, 5 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 6 | 7 | require File.expand_path("config/application", __dir__) 8 | 9 | Dummy::Application.load_tasks 10 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | // JS and CSS bundles 2 | // 3 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/custom_authorizations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CustomAuthorizationsController < ::ApplicationController 4 | %w[index show new create edit update destroy].each do |action| 5 | define_method action do 6 | render nothing: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/full_protected_resources_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FullProtectedResourcesController < ApplicationController 4 | before_action -> { doorkeeper_authorize! :write, :admin }, only: :show 5 | before_action :doorkeeper_authorize!, only: :index 6 | 7 | def index 8 | render plain: "index" 9 | end 10 | 11 | def show 12 | render plain: "show" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HomeController < ApplicationController 4 | def index; end 5 | 6 | def sign_in 7 | session[:user_id] = if Rails.env.development? 8 | User.first || User.create!(name: "Joe", password: "sekret") 9 | else 10 | User.first 11 | end 12 | redirect_to "/" 13 | end 14 | 15 | def callback 16 | render plain: "ok" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/metal_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MetalController < ActionController::Metal 4 | include AbstractController::Callbacks 5 | include ActionController::Head 6 | include Doorkeeper::Rails::Helpers 7 | 8 | before_action :doorkeeper_authorize! 9 | 10 | def index 11 | self.response_body = { ok: true }.to_json 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/semi_protected_resources_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SemiProtectedResourcesController < ApplicationController 4 | before_action :doorkeeper_authorize!, only: :index 5 | 6 | def index 7 | render plain: "protected index" 8 | end 9 | 10 | def show 11 | render plain: "non protected show" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | def current_user 5 | @current_user ||= User.find_by(id: session[:user_id]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ::ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | 7 | class User < ApplicationRecord 8 | def self.authenticate!(name, password) 9 | User.where(name: name, password: password).first 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/views/home/index.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorkeeper-gem/doorkeeper/9218cdec8eb295366ffe8ec9129c377b30c00bea/spec/dummy/app/views/home/index.html.erb -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= csrf_meta_tags %> 6 | 7 | 8 | 9 | <%= link_to "Sign in", '/sign_in' %> 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require ::File.expand_path('config/environment', __dir__) 6 | run Dummy::Application 7 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("boot", __dir__) 2 | 3 | require "rails" 4 | 5 | %w[ 6 | action_controller/railtie 7 | action_view/railtie 8 | action_cable/engine 9 | sprockets/railtie 10 | ].each do |railtie| 11 | begin 12 | require railtie 13 | rescue LoadError => e 14 | puts "Error loading '#{railtie}' (#{e.message})" 15 | end 16 | end 17 | 18 | Bundler.require(*Rails.groups) 19 | 20 | require "yaml" 21 | 22 | orm = if DOORKEEPER_ORM =~ /mongoid/ 23 | Mongoid.load!(File.join(File.dirname(File.expand_path(__FILE__)), "#{DOORKEEPER_ORM}.yml")) 24 | :mongoid 25 | else 26 | DOORKEEPER_ORM 27 | end 28 | require "#{orm}/railtie" 29 | 30 | module Dummy 31 | class Application < Rails::Application 32 | if Rails.gem_version < Gem::Version.new("5.1") 33 | config.action_controller.per_form_csrf_tokens = true 34 | config.action_controller.forgery_protection_origin_check = true 35 | 36 | ActiveSupport.to_time_preserves_timezone = true 37 | 38 | if DOORKEEPER_ORM =~ /active_record/ 39 | config.active_record.belongs_to_required_by_default = true 40 | end 41 | 42 | config.ssl_options = { hsts: { subdomains: true } } 43 | else 44 | config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" 45 | end 46 | 47 | # Settings in config/environments/* take precedence over those specified here. 48 | # Application configuration should go into files in config/initializers 49 | # -- all .rb files in that directory are automatically loaded. 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | 4 | orm = ENV["BUNDLE_GEMFILE"].match(/Gemfile\.(.+)\.rb/) 5 | DOORKEEPER_ORM = (orm && orm[1]) || :active_record unless defined?(DOORKEEPER_ORM) 6 | 7 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) 8 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: sqlite3 3 | database: db/development.sqlite3 4 | pool: 5 5 | timeout: 5000 6 | 7 | test: 8 | adapter: sqlite3 9 | database: ":memory:" 10 | timeout: 500 11 | 12 | production: 13 | adapter: sqlite3 14 | database: ":memory:" 15 | timeout: 500 16 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path("application", __dir__) 3 | 4 | # Initialize the rails application 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb 5 | 6 | # In the development environment your application's code is reloaded on 7 | # every request. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Show full error reports and disable caching 12 | config.consider_all_requests_local = true 13 | config.action_controller.perform_caching = false 14 | 15 | # Don't care if the mailer can't send 16 | # config.action_mailer.raise_delivery_errors = false 17 | 18 | # Print deprecation notices to the Rails logger 19 | config.active_support.deprecation = :log 20 | 21 | # Only use best-standards-support built into browsers 22 | config.action_dispatch.best_standards_support = :builtin 23 | 24 | # Do not compress assets 25 | config.assets.compress = false 26 | 27 | # Expands the lines which load the assets 28 | config.assets.debug = true 29 | 30 | config.eager_load = false 31 | end 32 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb 5 | 6 | # The test environment is used exclusively to run your application's 7 | # test suite. You never need to work with it otherwise. Remember that 8 | # your test database is "scratch space" for the test suite and is wiped 9 | # and recreated between test runs. Don't rely on the data there! 10 | config.cache_classes = true 11 | 12 | config.assets.enabled = true 13 | config.assets.version = "1.0" 14 | config.assets.digest = false 15 | 16 | # Do not eager load code on boot. This avoids loading your whole application 17 | # just for the purpose of running a single test. If you are using a tool that 18 | # preloads Rails for running tests, you may have to set it to true. 19 | config.eager_load = false 20 | 21 | # Show full error reports and disable caching 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates 26 | # Rails 7.1 deprecated false in favor of :none, but we need to use false for 27 | # backwards compatibility: https://github.com/rails/rails/pull/45867 28 | config.action_dispatch.show_exceptions = 29 | Gem::Version.new(Rails.version) >= Gem::Version.new('7.1.0') ? :none : false 30 | 31 | # Disable request forgery protection in test environment 32 | config.action_controller.allow_forgery_protection = false 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | # config.action_mailer.delivery_method = :test 38 | 39 | # Use SQL instead of Active Record's schema dumper when creating the test database. 40 | # This is necessary if your schema can't be completely dumped by the schema dumper, 41 | # like if you have constraints or database-specific column types 42 | # config.active_record.schema_format = :sql 43 | 44 | # Print deprecation notices to the stderr 45 | config.active_support.deprecation = :stderr 46 | 47 | config.eager_load = true 48 | end 49 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 9 | # Rails.backtrace_cleaner.remove_silencers! 10 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Your secret key for verifying the integrity of signed cookies. 6 | # If you change this key, all old signed cookies will become invalid! 7 | # Make sure the secret is at least 30 characters and all random, 8 | # no regular words or you'll be exposed to dictionary attacks. 9 | Dummy::Application.config.secret_key_base = 10 | "c00157b5a1bb6181792f0f4a8a080485de7bab9987e6cf159" 11 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Dummy::Application.config.session_store :cookie_store, key: "_dummy_session" 6 | 7 | # Use the database for sessions instead of the cookie-based default, 8 | # which shouldn't be used to store highly confidential information 9 | # (create the session table with "rails generate session_migration") 10 | # Dummy::Application.config.session_store :active_record_store 11 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | # 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] 11 | end 12 | 13 | # Disable root element in JSON by default. 14 | ActiveSupport.on_load(:active_record) do 15 | self.include_root_in_json = false 16 | end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/doorkeeper.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | doorkeeper: 3 | scopes: 4 | public: "Access your public data" 5 | write: "Update your data" 6 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | use_doorkeeper 3 | 4 | resources :semi_protected_resources 5 | resources :full_protected_resources 6 | 7 | get "metal.json" => "metal#index" 8 | 9 | get "/callback", to: "home#callback" 10 | get "/sign_in", to: "home#sign_in" 11 | 12 | root to: "home#index" 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20111122132257_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateUsers < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :users do |t| 6 | t.string :name 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20120312140401_add_password_to_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPasswordToUsers < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :users, :password, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOwnerToApplication < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :oauth_applications, :owner_id, :integer, null: true 6 | add_column :oauth_applications, :owner_type, :string, null: true 7 | add_index :oauth_applications, %i[owner_id owner_type] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20160320211015_add_previous_refresh_token_to_access_tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column( 6 | :oauth_access_tokens, 7 | :previous_refresh_token, 8 | :string, 9 | default: "", 10 | null: false, 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20170822064514_enable_pkce.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EnablePkce < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :oauth_access_grants, :code_challenge, :string, null: true 6 | add_column :oauth_access_grants, :code_challenge_method, :string, null: true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20180210183654_add_confidential_to_applications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddConfidentialToApplications < ActiveRecord::Migration[5.1] 4 | def change 5 | add_column( 6 | :oauth_applications, 7 | :confidential, 8 | :boolean, 9 | null: false, 10 | default: true, # maintaining backwards compatibility: require secrets 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20230205064514_add_custom_attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCustomAttributes < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :oauth_access_grants, :tenant_name, :string 6 | add_column :oauth_access_tokens, :tenant_name, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

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

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

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

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

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

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorkeeper-gem/doorkeeper/9218cdec8eb295366ffe8ec9129c377b30c00bea/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # This command will automatically be run when you run "rails" with Rails 3 gems 5 | # installed from the root of your application. 6 | 7 | APP_PATH = File.expand_path("../config/application", __dir__) 8 | require File.expand_path("../config/boot", __dir__) 9 | require "rails/commands" 10 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :access_grant, class: "Doorkeeper::AccessGrant" do 5 | sequence(:resource_owner_id) { |n| n } 6 | application 7 | redirect_uri { "https://app.com/callback" } 8 | expires_in { 100 } 9 | scopes { "public write" } 10 | end 11 | 12 | factory :access_token, class: "Doorkeeper::AccessToken" do 13 | sequence(:resource_owner_id) { |n| n } 14 | application 15 | expires_in { 2.hours } 16 | 17 | factory :clientless_access_token do 18 | application { nil } 19 | end 20 | end 21 | 22 | factory :application, class: "Doorkeeper::Application" do 23 | sequence(:name) { |n| "Application #{n}" } 24 | redirect_uri { "https://app.com/callback" } 25 | end 26 | 27 | # do not name this factory :user, otherwise it will conflict with factories 28 | # from applications that use doorkeeper factories in their own tests 29 | factory :doorkeeper_testing_user, class: :user, aliases: [:resource_owner] 30 | end 31 | -------------------------------------------------------------------------------- /spec/generators/application_owner_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/doorkeeper/application_owner_generator" 5 | 6 | RSpec.describe Doorkeeper::ApplicationOwnerGenerator do 7 | include GeneratorSpec::TestCase 8 | 9 | tests described_class 10 | destination ::File.expand_path('tmp/dummy', __dir__) 11 | 12 | describe "after running the generator" do 13 | before do 14 | prepare_destination 15 | end 16 | 17 | it "creates a migration with a version specifier" do 18 | stub_const("ActiveRecord::VERSION::MAJOR", 5) 19 | stub_const("ActiveRecord::VERSION::MINOR", 0) 20 | 21 | run_generator 22 | 23 | assert_migration "db/migrate/add_owner_to_application.rb" do |migration| 24 | assert migration.include?("ActiveRecord::Migration[5.0]\n") 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/generators/confidential_applications_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/doorkeeper/confidential_applications_generator" 5 | 6 | RSpec.describe Doorkeeper::ConfidentialApplicationsGenerator do 7 | include GeneratorSpec::TestCase 8 | 9 | tests described_class 10 | destination ::File.expand_path('tmp/dummy', __dir__) 11 | 12 | describe "after running the generator" do 13 | before do 14 | prepare_destination 15 | end 16 | 17 | it "creates a migration with a version specifier" do 18 | stub_const("ActiveRecord::VERSION::MAJOR", 5) 19 | stub_const("ActiveRecord::VERSION::MINOR", 0) 20 | 21 | run_generator 22 | 23 | assert_migration "db/migrate/add_confidential_to_applications.rb" do |migration| 24 | assert migration.include?("ActiveRecord::Migration[5.0]\n") 25 | assert migration.include?(":confidential") 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/generators/enable_polymorphic_resource_owner_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/doorkeeper/enable_polymorphic_resource_owner_generator" 5 | 6 | RSpec.describe Doorkeeper::EnablePolymorphicResourceOwnerGenerator do 7 | include GeneratorSpec::TestCase 8 | 9 | tests described_class 10 | destination ::File.expand_path('tmp/dummy', __dir__) 11 | 12 | describe "after running the generator" do 13 | before do 14 | prepare_destination 15 | FileUtils.mkdir_p(::File.expand_path("config/initializers", Pathname(destination_root))) 16 | FileUtils.copy_file( 17 | ::File.expand_path("../../lib/generators/doorkeeper/templates/initializer.rb", __dir__), 18 | ::File.expand_path("config/initializers/doorkeeper.rb", Pathname.new(destination_root)), 19 | ) 20 | end 21 | 22 | it "creates a migration with a version specifier and changes the initializer" do 23 | stub_const("ActiveRecord::VERSION::MAJOR", 5) 24 | stub_const("ActiveRecord::VERSION::MINOR", 0) 25 | 26 | run_generator 27 | 28 | assert_migration "db/migrate/enable_polymorphic_resource_owner.rb" do |migration| 29 | assert migration.include?("ActiveRecord::Migration[5.0]\n") 30 | end 31 | 32 | # generator_spec gem requires such block definition :( 33 | # 34 | # rubocop:disable Style/BlockDelimiters 35 | expect(destination_root).to(have_structure { 36 | directory "config" do 37 | directory "initializers" do 38 | file "doorkeeper.rb" do 39 | contains " use_polymorphic_resource_owner" 40 | end 41 | end 42 | end 43 | }) 44 | # rubocop:enable Style/BlockDelimiters 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/generators/install_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/doorkeeper/install_generator" 5 | 6 | RSpec.describe Doorkeeper::InstallGenerator do 7 | include GeneratorSpec::TestCase 8 | 9 | tests described_class 10 | destination ::File.expand_path('tmp/dummy', __dir__) 11 | 12 | describe "after running the generator" do 13 | before do 14 | prepare_destination 15 | FileUtils.mkdir(::File.expand_path("config", Pathname(destination_root))) 16 | FileUtils.mkdir(::File.expand_path("db", Pathname(destination_root))) 17 | FileUtils.copy_file( 18 | ::File.expand_path('templates/routes.rb', __dir__), 19 | ::File.expand_path("config/routes.rb", Pathname.new(destination_root)), 20 | ) 21 | run_generator 22 | end 23 | 24 | it "creates an initializer file" do 25 | assert_file "config/initializers/doorkeeper.rb" 26 | end 27 | 28 | it "copies the locale file" do 29 | assert_file "config/locales/doorkeeper.en.yml" 30 | end 31 | 32 | it "adds sample route" do 33 | assert_file "config/routes.rb", /use_doorkeeper/ 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/generators/migration_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/doorkeeper/migration_generator" 5 | 6 | RSpec.describe Doorkeeper::MigrationGenerator do 7 | include GeneratorSpec::TestCase 8 | 9 | tests described_class 10 | destination ::File.expand_path('tmp/dummy', __dir__) 11 | 12 | describe "after running the generator" do 13 | before do 14 | prepare_destination 15 | end 16 | 17 | it "creates a migration with a version specifier" do 18 | stub_const("ActiveRecord::VERSION::MAJOR", 5) 19 | stub_const("ActiveRecord::VERSION::MINOR", 0) 20 | 21 | run_generator 22 | 23 | assert_migration "db/migrate/create_doorkeeper_tables.rb" do |migration| 24 | assert migration.include?("ActiveRecord::Migration[5.0]\n") 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/generators/pkce_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/doorkeeper/pkce_generator" 5 | 6 | RSpec.describe Doorkeeper::PkceGenerator do 7 | include GeneratorSpec::TestCase 8 | 9 | tests described_class 10 | destination ::File.expand_path('tmp/dummy', __dir__) 11 | 12 | describe "after running the generator" do 13 | before do 14 | prepare_destination 15 | end 16 | 17 | it "creates a migration with a version specifier" do 18 | stub_const("ActiveRecord::VERSION::MAJOR", 5) 19 | stub_const("ActiveRecord::VERSION::MINOR", 0) 20 | 21 | run_generator 22 | 23 | assert_migration "db/migrate/enable_pkce.rb" do |migration| 24 | assert migration.include?("ActiveRecord::Migration[5.0]\n") 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/generators/previous_refresh_token_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/doorkeeper/previous_refresh_token_generator" 5 | 6 | RSpec.describe Doorkeeper::PreviousRefreshTokenGenerator do 7 | include GeneratorSpec::TestCase 8 | 9 | tests described_class 10 | destination ::File.expand_path('tmp/dummy', __dir__) 11 | 12 | describe "after running the generator" do 13 | before do 14 | prepare_destination 15 | 16 | allow_any_instance_of(described_class).to( 17 | receive(:no_previous_refresh_token_column?).and_return(true), 18 | ) 19 | end 20 | 21 | it "creates a migration with a version specifier" do 22 | stub_const("ActiveRecord::VERSION::MAJOR", 5) 23 | stub_const("ActiveRecord::VERSION::MINOR", 0) 24 | 25 | run_generator 26 | 27 | assert_migration "db/migrate/add_previous_refresh_token_to_access_tokens.rb" do |migration| 28 | assert migration.include?("ActiveRecord::Migration[5.0]\n") 29 | end 30 | end 31 | 32 | context "when file already exist" do 33 | it "does not create a migration" do 34 | allow_any_instance_of(described_class).to( 35 | receive(:no_previous_refresh_token_column?).and_call_original, 36 | ) 37 | 38 | run_generator 39 | 40 | assert_no_migration "db/migrate/add_previous_refresh_token_to_access_tokens.rb" 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/generators/remove_applications_secret_not_null_constraint_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/doorkeeper/remove_applications_secret_not_null_constraint_generator" 5 | 6 | RSpec.describe Doorkeeper::RemoveApplicationSecretNotNullConstraint do 7 | include GeneratorSpec::TestCase 8 | 9 | tests described_class 10 | destination ::File.expand_path('tmp/dummy', __dir__) 11 | 12 | describe "after running the generator" do 13 | before do 14 | prepare_destination 15 | end 16 | 17 | it "creates a migration with a version specifier" do 18 | run_generator 19 | 20 | assert_migration "db/migrate/remove_applications_secret_not_null_constraint.rb" do |migration| 21 | assert migration.include?("change_column_null :oauth_applications, :secret") 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/generators/templates/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | end 5 | -------------------------------------------------------------------------------- /spec/generators/views_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/doorkeeper/views_generator" 5 | 6 | RSpec.describe Doorkeeper::Generators::ViewsGenerator do 7 | include GeneratorSpec::TestCase 8 | 9 | tests described_class 10 | destination File.expand_path("tmp/dummy", __dir__) 11 | 12 | before do 13 | prepare_destination 14 | end 15 | 16 | it "create all views" do 17 | run_generator 18 | assert_file "app/views/doorkeeper/applications/_form.html.erb" 19 | assert_file "app/views/doorkeeper/applications/edit.html.erb" 20 | assert_file "app/views/doorkeeper/applications/index.html.erb" 21 | assert_file "app/views/doorkeeper/applications/new.html.erb" 22 | assert_file "app/views/doorkeeper/applications/show.html.erb" 23 | 24 | assert_file "app/views/doorkeeper/authorizations/error.html.erb" 25 | assert_file "app/views/doorkeeper/authorizations/new.html.erb" 26 | 27 | assert_file "app/views/doorkeeper/authorized_applications/index.html.erb" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/helpers/doorkeeper/dashboard_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::DashboardHelper do 6 | describe "#doorkeeper_errors_for" do 7 | let(:object) { double errors: { method: messages } } 8 | let(:messages) { ["first message", "second message"] } 9 | 10 | context "when object has errors" do 11 | it "returns error messages" do 12 | messages.each do |message| 13 | expect(helper.doorkeeper_errors_for(object, :method)).to include( 14 | message.capitalize, 15 | ) 16 | end 17 | end 18 | end 19 | 20 | context "when object has no errors" do 21 | it "returns nil" do 22 | expect(helper.doorkeeper_errors_for(object, :amonter_method)).to be_nil 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/lib/doorkeeper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper do 6 | describe "#authenticate" do 7 | let(:request) { double } 8 | 9 | it "calls OAuth::Token#authenticate" do 10 | token_strategies = described_class.config.access_token_methods 11 | 12 | expect(Doorkeeper::OAuth::Token).to receive(:authenticate) 13 | .with(request, *token_strategies) 14 | 15 | described_class.authenticate(request) 16 | end 17 | 18 | it "accepts custom token strategies" do 19 | token_strategies = %i[first_way second_way] 20 | 21 | expect(Doorkeeper::OAuth::Token).to receive(:authenticate) 22 | .with(request, *token_strategies) 23 | 24 | described_class.authenticate(request, token_strategies) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/lib/models/expirable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::Models::Expirable do 6 | subject(:fake_object) do 7 | Class.new do 8 | include Doorkeeper::Models::Expirable 9 | end.new 10 | end 11 | 12 | before do 13 | allow(fake_object).to receive(:created_at).and_return(1.minute.ago) 14 | end 15 | 16 | describe "#expired?" do 17 | it "is not expired if time has not passed" do 18 | allow(fake_object).to receive(:expires_in).and_return(2.minutes) 19 | expect(fake_object).not_to be_expired 20 | end 21 | 22 | it "is expired if time has passed" do 23 | allow(fake_object).to receive(:expires_in).and_return(10.seconds) 24 | expect(fake_object).to be_expired 25 | end 26 | 27 | it "is not expired if expires_in is not set" do 28 | allow(fake_object).to receive(:expires_in).and_return(nil) 29 | expect(fake_object).not_to be_expired 30 | end 31 | end 32 | 33 | describe "#expires_in_seconds" do 34 | it "returns the amount of time remaining until the token is expired" do 35 | allow(fake_object).to receive(:expires_in).and_return(2.minutes) 36 | expect(fake_object.expires_in_seconds).to eq(60) 37 | end 38 | 39 | it "returns 0 when expired" do 40 | allow(fake_object).to receive(:expires_in).and_return(30.seconds) 41 | expect(fake_object.expires_in_seconds).to eq(0) 42 | end 43 | 44 | it "returns nil when expires_in is nil" do 45 | allow(fake_object).to receive(:expires_in).and_return(nil) 46 | expect(fake_object.expires_in_seconds).to be_nil 47 | end 48 | end 49 | 50 | describe "#expires_at" do 51 | it "returns the expiration time of the token" do 52 | allow(fake_object).to receive(:expires_in).and_return(2.minutes) 53 | expect(fake_object.expires_at).to be_a(Time) 54 | end 55 | 56 | it "returns nil when expires_in is nil" do 57 | allow(fake_object).to receive(:expires_in).and_return(nil) 58 | expect(fake_object.expires_at).to be_nil 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/lib/models/reusable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::Models::Reusable do 6 | subject(:fake_object) do 7 | Class.new do 8 | include Doorkeeper::Models::Reusable 9 | end.new 10 | end 11 | 12 | describe "#reusable?" do 13 | it "is reusable if its expires_in is nil" do 14 | allow(fake_object).to receive(:expired?).and_return(false) 15 | allow(fake_object).to receive(:expires_in).and_return(nil) 16 | expect(fake_object).to be_reusable 17 | end 18 | 19 | it "is reusable if its expiry has crossed reusable limit" do 20 | allow(fake_object).to receive(:expired?).and_return(false) 21 | allow(Doorkeeper.configuration).to receive(:token_reuse_limit).and_return(90) 22 | allow(fake_object).to receive(:expires_in).and_return(100.seconds) 23 | allow(fake_object).to receive(:expires_in_seconds).and_return(20.seconds) 24 | expect(fake_object).to be_reusable 25 | end 26 | 27 | it "is not reusable if its expiry has crossed reusable limit" do 28 | allow(fake_object).to receive(:expired?).and_return(false) 29 | allow(Doorkeeper.configuration).to receive(:token_reuse_limit).and_return(90) 30 | allow(fake_object).to receive(:expires_in).and_return(100.seconds) 31 | allow(fake_object).to receive(:expires_in_seconds).and_return(5.seconds) 32 | expect(fake_object).not_to be_reusable 33 | end 34 | 35 | it "is not reusable if it is already expired" do 36 | allow(fake_object).to receive(:expired?).and_return(true) 37 | expect(fake_object).not_to be_reusable 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/lib/models/revocable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::Models::Revocable do 6 | subject(:fake_object) do 7 | Class.new do 8 | include Doorkeeper::Models::Revocable 9 | end.new 10 | end 11 | 12 | describe "#revoke" do 13 | it "updates :revoked_at attribute with current time" do 14 | utc = double utc: double 15 | clock = double now: utc 16 | expect(fake_object).to receive(:update_attribute).with(:revoked_at, clock.now.utc) 17 | fake_object.revoke(clock) 18 | end 19 | end 20 | 21 | describe "#revoked?" do 22 | it "is revoked if :revoked_at has passed" do 23 | allow(fake_object).to receive(:revoked_at).and_return(Time.now.utc - 1000) 24 | expect(fake_object).to be_revoked 25 | end 26 | 27 | it "is not revoked if :revoked_at has not passed" do 28 | allow(fake_object).to receive(:revoked_at).and_return(Time.now.utc + 1000) 29 | expect(fake_object).not_to be_revoked 30 | end 31 | 32 | it "is not revoked if :revoked_at is not set" do 33 | allow(fake_object).to receive(:revoked_at).and_return(nil) 34 | expect(fake_object).not_to be_revoked 35 | end 36 | end 37 | 38 | describe "#revoke_previous_refresh_token!" do 39 | it "revokes the previous token if exists and resets the `previous_refresh_token` attribute" do 40 | previous_token = FactoryBot.create( 41 | :access_token, 42 | refresh_token: "refresh_token", 43 | ) 44 | current_token = FactoryBot.create( 45 | :access_token, 46 | previous_refresh_token: previous_token.refresh_token, 47 | ) 48 | 49 | current_token.revoke_previous_refresh_token! 50 | 51 | expect(current_token.previous_refresh_token).to be_empty 52 | expect(previous_token.reload).to be_revoked 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/lib/models/scopes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::Models::Scopes do 6 | subject(:fake_object) do 7 | Class.new(Struct.new(:scopes)) do 8 | include Doorkeeper::Models::Scopes 9 | end.new 10 | end 11 | 12 | before do 13 | fake_object[:scopes] = "public admin" 14 | end 15 | 16 | describe "#scopes" do 17 | it "is a `Scopes` class" do 18 | expect(fake_object.scopes).to be_a(Doorkeeper::OAuth::Scopes) 19 | end 20 | 21 | it "includes scopes" do 22 | expect(fake_object.scopes).to include("public") 23 | end 24 | end 25 | 26 | describe "#scopes=" do 27 | it "accepts String" do 28 | fake_object.scopes = "private admin" 29 | expect(fake_object.scopes_string).to eq("private admin") 30 | end 31 | 32 | it "accepts Array" do 33 | fake_object.scopes = %w[private admin] 34 | expect(fake_object.scopes_string).to eq("private admin") 35 | end 36 | 37 | it "ignores duplicated scopes" do 38 | fake_object.scopes = %w[private admin admin] 39 | expect(fake_object.scopes_string).to eq("private admin") 40 | 41 | fake_object.scopes = "private admin admin" 42 | expect(fake_object.scopes_string).to eq("private admin") 43 | end 44 | end 45 | 46 | describe "#scopes_string" do 47 | it "is a `Scopes` class" do 48 | expect(fake_object.scopes_string).to eq("public admin") 49 | end 50 | end 51 | 52 | describe "#includes_scope?" do 53 | it "returns true if at least one scope is included" do 54 | expect(fake_object.includes_scope?("public", "private")).to be true 55 | end 56 | 57 | it "returns false if no scopes are included" do 58 | expect(fake_object.includes_scope?("teacher", "student")).to be false 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/lib/oauth/authorization/uri_builder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::OAuth::Authorization::URIBuilder do 6 | describe ".uri_with_query" do 7 | it "returns the uri with query" do 8 | uri = described_class.uri_with_query "http://example.com/", parameter: "value" 9 | expect(uri).to eq("http://example.com/?parameter=value") 10 | end 11 | 12 | it "rejects nil values" do 13 | uri = described_class.uri_with_query "http://example.com/", parameter: "" 14 | expect(uri).to eq("http://example.com/?") 15 | end 16 | 17 | it "preserves original query parameters" do 18 | uri = described_class.uri_with_query "http://example.com/?query1=value", parameter: "value" 19 | expect(uri).to match(/query1=value/) 20 | expect(uri).to match(/parameter=value/) 21 | end 22 | end 23 | 24 | describe ".uri_with_fragment" do 25 | it "returns uri with parameters as fragments" do 26 | uri = described_class.uri_with_fragment "http://example.com/", parameter: "value" 27 | expect(uri).to eq("http://example.com/#parameter=value") 28 | end 29 | 30 | it "preserves original query parameters" do 31 | uri = described_class.uri_with_fragment "http://example.com/?query1=value1", parameter: "value" 32 | expect(uri).to eq("http://example.com/?query1=value1#parameter=value") 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/lib/oauth/base_response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::OAuth::BaseResponse do 6 | subject(:response) do 7 | described_class.new 8 | end 9 | 10 | describe "#body" do 11 | it "returns an empty Hash" do 12 | expect(response.body).to eq({}) 13 | end 14 | end 15 | 16 | describe "#description" do 17 | it "returns an empty String" do 18 | expect(response.description).to eq("") 19 | end 20 | end 21 | 22 | describe "#headers" do 23 | it "returns an empty Hash" do 24 | expect(response.headers).to eq({}) 25 | end 26 | end 27 | 28 | describe "#redirectable?" do 29 | it "returns false" do 30 | expect(response.redirectable?).to eq(false) 31 | end 32 | end 33 | 34 | describe "#redirect_uri" do 35 | it "returns an empty String" do 36 | expect(response.redirect_uri).to eq("") 37 | end 38 | end 39 | 40 | describe "#status" do 41 | it "returns :ok" do 42 | expect(response.status).to eq(:ok) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/lib/oauth/client_credentials_integration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::OAuth::ClientCredentialsRequest do 6 | let(:server) { Doorkeeper.configuration } 7 | 8 | context "with a valid request" do 9 | let(:client) { Doorkeeper::OAuth::Client.new(FactoryBot.build_stubbed(:application)) } 10 | 11 | it "issues an access token" do 12 | request = described_class.new(server, client, {}) 13 | expect do 14 | request.authorize 15 | end.to change { Doorkeeper::AccessToken.count }.by(1) 16 | end 17 | end 18 | 19 | describe "with an invalid request" do 20 | it "does not issue an access token" do 21 | request = described_class.new(server, nil, {}) 22 | expect do 23 | request.authorize 24 | end.not_to(change { Doorkeeper::AccessToken.count }) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/lib/oauth/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::OAuth::Client do 6 | describe ".find" do 7 | let(:method) { double } 8 | 9 | it "finds the client via uid" do 10 | client = double 11 | expect(method).to receive(:call).with("uid").and_return(client) 12 | expect(described_class.find("uid", method)) 13 | .to be_a(described_class) 14 | end 15 | 16 | it "returns nil if client was not found" do 17 | expect(method).to receive(:call).with("uid").and_return(nil) 18 | expect(described_class.find("uid", method)).to be_nil 19 | end 20 | end 21 | 22 | describe ".authenticate" do 23 | it "returns the authenticated client via credentials" do 24 | credentials = Doorkeeper::OAuth::Client::Credentials.new("some-uid", "some-secret") 25 | authenticator = double 26 | expect(authenticator).to receive(:call).with("some-uid", "some-secret").and_return(double) 27 | expect(described_class.authenticate(credentials, authenticator)) 28 | .to be_a(described_class) 29 | end 30 | 31 | it "returns nil if client was not authenticated" do 32 | credentials = Doorkeeper::OAuth::Client::Credentials.new("some-uid", "some-secret") 33 | authenticator = double 34 | expect(authenticator).to receive(:call).with("some-uid", "some-secret").and_return(nil) 35 | expect(described_class.authenticate(credentials, authenticator)).to be_nil 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/lib/oauth/code_request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::OAuth::CodeRequest do 6 | subject(:request) do 7 | described_class.new(pre_auth, owner) 8 | end 9 | 10 | let(:pre_auth) do 11 | allow(Doorkeeper.config) 12 | .to receive(:default_scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("public")) 13 | allow(Doorkeeper.config) 14 | .to receive(:grant_flows).and_return(Doorkeeper::OAuth::Scopes.from_string("authorization_code")) 15 | 16 | application = FactoryBot.create(:application, scopes: "public") 17 | client = Doorkeeper::OAuth::Client.new(application) 18 | 19 | attributes = { 20 | client_id: client.uid, 21 | response_type: "code", 22 | redirect_uri: "https://app.com/callback", 23 | response_mode: response_mode, 24 | }.compact 25 | 26 | pre_auth = Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.config, attributes) 27 | pre_auth.authorizable? 28 | pre_auth 29 | end 30 | 31 | let(:response_mode) { nil } 32 | let(:owner) { FactoryBot.create(:resource_owner) } 33 | 34 | context "when pre_auth is authorized" do 35 | it "creates an access grant and returns a code response" do 36 | expect { request.authorize }.to change { Doorkeeper::AccessGrant.count }.by(1) 37 | expect(request.authorize).to be_a(Doorkeeper::OAuth::CodeResponse) 38 | expect(request.authorize.response_on_fragment).to be false 39 | end 40 | 41 | context "with 'fragment' as response_mode" do 42 | let(:response_mode) { "fragment" } 43 | 44 | it "returns a code response with response_on_fragment set to true" do 45 | expect(request.authorize.response_on_fragment).to be true 46 | end 47 | end 48 | end 49 | 50 | context "when pre_auth is denied" do 51 | it "does not create access grant and returns a error response" do 52 | expect { request.deny }.not_to(change { Doorkeeper::AccessGrant.count }) 53 | expect(request.deny).to be_a(Doorkeeper::OAuth::ErrorResponse) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/lib/oauth/error_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::OAuth::Error do 6 | subject(:error) { described_class.new(:some_error, :some_state, nil) } 7 | 8 | it { expect(error).to respond_to(:name) } 9 | it { expect(error).to respond_to(:state) } 10 | it { expect(error).to respond_to(:translate_options) } 11 | 12 | describe "#description" do 13 | it "is translated from translation messages" do 14 | expect(I18n).to receive(:translate).with( 15 | :some_error, 16 | scope: %i[doorkeeper errors messages], 17 | default: :server_error, 18 | ) 19 | error.description 20 | end 21 | 22 | context "when there are variables" do 23 | subject(:error) do 24 | described_class.new( 25 | :invalid_code_challenge_method, 26 | :some_state, 27 | { 28 | challenge_methods: "foo, bar", 29 | count: 2, 30 | } 31 | ) 32 | end 33 | 34 | it "is translated from translation messages with variables" do 35 | expect(error.description).to eq("The code_challenge_method must be one of foo, bar.") 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/lib/oauth/forbidden_token_response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::OAuth::ForbiddenTokenResponse do 6 | subject(:response) { described_class.new } 7 | 8 | describe "#name" do 9 | it { expect(response.name).to eq(:invalid_scope) } 10 | end 11 | 12 | describe "#status" do 13 | it { expect(response.status).to eq(:forbidden) } 14 | end 15 | 16 | describe ".from_scopes" do 17 | subject(:response) { described_class.from_scopes(["public"]) } 18 | 19 | it "includes a list of acceptable scopes" do 20 | expect(response.description).to include("public") 21 | end 22 | 23 | it "explains that the problem is due to a missing scope" do 24 | expect(response.description).to match(/requires scope/i) 25 | end 26 | 27 | it "does not use the scope description from authorize page" do 28 | expect(response.description).not_to eql("Access your public data") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/oauth/helpers/unique_token_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | module Doorkeeper::OAuth::Helpers 6 | describe UniqueToken do 7 | let :generator do 8 | ->(size) { "a" * size } 9 | end 10 | 11 | it "is able to customize the generator method" do 12 | token = described_class.generate(generator: generator) 13 | expect(token).to eq("a" * 32) 14 | end 15 | 16 | it "is able to customize the size of the token" do 17 | token = described_class.generate(generator: generator, size: 2) 18 | expect(token).to eq("aa") 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/lib/oauth/invalid_token_response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::OAuth::InvalidTokenResponse do 6 | let(:response) { described_class.new } 7 | 8 | describe "#name" do 9 | it { expect(response.name).to eq(:invalid_token) } 10 | end 11 | 12 | describe "#status" do 13 | it { expect(response.status).to eq(:unauthorized) } 14 | end 15 | 16 | describe ".from_access_token" do 17 | let(:response) { described_class.from_access_token(access_token) } 18 | 19 | context "when token revoked" do 20 | let(:access_token) { double(revoked?: true, expired?: true) } 21 | 22 | it "sets a description" do 23 | expect(response.description).to include("revoked") 24 | end 25 | 26 | it "sets the reason" do 27 | expect(response.reason).to eq(:revoked) 28 | end 29 | end 30 | 31 | context "when token expired" do 32 | let(:access_token) { double(revoked?: false, expired?: true) } 33 | 34 | it "sets a description" do 35 | expect(response.description).to include("expired") 36 | end 37 | 38 | it "sets the reason" do 39 | expect(response.reason).to eq(:expired) 40 | end 41 | end 42 | 43 | context "when unknown" do 44 | let(:access_token) { double(revoked?: false, expired?: false) } 45 | 46 | it "sets a description" do 47 | expect(response.description).to include("invalid") 48 | end 49 | 50 | it "sets the reason" do 51 | expect(response.reason).to eq(:unknown) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/lib/option_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::Config::Option do 6 | class Extension 7 | def self.configure(&block) 8 | @config = Config::Builder.new(Config.new, &block).build 9 | end 10 | 11 | def self.configuration 12 | @config || (raise Errors::MissingConfiguration) 13 | end 14 | 15 | class Config 16 | class Builder < Doorkeeper::Config::AbstractBuilder 17 | def enforce_something 18 | @config.instance_variable_set(:@enforce_something, true) 19 | end 20 | end 21 | 22 | def enforce_something? 23 | if defined?(@enforce_something) 24 | @enforce_something 25 | else 26 | false 27 | end 28 | end 29 | 30 | def self.builder_class 31 | Config::Builder 32 | end 33 | 34 | extend Doorkeeper::Config::Option 35 | end 36 | end 37 | 38 | it "allows to define custom options in extensions" do 39 | expect do 40 | Extension::Config.option(:some_option, default: 1) 41 | end.not_to raise_error 42 | 43 | Extension.configure do 44 | some_option 20 45 | enforce_something 46 | end 47 | 48 | expect(Extension.configuration.some_option).to eq(20) 49 | expect(Extension.configuration.enforce_something?).to be(true) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/lib/request/strategy_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Doorkeeper::Request::Strategy do 6 | subject(:strategy) { described_class.new(server) } 7 | 8 | let(:server) { double } 9 | 10 | describe "#initialize" do 11 | it "sets the server attribute" do 12 | expect(strategy.server).to eq server 13 | end 14 | end 15 | 16 | describe "#request" do 17 | it "requires an implementation" do 18 | expect { strategy.request }.to raise_exception NotImplementedError 19 | end 20 | end 21 | 22 | describe "a sample Strategy subclass" do 23 | subject(:strategy) { strategy_class.new(server) } 24 | 25 | let(:fake_request) { double } 26 | 27 | let(:strategy_class) do 28 | subclass = Class.new(described_class) do 29 | class << self 30 | attr_accessor :fake_request 31 | end 32 | 33 | def request 34 | self.class.fake_request 35 | end 36 | end 37 | 38 | subclass.fake_request = fake_request 39 | subclass 40 | end 41 | 42 | it "provides a request implementation" do 43 | expect(strategy.request).to eq fake_request 44 | end 45 | 46 | it "authorizes the request" do 47 | expect(fake_request).to receive :authorize 48 | strategy.authorize 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/lib/secret_storing/base_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe ::Doorkeeper::SecretStoring::Base do 6 | let(:instance) { double("instance", token: "foo") } 7 | 8 | describe "#transform_secret" do 9 | it "raises" do 10 | expect { described_class.transform_secret("foo") } 11 | .to raise_error(NotImplementedError) 12 | end 13 | end 14 | 15 | describe "#store_secret" do 16 | it "sends to response of #transform_secret to the instance" do 17 | expect(described_class) 18 | .to receive(:transform_secret).with("bar") 19 | .and_return "bar+transform" 20 | 21 | expect(instance).to receive(:token=).with "bar+transform" 22 | result = described_class.store_secret instance, :token, "bar" 23 | expect(result).to eq "bar+transform" 24 | end 25 | end 26 | 27 | describe "#restore_secret" do 28 | it "raises" do 29 | expect { described_class.restore_secret(described_class, :token) } 30 | .to raise_error(NotImplementedError) 31 | end 32 | end 33 | 34 | describe "#allows_restoring_secrets?" do 35 | it "does not allow it" do 36 | expect(described_class.allows_restoring_secrets?).to eq false 37 | end 38 | end 39 | 40 | describe "validate_for" do 41 | it "allows for valid model" do 42 | expect(described_class.validate_for(:application)).to eq true 43 | expect(described_class.validate_for(:token)).to eq true 44 | end 45 | 46 | it "raises for invalid model" do 47 | expect { described_class.validate_for(:wat) } 48 | .to raise_error(ArgumentError, /can not be used for wat/) 49 | end 50 | end 51 | 52 | describe "secret_matches?" do 53 | before do 54 | allow(described_class).to receive(:transform_secret) { |input| "transformed: #{input}" } 55 | end 56 | 57 | it "compares input with #transform_secret" do 58 | expect(described_class.secret_matches?("input", "input")).to eq false 59 | expect(described_class.secret_matches?("a", "transformed: a")).to eq true 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/lib/secret_storing/bcrypt_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "bcrypt" 5 | 6 | RSpec.describe ::Doorkeeper::SecretStoring::BCrypt do 7 | let(:instance) { double("instance", token: "foo") } 8 | 9 | describe "#transform_secret" do 10 | it "creates a bcrypt password" do 11 | expect(described_class.transform_secret("foo")).to be_a BCrypt::Password 12 | end 13 | end 14 | 15 | describe "#restore_secret" do 16 | it "raises" do 17 | expect { described_class.restore_secret(instance, :token) } 18 | .to raise_error(NotImplementedError) 19 | end 20 | end 21 | 22 | describe "#allows_restoring_secrets?" do 23 | it "does not allow it" do 24 | expect(described_class.allows_restoring_secrets?).to be(false) 25 | end 26 | end 27 | 28 | describe "validate_for" do 29 | it "allows for valid model" do 30 | expect(described_class.validate_for(:application)).to eq(true) 31 | end 32 | 33 | it "raises for invalid model" do 34 | expect { described_class.validate_for(:wat) } 35 | .to raise_error(ArgumentError, /can only be used for storing application secrets/) 36 | expect { described_class.validate_for(:token) } 37 | .to raise_error(ArgumentError, /can only be used for storing application secrets/) 38 | end 39 | end 40 | 41 | describe "secret_matches?" do 42 | it "compares input with #transform_secret" do 43 | expect(described_class.secret_matches?("input", "input")).to eq(false) 44 | 45 | password = BCrypt::Password.create("foobar") 46 | expect(described_class.secret_matches?("foobar", password.to_s)).to eq(true) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/lib/secret_storing/plain_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe ::Doorkeeper::SecretStoring::Plain do 6 | let(:instance) { double("instance", token: "foo") } 7 | 8 | describe "#transform_secret" do 9 | it "raises" do 10 | expect(described_class.transform_secret("foo")).to eq "foo" 11 | end 12 | end 13 | 14 | describe "#restore_secret" do 15 | it "raises" do 16 | expect(described_class.restore_secret(instance, :token)).to eq "foo" 17 | end 18 | end 19 | 20 | describe "#allows_restoring_secrets?" do 21 | it "does allow it" do 22 | expect(described_class.allows_restoring_secrets?).to eq true 23 | end 24 | end 25 | 26 | describe "validate_for" do 27 | it "allows for valid model" do 28 | expect(described_class.validate_for(:application)).to eq true 29 | expect(described_class.validate_for(:token)).to eq true 30 | end 31 | 32 | it "raises for invalid model" do 33 | expect { described_class.validate_for(:wat) } 34 | .to raise_error(ArgumentError, /can not be used for wat/) 35 | end 36 | end 37 | 38 | describe "secret_matches?" do 39 | it "compares input with #transform_secret" do 40 | expect(described_class.secret_matches?("input", "input")).to eq true 41 | expect(described_class.secret_matches?("a", "b")).to eq false 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/lib/secret_storing/sha256_hash_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe ::Doorkeeper::SecretStoring::Sha256Hash do 6 | let(:instance) { double("instance") } 7 | 8 | let(:hash_function) do 9 | ->(input) { ::Digest::SHA256.hexdigest(input) } 10 | end 11 | 12 | describe "#transform_secret" do 13 | it "raises" do 14 | expect(described_class.transform_secret("foo")).to eq hash_function.call("foo") 15 | end 16 | end 17 | 18 | describe "#restore_secret" do 19 | it "raises" do 20 | expect { described_class.restore_secret(instance, :token) }.to raise_error(NotImplementedError) 21 | end 22 | end 23 | 24 | describe "#allows_restoring_secrets?" do 25 | it "does not allow it" do 26 | expect(described_class.allows_restoring_secrets?).to eq false 27 | end 28 | end 29 | 30 | describe "validate_for" do 31 | it "allows for valid model" do 32 | expect(described_class.validate_for(:application)).to eq true 33 | expect(described_class.validate_for(:token)).to eq true 34 | end 35 | 36 | it "raises for invalid model" do 37 | expect { described_class.validate_for(:wat) }.to raise_error(ArgumentError, /can not be used for wat/) 38 | end 39 | end 40 | 41 | describe "secret_matches?" do 42 | it "compares input with #transform_secret" do 43 | expect(described_class.secret_matches?("input", "input")).to eq false 44 | expect(described_class.secret_matches?("a", hash_function.call("a"))).to eq true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/requests/applications/authorized_applications_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | feature "Authorized applications" do 6 | background do 7 | @user = User.create!(name: "Joe", password: "sekret") 8 | @client = client_exists(name: "Amazing Client App") 9 | resource_owner_is_authenticated @user 10 | client_is_authorized @client, @user 11 | end 12 | 13 | scenario "display user's authorized applications" do 14 | visit "/oauth/authorized_applications" 15 | i_should_see "Amazing Client App" 16 | end 17 | 18 | scenario "do not display other user's authorized applications" do 19 | client = client_exists(name: "Another Client App") 20 | client_is_authorized client, User.create!(name: "Joe", password: "sekret") 21 | visit "/oauth/authorized_applications" 22 | i_should_not_see "Another Client App" 23 | end 24 | 25 | scenario "user revoke access to application" do 26 | visit "/oauth/authorized_applications" 27 | i_should_see "Amazing Client App" 28 | click_on "Revoke" 29 | i_should_see "Application revoked" 30 | i_should_not_see "Amazing Client App" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/requests/flows/implicit_grant_errors_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | feature "Implicit Grant Flow Errors" do 6 | background do 7 | default_scopes_exist :default 8 | config_is_set(:authenticate_resource_owner) { User.first || redirect_to("/sign_in") } 9 | config_is_set(:grant_flows, ["implicit"]) 10 | client_exists 11 | create_resource_owner 12 | sign_in 13 | end 14 | 15 | after do 16 | access_token_should_not_exist 17 | end 18 | 19 | context "when validate client_id param" do 20 | scenario "displays invalid_client error for invalid client_id" do 21 | visit authorization_endpoint_url(client_id: "invalid", response_type: "token") 22 | i_should_not_see "Authorize" 23 | i_should_see_translated_error_message :invalid_client 24 | end 25 | 26 | scenario "displays invalid_request error when client_id is missing" do 27 | visit authorization_endpoint_url(client_id: "", response_type: "token") 28 | i_should_not_see "Authorize" 29 | i_should_see_translated_invalid_request_error_message :missing_param, :client_id 30 | end 31 | end 32 | 33 | context "when validate redirect_uri param" do 34 | scenario "displays invalid_redirect_uri error for invalid redirect_uri" do 35 | visit authorization_endpoint_url(client: @client, redirect_uri: "invalid", response_type: "token") 36 | i_should_not_see "Authorize" 37 | i_should_see_translated_error_message :invalid_redirect_uri 38 | end 39 | 40 | scenario "displays invalid_redirect_uri error when redirect_uri is missing" do 41 | visit authorization_endpoint_url(client: @client, redirect_uri: "", response_type: "token") 42 | i_should_not_see "Authorize" 43 | i_should_see_translated_error_message :invalid_redirect_uri 44 | end 45 | end 46 | 47 | context "when validate response_mode param" do 48 | scenario "displays unsupported_response_mode error when using 'query' response mode" do 49 | visit authorization_endpoint_url(client: @client, response_type: "token", response_mode: "query") 50 | i_should_not_see "Authorize" 51 | i_should_see_translated_error_message :unsupported_response_mode 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/requests/protected_resources/metal_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "ActionController::Metal API" do 6 | before do 7 | @client = FactoryBot.create(:application) 8 | @resource = User.create!(name: "Joe", password: "sekret") 9 | @token = client_is_authorized(@client, @resource) 10 | end 11 | 12 | it "client requests protected resource with valid token" do 13 | get "/metal.json?access_token=#{@token.token}" 14 | expect(json_response).to include("ok" => true) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/routing/default_routes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "Default routes" do 6 | it "GET /oauth/authorize routes to authorizations controller" do 7 | expect(get("/oauth/authorize")).to route_to("doorkeeper/authorizations#new") 8 | end 9 | 10 | it "POST /oauth/authorize routes to authorizations controller" do 11 | expect(post("/oauth/authorize")).to route_to("doorkeeper/authorizations#create") 12 | end 13 | 14 | it "DELETE /oauth/authorize routes to authorizations controller" do 15 | expect(delete("/oauth/authorize")).to route_to("doorkeeper/authorizations#destroy") 16 | end 17 | 18 | it "POST /oauth/token routes to tokens controller" do 19 | expect(post("/oauth/token")).to route_to("doorkeeper/tokens#create") 20 | end 21 | 22 | it "POST /oauth/revoke routes to tokens controller" do 23 | expect(post("/oauth/revoke")).to route_to("doorkeeper/tokens#revoke") 24 | end 25 | 26 | it "POST /oauth/introspect routes to tokens controller" do 27 | expect(post("/oauth/introspect")).to route_to("doorkeeper/tokens#introspect") 28 | end 29 | 30 | it "GET /oauth/applications routes to applications controller" do 31 | expect(get("/oauth/applications")).to route_to("doorkeeper/applications#index") 32 | end 33 | 34 | it "GET /oauth/authorized_applications routes to authorized applications controller" do 35 | expect(get("/oauth/authorized_applications")).to route_to("doorkeeper/authorized_applications#index") 36 | end 37 | 38 | it "GET /oauth/token/info route to authorized TokenInfo controller" do 39 | expect(get("/oauth/token/info")).to route_to("doorkeeper/token_info#show") 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/routing/scoped_routes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "Scoped routes" do 6 | before :all do 7 | Doorkeeper.configure do 8 | orm DOORKEEPER_ORM 9 | allow_token_introspection false 10 | end 11 | 12 | Rails.application.routes.disable_clear_and_finalize = true 13 | 14 | Rails.application.routes.draw do 15 | use_doorkeeper scope: "scope" 16 | end 17 | end 18 | 19 | after :all do 20 | Rails.application.routes.clear! 21 | 22 | load File.expand_path("../dummy/config/routes.rb", __dir__) 23 | end 24 | 25 | it "GET /scope/authorize routes to authorizations controller" do 26 | expect(get("/scope/authorize")).to route_to("doorkeeper/authorizations#new") 27 | end 28 | 29 | it "POST /scope/authorize routes to authorizations controller" do 30 | expect(post("/scope/authorize")).to route_to("doorkeeper/authorizations#create") 31 | end 32 | 33 | it "DELETE /scope/authorize routes to authorizations controller" do 34 | expect(delete("/scope/authorize")).to route_to("doorkeeper/authorizations#destroy") 35 | end 36 | 37 | it "POST /scope/token routes to tokens controller" do 38 | expect(post("/scope/token")).to route_to("doorkeeper/tokens#create") 39 | end 40 | 41 | it "GET /scope/applications routes to applications controller" do 42 | expect(get("/scope/applications")).to route_to("doorkeeper/applications#index") 43 | end 44 | 45 | it "GET /scope/authorized_applications routes to authorized applications controller" do 46 | expect(get("/scope/authorized_applications")).to route_to("doorkeeper/authorized_applications#index") 47 | end 48 | 49 | it "GET /scope/token/info route to authorized TokenInfo controller" do 50 | expect(get("/scope/token/info")).to route_to("doorkeeper/token_info#show") 51 | end 52 | 53 | it "POST /scope/introspect routes not to exist" do 54 | expect(post("/scope/introspect")).not_to be_routable 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "coveralls" 4 | 5 | Coveralls.wear!("rails") do 6 | add_filter("/spec/") 7 | add_filter("/lib/generators/doorkeeper/templates/") 8 | end 9 | 10 | ENV["RAILS_ENV"] ||= "test" 11 | 12 | $LOAD_PATH.unshift File.dirname(__FILE__) 13 | 14 | require "#{File.dirname(__FILE__)}/support/doorkeeper_rspec.rb" 15 | 16 | DOORKEEPER_ORM = Doorkeeper::RSpec.detect_orm 17 | 18 | require "dummy/config/environment" 19 | require "rspec/rails" 20 | require "capybara/rspec" 21 | require "database_cleaner" 22 | require "generator_spec/test_case" 23 | 24 | # Load JRuby SQLite3 if in that platform 25 | if defined? JRUBY_VERSION 26 | require "jdbc/sqlite3" 27 | Jdbc::SQLite3.load_driver 28 | end 29 | 30 | Doorkeeper::RSpec.print_configuration_info 31 | 32 | require "support/orm/#{DOORKEEPER_ORM}" 33 | require "support/render_with_matcher" 34 | 35 | Dir["#{File.dirname(__FILE__)}/support/{dependencies,helpers,shared}/*.rb"].sort.each { |file| require file } 36 | 37 | RSpec.configure do |config| 38 | config.infer_spec_type_from_file_location! 39 | config.mock_with :rspec 40 | 41 | config.infer_base_class_for_anonymous_controllers = false 42 | 43 | config.include RSpec::Rails::RequestExampleGroup, type: :request 44 | 45 | config.before do 46 | DatabaseCleaner.start 47 | Doorkeeper.configure { orm DOORKEEPER_ORM } 48 | end 49 | 50 | config.after do 51 | DatabaseCleaner.clean 52 | end 53 | 54 | config.order = "random" 55 | end 56 | -------------------------------------------------------------------------------- /spec/spec_helper_integration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # For compatibility only 4 | require "spec_helper" 5 | -------------------------------------------------------------------------------- /spec/support/dependencies/factory_bot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "factory_bot" 4 | FactoryBot.find_definitions 5 | -------------------------------------------------------------------------------- /spec/support/doorkeeper_rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | class RSpec 5 | # Print's useful information about env: Ruby / Rails versions, 6 | # Doorkeeper configuration, etc. 7 | def self.print_configuration_info 8 | puts <<-INFO.strip_heredoc 9 | ====> Doorkeeper ORM: '#{Doorkeeper.configuration.orm}' 10 | ====> Doorkeeper version: #{Doorkeeper.gem_version} 11 | ====> Rails version: #{::Rails.version} 12 | ====> Ruby version: #{RUBY_VERSION} on #{RUBY_PLATFORM} 13 | INFO 14 | end 15 | 16 | # Tries to find ORM from the Gemfile used to run test suite 17 | def self.detect_orm 18 | orm = (ENV["BUNDLE_GEMFILE"] || "").match(/Gemfile\.(.+)\.rb/) 19 | (orm && orm[1] || ENV["ORM"] || :active_record).to_sym 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/support/helpers/access_token_request_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AccessTokenRequestHelper 4 | def client_is_authorized(client, resource_owner, access_token_attributes = {}) 5 | attributes = { 6 | application: client, 7 | resource_owner_id: resource_owner.id, 8 | resource_owner_type: resource_owner.class.name, 9 | }.merge(access_token_attributes) 10 | FactoryBot.create(:access_token, attributes) 11 | end 12 | end 13 | 14 | RSpec.configuration.send :include, AccessTokenRequestHelper 15 | -------------------------------------------------------------------------------- /spec/support/helpers/authorization_request_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AuthorizationRequestHelper 4 | def resource_owner_is_authenticated(resource_owner = nil) 5 | resource_owner ||= User.create!(name: "Joe", password: "sekret") 6 | Doorkeeper.config.instance_variable_set(:@authenticate_resource_owner, proc { resource_owner }) 7 | end 8 | 9 | def resource_owner_is_not_authenticated 10 | Doorkeeper.config.instance_variable_set(:@authenticate_resource_owner, proc { redirect_to("/sign_in") }) 11 | end 12 | 13 | def default_scopes_exist(*scopes) 14 | Doorkeeper.config.instance_variable_set(:@default_scopes, Doorkeeper::OAuth::Scopes.from_array(scopes)) 15 | end 16 | 17 | def optional_scopes_exist(*scopes) 18 | Doorkeeper.config.instance_variable_set(:@optional_scopes, Doorkeeper::OAuth::Scopes.from_array(scopes)) 19 | end 20 | 21 | def client_should_be_authorized(client) 22 | expect(client.access_grants.size).to eq(1) 23 | end 24 | 25 | def client_should_not_be_authorized(client) 26 | expect(client.size).to eq(0) 27 | end 28 | 29 | def i_should_be_on_client_callback(client) 30 | expect(client.redirect_uri).to eq("#{current_uri.scheme}://#{current_uri.host}#{current_uri.path}") 31 | end 32 | 33 | def allowing_forgery_protection(&_block) 34 | original_value = ActionController::Base.allow_forgery_protection 35 | ActionController::Base.allow_forgery_protection = true 36 | 37 | yield 38 | ensure 39 | ActionController::Base.allow_forgery_protection = original_value 40 | end 41 | end 42 | 43 | RSpec.configuration.send :include, AuthorizationRequestHelper 44 | -------------------------------------------------------------------------------- /spec/support/helpers/config_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ConfigHelper 4 | def config_is_set(setting, value = nil, &block) 5 | setting_ivar = "@#{setting}" 6 | value = block_given? ? block : value 7 | Doorkeeper.config.instance_variable_set(setting_ivar, value) 8 | end 9 | end 10 | 11 | RSpec.configuration.send :include, ConfigHelper 12 | -------------------------------------------------------------------------------- /spec/support/orm/active_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # load schema to in memory sqlite 4 | ActiveRecord::Migration.verbose = false 5 | load Rails.root + "db/schema.rb" 6 | -------------------------------------------------------------------------------- /spec/support/render_with_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Adds the `render_with` matcher. 4 | # Ex: 5 | # expect(controller).to render_with(template: :show, locals: { alpha: "beta" }) 6 | # 7 | module RenderWithMatcher 8 | def self.included(base) 9 | # Setup spying for our "render_with" matcher 10 | base.before do 11 | allow(controller).to receive(:render).and_wrap_original do |original, *args, **kwargs, &block| 12 | original.call(*args, **kwargs, &block) 13 | end 14 | end 15 | end 16 | 17 | RSpec::Matchers.define :render_with do |expected| 18 | match do |actual| 19 | have_received(:render).with(expected).matches?(actual) 20 | end 21 | end 22 | end 23 | 24 | RSpec.configure do |config| 25 | config.include RenderWithMatcher, type: :controller 26 | end 27 | -------------------------------------------------------------------------------- /spec/support/shared/hashing_shared_context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_context "with token hashing enabled" do 4 | let(:hashed_or_plain_token_func) do 5 | Doorkeeper::SecretStoring::Sha256Hash.method(:transform_secret) 6 | end 7 | 8 | before do 9 | Doorkeeper.configure do 10 | orm DOORKEEPER_ORM 11 | hash_token_secrets 12 | end 13 | end 14 | end 15 | 16 | shared_context "with token hashing and fallback lookup enabled" do 17 | let(:hashed_or_plain_token_func) do 18 | Doorkeeper::SecretStoring::Sha256Hash.method(:transform_secret) 19 | end 20 | 21 | before do 22 | Doorkeeper.configure do 23 | orm DOORKEEPER_ORM 24 | hash_token_secrets fallback: :plain 25 | end 26 | end 27 | end 28 | 29 | shared_context "with application hashing enabled" do 30 | let(:hashed_or_plain_token_func) do 31 | Doorkeeper::SecretStoring::Sha256Hash.method(:transform_secret) 32 | end 33 | 34 | before do 35 | Doorkeeper.configure do 36 | orm DOORKEEPER_ORM 37 | hash_application_secrets 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/support/shared/models_shared_examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_examples "an accessible token" do 4 | describe "#accessible?" do 5 | it "is accessible if token is not expired" do 6 | allow(subject).to receive(:expired?).and_return(false) 7 | expect(subject).to be_accessible 8 | end 9 | 10 | it "is not accessible if token is expired" do 11 | allow(subject).to receive(:expired?).and_return(true) 12 | expect(subject).not_to be_accessible 13 | end 14 | end 15 | end 16 | 17 | shared_examples "a revocable token" do 18 | describe "#accessible?" do 19 | before { subject.save! } 20 | 21 | it "is accessible if token is not revoked" do 22 | expect(subject).to be_accessible 23 | end 24 | 25 | it "is not accessible if token is revoked" do 26 | subject.revoke 27 | expect(subject).not_to be_accessible 28 | end 29 | end 30 | end 31 | 32 | shared_examples "a unique token" do 33 | describe "#token" do 34 | let(:owner) { FactoryBot.create(:resource_owner) } 35 | 36 | it "is generated before validation" do 37 | expect { subject.valid? }.to change(subject, :token).from(nil) 38 | end 39 | 40 | it "is not valid if token exists" do 41 | token1 = FactoryBot.create factory_name, resource_owner_id: owner.id, resource_owner_type: owner.class.name 42 | token2 = FactoryBot.create factory_name, resource_owner_id: owner.id, resource_owner_type: owner.class.name 43 | token2.token = token1.token 44 | expect(token2).not_to be_valid 45 | end 46 | 47 | it "expects database to throw an error when tokens are the same" do 48 | token1 = FactoryBot.create factory_name, resource_owner_id: owner.id, resource_owner_type: owner.class.name 49 | token2 = FactoryBot.create factory_name, resource_owner_id: owner.id, resource_owner_type: owner.class.name 50 | token2.token = token1.token 51 | expect do 52 | token2.save!(validate: false) 53 | end.to raise_error(uniqueness_error) 54 | end 55 | end 56 | end 57 | --------------------------------------------------------------------------------