Repository: applicake/doorkeeper
Branch: main
Commit: df16e5553035
Files: 326
Total size: 843.7 KB
Directory structure:
gitextract_wns691gx/
├── .codeclimate.yml
├── .coveralls.yml
├── .dockerignore
├── .editorconfig
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── changelog.yml
│ ├── ci.yml
│ └── rubocop.yml
├── .gitignore
├── .hound.yml
├── .rspec
├── .rubocop.yml
├── .rubocop_todo.yml
├── AGENTS.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Gemfile
├── MIT-LICENSE
├── NEWS.md
├── README.md
├── RELEASING.md
├── Rakefile
├── SECURITY.md
├── UPGRADE.md
├── app/
│ ├── assets/
│ │ └── stylesheets/
│ │ └── doorkeeper/
│ │ ├── admin/
│ │ │ └── application.css
│ │ └── application.css
│ ├── controllers/
│ │ └── doorkeeper/
│ │ ├── application_controller.rb
│ │ ├── application_metal_controller.rb
│ │ ├── applications_controller.rb
│ │ ├── authorizations_controller.rb
│ │ ├── authorized_applications_controller.rb
│ │ ├── token_info_controller.rb
│ │ └── tokens_controller.rb
│ ├── helpers/
│ │ └── doorkeeper/
│ │ └── dashboard_helper.rb
│ └── views/
│ ├── doorkeeper/
│ │ ├── applications/
│ │ │ ├── _delete_form.html.erb
│ │ │ ├── _form.html.erb
│ │ │ ├── edit.html.erb
│ │ │ ├── index.html.erb
│ │ │ ├── new.html.erb
│ │ │ └── show.html.erb
│ │ ├── authorizations/
│ │ │ ├── error.html.erb
│ │ │ ├── form_post.html.erb
│ │ │ ├── new.html.erb
│ │ │ └── show.html.erb
│ │ └── authorized_applications/
│ │ ├── _delete_form.html.erb
│ │ └── index.html.erb
│ └── layouts/
│ └── doorkeeper/
│ ├── admin.html.erb
│ └── application.html.erb
├── benchmark/
│ ├── ruby/
│ │ └── client_credentials.rb
│ └── wrk/
│ └── .keep
├── bin/
│ └── console
├── config/
│ └── locales/
│ └── en.yml
├── doorkeeper.gemspec
├── gemfiles/
│ ├── rails_7_0.gemfile
│ ├── rails_7_1.gemfile
│ ├── rails_7_2.gemfile
│ ├── rails_8_0.gemfile
│ └── rails_edge.gemfile
├── lib/
│ ├── doorkeeper/
│ │ ├── config/
│ │ │ ├── abstract_builder.rb
│ │ │ ├── option.rb
│ │ │ └── validations.rb
│ │ ├── config.rb
│ │ ├── engine.rb
│ │ ├── errors.rb
│ │ ├── grant_flow/
│ │ │ ├── fallback_flow.rb
│ │ │ ├── flow.rb
│ │ │ └── registry.rb
│ │ ├── grant_flow.rb
│ │ ├── grape/
│ │ │ ├── authorization_decorator.rb
│ │ │ └── helpers.rb
│ │ ├── helpers/
│ │ │ └── controller.rb
│ │ ├── models/
│ │ │ ├── access_grant_mixin.rb
│ │ │ ├── access_token_mixin.rb
│ │ │ ├── application_mixin.rb
│ │ │ └── concerns/
│ │ │ ├── accessible.rb
│ │ │ ├── expirable.rb
│ │ │ ├── expiration_time_sql_math.rb
│ │ │ ├── orderable.rb
│ │ │ ├── ownership.rb
│ │ │ ├── polymorphic_resource_owner.rb
│ │ │ ├── resource_ownerable.rb
│ │ │ ├── reusable.rb
│ │ │ ├── revocable.rb
│ │ │ ├── scopes.rb
│ │ │ ├── secret_storable.rb
│ │ │ └── write_to_primary.rb
│ │ ├── oauth/
│ │ │ ├── authorization/
│ │ │ │ ├── code.rb
│ │ │ │ ├── context.rb
│ │ │ │ ├── token.rb
│ │ │ │ └── uri_builder.rb
│ │ │ ├── authorization_code_request.rb
│ │ │ ├── base_request.rb
│ │ │ ├── base_response.rb
│ │ │ ├── client/
│ │ │ │ └── credentials.rb
│ │ │ ├── client.rb
│ │ │ ├── client_credentials/
│ │ │ │ ├── creator.rb
│ │ │ │ ├── issuer.rb
│ │ │ │ └── validator.rb
│ │ │ ├── client_credentials_request.rb
│ │ │ ├── code_request.rb
│ │ │ ├── code_response.rb
│ │ │ ├── error.rb
│ │ │ ├── error_response.rb
│ │ │ ├── forbidden_token_response.rb
│ │ │ ├── helpers/
│ │ │ │ ├── scope_checker.rb
│ │ │ │ ├── unique_token.rb
│ │ │ │ └── uri_checker.rb
│ │ │ ├── hooks/
│ │ │ │ └── context.rb
│ │ │ ├── invalid_request_response.rb
│ │ │ ├── invalid_token_response.rb
│ │ │ ├── nonstandard.rb
│ │ │ ├── password_access_token_request.rb
│ │ │ ├── pre_authorization.rb
│ │ │ ├── refresh_token_request.rb
│ │ │ ├── scopes.rb
│ │ │ ├── token.rb
│ │ │ ├── token_introspection.rb
│ │ │ ├── token_request.rb
│ │ │ └── token_response.rb
│ │ ├── oauth.rb
│ │ ├── orm/
│ │ │ ├── active_record/
│ │ │ │ ├── access_grant.rb
│ │ │ │ ├── access_token.rb
│ │ │ │ ├── application.rb
│ │ │ │ ├── mixins/
│ │ │ │ │ ├── access_grant.rb
│ │ │ │ │ ├── access_token.rb
│ │ │ │ │ └── application.rb
│ │ │ │ ├── redirect_uri_validator.rb
│ │ │ │ └── stale_records_cleaner.rb
│ │ │ └── active_record.rb
│ │ ├── rails/
│ │ │ ├── helpers.rb
│ │ │ ├── routes/
│ │ │ │ ├── abstract_router.rb
│ │ │ │ ├── mapper.rb
│ │ │ │ ├── mapping.rb
│ │ │ │ └── registry.rb
│ │ │ └── routes.rb
│ │ ├── rake/
│ │ │ ├── db.rake
│ │ │ └── setup.rake
│ │ ├── rake.rb
│ │ ├── request/
│ │ │ ├── authorization_code.rb
│ │ │ ├── client_credentials.rb
│ │ │ ├── code.rb
│ │ │ ├── password.rb
│ │ │ ├── refresh_token.rb
│ │ │ ├── strategy.rb
│ │ │ └── token.rb
│ │ ├── request.rb
│ │ ├── revocable_tokens/
│ │ │ ├── revocable_access_token.rb
│ │ │ └── revocable_refresh_token.rb
│ │ ├── secret_storing/
│ │ │ ├── base.rb
│ │ │ ├── bcrypt.rb
│ │ │ ├── plain.rb
│ │ │ └── sha256_hash.rb
│ │ ├── server.rb
│ │ ├── stale_records_cleaner.rb
│ │ ├── validations.rb
│ │ └── version.rb
│ ├── doorkeeper.rb
│ └── generators/
│ └── doorkeeper/
│ ├── application_owner_generator.rb
│ ├── confidential_applications_generator.rb
│ ├── enable_polymorphic_resource_owner_generator.rb
│ ├── install_generator.rb
│ ├── migration_generator.rb
│ ├── pkce_generator.rb
│ ├── previous_refresh_token_generator.rb
│ ├── remove_applications_secret_not_null_constraint_generator.rb
│ ├── templates/
│ │ ├── README
│ │ ├── add_confidential_to_applications.rb.erb
│ │ ├── add_owner_to_application_migration.rb.erb
│ │ ├── add_previous_refresh_token_to_access_tokens.rb.erb
│ │ ├── enable_pkce_migration.rb.erb
│ │ ├── enable_polymorphic_resource_owner_migration.rb.erb
│ │ ├── initializer.rb
│ │ ├── migration.rb.erb
│ │ └── remove_applications_secret_not_null_constraint.rb.erb
│ └── views_generator.rb
└── spec/
├── controllers/
│ ├── application_controller_spec.rb
│ ├── application_metal_controller_spec.rb
│ ├── applications_controller_spec.rb
│ ├── authorizations_controller_spec.rb
│ ├── protected_resources_controller_spec.rb
│ ├── token_info_controller_spec.rb
│ └── tokens_controller_spec.rb
├── doorkeeper/
│ ├── redirect_uri_validator_spec.rb
│ ├── server_spec.rb
│ ├── stale_records_cleaner_spec.rb
│ └── version_spec.rb
├── dummy/
│ ├── Rakefile
│ ├── app/
│ │ ├── assets/
│ │ │ └── config/
│ │ │ └── manifest.js
│ │ ├── controllers/
│ │ │ ├── application_controller.rb
│ │ │ ├── custom_authorizations_controller.rb
│ │ │ ├── full_protected_resources_controller.rb
│ │ │ ├── home_controller.rb
│ │ │ ├── metal_controller.rb
│ │ │ └── semi_protected_resources_controller.rb
│ │ ├── helpers/
│ │ │ └── application_helper.rb
│ │ ├── models/
│ │ │ └── user.rb
│ │ └── views/
│ │ ├── home/
│ │ │ └── index.html.erb
│ │ └── layouts/
│ │ └── application.html.erb
│ ├── config/
│ │ ├── application.rb
│ │ ├── boot.rb
│ │ ├── database.yml
│ │ ├── environment.rb
│ │ ├── environments/
│ │ │ ├── development.rb
│ │ │ ├── production.rb
│ │ │ └── test.rb
│ │ ├── initializers/
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── doorkeeper.rb
│ │ │ ├── secret_token.rb
│ │ │ ├── session_store.rb
│ │ │ └── wrap_parameters.rb
│ │ ├── locales/
│ │ │ └── doorkeeper.en.yml
│ │ └── routes.rb
│ ├── config.ru
│ ├── db/
│ │ ├── migrate/
│ │ │ ├── 20111122132257_create_users.rb
│ │ │ ├── 20120312140401_add_password_to_users.rb
│ │ │ ├── 20151223192035_create_doorkeeper_tables.rb
│ │ │ ├── 20151223200000_add_owner_to_application.rb
│ │ │ ├── 20160320211015_add_previous_refresh_token_to_access_tokens.rb
│ │ │ ├── 20170822064514_enable_pkce.rb
│ │ │ ├── 20180210183654_add_confidential_to_applications.rb
│ │ │ └── 20230205064514_add_custom_attributes.rb
│ │ └── schema.rb
│ ├── public/
│ │ ├── 404.html
│ │ ├── 422.html
│ │ └── 500.html
│ └── script/
│ └── rails
├── factories.rb
├── generators/
│ ├── application_owner_generator_spec.rb
│ ├── confidential_applications_generator_spec.rb
│ ├── enable_polymorphic_resource_owner_generator_spec.rb
│ ├── install_generator_spec.rb
│ ├── migration_generator_spec.rb
│ ├── pkce_generator_spec.rb
│ ├── previous_refresh_token_generator_spec.rb
│ ├── remove_applications_secret_not_null_constraint_generator_spec.rb
│ ├── templates/
│ │ └── routes.rb
│ └── views_generator_spec.rb
├── grape/
│ └── grape_integration_spec.rb
├── helpers/
│ └── doorkeeper/
│ └── dashboard_helper_spec.rb
├── lib/
│ ├── config_spec.rb
│ ├── doorkeeper/
│ │ └── orm/
│ │ └── active_record_spec.rb
│ ├── doorkeeper_spec.rb
│ ├── grant_flow/
│ │ └── flow_spec.rb
│ ├── grant_flow_spec.rb
│ ├── models/
│ │ ├── concerns/
│ │ │ └── write_to_primary_spec.rb
│ │ ├── expirable_spec.rb
│ │ ├── reusable_spec.rb
│ │ ├── revocable_spec.rb
│ │ ├── scopes_spec.rb
│ │ └── secret_storable_spec.rb
│ ├── oauth/
│ │ ├── authorization/
│ │ │ ├── code_spec.rb
│ │ │ └── uri_builder_spec.rb
│ │ ├── authorization_code_request_spec.rb
│ │ ├── base_request_spec.rb
│ │ ├── base_response_spec.rb
│ │ ├── client/
│ │ │ └── credentials_spec.rb
│ │ ├── client_credentials/
│ │ │ ├── creator_spec.rb
│ │ │ ├── issuer_spec.rb
│ │ │ └── validation_spec.rb
│ │ ├── client_credentials_integration_spec.rb
│ │ ├── client_credentials_request_spec.rb
│ │ ├── client_spec.rb
│ │ ├── code_request_spec.rb
│ │ ├── code_response_spec.rb
│ │ ├── error_response_spec.rb
│ │ ├── error_spec.rb
│ │ ├── forbidden_token_response_spec.rb
│ │ ├── helpers/
│ │ │ ├── scope_checker_spec.rb
│ │ │ ├── unique_token_spec.rb
│ │ │ └── uri_checker_spec.rb
│ │ ├── invalid_request_response_spec.rb
│ │ ├── invalid_token_response_spec.rb
│ │ ├── password_access_token_request_spec.rb
│ │ ├── pre_authorization_spec.rb
│ │ ├── refresh_token_request_spec.rb
│ │ ├── scopes_spec.rb
│ │ ├── token_request_spec.rb
│ │ ├── token_response_spec.rb
│ │ └── token_spec.rb
│ ├── option_spec.rb
│ ├── request/
│ │ └── strategy_spec.rb
│ └── secret_storing/
│ ├── base_spec.rb
│ ├── bcrypt_spec.rb
│ ├── plain_spec.rb
│ └── sha256_hash_spec.rb
├── models/
│ └── doorkeeper/
│ ├── access_grant_spec.rb
│ ├── access_token_spec.rb
│ └── application_spec.rb
├── requests/
│ ├── applications/
│ │ ├── applications_request_spec.rb
│ │ └── authorized_applications_spec.rb
│ ├── endpoints/
│ │ ├── authorization_spec.rb
│ │ └── token_spec.rb
│ ├── flows/
│ │ ├── authorization_code_errors_spec.rb
│ │ ├── authorization_code_spec.rb
│ │ ├── client_credentials_spec.rb
│ │ ├── implicit_grant_errors_spec.rb
│ │ ├── implicit_grant_spec.rb
│ │ ├── password_spec.rb
│ │ ├── refresh_token_spec.rb
│ │ ├── revoke_token_spec.rb
│ │ └── skip_authorization_spec.rb
│ └── protected_resources/
│ ├── metal_spec.rb
│ └── private_api_spec.rb
├── routing/
│ ├── custom_controller_routes_spec.rb
│ ├── default_routes_spec.rb
│ └── scoped_routes_spec.rb
├── spec_helper.rb
├── spec_helper_integration.rb
└── support/
├── dependencies/
│ └── factory_bot.rb
├── doorkeeper_rspec.rb
├── helpers/
│ ├── access_token_request_helper.rb
│ ├── authorization_request_helper.rb
│ ├── config_helper.rb
│ ├── model_helper.rb
│ ├── request_spec_helper.rb
│ └── url_helper.rb
├── orm/
│ └── active_record.rb
├── render_with_matcher.rb
└── shared/
├── controllers_shared_context.rb
├── hashing_shared_context.rb
└── models_shared_examples.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .codeclimate.yml
================================================
exclude_patterns:
- "lib/doorkeeper/config.rb"
- "spec/"
================================================
FILE: .coveralls.yml
================================================
service_name: travis-ci
================================================
FILE: .dockerignore
================================================
Gemfile.lock
================================================
FILE: .editorconfig
================================================
root = true
[*.{rb,json}]
indent_style = space
indent_size = 2
insert_final_newline = true
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
open_collective: doorkeeper-gem
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
### Steps to reproduce
What we need to do to see your problem or bug?
The more detailed the issue, the more likely that we will fix it ASAP.
Don't use GitHub issues for questions like "How can I do that?" —
use [StackOverflow](https://stackoverflow.com/questions/tagged/doorkeeper)
instead with the corresponding tag.
### Expected behavior
Tell us what should happen
### Actual behavior
Tell us what happens instead
### System configuration
You can help us to understand your problem if you will share some very
useful information about your project environment (don't forget to
remove any confidential data if it exists).
**Doorkeeper initializer**:
```ruby
# config/initializers/doorkeeper.rb
Doorkeeper.configure do
# ...
end
```
**Ruby version**: ``
**Gemfile.lock**:
Gemfile.lock content
```
Place your Gemfile.lock content here
```
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
### Summary
Provide a general description of the code changes in your pull
request... were there any bugs you had fixed? If so, mention them. If
these bugs have open GitHub issues, be sure to tag them here as well,
to keep the conversation linked together.
### Other Information
If there's anything else that's important and relevant to your pull
request, mention that information here. This could include
benchmarks, or other information.
If you are updating CHANGELOG.md file or are asked to update it by reviewers,
please add the changelog entry at the top of the file.
Thanks for contributing to Doorkeeper project!
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: bundler
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
================================================
FILE: .github/workflows/changelog.yml
================================================
name: "Changelog verifier"
on:
pull_request:
# The specific activity types are listed here to include "labeled" and "unlabeled"
# (which are not included by default for the "pull_request" trigger).
# This is needed to allow skipping enforcement of the changelog in PRs with specific labels,
# as defined in the (optional) "skipLabels" property.
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
# Enforces the update of a changelog file on every pull request
changelog:
runs-on: ubuntu-latest
steps:
- uses: dangoslen/changelog-enforcer@v3
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
name: >-
Ruby ${{ matrix.ruby }} (${{ matrix.gemfile }})
env:
CI: true
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
runs-on: ${{ matrix.os }}
if: |
!( contains(github.event.pull_request.title, '[ci skip]')
|| contains(github.event.pull_request.title, '[skip ci]'))
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
ruby:
- "3.1"
- "3.2"
- "3.3"
- "3.4"
gemfile:
- gemfiles/rails_7_0.gemfile
- gemfiles/rails_7_1.gemfile
- gemfiles/rails_7_2.gemfile
- gemfiles/rails_8_0.gemfile
exclude:
- ruby: 3.1
gemfile: gemfiles/rails_8_0.gemfile
steps:
- name: Repo checkout
uses: actions/checkout@v6.0.2
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
rubygems: latest
- name: Run tests
timeout-minutes: 10
run: bundle exec rake spec
rails_edge:
runs-on: ubuntu-latest
env:
BUNDLE_GEMFILE: gemfiles/rails_edge.gemfile
steps:
- uses: actions/checkout@v6.0.2
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- run: bundle exec rake spec || echo "Rails edge test is done."
ruby_edge:
strategy:
matrix:
gemfile:
- gemfiles/rails_7_0.gemfile
- gemfiles/rails_7_1.gemfile
- gemfiles/rails_7_2.gemfile
- gemfiles/rails_8_0.gemfile
runs-on: ubuntu-latest
env:
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v6.0.2
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "ruby-head"
bundler-cache: true
- run: bundle exec rake spec || echo "Ruby edge test is done."
================================================
FILE: .github/workflows/rubocop.yml
================================================
name: rubocop
on:
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
rubocop:
name: runner / rubocop
runs-on: ubuntu-latest
env:
BUNDLE_ONLY: rubocop
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ruby/setup-ruby@1a615958ad9d422dd932dc1d5823942ee002799f # v1.227.0
with:
ruby-version: '3.1'
bundler-cache: true
- uses: reviewdog/action-rubocop@b6d5e953a5fc0bf3ab65254e77730ea2174d6d6d # v2.22.0
with:
reporter: github-pr-review # Default is github-pr-check
skip_install: true
use_bundler: true
================================================
FILE: .gitignore
================================================
.bundle/
vendor/bundle/
.rbx
*.rbc
log/*.log
pkg/
spec/dummy/db/*.sqlite3
spec/dummy/log/*.log
spec/dummy/tmp/
spec/generators/tmp
Gemfile.lock
gemfiles/*.lock
.rvmrc
*.swp
.idea
/.yardoc/
/_yardoc/
/doc/
/rdoc/
coverage
*.gem
gemfiles/vendor
vendor/bundle/
================================================
FILE: .hound.yml
================================================
rubocop:
config_file: .rubocop.yml
version: 1.5.2
================================================
FILE: .rspec
================================================
--colour
================================================
FILE: .rubocop.yml
================================================
inherit_from: .rubocop_todo.yml
plugins:
- rubocop-capybara
- rubocop-factory_bot
- rubocop-performance
- rubocop-rails
- rubocop-rspec
- rubocop-rspec_rails
AllCops:
TargetRubyVersion: 3.1
Exclude:
- "spec/generators/tmp/**/*"
- "spec/dummy/db/*"
- "spec/dummy/config/*"
- "Dangerfile"
- "gemfiles/*.gemfile"
Layout/MultilineMethodCallIndentation:
EnforcedStyle: indented
Layout/TrailingEmptyLines:
Enabled: true
Layout/DotPosition:
EnforcedStyle: leading
Layout/LineLength:
Exclude:
- spec/**/*
Metrics/BlockLength:
Exclude:
- spec/**/*
- lib/doorkeeper/rake/*
- doorkeeper.gemspec
Metrics/MethodLength:
Exclude:
- spec/dummy/db/**/*
Style/CaseEquality:
Exclude:
- lib/doorkeeper/grant_flow/flow.rb
Style/StringLiterals:
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes
Style/FrozenStringLiteralComment:
Enabled: true
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: consistent_comma
Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: consistent_comma
Style/TrailingCommaInArguments:
EnforcedStyleForMultiline: consistent_comma
Style/SymbolArray:
MinSize: 3
Style/WordArray:
MinSize: 3
Style/ClassAndModuleChildren:
Enabled: false
Style/NumericPredicate:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Rails/DynamicFindBy:
Whitelist:
- find_by_sql
- find_by_plaintext_token
- find_by_fallback_token
Rails/HttpPositionalArguments:
Exclude:
- spec/grape/*
Rails/HttpStatus:
Enabled: false
Rails/RakeEnvironment:
Exclude:
- Rakefile
Rails/ReflectionClassName:
Exclude:
- "lib/doorkeeper/orm/active_record/mixins/access_grant.rb"
- "lib/doorkeeper/orm/active_record/mixins/access_token.rb"
- "lib/doorkeeper/orm/active_record/mixins/application.rb"
Rails/SkipsModelValidations:
Enabled: false
RSpec/BeforeAfterAll:
Exclude:
- "spec/routing/scoped_routes_spec.rb"
- "spec/routing/custom_controller_routes_spec.rb"
RSpec/ContextWording:
Exclude:
- "spec/support/shared/controllers_shared_context.rb"
RSpec/DescribeClass:
Enabled: false
RSpec/ExampleLength:
Enabled: false
RSpec/SpecFilePathFormat:
Enabled: false
RSpec/MultipleExpectations:
Enabled: false
RSpec/NestedGroups:
Enabled: false
RSpec/NoExpectationExample:
Enabled: true
Exclude:
- "spec/requests/**/*"
================================================
FILE: .rubocop_todo.yml
================================================
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2020-06-04 00:15:49 +0300 using RuboCop version 0.84.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 13
# Configuration parameters: IgnoredMethods.
Metrics/AbcSize:
Max: 33
# Offense count: 2
# Configuration parameters: CountComments, ExcludedMethods.
# ExcludedMethods: refine
Metrics/BlockLength:
Max: 85
# Offense count: 2
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 242
# Offense count: 2
# Configuration parameters: IgnoredMethods.
Metrics/CyclomaticComplexity:
Max: 10
# Offense count: 24
# Configuration parameters: CountComments, ExcludedMethods.
Metrics/MethodLength:
Max: 29
# Offense count: 1
# Configuration parameters: CountComments.
Metrics/ModuleLength:
Max: 209
# Offense count: 2
# Configuration parameters: IgnoredMethods.
Metrics/PerceivedComplexity:
Max: 11
# Offense count: 1
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
Naming/MemoizedInstanceVariableName:
Exclude:
- 'lib/doorkeeper/config.rb'
# Offense count: 10
RSpec/AnyInstance:
Exclude:
- 'spec/generators/previous_refresh_token_generator_spec.rb'
- 'spec/lib/oauth/authorization_code_request_spec.rb'
- 'spec/lib/oauth/client_credentials/creator_spec.rb'
- 'spec/lib/oauth/password_access_token_request_spec.rb'
- 'spec/lib/oauth/token_request_spec.rb'
- 'spec/requests/flows/authorization_code_spec.rb'
- 'spec/requests/flows/refresh_token_spec.rb'
# Offense count: 2
RSpec/ExpectInHook:
Exclude:
- 'spec/controllers/protected_resources_controller_spec.rb'
# Offense count: 300
# Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable:
Enabled: false
# Offense count: 22
RSpec/LeakyConstantDeclaration:
Exclude:
- 'spec/controllers/authorizations_controller_spec.rb'
- 'spec/controllers/protected_resources_controller_spec.rb'
- 'spec/lib/config_spec.rb'
- 'spec/lib/option_spec.rb'
- 'spec/models/doorkeeper/access_token_spec.rb'
# Offense count: 7
RSpec/MessageChain:
Exclude:
- 'spec/controllers/authorizations_controller_spec.rb'
- 'spec/controllers/tokens_controller_spec.rb'
# Offense count: 98
# Configuration parameters: .
# SupportedStyles: have_received, receive
RSpec/MessageSpies:
EnforcedStyle: receive
# Offense count: 39
RSpec/SubjectStub:
Exclude:
- 'spec/lib/models/expirable_spec.rb'
- 'spec/lib/models/reusable_spec.rb'
- 'spec/lib/models/revocable_spec.rb'
- 'spec/lib/oauth/base_request_spec.rb'
- 'spec/lib/oauth/client_credentials_request_spec.rb'
- 'spec/support/shared/models_shared_examples.rb'
# Offense count: 73
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
RSpec/VerifiedDoubles:
Enabled: false
# Offense count: 4
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Exclude:
- 'lib/doorkeeper/config.rb'
- 'lib/doorkeeper/helpers/controller.rb'
- 'lib/doorkeeper/oauth/client/credentials.rb'
- 'lib/doorkeeper/oauth/token.rb'
================================================
FILE: AGENTS.md
================================================
# Doorkeeper Codebase Guide for AI Coding Agents
This is the code base of the OAuth 2 provider for Ruby web applications.
## Architecture Overview
Doorkeeper is a Ruby gem which is a Rails engine. It provides a set of models, controllers, and views that can be
mounted into a Rails application to handle OAuth 2 authorization flows.
**Key principle**: all the changes should conform OAuth 2 published specifications such as RFC 6749, RFC 6819 and etc.
## Testing Commands
From within the root directory (preferred method):
```bash
bundle exec rake spec
```
## Code Conventions
### Changelog Updates
When fixing bugs or adding features:
- Add an entry to the top of `CHANGELOG.md`
- Format: `- [PR number] Brief description`
- See existing entries for style
### Code Style
- Run RuboCop: `bundle exec rubocop` (there's a project-wide `.rubocop.yml`)
## Documentation
- API docs use YARD/RDoc format
================================================
FILE: CHANGELOG.md
================================================
# Changelog
See https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions for
upgrade guides.
User-visible changes worth mentioning.
## main
- [#1781] Honor `handle_auth_errors :raise` in `AuthorizationsController#authorize_response`
- [#1795] Fix: detailed error 'insufficient_scope' in protected resources 403s
- [#1797] Fix `doorkeeper:db:cleanup` rake task failure on PostgreSQL
- [#1800] Set `@grant_type` in `ClientCredentialsRequest` and `RefreshTokenRequest` constructors so `request.grant_type` returns
the correct value in hooks like `before_successful_strategy_response`.
- [#1802] Fix `filter_parameters` not applied when `Doorkeeper.configure` is called inside to_prepare.
- [#1804] Use `ActiveSupport.on_load(:active_record)` in ORM hooks to prevent loading ActiveRecord models too early
- [#1806] Fix token revocation bypass for public clients (RFC 7009)
- [#1815] Expose `current_resource_owner` as a view helper in `Doorkeeper::ApplicationController`.
- [#1818] Fix token introspection returning `exp: 0` for non-expiring tokens.
- [#1784] Remove hardcoded colons from view templates, move punctuation to i18n translation strings.
**[IMPORTANT]**: if you have customized Doorkeeper views (`authorizations/new`, `authorizations/show`,
`applications/show`) or overridden the default `en.yml` translations, you may need to update them.
Colons are no longer hardcoded in the views — they are now part of the translation strings.
Update the [doorkeeper-i18n](https://github.com/doorkeeper-gem/doorkeeper-i18n) gem to get the
updated translations for all locales.
## 5.9.0
- [#1791] Add support for Rails read replicas with automatic role switching via `enable_multiple_database_roles` configuration option
- [#1792] Consider expires_in when clear expired tokens with StaleRecordsCleaner.
- [#1790] Fix race condition in refresh token revocation check by moving InvalidGrantReuse check inside the lock block
- [#1788] Fix regex for basic auth to be case-insensitive
- [#1775] Fix Applications Secret Not Null Constraint generator
- [#1779] Only lock previous access token model when creating a new token from its refresh token if revoke_previous_refresh_token_on_use is false
- [#1778] Ensure that token revocation is idempotent by checking that that token has not already been revoked before revoking.
## 5.8.2
- [#1755] Fix the error message for force_pkce
- [#1761] Memoize authentication failure
- [#1762] Allow missing client to trigger invalid client error when force_pkce is enabled
- [#1767] Make sure error handling happens on a controller level opposed to action level to account for the controller being extended
## 5.8.1
- [#1752] Bump the range of supported Ruby and Rails versions
- [#1747] Fix unknown pkce method error when configured
- [#1744] Allow for expired refresh tokens to be revoked
- [#1754] Fix refresh tokens with dynamic scopes
## 5.8.0
- [#1739] Add support for dynamic scopes
- [#1715] Fix token introspection invalid request reason
- [#1714] Fix `Doorkeeper::AccessToken.find_or_create_for` with empty scopes which raises NoMethodError
- [#1712] Add `Pragma: no-cache` to token response
- [#1726] Refactor token introspection class.
- [#1727] Allow to set null secret value for Applications if they are public.
- [#1735] Add `pkce_code_challenge_methods` config option.
## 5.7.1
- [#1705] Add `force_pkce` option that requires non-confidential clients to use PKCE when requesting an access_token using an authorization code
## 5.7.0
- [#1696] Add missing `#issued_token` method to `OAuth::TokenResponse`
- [#1697] Allow a TokenResponse body to be customized (memoize response body).
- [#1702] Fix bugs for error response in the form_post and error view
- [#1660] Custom access token attributes are now considered when finding matching tokens (fixes #1665).
Introduce `revoke_previous_client_credentials_token` configuration option.
## 5.6.9
- [#1691] Make new Doorkeeper errors backward compatible with older extensions.
## 5.6.8
- [#1680] Fix handle_auth_errors :raise NotImplementedError
## 5.6.7
- [#1662] Specify uri_redirect validation class explicitly.
- [#1652] Add custom attributes support to token generator.
- [#1667] Pass `client` instead of `grant.application` to `find_or_create_access_token`.
- [#1673] Honor `custom_access_token_attributes` in client credentials grant flow.
- [#1676] Improve AuthorizationsController error response handling
- [#1677] Fix URIHelper.valid_for_authorization? breaking for non url URIs.
## 5.6.6
- [#1644] Update HTTP headers.
- [#1646] Block public clients automatic authorization skip.
- [#1648] Add custom token attributes to Refresh Token Request.
- [#1649] Fixed custom_access_token_attributes related errors.
## 5.6.5
- [#1602] Allow custom data to be stored inside access grants/tokens.
- [#1634] Code refactoring for custom token attributes.
- [#1639] Add grant type validation to avoid Internal Server Error for DELETE /oauth/authorize endpoint.
## 5.6.4
- [#1633] Apply ORM configuration in #to_prepare block to avoid autoloading errors.
## 5.6.3
- [#1622] Drop support for Rubies 2.5 and 2.6
- [#1605] Fix URI validation for Ruby 3.2+.
- [#1625] Exclude endless access tokens from `StaleRecordsCleaner`.
- [#1626] Remove deprecated `active_record_options` config option.
- [#1631] Fix regression with redirect behavior after token lookup optimizations (redirect to app URI when found).
- [#1630] Special case unique index creation for refresh_token on SQL Server.
- [#1627] Lazy evaluate Doorkeeper config when loading files and executing initializers.
## 5.6.2
- [#1604] Fix fetching of the application when custom application_class defined.
## 5.6.1
- [#1593] Add support for Trilogy ActiveRecord adapter.
- [#1597] Add optional support to use the url path for the native authorization code flow. Ports forward [#1143] from 4.4.3
- [#1599] Remove unnecessarily re-fetch of application object when creating an access token.
## 5.6.0
- [#1581] Consider `token_type_hint` when searching for access token in TokensController to avoid extra database calls.
## 5.6.0.rc2
- [#1558] Fixed bug: able to obtain a token with default scopes even if they are not present in the
application scopes when using client credentials.
- [#1567] Only filter `code` parameter if authorization_code grant flow is enabled.
## 5.6.0.rc1
- [#1551] Change lazy loading for ORM to be Ruby standard autoload.
- [#1552] Remove duplicate IDs on Auth form to improve accessibility.
- [#1542] Improve performance of `Doorkeeper::AccessToken#matching_token_for` using database specific SQL time math.
**[IMPORTANT]**: API of the `Doorkeeper::AccessToken#matching_token_for` method has changed and now it returns
only **active** access tokens (previously they were just not revoked). Please remember that the idea of the
`reuse_access_token` option is to check for existing _active_ token (see configuration option description).
## 5.5.4
- [#1535] Revert changes introduced in #1528 to allow query params in `redirect_uri` as per the spec.
## 5.5.3
- [#1528] Don't allow extra query params in redirect_uri.
- [#1525] I18n source for forbidden token error is now `doorkeeper.errors.messages.forbidden_token.missing_scope`.
- [#1531] Disable `strict-loading` for Doorkeeper models by default.
- [#1532] Add support for Rails 7.
## 5.5.2
- [#1502] Drop support for Ruby 2.4 because of EOL.
- [#1504] Updated the url fragment in the comment for code documentation.
- [#1512] Fix form behavior when response mode is form_post.
- [#1511] Fix that authorization code is returned by fragment if response_mode is fragment.
## 5.5.1
- [#1496] Revoke `old_refresh_token` if `previous_refresh_token` is present.
- [#1495] Fix `respond_to` undefined in API-only mode
- [#1488] Verify client authentication for Resource Owner Password Grant when
`config.skip_client_authentication_for_password_grant` is set and the client credentials
are sent in a HTTP Basic auth header.
## 5.5.0
- [#1482] Simplify `TokenInfoController` to be overridable (extract response rendering).
- [#1478] Fix ownership association and Rake tasks when custom models configured.
- [#1477] Respect `ActiveRecord::Base.pluralize_table_names` for Doorkeeper table names.
## 5.5.0.rc2
- [#1473] Enable `Applications` and `AuthorizedApplications` controllers in API mode.
**[IMPORTANT]** you can still skip these controllers using `skip_controllers` in
`use_doorkeeper` inside `routes.rb`. Please do it in case you don't need them.
- [#1472] Fix `establish_connection` configuration for custom defined models.
- [#1471] Add support for Ruby 3.0.
- [#1469] Check if `redirect_uri` exists.
- [#1465] Memoize nil doorkeeper_token.
- [#1459] Use built-in Ruby option to remove padding in PKCE code challenge value.
- [#1457] Make owner_id a bigint for newly-generated owner migrations
- [#1452] Empty previous_refresh_token only if present.
- [#1440] Validate empty host in redirect_uri.
- [#1438] Add form post response mode.
- [#1458] Make `config.skip_client_authentication_for_password_grant` a long term configuration option.
## 5.5.0.rc1
- [#1435] Make error response not redirectable when client is unauthorized
- [#1426] Ensure ActiveRecord callbacks are executed on token revocation.
- [#1407] Remove redundant and complex to support helpers froms tests (`should_have_json`, etc).
- [#1416] Don't add introspection route if token introspection completely disabled.
- [#1410] Properly memoize `current_resource_owner` value (consider `nil` and `false` values).
- [#1415] Ignore PKCE params for non-PKCE grants.
- [#1418] Add ability to register custom OAuth Grant Flows.
- [#1420] Require client authentication for Resource Owner Password Grant as stated in OAuth RFC.
**[IMPORTANT]** you need to create a new OAuth client (`Doorkeeper::Application`) if you didn't
have it before and use client credentials in HTTP Basic auth if you previously used this grant
flow without client authentication. To opt out of this you could set the
`skip_client_authentication_for_password_grant` configuration option to `true`, but note that
this is in violation of the OAuth spec and represents a security risk.
All the users of your provider application now need to include client credentials when they use
this grant flow.
- [#1421] Add Resource Owner instance to authorization hook context for `custom_access_token_expires_in`
configuration option to allow resource owner based Access Tokens TTL.
## 5.4.0
- [#1404] Make `Doorkeeper::Application#read_attribute_for_serialization` public.
## 5.4.0.rc2
- [#1371] Add `#as_json` method and attributes serialization restriction for Application model.
Fixes information disclosure vulnerability (CVE-2020-10187).
**[IMPORTANT]** you need to re-implement `#as_json` method for Doorkeeper Application model
if you previously used `#to_json` serialization with custom options or attributes or rely on
JSON response from /oauth/applications.json or /oauth/authorized_applications.json. This change
is a breaking change which restricts serialized attributes to a very small set of columns.
- [#1395] Fix `NameError: uninitialized constant Doorkeeper::AccessToken` for Rake tasks.
- [#1397] Add `as: :doorkeeper_application` on Doorkeeper application form in order to support
custom configured application model.
- [#1400] Correctly yield the application instance to `allow_grant_flow_for_client?` config
option (fixes #1398).
- [#1402] Handle trying authorization with client credentials.
## 5.4.0.rc1
- [#1366] Sets expiry of token generated using `refresh_token` to that of original token. (Fixes #1364)
- [#1354] Add `authorize_resource_owner_for_client` option to authorize the calling user to access an application.
- [#1355] Allow to enable polymorphic Resource Owner association for Access Token & Grant
models (`use_polymorphic_resource_owner` configuration option).
**[IMPORTANT]** Review your custom patches or extensions for Doorkeeper internals if you
have such - since now Doorkeeper passes Resource Owner instance to every objects and not
just it's ID. See PR description for details.
- [#1356] Remove duplicated scopes from Access Tokens and Grants on attribute assignment.
- [#1357] Fix `Doorkeeper::OAuth::PreAuthorization#as_json` method causing
`Stack level too deep` error with AMS (fix #1312).
- [#1358] Deprecate `active_record_options` configuration option.
- [#1359] Refactor Doorkeeper configuration options DSL to make it easy to reuse it
in external extensions.
- [#1360] Increase `matching_token_for` lookup size to 10 000 and make it configurable.
- [#1371] Fix controllers to use valid classes in case Doorkeeper has custom models configured.
- [#1370] Fix revocation response for invalid token and unauthorized requests to conform with RFC 7009 (fixes #1362).
**[IMPORTANT]** now fully according to RFC 7009 nobody can do a revocation request without `client_id`
(for public clients) and `client_secret` (for private clients). Please update your apps to include that
info in the revocation request payload.
- [#1373] Make Doorkeeper routes mapper reusable in extensions.
- [#1374] Revoke and issue client credentials token in a transaction with a row lock.
- [#1384] Add context object with auth/pre_auth and issued_token for authorization hooks.
- [#1387] Add `AccessToken#create_for` and use in `RefreshTokenRequest`.
- [#1392] Fix `enable_polymorphic_resource_owner` migration template to have proper index name.
- [#1393] Improve Applications #show page with more informative data on client secret and scopes.
- [#1394] Use Ruby `autoload` feature to load Doorkeeper files.
## 5.3.3
- [#1404] Backport: Make `Doorkeeper::Application#read_attribute_for_serialization` public.
## 5.3.2
- [#1371] Backport: add `#as_json` method and attributes serialization restriction for Application model.
Fixes information disclosure vulnerability (CVE-2020-10187).
## 5.3.1
- [#1360] Backport: Increase `matching_token_for` batch lookup size to 10 000 and make it configurable.
## 5.3.0
- [#1339] Validate Resource Owner in `PasswordAccessTokenRequest` against `nil` and `false` values.
- [#1341] Fix `refresh_token_revoked_on_use` with `hash_token_secrets` enabled.
- [#1343] Fix ruby 2.7 kwargs warning in InvalidTokenResponse.
- [#1345] Allow to set custom classes for Doorkeeper models, extract reusable AR mixins.
- [#1346] Refactor `Doorkeeper::Application#to_json` into convenient `#as_json` (fix #1344).
- [#1349] Fix `Doorkeeper::Application` AR associations using an incorrect foreign key name when using a custom class.
- [#1318] Make existing token revocation for client credentials optional and disable it by default.
**[IMPORTANT]** This is a change compared to the behaviour of version 5.2.
If you were relying on access tokens being revoked once the same client
requested a new access token, reenable it with `revoke_previous_client_credentials_token` in Doorkeeper
initialization file.
## 5.2.6
- [#1404] Backport: Make `Doorkeeper::Application#read_attribute_for_serialization` public.
## 5.2.5
- [#1371] Backport: add `#as_json` method and attributes serialization restriction for Application model.
Fixes information disclosure vulnerability (CVE-2020-10187).
## 5.2.4
- [#1360] Backport: Increase `matching_token_for` batch lookup size to 10 000 and make it configurable.
## 5.2.3
- [#1334] Remove `application_secret` flash helper and `redirect_to` keyword.
- [#1331] Move redirect_uri_validator to where it is used (`Application` model).
- [#1326] Move response_type check in pre_authorization to a method to be easily to override.
- [#1329] Fix `find_in_batches` order warning.
## 5.2.2
- [#1320] Call configured `authenticate_resource_owner` method once per request.
- [#1315] Allow generation of new secret with `Doorkeeper::Application#renew_secret`.
- [#1309] Allow `Doorkeeper::Application#to_json` to work without arguments.
## 5.2.1
- [#1308] Fix flash types for `api_only` mode (no flashes for `ActionController::API`).
- [#1306] Fix interpolation of `missing_param` I18n.
## 5.2.0
- [#1305] Make `Doorkeeper::ApplicationController` to inherit from `ActionController::API` in cases
when `api_mode` enabled (fixes #1302).
## 5.2.0.rc3
- [#1298] Slice strong params so doesn't error with Rails forms.
- [#1300] Limiting access to attributes of pre_authorization.
- [#1296] Adding client_id to strong parameters.
**[IMPORTANT]** `Doorkeeper::Server#client_via_uid` was removed.
- [#1293] Move ar specific redirect uri validator to ar orm directory.
- [#1288] Allow to pass attributes to the `Doorkeeper::OAuth::PreAuthorization#as_json` method to customize
the PreAuthorization response.
- [#1286] Add ability to customize grant flows per application (OAuth client) (#1245 , #1207)
- [#1283] Allow to customize base class for `Doorkeeper::ApplicationMetalController` (new configuration
option called `base_metal_controller` (fix #1273).
- [#1277] Prevent requested scope be empty on authorization request, handle and add description for invalid request.
## 5.2.0.rc2
- [#1270] Find matching tokens in batches for `reuse_access_token` option (fix #1193).
- [#1271] Reintroduce existing token revocation for client credentials.
**[IMPORTANT]** If you rely on being able to fetch multiple access tokens from the same
client using client credentials flow, you should skip to version 5.3, where this behaviour
is deactivated by default.
- [#1269] Update initializer template documentation.
- [#1266] Use strong parameters within pre-authorization.
- [#1264] Add :before_successful_authorization and :after_successful_authorization hooks in TokensController
- [#1263] Response properly when introspection fails and fix configurations's user guide.
## 5.2.0.rc1
- [#1260], [#1262] Improve Token Introspection configuration option (access to tokens, client).
- [#1257] Add constraint configuration when using client authentication on introspection endpoint.
- [#1252] Returning `unauthorized` when the revocation of the token should not be performed due to wrong permissions.
- [#1249] Specify case sensitive uniqueness to remove Rails 6 deprecation message
- [#1248] Display the Application Secret in HTML after creating a new application even when `hash_application_secrets` is used.
- [#1248] Return the unhashed Application Secret in the JSON response after creating new application even when `hash_application_secrets` is used.
- [#1238] Better support for native app with support for custom scheme and localhost redirection.
## 5.1.2
- [#1404] Backport: Make `Doorkeeper::Application#read_attribute_for_serialization` public.
## 5.1.1
- [#1371] Backport: add `#as_json` method and attributes serialization restriction for Application model.
Fixes information disclosure vulnerability (CVE-2020-10187).
## 5.1.0
- [#1243] Add nil check operator in token checking at token introspection.
- [#1241] Explaining foreign key options for resource owner in a single place
- [#1237] Allow to set blank redirect URI if Doorkeeper configured to use redirect URI-less grant flows.
- [#1234] Fix `StaleRecordsCleaner` to properly work with big amount of records.
- [#1228] Allow to explicitly set non-expiring tokens in `custom_access_token_expires_in` configuration
option using `Float::INFINITY` return value.
- [#1224] Do not try to store token if not found by fallback hashing strategy.
- [#1223] Update Hound/Rubocop rules, correct Doorkeeper codebase to follow style-guides.
- [#1220] Drop Rails 4.2 & Ruby < 2.4 support.
## 5.1.0.rc2
- [#1208] Unify hashing implementation into secret storing strategies
**[IMPORTANT]** If you have been using the master branch of doorkeeper with bcrypt in your Gemfile.lock,
your application secrets have been hashed using BCrypt. To restore this behavior, use the initializer option
`hash_application_secrets using: 'Doorkeeper::SecretStoring::BCrypt`.
- [#1216] Add nil check to `expires_at` method.
- [#1215] Fix deprecates for Rails 6.
- [#1214] Scopes field accepts array.
- [#1209] Fix tokens validation for Token Introspection request.
- [#1202] Use correct HTTP status codes for error responses.
**[IMPORTANT]**: this change might break your application if you were relying on the previous
401 status codes, this is now a 400 by default, or a 401 for `invalid_client` and `invalid_token` errors.
- [#1201] Fix custom TTL block `client` parameter to always be an `Doorkeeper::Application` instance.
**[IMPORTANT]**: those who defined `custom_access_token_expires_in` configuration option need to check
their block implementation: if you are using `oauth_client.application` to get `Doorkeeper::Application`
instance, then you need to replace it with just `oauth_client`.
- [#1200] Increase default Doorkeeper access token value complexity (`urlsafe_base64` instead of just `hex`)
matching RFC6749/RFC6750.
**[IMPORTANT]**: this change have possible side-effects in case you have custom database constraints for
access token value, application secrets, refresh tokens or you patched Doorkeeper models and introduced
token value validations, or you are using database with case-insensitive WHERE clause like MySQL
(you can face some collisions). Before this change access token value matched `[a-f0-9]` regex, and now
it matches `[a-zA-Z0-9\-_]`. In case you have such restrictions and your don't use custom token generator
please change configuration option `default_generator_method` to `:hex`.
- [#1195] Allow to customize Token Introspection response (fixes #1194).
- [#1189] Option to set `token_reuse_limit`.
- [#1191] Try to load bcrypt for hashing of application secrets, but add fallback.
## 5.1.0.rc1
- [#1188] Use `params` instead of `request.POST` in tokens controller (fixes #1183).
- [#1182] Fix loopback IP redirect URIs to conform with RFC8252, p. 7.3 (fixes #1170).
- [#1179] Authorization Code Grant Flow without client id returns invalid_client error.
- [#1177] Allow to limit `scopes` for certain `grant_types`
- [#1176] Fix test factory support for `factory_bot_rails`
- [#1175] Internal refactor: use `scopes_string` inside `scopes`.
- [#1168] Allow optional hashing of tokens and secrets.
- [#1164] Fix error when `root_path` is not defined.
- [#1162] Fix `enforce_content_type` for requests without body.
## 5.0.3
- [#1371] Backport: add `#as_json` method and attributes serialization restriction for Application model.
Fixes information disclosure vulnerability (CVE-2020-10187).
## 5.0.2
- [#1158] Fix initializer template: change `handle_auth_errors` option
- [#1157] Remove redundant index from migration template.
## 5.0.1
- [#1154] Refactor `StaleRecordsCleaner` to be ORM agnostic.
- [#1152] Fix migration template: change resource owner data type from integer to Rails generic `references`
- [#1151] Fix Refresh Token strategy: add proper validation of client credentials both for Public & Private clients.
- [#1149] Fix for `URIChecker#valid_for_authorization?` false negative when query is blank, but `?` present.
- [#1140] Allow rendering custom errors from exceptions (issue #844). Originally opened as [#944].
- [#1138] Revert regression bug (check for token expiration in Authorizations controller so authorization
triggers every time)
## 5.0.0
- [#1127] Change the token_type initials of the Banner Token to uppercase to comply with the RFC6750 specification.
## 5.0.0.rc2
- [#1122] Fix AuthorizationsController#new error response to be in JSON format
- [#1119] Fix token revocation for OAuth apps using "implicit" grant flow
- [#1116] `AccessGrant`s will now be revoked along with `AccessToken`s when
hitting the `AuthorizedApplicationController#destroy` route.
- [#1114] Make token info endpoint's attributes consistent with token creation
- [#1108] Simple formatting of callback URLs when listing oauth applications
- [#1106] Restrict access to AdminController with 'Forbidden 403' if admin_authenticator is not
configured by developers.
## 5.0.0.rc1
- [#1103] Allow customizing use_refresh_token
- [#1089] Removed enable_pkce_without_secret configuration option
- [#1102] Expiration time based on scopes
- [#1099] All the configuration variables in `Doorkeeper.configuration` now
always return a non-nil value (`true` or `false`)
- [#1099] ORM / Query optimization: Do not revoke the refresh token if it is not enabled
in `doorkeeper.rb`
- [#996] Expiration Time Base On Grant Type
- [#997] Allow PKCE authorization_code flow as specified in RFC7636
- [#907] Fix lookup for matching tokens in certain edge-cases
- [#992] Add API option to use Doorkeeper without management views for API only
Rails applications (`api_only`)
- [#1045] Validate redirect_uri as the native URI when making authorization code requests
- [#1048] Remove deprecated `Doorkeeper#configured?`, `Doorkeeper#database_installed?`, and
`Doorkeeper#installed?` method
- [#1031] Allow public clients to authenticate without `client_secret`. Define an app as
either public or private/confidential
**[IMPORTANT]**: all the applications (clients) now are considered as private by default.
You need to manually change `confidential` column to `false` if you are using public clients,
in other case your mobile (or other) applications will not be able to authorize.
See [#1142](https://github.com/doorkeeper-gem/doorkeeper/issues/1142) for more details.
- [#1010] Add configuration to enforce configured scopes (`default_scopes` and
`optional_scopes`) for applications
- [#1060] Ensure that the native redirect_uri parameter matches with redirect_uri of the client
- [#1064] Add :before_successful_authorization and :after_successful_authorization hooks
- [#1069] Upgrade Bootstrap to 4 for Admin
- [#1068] Add rake task to cleanup databases that can become large over time
- [#1072] AuthorizationsController: Memoize strategy.authorize_response result to enable
subclasses to use the response object.
- [#1075] Call `before_successful_authorization` and `after_successful_authorization` hooks
on `create` action as well as `new`
- [#1082] Fix #916: remember routes mapping and use it required places (fix error with
customized Token Info route).
- [#1086, #1088] Fix bug with receiving default scopes in the token even if they are
not present in the application scopes (use scopes intersection).
- [#1076] Add config to enforce content type to application/x-www-form-urlencoded
- Fix bug with `force_ssl_in_redirect_uri` when it breaks existing applications with an
SSL redirect_uri.
## 4.4.3
- [#1143] Adds a config option `opt_out_native_route_change` to opt out of the breaking api
changed introduced in https://github.com/doorkeeper-gem/doorkeeper/pull/1003
## 4.4.2
- [#1130] Backport fix for native redirect_uri from 5.x.
## 4.4.1
- [#1127] Backport token type to comply with the RFC6750 specification.
- [#1125] Backport Quote surround I18n yes/no keys
## 4.4.0
- [#1120] Backport security fix from 5.x for token revocation when using public clients
**[IMPORTANT]**: all the applications (clients) now are considered as private by default.
You need to manually change `confidential` column to `false` if you are using public clients,
in other case your mobile (or other) applications will not be able to authorize.
See [#1142](https://github.com/doorkeeper-gem/doorkeeper/issues/1142) for more details.
## 4.3.2
- [#1053] Support authorizing with query params in the request `redirect_uri` if explicitly present in app's `Application#redirect_uri`
## 4.3.1
- Remove `BaseRecord` and introduce additional concern for ordering methods to fix
braking changes for Doorkeeper models.
- [#1032] Refactor BaseRequest callbacks into configurable lambdas
- [#1040] Clear mixins from ActiveRecord DSL and save only overridable API. It
allows to use this mixins in Doorkeeper ORM extensions with minimum code boilerplate.
## 4.3.0
- [#976] Fix to invalidate the second redirect URI when the first URI is the native URI
- [#1035] Allow `Application#redirect_uri=` to handle array of URIs.
- [#1036] Allow to forbid Application redirect URI's with specific rules.
- [#1029] Deprecate `order_method` and introduce `ordered_by`. Sort applications
by `created_at` in index action.
- [#1033] Allow Doorkeeper configuration option #force_ssl_in_redirect_uri to be a callable object.
- Fix Grape integration & add specs for it
- [#913] Deferred ORM (ActiveRecord) models loading
- [#943] Fix Access Token token generation when certain errors occur in custom token generators
- [#1026] Implement RFC7662 - OAuth 2.0 Token Introspection
- [#985] Generate valid migration files for Rails >= 5
- [#972] Replace Struct subclassing with block-form initialization
- [#1003] Use URL query param to pass through native redirect auth code so automated apps can find it.
**[IMPORTANT]**: Previously authorization code response route was `/oauth/authorize/`,
now it is `oauth/authorize/native?code=` (in order to help applications to automatically find the code value).
- [#868] `Scopes#&` and `Scopes#+` now take an array or any other enumerable
object.
- [#1019] Remove translation not in use: `invalid_resource_owner`.
- Use Ruby 2 hash style syntax (min required Ruby version = 2.1)
- [#948] Make Scopes.<=> work with any "other" value.
- [#974] Redirect URI is checked without query params within AuthorizationCodeRequest.
- [#1004] More explicit help text for `native_redirect_uri`.
- [#1023] Update Ruby versions and test against 2.5.0 on Travis CI.
- [#1024] Migrate from FactoryGirl to FactoryBot.
- [#1025] Improve documentation for adding foreign keys
- [#1028] Make it possible to have composite strategy names.
## 4.2.6
- [#970] Escape certain attributes in authorization forms.
## 4.2.5
- [#936] Deprecate `Doorkeeper#configured?`, `Doorkeeper#database_installed?`, and
`Doorkeeper#installed?`
- [#909] Add `InvalidTokenResponse#reason` reader method to allow read the kind
of invalid token error.
- [#928] Test against more recent Ruby versions
- Small refactorings within the codebase
- [#921] Switch to Appraisal, and test against Rails master
- [#892] Add minimum Ruby version requirement
## 4.2.0
- Security fix: Address CVE-2016-6582, implement token revocation according to
spec (tokens might not be revoked if client follows the spec).
- [#873] Add hooks to Doorkeeper::ApplicationMetalController
- [#871] Allow downstream users to better utilize doorkeeper spec factories by
eliminating name conflict on `:user` factory.
## 4.1.0
- [#845] Allow customising the `Doorkeeper::ApplicationController` base
controller
## 4.0.0
- [#834] Fix AssetNotPrecompiled error with Sprockets 4
- [#843] Revert "Fix validation error messages"
- [#847] Specify Null option to timestamps
## 4.0.0.rc4
- [#777] Add support for public client in password grant flow
- [#823] Make configuration and specs ORM independent
- [#745] Add created_at timestamp to token generation options
- [#838] Drop `Application#scopes` generator and warning, introduced for
upgrading doorkeeper from v2 to v3.
- [#801] Fix Rails 5 warning messages
- Test against Rails 5 RC1
## 4.0.0.rc3
- [#769] Revoke refresh token on access token use. To make use of the new config
add `previous_refresh_token` column to `oauth_access_tokens`:
```
rails generate doorkeeper:previous_refresh_token
```
- [#811] Toughen parameters filter with exact match
- [#813] Applications admin bugfix
- [#799] Fix Ruby Warnings
- Drop `attr_accessible` from models
### Backward incompatible changes
- [#730] Force all timezones to use UTC to prevent comparison issues.
- [#802] Remove `config.i18n.fallbacks` from engine
## 4.0.0.rc2
- Fix optional belongs_to for Rails 5
- Fix Ruby warnings
## 4.0.0.rc1
### Backward incompatible changes
- Drops support for Rails 4.1 and earlier
- Drops support for Ruby 2.0
- [#778] Bug fix: use the remaining time that a token is still valid when
building the redirect URI for the implicit grant flow
### Other changes
- [#771] Validation error messages fixes
- Adds foreign key constraints in generated migrations between tokens and
grants, and applications
- Support Rails 5
## 3.1.0
- [#736] Existing valid tokens are now reused in client_credentials flow
- [#749] Allow user to raise authorization error with custom messages.
Under `resource_owner_authenticator` block a user can
`raise Doorkeeper::Errors::DoorkeeperError.new('custom_message')`
- [#762] Check doesn’t abort the actual migration, so it runs
- [#722] `doorkeeper_forbidden_render_options` now supports returning a 404 by
specifying `respond_not_found_when_forbidden: true` in the
`doorkeeper_forbidden_render_options` method.
- [#734] Simplify and remove duplication in request strategy classes
## 3.0.1
- [#712] Wrap exchange of grant token for access token and access token refresh
in transactions
- [#704] Allow applications scopes to be mass assigned
- [#707] Fixed order of Mixin inclusion and table_name configuration in models
- [#712] Wrap access token and refresh grants in transactions
- Adds JRuby support
- Specs, views and documentation adjustments
## 3.0.0
### Other changes
- [#693] Updates `en.yml`.
## 3.0.0 (rc2)
### Backward incompatible changes
- [#678] Change application-specific scopes to take precedence over server-wide
scopes. This removes the previous behavior where the intersection between
application and server scopes was used.
### Other changes
- [#671] Fixes `NoMethodError - undefined method 'getlocal'` when calling
the /oauth/token path. Switch from using a DateTime object to update
AR to using a Time object. (Issue #668)
- [#677] Support editing application-specific scopes via the standard forms
- [#682] Pass error hash to Grape `error!`
- [#683] Generate application secret/UID if fields are blank strings
## 3.0.0 (rc1)
### Backward incompatible changes
- [#648] Extracts mongodb ORMs to
https://github.com/doorkeeper-gem/doorkeeper-mongodb. If you use ActiveRecord
you don’t need to do any change, otherwise you will need to install the new
plugin.
- [#665] `doorkeeper_unauthorized_render_options(error:)` and
`doorkeeper_forbidden_render_options(error:)` now accept `error` keyword
argument.
### Removed deprecations
- Removes `doorkeeper_for` deprecation notice.
- Remove `applications.scopes` upgrade notice.
## 2.2.2
- [#541] Fixed `undefined method attr_accessible` problem on Rails 4
(happens only when ProtectedAttributes gem is used) in #599
## 2.2.1
- [#636] `custom_access_token_expires_in` bugfixes
- [#641] syntax error fix (Issue #612)
- [#633] Send extra details to Custom Token Generator
- [#628] Refactor: improve orm adapters to ease extension
- [#637] Upgrade to rspec to 3.2
## 2.2.0 - 2015-04-19
- [#611] Allow custom access token generators to be used
- [#632] Properly fallback to `default_scopes` when no scope is specified
- [#622] Clarify that there is a logical OR between scopes for authorizing
- [#635] Upgrade to rspec 3
- [#627] i18n fallbacks to english
- Moved CHANGELOG to NEWS.md
## 2.1.4 - 2015-03-27
- [#595] HTTP spec: Add `scope` for refresh token scope param
- [#596] Limit scopes in app scopes for client credentials
- [#567] Add Grape helpers for easier integration with Grape framework
- [#606] Add custom access token expiration support for Client Credentials flow
## 2.1.3 - 2015-03-01
- [#588] Fixes scopes_match? bug that skipped authorization form in some cases
## 2.1.2 - 2015-02-25
- [#574] Remove unused update authorization route.
- [#576] Filter out sensitive parameters from logs.
- [#582] The Authorization HTTP header fields are now case insensitive.
- [#583] Database connection bugfix in certain scenarios.
- Testing improvements
## 2.1.1 - 2015-02-06
- Remove `wildcard_redirect_url` option
- [#481] Customize token flow OAuth expirations with a config lambda
- [#568] TokensController: Memoize strategy.authorize_response result to enable
subclasses to use the response object.
- [#571] Fix database initialization issues in some configurations.
- Documentation improvements
## 2.1.0 - 2015-01-13
- [#540] Include `created_at` in response.
- [#538] Check application-level scopes in client_credentials and password flow.
- [5596227] Check application scopes in AccessToken when present. Fixes a bug in
doorkeeper 2.0.0 and 2.0.1 referring to application specific scopes.
- [#534] Internationalizes doorkeeper views.
- [#545] Ensure there is a connection to the database before checking for
missing columns
- [#546] Use `Doorkeeper::` prefix when referencing `Application` to avoid
possible application model name conflict.
- [#538] Test with Rails ~> 4.2.
### Potentially backward incompatible changes
- Enable by default `authorization_code` and `client_credentials` grant flows.
Disables implicit and password grant flows by default.
- [#510, #544, 722113f] Revoked refresh token response bugfix.
## 2.0.1 - 2014-12-17
- [#525, #526, #527] Fix `ActiveRecord::NoDatabaseError` on gem load.
## 2.0.0 - 2014-12-16
### Backward incompatible changes
- [#448] Removes `doorkeeper_for` helper. Now we use
`before_action :doorkeeper_authorize!`.
- [#469] Allow client applications to restrict the set of allowable scopes.
Fixes #317. `oauth_applications` relation needs a new `scopes` string column,
non nullable, which defaults to an empty string. To add the column run:
```
rails generate doorkeeper:application_scopes
```
If you’d rather do it by hand, your ActiveRecord migration should contain:
```ruby
add_column :oauth_applications, :scopes, :string, null: false, default: ‘’
```
### Removed deprecations
- Removes `test_redirect_uri` option. It is now called `native_redirect_uri`.
- [#446] Removes `mount Doorkeeper::Engine`. Now we use `use_doorkeeper`.
### Others
- [#484] Performance improvement - avoid performing order_by when not required.
- [#450] When password is invalid in Password Credentials Grant, Doorkeeper
returned 'invalid_resource_owner' instead of 'invalid_grant', as the spec
declares. Fixes #444.
- [#452] Allows `revoked_at` to be set in the future, for future expiry.
Rationale: https://github.com/doorkeeper-gem/doorkeeper/pull/452#issuecomment-51431459
- [#480] For Implicit grant flow, access tokens can now be reused. Fixes #421.
- [#491] Reworks of @jasl's #454 and #478. ORM refactor that allows doorkeeper
to be extended more easily with unsupported ORMs. It also marks the boundaries
between shared model code and ORM specifics inside of the gem.
- [#496] Tests with Rails 4.2.
- [#489] Adds `force_ssl_in_redirect_uri` to force the usage of the HTTPS
protocol in non-native redirect uris.
- [#516] SECURITY: Adds `protect_from_forgery` to `Doorkeeper::ApplicationController`
- [#518] Fix random failures in mongodb.
---
## 1.4.2 - 2015-03-02
- [#576] Filter out sensitive parameters from logs
## 1.4.1 - 2014-12-17
- [#516] SECURITY: Adds `protect_from_forgery` to `Doorkeeper::ApplicationController`
## 1.4.0 - 2014-07-31
- internals
- [#427] Adds specs expectations.
- [#428] Error response refactor.
- [#417] Moves token validation into Access Token class.
- [#439] Removes redundant module includes.
- [#443] TokensController and TokenInfoController inherit from ActionController::Metal
- bug
- [#418] fixes #243, requests with insufficient scope now respond 403 instead
of 401. (API change)
- [#438] fixes #398, native redirect for implicit token grant bug.
- [#440] namespace fixes
- enhancements
- [#432] Keeps query parameters
## 1.3.1 - 2014-07-06
- enhancements
- [#405] Adds facade to more easily get the token from a request in a route
constraint.
- [#415] Extend Doorkeeper TokenResponse with an `after_successful_response`
callback that allows handling of `response` object.
- internals
- [#409] Deprecates `test_redirect_uri` in favor of `native_redirect_uri`.
See discussion in: [#351].
- [#411] Clean rspec deprecations. General test improvements.
- [#412] rspec line width can go longer than 80 (hound CI config).
- bug
- [#413] fixes #340, routing scope is now taken into account in redirect.
- [#401] and [#425] application is not required any longer for access_token.
## 1.3.0 - 2014-05-23
- enhancements
- [#387] Adds reuse_access_token configuration option.
## 1.2.0 - 2014-05-02
- enhancements
- [#376] Allow users to enable basic header authorization for access tokens.
- [#374] Token revocation implementation [RFC 7009]
- [#295] Only enable specific grant flows.
- internals
- [#381] Locale source fix.
- [#380] Renames `errors_for` to `doorkeeper_errors_for`.
- [#390] Style adjustments in accordance with Ruby Style Guide form
Thoughtbot.
## 1.1.0 - 2014-03-29
- enhancements
- [#336] mongoid4 support.
- [#372] Allow users to set ActiveRecord table_name_prefix/suffix options
- internals
- [#343] separate OAuth's admin and user end-point to different layouts, upgrade theme to Bootstrap 3.1.
- [#348] Move render_options in filter after `@error` has been set
## 1.0.0 - 2014-01-13
- bug (spec)
- [#228] token response `expires_in` value is now in seconds, relative to
request time
- [#296] client is optional for password grant type.
- [#319] If client credentials are present on password grant type they are validated
- [#326] If client credentials are present in refresh token they are validated
- [#326] If authenticated client does not match original client that
obtained a refresh token it responds `invalid_grant` instead of
`invalid_client`. Previous usage was invalid according to Section 5.2 of
the spec.
- [#329] access tokens' `scopes` string wa being compared against
`default_scopes` symbols, always unauthorizing.
- [#318] Include "WWW-Authenticate" header with Unauthorized responses
- enhancements
- [#293] Adds ActionController::Instrumentation in TokensController
- [#298] Support for multiple redirect_uris added.
- [#313] `AccessToken.revoke_all_for` actually revokes all non-revoked
tokens for an application/owner instead of deleting them.
- [#333] Rails 4.1 support
- internals
- Removes jQuery dependency [fixes #300][pr #312 is related]
- [#294] Client uid and secret will be generated only if not present.
- [#316] Test warnings addressed.
- [#338] Rspec 3 syntax.
---
## 0.7.4 - 2013-12-01
- bug
- Symbols instead of strings for user input.
## 0.7.3 - 2013-10-04
- enhancements
- [#204] Allow to overwrite scope in routes
- internals
- Returns only present keys in Token Response (may imply a backwards
incompatible change). https://github.com/doorkeeper-gem/doorkeeper/issues/220
- bug
- [#290] Support for Rails 4 when 'protected_attributes' gem is present.
## 0.7.2 - 2013-09-11
- enhancements
- [#272] Allow issuing multiple access_tokens for one user/application for multiple devices
- [#170] Increase length of allowed redirect URIs
- [#239] Do not try to load unavailable Request class for the current phase.
- [#273] Relax jquery-rails gem dependency
## 0.7.1 - 2013-08-30
- bug
- [#269] Rails 3.2 raised `ActiveModel::MassAssignmentSecurity::Error`.
## 0.7.0 - 2013-08-21
- enhancements
- [#229] Rails 4!
- internals
- [#203] Changing table name to be specific in column_names_with_table
- [#215] README update
- [#227] Use Rails.config.paths["config/routes"] instead of assuming "config/routes.rb" exists
- [#262] Add jquery as gem dependency
- [#263] Add a configuration for ActiveRecord.establish_connection
- Deprecation and Ruby warnings (PRs merged outside of GitHub).
## 0.6.7 - 2013-01-13
- internals
- [#188] Add IDs to the show views for integration testing [@egtann](https://github.com/egtann)
## 0.6.6 - 2013-01-04
- enhancements
- [#187] Raise error if configuration is not set
## 0.6.5 - 2012-12-26
- enhancements
- [#184] Vendor the Bootstrap CSS [@tylerhunt](https://github.com/tylerhunt)
## 0.6.4 - 2012-12-15
- bug
- [#180] Add localization to authorized_applications destroy notice [@aalvarado](https://github.com/aalvarado)
## 0.6.3 - 2012-12-07
- bugfixes
- [#163] Error response content-type header should be application/json [@ggayan](https://github.com/ggayan)
- [#175] Make token.expires_in_seconds return nil when expires_in is nil [@miyagawa](https://github.com/miyagawa)
- enhancements
- [#166, #172, #174] Behavior to automatically authorize based on a configured proc
- internals
- [#168] Using expectation syntax for controller specs [@rdsoze](https://github.com/rdsoze)
## 0.6.2 - 2012-11-10
- bugfixes
- [#162] Remove ownership columns from base migration template [@rdsoze](https://github.com/rdsoze)
## 0.6.1 - 2012-11-07
- bugfixes
- [#160] Removed |routes| argument from initializer authenticator blocks
- documentation
- [#160] Fixed description of context of authenticator blocks
## 0.6.0 - 2012-11-05
- enhancements
- Mongoid `orm` configuration accepts only :mongoid2 or :mongoid3
- Authorization endpoint does not redirect in #new action anymore. It wasn't specified by OAuth spec
- TokensController now inherits from ActionController::Metal. There might be performance upgrades
- Add link to authorization in Applications scaffold
- [#116] MongoMapper support [@carols10cents](https://github.com/carols10cents)
- [#122] Mongoid3 support [@petergoldstein](https://github.com/petergoldstein)
- [#150] Introduce test redirect uri for applications
- bugfixes
- [#157] Response token status should be `:ok`, not `:success` [@theycallmeswift](https://github.com/theycallmeswift)
- [#159] Remove ActionView::Base.field_error_proc override (fixes #145)
- internals
- Update development dependencies
- Several refactorings
- Rails/ORM are easily swichable with env vars (rails and orm)
- Travis now tests against Mongoid v2
## 0.5.0 - 2012-10-20
Official support for rubinius was removed.
- enhancements
- Configure the way access token is retrieved from request (default to bearer header)
- Authorization Code expiration time is now configurable
- Add support for mongoid
- [#78, #128, #137, #138] Application Ownership
- [#92] Allow users to skip controllers
- [#99] Remove deprecated warnings for data-\* attributes [@towerhe](https://github.com/towerhe)
- [#101] Return existing access_token for PasswordAccessTokenRequest [@benoist](https://github.com/benoist)
- [#104] Changed access token scopes example code to default_scopes and optional_scopes [@amkirwan](https://github.com/amkirwan)
- [#107] Fix typos in initializer
- [#123] i18n for validator, flash messages [@petergoldstein](https://github.com/petergoldstein)
- [#140] ActiveRecord is the default value for the ORM [@petergoldstein](https://github.com/petergoldstein)
- internals
- [#112, #120] Replacing update_attribute with update_column to eliminate deprecation warnings [@rmoriz](https://github.com/rmoriz), [@petergoldstein](https://github.com/petergoldstein)
- [#121] Updating all development dependencies to recent versions. [@petergoldstein](https://github.com/petergoldstein)
- [#144] Adding MongoDB dependency to .travis.yml [@petergoldstein](https://github.com/petergoldstein)
- [#143] Displays errors for unconfigured error messages [@timgaleckas](https://github.com/timgaleckas)
- bugfixes
- [#102] Not returning 401 when access token generation fails [@cslew](https://github.com/cslew)
- [#125] Doorkeeper is using ActiveRecord version of as_json in ORM agnostic code [@petergoldstein](https://github.com/petergoldstein)
- [#142] Prevent double submission of password based authentication [@bdurand](https://github.com/bdurand)
- documentation
- [#141] Add rack-cors middleware to readme [@gottfrois](https://github.com/gottfrois)
## 0.4.2 - 2012-06-05
- bugfixes:
- [#94] Uninitialized Constant in Password Flow
## 0.4.1 - 2012-06-02
- enhancements:
- Backport: Move doorkeeper_for extension to Filter helper
## 0.4.0 - 2012-05-26
- deprecation
- Deprecate authorization_scopes
- database changes
- AccessToken#resource_owner_id is not nullable
- enhancements
- [#83] Add Resource Owner Password Credentials flow [@jaimeiniesta](https://github.com/jaimeiniesta)
- [#76] Allow token expiration to be disabled [@mattgreen](https://github.com/mattgreen)
- [#89] Configure the way client credentials are retrieved from request
- [#b6470a] Add Client Credentials flow
- internals
- [#2ece8d, #f93778] Introduce Client and ErrorResponse classes
## 0.3.4 - 2012-05-24
- Fix attr_accessible for rails 3.2.x
## 0.3.3 - 2012-05-07
- [#86] shrink gem package size
## 0.3.2 - 2012-04-29
- enhancements
- [#54] Ignore Authorization: headers that are not Bearer [@miyagawa](https://github.com/miyagawa)
- [#58, #64] Add destroy action to applications endpoint [@jaimeiniesta](https://github.com/jaimeiniesta), [@davidfrey](https://github.com/davidfrey)
- [#63] TokensController responds with `401 unauthorized` [@jaimeiniesta](https://github.com/jaimeiniesta)
- [#67, #72] Fix for mass-assignment [@cicloid](https://github.com/cicloid)
- internals
- [#49] Add Gemnasium status image to README [@laserlemon](https://github.com/laserlemon)
- [#50] Fix typos [@tomekw](https://github.com/tomekw)
- [#51] Updated the factory_girl_rails dependency, fix expires_in response which returned a float number instead of integer [@antekpiechnik](https://github.com/antekpiechnik)
- [#62] Typos, .gitignore [@jaimeiniesta](https://github.com/jaimeiniesta)
- [#65] Change \_path redirections to \_url redirections [@jaimeiniesta](https://github.com/jaimeiniesta)
- [#75] Fix unknown method #authenticate_admin! [@mattgreen](https://github.com/mattgreen)
- Remove application link in authorized app view
## 0.3.1 - 2012-02-17
- enhancements
- [#48] Add if, else options to doorkeeper_for
- Add views generator
- internals
- Namespace models
## 0.3.0 - 2012-02-11
- enhancements
- [#17, #31] Add support for client credentials in basic auth header [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners)
- [#28] Add indices to migration [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners)
- [#29] Allow doorkeeper to run with rails 3.2 [@john-griffin](https://github.com/john-griffin)
- [#30] Improve client's redirect uri validation [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners)
- [#32] Add token (implicit grant) flow [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners)
- [#34] Add support for custom unathorized responses [@GoldsteinTechPartners](https://github.com/GoldsteinTechPartners)
- [#36] Remove repetitions from the Authorised Applications view [@carvil](https://github.com/carvil)
- When user revoke an application, all tokens for that application are revoked
- Error messages now can be translated
- Install generator copies the error messages localization file
- internals
- Fix deprecation warnings in ActiveSupport::Base64
- Remove deprecation in doorkeeper_for that handles hash arguments
- Depends on railties instead of whole rails framework
- CI now integrates with rails 3.1 and 3.2
## 0.2.0 - 2011-12-17
- enhancements
- [#4] Add authorized applications endpoint
- [#5, #11] Add access token scopes
- [#10] Add access token expiration by default
- [#9, #12] Add refresh token flow
- internals
- [#7] Improve configuration options with :default
- Improve configuration options with :builder
- Refactor config class
- Improve coverage of authorization request integration
- bug fixes
- [#6, #20] Fix access token response headers
- Fix issue with state parameter
- deprecation
- deprecate :only and :except options in doorkeeper_for
## 0.1.1 - 2011-11-30
- enhancements
- [#3] Authorization code must be short lived and single use
- [#2] Improve views provided by doorkeeper
- [#1] Skips authorization form if the client has been authorized by the resource owner
- Improve readme
- bugfixes
- Fix issue when creating the access token (wrong client id)
## 0.1.0 - 2011-11-25
- Authorization Code flow
- OAuth applications endpoint
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team members or current maintainer email, specified in gemspec. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
We love pull requests from everyone. By participating in this project, you agree
to abide by the [code of conduct](CODE_OF_CONDUCT.md).
Fork, then clone the repo:
git clone git@github.com:your-username/doorkeeper.git
### Docker Setup
Build the container image with: `docker build --pull -t doorkeeper:test .`
Run the tests with: `docker run -it --rm doorkeeper:test`
### Local Setup
* Set up Ruby dependencies via Bundler
bundle install
* Make sure the tests pass:
rake spec
* Make your changes.
* Write tests.
* Follow our [style guides](.rubocop.yml).
* Make the tests pass:
rake spec
* Add notes about your changes to the `CHANGELOG.md` file.
* Write a [good commit message][commit].
* Push to your fork.
* [Submit a pull request][pr].
[commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
[pr]: https://github.com/doorkeeper-gem/doorkeeper/compare/
* If [Rubocop] catches style violations, fix them. If our bot suggested changes — please add them.
* Wait for us. We try to at least comment on pull requests within one business day.
* We may suggest changes.
* Please, squash your commits to a single one if you introduced a new changes or pushed more than
one commit. Let's keep the history clean.
Thank you for your contribution! :handshake:
================================================
FILE: Dockerfile
================================================
FROM ruby:3.3.4-alpine
# Linux UID (user id) for the doorkeeper user, change with [--build-arg UID=1234]
ARG UID="991"
# Linux GID (group id) for the doorkeeper user, change with [--build-arg GID=1234]
ARG GID="991"
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
ARG TZ="Etc/UTC"
# Apply timezone
ENV TZ=${TZ}
RUN addgroup -g "${GID}" doorkeeper; \
adduser -u "${UID}" -G "doorkeeper" -h /srv doorkeeper; \
echo "${TZ}" > /etc/localtime;
RUN apk add --no-cache \
ca-certificates \
wget \
openssl \
bash \
build-base \
git \
sqlite-dev \
tzdata
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
ENV BUNDLER_VERSION=2.5.11
RUN gem install bundler -v ${BUNDLER_VERSION} -i /usr/local/lib/ruby/gems/$(ls /usr/local/lib/ruby/gems) --force
WORKDIR /srv
COPY Gemfile doorkeeper.gemspec /srv/
COPY lib/doorkeeper/version.rb /srv/lib/doorkeeper/version.rb
# This is a fix for sqlite alpine issues
RUN bundle config force_ruby_platform true
RUN bundle install
COPY . /srv/
RUN chown -R doorkeeper:doorkeeper /srv/coverage /srv/spec/dummy/tmp /srv/spec/generators/tmp
# Set the running user for resulting container
USER doorkeeper
CMD ["rake"]
================================================
FILE: Gemfile
================================================
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gemspec
gem "rails", ">= 7.0", "< 8.1"
gem "sprockets-rails"
gem "rspec-core"
gem "rspec-expectations"
gem "rspec-mocks"
gem "rspec-rails", "~> 8.0"
gem "rspec-support"
group :development, :rubocop do
gem "rubocop", "~> 1.72"
gem "rubocop-capybara", "~> 2.22", require: false
gem "rubocop-factory_bot", "~> 2.27", require: false
gem "rubocop-performance", "~> 1.24", require: false
gem "rubocop-rails", "~> 2.30", require: false
gem "rubocop-rspec", "~> 3.5", require: false
gem "rubocop-rspec_rails", "~> 2.31", require: false
end
gem "bcrypt", "~> 3.1", require: false
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw]
gem "timecop"
gem 'irb', '~> 1.8'
# Interactive Debugging tools
gem 'debug', '~> 1.8'
================================================
FILE: MIT-LICENSE
================================================
Copyright 2011 Applicake. http://applicake.com
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: NEWS.md
================================================
Document moved [here](CHANGELOG.md)
================================================
FILE: README.md
================================================
# Doorkeeper — awesome OAuth 2 provider for your Rails / Grape app.
[](https://rubygems.org/gems/doorkeeper)
[](https://github.com/doorkeeper-gem/doorkeeper/actions/workflows/ci.yml)
[](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper)
[](https://coveralls.io/github/doorkeeper-gem/doorkeeper?branch=main)
[](https://dashboard.guardrails.io/gh/doorkeeper-gem/repos/21183)
[](https://dependabot.com)
Doorkeeper is a gem (Rails engine) that makes it easy to introduce OAuth 2 provider
functionality to your Ruby on Rails or Grape application.
Supported features:
- [The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)
- [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1)
- [Access Token Scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3)
- [Refresh token](https://datatracker.ietf.org/doc/html/rfc6749#section-1.5)
- [Implicit grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.2)
- [Resource Owner Password Credentials](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3)
- [Client Credentials](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)
- [OAuth 2.0 Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009)
- [OAuth 2.0 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
- [OAuth 2.0 Threat Model and Security Considerations](https://datatracker.ietf.org/doc/html/rfc6819)
- [OAuth 2.0 for Native Apps](https://datatracker.ietf.org/doc/html/rfc8252)
- [Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636)
## Table of Contents
- [Documentation](#documentation)
- [Installation](#installation)
- [Ruby on Rails](#ruby-on-rails)
- [Grape](#grape)
- [ORMs](#orms)
- [Extensions](#extensions)
- [Example Applications](#example-applications)
- [Sponsors](#sponsors)
- [Development](#development)
- [Contributing](#contributing)
- [Contributors](#contributors)
- [License](#license)
## Documentation
This documentation is valid for `main` branch. Please check the documentation for the version of doorkeeper you are using in:
https://github.com/doorkeeper-gem/doorkeeper/releases.
Additionally, other resources can be found on:
- [Guides](https://doorkeeper.gitbook.io/guides/) with how-to get started and configuration documentation
- See the [Wiki](https://github.com/doorkeeper-gem/doorkeeper/wiki) for articles on how to integrate with other solutions
- Screencast from [railscasts.com](http://railscasts.com/): [#353
OAuth with
Doorkeeper](http://railscasts.com/episodes/353-oauth-with-doorkeeper)
- See [upgrade guides](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions)
- For general questions, please post on [Stack Overflow](http://stackoverflow.com/questions/tagged/doorkeeper)
- See [SECURITY.md](SECURITY.md) for this project's security disclose
policy
## Installation
Installation depends on the framework you're using. The first step is to add the following to your Gemfile:
```ruby
gem 'doorkeeper'
```
And run `bundle install`. After this, check out the guide related to the framework you're using.
### Ruby on Rails
Doorkeeper currently supports Ruby on Rails >= 5.0. See the guide [here](https://doorkeeper.gitbook.io/guides/ruby-on-rails/getting-started).
### Grape
Guide for integration with Grape framework can be found [here](https://doorkeeper.gitbook.io/guides/grape/grape).
## ORMs
Doorkeeper supports Active Record by default, but can be configured to work with the following ORMs:
| ORM | Support via |
| :--- | :--- |
| Active Record | by default |
| MongoDB | [doorkeeper-gem/doorkeeper-mongodb](https://github.com/doorkeeper-gem/doorkeeper-mongodb) |
| Sequel | [nbulaj/doorkeeper-sequel](https://github.com/nbulaj/doorkeeper-sequel) |
| Couchbase | [acaprojects/doorkeeper-couchbase](https://github.com/acaprojects/doorkeeper-couchbase) |
| RethinkDB | [aca-labs/doorkeeper-rethinkdb](https://github.com/aca-labs/doorkeeper-rethinkdb) |
## Extensions
Extensions that are not included by default and can be installed separately.
| | Link |
| :--- | :--- |
| OpenID Connect extension | [doorkeeper-gem/doorkeeper-openid\_connect](https://github.com/doorkeeper-gem/doorkeeper-openid_connect) |
| JWT Token support | [doorkeeper-gem/doorkeeper-jwt](https://github.com/doorkeeper-gem/doorkeeper-jwt) |
| Assertion grant extension | [doorkeeper-gem/doorkeeper-grants\_assertion](https://github.com/doorkeeper-gem/doorkeeper-grants_assertion) |
| I18n translations | [doorkeeper-gem/doorkeeper-i18n](https://github.com/doorkeeper-gem/doorkeeper-i18n) |
| CIBA - Client Initiated Backchannel Authentication Flow extension | [doorkeeper-ciba](https://github.com/autoseg/doorkeeper-ciba) |
| Device Authorization Grant | [doorkeeper-device_authorization_grant](https://github.com/exop-group/doorkeeper-device_authorization_grant) |
## Example Applications
These applications show how Doorkeeper works and how to integrate with it. Start with the oAuth2 server and use the clients to connect with the server.
| Application | Link |
| :--- | :--- |
| OAuth2 Server with Doorkeeper | [doorkeeper-gem/doorkeeper-provider-app](https://github.com/doorkeeper-gem/doorkeeper-provider-app) |
| Sinatra Client connected to Provider App | [doorkeeper-gem/doorkeeper-sinatra-client](https://github.com/doorkeeper-gem/doorkeeper-sinatra-client) |
| Devise + Omniauth Client | [doorkeeper-gem/doorkeeper-devise-client](https://github.com/doorkeeper-gem/doorkeeper-devise-client) |
You may want to create a client application to
test the integration. Check out these [client
examples](https://github.com/doorkeeper-gem/doorkeeper/wiki/Example-Applications)
in our wiki or follow this [tutorial
here](https://github.com/doorkeeper-gem/doorkeeper/wiki/Testing-your-provider-with-OAuth2-gem).
## Sponsors
[](#backers)
[](#sponsors)
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/doorkeeper-gem#sponsor)]
> Codecademy supports open source as part of its mission to democratize tech. Come help us build the education the world deserves: [https://codecademy.com/about/careers](https://codecademy.com/about/careers?utm_source=doorkeeper-gem)
> If you prefer not to deal with the gory details of OAuth 2, need dedicated customer support & consulting, try the cloud-based SaaS version: [https://oauth.io](https://oauth.io/?utm_source=doorkeeper-gem)
> Wealthsimple is a financial company on a mission to help everyone achieve financial freedom by providing products and advice that are accessible and affordable. Using smart technology, Wealthsimple takes financial services that are often confusing, opaque and expensive and makes them simple, transparent, and low-cost. See what Investing on Autopilot is all about: [https://www.wealthsimple.com](https://www.wealthsimple.com/?utm_source=doorkeeper-gem)
## Development
To run the local engine server:
```
bundle install
bundle exec rake doorkeeper:server
````
By default, it uses the latest Rails version with ActiveRecord. To run the
tests with a specific Rails version:
```
BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake
```
You can also experiment with the changes using `bin/console`. It uses in-memory SQLite database and default
Doorkeeper config, but you can reestablish connection or reconfigure the gem if you need.
## Contributing
Want to contribute and don't know where to start? Check out [features we're
missing](https://github.com/doorkeeper-gem/doorkeeper/wiki/Supported-Features),
create [example
apps](https://github.com/doorkeeper-gem/doorkeeper/wiki/Example-Applications),
integrate the gem with your app and let us know!
Also, check out our [contributing guidelines page](CONTRIBUTING.md).
## Contributors
Thanks to all our [awesome
contributors](https://github.com/doorkeeper-gem/doorkeeper/graphs/contributors)!
## License
MIT License. Created in Applicake. Maintained by the community.
================================================
FILE: RELEASING.md
================================================
# Releasing Doorkeeper
How to release Doorkeeper in five easy steps!
1. Update `lib/doorkeeper/version.rb` file accordingly.
2. Update `CHANGELOG.md` to reflect the changes since last release.
3. Commit changes: `git commit -am 'Bump to vVERSION'`.
4. Build and publish the gem.
4. Create GitHub release.
5. Announce the new release, making sure to say “thank you” to the contributors
who helped shape this version!
================================================
FILE: Rakefile
================================================
# frozen_string_literal: true
require "bundler/setup"
require "rspec/core/rake_task"
desc "Default: run specs."
task default: :spec
desc "Run all specs"
RSpec::Core::RakeTask.new(:spec) do |config|
config.verbose = false
end
namespace :doorkeeper do
desc "Install doorkeeper in dummy app"
task :install do
cd "spec/dummy"
system "bundle exec rails g doorkeeper:install --force"
end
desc "Runs local test server"
task :server do
cd "spec/dummy"
system "bundle exec rails server"
end
end
Bundler::GemHelper.install_tasks
================================================
FILE: SECURITY.md
================================================
# Reporting security issues in Doorkeeper
Hello! Thank you for wanting to disclose a possible security
vulnerability within the Doorkeeper gem! Please follow our disclosure
policy as outlined below:
1. Do NOT open up a GitHub issue with your report. Security reports
should be kept private until a possible fix is determined.
2. Send an email to Nikita Bulai at bulaj.nikita AT gmail.com or one of
the others Doorkeeper maintainers listed in gemspec. You should receive
a prompt response.
3. Be patient. Since Doorkeeper is in a stable maintenance phase, we want to
do as little as possible to rock the boat of the project.
Thank you very much for adhering for these policies!
================================================
FILE: UPGRADE.md
================================================
See [Upgrade Guides](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions)
in the project Wiki.
================================================
FILE: app/assets/stylesheets/doorkeeper/admin/application.css
================================================
/*
*= require doorkeeper/bootstrap.min
*
*= require_self
*= require_tree .
*/
.doorkeeper-admin .form-group > .field_with_errors {
width: 16.66667%;
}
================================================
FILE: app/assets/stylesheets/doorkeeper/application.css
================================================
/*
*= require doorkeeper/bootstrap.min
*
*= require_self
*= require_tree .
*/
body {
background-color: #eee;
font-size: 14px;
}
#container {
background-color: #fff;
border: 1px solid #999;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 6px;
-webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
box-shadow: 0 3px 20px rgba(0, 0, 0, 0.3);
margin: 2em auto;
max-width: 600px;
outline: 0;
padding: 1em;
width: 80%;
}
.page-header {
margin-top: 20px;
}
#oauth-permissions {
width: 260px;
}
.actions {
border-top: 1px solid #eee;
margin-top: 1em;
padding-top: 9px;
}
.actions > form > .btn {
margin-top: 5px;
}
.separator {
color: #eee;
padding: 0 .5em;
}
.inline_block {
display: inline-block;
}
#oauth {
margin-bottom: 1em;
}
#oauth > .btn {
width: 7em;
}
td {
vertical-align: middle !important;
}
================================================
FILE: app/controllers/doorkeeper/application_controller.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class ApplicationController <
Doorkeeper.config.resolve_controller(:base)
include Helpers::Controller
include ActionController::MimeResponds if Doorkeeper.config.api_only
unless Doorkeeper.config.api_only
protect_from_forgery with: :exception
helper "doorkeeper/dashboard"
end
end
end
================================================
FILE: app/controllers/doorkeeper/application_metal_controller.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class ApplicationMetalController <
Doorkeeper.config.resolve_controller(:base_metal)
include Helpers::Controller
before_action :enforce_content_type,
if: -> { Doorkeeper.config.enforce_content_type }
ActiveSupport.run_load_hooks(:doorkeeper_metal_controller, self)
end
end
================================================
FILE: app/controllers/doorkeeper/applications_controller.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class ApplicationsController < Doorkeeper::ApplicationController
layout "doorkeeper/admin" unless Doorkeeper.configuration.api_only
before_action :authenticate_admin!
before_action :set_application, only: %i[show edit update destroy]
def index
@applications = Doorkeeper.config.application_model.ordered_by(:created_at)
respond_to do |format|
format.html
format.json { head :no_content }
end
end
def show
respond_to do |format|
format.html
format.json { render json: @application, as_owner: true }
end
end
def new
@application = Doorkeeper.config.application_model.new
end
def create
@application = Doorkeeper.config.application_model.new(application_params)
if @application.save
flash[:notice] = I18n.t(:notice, scope: %i[doorkeeper flash applications create])
flash[:application_secret] = @application.plaintext_secret
respond_to do |format|
format.html { redirect_to oauth_application_url(@application) }
format.json { render json: @application, as_owner: true }
end
else
respond_to do |format|
format.html { render :new }
format.json do
errors = @application.errors.full_messages
render json: { errors: errors }, status: :unprocessable_entity
end
end
end
end
def edit; end
def update
if @application.update(application_params)
flash[:notice] = I18n.t(:notice, scope: i18n_scope(:update))
respond_to do |format|
format.html { redirect_to oauth_application_url(@application) }
format.json { render json: @application, as_owner: true }
end
else
respond_to do |format|
format.html { render :edit }
format.json do
errors = @application.errors.full_messages
render json: { errors: errors }, status: :unprocessable_entity
end
end
end
end
def destroy
flash[:notice] = I18n.t(:notice, scope: i18n_scope(:destroy)) if @application.destroy
respond_to do |format|
format.html { redirect_to oauth_applications_url }
format.json { head :no_content }
end
end
private
def set_application
@application = Doorkeeper.config.application_model.find(params[:id])
end
def application_params
params.require(:doorkeeper_application)
.permit(:name, :redirect_uri, :scopes, :confidential)
end
def i18n_scope(action)
%i[doorkeeper flash applications] << action
end
end
end
================================================
FILE: app/controllers/doorkeeper/authorizations_controller.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class AuthorizationsController < Doorkeeper::ApplicationController
before_action :authenticate_resource_owner!
def new
if pre_auth.authorizable?
render_success
else
render_error
end
end
def create
redirect_or_render(authorize_response)
end
def destroy
redirect_or_render(authorization.deny)
rescue Doorkeeper::Errors::InvalidTokenStrategy => e
error_response = get_error_response_from_exception(e)
if Doorkeeper.configuration.api_only
render json: error_response.body, status: :bad_request
else
render :error, locals: { error_response: error_response }
end
end
private
def render_success
if skip_authorization? || can_authorize_response?
redirect_or_render(authorize_response)
elsif Doorkeeper.configuration.api_only
render json: pre_auth
else
render :new
end
end
def render_error
pre_auth.error_response.raise_exception! if Doorkeeper.config.raise_on_errors?
if Doorkeeper.configuration.redirect_on_errors? && pre_auth.error_response.redirectable?
redirect_or_render(pre_auth.error_response)
elsif Doorkeeper.configuration.api_only
render json: pre_auth.error_response.body, status: pre_auth.error_response.status
else
render :error, locals: { error_response: pre_auth.error_response }, status: pre_auth.error_response.status
end
end
def can_authorize_response?
Doorkeeper.config.custom_access_token_attributes.empty? && pre_auth.client.application.confidential? && matching_token?
end
# Active access token issued for the same client and resource owner with
# the same set of the scopes exists?
def matching_token?
# We don't match tokens on the custom attributes here - we're in the pre-auth here,
# so they haven't been supplied yet (there are no custom attributes to match on yet)
@matching_token ||= Doorkeeper.config.access_token_model.matching_token_for(
pre_auth.client,
current_resource_owner,
pre_auth.scopes,
)
end
def redirect_or_render(auth)
if auth.redirectable?
if Doorkeeper.configuration.api_only
if pre_auth.form_post_response?
render(
json: { status: :post, redirect_uri: pre_auth.redirect_uri, body: auth.body },
status: auth.status,
)
else
render(
json: { status: :redirect, redirect_uri: auth.redirect_uri },
status: auth.status,
)
end
elsif pre_auth.form_post_response?
render :form_post, locals: { auth: auth }
else
redirect_to auth.redirect_uri, allow_other_host: true
end
else
render json: auth.body, status: auth.status
end
end
def pre_auth
@pre_auth ||= OAuth::PreAuthorization.new(
Doorkeeper.configuration,
pre_auth_params,
current_resource_owner,
)
end
def pre_auth_params
params.slice(*pre_auth_param_fields).permit(*pre_auth_param_fields)
end
def pre_auth_param_fields
custom_access_token_attributes + %i[
client_id
code_challenge
code_challenge_method
response_type
response_mode
redirect_uri
scope
state
]
end
def custom_access_token_attributes
Doorkeeper.config.custom_access_token_attributes.map(&:to_sym)
end
def authorization
@authorization ||= strategy.request
end
def strategy
@strategy ||= server.authorization_request(pre_auth.response_type)
end
def authorize_response
@authorize_response ||= begin
unless pre_auth.authorizable?
response = pre_auth.error_response
response.raise_exception! if Doorkeeper.config.raise_on_errors?
return response
end
context = build_context(pre_auth: pre_auth)
before_successful_authorization(context)
auth = strategy.authorize
context = build_context(auth: auth)
after_successful_authorization(context)
auth
end
end
def build_context(**attributes)
Doorkeeper::OAuth::Hooks::Context.new(**attributes)
end
def before_successful_authorization(context = nil)
Doorkeeper.config.before_successful_authorization.call(self, context)
end
def after_successful_authorization(context)
Doorkeeper.config.after_successful_authorization.call(self, context)
end
end
end
================================================
FILE: app/controllers/doorkeeper/authorized_applications_controller.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class AuthorizedApplicationsController < Doorkeeper::ApplicationController
before_action :authenticate_resource_owner!
def index
@applications = Doorkeeper.config.application_model.authorized_for(current_resource_owner)
respond_to do |format|
format.html
format.json { render json: @applications, current_resource_owner: current_resource_owner }
end
end
def destroy
Doorkeeper.config.application_model.revoke_tokens_and_grants_for(
params[:id],
current_resource_owner,
)
respond_to do |format|
format.html do
redirect_to oauth_authorized_applications_url, notice: I18n.t(
:notice, scope: %i[doorkeeper flash authorized_applications destroy],
)
end
format.json { head :no_content }
end
end
end
end
================================================
FILE: app/controllers/doorkeeper/token_info_controller.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class TokenInfoController < Doorkeeper::ApplicationMetalController
def show
if doorkeeper_token&.accessible?
render json: doorkeeper_token_to_json, status: :ok
else
error = OAuth::InvalidTokenResponse.new
response.headers.merge!(error.headers)
render json: error_to_json(error), status: error.status
end
end
protected
def doorkeeper_token_to_json
doorkeeper_token
end
def error_to_json(error)
error.body
end
end
end
================================================
FILE: app/controllers/doorkeeper/tokens_controller.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class TokensController < Doorkeeper::ApplicationMetalController
before_action :validate_presence_of_client, only: [:revoke]
rescue_from Errors::DoorkeeperError do |e|
handle_token_exception(e)
end
def create
headers.merge!(authorize_response.headers)
render json: authorize_response.body,
status: authorize_response.status
end
# OAuth 2.0 Token Revocation - https://datatracker.ietf.org/doc/html/rfc7009
def revoke
# The authorization server responds with HTTP status code 200 if the client
# submitted an invalid token or the token has been revoked successfully.
if token.blank?
render json: {}, status: 200
# The authorization server validates [...] and whether the token
# was issued to the client making the revocation request. If this
# validation fails, the request is refused and the client is informed
# of the error by the authorization server as described below.
elsif authorized?
revoke_token
render json: {}, status: 200
else
render json: revocation_error_response, status: :forbidden
end
end
# OAuth 2.0 Token Introspection - https://datatracker.ietf.org/doc/html/rfc7662
def introspect
introspection = OAuth::TokenIntrospection.new(server, token)
if introspection.authorized?
render json: introspection.to_json, status: 200
else
error = introspection.error_response
headers.merge!(error.headers)
render json: error.body, status: error.status
end
end
private
def validate_presence_of_client
return if Doorkeeper.config.skip_client_authentication_for_password_grant
# @see 2.1. Revocation Request
#
# The client constructs the request by including the following
# parameters using the "application/x-www-form-urlencoded" format in
# the HTTP request entity-body:
# token REQUIRED.
# token_type_hint OPTIONAL.
#
# The client also includes its authentication credentials as described
# in Section 2.3. of [RFC6749].
#
# The authorization server first validates the client credentials (in
# case of a confidential client) and then verifies whether the token
# was issued to the client making the revocation request.
return if server.client
# If this validation [client credentials / token ownership] fails, the request is
# refused and the client is informed of the error by the authorization server as
# described below.
#
# @see 2.2.1. Error Response
#
# The error presentation conforms to the definition in Section 5.2 of [RFC6749].
render json: revocation_error_response, status: :forbidden
end
# OAuth 2.0 Section 2.1 defines two client types, "public" & "confidential".
#
# RFC7009
# Section 5. Security Considerations
# A malicious client may attempt to guess valid tokens on this endpoint
# by making revocation requests against potential token strings.
# According to this specification, a client's request must contain a
# valid client_id, in the case of a public client, or valid client
# credentials, in the case of a confidential client. The token being
# revoked must also belong to the requesting client.
#
# Once a client is authenticated, it must be authorized to
# revoke the provided access or refresh token. This ensures one client
# cannot revoke another's tokens.
#
# Doorkeeper checks token ownership for any token that has an
# application_id set. Tokens issued without a client (application_id
# is null) can be revoked without client authorization.
#
# https://datatracker.ietf.org/doc/html/rfc6749#section-2.1
# https://datatracker.ietf.org/doc/html/rfc7009
def authorized?
# Token belongs to specific client, so we need to check if
# authenticated client could access it.
if token.application_id?
# We authorize client by comparing client and token application IDs
server.client && server.client.id == token.application_id
else
# Token was issued without client, authorization unnecessary
true
end
end
def revoke_token
# The authorization server responds with HTTP status code 200 if the token
# has been revoked successfully or if the client submitted an invalid
# token
revocable_token.revoke if revocable_token.revocable?
end
def token
revocable_token&.token
end
def revocable_token
return @revocable_token if defined? @revocable_token
@revocable_token =
if params[:token_type_hint] == "refresh_token"
refresh_token
else
access_token || refresh_token
end
end
def refresh_token
token = Doorkeeper.config.access_token_model.by_refresh_token(params["token"])
return unless token
RevocableTokens::RevocableRefreshToken.new(token)
end
def access_token
token = Doorkeeper.config.access_token_model.by_token(params["token"])
return unless token
RevocableTokens::RevocableAccessToken.new(token)
end
def strategy
@strategy ||= server.token_request(params[:grant_type])
end
def authorize_response
@authorize_response ||= begin
before_successful_authorization
auth = strategy.authorize
context = build_context(auth: auth)
after_successful_authorization(context) unless auth.is_a?(Doorkeeper::OAuth::ErrorResponse)
auth
end
end
def build_context(**attributes)
Doorkeeper::OAuth::Hooks::Context.new(**attributes)
end
def before_successful_authorization(context = nil)
Doorkeeper.config.before_successful_authorization.call(self, context)
end
def after_successful_authorization(context)
Doorkeeper.config.after_successful_authorization.call(self, context)
end
def revocation_error_response
error_description = I18n.t(:unauthorized, scope: %i[doorkeeper errors messages revoke])
{ error: :unauthorized_client, error_description: error_description }
end
end
end
================================================
FILE: app/helpers/doorkeeper/dashboard_helper.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module DashboardHelper
def doorkeeper_errors_for(object, method)
return if object.errors[method].blank?
output = object.errors[method].map do |msg|
content_tag(:span, class: "invalid-feedback") do
msg.capitalize
end
end
safe_join(output)
end
def doorkeeper_submit_path(application)
application.persisted? ? oauth_application_path(application) : oauth_applications_path
end
end
end
================================================
FILE: app/views/doorkeeper/applications/_delete_form.html.erb
================================================
<%- submit_btn_css ||= 'btn btn-link' %>
<%= form_tag oauth_application_path(application), method: :delete do %>
<%= submit_tag t('doorkeeper.applications.buttons.destroy'),
onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')",
class: submit_btn_css %>
<% end %>
================================================
FILE: app/views/doorkeeper/applications/_form.html.erb
================================================
<%= form_for application, url: doorkeeper_submit_path(application), as: :doorkeeper_application, html: { role: 'form' } do |f| %>
<% if application.errors.any? %>
================================================
FILE: benchmark/ruby/client_credentials.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "..", "lib"))
begin
require "bundler/inline"
rescue LoadError => e
warn "Bundler version 1.10 or later is required. Please update your Bundler"
raise e
end
gemfile(true) do
source "https://rubygems.org"
gem "sqlite3"
gem "rails"
gem "benchmark-ips"
end
require "benchmark/ips"
require "ostruct"
require "doorkeeper"
require "active_support/all"
require "active_record/railtie"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = ENV["LOGGER"].present? ? Logger.new(STDOUT) : nil
# Load database schema
load File.expand_path("../../spec/dummy/db/schema.rb", __dir__)
Doorkeeper.configure do
orm :active_record
grant_flows %w[password authorization_code client_credentials]
skip_authorization do
true
end
end
client = Doorkeeper::Application.create!(
name: "test",
uid: "123456789",
secret: "987654321",
redirect_uri: "https://doorkeeper.test",
)
context = OpenStruct.new
request = OpenStruct.new
request.parameters = {
client_id: client.uid,
client_secret: client.secret,
}.with_indifferent_access
context.request = request
Benchmark.ips do |ips|
ips.report("Client credentials") do
server = Doorkeeper::Server.new(context)
strategy = server.token_request("client_credentials")
strategy.authorize
end
end
================================================
FILE: benchmark/wrk/.keep
================================================
================================================
FILE: bin/console
================================================
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "rails/all"
require "active_support/all"
require "irb"
require "debug"
require "doorkeeper"
Rails.logger = Logger.new(STDOUT)
Rails.logger.info("Doorkeeper version: #{Doorkeeper::VERSION::STRING}")
Rails.logger.info("Rails version: #{Rails::VERSION::STRING}")
# Default Doorkeeper config
Doorkeeper.configure do
orm :active_record
end
# Generate in-memory database for testing
ActiveRecord::Base.establish_connection(
adapter: "sqlite3",
database: ":memory:",
)
# Load database schema
load File.expand_path("../spec/dummy/db/schema.rb", __dir__)
IRB.start(__FILE__)
================================================
FILE: config/locales/en.yml
================================================
en:
activerecord:
attributes:
doorkeeper/application:
name: 'Name'
redirect_uri: 'Redirect URI'
errors:
models:
doorkeeper/application:
attributes:
redirect_uri:
fragment_present: 'cannot contain a fragment.'
invalid_uri: 'must be a valid URI.'
unspecified_scheme: 'must specify a scheme.'
relative_uri: 'must be an absolute URI.'
secured_uri: 'must be an HTTPS/SSL URI.'
forbidden_uri: 'is forbidden by the server.'
scopes:
not_match_configured: "doesn't match those configured on the server."
doorkeeper:
applications:
confirmations:
destroy: 'Are you sure?'
buttons:
edit: 'Edit'
destroy: 'Destroy'
submit: 'Submit'
cancel: 'Cancel'
authorize: 'Authorize'
form:
error: 'Whoops! Check your form for possible errors'
help:
confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.'
redirect_uri: 'Use one line per URI'
blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require a redirect URI."
scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.'
edit:
title: 'Edit application'
index:
title: 'Your applications'
new: 'New Application'
name: 'Name'
callback_url: 'Callback URL'
confidential: 'Confidential?'
actions: 'Actions'
confidentiality:
'yes': 'Yes'
'no': 'No'
new:
title: 'New Application'
show:
title: 'Application: %{name}'
application_id: 'UID:'
secret: 'Secret:'
secret_hashed: 'Secret hashed'
scopes: 'Scopes:'
confidential: 'Confidential:'
callback_urls: 'Callback URLs:'
actions: 'Actions'
not_defined: 'Not defined'
authorizations:
buttons:
authorize: 'Authorize'
deny: 'Deny'
error:
title: 'An error has occurred'
new:
title: 'Authorization required'
prompt: 'Authorize %{client_name} to use your account?'
able_to: 'This application will be able to:'
show:
title: 'Authorization code:'
form_post:
title: 'Submit this form'
authorized_applications:
confirmations:
revoke: 'Are you sure?'
buttons:
revoke: 'Revoke'
index:
title: 'Your authorized applications'
application: 'Application'
created_at: 'Created At'
date_format: '%Y-%m-%d %H:%M:%S'
pre_authorization:
status: 'Pre-authorization'
errors:
messages:
# Common error messages
invalid_request:
unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.'
missing_param: 'Missing required parameter: %{value}.'
request_not_authorized: 'Request needs to be authorized. Required parameter for authorizing the request is missing or invalid.'
invalid_code_challenge: 'Code challenge is required.'
invalid_redirect_uri: "The requested redirect URI is malformed or doesn't match the client redirect URI."
unauthorized_client: 'The client is not authorized to perform this request using this method.'
access_denied: 'The resource owner or authorization server denied the request.'
invalid_scope: 'The requested scope is invalid, unknown, or malformed.'
invalid_code_challenge_method:
zero: 'The authorization server does not support PKCE as there are no accepted code_challenge_method values.'
one: 'The code_challenge_method must be %{challenge_methods}.'
other: 'The code_challenge_method must be one of %{challenge_methods}.'
server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
# Configuration error messages
credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.'
resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.'
admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.'
# Access grant errors
unsupported_response_type: 'The authorization server does not support this response type.'
unsupported_response_mode: 'The authorization server does not support this response mode.'
# Access token errors
invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'
invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'
unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.'
invalid_token:
revoked: "The access token was revoked"
expired: "The access token expired"
unknown: "The access token is invalid"
revoke:
unauthorized: "You are not authorized to revoke this token"
forbidden_token:
missing_scope: 'Access to this resource requires scope "%{oauth_scopes}".'
flash:
applications:
create:
notice: 'Application created.'
destroy:
notice: 'Application deleted.'
update:
notice: 'Application updated.'
authorized_applications:
destroy:
notice: 'Application revoked.'
layouts:
admin:
title: 'Doorkeeper'
nav:
oauth2_provider: 'OAuth2 Provider'
applications: 'Applications'
home: 'Home'
application:
title: 'OAuth authorization required'
================================================
FILE: doorkeeper.gemspec
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift(File.expand_path("lib", __dir__))
require "doorkeeper/version"
Gem::Specification.new do |gem|
gem.name = "doorkeeper"
gem.version = Doorkeeper::VERSION::STRING
gem.authors = ["Felipe Elias Philipp", "Tute Costa", "Jon Moss", "Nikita Bulai"]
gem.email = %w[bulaj.nikita@gmail.com]
gem.homepage = "https://github.com/doorkeeper-gem/doorkeeper"
gem.summary = "OAuth 2 provider for Rails and Grape"
gem.description = "Doorkeeper is an OAuth 2 provider for Rails and Grape."
gem.license = "MIT"
gem.files = Dir[
"{app,config,lib,vendor}/**/*",
"CHANGELOG.md",
"MIT-LICENSE",
"README.md",
]
gem.require_paths = ["lib"]
gem.metadata = {
"homepage_uri" => "https://github.com/doorkeeper-gem/doorkeeper",
"changelog_uri" => "https://github.com/doorkeeper-gem/doorkeeper/blob/main/CHANGELOG.md",
"source_code_uri" => "https://github.com/doorkeeper-gem/doorkeeper",
"bug_tracker_uri" => "https://github.com/doorkeeper-gem/doorkeeper/issues",
"documentation_uri" => "https://doorkeeper.gitbook.io/guides/",
"funding_uri" => "https://opencollective.com/doorkeeper-gem",
}
gem.add_dependency "railties", ">= 5"
gem.required_ruby_version = ">= 2.7"
gem.add_development_dependency "appraisal"
gem.add_development_dependency "capybara"
gem.add_development_dependency "coveralls_reborn"
gem.add_development_dependency "database_cleaner", "~> 2.0"
gem.add_development_dependency "factory_bot", "~> 6.0"
gem.add_development_dependency "generator_spec", "~> 0.10.0"
gem.add_development_dependency "grape"
gem.add_development_dependency "rake", ">= 11.3.0"
gem.add_development_dependency "rspec-rails"
gem.add_development_dependency "timecop"
end
================================================
FILE: gemfiles/rails_7_0.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "rails", "~> 7.0.0"
gem "rspec-core"
gem "rspec-expectations"
gem "rspec-mocks"
gem "rspec-rails", "~> 5.0"
gem "rspec-support"
gem "rubocop", "~> 1.4"
gem "rubocop-performance", require: false
gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
gem "bcrypt", "~> 3.1", require: false
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
gem "sprockets-rails"
gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw]
gem "timecop"
gem "base64"
gem "drb"
gem "mutex_m"
gem "concurrent-ruby", "1.3.4"
gemspec path: "../"
================================================
FILE: gemfiles/rails_7_1.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "rails", "~> 7.1.0"
gem "rspec-core"
gem "rspec-expectations"
gem "rspec-mocks"
gem "rspec-rails", "~> 7.1"
gem "rspec-support"
gem "rubocop", "~> 1.4"
gem "rubocop-performance", require: false
gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
gem "bcrypt", "~> 3.1", require: false
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
gem "sprockets-rails"
gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw]
gem "timecop"
gemspec path: "../"
================================================
FILE: gemfiles/rails_7_2.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "rails", "~> 7.2.0"
gem "rspec-core"
gem "rspec-expectations"
gem "rspec-mocks"
gem "rspec-rails", "~> 7.1"
gem "rspec-support"
gem "rubocop", "~> 1.4"
gem "rubocop-performance", require: false
gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
gem "bcrypt", "~> 3.1", require: false
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
gem "sprockets-rails"
gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw]
gem "timecop"
gemspec path: "../"
================================================
FILE: gemfiles/rails_8_0.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "rails", "~> 8.0.0"
gem "rspec-core"
gem "rspec-expectations"
gem "rspec-mocks"
gem "rspec-rails", "~> 7.1"
gem "rspec-support"
gem "rubocop", "~> 1.4"
gem "rubocop-performance", require: false
gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
gem "bcrypt", "~> 3.1", require: false
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
gem "sprockets-rails"
gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw]
gem "timecop"
gemspec path: "../"
================================================
FILE: gemfiles/rails_edge.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "rails", git: "https://github.com/rails/rails"
gem "rspec-core"
gem "rspec-expectations"
gem "rspec-mocks"
gem "rspec-rails", "~> 7.1"
gem "rspec-support"
gem "rubocop", "~> 1.4"
gem "rubocop-performance", require: false
gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
gem "bcrypt", "~> 3.1", require: false
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
gem "sprockets-rails"
gem "sqlite3", "~> 2.3", platform: [:ruby, :mswin, :mingw, :x64_mingw]
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw]
gem "timecop"
gemspec path: "../"
================================================
FILE: lib/doorkeeper/config/abstract_builder.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class Config
# Abstract base class for Doorkeeper and it's extensions configuration
# builder. Instantiates and validates gem configuration.
#
class AbstractBuilder
attr_reader :config
# @param [Class] config class
#
def initialize(config = Config.new, &block)
@config = config
instance_eval(&block) if block_given?
end
# Builds and validates configuration.
#
# @return [Doorkeeper::Config] config instance
#
def build
@config.validate! if @config.respond_to?(:validate!)
@config
end
end
end
end
================================================
FILE: lib/doorkeeper/config/option.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class Config
# Doorkeeper configuration option DSL
module Option
# Defines configuration option
#
# When you call option, it defines two methods. One method will take place
# in the +Config+ class and the other method will take place in the
# +Builder+ class.
#
# The +name+ parameter will set both builder method and config attribute.
# If the +:as+ option is defined, the builder method will be the specified
# option while the config attribute will be the +name+ parameter.
#
# If you want to introduce another level of config DSL you can
# define +builder_class+ parameter.
# Builder should take a block as the initializer parameter and respond to function +build+
# that returns the value of the config attribute.
#
# ==== Options
#
# * [:+as+] Set the builder method that goes inside +configure+ block
# * [+:default+] The default value in case no option was set
# * [+:builder_class+] Configuration option builder class
#
# ==== Examples
#
# option :name
# option :name, as: :set_name
# option :name, default: 'My Name'
# option :scopes builder_class: ScopesBuilder
#
def option(name, options = {})
attribute = options[:as] || name
attribute_builder = options[:builder_class]
builder_class.instance_eval do
if method_defined?(name)
Kernel.warn "[DOORKEEPER] Option #{name} already defined and will be overridden"
remove_method name
end
define_method name do |*args, &block|
if (deprecation_opts = options[:deprecated])
warning = "[DOORKEEPER] #{name} has been deprecated and will soon be removed"
warning = "#{warning}\n#{deprecation_opts.fetch(:message)}" if deprecation_opts.is_a?(Hash)
Kernel.warn(warning)
end
value = if attribute_builder
attribute_builder.new(&block).build
else
block || args.first
end
@config.instance_variable_set(:"@#{attribute}", value)
end
end
define_method attribute do |*_args|
if instance_variable_defined?(:"@#{attribute}")
instance_variable_get(:"@#{attribute}")
else
options[:default]
end
end
public attribute
end
def self.extended(base)
return if base.respond_to?(:builder_class)
raise Doorkeeper::MissingConfigurationBuilderClass, "Define `self.builder_class` method " \
"for #{base} that returns your custom Builder class to use options DSL!"
end
end
end
end
================================================
FILE: lib/doorkeeper/config/validations.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class Config
# Doorkeeper configuration validator.
#
module Validations
# Validates configuration options to be set properly.
#
def validate!
validate_reuse_access_token_value
validate_token_reuse_limit
validate_secret_strategies
validate_pkce_code_challenge_methods
end
private
# Determine whether +reuse_access_token+ and a non-restorable
# +token_secret_strategy+ have both been activated.
#
# In that case, disable reuse_access_token value and warn the user.
def validate_reuse_access_token_value
strategy = token_secret_strategy
return if !reuse_access_token || strategy.allows_restoring_secrets?
::Rails.logger.warn(
"[DOORKEEPER] You have configured both reuse_access_token " \
"AND '#{strategy}' strategy which cannot restore tokens. " \
"This combination is unsupported. reuse_access_token will be disabled",
)
@reuse_access_token = false
end
# Validate that the provided strategies are valid for
# tokens and applications
def validate_secret_strategies
token_secret_strategy.validate_for(:token)
application_secret_strategy.validate_for(:application)
end
def validate_token_reuse_limit
return if !reuse_access_token ||
(token_reuse_limit > 0 && token_reuse_limit <= 100)
::Rails.logger.warn(
"[DOORKEEPER] You have configured an invalid value for token_reuse_limit option. " \
"It will be set to default 100",
)
@token_reuse_limit = 100
end
def validate_pkce_code_challenge_methods
return if pkce_code_challenge_methods.all? {|method| method =~ /^plain$|^S256$/ }
::Rails.logger.warn(
"[DOORKEEPER] You have configured an invalid value for pkce_code_challenge_methods option. " \
"It will be set to default ['plain', 'S256']",
)
@pkce_code_challenge_methods = ['plain', 'S256']
end
end
end
end
================================================
FILE: lib/doorkeeper/config.rb
================================================
# frozen_string_literal: true
require "doorkeeper/config/abstract_builder"
require "doorkeeper/config/option"
require "doorkeeper/config/validations"
module Doorkeeper
# Doorkeeper option DSL could be reused in extensions to build their own
# configurations. To use the Option DSL gems need to define `builder_class` method
# that returns configuration Builder class. This exception raises when they don't
# define it.
#
class Config
# Default Doorkeeper configuration builder
class Builder < AbstractBuilder
# Provide support for an owner to be assigned to each registered
# application (disabled by default)
# Optional parameter confirmation: true (default false) if you want
# to enforce ownership of a registered application
#
# @param opts [Hash] the options to confirm if an application owner
# is present
# @option opts[Boolean] :confirmation (false)
# Set confirm_application_owner variable
def enable_application_owner(opts = {})
@config.instance_variable_set(:@enable_application_owner, true)
confirm_application_owner if opts[:confirmation].present? && opts[:confirmation]
end
def confirm_application_owner
@config.instance_variable_set(:@confirm_application_owner, true)
end
# Provide support for dynamic scopes (e.g. user:*) (disabled by default)
# Optional parameter delimiter (default ":") if you want to customize
# the delimiter separating the scope name and matching value.
#
# @param opts [Hash] the options to configure dynamic scopes
def enable_dynamic_scopes(opts = {})
@config.instance_variable_set(:@enable_dynamic_scopes, true)
@config.instance_variable_set(:@dynamic_scopes_delimiter, opts[:delimiter] || ':')
end
# Define default access token scopes for your provider
#
# @param scopes [Array] Default set of access (OAuth::Scopes.new)
# token scopes
def default_scopes(*scopes)
@config.instance_variable_set(:@default_scopes, OAuth::Scopes.from_array(scopes))
end
# Define default access token scopes for your provider
#
# @param scopes [Array] Optional set of access (OAuth::Scopes.new)
# token scopes
def optional_scopes(*scopes)
@config.instance_variable_set(:@optional_scopes, OAuth::Scopes.from_array(scopes))
end
# Define scopes_by_grant_type to limit certain scope to certain grant_type
# @param { Hash } with grant_types as keys.
# Default set to {} i.e. no limitation on scopes usage
def scopes_by_grant_type(hash = {})
@config.instance_variable_set(:@scopes_by_grant_type, hash)
end
# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
# falls back to the `:client_id` and `:client_secret` params from the
# `params` object.
#
# @param methods [Array] Define client credentials
def client_credentials(*methods)
@config.instance_variable_set(:@client_credentials_methods, methods)
end
# Change the way access token is authenticated from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
# falls back to the `:access_token` or `:bearer_token` params from the
# `params` object.
#
# @param methods [Array] Define access token methods
def access_token_methods(*methods)
@config.instance_variable_set(:@access_token_methods, methods)
end
# Issue access tokens with refresh token (disabled if not set)
def use_refresh_token(enabled = true, &block)
@config.instance_variable_set(
:@refresh_token_enabled,
block || enabled,
)
end
# Reuse access token for the same resource owner within an application
# (disabled by default)
# Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
def reuse_access_token
@config.instance_variable_set(:@reuse_access_token, true)
end
# Enable support for multiple database configurations with read replicas.
# When enabled, wraps database write operations to ensure they use the primary
# (writable) database when automatic role switching is enabled.
#
# For ActiveRecord (Rails 6.1+), this uses `ActiveRecord::Base.connected_to(role: :writing)`.
# Other ORM extensions can implement their own primary database targeting logic.
#
# This prevents `ActiveRecord::ReadOnlyError` when using read replicas with Rails
# automatic role switching. Enable this if your application uses multiple databases
# with automatic role switching for read replicas.
#
# See: https://guides.rubyonrails.org/active_record_multiple_databases.html#activating-automatic-role-switching
def enable_multiple_database_roles
@config.instance_variable_set(:@enable_multiple_database_roles, true)
end
# Choose to use the url path for native autorization codes
# Enabling this flag sets the authorization code response route for
# native redirect uris to oauth/authorize/. The default is
# oauth/authorize/native?code=.
# Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1143
def use_url_path_for_native_authorization
@config.instance_variable_set(:@use_url_path_for_native_authorization, true)
end
# TODO: maybe make it more generic for other flows too?
# Only allow one valid access token obtained via client credentials
# per client. If a new access token is obtained before the old one
# expired, the old one gets revoked (disabled by default)
def revoke_previous_client_credentials_token
@config.instance_variable_set(:@revoke_previous_client_credentials_token, true)
end
# Only allow one valid access token obtained via authorization code
# per client. If a new access token is obtained before the old one
# expired, the old one gets revoked (disabled by default)
def revoke_previous_authorization_code_token
@config.instance_variable_set(:@revoke_previous_authorization_code_token, true)
end
# Require non-confidential apps to use PKCE (send a code_verifier) when requesting
# an access_token using an authorization code (disabled by default)
def force_pkce
@config.instance_variable_set(:@force_pkce, true)
end
# Use an API mode for applications generated with --api argument
# It will skip applications controller, disable forgery protection
def api_only
@config.instance_variable_set(:@api_only, true)
end
# Enables polymorphic Resource Owner association for Access Grant and
# Access Token models. Requires additional database columns to be setup.
def use_polymorphic_resource_owner
@config.instance_variable_set(:@polymorphic_resource_owner, true)
end
# Forbids creating/updating applications with arbitrary scopes that are
# not in configuration, i.e. `default_scopes` or `optional_scopes`.
# (disabled by default)
def enforce_configured_scopes
@config.instance_variable_set(:@enforce_configured_scopes, true)
end
# Enforce request content type as the spec requires:
# disabled by default for backward compatibility.
def enforce_content_type
@config.instance_variable_set(:@enforce_content_type, true)
end
# Allow optional hashing of input tokens before persisting them.
# Will be used for hashing of input token and grants.
#
# @param using
# Provide a different secret storage implementation class for tokens
# @param fallback
# Provide a fallback secret storage implementation class for tokens
# or use :plain to fallback to plain tokens
def hash_token_secrets(using: nil, fallback: nil)
default = "::Doorkeeper::SecretStoring::Sha256Hash"
configure_secrets_for :token,
using: using || default,
fallback: fallback
end
# Allow optional hashing of application secrets before persisting them.
# Will be used for hashing of input token and grants.
#
# @param using
# Provide a different secret storage implementation for applications
# @param fallback
# Provide a fallback secret storage implementation for applications
# or use :plain to fallback to plain application secrets
def hash_application_secrets(using: nil, fallback: nil)
default = "::Doorkeeper::SecretStoring::Sha256Hash"
configure_secrets_for :application,
using: using || default,
fallback: fallback
end
private
# Configure the secret storing functionality
def configure_secrets_for(type, using:, fallback:)
raise ArgumentError, "Invalid type #{type}" if %i[application token].exclude?(type)
@config.instance_variable_set(:"@#{type}_secret_strategy", using.constantize)
if fallback.nil?
return
elsif fallback.to_sym == :plain
fallback = "::Doorkeeper::SecretStoring::Plain"
end
@config.instance_variable_set(:"@#{type}_secret_fallback_strategy", fallback.constantize)
end
end
# Replace with `default: Builder` when we drop support of Rails < 5.2
mattr_reader(:builder_class) { Builder }
extend Option
include Validations
option :resource_owner_authenticator,
as: :authenticate_resource_owner,
default: (lambda do |_routes|
::Rails.logger.warn(
I18n.t("doorkeeper.errors.messages.resource_owner_authenticator_not_configured"),
)
nil
end)
option :admin_authenticator,
as: :authenticate_admin,
default: (lambda do |_routes|
::Rails.logger.warn(
I18n.t("doorkeeper.errors.messages.admin_authenticator_not_configured"),
)
head :forbidden
end)
option :resource_owner_from_credentials,
default: (lambda do |_routes|
::Rails.logger.warn(
I18n.t("doorkeeper.errors.messages.credential_flow_not_configured"),
)
nil
end)
# Hooks for authorization
option :before_successful_authorization, default: ->(_controller, _context = nil) {}
option :after_successful_authorization, default: ->(_controller, _context = nil) {}
# Hooks for strategies responses
option :before_successful_strategy_response, default: ->(_request) {}
option :after_successful_strategy_response, default: ->(_request, _response) {}
# Allows to customize Token Introspection response
option :custom_introspection_response, default: ->(_token, _context) { {} }
option :skip_authorization, default: ->(_routes) {}
option :access_token_expires_in, default: 7200
option :custom_access_token_expires_in, default: ->(_context) { nil }
option :authorization_code_expires_in, default: 600
option :orm, default: :active_record
option :native_redirect_uri, default: "urn:ietf:wg:oauth:2.0:oob", deprecated: true
option :grant_flows, default: %w[authorization_code client_credentials]
option :pkce_code_challenge_methods, default: %w[plain S256]
option :handle_auth_errors, default: :render
option :token_lookup_batch_size, default: 10_000
# Sets the token_reuse_limit
# It will be used only when reuse_access_token option in enabled
# By default it will be 100
# It will be used for token reusablity to some threshold percentage
# Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189
option :token_reuse_limit, default: 100
# Don't require client authentication for password grants. If client credentials
# are present they will still be validated, and the grant rejected if the credentials
# are invalid.
#
# This is discouraged. Spec says that password grants always require a client.
#
# See https://github.com/doorkeeper-gem/doorkeeper/issues/1412#issuecomment-632750422
# and https://github.com/doorkeeper-gem/doorkeeper/pull/1420
#
# Since many applications use this unsafe behavior in the wild, this is kept as a
# not-recommended option. You should be aware that you are not following the OAuth
# spec, and understand the security implications of doing so.
option :skip_client_authentication_for_password_grant,
default: false
# Hook to allow arbitrary user-client authorization
option :authorize_resource_owner_for_client,
default: ->(_client, _resource_owner) { true }
# Allows to customize OAuth grant flows that +each+ application support.
# You can configure a custom block (or use a class respond to `#call`) that must
# return `true` in case Application instance supports requested OAuth grant flow
# during the authorization request to the server. This configuration +doesn't+
# set flows per application, it only allows to check if application supports
# specific grant flow.
#
# For example you can add an additional database column to `oauth_applications` table,
# say `t.array :grant_flows, default: []`, and store allowed grant flows that can
# be used with this application there. Then when authorization requested Doorkeeper
# will call this block to check if specific Application (passed with client_id and/or
# client_secret) is allowed to perform the request for the specific grant type
# (authorization, password, client_credentials, etc).
#
# Example of the block:
#
# ->(flow, client) { client.grant_flows.include?(flow) }
#
# In case this option invocation result is `false`, Doorkeeper server returns
# :unauthorized_client error and stops the request.
#
# @param allow_grant_flow_for_client [Proc] Block or any object respond to #call
# @return [Boolean] `true` if allow or `false` if forbid the request
#
option :allow_grant_flow_for_client, default: ->(_grant_flow, _client) { true }
# Allows to forbid specific Application redirect URI's by custom rules.
# Doesn't forbid any URI by default.
#
# @param forbid_redirect_uri [Proc] Block or any object respond to #call
#
option :forbid_redirect_uri, default: ->(_uri) { false }
# WWW-Authenticate Realm (default "Doorkeeper").
#
# @param realm [String] ("Doorkeeper") Authentication realm
#
option :realm, default: "Doorkeeper"
# Forces the usage of the HTTPS protocol in non-native redirect uris
# (enabled by default in non-development environments). OAuth2
# delegates security in communication to the HTTPS protocol so it is
# wise to keep this enabled.
#
# @param [Boolean] boolean_or_block value for the parameter, true by default in
# non-development environment
#
# @yield [uri] Conditional usage of SSL redirect uris.
# @yieldparam [URI] Redirect URI
# @yieldreturn [Boolean] Indicates necessity of usage of the HTTPS protocol
# in non-native redirect uris
#
option :force_ssl_in_redirect_uri, default: !Rails.env.development?
# Use a custom class for generating the access token.
# https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator
#
# @param access_token_generator [String]
# the name of the access token generator class
#
option :access_token_generator,
default: "Doorkeeper::OAuth::Helpers::UniqueToken"
# Allows additional data to be received when granting access to an Application, and for this
# additional data to be sent with subsequently generated access tokens. The access grant and
# access token models will both need to respond to the specified attribute names.
#
# @param attributes [Array] The array of custom attribute names to be saved
#
option :custom_access_token_attributes,
default: []
# Use a custom class for generating the application secret.
# https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-application-secret-generator
#
# @param application_secret_generator [String]
# the name of the application secret generator class
#
option :application_secret_generator,
default: "Doorkeeper::OAuth::Helpers::UniqueToken"
# Default access token generator is a SecureRandom class from Ruby stdlib.
# This option defines which method will be used to generate a unique token value.
#
# @param default_generator_method [Symbol]
# the method name of the default access token generator
#
option :default_generator_method, default: :urlsafe_base64
# The controller Doorkeeper::ApplicationController inherits from.
# Defaults to ActionController::Base.
# https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-controllers
#
# @param base_controller [String] the name of the base controller
option :base_controller,
default: (lambda do
api_only ? "ActionController::API" : "ActionController::Base"
end)
# The controller Doorkeeper::ApplicationMetalController inherits from.
# Defaults to ActionController::API.
#
# @param base_metal_controller [String] the name of the base controller
option :base_metal_controller,
default: "ActionController::API"
option :access_token_class,
default: "Doorkeeper::AccessToken"
option :access_grant_class,
default: "Doorkeeper::AccessGrant"
option :application_class,
default: "Doorkeeper::Application"
# Allows to set blank redirect URIs for Applications in case
# server configured to use URI-less grant flows.
#
option :allow_blank_redirect_uri,
default: (lambda do |grant_flows, _application|
grant_flows.exclude?("authorization_code") &&
grant_flows.exclude?("implicit")
end)
# Configure protection of token introspection request.
# By default this configuration allows to introspect a token by
# another token of the same application, or to introspect the token
# that belongs to authorized client, or access token has been introspected
# is a public one (doesn't belong to any client)
#
# You can define any custom rule you need or just disable token
# introspection at all.
#
# @param token [Doorkeeper::AccessToken]
# token to be introspected
#
# @param authorized_client [Doorkeeper::Application]
# authorized client (if request is authorized using Basic auth with
# Client Credentials for example)
#
# @param authorized_token [Doorkeeper::AccessToken]
# Bearer token used to authorize the request
#
option :allow_token_introspection,
default: (lambda do |token, authorized_client, authorized_token|
if authorized_token
authorized_token.application == token&.application
elsif token&.application
authorized_client == token.application
else
true
end
end)
attr_reader :reuse_access_token,
:enable_multiple_database_roles,
:token_secret_fallback_strategy,
:application_secret_fallback_strategy
def clear_cache!
%i[
application_model
access_token_model
access_grant_model
].each do |var|
remove_instance_variable("@#{var}") if instance_variable_defined?("@#{var}")
end
end
# Doorkeeper Access Token model class.
#
# @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model]
#
def access_token_model
@access_token_model ||= access_token_class.constantize
end
# Doorkeeper Access Grant model class.
#
# @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model]
#
def access_grant_model
@access_grant_model ||= access_grant_class.constantize
end
# Doorkeeper Application model class.
#
# @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model]
#
def application_model
@application_model ||= application_class.constantize
end
def api_only
@api_only ||= false
end
def enforce_content_type
@enforce_content_type ||= false
end
def refresh_token_enabled?
if defined?(@refresh_token_enabled)
@refresh_token_enabled
else
false
end
end
def resolve_controller(name)
config_option = public_send(:"#{name}_controller")
controller_name = if config_option.respond_to?(:call)
instance_exec(&config_option)
else
config_option
end
controller_name.constantize
end
def revoke_previous_client_credentials_token?
option_set? :revoke_previous_client_credentials_token
end
def revoke_previous_authorization_code_token?
option_set? :revoke_previous_authorization_code_token
end
def force_pkce?
option_set? :force_pkce
end
def enforce_configured_scopes?
option_set? :enforce_configured_scopes
end
def enable_application_owner?
option_set? :enable_application_owner
end
def enable_dynamic_scopes?
option_set? :enable_dynamic_scopes
end
def dynamic_scopes_delimiter
@dynamic_scopes_delimiter
end
def polymorphic_resource_owner?
option_set? :polymorphic_resource_owner
end
def confirm_application_owner?
option_set? :confirm_application_owner
end
def raise_on_errors?
handle_auth_errors == :raise
end
def redirect_on_errors?
handle_auth_errors == :redirect
end
def application_secret_hashed?
instance_variable_defined?(:"@application_secret_strategy")
end
def token_secret_strategy
@token_secret_strategy ||= ::Doorkeeper::SecretStoring::Plain
end
def application_secret_strategy
@application_secret_strategy ||= ::Doorkeeper::SecretStoring::Plain
end
def default_scopes
@default_scopes ||= OAuth::Scopes.new
end
def optional_scopes
@optional_scopes ||= OAuth::Scopes.new
end
def scopes
@scopes ||= default_scopes + optional_scopes
end
def scopes_by_grant_type
@scopes_by_grant_type ||= {}
end
def pkce_code_challenge_methods_supported
return [] unless access_grant_model.pkce_supported?
pkce_code_challenge_methods
end
def client_credentials_methods
@client_credentials_methods ||= %i[from_basic from_params]
end
def access_token_methods
@access_token_methods ||= %i[
from_bearer_authorization
from_access_token_param
from_bearer_param
]
end
def enabled_grant_flows
@enabled_grant_flows ||= calculate_grant_flows.map { |name| Doorkeeper::GrantFlow.get(name) }.compact
end
def authorization_response_flows
@authorization_response_flows ||= enabled_grant_flows.select(&:handles_response_type?) +
deprecated_authorization_flows
end
def token_grant_flows
@token_grant_flows ||= calculate_token_grant_flows
end
def authorization_response_types
authorization_response_flows.map(&:response_type_matches)
end
def token_grant_types
token_grant_flows.map(&:grant_type_matches)
end
# [NOTE]: deprecated and will be removed soon
def deprecated_token_grant_types_resolver
@deprecated_token_grant_types ||= calculate_token_grant_types
end
def native_authorization_code_route
@use_url_path_for_native_authorization = false unless defined?(@use_url_path_for_native_authorization)
@use_url_path_for_native_authorization ? '/:code' : '/native'
end
# [NOTE]: deprecated and will be removed soon
def deprecated_authorization_flows
response_types = calculate_authorization_response_types
if response_types.any?
::Kernel.warn <<~WARNING
Please, don't patch Doorkeeper::Config#calculate_authorization_response_types method.
Register your custom grant flows using the public API:
`Doorkeeper::GrantFlow.register(grant_flow_name, **options)`.
WARNING
end
response_types.map do |response_type|
Doorkeeper::GrantFlow::FallbackFlow.new(response_type, response_type_matches: response_type)
end
end
# [NOTE]: deprecated and will be removed soon
def calculate_authorization_response_types
[]
end
# [NOTE]: deprecated and will be removed soon
def calculate_token_grant_types
types = grant_flows - ["implicit"]
types << "refresh_token" if refresh_token_enabled?
types
end
# Calculates grant flows configured by the user in Doorkeeper
# configuration considering registered aliases that is exposed
# to single or multiple other flows.
#
def calculate_grant_flows
configured_flows = grant_flows.map(&:to_s)
aliases = Doorkeeper::GrantFlow.aliases.keys.map(&:to_s)
flows = configured_flows - aliases
aliases.each do |flow_alias|
next unless configured_flows.include?(flow_alias)
flows.concat(Doorkeeper::GrantFlow.expand_alias(flow_alias))
end
flows.flatten.uniq
end
def allow_blank_redirect_uri?(application = nil)
if allow_blank_redirect_uri.respond_to?(:call)
allow_blank_redirect_uri.call(grant_flows, application)
else
allow_blank_redirect_uri
end
end
def allow_grant_flow_for_client?(grant_flow, client)
return true unless option_defined?(:allow_grant_flow_for_client)
allow_grant_flow_for_client.call(grant_flow, client)
end
def option_defined?(name)
instance_variable_defined?("@#{name}")
end
private
# Helper to read boolearized configuration option
def option_set?(instance_key)
var = instance_variable_get("@#{instance_key}")
!!(defined?(var) && var)
end
def calculate_token_grant_flows
flows = enabled_grant_flows.select(&:handles_grant_type?)
flows << Doorkeeper::GrantFlow.get("refresh_token") if refresh_token_enabled?
flows
end
end
end
================================================
FILE: lib/doorkeeper/engine.rb
================================================
# frozen_string_literal: true
module Doorkeeper
class Engine < Rails::Engine
initializer "doorkeeper.params.filter", after: :load_config_initializers do |app|
app.config.to_prepare do
Doorkeeper.setup_filter_parameters
end
end
initializer "doorkeeper.routes" do
Doorkeeper::Rails::Routes.install!
end
initializer "doorkeeper.helpers" do
ActiveSupport.on_load(:action_controller) do
include Doorkeeper::Rails::Helpers
end
end
config.to_prepare do
Doorkeeper.run_orm_hooks
end
if defined?(Sprockets) && Sprockets::VERSION.chr.to_i >= 4
initializer "doorkeeper.assets.precompile" do |app|
# Force users to use:
# //= link doorkeeper/admin/application.css
# in Doorkeeper 5 for Sprockets 4 instead of precompile.
# Add note to official docs & Wiki
app.config.assets.precompile += %w[
doorkeeper/application.css
doorkeeper/admin/application.css
]
end
end
end
end
================================================
FILE: lib/doorkeeper/errors.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Errors
class DoorkeeperError < StandardError
def type
message
end
def self.translate_options
{}
end
end
class InvalidGrantReuse < DoorkeeperError
def type
:invalid_grant
end
end
class InvalidTokenStrategy < DoorkeeperError
def type
:unsupported_grant_type
end
end
class MissingRequiredParameter < DoorkeeperError
attr_reader :missing_param
def initialize(missing_param)
super
@missing_param = missing_param
end
def type
:invalid_request
end
end
class BaseResponseError < DoorkeeperError
attr_reader :response
def initialize(response)
@response = response
end
def self.name_for_response
self.name.demodulize.underscore.to_sym
end
end
class InvalidCodeChallengeMethod < BaseResponseError
def self.translate_options
challenge_methods = Doorkeeper.config.pkce_code_challenge_methods_supported
{
challenge_methods: challenge_methods.join(", "),
count: challenge_methods.length
}
end
end
UnableToGenerateToken = Class.new(DoorkeeperError)
TokenGeneratorNotFound = Class.new(DoorkeeperError)
NoOrmCleaner = Class.new(DoorkeeperError)
InvalidRequest = Class.new(BaseResponseError)
InvalidToken = Class.new(BaseResponseError)
InvalidClient = Class.new(BaseResponseError)
InvalidScope = Class.new(BaseResponseError)
InvalidRedirectUri = Class.new(BaseResponseError)
InvalidGrant = Class.new(BaseResponseError)
UnauthorizedClient = Class.new(BaseResponseError)
UnsupportedResponseType = Class.new(BaseResponseError)
UnsupportedResponseMode = Class.new(BaseResponseError)
AccessDenied = Class.new(BaseResponseError)
ServerError = Class.new(BaseResponseError)
TokenExpired = Class.new(InvalidToken)
TokenRevoked = Class.new(InvalidToken)
TokenUnknown = Class.new(InvalidToken)
TokenForbidden = Class.new(InvalidToken)
end
end
================================================
FILE: lib/doorkeeper/grant_flow/fallback_flow.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module GrantFlow
class FallbackFlow < Flow
def handles_grant_type?
false
end
def handles_response_type?
false
end
end
end
end
================================================
FILE: lib/doorkeeper/grant_flow/flow.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module GrantFlow
class Flow
attr_reader :name, :grant_type_matches, :grant_type_strategy,
:response_type_matches, :response_type_strategy,
:response_mode_matches
def initialize(name, **options)
@name = name
@grant_type_matches = options[:grant_type_matches]
@grant_type_strategy = options[:grant_type_strategy]
@response_type_matches = options[:response_type_matches]
@response_type_strategy = options[:response_type_strategy]
@response_mode_matches = options[:response_mode_matches]
end
def handles_grant_type?
grant_type_matches.present?
end
def handles_response_type?
response_type_matches.present?
end
def matches_grant_type?(value)
grant_type_matches === value
end
def matches_response_type?(value)
response_type_matches === value
end
def default_response_mode
response_mode_matches[0]
end
def matches_response_mode?(value)
response_mode_matches.include?(value)
end
end
end
end
================================================
FILE: lib/doorkeeper/grant_flow/registry.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module GrantFlow
module Registry
mattr_accessor :flows
self.flows = {}
mattr_accessor :aliases
self.aliases = {}
# Allows to register custom OAuth grant flow so that Doorkeeper
# could recognize and process it.
#
def register(name_or_flow, **options)
unless name_or_flow.is_a?(Doorkeeper::GrantFlow::Flow)
name_or_flow = Flow.new(name_or_flow, **options)
end
flow_key = name_or_flow.name.to_sym
if flows.key?(flow_key)
::Kernel.warn <<~WARNING
[DOORKEEPER] '#{flow_key}' grant flow already registered and will be overridden
in #{caller(1..1).first}
WARNING
end
flows[flow_key] = name_or_flow
end
# Allows to register aliases that could be used in `grant_flows`
# configuration option. It is possible to have aliases like 1:1 or
# 1:N, i.e. "implicit_oidc" => ['token', 'id_token', 'id_token token'].
#
def register_alias(alias_name, **options)
aliases[alias_name.to_sym] = Array.wrap(options.fetch(:as))
end
def expand_alias(alias_name)
aliases.fetch(alias_name.to_sym, [])
end
# [NOTE]: make it to use #fetch after removing fallbacks
def get(name)
flows[name.to_sym]
end
end
end
end
================================================
FILE: lib/doorkeeper/grant_flow.rb
================================================
# frozen_string_literal: true
require "doorkeeper/grant_flow/flow"
require "doorkeeper/grant_flow/fallback_flow"
require "doorkeeper/grant_flow/registry"
module Doorkeeper
module GrantFlow
extend Registry
register(
:implicit,
response_type_matches: "token",
response_mode_matches: %w[fragment form_post],
response_type_strategy: Doorkeeper::Request::Token,
)
register(
:authorization_code,
response_type_matches: "code",
response_mode_matches: %w[query fragment form_post],
response_type_strategy: Doorkeeper::Request::Code,
grant_type_matches: "authorization_code",
grant_type_strategy: Doorkeeper::Request::AuthorizationCode,
)
register(
:client_credentials,
grant_type_matches: "client_credentials",
grant_type_strategy: Doorkeeper::Request::ClientCredentials,
)
register(
:password,
grant_type_matches: "password",
grant_type_strategy: Doorkeeper::Request::Password,
)
register(
:refresh_token,
grant_type_matches: "refresh_token",
grant_type_strategy: Doorkeeper::Request::RefreshToken,
)
end
end
================================================
FILE: lib/doorkeeper/grape/authorization_decorator.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Grape
class AuthorizationDecorator < SimpleDelegator
def parameters
params
end
def authorization
env = __getobj__.env
env["HTTP_AUTHORIZATION"] ||
env["X-HTTP_AUTHORIZATION"] ||
env["X_HTTP_AUTHORIZATION"] ||
env["REDIRECT_X_HTTP_AUTHORIZATION"]
end
end
end
end
================================================
FILE: lib/doorkeeper/grape/helpers.rb
================================================
# frozen_string_literal: true
require "doorkeeper/grape/authorization_decorator"
module Doorkeeper
module Grape
# Doorkeeper helpers for Grape applications.
# Provides helpers for endpoints authorization based on defined set of scopes.
module Helpers
# These helpers are for grape >= 0.10
extend ::Grape::API::Helpers
include Doorkeeper::Rails::Helpers
# endpoint specific scopes > parameter scopes > default scopes
def doorkeeper_authorize!(*scopes)
endpoint_scopes = endpoint.route_setting(:scopes) ||
endpoint.options[:route_options][:scopes]
scopes = if endpoint_scopes
Doorkeeper::OAuth::Scopes.from_array(endpoint_scopes)
elsif scopes.present?
Doorkeeper::OAuth::Scopes.from_array(scopes)
end
super(*scopes)
end
def doorkeeper_render_error_with(error)
status_code = error_status_codes[error.status]
error!({ error: error.description }, status_code, error.headers)
end
private
def endpoint
env["api.endpoint"]
end
def doorkeeper_token
@doorkeeper_token ||= OAuth::Token.authenticate(
decorated_request,
*Doorkeeper.config.access_token_methods,
)
end
def decorated_request
AuthorizationDecorator.new(request)
end
def error_status_codes
{
unauthorized: 401,
forbidden: 403,
}
end
end
end
end
================================================
FILE: lib/doorkeeper/helpers/controller.rb
================================================
# frozen_string_literal: true
# Define methods that can be called in any controller that inherits from
# Doorkeeper::ApplicationMetalController or Doorkeeper::ApplicationController
module Doorkeeper
module Helpers
# Rails controller helpers.
#
module Controller
def self.included(base)
base.helper_method :current_resource_owner if base.respond_to?(:helper_method)
end
private
# :doc:
def authenticate_resource_owner!
current_resource_owner
end
# :doc:
def current_resource_owner
return @current_resource_owner if defined?(@current_resource_owner)
@current_resource_owner ||= begin
instance_eval(&Doorkeeper.config.authenticate_resource_owner)
end
end
def resource_owner_from_credentials
instance_eval(&Doorkeeper.config.resource_owner_from_credentials)
end
# :doc:
def authenticate_admin!
instance_eval(&Doorkeeper.config.authenticate_admin)
end
def server
@server ||= Server.new(self)
end
# :doc:
def doorkeeper_token
return @doorkeeper_token if defined?(@doorkeeper_token)
@doorkeeper_token ||= OAuth::Token.authenticate(request, *config_methods)
end
def config_methods
@config_methods ||= Doorkeeper.config.access_token_methods
end
def get_error_response_from_exception(exception)
if exception.respond_to?(:response)
exception.response
elsif exception.type == :invalid_request
OAuth::InvalidRequestResponse.new(
name: exception.type,
state: params[:state],
missing_param: exception.missing_param,
)
else
OAuth::ErrorResponse.new(name: exception.type, state: params[:state])
end
end
def handle_token_exception(exception)
error = get_error_response_from_exception(exception)
headers.merge!(error.headers)
self.response_body = error.body.to_json
self.status = error.status
end
def skip_authorization?
!!instance_exec(
[server.current_resource_owner, @pre_auth.client],
&Doorkeeper.config.skip_authorization
)
end
def enforce_content_type
if (request.put? || request.post? || request.patch?) && !x_www_form_urlencoded?
render json: {}, status: :unsupported_media_type
end
end
def x_www_form_urlencoded?
request.media_type == "application/x-www-form-urlencoded"
end
end
end
end
================================================
FILE: lib/doorkeeper/models/access_grant_mixin.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module AccessGrantMixin
extend ActiveSupport::Concern
include OAuth::Helpers
include Models::Expirable
include Models::Revocable
include Models::Accessible
include Models::Orderable
include Models::SecretStorable
include Models::Scopes
include Models::ResourceOwnerable
include Models::Concerns::WriteToPrimary
include Models::ExpirationTimeSqlMath
# Never uses PKCE if PKCE migrations were not generated
def uses_pkce?
self.class.pkce_supported? && code_challenge.present?
end
module ClassMethods
# Searches for Doorkeeper::AccessGrant record with the
# specific token value.
#
# @param token [#to_s] token value (any object that responds to `#to_s`)
#
# @return [Doorkeeper::AccessGrant, nil]
# AccessGrant object or nil if there is no record with such token
#
def by_token(token)
find_by_plaintext_token(:token, token)
end
# Revokes AccessGrant records that have not been revoked and associated
# with the specific Application and Resource Owner.
#
# @param application_id [Integer]
# ID of the Application
# @param resource_owner [ActiveRecord::Base, Integer]
# instance of the Resource Owner model or it's ID
#
def revoke_all_for(application_id, resource_owner, clock = Time)
with_primary_role do
by_resource_owner(resource_owner)
.where(
application_id: application_id,
revoked_at: nil,
)
.update_all(revoked_at: clock.now.utc)
end
end
# Implements PKCE code_challenge encoding without base64 padding as described in the spec.
# https://datatracker.ietf.org/doc/html/rfc7636#appendix-A
# Appendix A. Notes on Implementing Base64url Encoding without Padding
#
# This appendix describes how to implement a base64url-encoding
# function without padding, based upon the standard base64-encoding
# function that uses padding.
#
# To be concrete, example C# code implementing these functions is shown
# below. Similar code could be used in other languages.
#
# static string base64urlencode(byte [] arg)
# {
# string s = Convert.ToBase64String(arg); // Regular base64 encoder
# s = s.Split('=')[0]; // Remove any trailing '='s
# s = s.Replace('+', '-'); // 62nd char of encoding
# s = s.Replace('/', '_'); // 63rd char of encoding
# return s;
# }
#
# An example correspondence between unencoded and encoded values
# follows. The octet sequence below encodes into the string below,
# which when decoded, reproduces the octet sequence.
#
# 3 236 255 224 193
#
# A-z_4ME
#
# https://ruby-doc.org/stdlib-2.1.3/libdoc/base64/rdoc/Base64.html#method-i-urlsafe_encode64
#
# urlsafe_encode64(bin)
# Returns the Base64-encoded version of bin. This method complies with
# "Base 64 Encoding with URL and Filename Safe Alphabet" in RFC 4648.
# The alphabet uses '-' instead of '+' and '_' instead of '/'.
# @param code_verifier [#to_s] a one time use value (any object that responds to `#to_s`)
#
# @return [#to_s] An encoded code challenge based on the provided verifier
# suitable for PKCE validation
#
def generate_code_challenge(code_verifier)
Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
end
def pkce_supported?
column_names.include?("code_challenge")
end
##
# Determines the secret storing transformer
# Unless configured otherwise, uses the plain secret strategy
#
# @return [Doorkeeper::SecretStoring::Base]
#
def secret_strategy
::Doorkeeper.config.token_secret_strategy
end
##
# Determine the fallback storing strategy
# Unless configured, there will be no fallback
#
# @return [Doorkeeper::SecretStoring::Base]
#
def fallback_secret_strategy
::Doorkeeper.config.token_secret_fallback_strategy
end
end
end
end
================================================
FILE: lib/doorkeeper/models/access_token_mixin.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module AccessTokenMixin
extend ActiveSupport::Concern
include OAuth::Helpers
include Models::Expirable
include Models::Reusable
include Models::Revocable
include Models::Accessible
include Models::Orderable
include Models::SecretStorable
include Models::Scopes
include Models::ResourceOwnerable
include Models::ExpirationTimeSqlMath
include Models::Concerns::WriteToPrimary
module ClassMethods
# Returns an instance of the Doorkeeper::AccessToken with
# specific plain text token value.
#
# @param token [#to_s]
# Plain text token value (any object that responds to `#to_s`)
#
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
# if there is no record with such token
#
def by_token(token)
find_by_plaintext_token(:token, token)
end
# Returns an instance of the Doorkeeper::AccessToken
# with specific token value.
#
# @param refresh_token [#to_s]
# refresh token value (any object that responds to `#to_s`)
#
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
# if there is no record with such refresh token
#
def by_refresh_token(refresh_token)
find_by_plaintext_token(:refresh_token, refresh_token)
end
# Returns an instance of the Doorkeeper::AccessToken
# found by previous refresh token. Keep in mind that value
# of the previous_refresh_token isn't encrypted using
# secrets strategy.
#
# @param previous_refresh_token [#to_s]
# previous refresh token value (any object that responds to `#to_s`)
#
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
# if there is no record with such refresh token
#
def by_previous_refresh_token(previous_refresh_token)
find_by(refresh_token: previous_refresh_token)
end
# Revokes AccessToken records that have not been revoked and associated
# with the specific Application and Resource Owner.
#
# @param application_id [Integer]
# ID of the Application
# @param resource_owner [ActiveRecord::Base, Integer]
# instance of the Resource Owner model or it's ID
#
def revoke_all_for(application_id, resource_owner, clock = Time)
with_primary_role do
by_resource_owner(resource_owner)
.where(
application_id: application_id,
revoked_at: nil,
)
.update_all(revoked_at: clock.now.utc)
end
end
# Looking for not revoked Access Token with a matching set of scopes
# that belongs to specific Application and Resource Owner.
#
# @param application [Doorkeeper::Application]
# Application instance
# @param resource_owner [ActiveRecord::Base, Integer]
# Resource Owner model instance or it's ID
# @param scopes [String, Doorkeeper::OAuth::Scopes]
# set of scopes
# @param custom_attributes [Nilable Hash]
# A nil value, or hash with keys corresponding to the custom attributes
# configured with the `custom_access_token_attributes` config option.
# A nil value will ignore custom attributes.
#
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
# nil if matching record was not found
#
def matching_token_for(application, resource_owner, scopes, custom_attributes: nil, include_expired: true)
tokens = authorized_tokens_for(application&.id, resource_owner)
tokens = tokens.not_expired unless include_expired
find_matching_token(tokens, application, custom_attributes, scopes)
end
# Interface to enumerate access token records in batches in order not
# to bloat the memory. Could be overloaded in any ORM extension.
#
def find_access_token_in_batches(relation, **args, &block)
relation.find_in_batches(**args, &block)
end
# Enumerates AccessToken records in batches to find a matching token.
# Batching is required in order not to pollute the memory if Application
# has huge amount of associated records.
#
# ActiveRecord 5.x - 6.x ignores custom ordering so we can't perform a
# database sort by created_at, so we need to load all the matching records,
# sort them and find latest one.
#
# @param relation [ActiveRecord::Relation]
# Access tokens relation
# @param application [Doorkeeper::Application]
# Application instance
# @param scopes [String, Doorkeeper::OAuth::Scopes]
# set of scopes
# @param custom_attributes [Nilable Hash]
# A nil value, or hash with keys corresponding to the custom attributes
# configured with the `custom_access_token_attributes` config option.
# A nil value will ignore custom attributes.
#
# @return [Doorkeeper::AccessToken, nil] Access Token instance or
# nil if matching record was not found
#
def find_matching_token(relation, application, custom_attributes, scopes)
return nil unless relation
matching_tokens = []
batch_size = Doorkeeper.configuration.token_lookup_batch_size
find_access_token_in_batches(relation, batch_size: batch_size) do |batch|
tokens = batch.select do |token|
scopes_match?(token.scopes, scopes, application&.scopes) &&
custom_attributes_match?(token, custom_attributes)
end
matching_tokens.concat(tokens)
end
matching_tokens.max_by(&:created_at)
end
# Checks whether the token scopes match the scopes from the parameters
#
# @param token_scopes [#to_s]
# set of scopes (any object that responds to `#to_s`)
# @param param_scopes [Doorkeeper::OAuth::Scopes]
# scopes from params
# @param app_scopes [Doorkeeper::OAuth::Scopes]
# Application scopes
#
# @return [Boolean] true if the param scopes match the token scopes,
# and all the param scopes are defined in the application (or in the
# server configuration if the application doesn't define any scopes),
# and false in other cases
#
def scopes_match?(token_scopes, param_scopes, app_scopes)
return true if token_scopes.empty? && param_scopes.empty?
(token_scopes.sort == param_scopes.sort) &&
Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(
scope_str: param_scopes.to_s,
server_scopes: Doorkeeper.config.scopes,
app_scopes: app_scopes,
)
end
# Checks whether the token custom attribute values match the custom
# attributes from the parameters.
#
# @param token [Doorkeeper::AccessToken]
# The access token whose custom attributes are being compared
# to the custom_attributes.
#
# @param custom_attributes [Hash]
# A hash of the attributes for which we want to determine whether
# the token's custom attributes match.
#
# @return [Boolean] true if the token's custom attribute values
# match those in the custom_attributes, or if both are empty/blank.
# False otherwise.
def custom_attributes_match?(token, custom_attributes)
return true if custom_attributes.nil?
token_attribs = token.custom_attributes
return true if token_attribs.blank? && custom_attributes.blank?
Doorkeeper.config.custom_access_token_attributes.all? do |attribute|
token_attribs[attribute] == custom_attributes[attribute]
end
end
# Looking for not expired AccessToken record with a matching set of
# scopes that belongs to specific Application and Resource Owner.
# If it doesn't exists - then creates it.
#
# @param application [Doorkeeper::Application]
# Application instance
# @param resource_owner [ActiveRecord::Base, Integer]
# Resource Owner model instance or it's ID
# @param scopes [#to_s]
# set of scopes (any object that responds to `#to_s`)
# @param token_attributes [Hash]
# Additional attributes to use when creating a token
# @option token_attributes [Integer] :expires_in
# token lifetime in seconds
# @option token_attributes [Boolean] :use_refresh_token
# whether to use the refresh token
#
# @return [Doorkeeper::AccessToken] existing record or a new one
#
def find_or_create_for(application:, resource_owner:, scopes:, **token_attributes)
scopes = Doorkeeper::OAuth::Scopes.from_string(scopes) if scopes.is_a?(String)
if Doorkeeper.config.reuse_access_token
custom_attributes = extract_custom_attributes(token_attributes).presence
access_token = matching_token_for(
application, resource_owner, scopes, custom_attributes: custom_attributes, include_expired: false)
return access_token if access_token&.reusable?
end
create_for(
application: application,
resource_owner: resource_owner,
scopes: scopes,
**token_attributes,
)
end
# Creates a not expired AccessToken record with a matching set of
# scopes that belongs to specific Application and Resource Owner.
#
# @param application [Doorkeeper::Application]
# Application instance
# @param resource_owner [ActiveRecord::Base, Integer]
# Resource Owner model instance or it's ID
# @param scopes [#to_s]
# set of scopes (any object that responds to `#to_s`)
# @param token_attributes [Hash]
# Additional attributes to use when creating a token
# @option token_attributes [Integer] :expires_in
# token lifetime in seconds
# @option token_attributes [Boolean] :use_refresh_token
# whether to use the refresh token
#
# @return [Doorkeeper::AccessToken] new access token
#
def create_for(application:, resource_owner:, scopes:, **token_attributes)
token_attributes[:application] = application
token_attributes[:scopes] = scopes.to_s
if Doorkeeper.config.polymorphic_resource_owner?
token_attributes[:resource_owner] = resource_owner
else
token_attributes[:resource_owner_id] = resource_owner_id_for(resource_owner)
end
with_primary_role do
create!(token_attributes)
end
end
# Looking for not revoked Access Token records that belongs to specific
# Application and Resource Owner.
#
# @param application_id [Integer]
# ID of the Application model instance
# @param resource_owner [ActiveRecord::Base, Integer]
# Resource Owner model instance or it's ID
#
# @return [ActiveRecord::Relation]
# collection of matching AccessToken objects
#
def authorized_tokens_for(application_id, resource_owner)
by_resource_owner(resource_owner).where(
application_id: application_id,
revoked_at: nil,
)
end
# Convenience method for backwards-compatibility, return the last
# matching token for the given Application and Resource Owner.
#
# @param application_id [Integer]
# ID of the Application model instance
# @param resource_owner [ActiveRecord::Base, Integer]
# ID of the Resource Owner model instance
#
# @return [Doorkeeper::AccessToken, nil] matching AccessToken object or
# nil if nothing was found
#
def last_authorized_token_for(application_id, resource_owner)
authorized_tokens_for(application_id, resource_owner)
.ordered_by(:created_at, :desc)
.first
end
##
# Determines the secret storing transformer
# Unless configured otherwise, uses the plain secret strategy
#
# @return [Doorkeeper::SecretStoring::Base]
#
def secret_strategy
::Doorkeeper.config.token_secret_strategy
end
##
# Determine the fallback storing strategy
# Unless configured, there will be no fallback
def fallback_secret_strategy
::Doorkeeper.config.token_secret_fallback_strategy
end
# Extracts the token's custom attributes (defined by the
# custom_access_token_attributes config option) from the token's attributes.
#
# @param attributes [Hash]
# A hash of the access token's attributes.
# @return [Hash]
# A hash containing only the custom access token attributes.
def extract_custom_attributes(attributes)
attributes.with_indifferent_access.slice(
*Doorkeeper.configuration.custom_access_token_attributes)
end
end
# Access Token type: Bearer.
# @see https://datatracker.ietf.org/doc/html/rfc6750
# The OAuth 2.0 Authorization Framework: Bearer Token Usage
#
def token_type
"Bearer"
end
def use_refresh_token?
@use_refresh_token ||= false
!!@use_refresh_token
end
# JSON representation of the Access Token instance.
#
# @return [Hash] hash with token data
def as_json(_options = {})
{
resource_owner_id: resource_owner_id,
scope: scopes,
expires_in: expires_in_seconds,
application: { uid: application.try(:uid) },
created_at: created_at.to_i,
}.tap do |json|
if Doorkeeper.configuration.polymorphic_resource_owner?
json[:resource_owner_type] = resource_owner_type
end
end
end
# The token's custom attributes, as defined by
# the custom_access_token_attributes config option.
#
# @return [Hash] hash of custom access token attributes.
def custom_attributes
self.class.extract_custom_attributes(attributes)
end
# Indicates whether the token instance have the same credential
# as the other Access Token.
#
# @param access_token [Doorkeeper::AccessToken] other token
#
# @return [Boolean] true if credentials are same of false in other cases
#
def same_credential?(access_token)
application_id == access_token.application_id &&
same_resource_owner?(access_token)
end
# Indicates whether the token instance have the same credential
# as the other Access Token.
#
# @param access_token [Doorkeeper::AccessToken] other token
#
# @return [Boolean] true if credentials are same of false in other cases
#
def same_resource_owner?(access_token)
if Doorkeeper.configuration.polymorphic_resource_owner?
resource_owner == access_token.resource_owner
else
resource_owner_id == access_token.resource_owner_id
end
end
# Indicates if token is acceptable for specific scopes.
#
# @param scopes [Array] scopes
#
# @return [Boolean] true if record is accessible and includes scopes or
# false in other cases
#
def acceptable?(scopes)
accessible? && includes_scope?(*scopes)
end
# We keep a volatile copy of the raw refresh token for initial communication
# The stored refresh_token may be mapped and not available in cleartext.
def plaintext_refresh_token
if secret_strategy.allows_restoring_secrets?
secret_strategy.restore_secret(self, :refresh_token)
else
@raw_refresh_token
end
end
# We keep a volatile copy of the raw token for initial communication
# The stored refresh_token may be mapped and not available in cleartext.
#
# Some strategies allow restoring stored secrets (e.g. symmetric encryption)
# while hashing strategies do not, so you cannot rely on this value
# returning a present value for persisted tokens.
def plaintext_token
if secret_strategy.allows_restoring_secrets?
secret_strategy.restore_secret(self, :token)
else
@raw_token
end
end
# Revokes token with `:refresh_token` equal to `:previous_refresh_token`
# and clears `:previous_refresh_token` attribute.
#
def revoke_previous_refresh_token!
return if !self.class.refresh_token_revoked_on_use? || previous_refresh_token.blank?
old_refresh_token&.revoke
if self.class.respond_to?(:with_primary_role)
self.class.with_primary_role { update_attribute(:previous_refresh_token, "") }
else
update_attribute(:previous_refresh_token, "")
end
end
private
# Searches for Access Token record with `:refresh_token` equal to
# `:previous_refresh_token` value.
#
# @return [Doorkeeper::AccessToken, nil]
# Access Token record or nil if nothing found
#
def old_refresh_token
@old_refresh_token ||= self.class.by_previous_refresh_token(previous_refresh_token)
end
# Generates refresh token with UniqueToken generator.
#
# @return [String] refresh token value
#
def generate_refresh_token
@raw_refresh_token = UniqueToken.generate
secret_strategy.store_secret(self, :refresh_token, @raw_refresh_token)
end
# Generates and sets the token value with the
# configured Generator class (see Doorkeeper.config).
#
# @return [String] generated token value
#
# @raise [Doorkeeper::Errors::UnableToGenerateToken]
# custom class doesn't implement .generate method
# @raise [Doorkeeper::Errors::TokenGeneratorNotFound]
# custom class doesn't exist
#
def generate_token
self.created_at ||= Time.now.utc
@raw_token = token_generator.generate(attributes_for_token_generator)
secret_strategy.store_secret(self, :token, @raw_token)
@raw_token
end
# Set of attributes that would be passed to token generator to
# generate unique token based on them.
#
# @return [Hash] set of attributes
#
def attributes_for_token_generator
{
resource_owner_id: resource_owner_id,
scopes: scopes,
application: application,
expires_in: expires_in,
created_at: created_at,
}.tap do |attributes|
if Doorkeeper.config.polymorphic_resource_owner?
attributes[:resource_owner] = resource_owner
end
Doorkeeper.config.custom_access_token_attributes.each do |attribute_name|
attributes[attribute_name] = public_send(attribute_name)
end
end
end
def token_generator
generator_name = Doorkeeper.config.access_token_generator
generator = generator_name.constantize
return generator if generator.respond_to?(:generate)
raise Errors::UnableToGenerateToken, "#{generator} does not respond to `.generate`."
rescue NameError
raise Errors::TokenGeneratorNotFound, "#{generator_name} not found"
end
end
end
================================================
FILE: lib/doorkeeper/models/application_mixin.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module ApplicationMixin
extend ActiveSupport::Concern
include OAuth::Helpers
include Models::Orderable
include Models::SecretStorable
include Models::Scopes
# :nodoc
module ClassMethods
# Returns an instance of the Doorkeeper::Application with
# specific UID and secret.
#
# Public/Non-confidential applications will only find by uid if secret is
# blank.
#
# @param uid [#to_s] UID (any object that responds to `#to_s`)
# @param secret [#to_s] secret (any object that responds to `#to_s`)
#
# @return [Doorkeeper::Application, nil]
# Application instance or nil if there is no record with such credentials
#
def by_uid_and_secret(uid, secret)
app = by_uid(uid)
return unless app
return app if secret.blank? && !app.confidential?
return unless app.secret_matches?(secret)
app
end
# Returns an instance of the Doorkeeper::Application with specific UID.
#
# @param uid [#to_s] UID (any object that responds to `#to_s`)
#
# @return [Doorkeeper::Application, nil] Application instance or nil
# if there is no record with such UID
#
def by_uid(uid)
find_by(uid: uid.to_s)
end
##
# Determines the secret storing transformer
# Unless configured otherwise, uses the plain secret strategy
def secret_strategy
::Doorkeeper.config.application_secret_strategy
end
##
# Determine the fallback storing strategy
# Unless configured, there will be no fallback
def fallback_secret_strategy
::Doorkeeper.config.application_secret_fallback_strategy
end
end
# Set an application's valid redirect URIs.
#
# @param uris [String, Array] Newline-separated string or array the URI(s)
#
# @return [String] The redirect URI(s) separated by newlines.
#
def redirect_uri=(uris)
super(uris.is_a?(Array) ? uris.join("\n") : uris)
end
# Check whether the given plain text secret matches our stored secret
#
# @param input [#to_s] Plain secret provided by user
# (any object that responds to `#to_s`)
#
# @return [Boolean] Whether the given secret matches the stored secret
# of this application.
#
def secret_matches?(input)
# return false if either is nil, since secure_compare depends on strings
# but Application secrets MAY be nil depending on confidentiality.
return false if input.nil? || secret.nil?
# When matching the secret by comparer function, all is well.
return true if secret_strategy.secret_matches?(input, secret)
# When fallback lookup is enabled, ensure applications
# with plain secrets can still be found
if fallback_secret_strategy
fallback_secret_strategy.secret_matches?(input, secret)
else
false
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/accessible.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module Accessible
# Indicates whether the object is accessible (not expired and not revoked).
#
# @return [Boolean] true if object accessible or false in other case
#
def accessible?
!expired? && !revoked?
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/expirable.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module Expirable
# Indicates whether the object is expired (`#expires_in` present and
# expiration time has come).
#
# @return [Boolean] true if object expired and false in other case
def expired?
!!(expires_in && Time.now.utc > expires_at)
end
# Calculates expiration time in seconds.
#
# @return [Integer, nil] number of seconds if object has expiration time
# or nil if object never expires.
def expires_in_seconds
return nil if expires_in.nil?
expires = expires_at - Time.now.utc
expires_sec = expires.seconds.round(0)
expires_sec > 0 ? expires_sec : 0
end
# Expiration time (date time of creation + TTL).
#
# @return [Time, nil] expiration time in UTC
# or nil if the object never expires.
#
def expires_at
expires_in && created_at + expires_in.seconds
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/expiration_time_sql_math.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module ExpirationTimeSqlMath
extend ::ActiveSupport::Concern
WARNING_MESSAGE = <<~WARNING.squish
[DOORKEEPER] Doorkeeper doesn't support expiration time math for your database adapter.
Records with an individual expires_in value longer than the global TTL may be incorrectly processed.
Please add a class method `custom_expiration_time_sql` to your AccessToken/AccessGrant models/mixins to provide a custom
SQL expression to calculate access token expiration time. See lib/doorkeeper/orm/active_record/mixins/access_token.rb
for more details.
WARNING
class ExpirationTimeSqlGenerator
attr_reader :model
delegate :table_name, to: :@model
def initialize(model)
@model = model
end
def generate_sql
raise "`generate_sql` should be overridden for a #{self.class.name}!"
end
end
class MySqlExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
def generate_sql
Arel.sql("DATE_ADD(#{table_name}.created_at, INTERVAL #{table_name}.expires_in SECOND)")
end
end
class SqlLiteExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
def generate_sql
Arel.sql("DATETIME(#{table_name}.created_at, '+' || #{table_name}.expires_in || ' SECONDS')")
end
end
class SqlServerExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
def generate_sql
Arel.sql("DATEADD(second, #{table_name}.expires_in, #{table_name}.created_at) AT TIME ZONE 'UTC'")
end
end
class OracleExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
def generate_sql
Arel.sql("#{table_name}.created_at + INTERVAL to_char(#{table_name}.expires_in) second")
end
end
class PostgresExpirationTimeSqlGenerator < ExpirationTimeSqlGenerator
def generate_sql
Arel.sql("#{table_name}.created_at + #{table_name}.expires_in * INTERVAL '1 SECOND'")
end
end
ADAPTERS_MAPPING = {
"sqlite" => SqlLiteExpirationTimeSqlGenerator,
"sqlite3" => SqlLiteExpirationTimeSqlGenerator,
"postgis" => PostgresExpirationTimeSqlGenerator,
"postgresql" => PostgresExpirationTimeSqlGenerator,
"mysql" => MySqlExpirationTimeSqlGenerator,
"mysql2" => MySqlExpirationTimeSqlGenerator,
"trilogy" => MySqlExpirationTimeSqlGenerator,
"sqlserver" => SqlServerExpirationTimeSqlGenerator,
"oracleenhanced" => OracleExpirationTimeSqlGenerator,
}.freeze
module ClassMethods
def supports_expiration_time_math?
ADAPTERS_MAPPING.key?(adapter_name.downcase) ||
respond_to?(:custom_expiration_time_sql)
end
def expiration_time_sql
if respond_to?(:custom_expiration_time_sql)
custom_expiration_time_sql
else
expiration_time_sql_expression
end
end
def expiration_time_sql_expression
ADAPTERS_MAPPING.fetch(adapter_name.downcase).new(self).generate_sql
end
def adapter_name
ActiveRecord::Base.connection.adapter_name
end
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/orderable.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module Orderable
extend ActiveSupport::Concern
module ClassMethods
def ordered_by(attribute, direction = :asc)
order(attribute => direction)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/ownership.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module Ownership
extend ActiveSupport::Concern
included do
belongs_to :owner, polymorphic: true, optional: true
validates :owner, presence: true, if: :validate_owner?
end
def validate_owner?
Doorkeeper.config.confirm_application_owner?
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/polymorphic_resource_owner.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module PolymorphicResourceOwner
module ForAccessGrant
extend ActiveSupport::Concern
included do
if Doorkeeper.config.polymorphic_resource_owner?
belongs_to :resource_owner, polymorphic: true, optional: false
else
validates :resource_owner_id, presence: true
end
end
end
module ForAccessToken
extend ActiveSupport::Concern
included do
if Doorkeeper.config.polymorphic_resource_owner?
belongs_to :resource_owner, polymorphic: true, optional: true
end
end
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/resource_ownerable.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module ResourceOwnerable
extend ActiveSupport::Concern
module ClassMethods
# Searches for record by Resource Owner considering Doorkeeper
# configuration for resource owner association.
#
# @param resource_owner [ActiveRecord::Base, Integer]
# resource owner
#
# @return [Doorkeeper::AccessGrant, Doorkeeper::AccessToken]
# collection of records
#
def by_resource_owner(resource_owner)
if Doorkeeper.configuration.polymorphic_resource_owner?
where(resource_owner: resource_owner)
else
where(resource_owner_id: resource_owner_id_for(resource_owner))
end
end
protected
# Backward compatible way to retrieve resource owner itself (if
# polymorphic association enabled) or just it's ID.
#
# @param resource_owner [ActiveRecord::Base, Integer]
# resource owner
#
# @return [ActiveRecord::Base, Integer]
# instance of Resource Owner or it's ID
#
def resource_owner_id_for(resource_owner)
if resource_owner.respond_to?(:to_key)
resource_owner.id
else
resource_owner
end
end
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/reusable.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module Reusable
# Indicates whether the object is reusable (i.e. It is not expired and
# has not crossed reuse_limit).
#
# @return [Boolean] true if can be reused and false in other case
def reusable?
return false if expired?
return true unless expires_in
threshold_limit = 100 - Doorkeeper.config.token_reuse_limit
expires_in_seconds >= threshold_limit * expires_in / 100
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/revocable.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module Revocable
# Revokes the object (updates `:revoked_at` attribute setting its value
# to the specific time).
#
# @param clock [Time] time object
#
def revoke(clock = Time)
return if revoked?
# Wrap in with_primary_role if the model class supports it
if self.class.respond_to?(:with_primary_role)
self.class.with_primary_role { update_attribute(:revoked_at, clock.now.utc) }
else
update_attribute(:revoked_at, clock.now.utc)
end
end
# Indicates whether the object has been revoked.
#
# @return [Boolean] true if revoked, false in other case
#
def revoked?
!!(revoked_at && revoked_at <= Time.now.utc)
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/scopes.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module Scopes
def scopes
OAuth::Scopes.from_string(scopes_string)
end
def scopes=(value)
if value.is_a?(Array)
super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
else
super(Doorkeeper::OAuth::Scopes.from_string(value.to_s).to_s)
end
end
def scopes_string
self[:scopes]
end
def includes_scope?(*required_scopes)
required_scopes.blank? || required_scopes.any? { |scope| scopes.exists?(scope.to_s) }
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/secret_storable.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
##
# Storable finder to provide lookups for input plaintext values which are
# mapped to their stored versions (e.g., hashing, encryption) before lookup.
module SecretStorable
extend ActiveSupport::Concern
delegate :secret_strategy,
:fallback_secret_strategy,
to: :class
# :nodoc
module ClassMethods
# Compare the given plaintext with the secret
#
# @param input [String]
# The plain input to compare.
#
# @param secret [String]
# The secret value to compare with.
#
# @return [Boolean]
# Whether input matches secret as per the secret strategy
#
delegate :secret_matches?, to: :secret_strategy
# Returns an instance of the Doorkeeper::AccessToken with
# specific token value.
#
# @param attr [Symbol]
# The token attribute we're looking with.
#
# @param token [#to_s]
# token value (any object that responds to `#to_s`)
#
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
# if there is no record with such token
#
def find_by_plaintext_token(attr, token)
token = token.to_s
find_by(attr => secret_strategy.transform_secret(token)) ||
find_by_fallback_token(attr, token)
end
# Allow looking up previously plain tokens as a fallback
# IFF a fallback strategy has been defined
#
# @param attr [Symbol]
# The token attribute we're looking with.
#
# @param plain_secret [#to_s]
# plain secret value (any object that responds to `#to_s`)
#
# @return [Doorkeeper::AccessToken, nil] AccessToken object or nil
# if there is no record with such token
#
def find_by_fallback_token(attr, plain_secret)
return nil unless fallback_secret_strategy
# Use the previous strategy to look up
stored_token = fallback_secret_strategy.transform_secret(plain_secret)
find_by(attr => stored_token).tap do |resource|
return nil unless resource
upgrade_fallback_value resource, attr, plain_secret
end
end
# Allow implementations in ORMs to replace a plain
# value falling back to to avoid it remaining as plain text.
#
# @param instance
# An instance of this model with a plain value token.
#
# @param attr
# The secret attribute name to upgrade.
#
# @param plain_secret
# The plain secret to upgrade.
#
def upgrade_fallback_value(instance, attr, plain_secret)
upgraded = secret_strategy.store_secret(instance, attr, plain_secret)
instance.update(attr => upgraded)
end
##
# Determines the secret storing transformer
# Unless configured otherwise, uses the plain secret strategy
def secret_strategy
::Doorkeeper::SecretStoring::Plain
end
##
# Determine the fallback storing strategy
# Unless configured, there will be no fallback
def fallback_secret_strategy
nil
end
end
end
end
end
================================================
FILE: lib/doorkeeper/models/concerns/write_to_primary.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Models
module Concerns
# Provides support for Rails read replicas by ensuring write operations
# use the primary database when automatic role switching is enabled.
#
# When Rails uses automatic role switching with read replicas, GET requests
# are routed to read-only databases. However, Doorkeeper may need to write
# to the database during GET requests (e.g., creating access tokens during
# implicit grant flow). This concern wraps write operations with
# `connected_to(role: :writing)` to ensure they use the primary database.
#
# This concern is only active when:
# 1. ActiveRecord supports `connected_to` (Rails 6.1+)
# 2. The configuration option is enabled
#
module WriteToPrimary
extend ActiveSupport::Concern
class_methods do
# Executes the given block with a connection to the primary database
# for writing, if read replica support is enabled and available.
#
# @yield Block to execute with write connection
# @return The result of the block
#
def with_primary_role(&block)
if should_use_primary_role?
::ActiveRecord::Base.connected_to(role: :writing, &block)
else
yield
end
end
private
# Determines if we should explicitly use the primary role for writes
#
# @return [Boolean]
#
def should_use_primary_role?
# Guard clause: return false if ActiveRecord is not available
return false unless defined?(::ActiveRecord::Base)
# Only use primary role if:
# 1. The enable_multiple_database_roles option is enabled in config
# 2. ActiveRecord supports connected_to (Rails 6.1+)
Doorkeeper.config.enable_multiple_database_roles &&
::ActiveRecord::Base.respond_to?(:connected_to)
end
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/authorization/code.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
module Authorization
class Code
attr_reader :pre_auth, :resource_owner, :token
def initialize(pre_auth, resource_owner)
@pre_auth = pre_auth
@resource_owner = resource_owner
end
def issue_token!
return @token if defined?(@token)
@token = Doorkeeper.config.access_grant_model.with_primary_role do
Doorkeeper.config.access_grant_model.create!(access_grant_attributes)
end
end
def oob_redirect
{ action: :show, code: token.plaintext_token }
end
def access_grant?
true
end
private
def authorization_code_expires_in
Doorkeeper.config.authorization_code_expires_in
end
def access_grant_attributes
attributes = {
application_id: pre_auth.client.id,
expires_in: authorization_code_expires_in,
redirect_uri: pre_auth.redirect_uri,
scopes: pre_auth.scopes.to_s,
}
if Doorkeeper.config.polymorphic_resource_owner?
attributes[:resource_owner] = resource_owner
else
attributes[:resource_owner_id] = resource_owner.id
end
pkce_attributes.merge(attributes).merge(custom_attributes)
end
def custom_attributes
# Custom access token attributes are saved into the access grant,
# and then included in subsequently generated access tokens.
@pre_auth.custom_access_token_attributes.to_h.with_indifferent_access
end
def pkce_attributes
return {} unless pkce_supported?
{
code_challenge: pre_auth.code_challenge,
code_challenge_method: pre_auth.code_challenge_method,
}
end
# Ensures firstly, if migration with additional PKCE columns was
# generated and migrated
def pkce_supported?
Doorkeeper.config.access_grant_model.pkce_supported?
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/authorization/context.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
module Authorization
class Context
attr_reader :client, :grant_type, :resource_owner, :scopes
def initialize(**attributes)
attributes.each do |name, value|
instance_variable_set(:"@#{name}", value) if respond_to?(name)
end
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/authorization/token.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
module Authorization
class Token
attr_reader :pre_auth, :resource_owner, :token
class << self
def build_context(pre_auth_or_oauth_client, grant_type, scopes, resource_owner)
oauth_client = if pre_auth_or_oauth_client.respond_to?(:application)
pre_auth_or_oauth_client.application
elsif pre_auth_or_oauth_client.respond_to?(:client)
pre_auth_or_oauth_client.client
else
pre_auth_or_oauth_client
end
Doorkeeper::OAuth::Authorization::Context.new(
client: oauth_client,
grant_type: grant_type,
scopes: scopes,
resource_owner: resource_owner,
)
end
def access_token_expires_in(configuration, context)
if configuration.option_defined?(:custom_access_token_expires_in)
expiration = configuration.custom_access_token_expires_in.call(context)
return nil if expiration == Float::INFINITY
expiration || configuration.access_token_expires_in
else
configuration.access_token_expires_in
end
end
def refresh_token_enabled?(server, context)
if server.refresh_token_enabled?.respond_to?(:call)
server.refresh_token_enabled?.call(context)
else
!!server.refresh_token_enabled?
end
end
end
def initialize(pre_auth, resource_owner)
@pre_auth = pre_auth
@resource_owner = resource_owner
end
def issue_token!
return @token if defined?(@token)
context = self.class.build_context(
pre_auth.client,
Doorkeeper::OAuth::IMPLICIT,
pre_auth.scopes,
resource_owner,
)
@token = Doorkeeper.config.access_token_model.find_or_create_for(
application: application,
resource_owner: resource_owner,
scopes: pre_auth.scopes,
expires_in: self.class.access_token_expires_in(Doorkeeper.config, context),
use_refresh_token: false,
)
end
def application
return unless pre_auth.client
pre_auth.client.is_a?(Doorkeeper.config.application_model) ? pre_auth.client : pre_auth.client.application
end
def oob_redirect
{
controller: controller,
action: :show,
access_token: token.plaintext_token,
}
end
def access_token?
true
end
private
def controller
@controller ||= begin
mapping = Doorkeeper::Rails::Routes.mapping[:token_info] || {}
mapping[:controllers] || "doorkeeper/token_info"
end
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/authorization/uri_builder.rb
================================================
# frozen_string_literal: true
require "rack/utils"
module Doorkeeper
module OAuth
module Authorization
class URIBuilder
class << self
def uri_with_query(url, parameters = {})
uri = URI.parse(url)
original_query = Rack::Utils.parse_query(uri.query)
uri.query = build_query(original_query.merge(parameters))
uri.to_s
end
def uri_with_fragment(url, parameters = {})
uri = URI.parse(url)
uri.fragment = build_query(parameters)
uri.to_s
end
private
def build_query(parameters = {})
parameters.reject! { |_, value| value.blank? }
Rack::Utils.build_query(parameters)
end
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/authorization_code_request.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class AuthorizationCodeRequest < BaseRequest
validate :params, error: Errors::InvalidRequest
validate :client, error: Errors::InvalidClient
validate :grant, error: Errors::InvalidGrant
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
validate :redirect_uri, error: Errors::InvalidGrant
validate :code_verifier, error: Errors::InvalidGrant
attr_reader :grant, :client, :redirect_uri, :access_token, :code_verifier,
:invalid_request_reason, :missing_param
def initialize(server, grant, client, parameters = {})
@server = server
@client = client
@grant = grant
@grant_type = Doorkeeper::OAuth::AUTHORIZATION_CODE
@redirect_uri = parameters[:redirect_uri]
@code_verifier = parameters[:code_verifier]
end
private
def before_successful_response
grant.transaction do
grant.lock!
raise Errors::InvalidGrantReuse if grant.revoked?
if Doorkeeper.config.revoke_previous_authorization_code_token?
revoke_previous_tokens(grant.application, resource_owner)
end
grant.revoke
find_or_create_access_token(
client,
resource_owner,
grant.scopes,
custom_token_attributes_with_data,
server,
)
end
super
end
def resource_owner
if Doorkeeper.config.polymorphic_resource_owner?
grant.resource_owner
else
grant.resource_owner_id
end
end
def pkce_supported?
Doorkeeper.config.access_grant_model.pkce_supported?
end
def validate_params
@missing_param =
if grant&.uses_pkce? && code_verifier.blank?
:code_verifier
elsif client && !client.confidential && Doorkeeper.config.force_pkce? && code_verifier.blank?
:code_verifier
elsif redirect_uri.blank?
:redirect_uri
end
@missing_param.nil?
end
def validate_client
client.present?
end
def validate_grant
return false unless grant && grant.application_id == client.id
grant.accessible?
end
def validate_redirect_uri
Helpers::URIChecker.valid_for_authorization?(
redirect_uri,
grant.redirect_uri,
)
end
# if either side (server or client) request PKCE, check the verifier
# against the DB - if PKCE is supported
def validate_code_verifier
return true unless pkce_supported?
return grant.code_challenge.blank? if code_verifier.blank?
if grant.code_challenge_method == "S256"
grant.code_challenge == generate_code_challenge(code_verifier)
elsif grant.code_challenge_method == "plain"
grant.code_challenge == code_verifier
else
false
end
end
def generate_code_challenge(code_verifier)
Doorkeeper.config.access_grant_model.generate_code_challenge(code_verifier)
end
def custom_token_attributes_with_data
grant
.attributes
.with_indifferent_access
.slice(*Doorkeeper.config.custom_access_token_attributes)
.symbolize_keys
end
def revoke_previous_tokens(application, resource_owner)
Doorkeeper.config.access_token_model.revoke_all_for(application.id, resource_owner)
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/base_request.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class BaseRequest
include Validations
attr_reader :grant_type, :server
delegate :default_scopes, to: :server
def authorize
if valid?
before_successful_response
@response = TokenResponse.new(access_token)
after_successful_response
@response
elsif error == Errors::InvalidRequest
@response = InvalidRequestResponse.from_request(self)
else
@response = ErrorResponse.from_request(self)
end
end
def scopes
@scopes ||= build_scopes
end
def find_or_create_access_token(client, resource_owner, scopes, custom_attributes, server)
context = Authorization::Token.build_context(client, grant_type, scopes, resource_owner)
application = client.is_a?(Doorkeeper.config.application_model) ? client : client&.application
token_attributes = {
application: application,
resource_owner: resource_owner,
scopes: scopes,
expires_in: Authorization::Token.access_token_expires_in(server, context),
use_refresh_token: Authorization::Token.refresh_token_enabled?(server, context),
}
@access_token =
Doorkeeper.config.access_token_model.find_or_create_for(**token_attributes.merge(custom_attributes))
end
def before_successful_response
Doorkeeper.config.before_successful_strategy_response.call(self)
end
def after_successful_response
Doorkeeper.config.after_successful_strategy_response.call(self, @response)
end
private
def build_scopes
if @original_scopes.present?
OAuth::Scopes.from_string(@original_scopes)
else
client_scopes = @client&.scopes
return default_scopes if client_scopes.blank?
# Avoid using Scope#& for dynamic scopes
client_scopes.allowed(default_scopes)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/base_response.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class BaseResponse
def body
{}
end
def description
""
end
def headers
{}
end
def redirectable?
false
end
def redirect_uri
""
end
def status
:ok
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/client/credentials.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class Client
Credentials = Struct.new(:uid, :secret) do
class << self
def from_request(request, *credentials_methods)
credentials_methods.inject(nil) do |_, method|
method = self.method(method) if method.is_a?(Symbol)
credentials = Credentials.new(*method.call(request))
break credentials if credentials.present?
end
end
def from_params(request)
request.parameters.values_at(:client_id, :client_secret)
end
def from_basic(request)
authorization = request.authorization
if authorization.present? && authorization =~ /^Basic (.*)/im
Base64.decode64(Regexp.last_match(1)).split(/:/, 2)
end
end
end
# Public clients may have their secret blank, but "credentials" are
# still present
delegate :blank?, to: :uid
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/client.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class Client
attr_reader :application
delegate :id, :name, :uid, :redirect_uri, :scopes, :confidential, to: :@application
def initialize(application)
@application = application
end
def self.find(uid, method = Doorkeeper.config.application_model.method(:by_uid))
return unless (application = method.call(uid))
new(application)
end
def self.authenticate(credentials, method = Doorkeeper.config.application_model.method(:by_uid_and_secret))
return if credentials.blank?
return unless (application = method.call(credentials.uid, credentials.secret))
new(application)
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/client_credentials/creator.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
module ClientCredentials
class Creator
def call(client, scopes, attributes = {})
existing_token = nil
if lookup_existing_token?
existing_token = find_active_existing_token_for(client, scopes, attributes)
return existing_token if Doorkeeper.config.reuse_access_token && existing_token&.reusable?
end
with_revocation(existing_token: existing_token) do
application = client.is_a?(Doorkeeper.config.application_model) ? client : client&.application
Doorkeeper.config.access_token_model.create_for(
application: application,
resource_owner: nil,
scopes: scopes,
**attributes,
)
end
end
private
def with_revocation(existing_token:)
if existing_token && Doorkeeper.config.revoke_previous_client_credentials_token?
existing_token.with_lock do
raise Errors::DoorkeeperError, :invalid_token_reuse if existing_token.revoked?
existing_token.revoke
yield
end
else
yield
end
end
def lookup_existing_token?
Doorkeeper.config.reuse_access_token ||
Doorkeeper.config.revoke_previous_client_credentials_token?
end
def find_active_existing_token_for(client, scopes, attributes)
custom_attributes = Doorkeeper.config.access_token_model.
extract_custom_attributes(attributes).presence
Doorkeeper.config.access_token_model.matching_token_for(
client, nil, scopes, custom_attributes: custom_attributes, include_expired: false)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/client_credentials/issuer.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
module ClientCredentials
class Issuer
attr_reader :token, :validator, :error
def initialize(server, validator)
@server = server
@validator = validator
end
def create(client, scopes, attributes = {}, creator = Creator.new)
if validator.valid?
@token = create_token(client, scopes, attributes, creator)
@error = Errors::ServerError unless @token
else
@token = false
@error = validator.error
end
@token
end
private
def create_token(client, scopes, attributes, creator)
context = Authorization::Token.build_context(
client,
Doorkeeper::OAuth::CLIENT_CREDENTIALS,
scopes,
nil,
)
ttl = Authorization::Token.access_token_expires_in(@server, context)
creator.call(
client,
scopes,
use_refresh_token: false,
expires_in: ttl,
**attributes
)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/client_credentials/validator.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
module ClientCredentials
class Validator
include Validations
include OAuth::Helpers
validate :client, error: Errors::InvalidClient
validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
validate :scopes, error: Errors::InvalidScope
def initialize(server, request)
@server = server
@request = request
@client = request.client
validate
end
private
def validate_client
@client.present?
end
def validate_client_supports_grant_flow
return if @client.blank?
Doorkeeper.config.allow_grant_flow_for_client?(
Doorkeeper::OAuth::CLIENT_CREDENTIALS,
@client.application,
)
end
def validate_scopes
application_scopes = if @client.present?
@client.application.scopes
else
""
end
return true if @request.scopes.blank? && application_scopes.blank?
ScopeChecker.valid?(
scope_str: @request.scopes.to_s,
server_scopes: @server.scopes,
app_scopes: application_scopes,
grant_type: Doorkeeper::OAuth::CLIENT_CREDENTIALS,
)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/client_credentials_request.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class ClientCredentialsRequest < BaseRequest
attr_reader :client, :original_scopes, :parameters, :response
alias error_response response
delegate :error, to: :issuer
def initialize(server, client, parameters = {})
@client = client
@server = server
@response = nil
@grant_type = Doorkeeper::OAuth::CLIENT_CREDENTIALS
@original_scopes = parameters[:scope]
@parameters = parameters.except(:scope)
end
def access_token
issuer.token
end
def issuer
@issuer ||= ClientCredentials::Issuer.new(
server,
ClientCredentials::Validator.new(server, self),
)
end
private
def valid?
issuer.create(client, scopes, custom_token_attributes_with_data)
end
def custom_token_attributes_with_data
parameters
.with_indifferent_access
.slice(*Doorkeeper.config.custom_access_token_attributes)
.symbolize_keys
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/code_request.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class CodeRequest
attr_reader :pre_auth, :resource_owner
def initialize(pre_auth, resource_owner)
@pre_auth = pre_auth
@resource_owner = resource_owner
end
def authorize
auth = Authorization::Code.new(pre_auth, resource_owner)
auth.issue_token!
CodeResponse.new(pre_auth, auth, response_on_fragment: pre_auth.response_mode == "fragment")
end
def deny
pre_auth.error = Errors::AccessDenied
pre_auth.error_response
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/code_response.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class CodeResponse < BaseResponse
include OAuth::Helpers
attr_reader :pre_auth, :auth, :response_on_fragment
def initialize(pre_auth, auth, options = {})
@pre_auth = pre_auth
@auth = auth
@response_on_fragment = options[:response_on_fragment]
end
def redirectable?
true
end
def issued_token
auth.token
end
def body
if auth.try(:access_token?)
{
access_token: auth.token.plaintext_token,
token_type: auth.token.token_type,
expires_in: auth.token.expires_in_seconds,
state: pre_auth.state,
}
elsif auth.try(:access_grant?)
{
code: auth.token.plaintext_token,
state: pre_auth.state,
}
end
end
def redirect_uri
if URIChecker.oob_uri?(pre_auth.redirect_uri)
auth.oob_redirect
elsif response_on_fragment
Authorization::URIBuilder.uri_with_fragment(pre_auth.redirect_uri, body)
else
Authorization::URIBuilder.uri_with_query(pre_auth.redirect_uri, body)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/error.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
Error = Struct.new(:name, :state, :translate_options) do
def description
options = (translate_options || {}).merge(
scope: %i[doorkeeper errors messages],
default: :server_error,
)
I18n.translate(name, **options)
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/error_response.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class ErrorResponse < BaseResponse
include OAuth::Helpers
NON_REDIRECTABLE_STATES = %i[invalid_redirect_uri invalid_client unauthorized_client].freeze
def self.from_request(request, attributes = {})
new(
attributes.merge(
name: error_name_for(request.error),
exception_class: exception_class_for(request.error),
translate_options: request.error.try(:translate_options),
state: request.try(:state),
redirect_uri: request.try(:redirect_uri),
),
)
end
def self.error_name_for(error)
error.respond_to?(:name_for_response) ? error.name_for_response : error
end
def self.exception_class_for(error)
return error if error.respond_to?(:name_for_response)
"Doorkeeper::Errors::#{error.to_s.classify}".safe_constantize
end
private_class_method :error_name_for, :exception_class_for
delegate :name, :description, :state, to: :@error
def initialize(attributes = {})
@error = OAuth::Error.new(*attributes.values_at(:name, :state, :translate_options))
@exception_class = attributes[:exception_class]
@redirect_uri = attributes[:redirect_uri]
@response_on_fragment = attributes[:response_on_fragment]
end
def body
{
error: name,
error_description: description,
state: state,
}.reject { |_, v| v.blank? }
end
def status
if name == :invalid_client || name == :unauthorized_client
:unauthorized
else
:bad_request
end
end
def redirectable?
!NON_REDIRECTABLE_STATES.include?(name) && !URIChecker.oob_uri?(@redirect_uri)
end
def redirect_uri
if @response_on_fragment
Authorization::URIBuilder.uri_with_fragment(@redirect_uri, body)
else
Authorization::URIBuilder.uri_with_query(@redirect_uri, body)
end
end
def headers
{
"Cache-Control" => "no-store, no-cache",
"Content-Type" => "application/json; charset=utf-8",
"WWW-Authenticate" => authenticate_info,
}
end
def raise_exception!
raise exception_class.new(self), description
end
protected
def realm
Doorkeeper.config.realm
end
def exception_class
return @exception_class if @exception_class
raise NotImplementedError, "error response must define #exception_class"
end
private
def authenticate_info
%(Bearer realm="#{realm}", error="#{sanitize_error_values(name)}", error_description="#{sanitize_error_values(description)}")
end
# This method removes any characters that are invalid in error
# details per RFC6750.
#
# > Values for the "error" and "error_description" attributes
# > (specified in Appendixes A.7 and A.8 of [RFC6749]) MUST NOT
# > include characters outside the set %x20-21 (" " or "!") / %x23-5B /
# > %x5D-7E (ascii "#" to "~" without "\").
def sanitize_error_values(string)
string.to_s.each_char.map do |char|
if char.in?("\x20".encode("utf-8").."\x21".encode("utf-8")) ||
char.in?("\x23".encode("utf-8").."\x5B".encode("utf-8")) ||
char.in?("\x5D".encode("utf-8").."\x7E".encode("utf-8"))
char
else
"_"
end
end.join("")
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/forbidden_token_response.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class ForbiddenTokenResponse < ErrorResponse
def self.from_scopes(scopes, attributes = {})
new(attributes.merge(scopes: scopes))
end
def initialize(attributes = {})
super(attributes.merge(name: :insufficient_scope, state: :forbidden))
@scopes = attributes[:scopes]
end
def status
:forbidden
end
def description
@description ||= I18n.t("doorkeeper.errors.messages.forbidden_token.missing_scope",
oauth_scopes: @scopes.map(&:to_s).join(" "),)
end
protected
def exception_class
Doorkeeper::Errors::TokenForbidden
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/helpers/scope_checker.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
module Helpers
module ScopeChecker
class Validator
attr_reader :parsed_scopes, :scope_str
def initialize(scope_str, server_scopes, app_scopes, grant_type)
@parsed_scopes = OAuth::Scopes.from_string(scope_str)
@scope_str = scope_str
@valid_scopes = valid_scopes(server_scopes, app_scopes)
@scopes_by_grant_type = Doorkeeper.config.scopes_by_grant_type[grant_type.to_sym] if grant_type
end
def valid?
scope_str.present? &&
scope_str !~ /[\n\r\t]/ &&
@valid_scopes.has_scopes?(parsed_scopes) &&
permitted_to_grant_type?
end
private
def valid_scopes(server_scopes, app_scopes)
app_scopes.presence || server_scopes
end
def permitted_to_grant_type?
return true unless @scopes_by_grant_type
OAuth::Scopes.from_array(@scopes_by_grant_type)
.has_scopes?(parsed_scopes)
end
end
def self.valid?(scope_str:, server_scopes:, app_scopes: nil, grant_type: nil)
Validator.new(
scope_str,
server_scopes,
app_scopes,
grant_type,
).valid?
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/helpers/unique_token.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
module Helpers
# Default Doorkeeper token generator. Follows OAuth RFC and
# could be customized using `default_generator_method` in
# configuration.
module UniqueToken
def self.generate(options = {})
# Access Token value must be 1*VSCHAR or
# 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
#
# @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.12
# @see https://datatracker.ietf.org/doc/html/rfc6750#section-2.1
#
generator = options.delete(:generator) || SecureRandom.method(default_generator_method)
token_size = options.delete(:size) || 32
generator.call(token_size)
end
# Generator method for default generator class (SecureRandom)
#
def self.default_generator_method
Doorkeeper.config.default_generator_method
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/helpers/uri_checker.rb
================================================
# frozen_string_literal: true
require "ipaddr"
module Doorkeeper
module OAuth
module Helpers
module URIChecker
def self.valid?(url)
return true if oob_uri?(url)
uri = as_uri(url)
valid_scheme?(uri) && iff_host?(uri) && uri.fragment.nil? && uri.opaque.nil?
rescue URI::InvalidURIError
false
end
def self.matches?(url, client_url)
url = as_uri(url)
client_url = as_uri(client_url)
unless client_url.query.nil?
return false unless query_matches?(url.query, client_url.query)
# Clear out queries so rest of URI can be tested. This allows query
# params to be in the request but order not mattering.
client_url.query = nil
end
# RFC8252, Paragraph 7.3
# @see https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
if loopback_uri?(url) && loopback_uri?(client_url)
url.port = nil
client_url.port = nil
end
url.query = nil
url == client_url
end
def self.loopback_uri?(uri)
IPAddr.new(uri.host).loopback?
rescue IPAddr::Error, IPAddr::InvalidAddressError
false
end
def self.valid_for_authorization?(url, client_url)
valid?(url) && client_url.split.any? { |other_url| matches?(url, other_url) }
end
def self.as_uri(url)
URI.parse(url)
end
def self.query_matches?(query, client_query)
return true if client_query.blank? && query.blank?
return false if client_query.nil? || query.nil?
# Will return true independent of query order
client_query.split("&").sort == query.split("&").sort
end
def self.valid_scheme?(uri)
return false if uri.scheme.blank?
%w[localhost].exclude?(uri.scheme)
end
def self.hypertext_scheme?(uri)
%w[http https].include?(uri.scheme)
end
def self.iff_host?(uri)
!(hypertext_scheme?(uri) && uri.host.blank?)
end
def self.oob_uri?(uri)
NonStandard::IETF_WG_OAUTH2_OOB_METHODS.include?(uri)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/hooks/context.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
module Hooks
class Context
attr_reader :auth, :pre_auth
def initialize(**attributes)
attributes.each do |name, value|
instance_variable_set(:"@#{name}", value) if respond_to?(name)
end
end
def issued_token
auth&.issued_token
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/invalid_request_response.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class InvalidRequestResponse < ErrorResponse
attr_reader :reason
def self.from_request(request, attributes = {})
new(
attributes.merge(
state: request.try(:state),
redirect_uri: request.try(:redirect_uri),
missing_param: request.try(:missing_param),
reason: request.try(:invalid_request_reason),
),
)
end
def initialize(attributes = {})
super(attributes.merge(name: :invalid_request))
@missing_param = attributes[:missing_param]
@reason = @missing_param.nil? ? attributes[:reason] : :missing_param
end
def status
:bad_request
end
def description
I18n.translate(
reason,
scope: %i[doorkeeper errors messages invalid_request],
default: :unknown,
value: @missing_param,
)
end
def exception_class
Doorkeeper::Errors::InvalidRequest
end
def redirectable?
super && @missing_param != :client_id
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/invalid_token_response.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class InvalidTokenResponse < ErrorResponse
attr_reader :reason
def self.from_access_token(access_token, attributes = {})
reason = if access_token&.revoked?
:revoked
elsif access_token&.expired?
:expired
else
:unknown
end
new(attributes.merge(reason: reason))
end
def initialize(attributes = {})
super(attributes.merge(name: :invalid_token, state: :unauthorized))
@reason = attributes[:reason] || :unknown
end
def status
:unauthorized
end
def description
@description ||=
I18n.translate(
@reason,
scope: %i[doorkeeper errors messages invalid_token],
)
end
protected
def exception_class
errors_mapping.fetch(reason)
end
private
def errors_mapping
{
expired: Doorkeeper::Errors::TokenExpired,
revoked: Doorkeeper::Errors::TokenRevoked,
unknown: Doorkeeper::Errors::TokenUnknown,
}
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/nonstandard.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class NonStandard
# These are not part of the OAuth 2 specification but are still in use by Google
# and in some other implementations. Native applications should use one of the
# approaches discussed in RFC8252. OOB is 'Out of Band'
# This value signals to the Google Authorization Server that the authorization
# code should be returned in the title bar of the browser, with the page text
# prompting the user to copy the code and paste it in the application.
# This is useful when the client (such as a Windows application) cannot listen
# on an HTTP port without significant client configuration.
# When you use this value, your application can then detect that the page has loaded, and can
# read the title of the HTML page to obtain the authorization code. It is then up to your
# application to close the browser window if you want to ensure that the user never sees the
# page that contains the authorization code. The mechanism for doing this varies from platform
# to platform.
#
# If your platform doesn't allow you to detect that the page has loaded or read the title of
# the page, you can have the user paste the code back to your application, as prompted by the
# text in the confirmation page that the OAuth 2.0 server generates.
IETF_WG_OAUTH2_OOB = "urn:ietf:wg:oauth:2.0:oob"
# This is identical to urn:ietf:wg:oauth:2.0:oob, but the text in the confirmation page that
# the OAuth 2.0 server generates won't instruct the user to copy the authorization code, but
# instead will simply ask the user to close the window.
#
# This is useful when your application reads the title of the HTML page (by checking window
# titles on the desktop, for example) to obtain the authorization code, but can't close the
# page on its own.
IETF_WG_OAUTH2_OOB_AUTO = "urn:ietf:wg:oauth:2.0:oob:auto"
IETF_WG_OAUTH2_OOB_METHODS = [IETF_WG_OAUTH2_OOB, IETF_WG_OAUTH2_OOB_AUTO].freeze
end
end
end
================================================
FILE: lib/doorkeeper/oauth/password_access_token_request.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class PasswordAccessTokenRequest < BaseRequest
include OAuth::Helpers
validate :client, error: Errors::InvalidClient
validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
validate :resource_owner, error: Errors::InvalidGrant
validate :scopes, error: Errors::InvalidScope
attr_reader :client, :credentials, :resource_owner, :parameters, :access_token
def initialize(server, client, credentials, resource_owner, parameters = {})
@server = server
@resource_owner = resource_owner
@client = client
@credentials = credentials
@parameters = parameters
@original_scopes = parameters[:scope]
@grant_type = Doorkeeper::OAuth::PASSWORD
end
private
def before_successful_response
find_or_create_access_token(client, resource_owner, scopes, {}, server)
super
end
def validate_scopes
return true if scopes.blank?
ScopeChecker.valid?(
scope_str: scopes.to_s,
server_scopes: server.scopes,
app_scopes: client.try(:scopes),
grant_type: grant_type,
)
end
def validate_resource_owner
resource_owner.present?
end
# Section 4.3.2. Access Token Request for Resource Owner Password Credentials Grant:
#
# If the client type is confidential or the client was issued client credentials (or assigned
# other authentication requirements), the client MUST authenticate with the authorization
# server as described in Section 3.2.1.
#
# The authorization server MUST:
#
# o require client authentication for confidential clients or for any client that was
# issued client credentials (or with other authentication requirements)
#
# o authenticate the client if client authentication is included,
#
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
#
def validate_client
if Doorkeeper.config.skip_client_authentication_for_password_grant
client.present? || (!parameters[:client_id] && credentials.blank?)
else
client.present?
end
end
def validate_client_supports_grant_flow
Doorkeeper.config.allow_grant_flow_for_client?(grant_type, client&.application)
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/pre_authorization.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class PreAuthorization
include Validations
validate :client_id, error: Errors::InvalidRequest
validate :client, error: Errors::InvalidClient
validate :client_supports_grant_flow, error: Errors::UnauthorizedClient
validate :resource_owner_authorize_for_client, error: Errors::InvalidClient
validate :redirect_uri, error: Errors::InvalidRedirectUri
validate :params, error: Errors::InvalidRequest
validate :response_type, error: Errors::UnsupportedResponseType
validate :response_mode, error: Errors::UnsupportedResponseMode
validate :scopes, error: Errors::InvalidScope
validate :code_challenge, error: Errors::InvalidRequest
validate :code_challenge_method, error: Errors::InvalidCodeChallengeMethod
attr_reader :client, :code_challenge, :code_challenge_method, :missing_param,
:redirect_uri, :resource_owner, :response_type, :state,
:authorization_response_flow, :response_mode, :custom_access_token_attributes,
:invalid_request_reason
def initialize(server, parameters = {}, resource_owner = nil)
@server = server
@client_id = parameters[:client_id]
@response_type = parameters[:response_type]
@response_mode = parameters[:response_mode]
@redirect_uri = parameters[:redirect_uri]
@scope = parameters[:scope]
@state = parameters[:state]
@code_challenge = parameters[:code_challenge]
@code_challenge_method = parameters[:code_challenge_method]
@resource_owner = resource_owner
@custom_access_token_attributes = parameters.slice(*Doorkeeper.config.custom_access_token_attributes).to_h
end
def authorizable?
valid?
end
def scopes
Scopes.from_string(scope)
end
def scope
@scope.presence || (server.default_scopes.presence && build_scopes)
end
def error_response
if error == Errors::InvalidRequest
OAuth::InvalidRequestResponse.from_request(
self,
response_on_fragment: response_on_fragment?,
)
else
OAuth::ErrorResponse.from_request(self, response_on_fragment: response_on_fragment?)
end
end
def as_json(_options = nil)
pre_auth_hash
end
def form_post_response?
response_mode == "form_post"
end
private
attr_reader :client_id, :server
def build_scopes
client_scopes = client.scopes
if client_scopes.blank?
server.default_scopes.to_s
else
server.default_scopes.allowed(client_scopes).to_s
end
end
def validate_client_id
@missing_param = :client_id if client_id.blank?
@missing_param.nil?
end
def validate_client
@client = OAuth::Client.find(client_id)
@client.present?
end
def validate_client_supports_grant_flow
Doorkeeper.config.allow_grant_flow_for_client?(grant_type, client.application)
end
def validate_resource_owner_authorize_for_client
# The `authorize_resource_owner_for_client` config option is used for this validation
client.application.authorized_for_resource_owner?(@resource_owner)
end
def validate_redirect_uri
return false if redirect_uri.blank?
Helpers::URIChecker.valid_for_authorization?(
redirect_uri,
client.redirect_uri,
)
end
def validate_params
@missing_param = if response_type.blank?
:response_type
elsif @scope.blank? && server.default_scopes.blank?
:scope
end
@missing_param.nil?
end
def validate_response_type
server.authorization_response_flows.any? do |flow|
if flow.matches_response_type?(response_type)
@authorization_response_flow = flow
true
end
end
end
def validate_response_mode
if response_mode.blank?
@response_mode = authorization_response_flow.default_response_mode
return true
end
authorization_response_flow.matches_response_mode?(response_mode)
end
def validate_scopes
Helpers::ScopeChecker.valid?(
scope_str: scope,
server_scopes: server.scopes,
app_scopes: client.scopes,
grant_type: grant_type,
)
end
def validate_code_challenge
return true unless Doorkeeper.config.force_pkce?
return true if client.confidential
return true if code_challenge.present?
@invalid_request_reason = :invalid_code_challenge
false
end
def validate_code_challenge_method
return true unless Doorkeeper.config.access_grant_model.pkce_supported?
code_challenge.blank? ||
(code_challenge_method.present? && Doorkeeper.config.pkce_code_challenge_methods_supported.include?(code_challenge_method))
end
def response_on_fragment?
return response_type == "token" if response_mode.nil?
response_mode == "fragment"
end
def grant_type
response_type == "code" ? AUTHORIZATION_CODE : IMPLICIT
end
def pre_auth_hash
{
client_id: client.uid,
redirect_uri: redirect_uri,
state: state,
response_type: response_type,
scope: scope,
client_name: client.name,
status: I18n.t("doorkeeper.pre_authorization.status"),
}
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/refresh_token_request.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class RefreshTokenRequest < BaseRequest
include OAuth::Helpers
validate :token_presence, error: Errors::InvalidRequest
validate :token, error: Errors::InvalidGrant
validate :client, error: Errors::InvalidClient
validate :client_match, error: Errors::InvalidGrant
validate :scope, error: Errors::InvalidScope
attr_reader :access_token, :client, :credentials, :refresh_token
attr_reader :missing_param
def initialize(server, refresh_token, credentials, parameters = {})
@server = server
@refresh_token = refresh_token
@credentials = credentials
@grant_type = Doorkeeper::OAuth::REFRESH_TOKEN
@original_scopes = parameters[:scope] || parameters[:scopes]
@refresh_token_parameter = parameters[:refresh_token]
@client = load_client(credentials) if credentials
end
private
def load_client(credentials)
Doorkeeper.config.application_model.by_uid_and_secret(credentials.uid, credentials.secret)
end
def before_successful_response
if refresh_token_revoked_on_use?
# No locking needed when refresh tokens are revoked on use
# because the old token is revoked later when the new token is used.
# This allows multiple concurrent refresh requests to succeed during the
# transition period, after which the old refresh token will be revoked.
raise Errors::InvalidGrantReuse if refresh_token.revoked?
create_access_token
else
# Use locking when refresh tokens are revoked immediately
# to prevent race conditions where multiple tokens could be created
refresh_token.with_lock do
raise Errors::InvalidGrantReuse if refresh_token.revoked?
refresh_token.revoke
create_access_token
end
end
super
end
def refresh_token_revoked_on_use?
Doorkeeper.config.access_token_model.refresh_token_revoked_on_use?
end
def default_scopes
refresh_token.scopes
end
def create_access_token
attributes = {}.merge(custom_token_attributes_with_data)
resource_owner =
if Doorkeeper.config.polymorphic_resource_owner?
refresh_token.resource_owner
else
refresh_token.resource_owner_id
end
if refresh_token_revoked_on_use?
attributes[:previous_refresh_token] = refresh_token.refresh_token
end
# RFC6749
# 1.5. Refresh Token
#
# Refresh tokens are issued to the client by the authorization server and are
# used to obtain a new access token when the current access token
# becomes invalid or expires, or to obtain additional access tokens
# with identical or narrower scope (access tokens may have a shorter
# lifetime and fewer permissions than authorized by the resource
# owner).
#
# Here we assume that TTL of the token received after refreshing should be
# the same as that of the original token.
#
@access_token = Doorkeeper.config.access_token_model.create_for(
application: refresh_token.application,
resource_owner: resource_owner,
scopes: scopes,
expires_in: refresh_token.expires_in,
use_refresh_token: true,
**attributes,
)
end
def validate_token_presence
@missing_param = :refresh_token if refresh_token.blank? && @refresh_token_parameter.blank?
@missing_param.nil?
end
def validate_token
refresh_token.present? && !refresh_token.revoked?
end
def validate_client
return true if credentials.blank?
client.present?
end
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-1.5
#
def validate_client_match
return true if refresh_token.application_id.blank?
client && refresh_token.application_id == client.id
end
def validate_scope
if @original_scopes.present?
ScopeChecker.valid?(
scope_str: @original_scopes,
server_scopes: refresh_token.scopes,
)
else
true
end
end
def custom_token_attributes_with_data
refresh_token
.attributes
.with_indifferent_access
.slice(*Doorkeeper.config.custom_access_token_attributes)
.symbolize_keys
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/scopes.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class Scopes
include Enumerable
include Comparable
DYNAMIC_SCOPE_WILDCARD = "*"
def self.from_string(string)
string ||= ""
new.tap do |scope|
scope.add(*string.split)
end
end
def self.from_array(array)
new.tap do |scope|
scope.add(*array)
end
end
delegate :each, :empty?, to: :@scopes
def initialize
@scopes = []
end
def exists?(scope)
scope = scope.to_s
@scopes.any? do |allowed_scope|
if dynamic_scopes_enabled? && dynamic_scopes_present?(allowed_scope, scope)
dynamic_scope_match?(allowed_scope, scope)
else
allowed_scope == scope
end
end
end
def add(*scopes)
@scopes.push(*scopes.map(&:to_s))
@scopes.uniq!
end
def all
@scopes
end
def to_s
@scopes.join(" ")
end
def scopes?(scopes)
scopes.all? { |scope| exists?(scope) }
end
alias has_scopes? scopes?
def +(other)
self.class.from_array(all + to_array(other))
end
def <=>(other)
if other.respond_to?(:map)
map(&:to_s).sort <=> other.map(&:to_s).sort
else
super
end
end
# DEPRECATED: With dynamic scopes, #allowed should be called because
# A & B doesn't really make sense with dynamic scopes.
#
# For example, if A = user:* and B is user:1, A & B = [].
# If we modified this method to take dynamic scopes into an account, then order
# becomes important, and this would violate the principle that A & B = B & A.
def &(other)
return allowed(other) if dynamic_scopes_enabled?
self.class.from_array(all & to_array(other))
end
# Returns a set of scopes that are allowed, taking dynamic
# scopes into account. This instance's scopes is taken as the allowed set,
# and the passed value is the set to filter.
#
# @param other The set of scopes to filter
def allowed(other)
filtered_scopes = other.select { |scope| self.exists?(scope) }
self.class.from_array(filtered_scopes)
end
private
def dynamic_scopes_enabled?
Doorkeeper.config.enable_dynamic_scopes?
end
def dynamic_scope_delimiter
return unless dynamic_scopes_enabled?
@dynamic_scope_delimiter ||= Doorkeeper.config.dynamic_scopes_delimiter
end
def dynamic_scopes_present?(allowed, requested)
allowed.include?(dynamic_scope_delimiter) && requested.include?(dynamic_scope_delimiter)
end
def dynamic_scope_match?(allowed, requested)
allowed_pattern = allowed.split(dynamic_scope_delimiter, 2)
request_pattern = requested.split(dynamic_scope_delimiter, 2)
return false if allowed_pattern[0] != request_pattern[0]
return false if allowed_pattern[1].blank?
return false if request_pattern[1].blank?
return true if allowed_pattern[1] == DYNAMIC_SCOPE_WILDCARD && allowed_pattern[1].present?
allowed_pattern[1] == request_pattern[1]
end
def to_array(other)
case other
when Scopes
other.all
else
other.to_a
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/token.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class Token
class << self
def from_request(request, *methods)
methods.inject(nil) do |_, method|
method = self.method(method) if method.is_a?(Symbol)
credentials = method.call(request)
break credentials if credentials.present?
end
end
def authenticate(request, *methods)
if (token = from_request(request, *methods))
access_token = Doorkeeper.config.access_token_model.by_token(token)
if access_token.present? && Doorkeeper.config.refresh_token_enabled?
access_token.revoke_previous_refresh_token!
end
access_token
end
end
def from_access_token_param(request)
request.parameters[:access_token]
end
def from_bearer_param(request)
request.parameters[:bearer_token]
end
def from_bearer_authorization(request)
pattern = /^Bearer /i
header = request.authorization
token_from_header(header, pattern) if match?(header, pattern)
end
def from_basic_authorization(request)
pattern = /^Basic /i
header = request.authorization
token_from_basic_header(header, pattern) if match?(header, pattern)
end
private
def token_from_basic_header(header, pattern)
encoded_header = token_from_header(header, pattern)
decode_basic_credentials_token(encoded_header)
end
def decode_basic_credentials_token(encoded_header)
Base64.decode64(encoded_header).split(/:/, 2).first
end
def token_from_header(header, pattern)
header.gsub(pattern, "")
end
def match?(header, pattern)
header&.match(pattern)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/token_introspection.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
# RFC7662 OAuth 2.0 Token Introspection
#
# @see https://datatracker.ietf.org/doc/html/rfc7662
class TokenIntrospection
attr_reader :token, :error, :invalid_request_reason
def initialize(server, token)
@server = server
@token = token
end
def authorized?
authorize!
@error.blank?
end
def error_response
return if @error.blank?
if @error == Errors::InvalidToken
OAuth::InvalidTokenResponse.from_access_token(authorized_token)
elsif @error == Errors::InvalidRequest
OAuth::InvalidRequestResponse.from_request(self)
else
OAuth::ErrorResponse.from_request(self)
end
end
def to_json(*)
active? ? success_response : failure_response
end
private
attr_reader :server
# If the protected resource uses OAuth 2.0 client credentials to
# authenticate to the introspection endpoint and its credentials are
# invalid, the authorization server responds with an HTTP 401
# (Unauthorized) as described in Section 5.2 of OAuth 2.0 [RFC6749].
#
# Endpoint must first validate the authentication.
# If the authentication is invalid, the endpoint should respond with
# an HTTP 401 status code and an invalid_client response.
#
# @see https://www.oauth.com/oauth2-servers/token-introspection-endpoint/
#
# To prevent token scanning attacks, the endpoint MUST also require
# some form of authorization to access this endpoint, such as client
# authentication as described in OAuth 2.0 [RFC6749] or a separate
# OAuth 2.0 access token such as the bearer token described in OAuth
# 2.0 Bearer Token Usage [RFC6750].
#
def authorize!
# Requested client authorization
if server.credentials
authorize_using_basic_auth!
elsif authorized_token
authorize_using_bearer_token!
else
@error = Errors::InvalidRequest
@invalid_request_reason = :request_not_authorized
end
end
def authorize_using_basic_auth!
# Note that a properly formed and authorized query for an inactive or
# otherwise invalid token (or a token the protected resource is not
# allowed to know about) is not considered an error response by this
# specification. In these cases, the authorization server MUST instead
# respond with an introspection response with the "active" field set to
# "false" as described in Section 2.2.
@error = Errors::InvalidClient unless authorized_client
end
def authorize_using_bearer_token!
# Requested bearer token authorization
#
# If the protected resource uses an OAuth 2.0 bearer token to authorize
# its call to the introspection endpoint and the token used for
# authorization does not contain sufficient privileges or is otherwise
# invalid for this request, the authorization server responds with an
# HTTP 401 code as described in Section 3 of OAuth 2.0 Bearer Token
# Usage [RFC6750].
#
@error = Errors::InvalidToken unless valid_authorized_token?
end
# Client Authentication
def authorized_client
@authorized_client ||= server.credentials && server.client
end
# Bearer Token Authentication
def authorized_token
@authorized_token ||= Doorkeeper.authenticate(server.context.request)
end
# 2.2. Introspection Response
def success_response
response = {
active: true,
scope: @token.scopes_string,
client_id: @token.try(:application).try(:uid),
token_type: @token.token_type,
iat: @token.created_at.to_i,
}
# `exp` is OPTIONAL per RFC 7662 §2.2; omit it for non-expiring tokens
# so clients don't interpret `0` as "expired at 1970-01-01".
response[:exp] = @token.expires_at.to_i if @token.expires_at
customize_response(response)
end
# If the introspection call is properly authorized but the token is not
# active, does not exist on this server, or the protected resource is
# not allowed to introspect this particular token, then the
# authorization server MUST return an introspection response with the
# "active" field set to "false". Note that to avoid disclosing too
# much of the authorization server's state to a third party, the
# authorization server SHOULD NOT include any additional information
# about an inactive token, including why the token is inactive.
#
# @see https://datatracker.ietf.org/doc/html/rfc7662 2.2. Introspection Response
#
def failure_response
{
active: false,
}
end
# Boolean indicator of whether or not the presented token
# is currently active. The specifics of a token's "active" state
# will vary depending on the implementation of the authorization
# server and the information it keeps about its tokens, but a "true"
# value return for the "active" property will generally indicate
# that a given token has been issued by this authorization server,
# has not been revoked by the resource owner, and is within its
# given time window of validity (e.g., after its issuance time and
# before its expiration time).
#
# Any other error is considered an "inactive" token.
#
# * The token requested does not exist or is invalid
# * The token expired
# * The token was issued to a different client than is making this request
#
# Since resource servers using token introspection rely on the
# authorization server to determine the state of a token, the
# authorization server MUST perform all applicable checks against a
# token's state. For instance, these tests include the following:
#
# o If the token can expire, the authorization server MUST determine
# whether or not the token has expired.
# o If the token can be issued before it is able to be used, the
# authorization server MUST determine whether or not a token's valid
# period has started yet.
# o If the token can be revoked after it was issued, the authorization
# server MUST determine whether or not such a revocation has taken
# place.
# o If the token has been signed, the authorization server MUST
# validate the signature.
# o If the token can be used only at certain resource servers, the
# authorization server MUST determine whether or not the token can
# be used at the resource server making the introspection call.
#
def active?
if authorized_client
valid_token? && token_introspection_allowed?(auth_client: authorized_client.application)
else
valid_token?
end
end
# Token can be valid only if it is not expired or revoked.
def valid_token?
@token&.accessible?
end
def valid_authorized_token?
!authorized_token_matches_introspected? &&
authorized_token.accessible? &&
token_introspection_allowed?(auth_token: authorized_token)
end
# RFC7662 Section 2.1
def authorized_token_matches_introspected?
authorized_token.token == @token&.token
end
# Config constraints for introspection in Doorkeeper.config.allow_token_introspection
def token_introspection_allowed?(auth_client: nil, auth_token: nil)
allow_introspection = Doorkeeper.config.allow_token_introspection
return allow_introspection unless allow_introspection.respond_to?(:call)
allow_introspection.call(@token, auth_client, auth_token)
end
# Allows to customize introspection response.
# Provides context (controller) and token for generating developer-specific
# response.
#
# @see https://datatracker.ietf.org/doc/html/rfc7662#section-2.2
#
def customize_response(response)
customized_response = Doorkeeper.config.custom_introspection_response.call(
token,
server.context,
)
return response if customized_response.blank?
response.merge(customized_response)
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/token_request.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class TokenRequest
attr_reader :pre_auth, :resource_owner
def initialize(pre_auth, resource_owner)
@pre_auth = pre_auth
@resource_owner = resource_owner
end
def authorize
auth = Authorization::Token.new(pre_auth, resource_owner)
auth.issue_token!
CodeResponse.new(pre_auth, auth, response_on_fragment: true)
end
def deny
pre_auth.error = Errors::AccessDenied
pre_auth.error_response
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth/token_response.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
class TokenResponse
attr_reader :token
alias issued_token token
def initialize(token)
@token = token
end
def body
@body ||= {
"access_token" => token.plaintext_token,
"token_type" => token.token_type,
"expires_in" => token.expires_in_seconds,
"refresh_token" => token.plaintext_refresh_token,
"scope" => token.scopes_string,
"created_at" => token.created_at.to_i,
}.reject { |_, value| value.blank? }
end
def status
:ok
end
def headers
{
"Cache-Control" => "no-store, no-cache",
"Content-Type" => "application/json; charset=utf-8",
"Pragma" => "no-cache",
}
end
end
end
end
================================================
FILE: lib/doorkeeper/oauth.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module OAuth
GRANT_TYPES = [
AUTHORIZATION_CODE = "authorization_code",
IMPLICIT = "implicit",
PASSWORD = "password",
CLIENT_CREDENTIALS = "client_credentials",
REFRESH_TOKEN = "refresh_token",
].freeze
end
end
================================================
FILE: lib/doorkeeper/orm/active_record/access_grant.rb
================================================
# frozen_string_literal: true
require "doorkeeper/orm/active_record/mixins/access_grant"
module Doorkeeper
class AccessGrant < ::ActiveRecord::Base
include Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant
end
end
================================================
FILE: lib/doorkeeper/orm/active_record/access_token.rb
================================================
# frozen_string_literal: true
require "doorkeeper/orm/active_record/mixins/access_token"
module Doorkeeper
class AccessToken < ::ActiveRecord::Base
include Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken
end
end
================================================
FILE: lib/doorkeeper/orm/active_record/application.rb
================================================
# frozen_string_literal: true
require "doorkeeper/orm/active_record/redirect_uri_validator"
require "doorkeeper/orm/active_record/mixins/application"
module Doorkeeper
class Application < ::ActiveRecord::Base
include ::Doorkeeper::Orm::ActiveRecord::Mixins::Application
end
end
================================================
FILE: lib/doorkeeper/orm/active_record/mixins/access_grant.rb
================================================
# frozen_string_literal: true
module Doorkeeper::Orm::ActiveRecord::Mixins
module AccessGrant
extend ActiveSupport::Concern
included do
self.table_name = compute_doorkeeper_table_name
self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default)
include ::Doorkeeper::AccessGrantMixin
belongs_to :application, class_name: Doorkeeper.config.application_class.to_s,
optional: true,
inverse_of: :access_grants
validates :application_id,
:token,
:expires_in,
:redirect_uri,
presence: true
validates :token, uniqueness: { case_sensitive: true }
before_validation :generate_token, on: :create
# We keep a volatile copy of the raw token for initial communication
# The stored refresh_token may be mapped and not available in cleartext.
#
# Some strategies allow restoring stored secrets (e.g. symmetric encryption)
# while hashing strategies do not, so you cannot rely on this value
# returning a present value for persisted tokens.
def plaintext_token
if secret_strategy.allows_restoring_secrets?
secret_strategy.restore_secret(self, :token)
else
@raw_token
end
end
private
# Generates token value with UniqueToken class.
#
# @return [String] token value
#
def generate_token
@raw_token = Doorkeeper::OAuth::Helpers::UniqueToken.generate
secret_strategy.store_secret(self, :token, @raw_token)
end
end
module ClassMethods
private
def compute_doorkeeper_table_name
table_name = "oauth_access_grant"
table_name = table_name.pluralize if pluralize_table_names
"#{table_name_prefix}#{table_name}#{table_name_suffix}"
end
end
end
end
================================================
FILE: lib/doorkeeper/orm/active_record/mixins/access_token.rb
================================================
# frozen_string_literal: true
module Doorkeeper::Orm::ActiveRecord::Mixins
module AccessToken
extend ActiveSupport::Concern
included do
self.table_name = compute_doorkeeper_table_name
self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default)
include ::Doorkeeper::AccessTokenMixin
belongs_to :application, class_name: Doorkeeper.config.application_class.to_s,
inverse_of: :access_tokens,
optional: true
validates :token, presence: true, uniqueness: { case_sensitive: true }
validates :refresh_token, uniqueness: { case_sensitive: true }, if: :use_refresh_token?
# @attr_writer [Boolean, nil] use_refresh_token
# indicates the possibility of using refresh token
attr_writer :use_refresh_token
before_validation :generate_token, on: :create
before_validation :generate_refresh_token,
on: :create, if: :use_refresh_token?
end
module ClassMethods
# Searches for not revoked Access Tokens associated with the
# specific Resource Owner.
#
# @param resource_owner [ActiveRecord::Base]
# Resource Owner model instance
#
# @return [ActiveRecord::Relation]
# active Access Tokens for Resource Owner
#
def active_for(resource_owner)
by_resource_owner(resource_owner).where(revoked_at: nil)
end
# Determines if refresh tokens should be revoked only when the new access token is used,
# rather than immediately upon refresh. This is based on the presence of the
# `previous_refresh_token` column in the database.
#
# When true (column exists):
# - Refresh tokens are NOT immediately revoked
# - New access token stores the old refresh token value in `previous_refresh_token`
# - Old refresh token is revoked later when the new access token is first used
# - Multiple concurrent refresh requests can succeed (no database locks)
# - Better database performance and lower latency
#
# When false (column does not exist):
# - Refresh tokens are immediately revoked using database locks
# - Only one concurrent refresh request can succeed
# - May experience database lock contention under high load
#
# To enable the revoke-on-use feature and improve performance:
# rails generate doorkeeper:previous_refresh_token
# rails db:migrate
#
# @return [Boolean] true if previous_refresh_token column exists
#
def refresh_token_revoked_on_use?
column_names.include?("previous_refresh_token")
end
# Returns non-expired and non-revoked access tokens
def not_expired
relation = where(revoked_at: nil)
if supports_expiration_time_math?
# have not reached the expiration time or it never expires
relation.where("#{expiration_time_sql} > ?", Time.now.utc).or(
relation.where(expires_in: nil)
)
else
::Kernel.warn(::Doorkeeper::Models::ExpirationTimeSqlMath::WARNING_MESSAGE)
relation
end
end
private
def compute_doorkeeper_table_name
table_name = "oauth_access_token"
table_name = table_name.pluralize if pluralize_table_names
"#{table_name_prefix}#{table_name}#{table_name_suffix}"
end
end
end
end
================================================
FILE: lib/doorkeeper/orm/active_record/mixins/application.rb
================================================
# frozen_string_literal: true
module Doorkeeper::Orm::ActiveRecord::Mixins
module Application
extend ActiveSupport::Concern
included do
self.table_name = compute_doorkeeper_table_name
self.strict_loading_by_default = false if respond_to?(:strict_loading_by_default)
include ::Doorkeeper::ApplicationMixin
has_many :access_grants,
foreign_key: :application_id,
dependent: :delete_all,
class_name: Doorkeeper.config.access_grant_class.to_s
has_many :access_tokens,
foreign_key: :application_id,
dependent: :delete_all,
class_name: Doorkeeper.config.access_token_class.to_s
validates :name, :uid, presence: true
validates :secret, presence: true, if: -> { secret_required? }
validates :uid, uniqueness: { case_sensitive: true }
validates :confidential, inclusion: { in: [true, false] }
validates_with Doorkeeper::RedirectUriValidator, attributes: [:redirect_uri]
validate :scopes_match_configured, if: :enforce_scopes?
before_validation :generate_uid, :generate_secret, on: :create
has_many :authorized_tokens,
-> { where(revoked_at: nil) },
foreign_key: :application_id,
class_name: Doorkeeper.config.access_token_class.to_s
has_many :authorized_applications,
through: :authorized_tokens,
source: :application
# Generates a new secret for this application, intended to be used
# for rotating the secret or in case of compromise.
#
# @return [String] new transformed secret value
#
def renew_secret
@raw_secret = secret_generator.generate
secret_strategy.store_secret(self, :secret, @raw_secret)
end
# We keep a volatile copy of the raw secret for initial communication
# The stored refresh_token may be mapped and not available in cleartext.
#
# Some strategies allow restoring stored secrets (e.g. symmetric encryption)
# while hashing strategies do not, so you cannot rely on this value
# returning a present value for persisted tokens.
def plaintext_secret
if secret_strategy.allows_restoring_secrets?
secret_strategy.restore_secret(self, :secret)
else
@raw_secret
end
end
# Represents client as set of it's attributes in JSON format.
# This is the right way how we want to override ActiveRecord #to_json.
#
# Respects privacy settings and serializes minimum set of attributes
# for public/private clients and full set for authorized owners.
#
# @return [Hash] entity attributes for JSON
#
def as_json(options = {})
# if application belongs to some owner we need to check if it's the same as
# the one passed in the options or check if we render the client as an owner
if (respond_to?(:owner) && owner && owner == options[:current_resource_owner]) ||
options[:as_owner]
# Owners can see all the client attributes, fallback to ActiveModel serialization
super
else
# if application has no owner or it's owner doesn't match one from the options
# we render only minimum set of attributes that could be exposed to a public
only = extract_serializable_attributes(options)
super(options.merge(only: only))
end
end
def authorized_for_resource_owner?(resource_owner)
Doorkeeper.configuration.authorize_resource_owner_for_client.call(self, resource_owner)
end
# We need to hook into this method to allow serializing plan-text secrets
# when secrets hashing enabled.
#
# @param key [String] attribute name
#
def read_attribute_for_serialization(key)
return super unless key.to_s == "secret"
plaintext_secret || secret
end
private
def secret_generator
generator_name = Doorkeeper.config.application_secret_generator
generator = generator_name.constantize
return generator if generator.respond_to?(:generate)
raise Errors::UnableToGenerateToken, "#{generator} does not respond to `.generate`."
rescue NameError
raise Errors::TokenGeneratorNotFound, "#{generator_name} not found"
end
def generate_uid
self.uid = Doorkeeper::OAuth::Helpers::UniqueToken.generate if uid.blank?
end
def generate_secret
return if secret.present? || !secret_required?
renew_secret
end
def scopes_match_configured
if scopes.present? && !Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(
scope_str: scopes.to_s,
server_scopes: Doorkeeper.config.scopes,
)
errors.add(:scopes, :not_match_configured)
end
end
def enforce_scopes?
Doorkeeper.config.enforce_configured_scopes?
end
def secret_required?
confidential? ||
!self.class.columns.detect { |column| column.name == "secret" }&.null
end
# Helper method to extract collection of serializable attribute names
# considering serialization options (like `only`, `except` and so on).
#
# @param options [Hash] serialization options
#
# @return [Array]
# collection of attributes to be serialized using #as_json
#
def extract_serializable_attributes(options = {})
opts = options.try(:dup) || {}
only = Array.wrap(opts[:only]).map(&:to_s)
only = if only.blank?
client_serializable_attributes
else
only & client_serializable_attributes
end
only -= Array.wrap(opts[:except]).map(&:to_s) if opts.key?(:except)
only.uniq
end
# Collection of attributes that could be serialized for public.
# Override this method if you need additional attributes to be serialized.
#
# @return [Array] collection of serializable attributes
#
# NOTE: `serializable_attributes` method already taken by Rails >= 6
#
def client_serializable_attributes
attributes = %w[id name created_at]
attributes << "uid" unless confidential?
attributes
end
end
module ClassMethods
# Returns Applications associated with active (not revoked) Access Tokens
# that are owned by the specific Resource Owner.
#
# @param resource_owner [ActiveRecord::Base]
# Resource Owner model instance
#
# @return [ActiveRecord::Relation]
# Applications authorized for the Resource Owner
#
def authorized_for(resource_owner)
resource_access_tokens = Doorkeeper.config.access_token_model.active_for(resource_owner)
where(id: resource_access_tokens.select(:application_id).distinct)
end
# Revokes AccessToken and AccessGrant records that have not been revoked and
# associated with the specific Application and Resource Owner.
#
# @param resource_owner [ActiveRecord::Base]
# instance of the Resource Owner model
#
def revoke_tokens_and_grants_for(id, resource_owner)
Doorkeeper.config.access_token_model.revoke_all_for(id, resource_owner)
Doorkeeper.config.access_grant_model.revoke_all_for(id, resource_owner)
end
private
def compute_doorkeeper_table_name
table_name = "oauth_application"
table_name = table_name.pluralize if pluralize_table_names
"#{table_name_prefix}#{table_name}#{table_name_suffix}"
end
end
end
end
================================================
FILE: lib/doorkeeper/orm/active_record/redirect_uri_validator.rb
================================================
# frozen_string_literal: true
require "uri"
module Doorkeeper
# ActiveModel validator for redirect URI validation in according
# to OAuth standards and Doorkeeper configuration.
class RedirectUriValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value.blank?
return if Doorkeeper.config.allow_blank_redirect_uri?(record)
record.errors.add(attribute, :blank)
else
value.split.each do |val|
next if oob_redirect_uri?(val)
uri = ::URI.parse(val)
record.errors.add(attribute, :forbidden_uri) if forbidden_uri?(uri)
record.errors.add(attribute, :fragment_present) unless uri.fragment.nil?
record.errors.add(attribute, :unspecified_scheme) if unspecified_scheme?(uri)
record.errors.add(attribute, :relative_uri) if relative_uri?(uri)
record.errors.add(attribute, :secured_uri) if invalid_ssl_uri?(uri)
record.errors.add(attribute, :invalid_uri) if unspecified_host?(uri)
end
end
rescue URI::InvalidURIError
record.errors.add(attribute, :invalid_uri)
end
private
def oob_redirect_uri?(uri)
Doorkeeper::OAuth::NonStandard::IETF_WG_OAUTH2_OOB_METHODS.include?(uri)
end
def forbidden_uri?(uri)
Doorkeeper.config.forbid_redirect_uri.call(uri)
end
def unspecified_scheme?(uri)
return true if uri.opaque.present?
%w[localhost].include?(uri.try(:scheme))
end
def unspecified_host?(uri)
uri.is_a?(URI::HTTP) && uri.host.blank?
end
def relative_uri?(uri)
uri.scheme.nil? && uri.host.blank?
end
def invalid_ssl_uri?(uri)
forces_ssl = Doorkeeper.config.force_ssl_in_redirect_uri
non_https = uri.try(:scheme) == "http"
if forces_ssl.respond_to?(:call)
forces_ssl.call(uri) && non_https
else
forces_ssl && non_https
end
end
end
end
================================================
FILE: lib/doorkeeper/orm/active_record/stale_records_cleaner.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Orm
module ActiveRecord
# Helper class to clear stale and non-active tokens and grants.
# Used by Doorkeeper Rake tasks.
#
class StaleRecordsCleaner
def initialize(base_scope)
@base_scope = base_scope
end
# Clears revoked records
def clean_revoked
table = @base_scope.arel_table
@base_scope
.where.not(revoked_at: nil)
.where(table[:revoked_at].lt(Time.current))
.in_batches(&:delete_all)
end
# Clears expired records
def clean_expired(ttl)
table = @base_scope.arel_table
model_class = @base_scope.is_a?(::ActiveRecord::Relation) ? @base_scope.klass : @base_scope
scope = @base_scope
.where.not(expires_in: nil)
.where(table[:created_at].lt(Time.current - ttl))
if model_class.respond_to?(:supports_expiration_time_math?) && model_class.supports_expiration_time_math?
scope = scope.where("#{model_class.expiration_time_sql} < ?", Time.current)
else
::Kernel.warn(::Doorkeeper::Models::ExpirationTimeSqlMath::WARNING_MESSAGE)
end
scope.in_batches(&:delete_all)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/orm/active_record.rb
================================================
# frozen_string_literal: true
module Doorkeeper
autoload :AccessGrant, "doorkeeper/orm/active_record/access_grant"
autoload :AccessToken, "doorkeeper/orm/active_record/access_token"
autoload :Application, "doorkeeper/orm/active_record/application"
autoload :RedirectUriValidator, "doorkeeper/orm/active_record/redirect_uri_validator"
module Models
autoload :Ownership, "doorkeeper/models/concerns/ownership"
end
# ActiveRecord ORM for Doorkeeper entity models.
# Consists of three main OAuth entities:
# * Access Token
# * Access Grant
# * Application (client)
#
# Do a lazy loading of all the required and configured stuff.
#
module Orm
module ActiveRecord
autoload :StaleRecordsCleaner, "doorkeeper/orm/active_record/stale_records_cleaner"
module Mixins
autoload :AccessGrant, "doorkeeper/orm/active_record/mixins/access_grant"
autoload :AccessToken, "doorkeeper/orm/active_record/mixins/access_token"
autoload :Application, "doorkeeper/orm/active_record/mixins/application"
end
def self.run_hooks
initialize_configured_associations
end
def self.initialize_configured_associations
# NOTE: on_load block is instance_exec'd on ActiveRecord::Base,
# so use fully qualified references (e.g. Doorkeeper.config).
ActiveSupport.on_load(:active_record) do
if Doorkeeper.config.enable_application_owner?
Doorkeeper.config.application_model.include ::Doorkeeper::Models::Ownership
end
Doorkeeper.config.access_grant_model.include ::Doorkeeper::Models::PolymorphicResourceOwner::ForAccessGrant
Doorkeeper.config.access_token_model.include ::Doorkeeper::Models::PolymorphicResourceOwner::ForAccessToken
end
end
end
end
end
================================================
FILE: lib/doorkeeper/rails/helpers.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Rails
module Helpers
def doorkeeper_authorize!(*scopes)
@_doorkeeper_scopes = scopes.presence || Doorkeeper.config.default_scopes
doorkeeper_render_error unless valid_doorkeeper_token?
end
def doorkeeper_unauthorized_render_options(**); end
def doorkeeper_forbidden_render_options(**); end
def valid_doorkeeper_token?
doorkeeper_token&.acceptable?(@_doorkeeper_scopes)
end
private
def doorkeeper_render_error
error = doorkeeper_error
error.raise_exception! if Doorkeeper.config.raise_on_errors?
headers.merge!(error.headers.reject { |k| k == "Content-Type" })
doorkeeper_render_error_with(error)
end
def doorkeeper_render_error_with(error)
options = doorkeeper_render_options(error) || {}
status = doorkeeper_status_for_error(
error, options.delete(:respond_not_found_when_forbidden),
)
if options.blank?
head status
else
options[:status] = status
options[:layout] = false if options[:layout].nil?
render options
end
end
def doorkeeper_error
if doorkeeper_invalid_token_response?
OAuth::InvalidTokenResponse.from_access_token(doorkeeper_token)
else
OAuth::ForbiddenTokenResponse.from_scopes(@_doorkeeper_scopes)
end
end
def doorkeeper_render_options(error)
if doorkeeper_invalid_token_response?
doorkeeper_unauthorized_render_options(error: error)
else
doorkeeper_forbidden_render_options(error: error)
end
end
def doorkeeper_status_for_error(error, respond_not_found_when_forbidden)
if respond_not_found_when_forbidden && error.status == :forbidden
:not_found
else
error.status
end
end
def doorkeeper_invalid_token_response?
!doorkeeper_token || !doorkeeper_token.accessible?
end
def doorkeeper_token
return @doorkeeper_token if defined?(@doorkeeper_token)
@doorkeeper_token = OAuth::Token.authenticate(
request,
*Doorkeeper.config.access_token_methods,
)
end
end
end
end
================================================
FILE: lib/doorkeeper/rails/routes/abstract_router.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Rails
# Abstract router module that implements base behavior
# for generating and mapping Rails routes.
#
# Could be reused in Doorkeeper extensions.
#
module AbstractRouter
extend ActiveSupport::Concern
attr_reader :routes
def initialize(routes, mapper = Mapper.new, &block)
@routes = routes
@mapping = mapper.map(&block)
end
def generate_routes!(**_options)
raise NotImplementedError, "must be redefined for #{self.class.name}!"
end
private
def map_route(name, method)
return if @mapping.skipped?(name)
send(method, @mapping[name])
mapping[name] = @mapping[name]
end
end
end
end
================================================
FILE: lib/doorkeeper/rails/routes/mapper.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Rails
class Routes # :nodoc:
class Mapper
def initialize(mapping = Mapping.new)
@mapping = mapping
end
def map(&block)
instance_eval(&block) if block
@mapping
end
def controllers(controller_names = {})
@mapping.controllers.merge!(controller_names)
end
def skip_controllers(*controller_names)
@mapping.skips = controller_names
end
def as(alias_names = {})
@mapping.as.merge!(alias_names)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/rails/routes/mapping.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Rails
class Routes # :nodoc:
class Mapping
attr_accessor :controllers, :as, :skips
def initialize
@controllers = {
authorizations: "doorkeeper/authorizations",
applications: "doorkeeper/applications",
authorized_applications: "doorkeeper/authorized_applications",
tokens: "doorkeeper/tokens",
token_info: "doorkeeper/token_info",
}
@as = {
authorizations: :authorization,
tokens: :token,
token_info: :token_info,
}
@skips = []
end
def [](routes)
{
controllers: @controllers[routes],
as: @as[routes],
}
end
def skipped?(controller)
@skips.include?(controller)
end
end
end
end
end
================================================
FILE: lib/doorkeeper/rails/routes/registry.rb
================================================
# frozen_string_literal: true
module Doorkeeper
module Rails
class Routes
# Thread-safe registry of any Doorkeeper additional routes.
# Used to allow implementing of Doorkeeper extensions that must
# use their own routes.
#
module Registry
ROUTES_ACCESS_LOCK = Mutex.new
ROUTES_DEFINITION_LOCK = Mutex.new
InvalidRouterClass = Class.new(StandardError)
# Collection of additional registered routes for Doorkeeper.
#
# @return [Array