diff --git a/doorkeeper/.coveralls.yml b/doorkeeper/.coveralls.yml new file mode 100644 index 0000000000..91600595a1 --- /dev/null +++ b/doorkeeper/.coveralls.yml @@ -0,0 +1 @@ +service_name: travis-ci diff --git a/doorkeeper/.github/ISSUE_TEMPLATE.md b/doorkeeper/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..5e4bbdeb2f --- /dev/null +++ b/doorkeeper/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,25 @@ +### 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 version**: + +**Gemfile.lock**: diff --git a/doorkeeper/.github/PULL_REQUEST_TEMPLATE.md b/doorkeeper/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..6b497019a7 --- /dev/null +++ b/doorkeeper/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +### 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 NEWS.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! diff --git a/doorkeeper/.gitignore b/doorkeeper/.gitignore new file mode 100644 index 0000000000..32c3f83431 --- /dev/null +++ b/doorkeeper/.gitignore @@ -0,0 +1,19 @@ +.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 diff --git a/doorkeeper/.hound.yml b/doorkeeper/.hound.yml new file mode 100644 index 0000000000..5d0ff60bbe --- /dev/null +++ b/doorkeeper/.hound.yml @@ -0,0 +1,2 @@ +ruby: + config_file: .rubocop.yml diff --git a/doorkeeper/.rspec b/doorkeeper/.rspec new file mode 100644 index 0000000000..53607ea52b --- /dev/null +++ b/doorkeeper/.rspec @@ -0,0 +1 @@ +--colour diff --git a/doorkeeper/.rubocop.yml b/doorkeeper/.rubocop.yml new file mode 100644 index 0000000000..e1d8271fc9 --- /dev/null +++ b/doorkeeper/.rubocop.yml @@ -0,0 +1,13 @@ +AllCops: + Exclude: + - "spec/dummy/db/*" + +LineLength: + Exclude: + - spec/**/* + +StringLiterals: + Enabled: false + +TrailingBlankLines: + Enabled: true diff --git a/doorkeeper/.travis.yml b/doorkeeper/.travis.yml new file mode 100644 index 0000000000..f28fb56d3f --- /dev/null +++ b/doorkeeper/.travis.yml @@ -0,0 +1,38 @@ +cache: bundler +language: ruby +sudo: false + +rvm: + - 2.1 + - 2.2 + - 2.3 + - 2.4 + - 2.5 + +before_install: + - gem update --system # Need for Ruby 2.5.0. https://github.com/travis-ci/travis-ci/issues/8978 + - gem install bundler -v '~> 1.10' + +gemfile: + - gemfiles/rails_4_2.gemfile + - gemfiles/rails_5_0.gemfile + - gemfiles/rails_5_1.gemfile + - gemfiles/rails_5_2.gemfile + - gemfiles/rails_master.gemfile + +matrix: + exclude: + - gemfile: gemfiles/rails_5_0.gemfile + rvm: 2.1 + - gemfile: gemfiles/rails_5_1.gemfile + rvm: 2.1 + - gemfile: gemfiles/rails_5_2.gemfile + rvm: 2.1 + - gemfile: gemfiles/rails_master.gemfile + rvm: 2.1 + - gemfile: gemfiles/rails_master.gemfile + rvm: 2.2 + - gemfile: gemfiles/rails_master.gemfile + rvm: 2.3 + allow_failures: + - gemfile: gemfiles/rails_master.gemfile diff --git a/doorkeeper/Appraisals b/doorkeeper/Appraisals new file mode 100644 index 0000000000..854ba79a2c --- /dev/null +++ b/doorkeeper/Appraisals @@ -0,0 +1,18 @@ +appraise "rails-4-2" do + gem "rails", "~> 4.2.0" +end + +appraise "rails-5-0" do + gem "rails", "~> 5.0.0" + gem "rspec-rails", "~> 3.5" +end + +appraise "rails-5-1" do + gem "rails", "~> 5.1.0" + gem "rspec-rails", "~> 3.5" +end + +appraise "rails-master" do + gem "rails", git: 'https://github.com/rails/rails' + gem "arel", git: 'https://github.com/rails/arel' +end diff --git a/doorkeeper/CODE_OF_CONDUCT.md b/doorkeeper/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..bd6787de0f --- /dev/null +++ b/doorkeeper/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# 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/ diff --git a/doorkeeper/CONTRIBUTING.md b/doorkeeper/CONTRIBUTING.md new file mode 100644 index 0000000000..27d2c3062a --- /dev/null +++ b/doorkeeper/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing + +We love pull requests from everyone. By participating in this project, you agree +to abide by the thoughtbot [code of conduct]. + +[code of conduct]: https://thoughtbot.com/open-source-code-of-conduct + +Fork, then clone the repo: + + git clone git@github.com:your-username/doorkeeper.git + +Set up Ruby dependencies via Bundler + + bundle install + +Make sure the tests pass: + + rake + +Make your change. +Write tests. +Follow our [style guide][style]. +Make the tests pass: + +[style]: https://github.com/thoughtbot/guides/tree/master/style + + rake + +Add notes on your change to the `NEWS.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 [Hound] catches style violations, +fix them. + +[hound]: https://houndci.com + +Wait for us. +We try to at least comment on pull requests within one business day. +We may suggest changes. + +Thank you for your contribution! diff --git a/doorkeeper/Gemfile b/doorkeeper/Gemfile new file mode 100644 index 0000000000..0aa8ad4426 --- /dev/null +++ b/doorkeeper/Gemfile @@ -0,0 +1,10 @@ +source "https://rubygems.org" + +gem "rails", "~> 5.1" + +gem "appraisal" + +gem "activerecord-jdbcsqlite3-adapter", platform: :jruby +gem "sqlite3", platform: [:ruby, :mswin, :mingw, :x64_mingw] +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] +gemspec diff --git a/doorkeeper/MIT-LICENSE b/doorkeeper/MIT-LICENSE new file mode 100644 index 0000000000..12cb693b5a --- /dev/null +++ b/doorkeeper/MIT-LICENSE @@ -0,0 +1,20 @@ +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. diff --git a/doorkeeper/NEWS.md b/doorkeeper/NEWS.md new file mode 100644 index 0000000000..0e497d2f82 --- /dev/null +++ b/doorkeeper/NEWS.md @@ -0,0 +1,647 @@ +# News + +User-visible changes worth mentioning. + +## master + +## 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. +- [#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 diff --git a/doorkeeper/README.md b/doorkeeper/README.md new file mode 100644 index 0000000000..e3afdedc5f --- /dev/null +++ b/doorkeeper/README.md @@ -0,0 +1,487 @@ +# Doorkeeper - awesome OAuth 2 provider for your Rails app. + +[![Gem Version](https://badge.fury.io/rb/doorkeeper.svg)](https://rubygems.org/gems/doorkeeper) +[![Build Status](https://travis-ci.org/doorkeeper-gem/doorkeeper.svg?branch=master)](https://travis-ci.org/doorkeeper-gem/doorkeeper) +[![Dependency Status](https://gemnasium.com/doorkeeper-gem/doorkeeper.svg?travis)](https://gemnasium.com/doorkeeper-gem/doorkeeper) +[![Code Climate](https://codeclimate.com/github/doorkeeper-gem/doorkeeper.svg)](https://codeclimate.com/github/doorkeeper-gem/doorkeeper) +[![Coverage Status](https://coveralls.io/repos/github/doorkeeper-gem/doorkeeper/badge.svg?branch=master)](https://coveralls.io/github/doorkeeper-gem/doorkeeper?branch=master) +[![Security](https://hakiri.io/github/doorkeeper-gem/doorkeeper/master.svg)](https://hakiri.io/github/doorkeeper-gem/doorkeeper/master) + +Doorkeeper is a gem that makes it easy to introduce OAuth 2 provider +functionality to your Rails or Grape application. + +Supported features: + +- [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749) + - [Authorization Code Flow](http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-4.1) + - [Access Token Scopes](http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.3) + - [Refresh token](http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-1.5) + - [Implicit grant](http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-4.2) + - [Resource Owner Password Credentials](http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-4.3) + - [Client Credentials](http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-4.4) +- [OAuth 2.0 Token Revocation](http://tools.ietf.org/html/rfc7009) +- [OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662) + +## Documentation valid for `master` branch + +Please check the documentation for the version of doorkeeper you are using in: +https://github.com/doorkeeper-gem/doorkeeper/releases + +- See the [wiki](https://github.com/doorkeeper-gem/doorkeeper/wiki) +- For general questions, please post in [Stack Overflow](http://stackoverflow.com/questions/tagged/doorkeeper) +- See [SECURITY.md](SECURITY.md) for this project's security disclose + policy + +## Table of Contents + + + + +- [Installation](#installation) +- [Configuration](#configuration) + - [ORM](#orm) + - [Active Record](#active-record) + - [MongoDB](#mongodb) + - [Sequel](#sequel) + - [Couchbase](#couchbase) + - [Routes](#routes) + - [Authenticating](#authenticating) + - [Internationalization (I18n)](#internationalization-i18n) +- [Protecting resources with OAuth (a.k.a your API endpoint)](#protecting-resources-with-oauth-aka-your-api-endpoint) + - [Ruby on Rails controllers](#ruby-on-rails-controllers) + - [Grape endpoints](#grape-endpoints) + - [Route Constraints and other integrations](#route-constraints-and-other-integrations) + - [Access Token Scopes](#access-token-scopes) + - [Custom Access Token Generator](#custom-access-token-generator) + - [Authenticated resource owner](#authenticated-resource-owner) + - [Applications list](#applications-list) +- [Other customizations](#other-customizations) +- [Testing](#testing) +- [Upgrading](#upgrading) +- [Development](#development) +- [Contributing](#contributing) +- [Other resources](#other-resources) + - [Wiki](#wiki) + - [Screencast](#screencast) + - [Client applications](#client-applications) + - [Contributors](#contributors) + - [IETF Standards](#ietf-standards) + - [License](#license) + + + +## Installation + +Put this in your Gemfile: + +``` ruby +gem 'doorkeeper' +``` + +Run the installation generator with: + + rails generate doorkeeper:install + +This will install the doorkeeper initializer into `config/initializers/doorkeeper.rb`. + +## Configuration + +### ORM + +#### Active Record + +By default doorkeeper is configured to use Active Record, so to start you have +to generate the migration tables (supports Rails >= 5 migrations versioning): + + rails generate doorkeeper:migration + +You may want to add foreign keys to your migration. For example, if you plan on +using `User` as the resource owner, add the following line to the migration file +for each table that includes a `resource_owner_id` column: + +```ruby +add_foreign_key :table_name, :users, column: :resource_owner_id +``` + +Then run migrations: + +```sh +rake db:migrate +``` + +Remember to add associations to your model so the related records are deleted. +If you don't do this an `ActiveRecord::InvalidForeignKey`-error will be raised +when you try to destroy a model with related access grants or access tokens. + +```ruby +class User < ApplicationRecord + has_many :access_grants, class_name: "Doorkeeper::AccessGrant", + foreign_key: :resource_owner_id, + dependent: :delete_all # or :destroy if you need callbacks + + has_many :access_tokens, class_name: "Doorkeeper::AccessToken", + foreign_key: :resource_owner_id, + dependent: :delete_all # or :destroy if you need callbacks +end +``` + +#### MongoDB + +See [doorkeeper-mongodb project] for Mongoid and MongoMapper support. Follow along +the implementation in that repository to extend doorkeeper with other ORMs. + +[doorkeeper-mongodb project]: https://github.com/doorkeeper-gem/doorkeeper-mongodb + +#### Sequel + +If you are using [Sequel gem] then you can add [doorkeeper-sequel extension] to your project. +Follow configuration instructions for setting up the necessary Doorkeeper ORM. + +[Sequel gem]: https://github.com/jeremyevans/sequel/ +[doorkeeper-sequel extension]: https://github.com/nbulaj/doorkeeper-sequel + +#### Couchbase + +Use [doorkeeper-couchbase] extension if you are using Couchbase database. + +[doorkeeper-couchbase]: https://github.com/acaprojects/doorkeeper-couchbase + +### Routes + +The installation script will also automatically add the Doorkeeper routes into +your app, like this: + +``` ruby +Rails.application.routes.draw do + use_doorkeeper + # your routes +end +``` + +This will mount following routes: + + GET /oauth/authorize/native?code + GET /oauth/authorize + POST /oauth/authorize + DELETE /oauth/authorize + POST /oauth/token + POST /oauth/revoke + POST /oauth/introspect + resources /oauth/applications + GET /oauth/authorized_applications + DELETE /oauth/authorized_applications/:id + GET /oauth/token/info + +For more information on how to customize routes, check out [this page on the +wiki](https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-routes). + +### Authenticating + +You need to configure Doorkeeper in order to provide `resource_owner` model +and authentication block in `config/initializers/doorkeeper.rb`: + +``` ruby +Doorkeeper.configure do + resource_owner_authenticator do + User.find_by(id: session[:current_user_id]) || redirect_to(login_url) + end +end +``` + +This code is run in the context of your application so you have access to your +models, session or routes helpers. However, since this code is not run in the +context of your application's `ApplicationController` it doesn't have access to +the methods defined over there. + +You may want to check other ways of authentication +[here](https://github.com/doorkeeper-gem/doorkeeper/wiki/Authenticating-using-Clearance-or-DIY). + +### Internationalization (I18n) + +See language files in [the I18n repository](https://github.com/doorkeeper-gem/doorkeeper-i18n). + +## Protecting resources with OAuth (a.k.a your API endpoint) + +### Ruby on Rails controllers + +To protect your controllers (usual one or `ActionController::API`) with OAuth, +you just need to setup `before_action`s specifying the actions you want to +protect. For example: + +``` ruby +class Api::V1::ProductsController < Api::V1::ApiController + before_action :doorkeeper_authorize! # Require access token for all actions + + # your actions +end +``` + +You can pass any option `before_action` accepts, such as `if`, `only`, +`except`, and others. + +### Grape endpoints + +Starting from version 2.2 Doorkeeper provides helpers for the +[Grape framework] >= 0.10. One of them is `doorkeeper_authorize!` that +can be used in a similar way as an example above to protect your API +with OAuth. Note that you have to use `require 'doorkeeper/grape/helpers'` +and `helpers Doorkeeper::Grape::Helpers` in your Grape API class. + +For more information about integration with Grape see the [Wiki]. + +[Grape framework]: https://github.com/ruby-grape/grape +[Wiki]: https://github.com/doorkeeper-gem/doorkeeper/wiki/Grape-Integration + +``` ruby +require 'doorkeeper/grape/helpers' + +module API + module V1 + class Users < Grape::API + helpers Doorkeeper::Grape::Helpers + + before do + doorkeeper_authorize! + end + + # route_setting :scopes, ['user:email'] - for old versions of Grape + get :emails, scopes: [:user, :write] do + [{'email' => current_user.email}] + end + + # ... + end + end +end +``` + +### Route Constraints and other integrations + +You can leverage the `Doorkeeper.authenticate` facade to easily extract a +`Doorkeeper::OAuth::Token` based on the current request. You can then ensure +that token is still good, find its associated `#resource_owner_id`, etc. + +```ruby +module Constraint + class Authenticated + + def matches?(request) + token = Doorkeeper.authenticate(request) + token && token.accessible? + end + end +end +``` + +For more information about integration and other integrations, check out [the +related wiki +page](https://github.com/doorkeeper-gem/doorkeeper/wiki/ActionController::Metal-with-doorkeeper). + +### Access Token Scopes + +You can also require the access token to have specific scopes in certain +actions: + +First configure the scopes in `initializers/doorkeeper.rb` + +```ruby +Doorkeeper.configure do + default_scopes :public # if no scope was requested, this will be the default + optional_scopes :admin, :write +end +``` + +And in your controllers: + +```ruby +class Api::V1::ProductsController < Api::V1::ApiController + before_action -> { doorkeeper_authorize! :public }, only: :index + before_action only: [:create, :update, :destroy] do + doorkeeper_authorize! :admin, :write + end +end +``` + +Please note that there is a logical OR between multiple required scopes. In the +above example, `doorkeeper_authorize! :admin, :write` means that the access +token is required to have either `:admin` scope or `:write` scope, but does not +need have both of them. + +If you want to require the access token to have multiple scopes at the same +time, use multiple `doorkeeper_authorize!`, for example: + +```ruby +class Api::V1::ProductsController < Api::V1::ApiController + before_action -> { doorkeeper_authorize! :public }, only: :index + before_action only: [:create, :update, :destroy] do + doorkeeper_authorize! :admin + doorkeeper_authorize! :write + end +end +``` + +In the above example, a client can call `:create` action only if its access token +has both `:admin` and `:write` scopes. + +### Custom Access Token Generator + +By default a 128 bit access token will be generated. If you require a custom +token, such as [JWT](http://jwt.io), specify an object that responds to +`.generate(options = {})` and returns a string to be used as the token. + +```ruby +Doorkeeper.configure do + access_token_generator "Doorkeeper::JWT" +end +``` + +JWT token support is available with +[Doorkeeper-JWT](https://github.com/chriswarren/doorkeeper-jwt). + +### Custom Base Controller + +By default Doorkeeper's main controller `Doorkeeper::ApplicationController` +inherits from `ActionController::Base`. You may want to use your own +controller to inherit from, to keep Doorkeeper controllers in the same +context than the rest your app: + +```ruby +Doorkeeper.configure do + base_controller 'ApplicationController' +end +``` + +### Authenticated resource owner + +If you want to return data based on the current resource owner, in other +words, the access token owner, you may want to define a method in your +controller that returns the resource owner instance: + +``` ruby +class Api::V1::CredentialsController < Api::V1::ApiController + before_action :doorkeeper_authorize! + respond_to :json + + # GET /me.json + def me + respond_with current_resource_owner + end + + private + + # Find the user that owns the access token + def current_resource_owner + User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token + end +end +``` + +In this example, we're returning the credentials (`me.json`) of the access +token owner. + +### Applications list + +By default, the applications list (`/oauth/applications`) is publicly available. +To protect the endpoint you should uncomment these lines: + +```ruby +# config/initializers/doorkeeper.rb +Doorkeeper.configure do + admin_authenticator do |routes| + Admin.find_by(id: session[:admin_id]) || redirect_to(routes.new_admin_session_url) + end +end +``` + +The logic is the same as the `resource_owner_authenticator` block. **Note:** +since the application list is just a scaffold, it's recommended to either +customize the controller used by the list or skip the controller all together. +For more information see the page +[in the wiki](https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-routes). + +## Other customizations + +- [Associate users to OAuth applications (ownership)](https://github.com/doorkeeper-gem/doorkeeper/wiki/Associate-users-to-OAuth-applications-%28ownership%29) +- [CORS - Cross Origin Resource Sharing](https://github.com/doorkeeper-gem/doorkeeper/wiki/%5BCORS%5D-Cross-Origin-Resource-Sharing) +- see more on [Wiki page](https://github.com/doorkeeper-gem/doorkeeper/wiki) + +## Testing + +You can use Doorkeeper models in your application test suite. Note that starting from +Doorkeeper 4.3.0 it uses [ActiveSupport lazy loading hooks](http://api.rubyonrails.org/classes/ActiveSupport/LazyLoadHooks.html) +to load models. There are [known issue](https://github.com/doorkeeper-gem/doorkeeper/issues/1043) +with the `factory_bot_rails` gem (it executes factories building before `ActiveRecord::Base` +is initialized using hooks in gem railtie, so you can catch a `uninitialized constant` error). +It is recommended to use pure `factory_bot` gem to solve this problem. + +## Upgrading + +If you want to upgrade doorkeeper to a new version, check out the [upgrading +notes](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions) +and take a look at the +[changelog](https://github.com/doorkeeper-gem/doorkeeper/blob/master/NEWS.md). + +Doorkeeper follows [semantic versioning](http://semver.org/). + +## Development + +To run the local engine server: + +``` +bundle install +bundle exec rails server +```` + +By default, it uses the latest Rails version with ActiveRecord. To run the +tests with a specific ORM and Rails version: + +``` +rails=4.2.0 orm=active_record bundle exec rake +``` + +## 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](https://github.com/doorkeeper-gem/doorkeeper/wiki/Contributing). + +## Other resources + +### Wiki + +You can find everything about Doorkeeper in our [wiki +here](https://github.com/doorkeeper-gem/doorkeeper/wiki). + +### Screencast + +Check out this screencast from [railscasts.com](http://railscasts.com/): [#353 +OAuth with +Doorkeeper](http://railscasts.com/episodes/353-oauth-with-doorkeeper) + +### Client applications + +After you set up the provider, 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). + +### Contributors + +Thanks to all our [awesome +contributors](https://github.com/doorkeeper-gem/doorkeeper/graphs/contributors)! + +### IETF Standards + +* [The OAuth 2.0 Authorization Framework](http://tools.ietf.org/html/rfc6749) +* [OAuth 2.0 Threat Model and Security Considerations](http://tools.ietf.org/html/rfc6819) +* [OAuth 2.0 Token Revocation](http://tools.ietf.org/html/rfc7009) + +### License + +MIT License. Copyright 2011 Applicake. diff --git a/doorkeeper/RELEASING.md b/doorkeeper/RELEASING.md new file mode 100644 index 0000000000..54b789d8c2 --- /dev/null +++ b/doorkeeper/RELEASING.md @@ -0,0 +1,10 @@ +# Releasing doorkeeper + +How to release doorkeeper in five easy steps! + +1. Update `lib/doorkeeper/version.rb` file accordingly. +2. Update `NEWS.md` to reflect the changes since last release. +3. Commit changes: `git commit -am 'Bump to vVERSION'` +4. Run `rake release` +5. Announce the new release, making sure to say “thank you” to the contributors + who helped shape this version! diff --git a/doorkeeper/Rakefile b/doorkeeper/Rakefile new file mode 100644 index 0000000000..e5f2d5242a --- /dev/null +++ b/doorkeeper/Rakefile @@ -0,0 +1,20 @@ +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 +end + +Bundler::GemHelper.install_tasks diff --git a/doorkeeper/SECURITY.md b/doorkeeper/SECURITY.md new file mode 100644 index 0000000000..0b313e91d0 --- /dev/null +++ b/doorkeeper/SECURITY.md @@ -0,0 +1,15 @@ +# 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! diff --git a/doorkeeper/app/assets/stylesheets/doorkeeper/admin/application.css b/doorkeeper/app/assets/stylesheets/doorkeeper/admin/application.css new file mode 100644 index 0000000000..31ab17abc4 --- /dev/null +++ b/doorkeeper/app/assets/stylesheets/doorkeeper/admin/application.css @@ -0,0 +1,10 @@ +/* + *= require doorkeeper/bootstrap.min + * + *= require_self + *= require_tree . +*/ + +td { + vertical-align: middle !important; +} diff --git a/doorkeeper/app/assets/stylesheets/doorkeeper/application.css b/doorkeeper/app/assets/stylesheets/doorkeeper/application.css new file mode 100644 index 0000000000..88abe3e5ec --- /dev/null +++ b/doorkeeper/app/assets/stylesheets/doorkeeper/application.css @@ -0,0 +1,64 @@ +/* + *= 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; +} diff --git a/doorkeeper/app/controllers/doorkeeper/application_controller.rb b/doorkeeper/app/controllers/doorkeeper/application_controller.rb new file mode 100644 index 0000000000..bee9a15f84 --- /dev/null +++ b/doorkeeper/app/controllers/doorkeeper/application_controller.rb @@ -0,0 +1,11 @@ +module Doorkeeper + class ApplicationController < + Doorkeeper.configuration.base_controller.constantize + + include Helpers::Controller + + protect_from_forgery with: :exception + + helper 'doorkeeper/dashboard' + end +end diff --git a/doorkeeper/app/controllers/doorkeeper/application_metal_controller.rb b/doorkeeper/app/controllers/doorkeeper/application_metal_controller.rb new file mode 100644 index 0000000000..9f84e79bbd --- /dev/null +++ b/doorkeeper/app/controllers/doorkeeper/application_metal_controller.rb @@ -0,0 +1,17 @@ +module Doorkeeper + class ApplicationMetalController < ActionController::Metal + MODULES = [ + ActionController::Instrumentation, + AbstractController::Rendering, + ActionController::Rendering, + ActionController::Renderers::All, + Helpers::Controller + ].freeze + + MODULES.each do |mod| + include mod + end + + ActiveSupport.run_load_hooks(:doorkeeper_metal_controller, self) + end +end diff --git a/doorkeeper/app/controllers/doorkeeper/applications_controller.rb b/doorkeeper/app/controllers/doorkeeper/applications_controller.rb new file mode 100644 index 0000000000..f06adabe49 --- /dev/null +++ b/doorkeeper/app/controllers/doorkeeper/applications_controller.rb @@ -0,0 +1,63 @@ +module Doorkeeper + class ApplicationsController < Doorkeeper::ApplicationController + layout 'doorkeeper/admin' + + before_action :authenticate_admin! + before_action :set_application, only: [:show, :edit, :update, :destroy] + + def index + @applications = if Application.respond_to?(:ordered_by) + Application.ordered_by(:created_at) + else + ActiveSupport::Deprecation.warn <<-MSG.squish + Doorkeeper #{Doorkeeper.configuration.orm} extension must implement #ordered_by + method for it's models as it will be used by default in Doorkeeper 5. + MSG + + Application.all + end + end + + def show; end + + def new + @application = Application.new + end + + def create + @application = Application.new(application_params) + if @application.save + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) + redirect_to oauth_application_url(@application) + else + render :new + end + end + + def edit; end + + def update + if @application.update_attributes(application_params) + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :update]) + redirect_to oauth_application_url(@application) + else + render :edit + end + end + + def destroy + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy]) if @application.destroy + redirect_to oauth_applications_url + end + + private + + def set_application + @application = Application.find(params[:id]) + end + + def application_params + params.require(:doorkeeper_application).permit(:name, :redirect_uri, :scopes) + end + end +end diff --git a/doorkeeper/app/controllers/doorkeeper/authorizations_controller.rb b/doorkeeper/app/controllers/doorkeeper/authorizations_controller.rb new file mode 100644 index 0000000000..8406c9e1d6 --- /dev/null +++ b/doorkeeper/app/controllers/doorkeeper/authorizations_controller.rb @@ -0,0 +1,57 @@ +module Doorkeeper + class AuthorizationsController < Doorkeeper::ApplicationController + before_action :authenticate_resource_owner! + + def new + if pre_auth.authorizable? + if skip_authorization? || matching_token? + auth = authorization.authorize + redirect_to auth.redirect_uri + else + render :new + end + else + render :error + end + end + + # TODO: Handle raise invalid authorization + def create + redirect_or_render authorization.authorize + end + + def destroy + redirect_or_render authorization.deny + end + + private + + def matching_token? + AccessToken.matching_token_for pre_auth.client, + current_resource_owner.id, + pre_auth.scopes + end + + def redirect_or_render(auth) + if auth.redirectable? + redirect_to auth.redirect_uri + else + render json: auth.body, status: auth.status + end + end + + def pre_auth + @pre_auth ||= OAuth::PreAuthorization.new(Doorkeeper.configuration, + server.client_via_uid, + params) + end + + def authorization + @authorization ||= strategy.request + end + + def strategy + @strategy ||= server.authorization_request pre_auth.response_type + end + end +end diff --git a/doorkeeper/app/controllers/doorkeeper/authorized_applications_controller.rb b/doorkeeper/app/controllers/doorkeeper/authorized_applications_controller.rb new file mode 100644 index 0000000000..258ad65ac8 --- /dev/null +++ b/doorkeeper/app/controllers/doorkeeper/authorized_applications_controller.rb @@ -0,0 +1,14 @@ +module Doorkeeper + class AuthorizedApplicationsController < Doorkeeper::ApplicationController + before_action :authenticate_resource_owner! + + def index + @applications = Application.authorized_for(current_resource_owner) + end + + def destroy + AccessToken.revoke_all_for params[:id], current_resource_owner + redirect_to oauth_authorized_applications_url, notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy]) + end + end +end diff --git a/doorkeeper/app/controllers/doorkeeper/token_info_controller.rb b/doorkeeper/app/controllers/doorkeeper/token_info_controller.rb new file mode 100644 index 0000000000..79249d5b71 --- /dev/null +++ b/doorkeeper/app/controllers/doorkeeper/token_info_controller.rb @@ -0,0 +1,13 @@ +module Doorkeeper + class TokenInfoController < Doorkeeper::ApplicationMetalController + def show + if doorkeeper_token && doorkeeper_token.accessible? + render json: doorkeeper_token, status: :ok + else + error = OAuth::ErrorResponse.new(name: :invalid_request) + response.headers.merge!(error.headers) + render json: error.body, status: error.status + end + end + end +end diff --git a/doorkeeper/app/controllers/doorkeeper/tokens_controller.rb b/doorkeeper/app/controllers/doorkeeper/tokens_controller.rb new file mode 100644 index 0000000000..9ba0459b81 --- /dev/null +++ b/doorkeeper/app/controllers/doorkeeper/tokens_controller.rb @@ -0,0 +1,93 @@ +module Doorkeeper + class TokensController < Doorkeeper::ApplicationMetalController + def create + response = authorize_response + headers.merge! response.headers + self.response_body = response.body.to_json + self.status = response.status + rescue Errors::DoorkeeperError => e + handle_token_exception e + end + + # OAuth 2.0 Token Revocation - http://tools.ietf.org/html/rfc7009 + def revoke + # The authorization server, if applicable, first authenticates the client + # and checks its ownership of the provided token. + # + # Doorkeeper does not use the token_type_hint logic described in the + # RFC 7009 due to the refresh token implementation that is a field in + # the access token model. + if authorized? + revoke_token + end + + # The authorization server responds with HTTP status code 200 if the token + # has been revoked successfully or if the client submitted an invalid + # token + render json: {}, status: 200 + end + + def introspect + introspection = OAuth::TokenIntrospection.new(server, token) + + if introspection.authorized? + render json: introspection.to_json, status: 200 + else + error = OAuth::ErrorResponse.new(name: introspection.error) + response.headers.merge!(error.headers) + render json: error.body, status: error.status + end + end + + private + + # OAuth 2.0 Section 2.1 defines two client types, "public" & "confidential". + # Public clients (as per RFC 7009) do not require authentication whereas + # confidential clients must be authenticated for their token revocation. + # + # Once a confidential 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 determines the client type implicitly via the presence of the + # OAuth client associated with a given access or refresh token. Since public + # clients authenticate the resource owner via "password" or "implicit" grant + # types, they set the application_id as null (since the claim cannot be + # verified). + # + # https://tools.ietf.org/html/rfc6749#section-2.1 + # https://tools.ietf.org/html/rfc7009 + def authorized? + if token.present? + # Client is confidential, therefore client authentication & authorization + # is required + if token.application_id? + # We authorize client by checking token's application + server.client && server.client.application == token.application + else + # Client is public, authentication unnecessary + true + end + end + end + + def revoke_token + if token.accessible? + token.revoke + end + end + + def token + @token ||= AccessToken.by_token(request.POST['token']) || + AccessToken.by_refresh_token(request.POST['token']) + end + + def strategy + @strategy ||= server.token_request params[:grant_type] + end + + def authorize_response + @authorize_response ||= strategy.authorize + end + end +end diff --git a/doorkeeper/app/helpers/doorkeeper/dashboard_helper.rb b/doorkeeper/app/helpers/doorkeeper/dashboard_helper.rb new file mode 100644 index 0000000000..f2412a25f7 --- /dev/null +++ b/doorkeeper/app/helpers/doorkeeper/dashboard_helper.rb @@ -0,0 +1,19 @@ +module Doorkeeper + module DashboardHelper + def doorkeeper_errors_for(object, method) + if object.errors[method].present? + output = object.errors[method].map do |msg| + content_tag(:span, class: 'help-block') do + msg.capitalize + end + end + + safe_join(output) + end + end + + def doorkeeper_submit_path(application) + application.persisted? ? oauth_application_path(application) : oauth_applications_path + end + end +end diff --git a/doorkeeper/app/validators/redirect_uri_validator.rb b/doorkeeper/app/validators/redirect_uri_validator.rb new file mode 100644 index 0000000000..3b9b52ae42 --- /dev/null +++ b/doorkeeper/app/validators/redirect_uri_validator.rb @@ -0,0 +1,44 @@ +require 'uri' + +class RedirectUriValidator < ActiveModel::EachValidator + def self.native_redirect_uri + Doorkeeper.configuration.native_redirect_uri + end + + def validate_each(record, attribute, value) + if value.blank? + record.errors.add(attribute, :blank) + else + value.split.each do |val| + uri = ::URI.parse(val) + next if native_redirect_uri?(uri) + record.errors.add(attribute, :forbidden_uri) if forbidden_uri?(uri) + record.errors.add(attribute, :fragment_present) unless uri.fragment.nil? + record.errors.add(attribute, :relative_uri) if uri.scheme.nil? || uri.host.nil? + record.errors.add(attribute, :secured_uri) if invalid_ssl_uri?(uri) + end + end + rescue URI::InvalidURIError + record.errors.add(attribute, :invalid_uri) + end + + private + + def native_redirect_uri?(uri) + self.class.native_redirect_uri.present? && uri.to_s == self.class.native_redirect_uri.to_s + end + + def forbidden_uri?(uri) + Doorkeeper.configuration.forbid_redirect_uri.call(uri) + end + + def invalid_ssl_uri?(uri) + forces_ssl = Doorkeeper.configuration.force_ssl_in_redirect_uri + + if forces_ssl.respond_to?(:call) + forces_ssl.call(uri) + else + forces_ssl && uri.try(:scheme) == 'http' + end + end +end diff --git a/doorkeeper/app/views/doorkeeper/applications/_delete_form.html.erb b/doorkeeper/app/views/doorkeeper/applications/_delete_form.html.erb new file mode 100644 index 0000000000..719f8261fb --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/applications/_delete_form.html.erb @@ -0,0 +1,4 @@ +<%- 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 %> diff --git a/doorkeeper/app/views/doorkeeper/applications/_form.html.erb b/doorkeeper/app/views/doorkeeper/applications/_form.html.erb new file mode 100644 index 0000000000..8e25102058 --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/applications/_form.html.erb @@ -0,0 +1,47 @@ +<%= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f| %> + <% if application.errors.any? %> +

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

+ <% end %> + + <%= content_tag :div, class: "form-group#{' has-error' if application.errors[:name].present?}" do %> + <%= f.label :name, class: 'col-sm-2 control-label' %> +
+ <%= f.text_field :name, class: 'form-control' %> + <%= doorkeeper_errors_for application, :name %> +
+ <% end %> + + <%= content_tag :div, class: "form-group#{' has-error' if application.errors[:redirect_uri].present?}" do %> + <%= f.label :redirect_uri, class: 'col-sm-2 control-label' %> +
+ <%= f.text_area :redirect_uri, class: 'form-control' %> + <%= doorkeeper_errors_for application, :redirect_uri %> + + <%= t('doorkeeper.applications.help.redirect_uri') %> + + <% if Doorkeeper.configuration.native_redirect_uri %> + + <%= raw t('doorkeeper.applications.help.native_redirect_uri', native_redirect_uri: content_tag(:code) { Doorkeeper.configuration.native_redirect_uri }) %> + + <% end %> +
+ <% end %> + + <%= content_tag :div, class: "form-group#{' has-error' if application.errors[:scopes].present?}" do %> + <%= f.label :scopes, class: 'col-sm-2 control-label' %> +
+ <%= f.text_field :scopes, class: 'form-control' %> + <%= doorkeeper_errors_for application, :scopes %> + + <%= t('doorkeeper.applications.help.scopes') %> + +
+ <% end %> + +
+
+ <%= f.submit t('doorkeeper.applications.buttons.submit'), class: "btn btn-primary" %> + <%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path, class: "btn btn-default" %> +
+
+<% end %> diff --git a/doorkeeper/app/views/doorkeeper/applications/edit.html.erb b/doorkeeper/app/views/doorkeeper/applications/edit.html.erb new file mode 100644 index 0000000000..05bddd2e4d --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/applications/edit.html.erb @@ -0,0 +1,5 @@ + + +<%= render 'form', application: @application %> diff --git a/doorkeeper/app/views/doorkeeper/applications/index.html.erb b/doorkeeper/app/views/doorkeeper/applications/index.html.erb new file mode 100644 index 0000000000..4a3df83058 --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/applications/index.html.erb @@ -0,0 +1,26 @@ + + +

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

+ + + + + + + + + + + + <% @applications.each do |application| %> + + + + + + + <% end %> + +
<%= t('.name') %><%= t('.callback_url') %>
<%= link_to application.name, oauth_application_path(application) %><%= application.redirect_uri %><%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %><%= render 'delete_form', application: application %>
diff --git a/doorkeeper/app/views/doorkeeper/applications/new.html.erb b/doorkeeper/app/views/doorkeeper/applications/new.html.erb new file mode 100644 index 0000000000..05bddd2e4d --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/applications/new.html.erb @@ -0,0 +1,5 @@ + + +<%= render 'form', application: @application %> diff --git a/doorkeeper/app/views/doorkeeper/applications/show.html.erb b/doorkeeper/app/views/doorkeeper/applications/show.html.erb new file mode 100644 index 0000000000..283e56c481 --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/applications/show.html.erb @@ -0,0 +1,39 @@ + + +
+
+

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

+

<%= @application.uid %>

+ +

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

+

<%= @application.secret %>

+ +

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

+

<%= @application.scopes %>

+ +

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

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

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

+ +

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

+ +

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

+
+
diff --git a/doorkeeper/app/views/doorkeeper/authorizations/error.html.erb b/doorkeeper/app/views/doorkeeper/authorizations/error.html.erb new file mode 100644 index 0000000000..2247c0d54c --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/authorizations/error.html.erb @@ -0,0 +1,7 @@ + + +
+
<%= @pre_auth.error_response.body[:error_description] %>
+
diff --git a/doorkeeper/app/views/doorkeeper/authorizations/new.html.erb b/doorkeeper/app/views/doorkeeper/authorizations/new.html.erb new file mode 100644 index 0000000000..015b51bf8f --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/authorizations/new.html.erb @@ -0,0 +1,40 @@ + + +
+

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

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

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

+ + +
+ <% end %> + +
+ <%= form_tag oauth_authorization_path, method: :post do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success btn-lg btn-block" %> + <% end %> + <%= form_tag oauth_authorization_path, method: :delete do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger btn-lg btn-block" %> + <% end %> +
+
diff --git a/doorkeeper/app/views/doorkeeper/authorizations/show.html.erb b/doorkeeper/app/views/doorkeeper/authorizations/show.html.erb new file mode 100644 index 0000000000..f4d6610193 --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/authorizations/show.html.erb @@ -0,0 +1,7 @@ + + +
+ <%= params[:code] %> +
diff --git a/doorkeeper/app/views/doorkeeper/authorized_applications/_delete_form.html.erb b/doorkeeper/app/views/doorkeeper/authorized_applications/_delete_form.html.erb new file mode 100644 index 0000000000..512e8ece3b --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/authorized_applications/_delete_form.html.erb @@ -0,0 +1,4 @@ +<%- submit_btn_css ||= 'btn btn-link' %> +<%= form_tag oauth_authorized_application_path(application), method: :delete do %> + <%= submit_tag t('doorkeeper.authorized_applications.buttons.revoke'), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %> +<% end %> diff --git a/doorkeeper/app/views/doorkeeper/authorized_applications/index.html.erb b/doorkeeper/app/views/doorkeeper/authorized_applications/index.html.erb new file mode 100644 index 0000000000..a3f5aaac31 --- /dev/null +++ b/doorkeeper/app/views/doorkeeper/authorized_applications/index.html.erb @@ -0,0 +1,24 @@ + + +
+ + + + + + + + + + <% @applications.each do |application| %> + + + + + + <% end %> + +
<%= t('doorkeeper.authorized_applications.index.application') %><%= t('doorkeeper.authorized_applications.index.created_at') %>
<%= application.name %><%= application.created_at.strftime(t('doorkeeper.authorized_applications.index.date_format')) %><%= render 'delete_form', application: application %>
+
diff --git a/doorkeeper/app/views/layouts/doorkeeper/admin.html.erb b/doorkeeper/app/views/layouts/doorkeeper/admin.html.erb new file mode 100644 index 0000000000..926542b8a7 --- /dev/null +++ b/doorkeeper/app/views/layouts/doorkeeper/admin.html.erb @@ -0,0 +1,37 @@ + + + + + + + Doorkeeper + <%= stylesheet_link_tag "doorkeeper/admin/application" %> + <%= csrf_meta_tags %> + + + +
+ <%- if flash[:notice].present? %> +
+ <%= flash[:notice] %> +
+ <% end -%> + + <%= yield %> +
+ + diff --git a/doorkeeper/app/views/layouts/doorkeeper/application.html.erb b/doorkeeper/app/views/layouts/doorkeeper/application.html.erb new file mode 100644 index 0000000000..562005af09 --- /dev/null +++ b/doorkeeper/app/views/layouts/doorkeeper/application.html.erb @@ -0,0 +1,23 @@ + + + + <%= t('doorkeeper.layouts.application.title') %> + + + + + <%= stylesheet_link_tag "doorkeeper/application" %> + <%= csrf_meta_tags %> + + +
+ <%- if flash[:notice].present? %> +
+ <%= flash[:notice] %> +
+ <% end -%> + + <%= yield %> +
+ + diff --git a/doorkeeper/config/locales/en.yml b/doorkeeper/config/locales/en.yml new file mode 100644 index 0000000000..cf68f52522 --- /dev/null +++ b/doorkeeper/config/locales/en.yml @@ -0,0 +1,122 @@ +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.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + forbidden_uri: 'is forbidden by 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: + redirect_uri: 'Use one line per URI' + native_redirect_uri: 'Use %{native_redirect_uri} if you want to add localhost URIs for development purposes' + 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' + new: + title: 'New Application' + show: + title: 'Application: %{name}' + application_id: 'Application Id' + secret: 'Secret' + scopes: 'Scopes' + callback_urls: 'Callback urls' + actions: 'Actions' + + 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' + + 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' + + errors: + messages: + # Common error messages + invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match 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.' + 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.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + + # 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" + + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' + + layouts: + admin: + nav: + oauth2_provider: 'OAuth2 Provider' + applications: 'Applications' + home: 'Home' + application: + title: 'OAuth authorization required' diff --git a/doorkeeper/doorkeeper.gemspec b/doorkeeper/doorkeeper.gemspec new file mode 100644 index 0000000000..6dc0710851 --- /dev/null +++ b/doorkeeper/doorkeeper.gemspec @@ -0,0 +1,30 @@ +$LOAD_PATH.push File.expand_path("../lib", __FILE__) + +require "doorkeeper/version" + +Gem::Specification.new do |s| + s.name = "doorkeeper" + s.version = Doorkeeper.gem_version + s.authors = ["Felipe Elias Philipp", "Tute Costa", "Jon Moss", "Nikita Bulai"] + s.email = %w(bulaj.nikita@gmail.com) + s.homepage = "https://github.com/doorkeeper-gem/doorkeeper" + s.summary = "OAuth 2 provider for Rails and Grape" + s.description = "Doorkeeper is an OAuth 2 provider for Rails and Grape." + s.license = 'MIT' + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- spec/*`.split("\n") + s.require_paths = ["lib"] + + s.add_dependency "railties", ">= 4.2" + s.required_ruby_version = ">= 2.1" + + s.add_development_dependency "capybara" + s.add_development_dependency "coveralls" + s.add_development_dependency "grape" + s.add_development_dependency "database_cleaner", "~> 1.6" + s.add_development_dependency "factory_bot", "~> 4.8" + s.add_development_dependency "generator_spec", "~> 0.9.3" + s.add_development_dependency "rake", ">= 11.3.0" + s.add_development_dependency "rspec-rails" +end diff --git a/doorkeeper/gemfiles/rails_4_2.gemfile b/doorkeeper/gemfiles/rails_4_2.gemfile new file mode 100644 index 0000000000..28d35b9558 --- /dev/null +++ b/doorkeeper/gemfiles/rails_4_2.gemfile @@ -0,0 +1,13 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 4.2.0" +gem "appraisal" +gem "activerecord-jdbcsqlite3-adapter", platform: :jruby +gem "sqlite3", platform: [:ruby, :mswin, :mingw, :x64_mingw] +gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] +# Older Grape requires Ruby >= 2.2.2 +gem "grape", '~> 0.16', '< 0.19.2' + +gemspec path: "../" diff --git a/doorkeeper/gemfiles/rails_5_0.gemfile b/doorkeeper/gemfiles/rails_5_0.gemfile new file mode 100644 index 0000000000..67c320e3a2 --- /dev/null +++ b/doorkeeper/gemfiles/rails_5_0.gemfile @@ -0,0 +1,12 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 5.0.0" +gem "appraisal" +gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby +gem "sqlite3", platforms: [:ruby, :mswin, :mingw, :x64_mingw] +gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] +gem "rspec-rails", "~> 3.5" + +gemspec path: "../" diff --git a/doorkeeper/gemfiles/rails_5_1.gemfile b/doorkeeper/gemfiles/rails_5_1.gemfile new file mode 100644 index 0000000000..181e367f2b --- /dev/null +++ b/doorkeeper/gemfiles/rails_5_1.gemfile @@ -0,0 +1,12 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "~> 5.1.0" +gem "appraisal" +gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby +gem "sqlite3", platforms: [:ruby, :mswin, :mingw, :x64_mingw] +gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] +gem "rspec-rails", "~> 3.7" + +gemspec path: "../" diff --git a/doorkeeper/gemfiles/rails_5_2.gemfile b/doorkeeper/gemfiles/rails_5_2.gemfile new file mode 100644 index 0000000000..543bb7e11a --- /dev/null +++ b/doorkeeper/gemfiles/rails_5_2.gemfile @@ -0,0 +1,12 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", "5.2.0.rc1" +gem "appraisal" +gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby +gem "sqlite3", platforms: [:ruby, :mswin, :mingw, :x64_mingw] +gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] +gem "rspec-rails", "~> 3.7" + +gemspec path: "../" diff --git a/doorkeeper/gemfiles/rails_master.gemfile b/doorkeeper/gemfiles/rails_master.gemfile new file mode 100644 index 0000000000..7073374594 --- /dev/null +++ b/doorkeeper/gemfiles/rails_master.gemfile @@ -0,0 +1,14 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rails", git: 'https://github.com/rails/rails' +gem "arel", git: 'https://github.com/rails/arel' + +gem "appraisal" +gem "activerecord-jdbcsqlite3-adapter", platform: :jruby +gem "sqlite3", platform: [:ruby, :mswin, :mingw, :x64_mingw] +gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw] +gem "rspec-rails", "~> 3.7" + +gemspec path: "../" diff --git a/doorkeeper/lib/doorkeeper.rb b/doorkeeper/lib/doorkeeper.rb new file mode 100644 index 0000000000..858c37e799 --- /dev/null +++ b/doorkeeper/lib/doorkeeper.rb @@ -0,0 +1,75 @@ +require 'doorkeeper/version' +require 'doorkeeper/engine' +require 'doorkeeper/config' + +require 'doorkeeper/errors' +require 'doorkeeper/server' +require 'doorkeeper/request' +require 'doorkeeper/validations' + +require 'doorkeeper/oauth/authorization/code' +require 'doorkeeper/oauth/authorization/token' +require 'doorkeeper/oauth/authorization/uri_builder' +require 'doorkeeper/oauth/helpers/scope_checker' +require 'doorkeeper/oauth/helpers/uri_checker' +require 'doorkeeper/oauth/helpers/unique_token' + +require 'doorkeeper/oauth/scopes' +require 'doorkeeper/oauth/error' +require 'doorkeeper/oauth/base_response' +require 'doorkeeper/oauth/code_response' +require 'doorkeeper/oauth/token_response' +require 'doorkeeper/oauth/error_response' +require 'doorkeeper/oauth/pre_authorization' +require 'doorkeeper/oauth/base_request' +require 'doorkeeper/oauth/authorization_code_request' +require 'doorkeeper/oauth/refresh_token_request' +require 'doorkeeper/oauth/password_access_token_request' +require 'doorkeeper/oauth/client_credentials_request' +require 'doorkeeper/oauth/code_request' +require 'doorkeeper/oauth/token_request' +require 'doorkeeper/oauth/client' +require 'doorkeeper/oauth/token' +require 'doorkeeper/oauth/token_introspection' +require 'doorkeeper/oauth/invalid_token_response' +require 'doorkeeper/oauth/forbidden_token_response' + +require 'doorkeeper/models/concerns/orderable' +require 'doorkeeper/models/concerns/scopes' +require 'doorkeeper/models/concerns/expirable' +require 'doorkeeper/models/concerns/revocable' +require 'doorkeeper/models/concerns/accessible' + +require 'doorkeeper/models/access_grant_mixin' +require 'doorkeeper/models/access_token_mixin' +require 'doorkeeper/models/application_mixin' + +require 'doorkeeper/helpers/controller' + +require 'doorkeeper/rails/routes' +require 'doorkeeper/rails/helpers' + +require 'doorkeeper/orm/active_record' + +require 'active_support/deprecation' + +module Doorkeeper + def self.configured? + ActiveSupport::Deprecation.warn "Method `Doorkeeper#configured?` has been deprecated without replacement." + @config.present? + end + + def self.database_installed? + ActiveSupport::Deprecation.warn "Method `Doorkeeper#database_installed?` has been deprecated without replacement." + [AccessToken, AccessGrant, Application].all?(&:table_exists?) + end + + def self.installed? + ActiveSupport::Deprecation.warn "Method `Doorkeeper#installed?` has been deprecated without replacement." + configured? && database_installed? + end + + def self.authenticate(request, methods = Doorkeeper.configuration.access_token_methods) + OAuth::Token.authenticate(request, *methods) + end +end diff --git a/doorkeeper/lib/doorkeeper/config.rb b/doorkeeper/lib/doorkeeper/config.rb new file mode 100644 index 0000000000..f98672e743 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/config.rb @@ -0,0 +1,319 @@ +module Doorkeeper + class MissingConfiguration < StandardError + # Defines a MissingConfiguration error for a missing Doorkeeper + # configuration + def initialize + super('Configuration for doorkeeper missing. Do you have doorkeeper initializer?') + end + end + + def self.configure(&block) + @config = Config::Builder.new(&block).build + setup_orm_adapter + setup_orm_models + setup_application_owner if @config.enable_application_owner? + end + + def self.configuration + @config || (fail MissingConfiguration) + end + + def self.setup_orm_adapter + @orm_adapter = "doorkeeper/orm/#{configuration.orm}".classify.constantize + rescue NameError => e + fail e, "ORM adapter not found (#{configuration.orm})", <<-ERROR_MSG.squish +[doorkeeper] ORM adapter not found (#{configuration.orm}), or there was an error +trying to load it. + +You probably need to add the related gem for this adapter to work with +doorkeeper. + ERROR_MSG + end + + def self.setup_orm_models + @orm_adapter.initialize_models! + end + + def self.setup_application_owner + @orm_adapter.initialize_application_owner! + end + + class Config + class Builder + def initialize(&block) + @config = Config.new + instance_eval(&block) + end + + def build + @config + end + + # 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 + + # 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 + + # 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) + 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 by default) + def use_refresh_token + @config.instance_variable_set(:@refresh_token_enabled, true) + 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 + end + + 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 + # + # ==== 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.instance_eval do + remove_method name if method_defined?(name) + define_method name do |*args, &block| + # TODO: is builder_class option being used? + value = if attribute_builder + attribute_builder.new(&block).build + else + block ? 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 + end + + extend Option + + 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: ->(_routes) {} + + option :resource_owner_from_credentials, + default: (lambda do |_routes| + ::Rails.logger.warn(I18n.t('doorkeeper.errors.messages.credential_flow_not_configured')) + nil + end) + option :before_successful_strategy_response, default: ->(_request) {} + option :after_successful_strategy_response, + default: ->(_request, _response) {} + option :skip_authorization, default: ->(_routes) {} + option :access_token_expires_in, default: 7200 + option :custom_access_token_expires_in, default: ->(_app) { 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' + option :active_record_options, default: {} + option :grant_flows, default: %w[authorization_code client_credentials] + + # 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://github.com/doorkeeper-gem/doorkeeper#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' + + # The controller Doorkeeper::ApplicationController inherits from. + # Defaults to ActionController::Base. + # https://github.com/doorkeeper-gem/doorkeeper#custom-base-controller + # + # @param base_controller [String] the name of the base controller + option :base_controller, + default: 'ActionController::Base' + + attr_reader :reuse_access_token + + def refresh_token_enabled? + @refresh_token_enabled ||= false + !!@refresh_token_enabled + end + + def enable_application_owner? + @enable_application_owner ||= false + !!@enable_application_owner + end + + def confirm_application_owner? + @confirm_application_owner ||= false + !!@confirm_application_owner + 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 client_credentials_methods + @client_credentials ||= %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 authorization_response_types + @authorization_response_types ||= calculate_authorization_response_types + end + + def token_grant_types + @token_grant_types ||= calculate_token_grant_types + end + + private + + # Determines what values are acceptable for 'response_type' param in + # authorization request endpoint, and return them as an array of strings. + # + def calculate_authorization_response_types + types = [] + types << 'code' if grant_flows.include? 'authorization_code' + types << 'token' if grant_flows.include? 'implicit' + types + end + + # Determines what values are acceptable for 'grant_type' param token + # request endpoint, and return them in array. + # + def calculate_token_grant_types + types = grant_flows - ['implicit'] + types << 'refresh_token' if refresh_token_enabled? + types + end + end +end diff --git a/doorkeeper/lib/doorkeeper/engine.rb b/doorkeeper/lib/doorkeeper/engine.rb new file mode 100644 index 0000000000..2244ef29d4 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/engine.rb @@ -0,0 +1,27 @@ +module Doorkeeper + class Engine < Rails::Engine + initializer "doorkeeper.params.filter" do |app| + parameters = %w[client_secret code authentication_token access_token refresh_token] + app.config.filter_parameters << /^(#{Regexp.union parameters})$/ + 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 + + if defined?(Sprockets) && Sprockets::VERSION.chr.to_i >= 4 + initializer 'doorkeeper.assets.precompile' do |app| + app.config.assets.precompile += %w[ + doorkeeper/application.css + doorkeeper/admin/application.css + ] + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/errors.rb b/doorkeeper/lib/doorkeeper/errors.rb new file mode 100644 index 0000000000..1f3b278630 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/errors.rb @@ -0,0 +1,45 @@ +module Doorkeeper + module Errors + class DoorkeeperError < StandardError + def type + message + end + end + + class InvalidAuthorizationStrategy < DoorkeeperError + def type + :unsupported_response_type + end + end + + class InvalidTokenReuse < DoorkeeperError + def type + :invalid_request + end + end + + class InvalidGrantReuse < DoorkeeperError + def type + :invalid_grant + end + end + + class InvalidTokenStrategy < DoorkeeperError + def type + :unsupported_grant_type + end + end + + class MissingRequestStrategy < DoorkeeperError + def type + :invalid_request + end + end + + class UnableToGenerateToken < DoorkeeperError + end + + class TokenGeneratorNotFound < DoorkeeperError + end + end +end diff --git a/doorkeeper/lib/doorkeeper/grape/authorization_decorator.rb b/doorkeeper/lib/doorkeeper/grape/authorization_decorator.rb new file mode 100644 index 0000000000..f5b71a3981 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/grape/authorization_decorator.rb @@ -0,0 +1,17 @@ +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 diff --git a/doorkeeper/lib/doorkeeper/grape/helpers.rb b/doorkeeper/lib/doorkeeper/grape/helpers.rb new file mode 100644 index 0000000000..8d6a3520a7 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/grape/helpers.rb @@ -0,0 +1,52 @@ +require 'doorkeeper/grape/authorization_decorator' + +module Doorkeeper + module Grape + 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 && !scopes.empty? + 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.configuration.access_token_methods + ) + end + + def decorated_request + AuthorizationDecorator.new(request) + end + + def error_status_codes + { + unauthorized: 401, + forbidden: 403 + } + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/helpers/controller.rb b/doorkeeper/lib/doorkeeper/helpers/controller.rb new file mode 100644 index 0000000000..c2a72f1953 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/helpers/controller.rb @@ -0,0 +1,56 @@ +# Define methods that can be called in any controller that inherits from +# Doorkeeper::ApplicationMetalController or Doorkeeper::ApplicationController +module Doorkeeper + module Helpers + module Controller + private + + # :doc: + def authenticate_resource_owner! + current_resource_owner + end + + # :doc: + def current_resource_owner + instance_eval(&Doorkeeper.configuration.authenticate_resource_owner) + end + + def resource_owner_from_credentials + instance_eval(&Doorkeeper.configuration.resource_owner_from_credentials) + end + + # :doc: + def authenticate_admin! + instance_eval(&Doorkeeper.configuration.authenticate_admin) + end + + def server + @server ||= Server.new(self) + end + + # :doc: + def doorkeeper_token + @token ||= OAuth::Token.authenticate request, *config_methods + end + + def config_methods + @methods ||= Doorkeeper.configuration.access_token_methods + end + + def get_error_response_from_exception(exception) + OAuth::ErrorResponse.new name: exception.type, state: params[:state] + 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.configuration.skip_authorization) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/models/access_grant_mixin.rb b/doorkeeper/lib/doorkeeper/models/access_grant_mixin.rb new file mode 100644 index 0000000000..f2c58f9d33 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/models/access_grant_mixin.rb @@ -0,0 +1,26 @@ +module Doorkeeper + module AccessGrantMixin + extend ActiveSupport::Concern + + include OAuth::Helpers + include Models::Expirable + include Models::Revocable + include Models::Accessible + include Models::Orderable + include Models::Scopes + + 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(token: token.to_s) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/models/access_token_mixin.rb b/doorkeeper/lib/doorkeeper/models/access_token_mixin.rb new file mode 100644 index 0000000000..9881122f9b --- /dev/null +++ b/doorkeeper/lib/doorkeeper/models/access_token_mixin.rb @@ -0,0 +1,246 @@ +module Doorkeeper + module AccessTokenMixin + extend ActiveSupport::Concern + + include OAuth::Helpers + include Models::Expirable + include Models::Revocable + include Models::Accessible + include Models::Orderable + include Models::Scopes + + module ClassMethods + # Returns an instance of the Doorkeeper::AccessToken with + # specific token value. + # + # @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 by_token(token) + find_by(token: token.to_s) + 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(refresh_token: refresh_token.to_s) + 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] + # instance of the Resource Owner model + # + def revoke_all_for(application_id, resource_owner, clock = Time) + where(application_id: application_id, + resource_owner_id: resource_owner.id, + revoked_at: nil). + update_all(revoked_at: clock.now.utc) + end + + # Looking for not expired 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_or_id [ActiveRecord::Base, Integer] + # Resource Owner model instance or it's ID + # @param scopes [String, Doorkeeper::OAuth::Scopes] + # set of scopes + # + # @return [Doorkeeper::AccessToken, nil] Access Token instance or + # nil if matching record was not found + # + def matching_token_for(application, resource_owner_or_id, scopes) + resource_owner_id = if resource_owner_or_id.respond_to?(:to_key) + resource_owner_or_id.id + else + resource_owner_or_id + end + token = last_authorized_token_for(application.try(:id), resource_owner_id) + if token && scopes_match?(token.scopes, scopes, application.try(:scopes)) + token + end + end + + # Checks whether the token scopes match the scopes from the parameters or + # Application scopes (if present). + # + # @param token_scopes [#to_s] + # set of scopes (any object that responds to `#to_s`) + # @param param_scopes [String] + # scopes from params + # @param app_scopes [String] + # Application scopes + # + # @return [Boolean] true if all scopes are blank or matches + # and false in other cases + # + def scopes_match?(token_scopes, param_scopes, app_scopes) + (!token_scopes.present? && !param_scopes.present?) || + Doorkeeper::OAuth::Helpers::ScopeChecker.match?( + token_scopes.to_s, + param_scopes, + app_scopes + ) + 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_id [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 expires_in [Integer] + # token lifetime in seconds + # @param use_refresh_token [Boolean] + # whether to use the refresh token + # + # @return [Doorkeeper::AccessToken] existing record or a new one + # + def find_or_create_for(application, resource_owner_id, scopes, expires_in, use_refresh_token) + if Doorkeeper.configuration.reuse_access_token + access_token = matching_token_for(application, resource_owner_id, scopes) + if access_token && !access_token.expired? + return access_token + end + end + + create!( + application_id: application.try(:id), + resource_owner_id: resource_owner_id, + scopes: scopes.to_s, + expires_in: expires_in, + use_refresh_token: use_refresh_token + ) + end + + # Looking for not revoked Access Token record that belongs to specific + # Application and Resource Owner. + # + # @param application_id [Integer] + # ID of the Application model instance + # @param resource_owner_id [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_id) + ordered_by(:created_at, :desc). + find_by(application_id: application_id, + resource_owner_id: resource_owner_id, + revoked_at: nil) + end + end + + # Access Token type: Bearer. + # @see https://tools.ietf.org/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, + scopes: scopes, + expires_in_seconds: expires_in_seconds, + application: { uid: application.try(:uid) }, + created_at: created_at.to_i + } + 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 && + resource_owner_id == access_token.resource_owner_id + 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 + + private + + # Generates refresh token with UniqueToken generator. + # + # @return [String] refresh token value + # + def generate_refresh_token + self.refresh_token = UniqueToken.generate + end + + # Generates and sets the token value with the + # configured Generator class (see Doorkeeper.configuration). + # + # @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 + + self.token = token_generator.generate( + resource_owner_id: resource_owner_id, + scopes: scopes, + application: application, + expires_in: expires_in, + created_at: created_at + ) + end + + def token_generator + generator_name = Doorkeeper.configuration.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 diff --git a/doorkeeper/lib/doorkeeper/models/application_mixin.rb b/doorkeeper/lib/doorkeeper/models/application_mixin.rb new file mode 100644 index 0000000000..d4514ac60b --- /dev/null +++ b/doorkeeper/lib/doorkeeper/models/application_mixin.rb @@ -0,0 +1,44 @@ +module Doorkeeper + module ApplicationMixin + extend ActiveSupport::Concern + + include OAuth::Helpers + include Models::Orderable + include Models::Scopes + + module ClassMethods + # Returns an instance of the Doorkeeper::Application with + # specific UID and secret. + # + # @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) + find_by(uid: uid.to_s, secret: secret.to_s) + 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 + 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) seperated by newlines. + def redirect_uri=(uris) + super(uris.is_a?(Array) ? uris.join("\n") : uris) + end + end +end diff --git a/doorkeeper/lib/doorkeeper/models/concerns/accessible.rb b/doorkeeper/lib/doorkeeper/models/concerns/accessible.rb new file mode 100644 index 0000000000..f2f0044f9c --- /dev/null +++ b/doorkeeper/lib/doorkeeper/models/concerns/accessible.rb @@ -0,0 +1,13 @@ +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 diff --git a/doorkeeper/lib/doorkeeper/models/concerns/expirable.rb b/doorkeeper/lib/doorkeeper/models/concerns/expirable.rb new file mode 100644 index 0000000000..541a099313 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/models/concerns/expirable.rb @@ -0,0 +1,32 @@ +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] expiration time in UTC + # + def expires_at + created_at + expires_in.seconds + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/models/concerns/orderable.rb b/doorkeeper/lib/doorkeeper/models/concerns/orderable.rb new file mode 100644 index 0000000000..072c8c42cb --- /dev/null +++ b/doorkeeper/lib/doorkeeper/models/concerns/orderable.rb @@ -0,0 +1,13 @@ +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 diff --git a/doorkeeper/lib/doorkeeper/models/concerns/ownership.rb b/doorkeeper/lib/doorkeeper/models/concerns/ownership.rb new file mode 100644 index 0000000000..535e4622bf --- /dev/null +++ b/doorkeeper/lib/doorkeeper/models/concerns/ownership.rb @@ -0,0 +1,21 @@ +module Doorkeeper + module Models + module Ownership + extend ActiveSupport::Concern + + included do + belongs_to_options = { polymorphic: true } + if defined?(ActiveRecord::Base) && ActiveRecord::VERSION::MAJOR >= 5 + belongs_to_options[:optional] = true + end + + belongs_to :owner, belongs_to_options + validates :owner, presence: true, if: :validate_owner? + end + + def validate_owner? + Doorkeeper.configuration.confirm_application_owner? + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/models/concerns/revocable.rb b/doorkeeper/lib/doorkeeper/models/concerns/revocable.rb new file mode 100644 index 0000000000..fac80c217d --- /dev/null +++ b/doorkeeper/lib/doorkeeper/models/concerns/revocable.rb @@ -0,0 +1,48 @@ +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) + update_attribute :revoked_at, clock.now.utc + 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 + + # Revokes token with `:refresh_token` equal to `:previous_refresh_token` + # and clears `:previous_refresh_token` attribute. + # + def revoke_previous_refresh_token! + return unless refresh_token_revoked_on_use? + old_refresh_token.revoke if old_refresh_token + update_attribute :previous_refresh_token, "" + 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 ||= + AccessToken.by_refresh_token(previous_refresh_token) + end + + def refresh_token_revoked_on_use? + AccessToken.refresh_token_revoked_on_use? + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/models/concerns/scopes.rb b/doorkeeper/lib/doorkeeper/models/concerns/scopes.rb new file mode 100644 index 0000000000..56f8619c39 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/models/concerns/scopes.rb @@ -0,0 +1,17 @@ +module Doorkeeper + module Models + module Scopes + def scopes + OAuth::Scopes.from_string(self[:scopes]) + end + + def scopes_string + self[:scopes] + end + + def includes_scope?(*required_scopes) + required_scopes.blank? || required_scopes.any? { |s| scopes.exists?(s.to_s) } + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/authorization/code.rb b/doorkeeper/lib/doorkeeper/oauth/authorization/code.rb new file mode 100644 index 0000000000..1479f6936b --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/authorization/code.rb @@ -0,0 +1,32 @@ +module Doorkeeper + module OAuth + module Authorization + class Code + attr_accessor :pre_auth, :resource_owner, :token + + def initialize(pre_auth, resource_owner) + @pre_auth = pre_auth + @resource_owner = resource_owner + end + + def issue_token + @token ||= AccessGrant.create!( + application_id: pre_auth.client.id, + resource_owner_id: resource_owner.id, + expires_in: configuration.authorization_code_expires_in, + redirect_uri: pre_auth.redirect_uri, + scopes: pre_auth.scopes.to_s + ) + end + + def native_redirect + { action: :show, code: token.token } + end + + def configuration + Doorkeeper.configuration + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/authorization/token.rb b/doorkeeper/lib/doorkeeper/oauth/authorization/token.rb new file mode 100644 index 0000000000..22a9f3908b --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/authorization/token.rb @@ -0,0 +1,60 @@ +module Doorkeeper + module OAuth + module Authorization + class Token + attr_accessor :pre_auth, :resource_owner, :token + + class << self + def access_token_expires_in(server, pre_auth_or_oauth_client) + if (expiration = custom_expiration(server, pre_auth_or_oauth_client)) + expiration + else + server.access_token_expires_in + end + end + + private + + def custom_expiration(server, pre_auth_or_oauth_client) + oauth_client = if pre_auth_or_oauth_client.respond_to?(:client) + pre_auth_or_oauth_client.client + else + pre_auth_or_oauth_client + end + + server.custom_access_token_expires_in.call(oauth_client) + end + end + + def initialize(pre_auth, resource_owner) + @pre_auth = pre_auth + @resource_owner = resource_owner + end + + def issue_token + @token ||= AccessToken.find_or_create_for( + pre_auth.client, + resource_owner.id, + pre_auth.scopes, + self.class.access_token_expires_in(configuration, pre_auth), + false + ) + end + + def native_redirect + { + controller: 'doorkeeper/token_info', + action: :show, + access_token: token.token + } + end + + private + + def configuration + Doorkeeper.configuration + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/authorization/uri_builder.rb b/doorkeeper/lib/doorkeeper/oauth/authorization/uri_builder.rb new file mode 100644 index 0000000000..d8546aa424 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/authorization/uri_builder.rb @@ -0,0 +1,31 @@ +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 = parameters.reject { |_, v| v.blank? } + Rack::Utils.build_query parameters + end + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/authorization_code_request.rb b/doorkeeper/lib/doorkeeper/oauth/authorization_code_request.rb new file mode 100644 index 0000000000..80428745a0 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/authorization_code_request.rb @@ -0,0 +1,56 @@ +module Doorkeeper + module OAuth + class AuthorizationCodeRequest < BaseRequest + validate :attributes, error: :invalid_request + validate :client, error: :invalid_client + validate :grant, error: :invalid_grant + # @see https://tools.ietf.org/html/rfc6749#section-5.2 + validate :redirect_uri, error: :invalid_grant + + attr_accessor :server, :grant, :client, :redirect_uri, :access_token + + def initialize(server, grant, client, parameters = {}) + @server = server + @client = client + @grant = grant + @redirect_uri = parameters[:redirect_uri] + end + + private + + def before_successful_response + grant.transaction do + grant.lock! + raise Errors::InvalidGrantReuse if grant.revoked? + + grant.revoke + find_or_create_access_token(grant.application, + grant.resource_owner_id, + grant.scopes, + server) + end + super + end + + def validate_attributes + redirect_uri.present? + end + + def validate_client + !!client + 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 + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/base_request.rb b/doorkeeper/lib/doorkeeper/oauth/base_request.rb new file mode 100644 index 0000000000..cd85a8ec2f --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/base_request.rb @@ -0,0 +1,55 @@ +module Doorkeeper + module OAuth + class BaseRequest + include Validations + + def authorize + validate + + if valid? + before_successful_response + @response = TokenResponse.new(access_token) + after_successful_response + @response + else + @response = ErrorResponse.from_request(self) + end + end + + def scopes + @scopes ||= if @original_scopes.present? + OAuth::Scopes.from_string(@original_scopes) + else + default_scopes + end + end + + def default_scopes + server.default_scopes + end + + def valid? + error.nil? + end + + def find_or_create_access_token(client, resource_owner_id, scopes, server) + @access_token = AccessToken.find_or_create_for( + client, + resource_owner_id, + scopes, + Authorization::Token.access_token_expires_in(server, client), + server.refresh_token_enabled? + ) + end + + def before_successful_response + Doorkeeper.configuration.before_successful_strategy_response.call(self) + end + + def after_successful_response + Doorkeeper.configuration.after_successful_strategy_response. + call(self, @response) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/base_response.rb b/doorkeeper/lib/doorkeeper/oauth/base_response.rb new file mode 100644 index 0000000000..dd7c1bebd7 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/base_response.rb @@ -0,0 +1,29 @@ +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 diff --git a/doorkeeper/lib/doorkeeper/oauth/client.rb b/doorkeeper/lib/doorkeeper/oauth/client.rb new file mode 100644 index 0000000000..0a0e61decc --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/client.rb @@ -0,0 +1,29 @@ +require 'doorkeeper/oauth/client/credentials' + +module Doorkeeper + module OAuth + class Client + attr_accessor :application + + delegate :id, :name, :uid, :redirect_uri, :scopes, to: :@application + + def initialize(application) + @application = application + end + + def self.find(uid, method = Application.method(:by_uid)) + if (application = method.call(uid)) + new(application) + end + end + + def self.authenticate(credentials, method = Application.method(:by_uid_and_secret)) + return false if credentials.blank? + + if (application = method.call(credentials.uid, credentials.secret)) + new(application) + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/client/credentials.rb b/doorkeeper/lib/doorkeeper/oauth/client/credentials.rb new file mode 100644 index 0000000000..3afb1ef28d --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/client/credentials.rb @@ -0,0 +1,32 @@ +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 |credentials, method| + method = self.method(method) if method.is_a?(Symbol) + credentials = Credentials.new(*method.call(request)) + break credentials unless credentials.blank? + 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 (.*)/m + Base64.decode64(Regexp.last_match(1)).split(/:/, 2) + end + end + end + + def blank? + uid.blank? || secret.blank? + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/client_credentials/creator.rb b/doorkeeper/lib/doorkeeper/oauth/client_credentials/creator.rb new file mode 100644 index 0000000000..e9ae204c2e --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/client_credentials/creator.rb @@ -0,0 +1,13 @@ +module Doorkeeper + module OAuth + class ClientCredentialsRequest < BaseRequest + class Creator + def call(client, scopes, attributes = {}) + AccessToken.find_or_create_for( + client, nil, scopes, attributes[:expires_in], + attributes[:use_refresh_token]) + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/client_credentials/issuer.rb b/doorkeeper/lib/doorkeeper/oauth/client_credentials/issuer.rb new file mode 100644 index 0000000000..5304d6f530 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/client_credentials/issuer.rb @@ -0,0 +1,40 @@ +require 'doorkeeper/oauth/client_credentials/validation' + +module Doorkeeper + module OAuth + class ClientCredentialsRequest < BaseRequest + class Issuer + attr_accessor :token, :validation, :error + + def initialize(server, validation) + @server = server + @validation = validation + end + + def create(client, scopes, creator = Creator.new) + if validation.valid? + @token = create_token(client, scopes, creator) + @error = :server_error unless @token + else + @token = false + @error = validation.error + end + @token + end + + private + + def create_token(client, scopes, creator) + ttl = Authorization::Token.access_token_expires_in(@server, client) + + creator.call( + client, + scopes, + use_refresh_token: false, + expires_in: ttl + ) + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/client_credentials/validation.rb b/doorkeeper/lib/doorkeeper/oauth/client_credentials/validation.rb new file mode 100644 index 0000000000..d17d86293e --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/client_credentials/validation.rb @@ -0,0 +1,45 @@ +require 'doorkeeper/validations' +require 'doorkeeper/oauth/scopes' +require 'doorkeeper/oauth/helpers/scope_checker' + +module Doorkeeper + module OAuth + class ClientCredentialsRequest < BaseRequest + class Validation + include Validations + include OAuth::Helpers + + validate :client, error: :invalid_client + validate :scopes, error: :invalid_scope + + def initialize(server, request) + @server, @request, @client = server, request, request.client + + validate + end + + private + + def validate_client + @client.present? + end + + def validate_scopes + return true unless @request.scopes.present? + + application_scopes = if @client.present? + @client.application.scopes + else + '' + end + + ScopeChecker.valid?( + @request.scopes.to_s, + @server.scopes, + application_scopes + ) + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/client_credentials_request.rb b/doorkeeper/lib/doorkeeper/oauth/client_credentials_request.rb new file mode 100644 index 0000000000..024f65109a --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/client_credentials_request.rb @@ -0,0 +1,38 @@ +require 'doorkeeper/oauth/client_credentials/creator' +require 'doorkeeper/oauth/client_credentials/issuer' +require 'doorkeeper/oauth/client_credentials/validation' + +module Doorkeeper + module OAuth + class ClientCredentialsRequest < BaseRequest + attr_accessor :server, :client, :original_scopes + attr_reader :response + attr_writer :issuer + + alias_method :error_response, :response + + delegate :error, to: :issuer + + def issuer + @issuer ||= Issuer.new(server, Validation.new(server, self)) + end + + def initialize(server, client, parameters = {}) + @client = client + @server = server + @response = nil + @original_scopes = parameters[:scope] + end + + def access_token + issuer.token + end + + private + + def valid? + issuer.create(client, scopes) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/code_request.rb b/doorkeeper/lib/doorkeeper/oauth/code_request.rb new file mode 100644 index 0000000000..a81f4b8c7a --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/code_request.rb @@ -0,0 +1,29 @@ +module Doorkeeper + module OAuth + class CodeRequest + attr_accessor :pre_auth, :resource_owner, :client + + def initialize(pre_auth, resource_owner) + @pre_auth = pre_auth + @client = pre_auth.client + @resource_owner = resource_owner + end + + def authorize + if pre_auth.authorizable? + auth = Authorization::Code.new(pre_auth, resource_owner) + auth.issue_token + @response = CodeResponse.new pre_auth, auth + else + @response = ErrorResponse.from_request pre_auth + end + end + + def deny + pre_auth.error = :access_denied + ErrorResponse.from_request pre_auth, + redirect_uri: pre_auth.redirect_uri + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/code_response.rb b/doorkeeper/lib/doorkeeper/oauth/code_response.rb new file mode 100644 index 0000000000..83cc32f195 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/code_response.rb @@ -0,0 +1,39 @@ +module Doorkeeper + module OAuth + class CodeResponse < BaseResponse + include OAuth::Helpers + + attr_accessor :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 redirect_uri + if URIChecker.native_uri? pre_auth.redirect_uri + auth.native_redirect + elsif response_on_fragment + Authorization::URIBuilder.uri_with_fragment( + pre_auth.redirect_uri, + access_token: auth.token.token, + token_type: auth.token.token_type, + expires_in: auth.token.expires_in_seconds, + state: pre_auth.state + ) + else + Authorization::URIBuilder.uri_with_query( + pre_auth.redirect_uri, + code: auth.token.token, + state: pre_auth.state + ) + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/error.rb b/doorkeeper/lib/doorkeeper/oauth/error.rb new file mode 100644 index 0000000000..93505ccf53 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/error.rb @@ -0,0 +1,13 @@ +module Doorkeeper + module OAuth + Error = Struct.new(:name, :state) do + def description + I18n.translate( + name, + scope: %i[doorkeeper errors messages], + default: :server_error + ) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/error_response.rb b/doorkeeper/lib/doorkeeper/oauth/error_response.rb new file mode 100644 index 0000000000..2ce667bb63 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/error_response.rb @@ -0,0 +1,65 @@ +module Doorkeeper + module OAuth + class ErrorResponse < BaseResponse + include OAuth::Helpers + + def self.from_request(request, attributes = {}) + new(attributes.merge(name: request.error, state: request.try(:state))) + end + + delegate :name, :description, :state, to: :@error + + def initialize(attributes = {}) + @error = OAuth::Error.new(*attributes.values_at(:name, :state)) + @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 + :unauthorized + end + + def redirectable? + name != :invalid_redirect_uri && name != :invalid_client && + !URIChecker.native_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', + 'Pragma' => 'no-cache', + 'Content-Type' => 'application/json; charset=utf-8', + 'WWW-Authenticate' => authenticate_info } + end + + protected + + delegate :realm, to: :configuration + + def configuration + Doorkeeper.configuration + end + + private + + def authenticate_info + %(Bearer realm="#{realm}", error="#{name}", error_description="#{description}") + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/forbidden_token_response.rb b/doorkeeper/lib/doorkeeper/oauth/forbidden_token_response.rb new file mode 100644 index 0000000000..c02ec7bac8 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/forbidden_token_response.rb @@ -0,0 +1,29 @@ +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: :invalid_scope, state: :forbidden)) + @scopes = attributes[:scopes] + end + + def status + :forbidden + end + + def headers + headers = super + headers.delete 'WWW-Authenticate' + headers + end + + def description + scope = { scope: %i[doorkeeper scopes] } + @description ||= @scopes.map { |r| I18n.translate r, scope }.join('\n') + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/helpers/scope_checker.rb b/doorkeeper/lib/doorkeeper/oauth/helpers/scope_checker.rb new file mode 100644 index 0000000000..126da818ff --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/helpers/scope_checker.rb @@ -0,0 +1,45 @@ +module Doorkeeper + module OAuth + module Helpers + module ScopeChecker + class Validator + attr_reader :parsed_scopes, :scope_str + + def initialize(scope_str, server_scopes, application_scopes) + @parsed_scopes = OAuth::Scopes.from_string(scope_str) + @scope_str = scope_str + @valid_scopes = valid_scopes(server_scopes, application_scopes) + end + + def valid? + scope_str.present? && + scope_str !~ /[\n\r\t]/ && + @valid_scopes.has_scopes?(parsed_scopes) + end + + def match? + valid? && parsed_scopes.has_scopes?(@valid_scopes) + end + + private + + def valid_scopes(server_scopes, application_scopes) + if application_scopes.present? + application_scopes + else + server_scopes + end + end + end + + def self.valid?(scope_str, server_scopes, application_scopes = nil) + Validator.new(scope_str, server_scopes, application_scopes).valid? + end + + def self.match?(scope_str, server_scopes, application_scopes = nil) + Validator.new(scope_str, server_scopes, application_scopes).match? + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/helpers/unique_token.rb b/doorkeeper/lib/doorkeeper/oauth/helpers/unique_token.rb new file mode 100644 index 0000000000..f506a85da4 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/helpers/unique_token.rb @@ -0,0 +1,13 @@ +module Doorkeeper + module OAuth + module Helpers + module UniqueToken + def self.generate(options = {}) + generator_method = options.delete(:generator) || SecureRandom.method(:hex) + token_size = options.delete(:size) || 32 + generator_method.call(token_size) + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/helpers/uri_checker.rb b/doorkeeper/lib/doorkeeper/oauth/helpers/uri_checker.rb new file mode 100644 index 0000000000..738c1e6c98 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/helpers/uri_checker.rb @@ -0,0 +1,47 @@ +module Doorkeeper + module OAuth + module Helpers + module URIChecker + def self.valid?(url) + uri = as_uri(url) + uri.fragment.nil? && !uri.host.nil? && !uri.scheme.nil? + rescue URI::InvalidURIError + false + end + + def self.matches?(url, client_url) + url = as_uri(url) + client_url = as_uri(client_url) + + if client_url.query.present? + 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 + url.query = nil + url == client_url + 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.nil? && query.nil? + 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.native_uri?(url) + url == Doorkeeper.configuration.native_redirect_uri + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/invalid_token_response.rb b/doorkeeper/lib/doorkeeper/oauth/invalid_token_response.rb new file mode 100644 index 0000000000..3d901e9739 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/invalid_token_response.rb @@ -0,0 +1,29 @@ +module Doorkeeper + module OAuth + class InvalidTokenResponse < ErrorResponse + attr_reader :reason + + def self.from_access_token(access_token, attributes = {}) + reason = if access_token.try(:revoked?) + :revoked + elsif access_token.try(: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 description + scope = { scope: %i[doorkeeper errors messages invalid_token] } + @description ||= I18n.translate @reason, scope + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/password_access_token_request.rb b/doorkeeper/lib/doorkeeper/oauth/password_access_token_request.rb new file mode 100644 index 0000000000..69074869b5 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/password_access_token_request.rb @@ -0,0 +1,42 @@ +module Doorkeeper + module OAuth + class PasswordAccessTokenRequest < BaseRequest + include OAuth::Helpers + + validate :client, error: :invalid_client + validate :resource_owner, error: :invalid_grant + validate :scopes, error: :invalid_scope + + attr_accessor :server, :client, :resource_owner, :parameters, + :access_token + + def initialize(server, client, resource_owner, parameters = {}) + @server = server + @resource_owner = resource_owner + @client = client + @parameters = parameters + @original_scopes = parameters[:scope] + end + + private + + def before_successful_response + find_or_create_access_token(client, resource_owner.id, scopes, server) + super + end + + def validate_scopes + return true unless @original_scopes.present? + ScopeChecker.valid? @original_scopes, server.scopes, client.try(:scopes) + end + + def validate_resource_owner + !!resource_owner + end + + def validate_client + !parameters[:client_id] || !!client + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/pre_authorization.rb b/doorkeeper/lib/doorkeeper/oauth/pre_authorization.rb new file mode 100644 index 0000000000..dae2ce51fa --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/pre_authorization.rb @@ -0,0 +1,66 @@ +module Doorkeeper + module OAuth + class PreAuthorization + include Validations + + validate :response_type, error: :unsupported_response_type + validate :client, error: :invalid_client + validate :scopes, error: :invalid_scope + validate :redirect_uri, error: :invalid_redirect_uri + + attr_accessor :server, :client, :response_type, :redirect_uri, :state + attr_writer :scope + + def initialize(server, client, attrs = {}) + @server = server + @client = client + @response_type = attrs[:response_type] + @redirect_uri = attrs[:redirect_uri] + @scope = attrs[:scope] + @state = attrs[:state] + end + + def authorizable? + valid? + end + + def scopes + Scopes.from_string scope + end + + def scope + @scope.presence || server.default_scopes.to_s + end + + def error_response + OAuth::ErrorResponse.from_request(self) + end + + private + + def validate_response_type + server.authorization_response_types.include? response_type + end + + def validate_client + client.present? + end + + def validate_scopes + return true unless scope.present? + Helpers::ScopeChecker.valid?( + scope, + server.scopes, + client.application.scopes + ) + end + + # TODO: test uri should be matched against the client's one + def validate_redirect_uri + return false unless redirect_uri.present? + Helpers::URIChecker.native_uri?(redirect_uri) || + Helpers::URIChecker.valid_for_authorization?(redirect_uri, client.redirect_uri) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/refresh_token_request.rb b/doorkeeper/lib/doorkeeper/oauth/refresh_token_request.rb new file mode 100644 index 0000000000..21028d5991 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/refresh_token_request.rb @@ -0,0 +1,96 @@ +module Doorkeeper + module OAuth + class RefreshTokenRequest < BaseRequest + include OAuth::Helpers + + validate :token_presence, error: :invalid_request + validate :token, error: :invalid_grant + validate :client, error: :invalid_client + validate :client_match, error: :invalid_grant + validate :scope, error: :invalid_scope + + attr_accessor :access_token, :client, :credentials, :refresh_token, + :server + + def initialize(server, refresh_token, credentials, parameters = {}) + @server = server + @refresh_token = refresh_token + @credentials = credentials + @original_scopes = parameters[:scope] || parameters[:scopes] + @refresh_token_parameter = parameters[:refresh_token] + + if credentials + @client = Application.by_uid_and_secret credentials.uid, + credentials.secret + end + end + + private + + def before_successful_response + refresh_token.transaction do + refresh_token.lock! + raise Errors::InvalidTokenReuse if refresh_token.revoked? + + refresh_token.revoke unless refresh_token_revoked_on_use? + create_access_token + end + super + end + + def refresh_token_revoked_on_use? + Doorkeeper::AccessToken.refresh_token_revoked_on_use? + end + + def default_scopes + refresh_token.scopes + end + + def create_access_token + @access_token = AccessToken.create!(access_token_attributes) + end + + def access_token_attributes + { + application_id: refresh_token.application_id, + resource_owner_id: refresh_token.resource_owner_id, + scopes: scopes.to_s, + expires_in: access_token_expires_in, + use_refresh_token: true + }.tap do |attributes| + if refresh_token_revoked_on_use? + attributes[:previous_refresh_token] = refresh_token.refresh_token + end + end + end + + def access_token_expires_in + Authorization::Token.access_token_expires_in(server, client) + end + + def validate_token_presence + refresh_token.present? || @refresh_token_parameter.present? + end + + def validate_token + refresh_token.present? && !refresh_token.revoked? + end + + def validate_client + !credentials || !!client + end + + def validate_client_match + !client || refresh_token.application_id == client.id + end + + def validate_scope + if @original_scopes.present? + ScopeChecker.valid?(@original_scopes, refresh_token.scopes) + else + true + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/scopes.rb b/doorkeeper/lib/doorkeeper/oauth/scopes.rb new file mode 100644 index 0000000000..b470fb0137 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/scopes.rb @@ -0,0 +1,75 @@ +module Doorkeeper + module OAuth + class Scopes + include Enumerable + include Comparable + + 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) + @scopes.include? scope.to_s + end + + def add(*scopes) + @scopes.push(*scopes.map(&:to_s)) + @scopes.uniq! + end + + def all + @scopes + end + + def to_s + @scopes.join(' ') + end + + def has_scopes?(scopes) + scopes.all? { |s| exists?(s) } + end + + 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 + + def &(other) + self.class.from_array(all & to_array(other)) + end + + private + + def to_array(other) + case other + when Scopes + other.all + else + other.to_a + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/token.rb b/doorkeeper/lib/doorkeeper/oauth/token.rb new file mode 100644 index 0000000000..8835a7d610 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/token.rb @@ -0,0 +1,62 @@ +module Doorkeeper + module OAuth + class Token + class << self + def from_request(request, *methods) + methods.inject(nil) do |credentials, method| + method = self.method(method) if method.is_a?(Symbol) + credentials = method.call(request) + break credentials unless credentials.blank? + end + end + + def authenticate(request, *methods) + if (token = from_request(request, *methods)) + access_token = AccessToken.by_token(token) + access_token.revoke_previous_refresh_token! if access_token + 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 && header.match(pattern) + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/token_introspection.rb b/doorkeeper/lib/doorkeeper/oauth/token_introspection.rb new file mode 100644 index 0000000000..004680096d --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/token_introspection.rb @@ -0,0 +1,128 @@ +module Doorkeeper + module OAuth + # RFC7662 OAuth 2.0 Token Introspection + # + # @see https://tools.ietf.org/html/rfc7662 + class TokenIntrospection + attr_reader :server, :token + attr_reader :error + + def initialize(server, token) + @server = server + @token = token + + authorize! + end + + def authorized? + @error.blank? + end + + def to_json + active? ? success_response : failure_response + end + + private + + # 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/ + # + def authorize! + # Requested client authorization + if server.credentials + @error = :invalid_client unless authorized_client + else + # Requested bearer token authorization + @error = :invalid_request unless authorized_token + end + end + + # Client Authentication + def authorized_client + @_authorized_client ||= server.credentials && server.client + end + + # Bearer Token Authentication + def authorized_token + @_authorized_token ||= + OAuth::Token.authenticate(server.context.request, :from_bearer_authorization) + end + + # 2.2. Introspection Response + def success_response + { + active: true, + scope: @token.scopes_string, + client_id: @token.try(:application).try(:uid), + token_type: @token.token_type, + exp: @token.expires_at.to_i, + iat: @token.created_at.to_i + } + 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://tools.ietf.org/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 + # + def active? + if authorized_client + valid_token? && authorized_for_client? + else + valid_token? + end + end + + # Token can be valid only if it is not expired or revoked. + def valid_token? + @token.present? && @token.accessible? + end + + # If token doesn't belong to some client, then it is public. + # Otherwise in it required for token to be connected to the same client. + def authorized_for_client? + if @token.application.present? + @token.application == authorized_client.application + else + true + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/token_request.rb b/doorkeeper/lib/doorkeeper/oauth/token_request.rb new file mode 100644 index 0000000000..9908c23ef2 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/token_request.rb @@ -0,0 +1,37 @@ +module Doorkeeper + module OAuth + class TokenRequest + attr_accessor :pre_auth, :resource_owner + + def initialize(pre_auth, resource_owner) + @pre_auth = pre_auth + @resource_owner = resource_owner + end + + def authorize + if pre_auth.authorizable? + auth = Authorization::Token.new(pre_auth, resource_owner) + auth.issue_token + @response = CodeResponse.new pre_auth, + auth, + response_on_fragment: true + else + @response = error_response + end + end + + def deny + pre_auth.error = :access_denied + error_response + end + + private + + def error_response + ErrorResponse.from_request pre_auth, + redirect_uri: pre_auth.redirect_uri, + response_on_fragment: true + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/oauth/token_response.rb b/doorkeeper/lib/doorkeeper/oauth/token_response.rb new file mode 100644 index 0000000000..799e8afc39 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/oauth/token_response.rb @@ -0,0 +1,32 @@ +module Doorkeeper + module OAuth + class TokenResponse + attr_accessor :token + + def initialize(token) + @token = token + end + + def body + { + 'access_token' => token.token, + 'token_type' => token.token_type, + 'expires_in' => token.expires_in_seconds, + 'refresh_token' => token.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', + 'Pragma' => 'no-cache', + 'Content-Type' => 'application/json; charset=utf-8' } + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/orm/active_record.rb b/doorkeeper/lib/doorkeeper/orm/active_record.rb new file mode 100644 index 0000000000..f8d4a95bd4 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/orm/active_record.rb @@ -0,0 +1,34 @@ +require 'active_support/lazy_load_hooks' + +module Doorkeeper + module Orm + module ActiveRecord + def self.initialize_models! + lazy_load do + require 'doorkeeper/orm/active_record/access_grant' + require 'doorkeeper/orm/active_record/access_token' + require 'doorkeeper/orm/active_record/application' + + if Doorkeeper.configuration.active_record_options[:establish_connection] + [Doorkeeper::AccessGrant, Doorkeeper::AccessToken, Doorkeeper::Application].each do |model| + options = Doorkeeper.configuration.active_record_options[:establish_connection] + model.establish_connection(options) + end + end + end + end + + def self.initialize_application_owner! + lazy_load do + require 'doorkeeper/models/concerns/ownership' + + Doorkeeper::Application.send :include, Doorkeeper::Models::Ownership + end + end + + def self.lazy_load(&block) + ActiveSupport.on_load(:active_record, {}, &block) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/orm/active_record/access_grant.rb b/doorkeeper/lib/doorkeeper/orm/active_record/access_grant.rb new file mode 100644 index 0000000000..218562ec9f --- /dev/null +++ b/doorkeeper/lib/doorkeeper/orm/active_record/access_grant.rb @@ -0,0 +1,34 @@ +module Doorkeeper + class AccessGrant < ActiveRecord::Base + self.table_name = "#{table_name_prefix}oauth_access_grants#{table_name_suffix}".to_sym + + include AccessGrantMixin + include ActiveModel::MassAssignmentSecurity if defined?(::ProtectedAttributes) + + belongs_to_options = { + class_name: 'Doorkeeper::Application', + inverse_of: :access_grants + } + + if defined?(ActiveRecord::Base) && ActiveRecord::VERSION::MAJOR >= 5 + belongs_to_options[:optional] = true + end + + belongs_to :application, belongs_to_options + + validates :resource_owner_id, :application_id, :token, :expires_in, :redirect_uri, presence: true + validates :token, uniqueness: true + + before_validation :generate_token, on: :create + + private + + # Generates token value with UniqueToken class. + # + # @return [String] token value + # + def generate_token + self.token = UniqueToken.generate + end + end +end diff --git a/doorkeeper/lib/doorkeeper/orm/active_record/access_token.rb b/doorkeeper/lib/doorkeeper/orm/active_record/access_token.rb new file mode 100644 index 0000000000..f2d3490977 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/orm/active_record/access_token.rb @@ -0,0 +1,47 @@ +module Doorkeeper + class AccessToken < ActiveRecord::Base + self.table_name = "#{table_name_prefix}oauth_access_tokens#{table_name_suffix}".to_sym + + include AccessTokenMixin + include ActiveModel::MassAssignmentSecurity if defined?(::ProtectedAttributes) + + belongs_to_options = { + class_name: 'Doorkeeper::Application', + inverse_of: :access_tokens + } + + if defined?(ActiveRecord::Base) && ActiveRecord::VERSION::MAJOR >= 5 + belongs_to_options[:optional] = true + end + + belongs_to :application, belongs_to_options + + validates :token, presence: true, uniqueness: true + validates :refresh_token, uniqueness: 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? + + # 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 self.active_for(resource_owner) + where(resource_owner_id: resource_owner.id, revoked_at: nil) + end + + def self.refresh_token_revoked_on_use? + column_names.include?('previous_refresh_token') + end + end +end diff --git a/doorkeeper/lib/doorkeeper/orm/active_record/application.rb b/doorkeeper/lib/doorkeeper/orm/active_record/application.rb new file mode 100644 index 0000000000..65fad93a0b --- /dev/null +++ b/doorkeeper/lib/doorkeeper/orm/active_record/application.rb @@ -0,0 +1,44 @@ +module Doorkeeper + class Application < ActiveRecord::Base + self.table_name = "#{table_name_prefix}oauth_applications#{table_name_suffix}".to_sym + + include ApplicationMixin + include ActiveModel::MassAssignmentSecurity if defined?(::ProtectedAttributes) + + has_many :access_grants, dependent: :delete_all, class_name: 'Doorkeeper::AccessGrant' + has_many :access_tokens, dependent: :delete_all, class_name: 'Doorkeeper::AccessToken' + + validates :name, :secret, :uid, presence: true + validates :uid, uniqueness: true + validates :redirect_uri, redirect_uri: true + + before_validation :generate_uid, :generate_secret, on: :create + + has_many :authorized_tokens, -> { where(revoked_at: nil) }, class_name: 'AccessToken' + has_many :authorized_applications, through: :authorized_tokens, source: :application + + # 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 self.authorized_for(resource_owner) + resource_access_tokens = AccessToken.active_for(resource_owner) + where(id: resource_access_tokens.select(:application_id).distinct) + end + + private + + def generate_uid + self.uid = UniqueToken.generate if uid.blank? + end + + def generate_secret + self.secret = UniqueToken.generate if secret.blank? + end + end +end diff --git a/doorkeeper/lib/doorkeeper/rails/helpers.rb b/doorkeeper/lib/doorkeeper/rails/helpers.rb new file mode 100644 index 0000000000..0821992847 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/rails/helpers.rb @@ -0,0 +1,78 @@ +module Doorkeeper + module Rails + module Helpers + def doorkeeper_authorize!(*scopes) + @_doorkeeper_scopes = scopes.presence || Doorkeeper.configuration.default_scopes + + unless valid_doorkeeper_token? + doorkeeper_render_error + end + end + + def doorkeeper_unauthorized_render_options(**); end + + def doorkeeper_forbidden_render_options(**); end + + def valid_doorkeeper_token? + doorkeeper_token && doorkeeper_token.acceptable?(@_doorkeeper_scopes) + end + + private + + def doorkeeper_render_error + error = doorkeeper_error + 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 + @_doorkeeper_token ||= OAuth::Token.authenticate( + request, + *Doorkeeper.configuration.access_token_methods + ) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/rails/routes.rb b/doorkeeper/lib/doorkeeper/rails/routes.rb new file mode 100644 index 0000000000..674c05cc73 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/rails/routes.rb @@ -0,0 +1,90 @@ +require 'doorkeeper/rails/routes/mapping' +require 'doorkeeper/rails/routes/mapper' + +module Doorkeeper + module Rails + class Routes # :nodoc: + module Helper + def use_doorkeeper(options = {}, &block) + Doorkeeper::Rails::Routes.new(self, &block).generate_routes!(options) + end + end + + def self.install! + ActionDispatch::Routing::Mapper.send :include, Doorkeeper::Rails::Routes::Helper + end + + attr_reader :routes + + def initialize(routes, &block) + @routes = routes + @mapping = Mapper.new.map(&block) + end + + def generate_routes!(options) + routes.scope options[:scope] || 'oauth', as: 'oauth' do + map_route(:authorizations, :authorization_routes) + map_route(:tokens, :token_routes) + map_route(:tokens, :revoke_routes) + map_route(:tokens, :introspect_routes) + map_route(:applications, :application_routes) + map_route(:authorized_applications, :authorized_applications_routes) + map_route(:token_info, :token_info_routes) + end + end + + private + + def map_route(name, method) + send(method, @mapping[name]) unless @mapping.skipped?(name) + end + + def authorization_routes(mapping) + routes.resource( + :authorization, + path: 'authorize', + only: %i[create destroy], + as: mapping[:as], + controller: mapping[:controllers] + ) do + routes.get '/native', action: :show, on: :member + routes.get '/', action: :new, on: :member + end + end + + def token_routes(mapping) + routes.resource( + :token, + path: 'token', + only: [:create], as: mapping[:as], + controller: mapping[:controllers] + ) + end + + def revoke_routes(mapping) + routes.post 'revoke', controller: mapping[:controllers], action: :revoke + end + + def introspect_routes(mapping) + routes.post 'introspect', controller: mapping[:controllers], action: :introspect + end + + def token_info_routes(mapping) + routes.resource( + :token_info, + path: 'token/info', + only: [:show], as: mapping[:as], + controller: mapping[:controllers] + ) + end + + def application_routes(mapping) + routes.resources :doorkeeper_applications, controller: mapping[:controllers], as: :applications, path: 'applications' + end + + def authorized_applications_routes(mapping) + routes.resources :authorized_applications, only: %i[index destroy], controller: mapping[:controllers] + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/rails/routes/mapper.rb b/doorkeeper/lib/doorkeeper/rails/routes/mapper.rb new file mode 100644 index 0000000000..bfee19b522 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/rails/routes/mapper.rb @@ -0,0 +1,28 @@ +module Doorkeeper + module Rails + class Routes # :nodoc: + class Mapper + def initialize + @mapping = Mapping.new + 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 diff --git a/doorkeeper/lib/doorkeeper/rails/routes/mapping.rb b/doorkeeper/lib/doorkeeper/rails/routes/mapping.rb new file mode 100644 index 0000000000..d2f129ce14 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/rails/routes/mapping.rb @@ -0,0 +1,38 @@ +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 diff --git a/doorkeeper/lib/doorkeeper/request.rb b/doorkeeper/lib/doorkeeper/request.rb new file mode 100644 index 0000000000..d92c1a060b --- /dev/null +++ b/doorkeeper/lib/doorkeeper/request.rb @@ -0,0 +1,46 @@ +require 'doorkeeper/request/authorization_code' +require 'doorkeeper/request/client_credentials' +require 'doorkeeper/request/code' +require 'doorkeeper/request/password' +require 'doorkeeper/request/refresh_token' +require 'doorkeeper/request/token' + +module Doorkeeper + module Request + module_function + + def authorization_strategy(response_type) + get_strategy response_type, authorization_response_types + rescue NameError + raise Errors::InvalidAuthorizationStrategy + end + + def token_strategy(grant_type) + get_strategy grant_type, token_grant_types + rescue NameError + raise Errors::InvalidTokenStrategy + end + + def get_strategy(grant_or_request_type, available) + fail Errors::MissingRequestStrategy unless grant_or_request_type.present? + fail NameError unless available.include?(grant_or_request_type.to_s) + strategy_class(grant_or_request_type) + end + + def authorization_response_types + Doorkeeper.configuration.authorization_response_types + end + private_class_method :authorization_response_types + + def token_grant_types + Doorkeeper.configuration.token_grant_types + end + private_class_method :token_grant_types + + def strategy_class(grant_or_request_type) + strategy_class_name = grant_or_request_type.to_s.tr(' ', '_').camelize + "Doorkeeper::Request::#{strategy_class_name}".constantize + end + private_class_method :strategy_class + end +end diff --git a/doorkeeper/lib/doorkeeper/request/authorization_code.rb b/doorkeeper/lib/doorkeeper/request/authorization_code.rb new file mode 100644 index 0000000000..7a8c9262a9 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/request/authorization_code.rb @@ -0,0 +1,24 @@ +require 'doorkeeper/request/strategy' + +module Doorkeeper + module Request + class AuthorizationCode < Strategy + delegate :client, :parameters, to: :server + + def request + @request ||= OAuth::AuthorizationCodeRequest.new( + Doorkeeper.configuration, + grant, + client, + parameters + ) + end + + private + + def grant + AccessGrant.by_token(parameters[:code]) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/request/client_credentials.rb b/doorkeeper/lib/doorkeeper/request/client_credentials.rb new file mode 100644 index 0000000000..3b73e01ab7 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/request/client_credentials.rb @@ -0,0 +1,17 @@ +require 'doorkeeper/request/strategy' + +module Doorkeeper + module Request + class ClientCredentials < Strategy + delegate :client, :parameters, to: :server + + def request + @request ||= OAuth::ClientCredentialsRequest.new( + Doorkeeper.configuration, + client, + parameters + ) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/request/code.rb b/doorkeeper/lib/doorkeeper/request/code.rb new file mode 100644 index 0000000000..606f646118 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/request/code.rb @@ -0,0 +1,17 @@ +require 'doorkeeper/request/strategy' + +module Doorkeeper + module Request + class Code < Strategy + delegate :current_resource_owner, to: :server + + def pre_auth + server.context.send(:pre_auth) + end + + def request + @request ||= OAuth::CodeRequest.new(pre_auth, current_resource_owner) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/request/password.rb b/doorkeeper/lib/doorkeeper/request/password.rb new file mode 100644 index 0000000000..5525560095 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/request/password.rb @@ -0,0 +1,28 @@ +require 'doorkeeper/request/strategy' + +module Doorkeeper + module Request + class Password < Strategy + delegate :credentials, :resource_owner, :parameters, to: :server + + def request + @request ||= OAuth::PasswordAccessTokenRequest.new( + Doorkeeper.configuration, + client, + resource_owner, + parameters + ) + end + + private + + def client + if credentials + server.client + elsif parameters[:client_id] + server.client_via_uid + end + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/request/refresh_token.rb b/doorkeeper/lib/doorkeeper/request/refresh_token.rb new file mode 100644 index 0000000000..8a7ee1fd1e --- /dev/null +++ b/doorkeeper/lib/doorkeeper/request/refresh_token.rb @@ -0,0 +1,21 @@ +require 'doorkeeper/request/strategy' + +module Doorkeeper + module Request + class RefreshToken < Strategy + delegate :credentials, :parameters, to: :server + + def refresh_token + AccessToken.by_refresh_token(parameters[:refresh_token]) + end + + def request + @request ||= OAuth::RefreshTokenRequest.new( + Doorkeeper.configuration, + refresh_token, credentials, + parameters + ) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/request/strategy.rb b/doorkeeper/lib/doorkeeper/request/strategy.rb new file mode 100644 index 0000000000..ecf6f41577 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/request/strategy.rb @@ -0,0 +1,17 @@ +module Doorkeeper + module Request + class Strategy + attr_accessor :server + + delegate :authorize, to: :request + + def initialize(server) + self.server = server + end + + def request + raise NotImplementedError, "request strategies must define #request" + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/request/token.rb b/doorkeeper/lib/doorkeeper/request/token.rb new file mode 100644 index 0000000000..bc542da441 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/request/token.rb @@ -0,0 +1,17 @@ +require 'doorkeeper/request/strategy' + +module Doorkeeper + module Request + class Token < Strategy + delegate :current_resource_owner, to: :server + + def pre_auth + server.context.send(:pre_auth) + end + + def request + @request ||= OAuth::TokenRequest.new(pre_auth, current_resource_owner) + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/server.rb b/doorkeeper/lib/doorkeeper/server.rb new file mode 100644 index 0000000000..697c2d9033 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/server.rb @@ -0,0 +1,46 @@ +module Doorkeeper + class Server + attr_accessor :context + + def initialize(context = nil) + @context = context + end + + def authorization_request(strategy) + klass = Request.authorization_strategy strategy + klass.new self + end + + def token_request(strategy) + klass = Request.token_strategy strategy + klass.new self + end + + # TODO: context should be the request + def parameters + context.request.parameters + end + + def client + @client ||= OAuth::Client.authenticate(credentials) + end + + def client_via_uid + @client_via_uid ||= OAuth::Client.find(parameters[:client_id]) + end + + def current_resource_owner + context.send :current_resource_owner + end + + # TODO: Use configuration and evaluate proper context on block + def resource_owner + context.send :resource_owner_from_credentials + end + + def credentials + methods = Doorkeeper.configuration.client_credentials_methods + @credentials ||= OAuth::Client::Credentials.from_request(context.request, *methods) + end + end +end diff --git a/doorkeeper/lib/doorkeeper/validations.rb b/doorkeeper/lib/doorkeeper/validations.rb new file mode 100644 index 0000000000..1fe8e5babe --- /dev/null +++ b/doorkeeper/lib/doorkeeper/validations.rb @@ -0,0 +1,31 @@ +module Doorkeeper + module Validations + extend ActiveSupport::Concern + + attr_accessor :error + + def validate + @error = nil + + self.class.validations.each do |validation| + @error = validation[:options][:error] unless send("validate_#{validation[:attribute]}") + break if @error + end + end + + def valid? + validate + @error.nil? + end + + module ClassMethods + def validate(attribute, options = {}) + validations << { attribute: attribute, options: options } + end + + def validations + @validations ||= [] + end + end + end +end diff --git a/doorkeeper/lib/doorkeeper/version.rb b/doorkeeper/lib/doorkeeper/version.rb new file mode 100644 index 0000000000..277c6eb9f1 --- /dev/null +++ b/doorkeeper/lib/doorkeeper/version.rb @@ -0,0 +1,15 @@ +module Doorkeeper + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + # Semantic versioning + MAJOR = 4 + MINOR = 3 + TINY = 2 + + # Full version number + STRING = [MAJOR, MINOR, TINY].compact.join('.') + end +end diff --git a/doorkeeper/lib/generators/doorkeeper/application_owner_generator.rb b/doorkeeper/lib/generators/doorkeeper/application_owner_generator.rb new file mode 100644 index 0000000000..058afd8346 --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/application_owner_generator.rb @@ -0,0 +1,27 @@ +require 'rails/generators/active_record' + +class Doorkeeper::ApplicationOwnerGenerator < Rails::Generators::Base + include Rails::Generators::Migration + source_root File.expand_path('../templates', __FILE__) + desc 'Provide support for client application ownership.' + + def application_owner + migration_template( + 'add_owner_to_application_migration.rb.erb', + 'db/migrate/add_owner_to_application.rb', + migration_version: migration_version + ) + end + + def self.next_migration_number(dirname) + ActiveRecord::Generators::Base.next_migration_number(dirname) + end + + private + + def migration_version + if ActiveRecord::VERSION::MAJOR >= 5 + "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" + end + end +end diff --git a/doorkeeper/lib/generators/doorkeeper/install_generator.rb b/doorkeeper/lib/generators/doorkeeper/install_generator.rb new file mode 100644 index 0000000000..04d4595032 --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/install_generator.rb @@ -0,0 +1,12 @@ +class Doorkeeper::InstallGenerator < ::Rails::Generators::Base + include Rails::Generators::Migration + source_root File.expand_path('../templates', __FILE__) + desc 'Installs Doorkeeper.' + + def install + template 'initializer.rb', 'config/initializers/doorkeeper.rb' + copy_file File.expand_path('../../../../config/locales/en.yml', __FILE__), 'config/locales/doorkeeper.en.yml' + route 'use_doorkeeper' + readme 'README' + end +end diff --git a/doorkeeper/lib/generators/doorkeeper/migration_generator.rb b/doorkeeper/lib/generators/doorkeeper/migration_generator.rb new file mode 100644 index 0000000000..cda0304fe6 --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/migration_generator.rb @@ -0,0 +1,27 @@ +require 'rails/generators/active_record' + +class Doorkeeper::MigrationGenerator < ::Rails::Generators::Base + include Rails::Generators::Migration + source_root File.expand_path('../templates', __FILE__) + desc 'Installs Doorkeeper migration file.' + + def install + migration_template( + 'migration.rb.erb', + 'db/migrate/create_doorkeeper_tables.rb', + migration_version: migration_version + ) + end + + def self.next_migration_number(dirname) + ActiveRecord::Generators::Base.next_migration_number(dirname) + end + + private + + def migration_version + if ActiveRecord::VERSION::MAJOR >= 5 + "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" + end + end +end diff --git a/doorkeeper/lib/generators/doorkeeper/previous_refresh_token_generator.rb b/doorkeeper/lib/generators/doorkeeper/previous_refresh_token_generator.rb new file mode 100644 index 0000000000..ed6df5e597 --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/previous_refresh_token_generator.rb @@ -0,0 +1,35 @@ +require 'rails/generators/active_record' + +class Doorkeeper::PreviousRefreshTokenGenerator < Rails::Generators::Base + include Rails::Generators::Migration + source_root File.expand_path('../templates', __FILE__) + desc 'Support revoke refresh token on access token use' + + def self.next_migration_number(path) + ActiveRecord::Generators::Base.next_migration_number(path) + end + + def previous_refresh_token + if no_previous_refresh_token_column? + migration_template( + 'add_previous_refresh_token_to_access_tokens.rb.erb', + 'db/migrate/add_previous_refresh_token_to_access_tokens.rb' + ) + end + end + + private + + def migration_version + if ActiveRecord::VERSION::MAJOR >= 5 + "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" + end + end + + def no_previous_refresh_token_column? + !ActiveRecord::Base.connection.column_exists?( + :oauth_access_tokens, + :previous_refresh_token + ) + end +end diff --git a/doorkeeper/lib/generators/doorkeeper/templates/README b/doorkeeper/lib/generators/doorkeeper/templates/README new file mode 100644 index 0000000000..37719d9a51 --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/templates/README @@ -0,0 +1,24 @@ +=============================================================================== + +There is a setup that you need to do before you can use doorkeeper. + +Step 1. +Go to config/initializers/doorkeeper.rb and configure +resource_owner_authenticator block. + +Step 2. +Choose the ORM: + +If you want to use ActiveRecord run: + + rails generate doorkeeper:migration + +And run + + rake db:migrate + +Step 3. +That's it, that's all. Enjoy! + +=============================================================================== + diff --git a/doorkeeper/lib/generators/doorkeeper/templates/add_owner_to_application_migration.rb.erb b/doorkeeper/lib/generators/doorkeeper/templates/add_owner_to_application_migration.rb.erb new file mode 100644 index 0000000000..542a17b022 --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/templates/add_owner_to_application_migration.rb.erb @@ -0,0 +1,7 @@ +class AddOwnerToApplication < ActiveRecord::Migration<%= migration_version %> + def change + add_column :oauth_applications, :owner_id, :integer, null: true + add_column :oauth_applications, :owner_type, :string, null: true + add_index :oauth_applications, [:owner_id, :owner_type] + end +end diff --git a/doorkeeper/lib/generators/doorkeeper/templates/add_previous_refresh_token_to_access_tokens.rb.erb b/doorkeeper/lib/generators/doorkeeper/templates/add_previous_refresh_token_to_access_tokens.rb.erb new file mode 100644 index 0000000000..9dc4475129 --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/templates/add_previous_refresh_token_to_access_tokens.rb.erb @@ -0,0 +1,11 @@ +class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration<%= migration_version %> + def change + add_column( + :oauth_access_tokens, + :previous_refresh_token, + :string, + default: "", + null: false + ) + end +end diff --git a/doorkeeper/lib/generators/doorkeeper/templates/initializer.rb b/doorkeeper/lib/generators/doorkeeper/templates/initializer.rb new file mode 100644 index 0000000000..d987c8dd7e --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/templates/initializer.rb @@ -0,0 +1,139 @@ +Doorkeeper.configure do + # Change the ORM that doorkeeper will use (needs plugins) + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + fail "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}" + # Put your resource owner authentication logic here. + # Example implementation: + # User.find_by_id(session[:user_id]) || redirect_to(new_user_session_url) + end + + # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url) + # end + + # Authorization Code expiration time (default 10 minutes). + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default 2 hours). + # If you want to disable expiration, set this to nil. + # access_token_expires_in 2.hours + + # Assign a custom TTL for implicit grants. + # custom_access_token_expires_in do |oauth_client| + # oauth_client.application.additional_settings.implicit_oauth_expiration + # end + + # Use a custom class for generating the access token. + # https://github.com/doorkeeper-gem/doorkeeper#custom-access-token-generator + # access_token_generator '::Doorkeeper::JWT' + + # The controller Doorkeeper::ApplicationController inherits from. + # Defaults to ActionController::Base. + # https://github.com/doorkeeper-gem/doorkeeper#custom-base-controller + # base_controller 'ApplicationController' + + # Reuse access token for the same resource owner within an application (disabled by default) + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # reuse_access_token + + # Issue access tokens with refresh token (disabled by default) + # use_refresh_token + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter confirmation: true (default false) if you want to enforce ownership of + # a registered application + # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support + # enable_application_owner confirmation: false + + # Define access token scopes for your provider + # For more information go to + # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes + # default_scopes :public + # optional_scopes :write, :update + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Change the native redirect uri for client apps + # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider + # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL + # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) + # + # native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob' + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # Callable objects such as proc, lambda, block or any object that responds to + # #call can be used in order to allow conditional checks (to allow non-SSL + # redirects to localhost for example). + # + # force_ssl_in_redirect_uri !Rails.env.development? + # + # force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' } + + # Specify what redirect URI's you want to block during creation. Any redirect + # URI is whitelisted by default. + # + # You can use this option in order to forbid URI's with 'javascript' scheme + # for example. + # + # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } + + # Specify what grant flows are enabled in array of Strings. The valid + # strings and the flows they enable are: + # + # "authorization_code" => Authorization Code Grant Flow + # "implicit" => Implicit Grant Flow + # "password" => Resource Owner Password Credentials Grant Flow + # "client_credentials" => Client Credentials Grant Flow + # + # If not specified, Doorkeeper enables authorization_code and + # client_credentials. + # + # implicit and password grant flows have risks that you should understand + # before enabling: + # http://tools.ietf.org/html/rfc6819#section-4.4.2 + # http://tools.ietf.org/html/rfc6819#section-4.4.3 + # + # grant_flows %w[authorization_code client_credentials] + + # Hook into the strategies' request & response life-cycle in case your + # application needs advanced customization or logging: + # + # before_successful_strategy_response do |request| + # puts "BEFORE HOOK FIRED! #{request}" + # end + # + # after_successful_strategy_response do |request, response| + # puts "AFTER HOOK FIRED! #{request}, #{response}" + # end + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + + # WWW-Authenticate Realm (default "Doorkeeper"). + # realm "Doorkeeper" +end diff --git a/doorkeeper/lib/generators/doorkeeper/templates/migration.rb.erb b/doorkeeper/lib/generators/doorkeeper/templates/migration.rb.erb new file mode 100644 index 0000000000..5a2bd6a521 --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/templates/migration.rb.erb @@ -0,0 +1,68 @@ +class CreateDoorkeeperTables < ActiveRecord::Migration<%= migration_version %> + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.timestamps null: false + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_grants do |t| + t.integer :resource_owner_id, null: false + t.references :application, null: false + t.string :token, null: false + t.integer :expires_in, null: false + t.text :redirect_uri, null: false + t.datetime :created_at, null: false + t.datetime :revoked_at + t.string :scopes + end + + add_index :oauth_access_grants, :token, unique: true + add_foreign_key( + :oauth_access_grants, + :oauth_applications, + column: :application_id + ) + + create_table :oauth_access_tokens do |t| + t.integer :resource_owner_id + t.references :application + + # If you use a custom token generator you may need to change this column + # from string to text, so that it accepts tokens larger than 255 + # characters. More info on custom token generators in: + # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator + # + # t.text :token, null: false + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, null: false + t.string :scopes + + # If there is a previous_refresh_token column, + # refresh tokens will be revoked after a related access token is used. + # If there is no previous_refresh_token column, + # previous tokens are revoked as soon as a new access token is created. + # Comment out this line if you'd rather have refresh tokens + # instantly revoked. + t.string :previous_refresh_token, null: false, default: "" + end + + add_index :oauth_access_tokens, :token, unique: true + add_index :oauth_access_tokens, :resource_owner_id + add_index :oauth_access_tokens, :refresh_token, unique: true + add_foreign_key( + :oauth_access_tokens, + :oauth_applications, + column: :application_id + ) + end +end diff --git a/doorkeeper/lib/generators/doorkeeper/views_generator.rb b/doorkeeper/lib/generators/doorkeeper/views_generator.rb new file mode 100644 index 0000000000..d766acd806 --- /dev/null +++ b/doorkeeper/lib/generators/doorkeeper/views_generator.rb @@ -0,0 +1,14 @@ +module Doorkeeper + module Generators + class ViewsGenerator < ::Rails::Generators::Base + source_root File.expand_path('../../../../app/views', __FILE__) + + desc 'Copies default Doorkeeper views and layouts to your application.' + + def manifest + directory 'doorkeeper', 'app/views/doorkeeper' + directory 'layouts/doorkeeper', 'app/views/layouts/doorkeeper' + end + end + end +end diff --git a/doorkeeper/spec/controllers/application_metal_controller.rb b/doorkeeper/spec/controllers/application_metal_controller.rb new file mode 100644 index 0000000000..7403f5a863 --- /dev/null +++ b/doorkeeper/spec/controllers/application_metal_controller.rb @@ -0,0 +1,10 @@ +require "spec_helper_integration" + +describe Doorkeeper::ApplicationMetalController do + it "lazy run hooks" do + i = 0 + ActiveSupport.on_load(:doorkeeper_metal_controller) { i += 1 } + + expect(i).to eq 1 + end +end diff --git a/doorkeeper/spec/controllers/applications_controller_spec.rb b/doorkeeper/spec/controllers/applications_controller_spec.rb new file mode 100644 index 0000000000..ed797f8d86 --- /dev/null +++ b/doorkeeper/spec/controllers/applications_controller_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper_integration' + +module Doorkeeper + describe ApplicationsController do + context 'when admin is not authenticated' do + before do + allow(Doorkeeper.configuration).to receive(:authenticate_admin).and_return(proc do + redirect_to main_app.root_url + end) + end + + it 'redirects as set in Doorkeeper.authenticate_admin' do + get :index + expect(response).to redirect_to(controller.main_app.root_url) + end + + it 'does not create application' do + expect do + post :create, doorkeeper_application: { + name: 'Example', + redirect_uri: 'https://example.com' } + end.not_to change { Doorkeeper::Application.count } + end + end + + context 'when admin is authenticated' do + render_views + + before do + allow(Doorkeeper.configuration).to receive(:authenticate_admin).and_return(->(*) { true }) + end + + it 'sorts applications by created_at' do + first_application = FactoryBot.create(:application) + second_application = FactoryBot.create(:application) + expect(Doorkeeper::Application).to receive(:ordered_by).and_call_original + get :index + expect(response.body).to have_selector("tbody tr:first-child#application_#{first_application.id}") + expect(response.body).to have_selector("tbody tr:last-child#application_#{second_application.id}") + end + + it 'creates application' do + expect do + post :create, doorkeeper_application: { + name: 'Example', + redirect_uri: 'https://example.com' } + end.to change { Doorkeeper::Application.count }.by(1) + expect(response).to be_redirect + end + + it 'does not allow mass assignment of uid or secret' do + application = FactoryBot.create(:application) + put :update, id: application.id, doorkeeper_application: { + uid: '1A2B3C4D', + secret: '1A2B3C4D' } + + expect(application.reload.uid).not_to eq '1A2B3C4D' + end + + it 'updates application' do + application = FactoryBot.create(:application) + put :update, id: application.id, doorkeeper_application: { + name: 'Example', + redirect_uri: 'https://example.com' } + expect(application.reload.name).to eq 'Example' + end + end + end +end diff --git a/doorkeeper/spec/controllers/authorizations_controller_spec.rb b/doorkeeper/spec/controllers/authorizations_controller_spec.rb new file mode 100644 index 0000000000..cabf62fbfa --- /dev/null +++ b/doorkeeper/spec/controllers/authorizations_controller_spec.rb @@ -0,0 +1,218 @@ +require 'spec_helper_integration' + +describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do + include AuthorizationRequestHelper + + if Rails::VERSION::MAJOR >= 5 + class ActionDispatch::TestResponse + def query_params + @_query_params ||= begin + fragment = URI.parse(location).fragment + Rack::Utils.parse_query(fragment) + end + end + end + else + class ActionController::TestResponse + def query_params + @_query_params ||= begin + fragment = URI.parse(location).fragment + Rack::Utils.parse_query(fragment) + end + end + end + end + + def translated_error_message(key) + I18n.translate key, scope: %i[doorkeeper errors messages] + end + + let(:client) { FactoryBot.create :application } + let(:user) { User.create!(name: 'Joe', password: 'sekret') } + let(:access_token) { FactoryBot.build :access_token, resource_owner_id: user.id, application_id: client.id } + + before do + allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(["implicit"]) + allow(controller).to receive(:current_resource_owner).and_return(user) + end + + describe 'POST #create' do + before do + post :create, client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri + end + + it 'redirects after authorization' do + expect(response).to be_redirect + end + + it 'redirects to client redirect uri' do + expect(response.location).to match(%r{^#{client.redirect_uri}}) + end + + it 'includes access token in fragment' do + expect(response.query_params['access_token']).to eq(Doorkeeper::AccessToken.first.token) + end + + it 'includes token type in fragment' do + expect(response.query_params['token_type']).to eq('bearer') + end + + it 'includes token expiration in fragment' do + expect(response.query_params['expires_in'].to_i).to eq(2.hours.to_i) + end + + it 'issues the token for the current client' do + expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) + end + + it 'issues the token for the current resource owner' do + expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) + end + end + + describe 'POST #create with errors' do + before do + default_scopes_exist :public + post :create, client_id: client.uid, response_type: 'token', scope: 'invalid', redirect_uri: client.redirect_uri + end + + it 'redirects after authorization' do + expect(response).to be_redirect + end + + it 'redirects to client redirect uri' do + expect(response.location).to match(%r{^#{client.redirect_uri}}) + end + + it 'does not include access token in fragment' do + expect(response.query_params['access_token']).to be_nil + end + + it 'includes error in fragment' do + expect(response.query_params['error']).to eq('invalid_scope') + end + + it 'includes error description in fragment' do + expect(response.query_params['error_description']).to eq(translated_error_message(:invalid_scope)) + end + + it 'does not issue any access token' do + expect(Doorkeeper::AccessToken.all).to be_empty + end + end + + describe 'POST #create with application already authorized' do + before do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) + + access_token.save! + post :create, client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri + end + + it 'returns the existing access token in a fragment' do + expect(response.query_params['access_token']).to eq(access_token.token) + end + + it 'does not creates a new access token' do + expect(Doorkeeper::AccessToken.count).to eq(1) + end + end + + describe 'GET #new token request with native url and skip_authorization true' do + before do + allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc do + true + end) + client.update_attribute :redirect_uri, 'urn:ietf:wg:oauth:2.0:oob' + get :new, client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri + end + + it 'should redirect immediately' do + expect(response).to be_redirect + expect(response.location).to match(/oauth\/token\/info\?access_token=/) + end + + it 'should not issue a grant' do + expect(Doorkeeper::AccessGrant.count).to be 0 + end + + it 'should issue a token' do + expect(Doorkeeper::AccessToken.count).to be 1 + end + end + + describe 'GET #new code request with native url and skip_authorization true' do + before do + allow(Doorkeeper.configuration).to receive(:grant_flows). + and_return(%w[authorization_code]) + allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc do + true + end) + client.update_attribute :redirect_uri, 'urn:ietf:wg:oauth:2.0:oob' + get :new, client_id: client.uid, response_type: 'code', redirect_uri: client.redirect_uri + end + + it 'should redirect immediately' do + expect(response).to be_redirect + expect(response.location).to match(/oauth\/authorize\/native\?code=#{Doorkeeper::AccessGrant.first.token}/) + end + + it 'should issue a grant' do + expect(Doorkeeper::AccessGrant.count).to be 1 + end + + it 'should not issue a token' do + expect(Doorkeeper::AccessToken.count).to be 0 + end + end + + describe 'GET #new with skip_authorization true' do + before do + allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc do + true + end) + get :new, client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri + end + + it 'should redirect immediately' do + expect(response).to be_redirect + expect(response.location).to match(%r{^#{client.redirect_uri}}) + end + + it 'should issue a token' do + expect(Doorkeeper::AccessToken.count).to be 1 + end + + it 'includes token type in fragment' do + expect(response.query_params['token_type']).to eq('bearer') + end + + it 'includes token expiration in fragment' do + expect(response.query_params['expires_in'].to_i).to eq(2.hours.to_i) + end + + it 'issues the token for the current client' do + expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id) + end + + it 'issues the token for the current resource owner' do + expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id) + end + end + + describe 'GET #new with errors' do + before do + default_scopes_exist :public + get :new, an_invalid: 'request' + end + + it 'does not redirect' do + expect(response).to_not be_redirect + end + + it 'does not issue any token' do + expect(Doorkeeper::AccessGrant.count).to eq 0 + expect(Doorkeeper::AccessToken.count).to eq 0 + end + end +end diff --git a/doorkeeper/spec/controllers/protected_resources_controller_spec.rb b/doorkeeper/spec/controllers/protected_resources_controller_spec.rb new file mode 100644 index 0000000000..4fc60f7423 --- /dev/null +++ b/doorkeeper/spec/controllers/protected_resources_controller_spec.rb @@ -0,0 +1,309 @@ +require 'spec_helper_integration' + +module ControllerActions + def index + render plain: 'index' + end + + def show + render plain: 'show' + end + + def doorkeeper_unauthorized_render_options(*); end + + def doorkeeper_forbidden_render_options(*); end +end + +describe 'doorkeeper authorize filter' do + context 'accepts token code specified as' do + controller do + before_action :doorkeeper_authorize! + + def index + render plain: 'index' + end + end + + let(:token_string) { '1A2BC3' } + let(:token) do + double(Doorkeeper::AccessToken, + acceptable?: true, previous_refresh_token: "", + revoke_previous_refresh_token!: true) + end + + it 'access_token param' do + expect(Doorkeeper::AccessToken).to receive(:by_token).with(token_string).and_return(token) + get :index, access_token: token_string + end + + it 'bearer_token param' do + expect(Doorkeeper::AccessToken).to receive(:by_token).with(token_string).and_return(token) + get :index, bearer_token: token_string + end + + it 'Authorization header' do + expect(Doorkeeper::AccessToken).to receive(:by_token).with(token_string).and_return(token) + request.env['HTTP_AUTHORIZATION'] = "Bearer #{token_string}" + get :index + end + + it 'different kind of Authorization header' do + expect(Doorkeeper::AccessToken).not_to receive(:by_token) + request.env['HTTP_AUTHORIZATION'] = "MAC #{token_string}" + get :index + end + + it 'does not change Authorization header value' do + expect(Doorkeeper::AccessToken).to receive(:by_token).exactly(2).times.and_return(token) + request.env['HTTP_AUTHORIZATION'] = "Bearer #{token_string}" + get :index + controller.send(:remove_instance_variable, :@_doorkeeper_token) + get :index + end + end + + context 'defined for all actions' do + controller do + before_action :doorkeeper_authorize! + + include ControllerActions + end + + context 'with valid token', token: :valid do + it 'allows into index action' do + get :index, access_token: token_string + expect(response).to be_successful + end + + it 'allows into show action' do + get :show, id: '4', access_token: token_string + expect(response).to be_successful + end + end + + context 'with invalid token', token: :invalid do + it 'does not allow into index action' do + get :index, access_token: token_string + expect(response.status).to eq 401 + expect(response.header['WWW-Authenticate']).to match(/^Bearer/) + end + + it 'does not allow into show action' do + get :show, id: '4', access_token: token_string + expect(response.status).to eq 401 + expect(response.header['WWW-Authenticate']).to match(/^Bearer/) + end + end + end + + context 'defined with scopes' do + controller do + before_action -> { doorkeeper_authorize! :write } + + include ControllerActions + end + + let(:token_string) { '1A2DUWE' } + + it 'allows if the token has particular scopes' do + token = double(Doorkeeper::AccessToken, + accessible?: true, scopes: %w[write public], + previous_refresh_token: "", + revoke_previous_refresh_token!: true) + expect(token).to receive(:acceptable?).with([:write]).and_return(true) + expect( + Doorkeeper::AccessToken + ).to receive(:by_token).with(token_string).and_return(token) + + get :index, access_token: token_string + expect(response).to be_successful + end + + it 'does not allow if the token does not include given scope' do + token = double(Doorkeeper::AccessToken, + accessible?: true, scopes: ['public'], revoked?: false, + expired?: false, previous_refresh_token: "", + revoke_previous_refresh_token!: true) + expect( + Doorkeeper::AccessToken + ).to receive(:by_token).with(token_string).and_return(token) + expect(token).to receive(:acceptable?).with([:write]).and_return(false) + + get :index, access_token: token_string + expect(response.status).to eq 403 + expect(response.header).to_not include('WWW-Authenticate') + end + end + + context 'when custom unauthorized render options are configured' do + controller do + before_action :doorkeeper_authorize! + + include ControllerActions + end + + context 'with a JSON custom render', token: :invalid do + before do + module ControllerActions + remove_method :doorkeeper_unauthorized_render_options + + def doorkeeper_unauthorized_render_options(error: nil) + { json: ActiveSupport::JSON.encode(error_message: error.description) } + end + end + end + + after do + module ControllerActions + remove_method :doorkeeper_unauthorized_render_options + + def doorkeeper_unauthorized_render_options(error: nil) + end + end + end + + it 'it renders a custom JSON response', token: :invalid do + get :index, access_token: token_string + expect(response.status).to eq 401 + expect(response.content_type).to eq('application/json') + expect(response.header['WWW-Authenticate']).to match(/^Bearer/) + + expect(json_response).not_to be_nil + expect(json_response['error_message']).to match('token is invalid') + end + end + + context 'with a text custom render', token: :invalid do + before do + module ControllerActions + remove_method :doorkeeper_unauthorized_render_options + + def doorkeeper_unauthorized_render_options(**) + { plain: 'Unauthorized' } + end + end + end + + after do + module ControllerActions + remove_method :doorkeeper_unauthorized_render_options + + def doorkeeper_unauthorized_render_options(error: nil); end + end + end + + it 'it renders a custom text response', token: :invalid do + get :index, access_token: token_string + expect(response.status).to eq 401 + expect(response.content_type).to eq('text/plain') + expect(response.header['WWW-Authenticate']).to match(/^Bearer/) + expect(response.body).to eq('Unauthorized') + end + end + end + + context 'when custom forbidden render options are configured' do + before do + expect(Doorkeeper::AccessToken).to receive(:by_token).with(token_string).and_return(token) + expect(token).to receive(:acceptable?).with([:write]).and_return(false) + end + + after do + module ControllerActions + remove_method :doorkeeper_forbidden_render_options + + def doorkeeper_forbidden_render_options(*); end + end + end + + controller do + before_action -> { doorkeeper_authorize! :write } + + include ControllerActions + end + + let(:token) do + double(Doorkeeper::AccessToken, + accessible?: true, scopes: ['public'], revoked?: false, + expired?: false, previous_refresh_token: "", + revoke_previous_refresh_token!: true) + end + + let(:token_string) { '1A2DUWE' } + + context 'with a JSON custom render' do + before do + module ControllerActions + remove_method :doorkeeper_forbidden_render_options + + def doorkeeper_forbidden_render_options(*) + { json: { error_message: 'Forbidden' } } + end + end + end + + it 'renders a custom JSON response' do + get :index, access_token: token_string + expect(response.header).to_not include('WWW-Authenticate') + expect(response.content_type).to eq('application/json') + expect(response.status).to eq 403 + + expect(json_response).not_to be_nil + expect(json_response['error_message']).to match('Forbidden') + end + end + + context 'with a status and JSON custom render' do + before do + module ControllerActions + remove_method :doorkeeper_forbidden_render_options + def doorkeeper_forbidden_render_options(*) + { json: { error_message: 'Not Found' }, + respond_not_found_when_forbidden: true } + end + end + end + + it 'overrides the default status code' do + get :index, access_token: token_string + expect(response.status).to eq 404 + end + end + + context 'with a text custom render' do + before do + module ControllerActions + remove_method :doorkeeper_forbidden_render_options + + def doorkeeper_forbidden_render_options(*) + { plain: 'Forbidden' } + end + end + end + + it 'renders a custom status code and text response' do + get :index, access_token: token_string + expect(response.header).to_not include('WWW-Authenticate') + expect(response.status).to eq 403 + expect(response.body).to eq('Forbidden') + end + end + + context 'with a status and text custom render' do + before do + module ControllerActions + remove_method :doorkeeper_forbidden_render_options + + def doorkeeper_forbidden_render_options(*) + { respond_not_found_when_forbidden: true, plain: 'Not Found' } + end + end + end + + it 'overrides the default status code' do + get :index, access_token: token_string + expect(response.status).to eq 404 + end + end + end +end diff --git a/doorkeeper/spec/controllers/token_info_controller_spec.rb b/doorkeeper/spec/controllers/token_info_controller_spec.rb new file mode 100644 index 0000000000..0df384d1eb --- /dev/null +++ b/doorkeeper/spec/controllers/token_info_controller_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper_integration' + +describe Doorkeeper::TokenInfoController do + describe 'when requesting token info with valid token' do + let(:doorkeeper_token) { FactoryBot.create(:access_token) } + + before(:each) do + allow(controller).to receive(:doorkeeper_token) { doorkeeper_token } + end + + describe 'successful request' do + it 'responds with tokeninfo' do + get :show + + expect(response.body).to eq(doorkeeper_token.to_json) + end + + it 'responds with a 200 status' do + get :show + + expect(response.status).to eq 200 + end + end + + describe 'invalid token response' do + before(:each) do + allow(controller).to receive(:doorkeeper_token).and_return(nil) + end + + it 'responds with 401 when doorkeeper_token is not valid' do + get :show + + expect(response.status).to eq 401 + expect(response.headers['WWW-Authenticate']).to match(/^Bearer/) + end + + it 'responds with 401 when doorkeeper_token is invalid, expired or revoked' do + allow(controller).to receive(:doorkeeper_token).and_return(doorkeeper_token) + allow(doorkeeper_token).to receive(:accessible?).and_return(false) + + get :show + + expect(response.status).to eq 401 + expect(response.headers['WWW-Authenticate']).to match(/^Bearer/) + end + + it 'responds body message for error' do + get :show + + expect(response.body).to eq( + Doorkeeper::OAuth::ErrorResponse.new(name: :invalid_request, status: :unauthorized).body.to_json + ) + end + end + end +end diff --git a/doorkeeper/spec/controllers/tokens_controller_spec.rb b/doorkeeper/spec/controllers/tokens_controller_spec.rb new file mode 100644 index 0000000000..1bb1dcf824 --- /dev/null +++ b/doorkeeper/spec/controllers/tokens_controller_spec.rb @@ -0,0 +1,222 @@ +require 'spec_helper_integration' + +describe Doorkeeper::TokensController do + describe 'when authorization has succeeded' do + let(:token) { double(:token, authorize: true) } + + before do + allow(controller).to receive(:token) { token } + end + + it 'returns the authorization' do + skip 'verify need of these specs' + + expect(token).to receive(:authorization) + + post :create + end + end + + describe 'when authorization has failed' do + it 'returns the error response' do + token = double(:token, authorize: false) + allow(controller).to receive(:token) { token } + + post :create + + expect(response.status).to eq 401 + expect(response.headers['WWW-Authenticate']).to match(/Bearer/) + end + end + + describe 'when there is a failure due to a custom error' do + it 'returns the error response with a custom message' do + # I18n looks for `doorkeeper.errors.messages.custom_message` in locale files + custom_message = "my_message" + allow(I18n).to receive(:translate). + with( + custom_message, + hash_including(scope: %i[doorkeeper errors messages]), + ). + and_return('Authorization custom message') + + doorkeeper_error = Doorkeeper::Errors::DoorkeeperError.new(custom_message) + + strategy = double(:strategy) + request = double(token_request: strategy) + allow(strategy).to receive(:authorize).and_raise(doorkeeper_error) + allow(controller).to receive(:server).and_return(request) + + post :create + + expected_response_body = { + "error" => custom_message, + "error_description" => "Authorization custom message" + } + expect(response.status).to eq 401 + expect(response.headers['WWW-Authenticate']).to match(/Bearer/) + expect(JSON.parse(response.body)).to eq expected_response_body + end + end + + describe 'when revoke authorization has failed' do + # http://tools.ietf.org/html/rfc7009#section-2.2 + it 'returns no error response' do + token = double(:token, authorize: false, application_id?: true) + allow(controller).to receive(:token) { token } + + post :revoke + + expect(response.status).to eq 200 + end + end + + describe 'authorize response memoization' do + it "memoizes the result of the authorization" do + strategy = double(:strategy, authorize: true) + expect(strategy).to receive(:authorize).once + allow(controller).to receive(:strategy) { strategy } + allow(controller).to receive(:create) do + controller.send :authorize_response + end + + post :create + end + end + + describe 'when requested token introspection' do + context 'authorized using Bearer token' do + let(:client) { FactoryBot.create(:application) } + let(:access_token) { FactoryBot.create(:access_token, application: client) } + + it 'responds with full token introspection' do + request.headers['Authorization'] = "Bearer #{access_token.token}" + + post :introspect, token: access_token.token + + should_have_json 'active', true + expect(json_response).to include('client_id', 'token_type', 'exp', 'iat') + end + end + + context 'authorized using Client Authentication' do + let(:client) { FactoryBot.create(:application) } + let(:access_token) { FactoryBot.create(:access_token, application: client) } + + it 'responds with full token introspection' do + request.headers['Authorization'] = basic_auth_header_for_client(client) + + post :introspect, token: access_token.token + + should_have_json 'active', true + expect(json_response).to include('client_id', 'token_type', 'exp', 'iat') + should_have_json 'client_id', client.uid + end + end + + context 'public access token' do + let(:client) { FactoryBot.create(:application) } + let(:access_token) { FactoryBot.create(:access_token, application: nil) } + + it 'responds with full token introspection' do + request.headers['Authorization'] = basic_auth_header_for_client(client) + + post :introspect, token: access_token.token + + should_have_json 'active', true + expect(json_response).to include('client_id', 'token_type', 'exp', 'iat') + should_have_json 'client_id', nil + end + end + + context 'token was issued to a different client than is making this request' do + let(:client) { FactoryBot.create(:application) } + let(:different_client) { FactoryBot.create(:application) } + let(:access_token) { FactoryBot.create(:access_token, application: client) } + + it 'responds with only active state' do + request.headers['Authorization'] = basic_auth_header_for_client(different_client) + + post :introspect, token: access_token.token + + expect(response).to be_successful + + should_have_json 'active', false + expect(json_response).not_to include('client_id', 'token_type', 'exp', 'iat') + end + end + + context 'using invalid credentials to authorize' do + let(:client) { double(uid: '123123', secret: '666999') } + let(:access_token) { FactoryBot.create(:access_token) } + + it 'responds with invalid_client error' do + request.headers['Authorization'] = basic_auth_header_for_client(client) + + post :introspect, token: access_token.token + + expect(response).not_to be_successful + response_status_should_be 401 + + should_not_have_json 'active' + should_have_json 'error', 'invalid_client' + end + end + + context 'using wrong token value' do + let(:client) { FactoryBot.create(:application) } + let(:access_token) { FactoryBot.create(:access_token, application: client) } + + it 'responds with only active state' do + request.headers['Authorization'] = basic_auth_header_for_client(client) + + post :introspect, token: SecureRandom.hex(16) + + should_have_json 'active', false + expect(json_response).not_to include('client_id', 'token_type', 'exp', 'iat') + end + end + + context 'when requested Access Token expired' do + let(:client) { FactoryBot.create(:application) } + let(:access_token) { FactoryBot.create(:access_token, application: client, created_at: 1.year.ago) } + + it 'responds with only active state' do + request.headers['Authorization'] = basic_auth_header_for_client(client) + + post :introspect, token: access_token.token + + should_have_json 'active', false + expect(json_response).not_to include('client_id', 'token_type', 'exp', 'iat') + end + end + + context 'when requested Access Token revoked' do + let(:client) { FactoryBot.create(:application) } + let(:access_token) { FactoryBot.create(:access_token, application: client, revoked_at: 1.year.ago) } + + it 'responds with only active state' do + request.headers['Authorization'] = basic_auth_header_for_client(client) + + post :introspect, token: access_token.token + + should_have_json 'active', false + expect(json_response).not_to include('client_id', 'token_type', 'exp', 'iat') + end + end + + context 'unauthorized (no bearer token or client credentials)' do + let(:access_token) { FactoryBot.create(:access_token) } + + it 'responds with invalid_request error' do + post :introspect, token: access_token.token + + expect(response).not_to be_successful + response_status_should_be 401 + + should_not_have_json 'active' + should_have_json 'error', 'invalid_request' + end + end + end +end diff --git a/doorkeeper/spec/dummy/Rakefile b/doorkeeper/spec/dummy/Rakefile new file mode 100644 index 0000000000..36458522cb --- /dev/null +++ b/doorkeeper/spec/dummy/Rakefile @@ -0,0 +1,7 @@ +#!/usr/bin/env rake +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +Dummy::Application.load_tasks diff --git a/doorkeeper/spec/dummy/app/controllers/application_controller.rb b/doorkeeper/spec/dummy/app/controllers/application_controller.rb new file mode 100644 index 0000000000..e8065d9505 --- /dev/null +++ b/doorkeeper/spec/dummy/app/controllers/application_controller.rb @@ -0,0 +1,3 @@ +class ApplicationController < ActionController::Base + protect_from_forgery +end diff --git a/doorkeeper/spec/dummy/app/controllers/custom_authorizations_controller.rb b/doorkeeper/spec/dummy/app/controllers/custom_authorizations_controller.rb new file mode 100644 index 0000000000..b490d27ce0 --- /dev/null +++ b/doorkeeper/spec/dummy/app/controllers/custom_authorizations_controller.rb @@ -0,0 +1,7 @@ +class CustomAuthorizationsController < ::ApplicationController + %w(index show new create edit update destroy).each do |action| + define_method action do + render nothing: true + end + end +end diff --git a/doorkeeper/spec/dummy/app/controllers/full_protected_resources_controller.rb b/doorkeeper/spec/dummy/app/controllers/full_protected_resources_controller.rb new file mode 100644 index 0000000000..e1fd9e710f --- /dev/null +++ b/doorkeeper/spec/dummy/app/controllers/full_protected_resources_controller.rb @@ -0,0 +1,12 @@ +class FullProtectedResourcesController < ApplicationController + before_action -> { doorkeeper_authorize! :write, :admin }, only: :show + before_action :doorkeeper_authorize!, only: :index + + def index + render plain: 'index' + end + + def show + render plain: 'show' + end +end diff --git a/doorkeeper/spec/dummy/app/controllers/home_controller.rb b/doorkeeper/spec/dummy/app/controllers/home_controller.rb new file mode 100644 index 0000000000..bce67de58a --- /dev/null +++ b/doorkeeper/spec/dummy/app/controllers/home_controller.rb @@ -0,0 +1,17 @@ +class HomeController < ApplicationController + def index + end + + def sign_in + session[:user_id] = if Rails.env.development? + User.first || User.create!(name: 'Joe', password: 'sekret') + else + User.first + end + redirect_to '/' + end + + def callback + render plain: 'ok' + end +end diff --git a/doorkeeper/spec/dummy/app/controllers/metal_controller.rb b/doorkeeper/spec/dummy/app/controllers/metal_controller.rb new file mode 100644 index 0000000000..83574feb17 --- /dev/null +++ b/doorkeeper/spec/dummy/app/controllers/metal_controller.rb @@ -0,0 +1,11 @@ +class MetalController < ActionController::Metal + include AbstractController::Callbacks + include ActionController::Head + include Doorkeeper::Rails::Helpers + + before_action :doorkeeper_authorize! + + def index + self.response_body = { ok: true }.to_json + end +end diff --git a/doorkeeper/spec/dummy/app/controllers/semi_protected_resources_controller.rb b/doorkeeper/spec/dummy/app/controllers/semi_protected_resources_controller.rb new file mode 100644 index 0000000000..cb36001514 --- /dev/null +++ b/doorkeeper/spec/dummy/app/controllers/semi_protected_resources_controller.rb @@ -0,0 +1,11 @@ +class SemiProtectedResourcesController < ApplicationController + before_action :doorkeeper_authorize!, only: :index + + def index + render plain: 'protected index' + end + + def show + render plain: 'non protected show' + end +end diff --git a/doorkeeper/spec/dummy/app/helpers/application_helper.rb b/doorkeeper/spec/dummy/app/helpers/application_helper.rb new file mode 100644 index 0000000000..2dbb236e4b --- /dev/null +++ b/doorkeeper/spec/dummy/app/helpers/application_helper.rb @@ -0,0 +1,5 @@ +module ApplicationHelper + def current_user + @current_user ||= User.find_by_id(session[:user_id]) + end +end diff --git a/doorkeeper/spec/dummy/app/models/user.rb b/doorkeeper/spec/dummy/app/models/user.rb new file mode 100644 index 0000000000..bb337d7c89 --- /dev/null +++ b/doorkeeper/spec/dummy/app/models/user.rb @@ -0,0 +1,5 @@ +class User < ActiveRecord::Base + def self.authenticate!(name, password) + User.where(name: name, password: password).first + end +end diff --git a/doorkeeper/spec/dummy/app/views/home/index.html.erb b/doorkeeper/spec/dummy/app/views/home/index.html.erb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doorkeeper/spec/dummy/app/views/layouts/application.html.erb b/doorkeeper/spec/dummy/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..8988b5d7ee --- /dev/null +++ b/doorkeeper/spec/dummy/app/views/layouts/application.html.erb @@ -0,0 +1,14 @@ + + + + Dummy + <%= csrf_meta_tags %> + + + +<%= link_to "Sign in", '/sign_in' %> + +<%= yield %> + + + diff --git a/doorkeeper/spec/dummy/config.ru b/doorkeeper/spec/dummy/config.ru new file mode 100644 index 0000000000..1989ed8d0c --- /dev/null +++ b/doorkeeper/spec/dummy/config.ru @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run Dummy::Application diff --git a/doorkeeper/spec/dummy/config/application.rb b/doorkeeper/spec/dummy/config/application.rb new file mode 100644 index 0000000000..581121dbd0 --- /dev/null +++ b/doorkeeper/spec/dummy/config/application.rb @@ -0,0 +1,23 @@ +require File.expand_path('../boot', __FILE__) + +require 'rails/all' + +Bundler.require(*Rails.groups) + +require 'yaml' + +orm = if DOORKEEPER_ORM =~ /mongoid/ + Mongoid.load!(File.join(File.dirname(File.expand_path(__FILE__)), "#{DOORKEEPER_ORM}.yml")) + :mongoid + else + DOORKEEPER_ORM + end +require "#{orm}/railtie" + +module Dummy + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + end +end diff --git a/doorkeeper/spec/dummy/config/boot.rb b/doorkeeper/spec/dummy/config/boot.rb new file mode 100644 index 0000000000..b3aa1b088d --- /dev/null +++ b/doorkeeper/spec/dummy/config/boot.rb @@ -0,0 +1,9 @@ +require 'rubygems' +require 'bundler/setup' + +orm = ENV['BUNDLE_GEMFILE'].match(/Gemfile\.(.+)\.rb/) +unless defined?(DOORKEEPER_ORM) + DOORKEEPER_ORM = (orm && orm[1]) || :active_record +end + +$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) diff --git a/doorkeeper/spec/dummy/config/database.yml b/doorkeeper/spec/dummy/config/database.yml new file mode 100644 index 0000000000..8e3a529005 --- /dev/null +++ b/doorkeeper/spec/dummy/config/database.yml @@ -0,0 +1,15 @@ +development: + adapter: sqlite3 + database: db/development.sqlite3 + pool: 5 + timeout: 5000 + +test: + adapter: sqlite3 + database: ":memory:" + timeout: 500 + +production: + adapter: sqlite3 + database: ":memory:" + timeout: 500 diff --git a/doorkeeper/spec/dummy/config/environment.rb b/doorkeeper/spec/dummy/config/environment.rb new file mode 100644 index 0000000000..df3006d349 --- /dev/null +++ b/doorkeeper/spec/dummy/config/environment.rb @@ -0,0 +1,5 @@ +# Load the rails application +require File.expand_path('../application', __FILE__) + +# Initialize the rails application +Rails.application.initialize! diff --git a/doorkeeper/spec/dummy/config/environments/development.rb b/doorkeeper/spec/dummy/config/environments/development.rb new file mode 100644 index 0000000000..df00f8a839 --- /dev/null +++ b/doorkeeper/spec/dummy/config/environments/development.rb @@ -0,0 +1,29 @@ +Dummy::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Don't care if the mailer can't send + # config.action_mailer.raise_delivery_errors = false + + # Print deprecation notices to the Rails logger + config.active_support.deprecation = :log + + # Only use best-standards-support built into browsers + config.action_dispatch.best_standards_support = :builtin + + # Do not compress assets + config.assets.compress = false + + # Expands the lines which load the assets + config.assets.debug = true + + config.eager_load = false +end diff --git a/doorkeeper/spec/dummy/config/environments/production.rb b/doorkeeper/spec/dummy/config/environments/production.rb new file mode 100644 index 0000000000..5c18740896 --- /dev/null +++ b/doorkeeper/spec/dummy/config/environments/production.rb @@ -0,0 +1,62 @@ +Dummy::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # Code is not reloaded between requests + config.cache_classes = true + + # Full error reports are disabled and caching is turned on + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Disable Rails's static asset server (Apache or nginx will already do this) + config.serve_static_assets = false + + # Compress JavaScripts and CSS + config.assets.compress = true + + # Don't fallback to assets pipeline if a precompiled asset is missed + config.assets.compile = false + + # Generate digests for assets URLs + config.assets.digest = true + + # Defaults to Rails.root.join("public/assets") + # config.assets.manifest = YOUR_PATH + + # Specifies the header that your server uses for sending files + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # See everything in the log (default is :info) + # config.log_level = :debug + + # Use a different logger for distributed setups + # config.logger = SyslogLogger.new + + # Use a different cache store in production + # config.cache_store = :mem_cache_store + + # Enable serving of images, stylesheets, and JavaScripts from an asset server + # config.action_controller.asset_host = "http://assets.example.com" + + # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) + # config.assets.precompile += %w( search.js ) + + # Disable delivery errors, bad email addresses will be ignored + # config.action_mailer.raise_delivery_errors = false + + # Enable threaded mode + # config.threadsafe! + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation can not be found) + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners + config.active_support.deprecation = :notify + + config.eager_load = true +end diff --git a/doorkeeper/spec/dummy/config/environments/test.rb b/doorkeeper/spec/dummy/config/environments/test.rb new file mode 100644 index 0000000000..08f4563e02 --- /dev/null +++ b/doorkeeper/spec/dummy/config/environments/test.rb @@ -0,0 +1,44 @@ +Dummy::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + # config.action_mailer.delivery_method = :test + + # Use SQL instead of Active Record's schema dumper when creating the test database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql + + # Print deprecation notices to the stderr + config.active_support.deprecation = :stderr + + config.eager_load = true + + if DOORKEEPER_ORM == :active_record + config.active_record.table_name_prefix = TABLE_NAME_PREFIX.to_s + config.active_record.table_name_suffix = TABLE_NAME_SUFFIX.to_s + end +end diff --git a/doorkeeper/spec/dummy/config/initializers/backtrace_silencers.rb b/doorkeeper/spec/dummy/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000000..59385cdf37 --- /dev/null +++ b/doorkeeper/spec/dummy/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/doorkeeper/spec/dummy/config/initializers/doorkeeper.rb b/doorkeeper/spec/dummy/config/initializers/doorkeeper.rb new file mode 100644 index 0000000000..7fdad53659 --- /dev/null +++ b/doorkeeper/spec/dummy/config/initializers/doorkeeper.rb @@ -0,0 +1,107 @@ +Doorkeeper.configure do + # Change the ORM that doorkeeper will use. + orm DOORKEEPER_ORM + + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + # Put your resource owner authentication logic here. + User.where(id: session[:user_id]).first || redirect_to(root_url, alert: 'Needs sign in.') + end + + # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url) + # end + + # Authorization Code expiration time (default 10 minutes). + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default 2 hours). + # If you want to disable expiration, set this to nil. + # access_token_expires_in 2.hours + + # Reuse access token for the same resource owner within an application (disabled by default) + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # reuse_access_token + + # Issue access tokens with refresh token (disabled by default) + use_refresh_token + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter confirmation: true (default false) if you want to enforce ownership of + # a registered application + # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support + # enable_application_owner confirmation: false + + # Define access token scopes for your provider + # For more information go to + # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes + default_scopes :public + optional_scopes :write, :update + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out the wiki for more information on customization + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out the wiki for more information on customization + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Change the native redirect uri for client apps + # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider + # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL + # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) + # + # native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob' + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # force_ssl_in_redirect_uri !Rails.env.development? + + # Specify what grant flows are enabled in array of Strings. The valid + # strings and the flows they enable are: + # + # "authorization_code" => Authorization Code Grant Flow + # "implicit" => Implicit Grant Flow + # "password" => Resource Owner Password Credentials Grant Flow + # "client_credentials" => Client Credentials Grant Flow + # + # If not specified, Doorkeeper enables authorization_code and + # client_credentials. + # + # implicit and password grant flows have risks that you should understand + # before enabling: + # http://tools.ietf.org/html/rfc6819#section-4.4.2 + # http://tools.ietf.org/html/rfc6819#section-4.4.3 + # + # grant_flows %w[authorization_code client_credentials] + + # Hook into the strategies' request & response life-cycle in case your + # application needs advanced customization or logging: + # + # before_successful_strategy_response do |request| + # puts "BEFORE HOOK FIRED! #{request}" + # end + # + # after_successful_strategy_response do |request, response| + # puts "AFTER HOOK FIRED! #{request}, #{response}" + # end + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + + # WWW-Authenticate Realm (default "Doorkeeper"). + realm "Doorkeeper" +end diff --git a/doorkeeper/spec/dummy/config/initializers/new_framework_defaults.rb b/doorkeeper/spec/dummy/config/initializers/new_framework_defaults.rb new file mode 100644 index 0000000000..f99c44727d --- /dev/null +++ b/doorkeeper/spec/dummy/config/initializers/new_framework_defaults.rb @@ -0,0 +1,6 @@ +# Require `belongs_to` associations by default. This is a new Rails 5.0 +# default, so it is introduced as a configuration option to ensure that apps +# made on earlier versions of Rails are not affected when upgrading. +if Rails::VERSION::MAJOR >= 5 + Rails.application.config.active_record.belongs_to_required_by_default = true +end diff --git a/doorkeeper/spec/dummy/config/initializers/secret_token.rb b/doorkeeper/spec/dummy/config/initializers/secret_token.rb new file mode 100644 index 0000000000..c15d6e2fde --- /dev/null +++ b/doorkeeper/spec/dummy/config/initializers/secret_token.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +Dummy::Application.config.secret_key_base = + 'c00157b5a1bb6181792f0f4a8a080485de7bab9987e6cf159dc74c4f0573345c1bfa713b5d756e1491fc0b098567e8a619e2f8d268eda86a20a720d05d633780' diff --git a/doorkeeper/spec/dummy/config/initializers/session_store.rb b/doorkeeper/spec/dummy/config/initializers/session_store.rb new file mode 100644 index 0000000000..952473ff9a --- /dev/null +++ b/doorkeeper/spec/dummy/config/initializers/session_store.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' + +# Use the database for sessions instead of the cookie-based default, +# which shouldn't be used to store highly confidential information +# (create the session table with "rails generate session_migration") +# Dummy::Application.config.session_store :active_record_store diff --git a/doorkeeper/spec/dummy/config/initializers/wrap_parameters.rb b/doorkeeper/spec/dummy/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000000..999df20181 --- /dev/null +++ b/doorkeeper/spec/dummy/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# Disable root element in JSON by default. +ActiveSupport.on_load(:active_record) do + self.include_root_in_json = false +end diff --git a/doorkeeper/spec/dummy/config/locales/doorkeeper.en.yml b/doorkeeper/spec/dummy/config/locales/doorkeeper.en.yml new file mode 100644 index 0000000000..60527e66cc --- /dev/null +++ b/doorkeeper/spec/dummy/config/locales/doorkeeper.en.yml @@ -0,0 +1,5 @@ +en: + doorkeeper: + scopes: + public: "Access your public data" + write: "Update your data" diff --git a/doorkeeper/spec/dummy/config/routes.rb b/doorkeeper/spec/dummy/config/routes.rb new file mode 100644 index 0000000000..ada394af26 --- /dev/null +++ b/doorkeeper/spec/dummy/config/routes.rb @@ -0,0 +1,52 @@ +Rails.application.routes.draw do + use_doorkeeper + use_doorkeeper scope: 'scope' + + scope 'inner_space' do + use_doorkeeper scope: 'scope' do + controllers authorizations: 'custom_authorizations', + tokens: 'custom_authorizations', + applications: 'custom_authorizations', + token_info: 'custom_authorizations' + + as authorizations: 'custom_auth', + tokens: 'custom_token', + token_info: 'custom_token_info' + end + end + + scope 'space' do + use_doorkeeper do + controllers authorizations: 'custom_authorizations', + tokens: 'custom_authorizations', + applications: 'custom_authorizations', + token_info: 'custom_authorizations' + + as authorizations: 'custom_auth', + tokens: 'custom_token', + token_info: 'custom_token_info' + end + end + + scope 'outer_space' do + use_doorkeeper do + controllers authorizations: 'custom_authorizations', + tokens: 'custom_authorizations', + token_info: 'custom_authorizations' + + as authorizations: 'custom_auth', + tokens: 'custom_token', + token_info: 'custom_token_info' + + skip_controllers :tokens, :applications, :token_info + end + end + + get 'metal.json' => 'metal#index' + + get '/callback', to: 'home#callback' + get '/sign_in', to: 'home#sign_in' + resources :semi_protected_resources + resources :full_protected_resources + root to: 'home#index' +end diff --git a/doorkeeper/spec/dummy/db/migrate/20111122132257_create_users.rb b/doorkeeper/spec/dummy/db/migrate/20111122132257_create_users.rb new file mode 100644 index 0000000000..37ce31726c --- /dev/null +++ b/doorkeeper/spec/dummy/db/migrate/20111122132257_create_users.rb @@ -0,0 +1,9 @@ +class CreateUsers < ActiveRecord::Migration + def change + create_table :users do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/doorkeeper/spec/dummy/db/migrate/20120312140401_add_password_to_users.rb b/doorkeeper/spec/dummy/db/migrate/20120312140401_add_password_to_users.rb new file mode 100644 index 0000000000..0770e36628 --- /dev/null +++ b/doorkeeper/spec/dummy/db/migrate/20120312140401_add_password_to_users.rb @@ -0,0 +1,5 @@ +class AddPasswordToUsers < ActiveRecord::Migration + def change + add_column :users, :password, :string + end +end diff --git a/doorkeeper/spec/dummy/db/migrate/20151223192035_create_doorkeeper_tables.rb b/doorkeeper/spec/dummy/db/migrate/20151223192035_create_doorkeeper_tables.rb new file mode 100644 index 0000000000..c66ee3c7ea --- /dev/null +++ b/doorkeeper/spec/dummy/db/migrate/20151223192035_create_doorkeeper_tables.rb @@ -0,0 +1,60 @@ +class CreateDoorkeeperTables < ActiveRecord::Migration + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.timestamps null: false + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_grants do |t| + t.integer :resource_owner_id, null: false + t.references :application, null: false + t.string :token, null: false + t.integer :expires_in, null: false + t.text :redirect_uri, null: false + t.datetime :created_at, null: false + t.datetime :revoked_at + t.string :scopes + end + + add_index :oauth_access_grants, :token, unique: true + add_foreign_key( + :oauth_access_grants, + :oauth_applications, + column: :application_id, + ) + + create_table :oauth_access_tokens do |t| + t.integer :resource_owner_id + t.references :application + + # If you use a custom token generator you may need to change this column + # from string to text, so that it accepts tokens larger than 255 + # characters. More info on custom token generators in: + # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator + # + # t.text :token, null: false + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, null: false + t.string :scopes + end + + add_index :oauth_access_tokens, :token, unique: true + add_index :oauth_access_tokens, :resource_owner_id + add_index :oauth_access_tokens, :refresh_token, unique: true + add_foreign_key( + :oauth_access_tokens, + :oauth_applications, + column: :application_id, + ) + end +end diff --git a/doorkeeper/spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb b/doorkeeper/spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb new file mode 100644 index 0000000000..76fcf8993d --- /dev/null +++ b/doorkeeper/spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb @@ -0,0 +1,7 @@ +class AddOwnerToApplication < ActiveRecord::Migration + def change + add_column :oauth_applications, :owner_id, :integer, null: true + add_column :oauth_applications, :owner_type, :string, null: true + add_index :oauth_applications, [:owner_id, :owner_type] + end +end diff --git a/doorkeeper/spec/dummy/db/migrate/20160320211015_add_previous_refresh_token_to_access_tokens.rb b/doorkeeper/spec/dummy/db/migrate/20160320211015_add_previous_refresh_token_to_access_tokens.rb new file mode 100644 index 0000000000..e3f07e352e --- /dev/null +++ b/doorkeeper/spec/dummy/db/migrate/20160320211015_add_previous_refresh_token_to_access_tokens.rb @@ -0,0 +1,11 @@ +class AddPreviousRefreshTokenToAccessTokens < ActiveRecord::Migration + def change + add_column( + :oauth_access_tokens, + :previous_refresh_token, + :string, + default: "", + null: false + ) + end +end diff --git a/doorkeeper/spec/dummy/db/schema.rb b/doorkeeper/spec/dummy/db/schema.rb new file mode 100644 index 0000000000..85cfe12a1a --- /dev/null +++ b/doorkeeper/spec/dummy/db/schema.rb @@ -0,0 +1,67 @@ +# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20160320211015) do + + create_table "oauth_access_grants", force: :cascade do |t| + t.integer "resource_owner_id", null: false + t.integer "application_id", null: false + t.string "token", null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.string "scopes" + end + + add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true + + create_table "oauth_access_tokens", force: :cascade do |t| + t.integer "resource_owner_id" + t.integer "application_id" + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" + t.datetime "revoked_at" + t.datetime "created_at", null: false + t.string "scopes" + t.string "previous_refresh_token", default: "", null: false + end + + add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true + add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" + add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true + + create_table "oauth_applications", force: :cascade do |t| + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.integer "owner_id" + t.string "owner_type" + end + + add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" + add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true + + create_table "users", force: :cascade do |t| + t.string "name" + t.datetime "created_at" + t.datetime "updated_at" + t.string "password" + end + +end diff --git a/doorkeeper/spec/dummy/public/404.html b/doorkeeper/spec/dummy/public/404.html new file mode 100644 index 0000000000..9a48320a5f --- /dev/null +++ b/doorkeeper/spec/dummy/public/404.html @@ -0,0 +1,26 @@ + + + + The page you were looking for doesn't exist (404) + + + + + +
+

The page you were looking for doesn't exist.

+

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

+
+ + diff --git a/doorkeeper/spec/dummy/public/422.html b/doorkeeper/spec/dummy/public/422.html new file mode 100644 index 0000000000..83660ab187 --- /dev/null +++ b/doorkeeper/spec/dummy/public/422.html @@ -0,0 +1,26 @@ + + + + The change you wanted was rejected (422) + + + + + +
+

The change you wanted was rejected.

+

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

+
+ + diff --git a/doorkeeper/spec/dummy/public/500.html b/doorkeeper/spec/dummy/public/500.html new file mode 100644 index 0000000000..b80307fc16 --- /dev/null +++ b/doorkeeper/spec/dummy/public/500.html @@ -0,0 +1,26 @@ + + + + We're sorry, but something went wrong (500) + + + + + +
+

We're sorry, but something went wrong.

+

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

+
+ + diff --git a/doorkeeper/spec/dummy/public/favicon.ico b/doorkeeper/spec/dummy/public/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doorkeeper/spec/dummy/script/rails b/doorkeeper/spec/dummy/script/rails new file mode 100755 index 0000000000..f8da2cffd4 --- /dev/null +++ b/doorkeeper/spec/dummy/script/rails @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +APP_PATH = File.expand_path('../../config/application', __FILE__) +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands' diff --git a/doorkeeper/spec/factories.rb b/doorkeeper/spec/factories.rb new file mode 100644 index 0000000000..4c91b355be --- /dev/null +++ b/doorkeeper/spec/factories.rb @@ -0,0 +1,28 @@ +FactoryBot.define do + factory :access_grant, class: Doorkeeper::AccessGrant do + sequence(:resource_owner_id) { |n| n } + application + redirect_uri 'https://app.com/callback' + expires_in 100 + scopes 'public write' + end + + factory :access_token, class: Doorkeeper::AccessToken do + sequence(:resource_owner_id) { |n| n } + application + expires_in 2.hours + + factory :clientless_access_token do + application nil + end + end + + factory :application, class: Doorkeeper::Application do + sequence(:name) { |n| "Application #{n}" } + redirect_uri 'https://app.com/callback' + end + + # do not name this factory :user, otherwise it will conflict with factories + # from applications that use doorkeeper factories in their own tests + factory :doorkeeper_testing_user, class: :user +end diff --git a/doorkeeper/spec/generators/application_owner_generator_spec.rb b/doorkeeper/spec/generators/application_owner_generator_spec.rb new file mode 100644 index 0000000000..9acae7bb34 --- /dev/null +++ b/doorkeeper/spec/generators/application_owner_generator_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper_integration' +require 'generators/doorkeeper/application_owner_generator' + +describe 'Doorkeeper::ApplicationOwnerGenerator' do + include GeneratorSpec::TestCase + + tests Doorkeeper::ApplicationOwnerGenerator + destination ::File.expand_path('../tmp/dummy', __FILE__) + + describe 'after running the generator' do + before :each do + prepare_destination + end + + context 'pre Rails 5.0.0' do + it 'creates a migration with no version specifier' do + stub_const("ActiveRecord::VERSION::MAJOR", 4) + stub_const("ActiveRecord::VERSION::MINOR", 2) + + run_generator + + assert_migration 'db/migrate/add_owner_to_application.rb' do |migration| + assert migration.include?("ActiveRecord::Migration\n") + end + end + end + + context 'post Rails 5.0.0' do + it 'creates a migration with a version specifier' do + stub_const("ActiveRecord::VERSION::MAJOR", 5) + stub_const("ActiveRecord::VERSION::MINOR", 0) + + run_generator + + assert_migration 'db/migrate/add_owner_to_application.rb' do |migration| + assert migration.include?("ActiveRecord::Migration[5.0]\n") + end + end + end + end +end diff --git a/doorkeeper/spec/generators/install_generator_spec.rb b/doorkeeper/spec/generators/install_generator_spec.rb new file mode 100644 index 0000000000..6485f91ae7 --- /dev/null +++ b/doorkeeper/spec/generators/install_generator_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper_integration' +require 'generators/doorkeeper/install_generator' + +describe 'Doorkeeper::InstallGenerator' do + include GeneratorSpec::TestCase + + tests Doorkeeper::InstallGenerator + destination ::File.expand_path('../tmp/dummy', __FILE__) + + describe 'after running the generator' do + before :each do + prepare_destination + FileUtils.mkdir(::File.expand_path('config', Pathname(destination_root))) + FileUtils.mkdir(::File.expand_path('db', Pathname(destination_root))) + FileUtils.copy_file(::File.expand_path('../templates/routes.rb', __FILE__), ::File.expand_path('config/routes.rb', Pathname.new(destination_root))) + run_generator + end + + it 'creates an initializer file' do + assert_file 'config/initializers/doorkeeper.rb' + end + + it 'copies the locale file' do + assert_file 'config/locales/doorkeeper.en.yml' + end + + it 'adds sample route' do + assert_file 'config/routes.rb', /use_doorkeeper/ + end + end +end diff --git a/doorkeeper/spec/generators/migration_generator_spec.rb b/doorkeeper/spec/generators/migration_generator_spec.rb new file mode 100644 index 0000000000..3644b66c1d --- /dev/null +++ b/doorkeeper/spec/generators/migration_generator_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper_integration' +require 'generators/doorkeeper/migration_generator' + +describe 'Doorkeeper::MigrationGenerator' do + include GeneratorSpec::TestCase + + tests Doorkeeper::MigrationGenerator + destination ::File.expand_path('../tmp/dummy', __FILE__) + + describe 'after running the generator' do + before :each do + prepare_destination + end + + context 'pre Rails 5.0.0' do + it 'creates a migration with no version specifier' do + stub_const('ActiveRecord::VERSION::MAJOR', 4) + stub_const('ActiveRecord::VERSION::MINOR', 2) + + run_generator + + assert_migration 'db/migrate/create_doorkeeper_tables.rb' do |migration| + assert migration.include?("ActiveRecord::Migration\n") + end + end + end + + context 'post Rails 5.0.0' do + it 'creates a migration with a version specifier' do + stub_const('ActiveRecord::VERSION::MAJOR', 5) + stub_const('ActiveRecord::VERSION::MINOR', 0) + + run_generator + + assert_migration 'db/migrate/create_doorkeeper_tables.rb' do |migration| + assert migration.include?("ActiveRecord::Migration[5.0]\n") + end + end + end + end +end diff --git a/doorkeeper/spec/generators/previous_refresh_token_generator_spec.rb b/doorkeeper/spec/generators/previous_refresh_token_generator_spec.rb new file mode 100644 index 0000000000..bae22850eb --- /dev/null +++ b/doorkeeper/spec/generators/previous_refresh_token_generator_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper_integration' +require 'generators/doorkeeper/previous_refresh_token_generator' + +describe 'Doorkeeper::PreviousRefreshTokenGenerator' do + include GeneratorSpec::TestCase + + tests Doorkeeper::PreviousRefreshTokenGenerator + destination ::File.expand_path('../tmp/dummy', __FILE__) + + describe 'after running the generator' do + before :each do + prepare_destination + + allow_any_instance_of(Doorkeeper::PreviousRefreshTokenGenerator).to( + receive(:no_previous_refresh_token_column?).and_return(true) + ) + end + + context 'pre Rails 5.0.0' do + it 'creates a migration with no version specifier' do + stub_const('ActiveRecord::VERSION::MAJOR', 4) + stub_const('ActiveRecord::VERSION::MINOR', 2) + + run_generator + + assert_migration 'db/migrate/add_previous_refresh_token_to_access_tokens.rb' do |migration| + assert migration.include?("ActiveRecord::Migration\n") + end + end + end + + context 'post Rails 5.0.0' do + it 'creates a migration with a version specifier' do + stub_const('ActiveRecord::VERSION::MAJOR', 5) + stub_const('ActiveRecord::VERSION::MINOR', 0) + + run_generator + + assert_migration 'db/migrate/add_previous_refresh_token_to_access_tokens.rb' do |migration| + assert migration.include?("ActiveRecord::Migration[5.0]\n") + end + end + end + + context 'already exist' do + it 'does not create a migration' do + allow_any_instance_of(Doorkeeper::PreviousRefreshTokenGenerator).to( + receive(:no_previous_refresh_token_column?).and_call_original + ) + + run_generator + + assert_no_migration 'db/migrate/add_previous_refresh_token_to_access_tokens.rb' + end + end + end +end diff --git a/doorkeeper/spec/generators/templates/routes.rb b/doorkeeper/spec/generators/templates/routes.rb new file mode 100644 index 0000000000..9240ef45e3 --- /dev/null +++ b/doorkeeper/spec/generators/templates/routes.rb @@ -0,0 +1,3 @@ +Rails.application.routes.draw do + +end diff --git a/doorkeeper/spec/generators/views_generator_spec.rb b/doorkeeper/spec/generators/views_generator_spec.rb new file mode 100644 index 0000000000..00f33fb87c --- /dev/null +++ b/doorkeeper/spec/generators/views_generator_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper_integration' +require 'generators/doorkeeper/views_generator' + +describe Doorkeeper::Generators::ViewsGenerator do + include GeneratorSpec::TestCase + + tests Doorkeeper::Generators::ViewsGenerator + destination File.expand_path('../tmp/dummy', __FILE__) + + before :each do + prepare_destination + end + + it 'create all views' do + run_generator + assert_file 'app/views/doorkeeper/applications/_form.html.erb' + assert_file 'app/views/doorkeeper/applications/edit.html.erb' + assert_file 'app/views/doorkeeper/applications/index.html.erb' + assert_file 'app/views/doorkeeper/applications/new.html.erb' + assert_file 'app/views/doorkeeper/applications/show.html.erb' + + assert_file 'app/views/doorkeeper/authorizations/error.html.erb' + assert_file 'app/views/doorkeeper/authorizations/new.html.erb' + + assert_file 'app/views/doorkeeper/authorized_applications/index.html.erb' + end +end diff --git a/doorkeeper/spec/grape/grape_integration_spec.rb b/doorkeeper/spec/grape/grape_integration_spec.rb new file mode 100644 index 0000000000..d1ee524012 --- /dev/null +++ b/doorkeeper/spec/grape/grape_integration_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper_integration' +require 'grape' +require 'rack/test' +require 'doorkeeper/grape/helpers' + +# Test Grape API application +module GrapeApp + class API < Grape::API + version 'v1', using: :path + format :json + prefix :api + + helpers Doorkeeper::Grape::Helpers + + resource :protected do + before do + doorkeeper_authorize! + end + + desc 'Protected resource, requires token.' + + get :status do + { token: doorkeeper_token.token } + end + end + + resource :protected_with_endpoint_scopes do + before do + doorkeeper_authorize! + end + + desc 'Protected resource, requires token with scopes (defined in endpoint).' + + get :status, scopes: [:admin] do + { response: 'OK' } + end + end + + resource :protected_with_helper_scopes do + before do + doorkeeper_authorize! :admin + end + + desc 'Protected resource, requires token with scopes (defined in helper).' + + get :status do + { response: 'OK' } + end + end + + resource :public do + desc "Public resource, no token required." + + get :status do + { response: 'OK' } + end + end + end +end + +describe 'Grape integration' do + include Rack::Test::Methods + + def app + GrapeApp::API + end + + def json_body + JSON.parse(last_response.body) + end + + let(:client) { FactoryBot.create(:application) } + let(:resource) { FactoryBot.create(:doorkeeper_testing_user, name: 'Joe', password: 'sekret') } + let(:access_token) { client_is_authorized(client, resource) } + + context 'with valid Access Token' do + it 'successfully requests protected resource' do + get "api/v1/protected/status.json?access_token=#{access_token.token}" + + expect(last_response).to be_successful + + expect(json_body['token']).to eq(access_token.token) + end + + it 'successfully requests protected resource with token that has required scopes (Grape endpoint)' do + access_token = client_is_authorized(client, resource, scopes: 'admin') + + get "api/v1/protected_with_endpoint_scopes/status.json?access_token=#{access_token.token}" + + expect(last_response).to be_successful + expect(json_body).to have_key('response') + end + + it 'successfully requests protected resource with token that has required scopes (Doorkeeper helper)' do + access_token = client_is_authorized(client, resource, scopes: 'admin') + + get "api/v1/protected_with_helper_scopes/status.json?access_token=#{access_token.token}" + + expect(last_response).to be_successful + expect(json_body).to have_key('response') + end + + it 'successfully requests public resource' do + get "api/v1/public/status.json" + + expect(last_response).to be_successful + expect(json_body).to have_key('response') + end + end + + context 'with invalid Access Token' do + it 'fails without access token' do + get "api/v1/protected/status.json" + + expect(last_response).not_to be_successful + expect(json_body).to have_key('error') + end + + it 'fails for access token without scopes' do + get "api/v1/protected_with_endpoint_scopes/status.json?access_token=#{access_token.token}" + + expect(last_response).not_to be_successful + expect(json_body).to have_key('error') + end + + it 'fails for access token with invalid scopes' do + access_token = client_is_authorized(client, resource, scopes: 'read write') + + get "api/v1/protected_with_endpoint_scopes/status.json?access_token=#{access_token.token}" + + expect(last_response).not_to be_successful + expect(json_body).to have_key('error') + end + end +end diff --git a/doorkeeper/spec/helpers/doorkeeper/dashboard_helper_spec.rb b/doorkeeper/spec/helpers/doorkeeper/dashboard_helper_spec.rb new file mode 100644 index 0000000000..d36a1fc54e --- /dev/null +++ b/doorkeeper/spec/helpers/doorkeeper/dashboard_helper_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper_integration' + +describe Doorkeeper::DashboardHelper do + describe '#doorkeeper_errors_for' do + let(:object) { double errors: { method: messages } } + let(:messages) { ['first message', 'second message'] } + + context 'when object has errors' do + it 'returns error messages' do + messages.each do |message| + expect(helper.doorkeeper_errors_for(object, :method)).to include( + message.capitalize + ) + end + end + end + + context 'when object has no errors' do + it 'returns nil' do + expect(helper.doorkeeper_errors_for(object, :amonter_method)).to be_nil + end + end + end +end diff --git a/doorkeeper/spec/lib/config_spec.rb b/doorkeeper/spec/lib/config_spec.rb new file mode 100644 index 0000000000..083b2e754e --- /dev/null +++ b/doorkeeper/spec/lib/config_spec.rb @@ -0,0 +1,437 @@ +require 'spec_helper_integration' + +describe Doorkeeper, 'configuration' do + subject { Doorkeeper.configuration } + + describe 'resource_owner_authenticator' do + it 'sets the block that is accessible via authenticate_resource_owner' do + block = proc {} + Doorkeeper.configure do + orm DOORKEEPER_ORM + resource_owner_authenticator(&block) + end + + expect(subject.authenticate_resource_owner).to eq(block) + end + + it 'prints warning message by default' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + end + + expect(Rails.logger).to receive(:warn).with( + I18n.t('doorkeeper.errors.messages.resource_owner_authenticator_not_configured') + ) + subject.authenticate_resource_owner.call(nil) + end + end + + describe 'resource_owner_from_credentials' do + it 'sets the block that is accessible via authenticate_resource_owner' do + block = proc {} + Doorkeeper.configure do + orm DOORKEEPER_ORM + resource_owner_from_credentials(&block) + end + + expect(subject.resource_owner_from_credentials).to eq(block) + end + + it 'prints warning message by default' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + end + + expect(Rails.logger).to receive(:warn).with( + I18n.t('doorkeeper.errors.messages.credential_flow_not_configured') + ) + subject.resource_owner_from_credentials.call(nil) + end + end + + describe 'setup_orm_adapter' do + it 'adds specific error message to NameError exception' do + expect do + Doorkeeper.configure { orm 'hibernate' } + end.to raise_error(NameError, /ORM adapter not found \(hibernate\)/) + end + + it 'does not change other exceptions' do + allow_any_instance_of(String).to receive(:classify) { raise NoMethodError } + + expect do + Doorkeeper.configure { orm 'hibernate' } + end.to raise_error(NoMethodError, /ORM adapter not found \(hibernate\)/) + end + end + + describe 'admin_authenticator' do + it 'sets the block that is accessible via authenticate_admin' do + block = proc {} + Doorkeeper.configure do + orm DOORKEEPER_ORM + admin_authenticator(&block) + end + + expect(subject.authenticate_admin).to eq(block) + end + end + + describe 'access_token_expires_in' do + it 'has 2 hours by default' do + expect(subject.access_token_expires_in).to eq(2.hours) + end + + it 'can change the value' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_expires_in 4.hours + end + expect(subject.access_token_expires_in).to eq(4.hours) + end + + it 'can be set to nil' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_expires_in nil + end + + expect(subject.access_token_expires_in).to be_nil + end + end + + describe 'scopes' do + it 'has default scopes' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + default_scopes :public + end + + expect(subject.default_scopes).to include('public') + end + + it 'has optional scopes' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + optional_scopes :write, :update + end + + expect(subject.optional_scopes).to include('write', 'update') + end + + it 'has all scopes' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + default_scopes :normal + optional_scopes :admin + end + + expect(subject.scopes).to include('normal', 'admin') + end + end + + describe 'use_refresh_token' do + it 'is false by default' do + expect(subject.refresh_token_enabled?).to be_falsey + end + + it 'can change the value' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + use_refresh_token + end + + expect(subject.refresh_token_enabled?).to be_truthy + end + + it "does not includes 'refresh_token' in authorization_response_types" do + expect(subject.token_grant_types).not_to include 'refresh_token' + end + + context "is enabled" do + before do + Doorkeeper.configure { + orm DOORKEEPER_ORM + use_refresh_token + } + end + + it "includes 'refresh_token' in authorization_response_types" do + expect(subject.token_grant_types).to include 'refresh_token' + end + end + end + + describe 'client_credentials' do + it 'has defaults order' do + expect(subject.client_credentials_methods).to eq([:from_basic, :from_params]) + end + + it 'can change the value' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + client_credentials :from_digest, :from_params + end + + expect(subject.client_credentials_methods).to eq([:from_digest, :from_params]) + end + end + + describe 'force_ssl_in_redirect_uri' do + it 'is true by default in non-development environments' do + expect(subject.force_ssl_in_redirect_uri).to be_truthy + end + + it 'can change the value' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + force_ssl_in_redirect_uri(false) + end + + expect(subject.force_ssl_in_redirect_uri).to be_falsey + end + + it 'can be a callable object' do + block = proc { false } + Doorkeeper.configure do + orm DOORKEEPER_ORM + force_ssl_in_redirect_uri(&block) + end + + expect(subject.force_ssl_in_redirect_uri).to eq(block) + expect(subject.force_ssl_in_redirect_uri.call).to be_falsey + end + end + + describe 'access_token_methods' do + it 'has defaults order' do + expect(subject.access_token_methods).to eq([:from_bearer_authorization, :from_access_token_param, :from_bearer_param]) + end + + it 'can change the value' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_methods :from_access_token_param, :from_bearer_param + end + + expect(subject.access_token_methods).to eq([:from_access_token_param, :from_bearer_param]) + end + end + + describe 'forbid_redirect_uri' do + it 'is false by default' do + expect(subject.forbid_redirect_uri.call(URI.parse('https://localhost'))).to be_falsey + end + + it 'can be a callable object' do + block = proc { true } + Doorkeeper.configure do + orm DOORKEEPER_ORM + forbid_redirect_uri(&block) + end + + expect(subject.forbid_redirect_uri).to eq(block) + expect(subject.forbid_redirect_uri.call).to be_truthy + end + end + + describe 'enable_application_owner' do + it 'is disabled by default' do + expect(Doorkeeper.configuration.enable_application_owner?).not_to be_truthy + end + + context 'when enabled without confirmation' do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + enable_application_owner + end + end + + it 'adds support for application owner' do + expect(Doorkeeper::Application.new).to respond_to :owner + end + + it 'Doorkeeper.configuration.confirm_application_owner? returns false' do + expect(Doorkeeper.configuration.confirm_application_owner?).not_to be_truthy + end + end + + context 'when enabled with confirmation set to true' do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + enable_application_owner confirmation: true + end + end + + it 'adds support for application owner' do + expect(Doorkeeper::Application.new).to respond_to :owner + end + + it 'Doorkeeper.configuration.confirm_application_owner? returns true' do + expect(Doorkeeper.configuration.confirm_application_owner?).to be_truthy + end + end + end + + describe 'realm' do + it 'is \'Doorkeeper\' by default' do + expect(Doorkeeper.configuration.realm).to eq('Doorkeeper') + end + + it 'can change the value' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + realm 'Example' + end + + expect(subject.realm).to eq('Example') + end + end + + describe "grant_flows" do + it "is set to all grant flows by default" do + expect(Doorkeeper.configuration.grant_flows). + to eq(%w[authorization_code client_credentials]) + end + + it "can change the value" do + Doorkeeper.configure do + orm DOORKEEPER_ORM + grant_flows ['authorization_code', 'implicit'] + end + + expect(subject.grant_flows).to eq ['authorization_code', 'implicit'] + end + + context "when including 'authorization_code'" do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + grant_flows ['authorization_code'] + end + end + + it "includes 'code' in authorization_response_types" do + expect(subject.authorization_response_types).to include 'code' + end + + it "includes 'authorization_code' in token_grant_types" do + expect(subject.token_grant_types).to include 'authorization_code' + end + end + + context "when including 'implicit'" do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + grant_flows ['implicit'] + end + end + + it "includes 'token' in authorization_response_types" do + expect(subject.authorization_response_types).to include 'token' + end + end + + context "when including 'password'" do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + grant_flows ['password'] + end + end + + it "includes 'password' in token_grant_types" do + expect(subject.token_grant_types).to include 'password' + end + end + + context "when including 'client_credentials'" do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + grant_flows ['client_credentials'] + end + end + + it "includes 'client_credentials' in token_grant_types" do + expect(subject.token_grant_types).to include 'client_credentials' + end + end + end + + it 'raises an exception when configuration is not set' do + old_config = Doorkeeper.configuration + Doorkeeper.module_eval do + @config = nil + end + + expect do + Doorkeeper.configuration + end.to raise_error Doorkeeper::MissingConfiguration + + Doorkeeper.module_eval do + @config = old_config + end + end + + describe 'access_token_generator' do + it 'is \'Doorkeeper::OAuth::Helpers::UniqueToken\' by default' do + expect(Doorkeeper.configuration.access_token_generator).to( + eq('Doorkeeper::OAuth::Helpers::UniqueToken') + ) + end + + it 'can change the value' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_generator 'Example' + end + expect(subject.access_token_generator).to eq('Example') + end + end + + describe 'base_controller' do + context 'default' do + it { expect(Doorkeeper.configuration.base_controller).to eq('ActionController::Base') } + end + + context 'custom' do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + base_controller 'ApplicationController' + end + end + + it { expect(Doorkeeper.configuration.base_controller).to eq('ApplicationController') } + end + end + + if DOORKEEPER_ORM == :active_record + describe 'active_record_options' do + let(:models) { [Doorkeeper::AccessGrant, Doorkeeper::AccessToken, Doorkeeper::Application] } + + before do + models.each do |model| + allow(model).to receive(:establish_connection).and_return(true) + end + end + + it 'establishes connection for Doorkeeper models based on options' do + models.each do |model| + expect(model).to receive(:establish_connection) + end + + Doorkeeper.configure do + orm DOORKEEPER_ORM + active_record_options( + establish_connection: Rails.configuration.database_configuration[Rails.env] + ) + end + end + end + end +end diff --git a/doorkeeper/spec/lib/doorkeeper_spec.rb b/doorkeeper/spec/lib/doorkeeper_spec.rb new file mode 100644 index 0000000000..7a6e5f683f --- /dev/null +++ b/doorkeeper/spec/lib/doorkeeper_spec.rb @@ -0,0 +1,150 @@ +require 'spec_helper_integration' + +describe Doorkeeper do + describe "#authenticate" do + let(:request) { double } + + it "calls OAuth::Token#authenticate" do + token_strategies = Doorkeeper.configuration.access_token_methods + + expect(Doorkeeper::OAuth::Token).to receive(:authenticate). + with(request, *token_strategies) + + Doorkeeper.authenticate(request) + end + + it "accepts custom token strategies" do + token_strategies = [:first_way, :second_way] + + expect(Doorkeeper::OAuth::Token).to receive(:authenticate). + with(request, *token_strategies) + + Doorkeeper.authenticate(request, token_strategies) + end + end + + describe "#configured?" do + after do + Doorkeeper.remove_instance_variable(:@config) + end + + context "@config is set" do + it "returns true" do + Doorkeeper.instance_variable_set(:@config, "hi") + + expect(Doorkeeper.configured?).to eq(true) + end + end + + context "@config is not set" do + it "returns false" do + Doorkeeper.instance_variable_set(:@config, nil) + + expect(Doorkeeper.configured?).to eq(false) + end + end + + it "is deprecated" do + expect(ActiveSupport::Deprecation).to receive(:warn). + with("Method `Doorkeeper#configured?` has been deprecated without replacement.") + + Doorkeeper.configured? + end + end + + describe "#database_installed?" do + before do + ["AccessToken", "AccessGrant", "Application"].each do |klass| + @original_classes ||= {} + @original_classes[klass] = Doorkeeper.const_get(klass) + Doorkeeper.send(:remove_const, klass) + end + end + + after do + ["AccessToken", "AccessGrant", "Application"].each do |klass| + Doorkeeper.send(:remove_const, klass) + Doorkeeper.const_set(klass, @original_classes[klass]) + end + end + + context "all tables exist" do + before do + klass = double table_exists?: true + + Doorkeeper.const_set(:AccessToken, klass) + Doorkeeper.const_set(:AccessGrant, klass) + Doorkeeper.const_set(:Application, klass) + end + + it "returns true" do + expect(Doorkeeper.database_installed?).to eq(true) + end + + it "is deprecated" do + expect(ActiveSupport::Deprecation).to receive(:warn). + with("Method `Doorkeeper#database_installed?` has been deprecated without replacement.") + + Doorkeeper.database_installed? + end + end + + context "all tables do not exist" do + before do + klass = double table_exists?: false + + Doorkeeper.const_set(:AccessToken, klass) + Doorkeeper.const_set(:AccessGrant, klass) + Doorkeeper.const_set(:Application, klass) + end + + it "returns false" do + expect(Doorkeeper.database_installed?).to eq(false) + end + + it "is deprecated" do + expect(ActiveSupport::Deprecation).to receive(:warn). + with("Method `Doorkeeper#database_installed?` has been deprecated without replacement.") + + Doorkeeper.database_installed? + end + end + end + + describe "#installed?" do + context "methods return true" do + before do + allow(Doorkeeper).to receive(:configured?).and_return(true).once + allow(Doorkeeper).to receive(:database_installed?).and_return(true).once + end + + it "returns true" do + expect(Doorkeeper.installed?).to eq(true) + end + end + + context "methods return false" do + before do + allow(Doorkeeper).to receive(:configured?).and_return(false).once + allow(Doorkeeper).to receive(:database_installed?).and_return(false).once + end + + it "returns false" do + expect(Doorkeeper.installed?).to eq(false) + end + end + + it "is deprecated" do + expect(ActiveSupport::Deprecation).to receive(:warn). + with("Method `Doorkeeper#configured?` has been deprecated without replacement.") + + expect(ActiveSupport::Deprecation).to receive(:warn). + with("Method `Doorkeeper#database_installed?` has been deprecated without replacement.") + + expect(ActiveSupport::Deprecation).to receive(:warn). + with("Method `Doorkeeper#installed?` has been deprecated without replacement.") + + Doorkeeper.installed? + end + end +end diff --git a/doorkeeper/spec/lib/models/expirable_spec.rb b/doorkeeper/spec/lib/models/expirable_spec.rb new file mode 100644 index 0000000000..e3a7959341 --- /dev/null +++ b/doorkeeper/spec/lib/models/expirable_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' +require 'active_support/time' +require 'doorkeeper/models/concerns/expirable' + +describe 'Expirable' do + subject do + Class.new do + include Doorkeeper::Models::Expirable + end.new + end + + before do + allow(subject).to receive(:created_at).and_return(1.minute.ago) + end + + describe :expired? do + it 'is not expired if time has not passed' do + allow(subject).to receive(:expires_in).and_return(2.minutes) + expect(subject).not_to be_expired + end + + it 'is expired if time has passed' do + allow(subject).to receive(:expires_in).and_return(10.seconds) + expect(subject).to be_expired + end + + it 'is not expired if expires_in is not set' do + allow(subject).to receive(:expires_in).and_return(nil) + expect(subject).not_to be_expired + end + end + + describe :expires_in_seconds do + it 'should return the amount of time remaining until the token is expired' do + allow(subject).to receive(:expires_in).and_return(2.minutes) + expect(subject.expires_in_seconds).to eq(60) + end + + it 'should return 0 when expired' do + allow(subject).to receive(:expires_in).and_return(30.seconds) + expect(subject.expires_in_seconds).to eq(0) + end + + it 'should return nil when expires_in is nil' do + allow(subject).to receive(:expires_in).and_return(nil) + expect(subject.expires_in_seconds).to be_nil + end + + end +end diff --git a/doorkeeper/spec/lib/models/revocable_spec.rb b/doorkeeper/spec/lib/models/revocable_spec.rb new file mode 100644 index 0000000000..aa04f84081 --- /dev/null +++ b/doorkeeper/spec/lib/models/revocable_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +require 'active_support/core_ext/object/blank' +require 'doorkeeper/models/concerns/revocable' + +describe 'Revocable' do + subject do + Class.new do + include Doorkeeper::Models::Revocable + end.new + end + + describe :revoke do + it 'updates :revoked_at attribute with current time' do + utc = double utc: double + clock = double now: utc + expect(subject).to receive(:update_attribute).with(:revoked_at, clock.now.utc) + subject.revoke(clock) + end + end + + describe :revoked? do + it 'is revoked if :revoked_at has passed' do + allow(subject).to receive(:revoked_at).and_return(Time.now.utc - 1000) + expect(subject).to be_revoked + end + + it 'is not revoked if :revoked_at has not passed' do + allow(subject).to receive(:revoked_at).and_return(Time.now.utc + 1000) + expect(subject).not_to be_revoked + end + + it 'is not revoked if :revoked_at is not set' do + allow(subject).to receive(:revoked_at).and_return(nil) + expect(subject).not_to be_revoked + end + end + + describe :revoke_previous_refresh_token! do + it "revokes the previous token if existing, and resets the + `previous_refresh_token` attribute" do + previous_token = FactoryBot.create( + :access_token, + refresh_token: "refresh_token" + ) + current_token = FactoryBot.create( + :access_token, + previous_refresh_token: previous_token.refresh_token + ) + + expect_any_instance_of( + Doorkeeper::AccessToken + ).to receive(:revoke).and_call_original + current_token.revoke_previous_refresh_token! + + expect(current_token.previous_refresh_token).to be_empty + expect(previous_token.reload).to be_revoked + end + end +end diff --git a/doorkeeper/spec/lib/models/scopes_spec.rb b/doorkeeper/spec/lib/models/scopes_spec.rb new file mode 100644 index 0000000000..efc979ddbd --- /dev/null +++ b/doorkeeper/spec/lib/models/scopes_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/object/blank' +require 'doorkeeper/oauth/scopes' +require 'doorkeeper/models/concerns/scopes' + +describe 'Doorkeeper::Models::Scopes' do + subject do + Class.new(Hash) do + include Doorkeeper::Models::Scopes + end.new + end + + before do + subject[:scopes] = 'public admin' + end + + describe :scopes do + it 'is a `Scopes` class' do + expect(subject.scopes).to be_a(Doorkeeper::OAuth::Scopes) + end + + it 'includes scopes' do + expect(subject.scopes).to include('public') + end + end + + describe :scopes_string do + it 'is a `Scopes` class' do + expect(subject.scopes_string).to eq('public admin') + end + end + + describe :includes_scope? do + it 'should return true if at least one scope is included' do + expect(subject.includes_scope?('public', 'private')).to be true + end + + it 'should return false if no scopes are included' do + expect(subject.includes_scope?('teacher', 'student')).to be false + end + end +end diff --git a/doorkeeper/spec/lib/oauth/authorization/uri_builder_spec.rb b/doorkeeper/spec/lib/oauth/authorization/uri_builder_spec.rb new file mode 100644 index 0000000000..4026645c63 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/authorization/uri_builder_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' +require 'active_support/core_ext/string' +require 'uri' +require 'rack/utils' +require 'doorkeeper/oauth/authorization/uri_builder' + +module Doorkeeper::OAuth::Authorization + describe URIBuilder do + subject { URIBuilder } + + describe :uri_with_query do + it 'returns the uri with query' do + uri = subject.uri_with_query 'http://example.com/', parameter: 'value' + expect(uri).to eq('http://example.com/?parameter=value') + end + + it 'rejects nil values' do + uri = subject.uri_with_query 'http://example.com/', parameter: '' + expect(uri).to eq('http://example.com/?') + end + + it 'preserves original query parameters' do + uri = subject.uri_with_query 'http://example.com/?query1=value', parameter: 'value' + expect(uri).to match(/query1=value/) + expect(uri).to match(/parameter=value/) + end + end + + describe :uri_with_fragment do + it 'returns uri with parameters as fragments' do + uri = subject.uri_with_fragment 'http://example.com/', parameter: 'value' + expect(uri).to eq('http://example.com/#parameter=value') + end + + it 'preserves original query parameters' do + uri = subject.uri_with_fragment 'http://example.com/?query1=value1', parameter: 'value' + expect(uri).to eq('http://example.com/?query1=value1#parameter=value') + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/authorization_code_request_spec.rb b/doorkeeper/spec/lib/oauth/authorization_code_request_spec.rb new file mode 100644 index 0000000000..c053898760 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/authorization_code_request_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper_integration' + +module Doorkeeper::OAuth + describe AuthorizationCodeRequest do + let(:server) do + double :server, + access_token_expires_in: 2.days, + refresh_token_enabled?: false, + custom_access_token_expires_in: ->(_app) { nil } + end + + let(:grant) { FactoryBot.create :access_grant } + let(:client) { grant.application } + let(:redirect_uri) { client.redirect_uri } + let(:params) { { redirect_uri: redirect_uri } } + + subject do + AuthorizationCodeRequest.new server, grant, client, params + end + + it 'issues a new token for the client' do + expect do + subject.authorize + end.to change { client.reload.access_tokens.count }.by(1) + end + + it "issues the token with same grant's scopes" do + subject.authorize + expect(Doorkeeper::AccessToken.last.scopes).to eq(grant.scopes) + end + + it 'revokes the grant' do + expect { subject.authorize }.to change { grant.reload.accessible? } + end + + it 'requires the grant to be accessible' do + grant.revoke + subject.validate + expect(subject.error).to eq(:invalid_grant) + end + + it 'requires the grant' do + subject.grant = nil + subject.validate + expect(subject.error).to eq(:invalid_grant) + end + + it 'requires the client' do + subject.client = nil + subject.validate + expect(subject.error).to eq(:invalid_client) + end + + it 'requires the redirect_uri' do + subject.redirect_uri = nil + subject.validate + expect(subject.error).to eq(:invalid_request) + end + + it "matches the redirect_uri with grant's one" do + subject.redirect_uri = 'http://other.com' + subject.validate + expect(subject.error).to eq(:invalid_grant) + end + + it "matches the client with grant's one" do + subject.client = FactoryBot.create :application + subject.validate + expect(subject.error).to eq(:invalid_grant) + end + + it 'skips token creation if there is a matching one' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + reuse_access_token + end + + FactoryBot.create(:access_token, application_id: client.id, + resource_owner_id: grant.resource_owner_id, scopes: grant.scopes.to_s) + + expect { subject.authorize }.to_not change { Doorkeeper::AccessToken.count } + end + + it "calls configured request callback methods" do + expect(Doorkeeper.configuration.before_successful_strategy_response).to receive(:call).with(subject).once + expect(Doorkeeper.configuration.after_successful_strategy_response).to receive(:call).with(subject, instance_of(Doorkeeper::OAuth::TokenResponse)).once + subject.authorize + end + + context "when redirect_uri contains some query params" do + let(:redirect_uri) { client.redirect_uri + "?query=q" } + + it "compares only host part with grant's redirect_uri" do + subject.validate + expect(subject.error).to eq(nil) + end + end + + context "when redirect_uri is not an URI" do + let(:redirect_uri) { '123d#!s' } + + it "responds with invalid_grant" do + subject.validate + expect(subject.error).to eq(:invalid_grant) + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/base_request_spec.rb b/doorkeeper/spec/lib/oauth/base_request_spec.rb new file mode 100644 index 0000000000..52c685b5c0 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/base_request_spec.rb @@ -0,0 +1,155 @@ +require 'spec_helper_integration' + +module Doorkeeper::OAuth + describe BaseRequest do + let(:access_token) do + double :access_token, + token: "some-token", + expires_in: "3600", + expires_in_seconds: "300", + scopes_string: "two scopes", + refresh_token: "some-refresh-token", + token_type: "bearer", + created_at: 0 + end + + let(:client) { double :client, id: '1' } + + let(:scopes_array) { %w[public write] } + + let(:server) do + double :server, + access_token_expires_in: 100, + custom_access_token_expires_in: ->(_) { nil }, + refresh_token_enabled?: false + end + + subject do + BaseRequest.new + end + + describe "#authorize" do + before do + allow(subject).to receive(:access_token).and_return(access_token) + end + + it "validates itself" do + expect(subject).to receive(:validate).once + subject.authorize + end + + context "valid" do + before do + allow(subject).to receive(:valid?).and_return(true) + end + + it "calls callback methods" do + expect(subject).to receive(:before_successful_response).once + expect(subject).to receive(:after_successful_response).once + subject.authorize + end + + it "returns a TokenResponse object" do + result = subject.authorize + + expect(result).to be_an_instance_of(TokenResponse) + expect(result.body).to eq( + TokenResponse.new(access_token).body + ) + end + end + + context "invalid" do + before do + allow(subject).to receive(:valid?).and_return(false) + allow(subject).to receive(:error).and_return("server_error") + allow(subject).to receive(:state).and_return("hello") + end + + it "returns an ErrorResponse object" do + error_description = I18n.translate( + "server_error", + scope: %i[doorkeeper errors messages] + ) + + result = subject.authorize + + expect(result).to be_an_instance_of(ErrorResponse) + + expect(result.body).to eq( + error: "server_error", + error_description: error_description, + state: "hello" + ) + end + end + end + + describe "#default_scopes" do + it "delegates to the server" do + expect(subject).to receive(:server).and_return(server).once + expect(server).to receive(:default_scopes).once + + subject.default_scopes + end + end + + describe "#find_or_create_access_token" do + it "returns an instance of AccessToken" do + result = subject.find_or_create_access_token( + client, + "1", + "public", + server + ) + + expect(result).to be_an_instance_of(Doorkeeper::AccessToken) + end + end + + describe "#scopes" do + context "@original_scopes is present" do + before do + subject.instance_variable_set(:@original_scopes, "public write") + end + + it "returns array of @original_scopes" do + result = subject.scopes + + expect(result).to eq(scopes_array) + end + end + + context "@original_scopes is not present" do + before do + subject.instance_variable_set(:@original_scopes, "") + end + + it "calls #default_scopes" do + allow(subject).to receive(:server).and_return(server).once + allow(server).to receive(:default_scopes).and_return(scopes_array).once + + result = subject.scopes + + expect(result).to eq(scopes_array) + end + end + end + + describe "#valid?" do + context "error is nil" do + it "returns true" do + allow(subject).to receive(:error).and_return(nil).once + expect(subject.valid?).to eq(true) + end + end + + context "error is not nil" do + it "returns false" do + allow(subject).to receive(:error).and_return(Object.new).once + expect(subject.valid?).to eq(false) + end + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/base_response_spec.rb b/doorkeeper/spec/lib/oauth/base_response_spec.rb new file mode 100644 index 0000000000..8f3b64853e --- /dev/null +++ b/doorkeeper/spec/lib/oauth/base_response_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper_integration' + +module Doorkeeper::OAuth + describe BaseResponse do + subject do + BaseResponse.new + end + + describe "#body" do + it "returns an empty Hash" do + expect(subject.body).to eq({}) + end + end + + describe "#description" do + it "returns an empty String" do + expect(subject.description).to eq("") + end + end + + describe "#headers" do + it "returns an empty Hash" do + expect(subject.headers).to eq({}) + end + end + + describe "#redirectable?" do + it "returns false" do + expect(subject.redirectable?).to eq(false) + end + end + + describe "#redirect_uri" do + it "returns an empty String" do + expect(subject.redirect_uri).to eq("") + end + end + + describe "#status" do + it "returns :ok" do + expect(subject.status).to eq(:ok) + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/client/credentials_spec.rb b/doorkeeper/spec/lib/oauth/client/credentials_spec.rb new file mode 100644 index 0000000000..a46223a76a --- /dev/null +++ b/doorkeeper/spec/lib/oauth/client/credentials_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' +require 'active_support/core_ext/string' +require 'doorkeeper/oauth/client' + +class Doorkeeper::OAuth::Client + describe Credentials do + let(:client_id) { 'some-uid' } + let(:client_secret) { 'some-secret' } + + it 'is blank when any of the credentials is blank' do + expect(Credentials.new(nil, 'something')).to be_blank + expect(Credentials.new('something', nil)).to be_blank + end + + describe :from_request do + let(:request) { double.as_null_object } + + let(:method) do + ->(_request) { return 'uid', 'secret' } + end + + it 'accepts anything that responds to #call' do + expect(method).to receive(:call).with(request) + Credentials.from_request request, method + end + + it 'delegates methods received as symbols to Credentials class' do + expect(Credentials).to receive(:from_params).with(request) + Credentials.from_request request, :from_params + end + + it 'stops at the first credentials found' do + not_called_method = double + expect(not_called_method).not_to receive(:call) + Credentials.from_request request, ->(_) {}, method, not_called_method + end + + it 'returns new Credentials' do + credentials = Credentials.from_request request, method + expect(credentials).to be_a(Credentials) + end + + it 'returns uid and secret from extractor method' do + credentials = Credentials.from_request request, method + expect(credentials.uid).to eq('uid') + expect(credentials.secret).to eq('secret') + end + end + + describe :from_params do + it 'returns credentials from parameters when Authorization header is not available' do + request = double parameters: { client_id: client_id, client_secret: client_secret } + uid, secret = Credentials.from_params(request) + + expect(uid).to eq('some-uid') + expect(secret).to eq('some-secret') + end + + it 'is blank when there are no credentials' do + request = double parameters: {} + uid, secret = Credentials.from_params(request) + + expect(uid).to be_blank + expect(secret).to be_blank + end + end + + describe :from_basic do + let(:credentials) { Base64.encode64("#{client_id}:#{client_secret}") } + + it 'decodes the credentials' do + request = double authorization: "Basic #{credentials}" + uid, secret = Credentials.from_basic(request) + + expect(uid).to eq('some-uid') + expect(secret).to eq('some-secret') + end + + it 'is blank if Authorization is not Basic' do + request = double authorization: "#{credentials}" + uid, secret = Credentials.from_basic(request) + + expect(uid).to be_blank + expect(secret).to be_blank + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/client_credentials/creator_spec.rb b/doorkeeper/spec/lib/oauth/client_credentials/creator_spec.rb new file mode 100644 index 0000000000..5614e2e4f2 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/client_credentials/creator_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper_integration' + +class Doorkeeper::OAuth::ClientCredentialsRequest + describe Creator do + let(:client) { FactoryBot.create :application } + let(:scopes) { Doorkeeper::OAuth::Scopes.from_string('public') } + + it 'creates a new token' do + expect do + subject.call(client, scopes) + end.to change { Doorkeeper::AccessToken.count }.by(1) + end + + context "when reuse_access_token is true" do + it "returns the existing valid token" do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) + existing_token = subject.call(client, scopes) + + result = subject.call(client, scopes) + + expect(Doorkeeper::AccessToken.count).to eq(1) + expect(result).to eq(existing_token) + end + end + + context "when reuse_access_token is false" do + it "returns a new token" do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(false) + existing_token = subject.call(client, scopes) + + result = subject.call(client, scopes) + + expect(Doorkeeper::AccessToken.count).to eq(2) + expect(result).not_to eq(existing_token) + end + end + + it 'returns false if creation fails' do + expect(Doorkeeper::AccessToken).to receive(:find_or_create_for).and_return(false) + created = subject.call(client, scopes) + expect(created).to be_falsey + end + end +end diff --git a/doorkeeper/spec/lib/oauth/client_credentials/issuer_spec.rb b/doorkeeper/spec/lib/oauth/client_credentials/issuer_spec.rb new file mode 100644 index 0000000000..32826dee06 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/client_credentials/issuer_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' +require 'active_support/all' +require 'doorkeeper/oauth/client_credentials/issuer' + +class Doorkeeper::OAuth::ClientCredentialsRequest + describe Issuer do + let(:creator) { double :acces_token_creator } + let(:server) do + double( + :server, + access_token_expires_in: 100, + custom_access_token_expires_in: ->(_app) { nil } + ) + end + let(:validation) { double :validation, valid?: true } + + subject { Issuer.new(server, validation) } + + describe :create do + let(:client) { double :client, id: 'some-id' } + let(:scopes) { 'some scope' } + + it 'creates and sets the token' do + expect(creator).to receive(:call).and_return('token') + subject.create client, scopes, creator + + expect(subject.token).to eq('token') + end + + it 'creates with correct token parameters' do + expect(creator).to receive(:call).with( + client, + scopes, + expires_in: 100, + use_refresh_token: false + ) + + subject.create client, scopes, creator + end + + it 'has error set to :server_error if creator fails' do + expect(creator).to receive(:call).and_return(false) + subject.create client, scopes, creator + + expect(subject.error).to eq(:server_error) + end + + context 'when validation fails' do + before do + allow(validation).to receive(:valid?).and_return(false) + allow(validation).to receive(:error).and_return(:validation_error) + expect(creator).not_to receive(:create) + end + + it 'has error set from validation' do + subject.create client, scopes, creator + expect(subject.error).to eq(:validation_error) + end + + it 'returns false' do + expect(subject.create(client, scopes, creator)).to be_falsey + end + end + + context 'with custom expirations' do + let(:custom_ttl) { 1233 } + let(:server) do + double( + :server, + custom_access_token_expires_in: ->(_app) { custom_ttl } + ) + end + + it 'creates with correct token parameters' do + expect(creator).to receive(:call).with( + client, + scopes, + expires_in: custom_ttl, + use_refresh_token: false + ) + subject.create client, scopes, creator + end + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/client_credentials/validation_spec.rb b/doorkeeper/spec/lib/oauth/client_credentials/validation_spec.rb new file mode 100644 index 0000000000..06f2703fe7 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/client_credentials/validation_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' +require 'active_support/all' +require 'doorkeeper/oauth/client_credentials/validation' + +class Doorkeeper::OAuth::ClientCredentialsRequest + describe Validation do + let(:server) { double :server, scopes: nil } + let(:application) { double scopes: nil } + let(:client) { double application: application } + let(:request) { double :request, client: client, scopes: nil } + + subject { Validation.new(server, request) } + + it 'is valid with valid request' do + expect(subject).to be_valid + end + + it 'is invalid when client is not present' do + allow(request).to receive(:client).and_return(nil) + expect(subject).not_to be_valid + end + + context 'with scopes' do + it 'is invalid when scopes are not included in the server' do + server_scopes = Doorkeeper::OAuth::Scopes.from_string 'email' + allow(server).to receive(:scopes).and_return(server_scopes) + allow(request).to receive(:scopes).and_return( + Doorkeeper::OAuth::Scopes.from_string 'invalid') + expect(subject).not_to be_valid + end + + context 'with application scopes' do + it 'is valid when scopes are included in the application' do + application_scopes = Doorkeeper::OAuth::Scopes.from_string 'app' + server_scopes = Doorkeeper::OAuth::Scopes.from_string 'email app' + allow(application).to receive(:scopes).and_return(application_scopes) + allow(server).to receive(:scopes).and_return(server_scopes) + allow(request).to receive(:scopes).and_return(application_scopes) + expect(subject).to be_valid + end + + it 'is invalid when scopes are not included in the application' do + application_scopes = Doorkeeper::OAuth::Scopes.from_string 'app' + server_scopes = Doorkeeper::OAuth::Scopes.from_string 'email app' + allow(application).to receive(:scopes).and_return(application_scopes) + allow(server).to receive(:scopes).and_return(server_scopes) + allow(request).to receive(:scopes).and_return( + Doorkeeper::OAuth::Scopes.from_string 'email') + expect(subject).not_to be_valid + end + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/client_credentials_integration_spec.rb b/doorkeeper/spec/lib/oauth/client_credentials_integration_spec.rb new file mode 100644 index 0000000000..8d532a4952 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/client_credentials_integration_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper_integration' + +module Doorkeeper::OAuth + describe ClientCredentialsRequest do + let(:server) { Doorkeeper.configuration } + + context 'with a valid request' do + let(:client) { FactoryBot.create :application } + + it 'issues an access token' do + request = ClientCredentialsRequest.new(server, client, {}) + expect do + request.authorize + end.to change { Doorkeeper::AccessToken.count }.by(1) + end + end + + describe 'with an invalid request' do + it 'does not issue an access token' do + request = ClientCredentialsRequest.new(server, nil, {}) + expect do + request.authorize + end.to_not change { Doorkeeper::AccessToken.count } + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/client_credentials_request_spec.rb b/doorkeeper/spec/lib/oauth/client_credentials_request_spec.rb new file mode 100644 index 0000000000..6c6592acfa --- /dev/null +++ b/doorkeeper/spec/lib/oauth/client_credentials_request_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' +require 'active_support/all' +require 'active_model' +require 'doorkeeper/oauth/client_credentials_request' + +module Doorkeeper::OAuth + describe ClientCredentialsRequest do + let(:server) do + double( + default_scopes: nil, + custom_access_token_expires_in: ->(_app) { nil } + ) + end + + let(:application) { double :application, scopes: Scopes.from_string('') } + let(:client) { double :client, application: application } + let(:token_creator) { double :issuer, create: true, token: double } + + subject { ClientCredentialsRequest.new(server, client) } + + before do + subject.issuer = token_creator + end + + it 'issues an access token for the current client' do + expect(token_creator).to receive(:create).with(client, nil) + subject.authorize + end + + it 'has successful response when issue was created' do + subject.authorize + expect(subject.response).to be_a(TokenResponse) + end + + context 'if issue was not created' do + before do + subject.issuer = double create: false, error: :invalid + end + + it 'has an error response' do + subject.authorize + expect(subject.response).to be_a(Doorkeeper::OAuth::ErrorResponse) + end + + it 'delegates the error to issuer' do + subject.authorize + expect(subject.error).to eq(:invalid) + end + end + + context 'with scopes' do + let(:default_scopes) { Doorkeeper::OAuth::Scopes.from_string('public email') } + + before do + allow(server).to receive(:default_scopes).and_return(default_scopes) + end + + it 'issues an access token with default scopes if none was requested' do + expect(token_creator).to receive(:create).with(client, default_scopes) + subject.authorize + end + + it 'issues an access token with requested scopes' do + subject = ClientCredentialsRequest.new(server, client, scope: 'email') + subject.issuer = token_creator + expect(token_creator).to receive(:create).with(client, Doorkeeper::OAuth::Scopes.from_string('email')) + subject.authorize + end + end + + context 'with restricted client' do + let(:default_scopes) do + Doorkeeper::OAuth::Scopes.from_string('public email') + end + let(:server_scopes) do + Doorkeeper::OAuth::Scopes.from_string('public email phone') + end + let(:client_scopes) do + Doorkeeper::OAuth::Scopes.from_string('public phone') + end + + before do + allow(server).to receive(:default_scopes).and_return(default_scopes) + allow(server).to receive(:scopes).and_return(server_scopes) + allow(server).to receive(:access_token_expires_in).and_return(100) + allow(application).to receive(:scopes).and_return(client_scopes) + allow(client).to receive(:id).and_return(nil) + end + + it 'delegates the error to issuer if no scope was requested' do + subject = ClientCredentialsRequest.new(server, client) + subject.authorize + expect(subject.response).to be_a(Doorkeeper::OAuth::ErrorResponse) + expect(subject.error).to eq(:invalid_scope) + end + + it 'issues an access token with requested scopes' do + subject = ClientCredentialsRequest.new(server, client, scope: 'phone') + subject.authorize + expect(subject.response).to be_a(Doorkeeper::OAuth::TokenResponse) + expect(subject.response.token.scopes_string).to eq('phone') + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/client_spec.rb b/doorkeeper/spec/lib/oauth/client_spec.rb new file mode 100644 index 0000000000..1f71660305 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/client_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' +require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/string' +require 'doorkeeper/oauth/client' + +module Doorkeeper::OAuth + describe Client do + describe :find do + let(:method) { double } + + it 'finds the client via uid' do + client = double + expect(method).to receive(:call).with('uid').and_return(client) + expect(Client.find('uid', method)).to be_a(Client) + end + + it 'returns nil if client was not found' do + expect(method).to receive(:call).with('uid').and_return(nil) + expect(Client.find('uid', method)).to be_nil + end + end + + describe :authenticate do + it 'returns the authenticated client via credentials' do + credentials = Client::Credentials.new('some-uid', 'some-secret') + authenticator = double + expect(authenticator).to receive(:call).with('some-uid', 'some-secret').and_return(double) + expect(Client.authenticate(credentials, authenticator)).to be_a(Client) + end + + it 'returns nil if client was not authenticated' do + credentials = Client::Credentials.new('some-uid', 'some-secret') + authenticator = double + expect(authenticator).to receive(:call).with('some-uid', 'some-secret').and_return(nil) + expect(Client.authenticate(credentials, authenticator)).to be_nil + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/code_request_spec.rb b/doorkeeper/spec/lib/oauth/code_request_spec.rb new file mode 100644 index 0000000000..e926e0e9cd --- /dev/null +++ b/doorkeeper/spec/lib/oauth/code_request_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper_integration' + +module Doorkeeper::OAuth + describe CodeRequest do + let(:pre_auth) do + double( + :pre_auth, + client: double(:application, id: 9990), + redirect_uri: 'http://tst.com/cb', + scopes: nil, + state: nil, + error: nil, + authorizable?: true + ) + end + + let(:owner) { double :owner, id: 8900 } + + subject do + CodeRequest.new(pre_auth, owner) + end + + it 'creates an access grant' do + expect do + subject.authorize + end.to change { Doorkeeper::AccessGrant.count }.by(1) + end + + it 'returns a code response' do + expect(subject.authorize).to be_a(CodeResponse) + end + + it 'does not create grant when not authorizable' do + allow(pre_auth).to receive(:authorizable?).and_return(false) + expect { subject.authorize }.not_to change { Doorkeeper::AccessGrant.count } + end + + it 'returns a error response' do + allow(pre_auth).to receive(:authorizable?).and_return(false) + expect(subject.authorize).to be_a(ErrorResponse) + end + end +end diff --git a/doorkeeper/spec/lib/oauth/code_response_spec.rb b/doorkeeper/spec/lib/oauth/code_response_spec.rb new file mode 100644 index 0000000000..3261a7a7ff --- /dev/null +++ b/doorkeeper/spec/lib/oauth/code_response_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +module Doorkeeper + module OAuth + describe CodeResponse do + describe '.redirect_uri' do + context 'when generating the redirect URI for an implicit grant' do + let :pre_auth do + double( + :pre_auth, + client: double(:application, id: 1), + redirect_uri: 'http://tst.com/cb', + state: nil, + scopes: Scopes.from_string('public'), + ) + end + + let :auth do + Authorization::Token.new(pre_auth, double(id: 1)).tap do |c| + c.issue_token + allow(c.token).to receive(:expires_in_seconds).and_return(3600) + end + end + + subject { CodeResponse.new(pre_auth, auth, response_on_fragment: true).redirect_uri } + + it 'includes the remaining TTL of the token relative to the time the token was generated' do + expect(subject).to include('expires_in=3600') + end + end + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/error_response_spec.rb b/doorkeeper/spec/lib/oauth/error_response_spec.rb new file mode 100644 index 0000000000..3c33eb689c --- /dev/null +++ b/doorkeeper/spec/lib/oauth/error_response_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'active_model' +require 'doorkeeper/oauth/error' +require 'doorkeeper/oauth/error_response' + +module Doorkeeper::OAuth + describe ErrorResponse do + describe '#status' do + it 'should have a status of unauthorized' do + expect(subject.status).to eq(:unauthorized) + end + end + + describe :from_request do + it 'has the error from request' do + error = ErrorResponse.from_request double(error: :some_error) + expect(error.name).to eq(:some_error) + end + + it 'ignores state if request does not respond to state' do + error = ErrorResponse.from_request double(error: :some_error) + expect(error.state).to be_nil + end + + it 'has state if request responds to state' do + error = ErrorResponse.from_request double(error: :some_error, state: :hello) + expect(error.state).to eq(:hello) + end + end + + it 'ignores empty error values' do + subject = ErrorResponse.new(error: :some_error, state: nil) + expect(subject.body).not_to have_key(:state) + end + + describe '.body' do + subject { ErrorResponse.new(name: :some_error, state: :some_state).body } + + describe '#body' do + it { expect(subject).to have_key(:error) } + it { expect(subject).to have_key(:error_description) } + it { expect(subject).to have_key(:state) } + end + end + + describe '.headers' do + let(:error_response) { ErrorResponse.new(name: :some_error, state: :some_state) } + subject { error_response.headers } + + it { expect(subject).to include 'WWW-Authenticate' } + + describe "WWW-Authenticate header" do + subject { error_response.headers["WWW-Authenticate"] } + + it { expect(subject).to include("realm=\"#{error_response.realm}\"") } + it { expect(subject).to include("error=\"#{error_response.name}\"") } + it { expect(subject).to include("error_description=\"#{error_response.description}\"") } + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/error_spec.rb b/doorkeeper/spec/lib/oauth/error_spec.rb new file mode 100644 index 0000000000..0422bfeb89 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/error_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' +require 'active_support/i18n' +require 'doorkeeper/oauth/error' + +module Doorkeeper::OAuth + describe Error do + subject(:error) { Error.new(:some_error, :some_state) } + + it { expect(subject).to respond_to(:name) } + it { expect(subject).to respond_to(:state) } + + describe :description do + it 'is translated from translation messages' do + expect(I18n).to receive(:translate).with( + :some_error, + scope: %i[doorkeeper errors messages], + default: :server_error + ) + error.description + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/forbidden_token_response_spec.rb b/doorkeeper/spec/lib/oauth/forbidden_token_response_spec.rb new file mode 100644 index 0000000000..c1b5e9db9c --- /dev/null +++ b/doorkeeper/spec/lib/oauth/forbidden_token_response_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' +require 'active_model' +require 'doorkeeper' +require 'doorkeeper/oauth/forbidden_token_response' + +module Doorkeeper::OAuth + describe ForbiddenTokenResponse do + describe '#name' do + it { expect(subject.name).to eq(:invalid_scope) } + end + + describe '#status' do + it { expect(subject.status).to eq(:forbidden) } + end + + describe :from_scopes do + it 'should have a list of acceptable scopes' do + response = ForbiddenTokenResponse.from_scopes(["public"]) + expect(response.description).to include('public') + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/helpers/scope_checker_spec.rb b/doorkeeper/spec/lib/oauth/helpers/scope_checker_spec.rb new file mode 100644 index 0000000000..6006004f40 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/helpers/scope_checker_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' +require 'active_support/core_ext/string' +require 'doorkeeper/oauth/helpers/scope_checker' +require 'doorkeeper/oauth/scopes' + +module Doorkeeper::OAuth::Helpers + describe ScopeChecker, '.valid?' do + let(:server_scopes) { Doorkeeper::OAuth::Scopes.new } + + it 'is valid if scope is present' do + server_scopes.add :scope + expect(ScopeChecker.valid?('scope', server_scopes)).to be_truthy + end + + it 'is invalid if includes tabs space' do + expect(ScopeChecker.valid?("\tsomething", server_scopes)).to be_falsey + end + + it 'is invalid if scope is not present' do + expect(ScopeChecker.valid?(nil, server_scopes)).to be_falsey + end + + it 'is invalid if scope is blank' do + expect(ScopeChecker.valid?(' ', server_scopes)).to be_falsey + end + + it 'is invalid if includes return space' do + expect(ScopeChecker.valid?("scope\r", server_scopes)).to be_falsey + end + + it 'is invalid if includes new lines' do + expect(ScopeChecker.valid?("scope\nanother", server_scopes)).to be_falsey + end + + it 'is invalid if any scope is not included in server scopes' do + expect(ScopeChecker.valid?('scope another', server_scopes)).to be_falsey + end + + context 'with application_scopes' do + let(:server_scopes) do + Doorkeeper::OAuth::Scopes.from_string 'common svr' + end + let(:application_scopes) do + Doorkeeper::OAuth::Scopes.from_string 'app123' + end + + it 'is valid if scope is included in the application scope list' do + expect(ScopeChecker.valid?( + 'app123', + server_scopes, + application_scopes + )).to be_truthy + end + + it 'is invalid if any scope is not included in the application' do + expect(ScopeChecker.valid?( + 'svr', + server_scopes, + application_scopes + )).to be_falsey + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/helpers/unique_token_spec.rb b/doorkeeper/spec/lib/oauth/helpers/unique_token_spec.rb new file mode 100644 index 0000000000..dd4247ae0e --- /dev/null +++ b/doorkeeper/spec/lib/oauth/helpers/unique_token_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' +require 'doorkeeper/oauth/helpers/unique_token' + +module Doorkeeper::OAuth::Helpers + describe UniqueToken do + let :generator do + ->(size) { 'a' * size } + end + + it 'is able to customize the generator method' do + token = UniqueToken.generate(generator: generator) + expect(token).to eq('a' * 32) + end + + it 'is able to customize the size of the token' do + token = UniqueToken.generate(generator: generator, size: 2) + expect(token).to eq('aa') + end + end +end diff --git a/doorkeeper/spec/lib/oauth/helpers/uri_checker_spec.rb b/doorkeeper/spec/lib/oauth/helpers/uri_checker_spec.rb new file mode 100644 index 0000000000..78ed90fba3 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/helpers/uri_checker_spec.rb @@ -0,0 +1,213 @@ +require 'spec_helper' +require 'uri' +require 'doorkeeper/oauth/helpers/uri_checker' + +module Doorkeeper::OAuth::Helpers + describe URIChecker do + describe '.valid?' do + it 'is valid for valid uris' do + uri = 'http://app.co' + expect(URIChecker.valid?(uri)).to be_truthy + end + + it 'is valid if include path param' do + uri = 'http://app.co/path' + expect(URIChecker.valid?(uri)).to be_truthy + end + + it 'is valid if include query param' do + uri = 'http://app.co/?query=1' + expect(URIChecker.valid?(uri)).to be_truthy + end + + it 'is invalid if uri includes fragment' do + uri = 'http://app.co/test#fragment' + expect(URIChecker.valid?(uri)).to be_falsey + end + + it 'is invalid if scheme is missing' do + uri = 'app.co' + expect(URIChecker.valid?(uri)).to be_falsey + end + + it 'is invalid if is a relative uri' do + uri = '/abc/123' + expect(URIChecker.valid?(uri)).to be_falsey + end + + it 'is invalid if is not a url' do + uri = 'http://' + expect(URIChecker.valid?(uri)).to be_falsey + end + + it 'is invalid if is not an uri' do + uri = ' ' + expect(URIChecker.valid?(uri)).to be_falsey + end + end + + describe '.matches?' do + it 'is true if both url matches' do + uri = client_uri = 'http://app.co/aaa' + expect(URIChecker.matches?(uri, client_uri)).to be_truthy + end + + it 'ignores query parameter on comparsion' do + uri = 'http://app.co/?query=hello' + client_uri = 'http://app.co' + expect(URIChecker.matches?(uri, client_uri)).to be_truthy + end + + it 'doesn\'t allow non-matching domains through' do + uri = 'http://app.abc/?query=hello' + client_uri = 'http://app.co' + expect(URIChecker.matches?(uri, client_uri)).to be_falsey + end + + it 'doesn\'t allow non-matching domains that don\'t start at the beginning' do + uri = 'http://app.co/?query=hello' + client_uri = 'http://example.com?app.co=test' + expect(URIChecker.matches?(uri, client_uri)).to be_falsey + end + + context "client registered query params" do + it "doesn't allow query being absent" do + uri = 'http://app.co' + client_uri = 'http://app.co/?vendorId=AJ4L7XXW9' + expect(URIChecker.matches?(uri, client_uri)).to be_falsey + end + + it "is false if query values differ but key same" do + uri = 'http://app.co/?vendorId=pancakes' + client_uri = 'http://app.co/?vendorId=waffles' + expect(URIChecker.matches?(uri, client_uri)).to be_falsey + end + + it "is false if query values same but key differs" do + uri = 'http://app.co/?foo=pancakes' + client_uri = 'http://app.co/?bar=pancakes' + expect(URIChecker.matches?(uri, client_uri)).to be_falsey + end + + it "is false if query present and match, but unknown queries present" do + uri = 'http://app.co/?vendorId=pancakes&unknown=query' + client_uri = 'http://app.co/?vendorId=waffles' + expect(URIChecker.matches?(uri, client_uri)).to be_falsey + end + + it "is true if queries are present and matche" do + uri = 'http://app.co/?vendorId=AJ4L7XXW9&foo=bar' + client_uri = 'http://app.co/?vendorId=AJ4L7XXW9&foo=bar' + expect(URIChecker.matches?(uri, client_uri)).to be_truthy + end + + it "is true if queries are present, match and in different order" do + uri = 'http://app.co/?bing=bang&foo=bar' + client_uri = 'http://app.co/?foo=bar&bing=bang' + expect(URIChecker.matches?(uri, client_uri)).to be_truthy + end + end + end + + describe '.valid_for_authorization?' do + it 'is true if valid and matches' do + uri = client_uri = 'http://app.co/aaa' + expect(URIChecker.valid_for_authorization?(uri, client_uri)).to be_truthy + end + + it 'is false if valid and mismatches' do + uri = 'http://app.co/aaa' + client_uri = 'http://app.co/bbb' + expect(URIChecker.valid_for_authorization?(uri, client_uri)).to be_falsey + end + + it 'is true if valid and included in array' do + uri = 'http://app.co/aaa' + client_uri = "http://example.com/bbb\nhttp://app.co/aaa" + expect(URIChecker.valid_for_authorization?(uri, client_uri)).to be_truthy + end + + it 'is false if valid and not included in array' do + uri = 'http://app.co/aaa' + client_uri = "http://example.com/bbb\nhttp://app.co/cc" + expect(URIChecker.valid_for_authorization?(uri, client_uri)).to be_falsey + end + + it 'is true if valid and matches' do + uri = client_uri = 'http://app.co/aaa' + expect(URIChecker.valid_for_authorization?(uri, client_uri)).to be true + end + + it 'is false if invalid' do + uri = 'http://app.co/aaa?pankcakes=abc' + client_uri = 'http://app.co/aaa?waffles=abc' + expect(URIChecker.valid_for_authorization?(uri, client_uri)).to be false + end + + it 'calls .matches?' do + uri = 'http://app.co/aaa?pankcakes=abc' + client_uri = 'http://app.co/aaa?waffles=abc' + expect(URIChecker).to receive(:matches?).with(uri, client_uri).once + URIChecker.valid_for_authorization?(uri, client_uri) + end + + it 'calls .valid?' do + uri = 'http://app.co/aaa?pankcakes=abc' + client_uri = 'http://app.co/aaa?waffles=abc' + expect(URIChecker).to receive(:valid?).with(uri).once + URIChecker.valid_for_authorization?(uri, client_uri) + end + end + + describe '.query_matches?' do + it 'is true if no queries' do + expect(URIChecker.query_matches?('', '')).to be_truthy + expect(URIChecker.query_matches?(nil, nil)).to be_truthy + end + + it 'is true if same query' do + expect(URIChecker.query_matches?('foo', 'foo')).to be_truthy + end + + it 'is false if different query' do + expect(URIChecker.query_matches?('foo', 'bar')).to be_falsey + end + + it 'is true if same queries' do + expect(URIChecker.query_matches?('foo&bar', 'foo&bar')).to be_truthy + end + + it 'is true if same queries, different order' do + expect(URIChecker.query_matches?('foo&bar', 'bar&foo')).to be_truthy + end + + it 'is false if one different query' do + expect(URIChecker.query_matches?('foo&bang', 'foo&bing')).to be_falsey + end + + it 'is true if same query with same value' do + expect(URIChecker.query_matches?('foo=bar', 'foo=bar')).to be_truthy + end + + it 'is true if same queries with same values' do + expect(URIChecker.query_matches?('foo=bar&bing=bang', 'foo=bar&bing=bang')).to be_truthy + end + + it 'is true if same queries with same values, different order' do + expect(URIChecker.query_matches?('foo=bar&bing=bang', 'bing=bang&foo=bar')).to be_truthy + end + + it 'is false if same query with different value' do + expect(URIChecker.query_matches?('foo=bar', 'foo=bang')).to be_falsey + end + + it 'is false if some queries missing' do + expect(URIChecker.query_matches?('foo=bar', 'foo=bar&bing=bang')).to be_falsey + end + + it 'is false if some queries different value' do + expect(URIChecker.query_matches?('foo=bar&bing=bang', 'foo=bar&bing=banana')).to be_falsey + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/invalid_token_response_spec.rb b/doorkeeper/spec/lib/oauth/invalid_token_response_spec.rb new file mode 100644 index 0000000000..ede5fc68bf --- /dev/null +++ b/doorkeeper/spec/lib/oauth/invalid_token_response_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'active_model' +require 'doorkeeper' +require 'doorkeeper/oauth/invalid_token_response' + +module Doorkeeper::OAuth + describe InvalidTokenResponse do + describe "#name" do + it { expect(subject.name).to eq(:invalid_token) } + end + + describe "#status" do + it { expect(subject.status).to eq(:unauthorized) } + end + + describe :from_access_token do + let(:response) { InvalidTokenResponse.from_access_token(access_token) } + + context "revoked" do + let(:access_token) { double(revoked?: true, expired?: true) } + + it "sets a description" do + expect(response.description).to include("revoked") + end + + it "sets the reason" do + expect(response.reason).to eq(:revoked) + end + end + + context "expired" do + let(:access_token) { double(revoked?: false, expired?: true) } + + it "sets a description" do + expect(response.description).to include("expired") + end + + it "sets the reason" do + expect(response.reason).to eq(:expired) + end + end + + context "unknown" do + let(:access_token) { double(revoked?: false, expired?: false) } + + it "sets a description" do + expect(response.description).to include("invalid") + end + + it "sets the reason" do + expect(response.reason).to eq(:unknown) + end + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/password_access_token_request_spec.rb b/doorkeeper/spec/lib/oauth/password_access_token_request_spec.rb new file mode 100644 index 0000000000..3f8bdb82a4 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/password_access_token_request_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper_integration' + +module Doorkeeper::OAuth + describe PasswordAccessTokenRequest do + let(:server) do + double( + :server, + default_scopes: Doorkeeper::OAuth::Scopes.new, + access_token_expires_in: 2.hours, + refresh_token_enabled?: false, + custom_access_token_expires_in: ->(_app) { nil } + ) + end + let(:client) { FactoryBot.create(:application) } + let(:owner) { double :owner, id: 99 } + + subject do + PasswordAccessTokenRequest.new(server, client, owner) + end + + it 'issues a new token for the client' do + expect do + subject.authorize + end.to change { client.reload.access_tokens.count }.by(1) + end + + it 'issues a new token without a client' do + expect do + subject.client = nil + subject.authorize + end.to change { Doorkeeper::AccessToken.count }.by(1) + end + + it 'does not issue a new token with an invalid client' do + expect do + subject.client = nil + subject.parameters = { client_id: 'bad_id' } + subject.authorize + end.to_not change { Doorkeeper::AccessToken.count } + + expect(subject.error).to eq(:invalid_client) + end + + it 'requires the owner' do + subject.resource_owner = nil + subject.validate + expect(subject.error).to eq(:invalid_grant) + end + + it 'optionally accepts the client' do + subject.client = nil + expect(subject).to be_valid + end + + it 'creates token even when there is already one (default)' do + FactoryBot.create(:access_token, application_id: client.id, resource_owner_id: owner.id) + expect do + subject.authorize + end.to change { Doorkeeper::AccessToken.count }.by(1) + end + + it 'skips token creation if there is already one' do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) + FactoryBot.create(:access_token, application_id: client.id, resource_owner_id: owner.id) + expect do + subject.authorize + end.to_not change { Doorkeeper::AccessToken.count } + end + + it "calls configured request callback methods" do + expect(Doorkeeper.configuration.before_successful_strategy_response).to receive(:call).with(subject).once + expect(Doorkeeper.configuration.after_successful_strategy_response).to receive(:call).with(subject, instance_of(Doorkeeper::OAuth::TokenResponse)).once + subject.authorize + end + + describe 'with scopes' do + subject do + PasswordAccessTokenRequest.new(server, client, owner, scope: 'public') + end + + it 'validates the current scope' do + allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string('another')) + subject.validate + expect(subject.error).to eq(:invalid_scope) + end + + it 'creates the token with scopes' do + allow(server).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string('public')) + expect do + subject.authorize + end.to change { Doorkeeper::AccessToken.count }.by(1) + expect(Doorkeeper::AccessToken.last.scopes).to include('public') + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/pre_authorization_spec.rb b/doorkeeper/spec/lib/oauth/pre_authorization_spec.rb new file mode 100644 index 0000000000..0b58b69266 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/pre_authorization_spec.rb @@ -0,0 +1,155 @@ +require 'spec_helper_integration' + +module Doorkeeper::OAuth + describe PreAuthorization do + let(:server) { + server = Doorkeeper.configuration + allow(server).to receive(:default_scopes).and_return(Scopes.new) + allow(server).to receive(:scopes).and_return(Scopes.from_string('public profile')) + server + } + + let(:application) do + application = double :application + allow(application).to receive(:scopes).and_return(Scopes.from_string('')) + application + end + + let(:client) do + double :client, redirect_uri: 'http://tst.com/auth', application: application + end + + let :attributes do + { + response_type: 'code', + redirect_uri: 'http://tst.com/auth', + state: 'save-this' + } + end + + subject do + PreAuthorization.new(server, client, attributes) + end + + it 'is authorizable when request is valid' do + expect(subject).to be_authorizable + end + + it 'accepts code as response type' do + subject.response_type = 'code' + expect(subject).to be_authorizable + end + + it 'accepts token as response type' do + allow(server).to receive(:grant_flows).and_return(['implicit']) + subject.response_type = 'token' + expect(subject).to be_authorizable + end + + context 'when using default grant flows' do + it 'accepts "code" as response type' do + subject.response_type = 'code' + expect(subject).to be_authorizable + end + + it 'accepts "token" as response type' do + allow(server).to receive(:grant_flows).and_return(['implicit']) + subject.response_type = 'token' + expect(subject).to be_authorizable + end + end + + context 'when authorization code grant flow is disabled' do + before do + allow(server).to receive(:grant_flows).and_return(['implicit']) + end + + it 'does not accept "code" as response type' do + subject.response_type = 'code' + expect(subject).not_to be_authorizable + end + end + + context 'when implicit grant flow is disabled' do + before do + allow(server).to receive(:grant_flows).and_return(['authorization_code']) + end + + it 'does not accept "token" as response type' do + subject.response_type = 'token' + expect(subject).not_to be_authorizable + end + end + + context 'client application does not restrict valid scopes' do + it 'accepts valid scopes' do + subject.scope = 'public' + expect(subject).to be_authorizable + end + + it 'rejects (globally) non-valid scopes' do + subject.scope = 'invalid' + expect(subject).not_to be_authorizable + end + end + + context 'client application restricts valid scopes' do + let(:application) do + application = double :application + allow(application).to receive(:scopes).and_return(Scopes.from_string('public nonsense')) + application + end + + it 'accepts valid scopes' do + subject.scope = 'public' + expect(subject).to be_authorizable + end + + it 'rejects (globally) non-valid scopes' do + subject.scope = 'invalid' + expect(subject).not_to be_authorizable + end + + it 'rejects (application level) non-valid scopes' do + subject.scope = 'profile' + expect(subject).to_not be_authorizable + end + end + + it 'uses default scopes when none is required' do + allow(server).to receive(:default_scopes).and_return(Scopes.from_string('default')) + subject.scope = nil + expect(subject.scope).to eq('default') + expect(subject.scopes).to eq(Scopes.from_string('default')) + end + + it 'accepts test uri' do + subject.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob' + expect(subject).to be_authorizable + end + + it 'matches the redirect uri against client\'s one' do + subject.redirect_uri = 'http://nothesame.com' + expect(subject).not_to be_authorizable + end + + it 'stores the state' do + expect(subject.state).to eq('save-this') + end + + it 'rejects if response type is not allowed' do + subject.response_type = 'whops' + expect(subject).not_to be_authorizable + end + + it 'requires an existing client' do + subject.client = nil + expect(subject).not_to be_authorizable + end + + it 'requires a redirect uri' do + subject.redirect_uri = nil + expect(subject).not_to be_authorizable + end + end +end diff --git a/doorkeeper/spec/lib/oauth/refresh_token_request_spec.rb b/doorkeeper/spec/lib/oauth/refresh_token_request_spec.rb new file mode 100644 index 0000000000..610515af12 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/refresh_token_request_spec.rb @@ -0,0 +1,166 @@ +require 'spec_helper_integration' + +module Doorkeeper::OAuth + describe RefreshTokenRequest do + before do + allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false) + end + + let(:server) do + double :server, + access_token_expires_in: 2.minutes, + custom_access_token_expires_in: -> (_oauth_client) { nil } + end + + let(:refresh_token) do + FactoryBot.create(:access_token, use_refresh_token: true) + end + + let(:client) { refresh_token.application } + let(:credentials) { Client::Credentials.new(client.uid, client.secret) } + + subject { RefreshTokenRequest.new server, refresh_token, credentials } + + it 'issues a new token for the client' do + expect { subject.authorize }.to change { client.reload.access_tokens.count }.by(1) + # #sort_by used for MongoDB ORM extensions for valid ordering + expect(client.reload.access_tokens.sort_by(&:created_at).last.expires_in).to eq(120) + end + + it 'issues a new token for the client with custom expires_in' do + server = double :server, + access_token_expires_in: 2.minutes, + custom_access_token_expires_in: ->(_oauth_client) { 1234 } + + allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false) + + RefreshTokenRequest.new(server, refresh_token, credentials).authorize + + # #sort_by used for MongoDB ORM extensions for valid ordering + expect(client.reload.access_tokens.sort_by(&:created_at).last.expires_in).to eq(1234) + end + + it 'revokes the previous token' do + expect { subject.authorize }.to change { refresh_token.revoked? }.from(false).to(true) + end + + it "calls configured request callback methods" do + expect(Doorkeeper.configuration.before_successful_strategy_response).to receive(:call).with(subject).once + expect(Doorkeeper.configuration.after_successful_strategy_response).to receive(:call).with(subject, instance_of(Doorkeeper::OAuth::TokenResponse)).once + subject.authorize + end + + it 'requires the refresh token' do + subject.refresh_token = nil + subject.validate + expect(subject.error).to eq(:invalid_request) + end + + it 'requires credentials to be valid if provided' do + subject.client = nil + subject.validate + expect(subject.error).to eq(:invalid_client) + end + + it "requires the token's client and current client to match" do + subject.client = FactoryBot.create(:application) + subject.validate + expect(subject.error).to eq(:invalid_grant) + end + + it 'rejects revoked tokens' do + refresh_token.revoke + subject.validate + expect(subject.error).to eq(:invalid_grant) + end + + it 'accepts expired tokens' do + refresh_token.expires_in = -1 + refresh_token.save + subject.validate + expect(subject).to be_valid + end + + context 'refresh tokens expire on access token use' do + let(:server) do + double :server, + access_token_expires_in: 2.minutes, + custom_access_token_expires_in: ->(_oauth_client) { 1234 } + end + + before do + allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(true) + end + + it 'issues a new token for the client' do + expect { subject.authorize }.to change { client.reload.access_tokens.count }.by(1) + end + + it 'does not revoke the previous token' do + subject.authorize + expect(refresh_token).not_to be_revoked + end + + it 'sets the previous refresh token in the new access token' do + subject.authorize + expect( + # #sort_by used for MongoDB ORM extensions for valid ordering + client.access_tokens.sort_by(&:created_at).last.previous_refresh_token + ).to eq(refresh_token.refresh_token) + end + end + + context 'clientless access tokens' do + let!(:refresh_token) { FactoryBot.create(:clientless_access_token, use_refresh_token: true) } + + subject { RefreshTokenRequest.new server, refresh_token, nil } + + it 'issues a new token without a client' do + expect { subject.authorize }.to change { Doorkeeper::AccessToken.count }.by(1) + end + end + + context 'with scopes' do + let(:refresh_token) do + FactoryBot.create :access_token, + use_refresh_token: true, + scopes: 'public write' + end + let(:parameters) { {} } + subject { RefreshTokenRequest.new server, refresh_token, credentials, parameters } + + it 'transfers scopes from the old token to the new token' do + subject.authorize + expect(Doorkeeper::AccessToken.last.scopes).to eq([:public, :write]) + end + + it 'reduces scopes to the provided scopes' do + parameters[:scopes] = 'public' + subject.authorize + expect(Doorkeeper::AccessToken.last.scopes).to eq([:public]) + end + + it 'validates that scopes are included in the original access token' do + parameters[:scopes] = 'public update' + + subject.validate + expect(subject.error).to eq(:invalid_scope) + end + + it 'uses params[:scope] in favor of scopes if present (valid)' do + parameters[:scopes] = 'public update' + parameters[:scope] = 'public' + subject.authorize + expect(Doorkeeper::AccessToken.last.scopes).to eq([:public]) + end + + it 'uses params[:scope] in favor of scopes if present (invalid)' do + parameters[:scopes] = 'public' + parameters[:scope] = 'public update' + + subject.validate + expect(subject.error).to eq(:invalid_scope) + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/scopes_spec.rb b/doorkeeper/spec/lib/oauth/scopes_spec.rb new file mode 100644 index 0000000000..6ea25a86a7 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/scopes_spec.rb @@ -0,0 +1,149 @@ +require 'spec_helper' +require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/string' +require 'doorkeeper/oauth/scopes' + +module Doorkeeper::OAuth + describe Scopes do + describe '#add' do + it 'allows you to add scopes with symbols' do + subject.add :public + expect(subject.all).to eq(['public']) + end + + it 'allows you to add scopes with strings' do + subject.add 'public' + expect(subject.all).to eq(['public']) + end + + it 'do not add already included scopes' do + subject.add :public + subject.add :public + expect(subject.all).to eq(['public']) + end + end + + describe '#exists' do + before do + subject.add :public + end + + it 'returns true if scope with given name is present' do + expect(subject.exists?('public')).to be_truthy + end + + it 'returns false if scope with given name does not exist' do + expect(subject.exists?('other')).to be_falsey + end + + it 'handles symbols' do + expect(subject.exists?(:public)).to be_truthy + expect(subject.exists?(:other)).to be_falsey + end + end + + describe '.from_string' do + let(:string) { 'public write' } + + subject { Scopes.from_string(string) } + + it { expect(subject).to be_a(Scopes) } + + describe '#all' do + it 'should be an array of the expected scopes' do + scopes_array = subject.all + expect(scopes_array.size).to eq(2) + expect(scopes_array).to include('public') + expect(scopes_array).to include('write') + end + end + end + + describe '#+' do + it 'can add to another scope object' do + scopes = Scopes.from_string('public') + Scopes.from_string('admin') + expect(scopes.all).to eq(%w[public admin]) + end + + it 'does not change the existing object' do + origin = Scopes.from_string('public') + expect(origin.to_s).to eq('public') + end + + it 'can add an array to a scope object' do + scopes = Scopes.from_string('public') + ['admin'] + expect(scopes.all).to eq(%w[public admin]) + end + + it 'raises an error if cannot handle addition' do + expect do + Scopes.from_string('public') + 'admin' + end.to raise_error(NoMethodError) + end + end + + describe '#&' do + it 'can get intersection with another scope object' do + scopes = Scopes.from_string('public admin') & Scopes.from_string('write admin') + expect(scopes.all).to eq(%w[admin]) + end + + it 'does not change the existing object' do + origin = Scopes.from_string('public admin') + origin & Scopes.from_string('write admin') + expect(origin.to_s).to eq('public admin') + end + + it 'can get intersection with an array' do + scopes = Scopes.from_string('public admin') & %w[write admin] + expect(scopes.all).to eq(%w[admin]) + end + end + + describe '#==' do + it 'is equal to another set of scopes' do + expect(Scopes.from_string('public')).to eq(Scopes.from_string('public')) + end + + it 'is equal to another set of scopes with no particular order' do + expect(Scopes.from_string('public write')).to eq(Scopes.from_string('write public')) + end + + it 'differs from another set of scopes when scopes are not the same' do + expect(Scopes.from_string('public write')).not_to eq(Scopes.from_string('write')) + end + + it "does not raise an error when compared to a non-enumerable object" do + expect { Scopes.from_string("public") == false }.not_to raise_error + end + end + + describe '#has_scopes?' do + subject { Scopes.from_string('public admin') } + + it 'returns true when at least one scope is included' do + expect(subject.has_scopes?(Scopes.from_string('public'))).to be_truthy + end + + it 'returns true when all scopes are included' do + expect(subject.has_scopes?(Scopes.from_string('public admin'))).to be_truthy + end + + it 'is true if all scopes are included in any order' do + expect(subject.has_scopes?(Scopes.from_string('admin public'))).to be_truthy + end + + it 'is false if no scopes are included' do + expect(subject.has_scopes?(Scopes.from_string('notexistent'))).to be_falsey + end + + it 'returns false when any scope is not included' do + expect(subject.has_scopes?(Scopes.from_string('public nope'))).to be_falsey + end + + it 'is false if no scopes are included even for existing ones' do + expect(subject.has_scopes?(Scopes.from_string('public admin notexistent'))).to be_falsey + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/token_request_spec.rb b/doorkeeper/spec/lib/oauth/token_request_spec.rb new file mode 100644 index 0000000000..bf1a48e33a --- /dev/null +++ b/doorkeeper/spec/lib/oauth/token_request_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper_integration' + +module Doorkeeper::OAuth + describe TokenRequest do + let :application do + scopes = double(all: ['public']) + double(:application, id: 9990, scopes: scopes) + end + + let :pre_auth do + double( + :pre_auth, + client: application, + redirect_uri: 'http://tst.com/cb', + state: nil, + scopes: Scopes.from_string('public'), + error: nil, + authorizable?: true + ) + end + + let :owner do + double :owner, id: 7866 + end + + subject do + TokenRequest.new(pre_auth, owner) + end + + it 'creates an access token' do + expect do + subject.authorize + end.to change { Doorkeeper::AccessToken.count }.by(1) + end + + it 'returns a code response' do + expect(subject.authorize).to be_a(CodeResponse) + end + + it 'does not create token when not authorizable' do + allow(pre_auth).to receive(:authorizable?).and_return(false) + expect { subject.authorize }.not_to change { Doorkeeper::AccessToken.count } + end + + it 'returns a error response' do + allow(pre_auth).to receive(:authorizable?).and_return(false) + expect(subject.authorize).to be_a(ErrorResponse) + end + + context 'with custom expirations' do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + custom_access_token_expires_in do |_oauth_client| + 1234 + end + end + end + + it 'should use the custom ttl' do + subject.authorize + token = Doorkeeper::AccessToken.first + expect(token.expires_in).to eq(1234) + end + end + + context 'token reuse' do + it 'creates a new token if there are no matching tokens' do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) + expect do + subject.authorize + end.to change { Doorkeeper::AccessToken.count }.by(1) + end + + it 'creates a new token if scopes do not match' do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) + FactoryBot.create(:access_token, application_id: pre_auth.client.id, + resource_owner_id: owner.id, scopes: '') + expect do + subject.authorize + end.to change { Doorkeeper::AccessToken.count }.by(1) + end + + it 'skips token creation if there is a matching one' do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) + allow(application.scopes).to receive(:has_scopes?).and_return(true) + allow(application.scopes).to receive(:all?).and_return(true) + + FactoryBot.create(:access_token, application_id: pre_auth.client.id, + resource_owner_id: owner.id, scopes: 'public') + + expect { subject.authorize }.not_to change { Doorkeeper::AccessToken.count } + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/token_response_spec.rb b/doorkeeper/spec/lib/oauth/token_response_spec.rb new file mode 100644 index 0000000000..f117d22b92 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/token_response_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' +require 'doorkeeper/oauth/token_response' + +module Doorkeeper::OAuth + describe TokenResponse do + subject { TokenResponse.new(double.as_null_object) } + + it 'includes access token response headers' do + headers = subject.headers + expect(headers.fetch('Cache-Control')).to eq('no-store') + expect(headers.fetch('Pragma')).to eq('no-cache') + end + + it 'status is ok' do + expect(subject.status).to eq(:ok) + end + + describe '.body' do + let(:access_token) do + double :access_token, + token: 'some-token', + expires_in: '3600', + expires_in_seconds: '300', + scopes_string: 'two scopes', + refresh_token: 'some-refresh-token', + token_type: 'bearer', + created_at: 0 + end + + subject { TokenResponse.new(access_token).body } + + it 'includes :access_token' do + expect(subject['access_token']).to eq('some-token') + end + + it 'includes :token_type' do + expect(subject['token_type']).to eq('bearer') + end + + # expires_in_seconds is returned as `expires_in` in order to match + # the OAuth spec (section 4.2.2) + it 'includes :expires_in' do + expect(subject['expires_in']).to eq('300') + end + + it 'includes :scope' do + expect(subject['scope']).to eq('two scopes') + end + + it 'includes :refresh_token' do + expect(subject['refresh_token']).to eq('some-refresh-token') + end + + it 'includes :created_at' do + expect(subject['created_at']).to eq(0) + end + end + + describe '.body filters out empty values' do + let(:access_token) do + double :access_token, + token: 'some-token', + expires_in_seconds: '', + scopes_string: '', + refresh_token: '', + token_type: 'bearer', + created_at: 0 + end + + subject { TokenResponse.new(access_token).body } + + it 'includes :expires_in' do + expect(subject['expires_in']).to be_nil + end + + it 'includes :scope' do + expect(subject['scope']).to be_nil + end + + it 'includes :refresh_token' do + expect(subject['refresh_token']).to be_nil + end + end + end +end diff --git a/doorkeeper/spec/lib/oauth/token_spec.rb b/doorkeeper/spec/lib/oauth/token_spec.rb new file mode 100644 index 0000000000..70300aa849 --- /dev/null +++ b/doorkeeper/spec/lib/oauth/token_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' +require 'active_support/core_ext/string' +require 'doorkeeper/oauth/token' + +module Doorkeeper + unless defined?(AccessToken) + class AccessToken + end + end + + module OAuth + describe Token do + describe :from_request do + let(:request) { double.as_null_object } + + let(:method) do + ->(request) { return 'token-value' } + end + + it 'accepts anything that responds to #call' do + expect(method).to receive(:call).with(request) + Token.from_request request, method + end + + it 'delegates methods received as symbols to Token class' do + expect(Token).to receive(:from_params).with(request) + Token.from_request request, :from_params + end + + it 'stops at the first credentials found' do + not_called_method = double + expect(not_called_method).not_to receive(:call) + Token.from_request request, ->(_r) {}, method, not_called_method + end + + it 'returns the credential from extractor method' do + credentials = Token.from_request request, method + expect(credentials).to eq('token-value') + end + end + + describe :from_access_token_param do + it 'returns token from access_token parameter' do + request = double parameters: { access_token: 'some-token' } + token = Token.from_access_token_param(request) + expect(token).to eq('some-token') + end + end + + describe :from_bearer_param do + it 'returns token from bearer_token parameter' do + request = double parameters: { bearer_token: 'some-token' } + token = Token.from_bearer_param(request) + expect(token).to eq('some-token') + end + end + + describe :from_bearer_authorization do + it 'returns token from capitalized authorization bearer' do + request = double authorization: 'Bearer SomeToken' + token = Token.from_bearer_authorization(request) + expect(token).to eq('SomeToken') + end + + it 'returns token from lowercased authorization bearer' do + request = double authorization: 'bearer SomeToken' + token = Token.from_bearer_authorization(request) + expect(token).to eq('SomeToken') + end + + it 'does not return token if authorization is not bearer' do + request = double authorization: 'MAC SomeToken' + token = Token.from_bearer_authorization(request) + expect(token).to be_blank + end + end + + describe :from_basic_authorization do + it 'returns token from capitalized authorization basic' do + request = double authorization: "Basic #{Base64.encode64 'SomeToken:'}" + token = Token.from_basic_authorization(request) + expect(token).to eq('SomeToken') + end + + it 'returns token from lowercased authorization basic' do + request = double authorization: "basic #{Base64.encode64 'SomeToken:'}" + token = Token.from_basic_authorization(request) + expect(token).to eq('SomeToken') + end + + it 'does not return token if authorization is not basic' do + request = double authorization: "MAC #{Base64.encode64 'SomeToken:'}" + token = Token.from_basic_authorization(request) + expect(token).to be_blank + end + end + + describe :authenticate do + it 'calls the finder if token was returned' do + token = ->(_r) { 'token' } + expect(AccessToken).to receive(:by_token).with('token') + Token.authenticate double, token + end + + it 'revokes previous refresh_token if token was found' do + token = ->(_r) { 'token' } + expect( + AccessToken + ).to receive(:by_token).with('token').and_return(token) + expect(token).to receive(:revoke_previous_refresh_token!) + Token.authenticate double, token + end + end + end + end +end diff --git a/doorkeeper/spec/lib/request/strategy_spec.rb b/doorkeeper/spec/lib/request/strategy_spec.rb new file mode 100644 index 0000000000..e0fdcfec5a --- /dev/null +++ b/doorkeeper/spec/lib/request/strategy_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' +require 'doorkeeper/request/strategy' + +module Doorkeeper + module Request + describe Strategy do + let(:server) { double } + subject(:strategy) { Strategy.new(server) } + + describe :initialize do + it "sets the server attribute" do + expect(strategy.server).to eq server + end + end + + describe :request do + it "requires an implementation" do + expect { strategy.request }.to raise_exception NotImplementedError + end + end + + describe "a sample Strategy subclass" do + let(:fake_request) { double } + + let(:strategy_class) do + subclass = Class.new(Strategy) do + class << self + attr_accessor :fake_request + end + + def request + self.class.fake_request + end + end + + subclass.fake_request = fake_request + subclass + end + + subject(:strategy) { strategy_class.new(server) } + + it "provides a request implementation" do + expect(strategy.request).to eq fake_request + end + + it "authorizes the request" do + expect(fake_request).to receive :authorize + strategy.authorize + end + end + end + end +end diff --git a/doorkeeper/spec/lib/server_spec.rb b/doorkeeper/spec/lib/server_spec.rb new file mode 100644 index 0000000000..d57b6fd1f0 --- /dev/null +++ b/doorkeeper/spec/lib/server_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Doorkeeper::Server do + let(:fake_class) { double :fake_class } + + subject do + described_class.new + end + + describe '.authorization_request' do + it 'raises error when strategy does not exist' do + expect do + subject.authorization_request(:duh) + end.to raise_error(Doorkeeper::Errors::InvalidAuthorizationStrategy) + end + + it 'raises error when strategy does not match phase' do + expect do + subject.token_request(:code) + end.to raise_error(Doorkeeper::Errors::InvalidTokenStrategy) + end + + context 'when only Authorization Code strategy is enabled' do + before do + allow(Doorkeeper.configuration). + to receive(:grant_flows). + and_return(['authorization_code']) + end + + it 'raises error when using the disabled Implicit strategy' do + expect do + subject.authorization_request(:token) + end.to raise_error(Doorkeeper::Errors::InvalidAuthorizationStrategy) + end + + it 'raises error when using the disabled Client Credentials strategy' do + expect do + subject.token_request(:client_credentials) + end.to raise_error(Doorkeeper::Errors::InvalidTokenStrategy) + end + end + + it 'builds the request with selected strategy' do + stub_const 'Doorkeeper::Request::Code', fake_class + expect(fake_class).to receive(:new).with(subject) + subject.authorization_request :code + end + + it 'builds the request with composit strategy name' do + allow(Doorkeeper.configuration). + to receive(:authorization_response_types). + and_return(['id_token token']) + + stub_const 'Doorkeeper::Request::IdTokenToken', fake_class + expect(fake_class).to receive(:new).with(subject) + subject.authorization_request 'id_token token' + end + end +end diff --git a/doorkeeper/spec/models/doorkeeper/access_grant_spec.rb b/doorkeeper/spec/models/doorkeeper/access_grant_spec.rb new file mode 100644 index 0000000000..58b2316f15 --- /dev/null +++ b/doorkeeper/spec/models/doorkeeper/access_grant_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper_integration' + +describe Doorkeeper::AccessGrant do + subject { FactoryBot.build(:access_grant) } + + it { expect(subject).to be_valid } + + it_behaves_like 'an accessible token' + it_behaves_like 'a revocable token' + it_behaves_like 'a unique token' do + let(:factory_name) { :access_grant } + end + + describe 'validations' do + it 'is invalid without resource_owner_id' do + subject.resource_owner_id = nil + expect(subject).not_to be_valid + end + + it 'is invalid without application_id' do + subject.application_id = nil + expect(subject).not_to be_valid + end + + it 'is invalid without token' do + subject.save + subject.token = nil + expect(subject).not_to be_valid + end + + it 'is invalid without expires_in' do + subject.expires_in = nil + expect(subject).not_to be_valid + end + end +end diff --git a/doorkeeper/spec/models/doorkeeper/access_token_spec.rb b/doorkeeper/spec/models/doorkeeper/access_token_spec.rb new file mode 100644 index 0000000000..3b226a6e0c --- /dev/null +++ b/doorkeeper/spec/models/doorkeeper/access_token_spec.rb @@ -0,0 +1,418 @@ +require 'spec_helper_integration' + +module Doorkeeper + describe AccessToken do + subject { FactoryBot.build(:access_token) } + + it { expect(subject).to be_valid } + + it_behaves_like 'an accessible token' + it_behaves_like 'a revocable token' + it_behaves_like 'a unique token' do + let(:factory_name) { :access_token } + end + + module CustomGeneratorArgs + def self.generate + end + end + + describe :generate_token do + it 'generates a token using the default method' do + FactoryBot.create :access_token + + token = FactoryBot.create :access_token + expect(token.token).to be_a(String) + end + + it 'generates a token using a custom object' do + eigenclass = class << CustomGeneratorArgs; self; end + eigenclass.class_eval do + remove_method :generate + end + module CustomGeneratorArgs + def self.generate(opts = {}) + "custom_generator_token_#{opts[:resource_owner_id]}" + end + end + + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_generator "Doorkeeper::CustomGeneratorArgs" + end + + token = FactoryBot.create :access_token + expect(token.token).to match(%r{custom_generator_token_\d+}) + end + + it 'allows the custom generator to access the application details' do + eigenclass = class << CustomGeneratorArgs; self; end + eigenclass.class_eval do + remove_method :generate + end + module CustomGeneratorArgs + def self.generate(opts = {}) + "custom_generator_token_#{opts[:application].name}" + end + end + + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_generator "Doorkeeper::CustomGeneratorArgs" + end + + token = FactoryBot.create :access_token + expect(token.token).to match(%r{custom_generator_token_Application \d+}) + end + + it 'allows the custom generator to access the scopes' do + eigenclass = class << CustomGeneratorArgs; self; end + eigenclass.class_eval do + remove_method :generate + end + module CustomGeneratorArgs + def self.generate(opts = {}) + "custom_generator_token_#{opts[:scopes].count}_#{opts[:scopes]}" + end + end + + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_generator "Doorkeeper::CustomGeneratorArgs" + end + + token = FactoryBot.create :access_token, scopes: 'public write' + + expect(token.token).to eq 'custom_generator_token_2_public write' + end + + it 'allows the custom generator to access the expiry length' do + eigenclass = class << CustomGeneratorArgs; self; end + eigenclass.class_eval do + remove_method :generate + end + module CustomGeneratorArgs + def self.generate(opts = {}) + "custom_generator_token_#{opts[:expires_in]}" + end + end + + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_generator "Doorkeeper::CustomGeneratorArgs" + end + + token = FactoryBot.create :access_token + expect(token.token).to eq 'custom_generator_token_7200' + end + + it 'allows the custom generator to access the created time' do + module CustomGeneratorArgs + def self.generate(opts = {}) + "custom_generator_token_#{opts[:created_at].to_i}" + end + end + + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_generator "Doorkeeper::CustomGeneratorArgs" + end + + token = FactoryBot.create :access_token + created_at = token.created_at + expect(token.token).to eq "custom_generator_token_#{created_at.to_i}" + end + + it 'raises an error if the custom object does not support generate' do + module NoGenerate + end + + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_generator "Doorkeeper::NoGenerate" + end + + expect { FactoryBot.create :access_token }.to( + raise_error(Doorkeeper::Errors::UnableToGenerateToken) + ) + end + + it 'raises original error if something went wrong in custom generator' do + eigenclass = class << CustomGeneratorArgs; self; end + eigenclass.class_eval do + remove_method :generate + end + + module CustomGeneratorArgs + def self.generate(opts = {}) + raise LoadError, 'custom behaviour' + end + end + + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_generator "Doorkeeper::CustomGeneratorArgs" + end + + expect { FactoryBot.create :access_token }.to( + raise_error(LoadError) + ) + end + + it 'raises an error if the custom object does not exist' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + access_token_generator "Doorkeeper::NotReal" + end + + expect { FactoryBot.create :access_token }.to( + raise_error(Doorkeeper::Errors::TokenGeneratorNotFound, /NotReal/) + ) + end + end + + describe :refresh_token do + it 'has empty refresh token if it was not required' do + token = FactoryBot.create :access_token + expect(token.refresh_token).to be_nil + end + + it 'generates a refresh token if it was requested' do + token = FactoryBot.create :access_token, use_refresh_token: true + expect(token.refresh_token).not_to be_nil + end + + it 'is not valid if token exists' do + token1 = FactoryBot.create :access_token, use_refresh_token: true + token2 = FactoryBot.create :access_token, use_refresh_token: true + token2.refresh_token = token1.refresh_token + expect(token2).not_to be_valid + end + + it 'expects database to raise an error if refresh tokens are the same' do + token1 = FactoryBot.create :access_token, use_refresh_token: true + token2 = FactoryBot.create :access_token, use_refresh_token: true + expect do + token2.refresh_token = token1.refresh_token + token2.save(validate: false) + end.to raise_error(uniqueness_error) + end + end + + describe 'validations' do + it 'is valid without resource_owner_id' do + # For client credentials flow + subject.resource_owner_id = nil + expect(subject).to be_valid + end + + it 'is valid without application_id' do + # For resource owner credentials flow + subject.application_id = nil + expect(subject).to be_valid + end + end + + describe '#same_credential?' do + + context 'with default parameters' do + + let(:resource_owner_id) { 100 } + let(:application) { FactoryBot.create :application } + let(:default_attributes) do + { application: application, resource_owner_id: resource_owner_id } + end + let(:access_token1) { FactoryBot.create :access_token, default_attributes } + + context 'the second token has the same owner and same app' do + let(:access_token2) { FactoryBot.create :access_token, default_attributes } + it 'success' do + expect(access_token1.same_credential?(access_token2)).to be_truthy + end + end + + context 'the second token has same owner and different app' do + let(:other_application) { FactoryBot.create :application } + let(:access_token2) { FactoryBot.create :access_token, application: other_application, resource_owner_id: resource_owner_id } + + it 'fail' do + expect(access_token1.same_credential?(access_token2)).to be_falsey + end + end + + context 'the second token has different owner and different app' do + + let(:other_application) { FactoryBot.create :application } + let(:access_token2) { FactoryBot.create :access_token, application: other_application, resource_owner_id: 42 } + + it 'fail' do + expect(access_token1.same_credential?(access_token2)).to be_falsey + end + end + + context 'the second token has different owner and same app' do + let(:access_token2) { FactoryBot.create :access_token, application: application, resource_owner_id: 42 } + + it 'fail' do + expect(access_token1.same_credential?(access_token2)).to be_falsey + end + end + end + end + + describe '#acceptable?' do + context 'a token that is not accessible' do + let(:token) { FactoryBot.create(:access_token, created_at: 6.hours.ago) } + + it 'should return false' do + expect(token.acceptable?(nil)).to be false + end + end + + context 'a token that has the incorrect scopes' do + let(:token) { FactoryBot.create(:access_token) } + + it 'should return false' do + expect(token.acceptable?(['public'])).to be false + end + end + + context 'a token is acceptable with the correct scopes' do + let(:token) do + token = FactoryBot.create(:access_token) + token[:scopes] = 'public' + token + end + + it 'should return true' do + expect(token.acceptable?(['public'])).to be true + end + end + end + + describe '.revoke_all_for' do + let(:resource_owner) { double(id: 100) } + let(:application) { FactoryBot.create :application } + let(:default_attributes) do + { application: application, resource_owner_id: resource_owner.id } + end + + it 'revokes all tokens for given application and resource owner' do + FactoryBot.create :access_token, default_attributes + AccessToken.revoke_all_for application.id, resource_owner + AccessToken.all.each do |token| + expect(token).to be_revoked + end + end + + it 'matches application' do + FactoryBot.create :access_token, default_attributes.merge(application: FactoryBot.create(:application)) + AccessToken.revoke_all_for application.id, resource_owner + expect(AccessToken.all).not_to be_empty + end + + it 'matches resource owner' do + FactoryBot.create :access_token, default_attributes.merge(resource_owner_id: 90) + AccessToken.revoke_all_for application.id, resource_owner + expect(AccessToken.all).not_to be_empty + end + end + + describe '.matching_token_for' do + let(:resource_owner_id) { 100 } + let(:application) { FactoryBot.create :application } + let(:scopes) { Doorkeeper::OAuth::Scopes.from_string('public write') } + let(:default_attributes) do + { + application: application, + resource_owner_id: resource_owner_id, + scopes: scopes.to_s + } + end + + it 'returns only one token' do + token = FactoryBot.create :access_token, default_attributes + last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) + expect(last_token).to eq(token) + end + + it 'accepts resource owner as object' do + resource_owner = double(to_key: true, id: 100) + token = FactoryBot.create :access_token, default_attributes + last_token = AccessToken.matching_token_for(application, resource_owner, scopes) + expect(last_token).to eq(token) + end + + it 'accepts nil as resource owner' do + token = FactoryBot.create :access_token, default_attributes.merge(resource_owner_id: nil) + last_token = AccessToken.matching_token_for(application, nil, scopes) + expect(last_token).to eq(token) + end + + it 'excludes revoked tokens' do + FactoryBot.create :access_token, default_attributes.merge(revoked_at: 1.day.ago) + last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) + expect(last_token).to be_nil + end + + it 'matches the application' do + FactoryBot.create :access_token, default_attributes.merge(application: FactoryBot.create(:application)) + last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) + expect(last_token).to be_nil + end + + it 'matches the resource owner' do + FactoryBot.create :access_token, default_attributes.merge(resource_owner_id: 2) + last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) + expect(last_token).to be_nil + end + + it 'matches token with fewer scopes' do + FactoryBot.create :access_token, default_attributes.merge(scopes: 'public') + last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) + expect(last_token).to be_nil + end + + it 'matches token with different scopes' do + FactoryBot.create :access_token, default_attributes.merge(scopes: 'public email') + last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) + expect(last_token).to be_nil + end + + it 'matches token with more scopes' do + FactoryBot.create :access_token, default_attributes.merge(scopes: 'public write email') + last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) + expect(last_token).to be_nil + end + + it 'matches application scopes' do + application = FactoryBot.create :application, scopes: "private read" + FactoryBot.create :access_token, default_attributes.merge( + application: application + ) + last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) + expect(last_token).to be_nil + end + + it 'returns the last created token' do + FactoryBot.create :access_token, default_attributes.merge(created_at: 1.day.ago) + token = FactoryBot.create :access_token, default_attributes + last_token = AccessToken.matching_token_for(application, resource_owner_id, scopes) + expect(last_token).to eq(token) + end + + it 'returns as_json hash' do + token = FactoryBot.create :access_token, default_attributes + token_hash = { + resource_owner_id: token.resource_owner_id, + scopes: token.scopes, + expires_in_seconds: token.expires_in_seconds, + application: { uid: token.application.uid }, + created_at: token.created_at.to_i, + } + expect(token.as_json).to eq token_hash + end + end + + end +end diff --git a/doorkeeper/spec/models/doorkeeper/application_spec.rb b/doorkeeper/spec/models/doorkeeper/application_spec.rb new file mode 100644 index 0000000000..3ce38437db --- /dev/null +++ b/doorkeeper/spec/models/doorkeeper/application_spec.rb @@ -0,0 +1,212 @@ +require 'spec_helper_integration' + +module Doorkeeper + describe Application do + let(:require_owner) { Doorkeeper.configuration.instance_variable_set('@confirm_application_owner', true) } + let(:unset_require_owner) { Doorkeeper.configuration.instance_variable_set('@confirm_application_owner', false) } + let(:new_application) { FactoryBot.build(:application) } + + let(:uid) { SecureRandom.hex(8) } + let(:secret) { SecureRandom.hex(8) } + + context 'application_owner is enabled' do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + enable_application_owner + end + end + + context 'application owner is not required' do + before(:each) do + unset_require_owner + end + + it 'is valid given valid attributes' do + expect(new_application).to be_valid + end + end + + context 'application owner is required' do + before(:each) do + require_owner + @owner = FactoryBot.build_stubbed(:doorkeeper_testing_user) + end + + it 'is invalid without an owner' do + expect(new_application).not_to be_valid + end + + it 'is valid with an owner' do + new_application.owner = @owner + expect(new_application).to be_valid + end + end + end + + it 'is invalid without a name' do + new_application.name = nil + expect(new_application).not_to be_valid + end + + it 'generates uid on create' do + expect(new_application.uid).to be_nil + new_application.save + expect(new_application.uid).not_to be_nil + end + + it 'generates uid on create if an empty string' do + new_application.uid = '' + new_application.save + expect(new_application.uid).not_to be_blank + end + + it 'generates uid on create unless one is set' do + new_application.uid = uid + new_application.save + expect(new_application.uid).to eq(uid) + end + + it 'is invalid without uid' do + new_application.save + new_application.uid = nil + expect(new_application).not_to be_valid + end + + it 'is invalid without redirect_uri' do + new_application.save + new_application.redirect_uri = nil + expect(new_application).not_to be_valid + end + + it 'checks uniqueness of uid' do + app1 = FactoryBot.create(:application) + app2 = FactoryBot.create(:application) + app2.uid = app1.uid + expect(app2).not_to be_valid + end + + it 'expects database to throw an error when uids are the same' do + app1 = FactoryBot.create(:application) + app2 = FactoryBot.create(:application) + app2.uid = app1.uid + expect { app2.save!(validate: false) }.to raise_error(uniqueness_error) + end + + it 'generate secret on create' do + expect(new_application.secret).to be_nil + new_application.save + expect(new_application.secret).not_to be_nil + end + + it 'generate secret on create if is blank string' do + new_application.secret = '' + new_application.save + expect(new_application.secret).not_to be_blank + end + + it 'generate secret on create unless one is set' do + new_application.secret = secret + new_application.save + expect(new_application.secret).to eq(secret) + end + + it 'is invalid without secret' do + new_application.save + new_application.secret = nil + expect(new_application).not_to be_valid + end + + describe 'destroy related models on cascade' do + before(:each) do + new_application.save + end + + it 'should destroy its access grants' do + FactoryBot.create(:access_grant, application: new_application) + expect { new_application.destroy }.to change { Doorkeeper::AccessGrant.count }.by(-1) + end + + it 'should destroy its access tokens' do + FactoryBot.create(:access_token, application: new_application) + FactoryBot.create(:access_token, application: new_application, revoked_at: Time.now.utc) + expect do + new_application.destroy + end.to change { Doorkeeper::AccessToken.count }.by(-2) + end + end + + describe :ordered_by do + let(:applications) { FactoryBot.create_list(:application, 5) } + + context 'when a direction is not specified' do + it 'calls order with a default order of asc' do + names = applications.map(&:name).sort + expect(Application.ordered_by(:name).map(&:name)).to eq(names) + end + end + + context 'when a direction is specified' do + it 'calls order with specified direction' do + names = applications.map(&:name).sort.reverse + expect(Application.ordered_by(:name, :desc).map(&:name)).to eq(names) + end + end + end + + describe "#redirect_uri=" do + context "when array of valid redirect_uris" do + it "should join by newline" do + new_application.redirect_uri = ['http://localhost/callback1', 'http://localhost/callback2'] + expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2") + end + end + context "when string of valid redirect_uris" do + it "should store as-is" do + new_application.redirect_uri = "http://localhost/callback1\nhttp://localhost/callback2" + expect(new_application.redirect_uri).to eq("http://localhost/callback1\nhttp://localhost/callback2") + end + end + end + + describe :authorized_for do + let(:resource_owner) { double(:resource_owner, id: 10) } + + it 'is empty if the application is not authorized for anyone' do + expect(Application.authorized_for(resource_owner)).to be_empty + end + + it 'returns only application for a specific resource owner' do + FactoryBot.create(:access_token, resource_owner_id: resource_owner.id + 1) + token = FactoryBot.create(:access_token, resource_owner_id: resource_owner.id) + expect(Application.authorized_for(resource_owner)).to eq([token.application]) + end + + it 'excludes revoked tokens' do + FactoryBot.create(:access_token, resource_owner_id: resource_owner.id, revoked_at: 2.days.ago) + expect(Application.authorized_for(resource_owner)).to be_empty + end + + it 'returns all applications that have been authorized' do + token1 = FactoryBot.create(:access_token, resource_owner_id: resource_owner.id) + token2 = FactoryBot.create(:access_token, resource_owner_id: resource_owner.id) + expect(Application.authorized_for(resource_owner)).to eq([token1.application, token2.application]) + end + + it 'returns only one application even if it has been authorized twice' do + application = FactoryBot.create(:application) + FactoryBot.create(:access_token, resource_owner_id: resource_owner.id, application: application) + FactoryBot.create(:access_token, resource_owner_id: resource_owner.id, application: application) + expect(Application.authorized_for(resource_owner)).to eq([application]) + end + end + + describe :authenticate do + it 'finds the application via uid/secret' do + app = FactoryBot.create :application + authenticated = Application.by_uid_and_secret(app.uid, app.secret) + expect(authenticated).to eq(app) + end + end + end +end diff --git a/doorkeeper/spec/requests/applications/applications_request_spec.rb b/doorkeeper/spec/requests/applications/applications_request_spec.rb new file mode 100644 index 0000000000..59f55c2037 --- /dev/null +++ b/doorkeeper/spec/requests/applications/applications_request_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper_integration' + +feature 'Adding applications' do + context 'in application form' do + background do + visit '/oauth/applications/new' + end + + scenario 'adding a valid app' do + fill_in 'doorkeeper_application[name]', with: 'My Application' + fill_in 'doorkeeper_application[redirect_uri]', + with: 'https://example.com' + + click_button 'Submit' + i_should_see 'Application created' + i_should_see 'My Application' + end + + scenario 'adding invalid app' do + click_button 'Submit' + i_should_see 'Whoops! Check your form for possible errors' + end + end +end + +feature 'Listing applications' do + background do + FactoryBot.create :application, name: 'Oauth Dude' + FactoryBot.create :application, name: 'Awesome App' + end + + scenario 'application list' do + visit '/oauth/applications' + i_should_see 'Awesome App' + i_should_see 'Oauth Dude' + end +end + +feature 'Show application' do + given :app do + FactoryBot.create :application, name: 'Just another oauth app' + end + + scenario 'visiting application page' do + visit "/oauth/applications/#{app.id}" + i_should_see 'Just another oauth app' + end +end + +feature 'Edit application' do + let :app do + FactoryBot.create :application, name: 'OMG my app' + end + + background do + visit "/oauth/applications/#{app.id}/edit" + end + + scenario 'updating a valid app' do + fill_in 'doorkeeper_application[name]', with: 'Serious app' + click_button 'Submit' + i_should_see 'Application updated' + i_should_see 'Serious app' + i_should_not_see 'OMG my app' + end + + scenario 'updating an invalid app' do + fill_in 'doorkeeper_application[name]', with: '' + click_button 'Submit' + i_should_see 'Whoops! Check your form for possible errors' + end +end + +feature 'Remove application' do + background do + @app = FactoryBot.create :application + end + + scenario 'deleting an application from list' do + visit '/oauth/applications' + i_should_see @app.name + within(:css, "tr#application_#{@app.id}") do + click_button 'Destroy' + end + i_should_see 'Application deleted' + i_should_not_see @app.name + end + + scenario 'deleting an application from show' do + visit "/oauth/applications/#{@app.id}" + click_button 'Destroy' + i_should_see 'Application deleted' + end +end diff --git a/doorkeeper/spec/requests/applications/authorized_applications_spec.rb b/doorkeeper/spec/requests/applications/authorized_applications_spec.rb new file mode 100644 index 0000000000..25d6d9d1d0 --- /dev/null +++ b/doorkeeper/spec/requests/applications/authorized_applications_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper_integration' + +feature 'Authorized applications' do + background do + @user = User.create!(name: 'Joe', password: 'sekret') + @client = client_exists(name: 'Amazing Client App') + resource_owner_is_authenticated @user + client_is_authorized @client, @user + end + + scenario 'display user\'s authorized applications' do + visit '/oauth/authorized_applications' + i_should_see 'Amazing Client App' + end + + scenario 'do not display other user\'s authorized applications' do + client = client_exists(name: 'Another Client App') + client_is_authorized client, User.create!(name: 'Joe', password: 'sekret') + visit '/oauth/authorized_applications' + i_should_not_see 'Another Client App' + end + + scenario 'user revoke access to application' do + visit '/oauth/authorized_applications' + i_should_see 'Amazing Client App' + click_on 'Revoke' + i_should_see 'Application revoked' + i_should_not_see 'Amazing Client App' + end +end diff --git a/doorkeeper/spec/requests/endpoints/authorization_spec.rb b/doorkeeper/spec/requests/endpoints/authorization_spec.rb new file mode 100644 index 0000000000..766981a0fd --- /dev/null +++ b/doorkeeper/spec/requests/endpoints/authorization_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper_integration' + +feature 'Authorization endpoint' do + background do + config_is_set(:authenticate_resource_owner) { User.first || redirect_to('/sign_in') } + client_exists(name: 'MyApp') + end + + scenario 'requires resource owner to be authenticated' do + visit authorization_endpoint_url(client: @client) + i_should_see 'Sign in' + i_should_be_on '/' + end + + context 'with authenticated resource owner' do + background do + create_resource_owner + sign_in + end + + scenario 'displays the authorization form' do + visit authorization_endpoint_url(client: @client) + i_should_see 'Authorize MyApp to use your account?' + end + + scenario 'displays all requested scopes' do + default_scopes_exist :public + optional_scopes_exist :write + visit authorization_endpoint_url(client: @client, scope: 'public write') + i_should_see 'Access your public data' + i_should_see 'Update your data' + end + end + + context 'with a invalid request' do + background do + create_resource_owner + sign_in + end + + scenario 'displays the related error' do + visit authorization_endpoint_url(client: @client, response_type: '') + i_should_not_see 'Authorize' + i_should_see_translated_error_message :unsupported_response_type + end + + scenario "displays unsupported_response_type error when using a disabled response type" do + config_is_set(:grant_flows, ['implicit']) + visit authorization_endpoint_url(client: @client, response_type: 'code') + i_should_not_see "Authorize" + i_should_see_translated_error_message :unsupported_response_type + end + end + + context 'forgery protection enabled' do + background do + create_resource_owner + sign_in + end + + scenario 'raises exception on forged requests' do + allowing_forgery_protection do + expect { + page.driver.post authorization_endpoint_url(client_id: @client.uid, + redirect_uri: @client.redirect_uri, + response_type: 'code') + }.to raise_error(ActionController::InvalidAuthenticityToken) + end + end + end +end diff --git a/doorkeeper/spec/requests/endpoints/token_spec.rb b/doorkeeper/spec/requests/endpoints/token_spec.rb new file mode 100644 index 0000000000..be8a639ddf --- /dev/null +++ b/doorkeeper/spec/requests/endpoints/token_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper_integration' + +describe 'Token endpoint' do + before do + client_exists + authorization_code_exists application: @client, scopes: 'public' + end + + it 'respond with correct headers' do + post token_endpoint_url(code: @authorization.token, client: @client) + should_have_header 'Pragma', 'no-cache' + + # Rails 5.2 changed headers + if ::Rails::VERSION::MAJOR >= 5 && ::Rails::VERSION::MINOR >= 2 || ::Rails::VERSION::MAJOR >= 6 + should_have_header 'Cache-Control', 'private, no-store' + else + should_have_header 'Cache-Control', 'no-store' + end + + should_have_header 'Content-Type', 'application/json; charset=utf-8' + end + + it 'accepts client credentials with basic auth header' do + post token_endpoint_url( + code: @authorization.token, + redirect_uri: @client.redirect_uri + ), {}, 'HTTP_AUTHORIZATION' => basic_auth_header_for_client(@client) + + should_have_json 'access_token', Doorkeeper::AccessToken.first.token + end + + it 'returns null for expires_in when a permanent token is set' do + config_is_set(:access_token_expires_in, nil) + post token_endpoint_url(code: @authorization.token, client: @client) + should_have_json 'access_token', Doorkeeper::AccessToken.first.token + should_not_have_json 'expires_in' + end + + it 'returns unsupported_grant_type for invalid grant_type param' do + post token_endpoint_url(code: @authorization.token, client: @client, grant_type: 'nothing') + + should_not_have_json 'access_token' + should_have_json 'error', 'unsupported_grant_type' + should_have_json 'error_description', translated_error_message('unsupported_grant_type') + end + + it 'returns unsupported_grant_type for disabled grant flows' do + config_is_set(:grant_flows, ['implicit']) + post token_endpoint_url(code: @authorization.token, client: @client, grant_type: 'authorization_code') + + should_not_have_json 'access_token' + should_have_json 'error', 'unsupported_grant_type' + should_have_json 'error_description', translated_error_message('unsupported_grant_type') + end + + it 'returns unsupported_grant_type when refresh_token is not in use' do + post token_endpoint_url(code: @authorization.token, client: @client, grant_type: 'refresh_token') + + should_not_have_json 'access_token' + should_have_json 'error', 'unsupported_grant_type' + should_have_json 'error_description', translated_error_message('unsupported_grant_type') + end + + it 'returns invalid_request if grant_type is missing' do + post token_endpoint_url(code: @authorization.token, client: @client, grant_type: '') + + should_not_have_json 'access_token' + should_have_json 'error', 'invalid_request' + should_have_json 'error_description', translated_error_message('invalid_request') + end +end diff --git a/doorkeeper/spec/requests/flows/authorization_code_errors_spec.rb b/doorkeeper/spec/requests/flows/authorization_code_errors_spec.rb new file mode 100644 index 0000000000..f9d256ce6a --- /dev/null +++ b/doorkeeper/spec/requests/flows/authorization_code_errors_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper_integration' + +feature 'Authorization Code Flow Errors' do + let(:client_params) { {} } + background do + config_is_set(:authenticate_resource_owner) { User.first || redirect_to('/sign_in') } + client_exists client_params + create_resource_owner + sign_in + end + + after do + access_grant_should_not_exist + end + + context "with a client trying to xss resource owner" do + let(:client_name) { "
XSS
" } + let(:client_params) { { name: client_name } } + scenario "resource owner visit authorization endpoint" do + visit authorization_endpoint_url(client: @client) + expect(page).not_to have_css("#xss") + end + end + + context 'when access was denied' do + scenario 'redirects with error' do + visit authorization_endpoint_url(client: @client) + click_on 'Deny' + + i_should_be_on_client_callback @client + url_should_not_have_param 'code' + url_should_have_param 'error', 'access_denied' + url_should_have_param 'error_description', translated_error_message(:access_denied) + end + + scenario 'redirects with state parameter' do + visit authorization_endpoint_url(client: @client, state: 'return-this') + click_on 'Deny' + + i_should_be_on_client_callback @client + url_should_not_have_param 'code' + url_should_have_param 'state', 'return-this' + end + end +end + +describe 'Authorization Code Flow Errors', 'after authorization' do + before do + client_exists + authorization_code_exists application: @client + end + + it 'returns :invalid_grant error when posting an already revoked grant code' do + # First successful request + post token_endpoint_url(code: @authorization.token, client: @client) + + # Second attempt with same token + expect do + post token_endpoint_url(code: @authorization.token, client: @client) + end.to_not change { Doorkeeper::AccessToken.count } + + should_not_have_json 'access_token' + should_have_json 'error', 'invalid_grant' + should_have_json 'error_description', translated_error_message('invalid_grant') + end + + it 'returns :invalid_grant error for invalid grant code' do + post token_endpoint_url(code: 'invalid', client: @client) + + access_token_should_not_exist + + should_not_have_json 'access_token' + should_have_json 'error', 'invalid_grant' + should_have_json 'error_description', translated_error_message('invalid_grant') + end +end diff --git a/doorkeeper/spec/requests/flows/authorization_code_spec.rb b/doorkeeper/spec/requests/flows/authorization_code_spec.rb new file mode 100644 index 0000000000..6a63b89487 --- /dev/null +++ b/doorkeeper/spec/requests/flows/authorization_code_spec.rb @@ -0,0 +1,149 @@ +require 'spec_helper_integration' + +feature 'Authorization Code Flow' do + background do + config_is_set(:authenticate_resource_owner) { User.first || redirect_to('/sign_in') } + client_exists + create_resource_owner + sign_in + end + + scenario 'resource owner authorizes the client' do + visit authorization_endpoint_url(client: @client) + click_on 'Authorize' + + access_grant_should_exist_for(@client, @resource_owner) + + i_should_be_on_client_callback(@client) + + url_should_have_param('code', Doorkeeper::AccessGrant.first.token) + url_should_not_have_param('state') + url_should_not_have_param('error') + end + + scenario 'resource owner authorizes using test url' do + @client.redirect_uri = Doorkeeper.configuration.native_redirect_uri + @client.save! + visit authorization_endpoint_url(client: @client) + click_on 'Authorize' + + access_grant_should_exist_for(@client, @resource_owner) + + url_should_have_param('code', Doorkeeper::AccessGrant.first.token) + i_should_see 'Authorization code:' + i_should_see Doorkeeper::AccessGrant.first.token + end + + scenario 'resource owner authorizes the client with state parameter set' do + visit authorization_endpoint_url(client: @client, state: 'return-me') + click_on 'Authorize' + url_should_have_param('code', Doorkeeper::AccessGrant.first.token) + url_should_have_param('state', 'return-me') + end + + scenario 'resource owner requests an access token with authorization code' do + visit authorization_endpoint_url(client: @client) + click_on 'Authorize' + + authorization_code = Doorkeeper::AccessGrant.first.token + create_access_token authorization_code, @client + + access_token_should_exist_for(@client, @resource_owner) + + should_not_have_json 'error' + + should_have_json 'access_token', Doorkeeper::AccessToken.first.token + should_have_json 'token_type', 'bearer' + should_have_json_within 'expires_in', Doorkeeper::AccessToken.first.expires_in, 1 + end + + context 'with scopes' do + background do + default_scopes_exist :public + optional_scopes_exist :write + end + + scenario 'resource owner authorizes the client with default scopes' do + visit authorization_endpoint_url(client: @client) + click_on 'Authorize' + access_grant_should_exist_for(@client, @resource_owner) + access_grant_should_have_scopes :public + end + + scenario 'resource owner authorizes the client with required scopes' do + visit authorization_endpoint_url(client: @client, scope: 'public write') + click_on 'Authorize' + access_grant_should_have_scopes :public, :write + end + + scenario 'resource owner authorizes the client with required scopes (without defaults)' do + visit authorization_endpoint_url(client: @client, scope: 'write') + click_on 'Authorize' + access_grant_should_have_scopes :write + end + + scenario 'new access token matches required scopes' do + visit authorization_endpoint_url(client: @client, scope: 'public write') + click_on 'Authorize' + + authorization_code = Doorkeeper::AccessGrant.first.token + create_access_token authorization_code, @client + + access_token_should_exist_for(@client, @resource_owner) + access_token_should_have_scopes :public, :write + end + + scenario 'returns new token if scopes have changed' do + client_is_authorized(@client, @resource_owner, scopes: 'public write') + visit authorization_endpoint_url(client: @client, scope: 'public') + click_on 'Authorize' + + authorization_code = Doorkeeper::AccessGrant.first.token + create_access_token authorization_code, @client + + expect(Doorkeeper::AccessToken.count).to be(2) + + should_have_json 'access_token', Doorkeeper::AccessToken.last.token + end + + scenario 'resource owner authorizes the client with extra scopes' do + client_is_authorized(@client, @resource_owner, scopes: 'public') + visit authorization_endpoint_url(client: @client, scope: 'public write') + click_on 'Authorize' + + authorization_code = Doorkeeper::AccessGrant.first.token + create_access_token authorization_code, @client + + expect(Doorkeeper::AccessToken.count).to be(2) + + should_have_json 'access_token', Doorkeeper::AccessToken.last.token + access_token_should_have_scopes :public, :write + end + end +end + +describe 'Authorization Code Flow' do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + use_refresh_token + end + client_exists + end + + context 'issuing a refresh token' do + before do + authorization_code_exists application: @client + end + + it 'second of simultaneous client requests get an error for revoked acccess token' do + authorization_code = Doorkeeper::AccessGrant.first.token + allow_any_instance_of(Doorkeeper::AccessGrant).to receive(:revoked?).and_return(false, true) + + post token_endpoint_url(code: authorization_code, client: @client) + + should_not_have_json 'access_token' + should_have_json 'error', 'invalid_grant' + end + end +end diff --git a/doorkeeper/spec/requests/flows/client_credentials_spec.rb b/doorkeeper/spec/requests/flows/client_credentials_spec.rb new file mode 100644 index 0000000000..17de92b5c7 --- /dev/null +++ b/doorkeeper/spec/requests/flows/client_credentials_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper_integration' + +describe 'Client Credentials Request' do + let(:client) { FactoryBot.create :application } + + context 'a valid request' do + it 'authorizes the client and returns the token response' do + headers = authorization client.uid, client.secret + params = { grant_type: 'client_credentials' } + + post '/oauth/token', params, headers + + should_have_json 'access_token', Doorkeeper::AccessToken.first.token + should_have_json_within 'expires_in', Doorkeeper.configuration.access_token_expires_in, 1 + should_not_have_json 'scope' + should_not_have_json 'refresh_token' + + should_not_have_json 'error' + should_not_have_json 'error_description' + end + + context 'with scopes' do + before do + optional_scopes_exist :write + default_scopes_exist :public + end + + it 'adds the scope to the token an returns in the response' do + headers = authorization client.uid, client.secret + params = { grant_type: 'client_credentials', scope: 'write' } + + post '/oauth/token', params, headers + + should_have_json 'access_token', Doorkeeper::AccessToken.first.token + should_have_json 'scope', 'write' + end + + context 'that are default' do + it 'adds the scope to the token an returns in the response' do + headers = authorization client.uid, client.secret + params = { grant_type: 'client_credentials', scope: 'public' } + + post '/oauth/token', params, headers + + should_have_json 'access_token', Doorkeeper::AccessToken.first.token + should_have_json 'scope', 'public' + end + end + + context 'that are invalid' do + it 'does not authorize the client and returns the error' do + headers = authorization client.uid, client.secret + params = { grant_type: 'client_credentials', scope: 'random' } + + post '/oauth/token', params, headers + + should_have_json 'error', 'invalid_scope' + should_have_json 'error_description', translated_error_message(:invalid_scope) + should_not_have_json 'access_token' + + expect(response.status).to eq(401) + end + end + end + end + + context 'an invalid request' do + it 'does not authorize the client and returns the error' do + headers = {} + params = { grant_type: 'client_credentials' } + + post '/oauth/token', params, headers + + should_have_json 'error', 'invalid_client' + should_have_json 'error_description', translated_error_message(:invalid_client) + should_not_have_json 'access_token' + + expect(response.status).to eq(401) + end + end + + def authorization(username, password) + credentials = ActionController::HttpAuthentication::Basic.encode_credentials username, password + { 'HTTP_AUTHORIZATION' => credentials } + end +end diff --git a/doorkeeper/spec/requests/flows/implicit_grant_errors_spec.rb b/doorkeeper/spec/requests/flows/implicit_grant_errors_spec.rb new file mode 100644 index 0000000000..9d9ca31a04 --- /dev/null +++ b/doorkeeper/spec/requests/flows/implicit_grant_errors_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper_integration' + +feature 'Implicit Grant Flow Errors' do + background do + config_is_set(:authenticate_resource_owner) { User.first || redirect_to('/sign_in') } + config_is_set(:grant_flows, ["implicit"]) + client_exists + create_resource_owner + sign_in + end + + after do + access_token_should_not_exist + end + + [ + [:client_id, :invalid_client], + [:redirect_uri, :invalid_redirect_uri] + ].each do |error| + scenario "displays #{error.last} error for invalid #{error.first}" do + visit authorization_endpoint_url(client: @client, error.first => 'invalid', response_type: 'token') + i_should_not_see 'Authorize' + i_should_see_translated_error_message error.last + end + + scenario "displays #{error.last} error when #{error.first} is missing" do + visit authorization_endpoint_url(client: @client, error.first => '', response_type: 'token') + i_should_not_see 'Authorize' + i_should_see_translated_error_message error.last + end + end +end diff --git a/doorkeeper/spec/requests/flows/implicit_grant_spec.rb b/doorkeeper/spec/requests/flows/implicit_grant_spec.rb new file mode 100644 index 0000000000..ee48860287 --- /dev/null +++ b/doorkeeper/spec/requests/flows/implicit_grant_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper_integration' + +feature 'Implicit Grant Flow (feature spec)' do + background do + config_is_set(:authenticate_resource_owner) { User.first || redirect_to('/sign_in') } + config_is_set(:grant_flows, ["implicit"]) + client_exists + create_resource_owner + sign_in + end + + scenario 'resource owner authorizes the client' do + visit authorization_endpoint_url(client: @client, response_type: 'token') + click_on 'Authorize' + + access_token_should_exist_for @client, @resource_owner + + i_should_be_on_client_callback @client + end +end + +describe 'Implicit Grant Flow (request spec)' do + before do + config_is_set(:authenticate_resource_owner) { User.first || redirect_to('/sign_in') } + config_is_set(:grant_flows, ["implicit"]) + client_exists + create_resource_owner + end + + context 'token reuse' do + it 'should return a new token each request' do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(false) + + token = client_is_authorized(@client, @resource_owner) + + post "/oauth/authorize", + client_id: @client.uid, + state: '', + redirect_uri: @client.redirect_uri, + response_type: 'token', + commit: 'Authorize' + + expect(response.location).not_to include(token.token) + end + + it 'should return the same token if it is still accessible' do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) + + token = client_is_authorized(@client, @resource_owner) + + post "/oauth/authorize", + client_id: @client.uid, + state: '', + redirect_uri: @client.redirect_uri, + response_type: 'token', + commit: 'Authorize' + + expect(response.location).to include(token.token) + end + end +end diff --git a/doorkeeper/spec/requests/flows/password_spec.rb b/doorkeeper/spec/requests/flows/password_spec.rb new file mode 100644 index 0000000000..25f8e2943f --- /dev/null +++ b/doorkeeper/spec/requests/flows/password_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper_integration' + +describe 'Resource Owner Password Credentials Flow not set up' do + before do + client_exists + create_resource_owner + end + + context 'with valid user credentials' do + it 'doesn\'t issue new token' do + expect do + post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) + end.to_not change { Doorkeeper::AccessToken.count } + end + end +end + +describe 'Resource Owner Password Credentials Flow' do + before do + config_is_set(:grant_flows, ["password"]) + config_is_set(:resource_owner_from_credentials) { User.authenticate! params[:username], params[:password] } + client_exists + create_resource_owner + end + + context 'with valid user credentials' do + it 'should issue new token with confidential client' do + expect do + post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) + end.to change { Doorkeeper::AccessToken.count }.by(1) + + token = Doorkeeper::AccessToken.first + + expect(token.application_id).to eq @client.id + should_have_json 'access_token', token.token + end + + it 'should issue new token with public client (only client_id present)' do + expect do + post password_token_endpoint_url(client_id: @client.uid, resource_owner: @resource_owner) + end.to change { Doorkeeper::AccessToken.count }.by(1) + + token = Doorkeeper::AccessToken.first + + expect(token.application_id).to eq @client.id + should_have_json 'access_token', token.token + end + + it 'should issue new token without client credentials' do + expect do + post password_token_endpoint_url(resource_owner: @resource_owner) + end.to change { Doorkeeper::AccessToken.count }.by(1) + + token = Doorkeeper::AccessToken.first + + expect(token.application_id).to be_nil + should_have_json 'access_token', token.token + end + + it 'should issue a refresh token if enabled' do + config_is_set(:refresh_token_enabled, true) + + post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) + + token = Doorkeeper::AccessToken.first + + should_have_json 'refresh_token', token.refresh_token + end + + it 'should return the same token if it is still accessible' do + allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true) + + client_is_authorized(@client, @resource_owner) + + post password_token_endpoint_url(client: @client, resource_owner: @resource_owner) + + expect(Doorkeeper::AccessToken.count).to be(1) + should_have_json 'access_token', Doorkeeper::AccessToken.first.token + end + + context 'with valid, default scope' do + before do + default_scopes_exist :public + end + + it 'should issue new token' do + expect do + post password_token_endpoint_url(client: @client, resource_owner: @resource_owner, scope: 'public') + end.to change { Doorkeeper::AccessToken.count }.by(1) + + token = Doorkeeper::AccessToken.first + + expect(token.application_id).to eq @client.id + should_have_json 'access_token', token.token + should_have_json 'scope', 'public' + end + end + end + + context 'with invalid scopes' do + subject do + post password_token_endpoint_url(client: @client, + resource_owner: @resource_owner, + scope: 'random') + end + + it 'should not issue new token' do + expect { subject }.to_not(change { Doorkeeper::AccessToken.count }) + end + + it 'should return invalid_scope error' do + subject + should_have_json 'error', 'invalid_scope' + should_have_json 'error_description', translated_error_message(:invalid_scope) + should_not_have_json 'access_token' + + expect(response.status).to eq(401) + end + end + + context 'with invalid user credentials' do + it 'should not issue new token with bad password' do + expect do + post password_token_endpoint_url(client: @client, + resource_owner_username: @resource_owner.name, + resource_owner_password: 'wrongpassword') + end.to_not change { Doorkeeper::AccessToken.count } + end + + it 'should not issue new token without credentials' do + expect do + post password_token_endpoint_url(client: @client) + end.to_not change { Doorkeeper::AccessToken.count } + end + end + + context 'with invalid confidential client credentials' do + it 'should not issue new token with bad client credentials' do + expect do + post password_token_endpoint_url(client_id: @client.uid, + client_secret: 'bad_secret', + resource_owner: @resource_owner) + end.to_not change { Doorkeeper::AccessToken.count } + end + end + + context 'with invalid public client id' do + it 'should not issue new token with bad client id' do + expect do + post password_token_endpoint_url(client_id: 'bad_id', resource_owner: @resource_owner) + end.to_not change { Doorkeeper::AccessToken.count } + end + end +end diff --git a/doorkeeper/spec/requests/flows/refresh_token_spec.rb b/doorkeeper/spec/requests/flows/refresh_token_spec.rb new file mode 100644 index 0000000000..32189e6322 --- /dev/null +++ b/doorkeeper/spec/requests/flows/refresh_token_spec.rb @@ -0,0 +1,174 @@ +require 'spec_helper_integration' + +describe 'Refresh Token Flow' do + before do + Doorkeeper.configure do + orm DOORKEEPER_ORM + use_refresh_token + end + client_exists + end + + context 'issuing a refresh token' do + before do + authorization_code_exists application: @client + end + + it 'client gets the refresh token and refreshses it' do + post token_endpoint_url(code: @authorization.token, client: @client) + + token = Doorkeeper::AccessToken.first + + should_have_json 'access_token', token.token + should_have_json 'refresh_token', token.refresh_token + + expect(@authorization.reload).to be_revoked + + post refresh_token_endpoint_url(client: @client, refresh_token: token.refresh_token) + + new_token = Doorkeeper::AccessToken.last + should_have_json 'access_token', new_token.token + should_have_json 'refresh_token', new_token.refresh_token + + expect(token.token).not_to eq(new_token.token) + expect(token.refresh_token).not_to eq(new_token.refresh_token) + end + end + + context 'refreshing the token' do + before do + @token = FactoryBot.create( + :access_token, + application: @client, + resource_owner_id: 1, + use_refresh_token: true + ) + end + + context "refresh_token revoked on use" do + it 'client request a token with refresh token' do + post refresh_token_endpoint_url( + client: @client, refresh_token: @token.refresh_token + ) + should_have_json( + 'refresh_token', Doorkeeper::AccessToken.last.refresh_token + ) + expect(@token.reload).not_to be_revoked + end + + it 'client request a token with expired access token' do + @token.update_attribute :expires_in, -100 + post refresh_token_endpoint_url( + client: @client, refresh_token: @token.refresh_token + ) + should_have_json( + 'refresh_token', Doorkeeper::AccessToken.last.refresh_token + ) + expect(@token.reload).not_to be_revoked + end + end + + context "refresh_token revoked on refresh_token request" do + before do + allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false) + end + + it 'client request a token with refresh token' do + post refresh_token_endpoint_url( + client: @client, refresh_token: @token.refresh_token + ) + should_have_json( + 'refresh_token', Doorkeeper::AccessToken.last.refresh_token + ) + expect(@token.reload).to be_revoked + end + + it 'client request a token with expired access token' do + @token.update_attribute :expires_in, -100 + post refresh_token_endpoint_url( + client: @client, refresh_token: @token.refresh_token + ) + should_have_json( + 'refresh_token', Doorkeeper::AccessToken.last.refresh_token + ) + expect(@token.reload).to be_revoked + end + end + + it 'client gets an error for invalid refresh token' do + post refresh_token_endpoint_url(client: @client, refresh_token: 'invalid') + should_not_have_json 'refresh_token' + should_have_json 'error', 'invalid_grant' + end + + it 'client gets an error for revoked access token' do + @token.revoke + post refresh_token_endpoint_url(client: @client, refresh_token: @token.refresh_token) + should_not_have_json 'refresh_token' + should_have_json 'error', 'invalid_grant' + end + + it 'second of simultaneous client requests get an error for revoked access token' do + allow_any_instance_of(Doorkeeper::AccessToken).to receive(:revoked?).and_return(false, true) + post refresh_token_endpoint_url(client: @client, refresh_token: @token.refresh_token) + + should_not_have_json 'refresh_token' + should_have_json 'error', 'invalid_request' + end + end + + context 'refreshing the token with multiple sessions (devices)' do + before do + # enable password auth to simulate other devices + config_is_set(:grant_flows, ["password"]) + config_is_set(:resource_owner_from_credentials) do + User.authenticate! params[:username], params[:password] + end + create_resource_owner + _another_token = post password_token_endpoint_url( + client: @client, resource_owner: @resource_owner + ) + last_token.update_attribute :created_at, 5.seconds.ago + + @token = FactoryBot.create( + :access_token, + application: @client, + resource_owner_id: @resource_owner.id, + use_refresh_token: true + ) + @token.update_attribute :expires_in, -100 + end + + context "refresh_token revoked on use" do + it 'client request a token after creating another token with the same user' do + post refresh_token_endpoint_url( + client: @client, refresh_token: @token.refresh_token + ) + + should_have_json 'refresh_token', last_token.refresh_token + expect(@token.reload).not_to be_revoked + end + end + + context "refresh_token revoked on refresh_token request" do + before do + allow(Doorkeeper::AccessToken).to receive(:refresh_token_revoked_on_use?).and_return(false) + end + + it 'client request a token after creating another token with the same user' do + post refresh_token_endpoint_url( + client: @client, refresh_token: @token.refresh_token + ) + + should_have_json 'refresh_token', last_token.refresh_token + expect(@token.reload).to be_revoked + end + end + + def last_token + Doorkeeper::AccessToken.last_authorized_token_for( + @client.id, @resource_owner.id + ) + end + end +end diff --git a/doorkeeper/spec/requests/flows/revoke_token_spec.rb b/doorkeeper/spec/requests/flows/revoke_token_spec.rb new file mode 100644 index 0000000000..5ae62967c7 --- /dev/null +++ b/doorkeeper/spec/requests/flows/revoke_token_spec.rb @@ -0,0 +1,157 @@ +require 'spec_helper_integration' + +describe 'Revoke Token Flow' do + before do + Doorkeeper.configure { orm DOORKEEPER_ORM } + end + + context 'with default parameters' do + let(:client_application) { FactoryBot.create :application } + let(:resource_owner) { User.create!(name: 'John', password: 'sekret') } + let(:access_token) do + FactoryBot.create(:access_token, + application: client_application, + resource_owner_id: resource_owner.id, + use_refresh_token: true) + end + + context 'with authenticated, confidential OAuth 2.0 client/application' do + let(:headers) do + client_id = client_application.uid + client_secret = client_application.secret + credentials = Base64.encode64("#{client_id}:#{client_secret}") + { 'HTTP_AUTHORIZATION' => "Basic #{credentials}" } + end + + it 'should revoke the access token provided' do + post revocation_token_endpoint_url, { token: access_token.token }, headers + + access_token.reload + + expect(response).to be_successful + expect(access_token.revoked?).to be_truthy + end + + it 'should revoke the refresh token provided' do + post revocation_token_endpoint_url, { token: access_token.refresh_token }, headers + + access_token.reload + + expect(response).to be_successful + expect(access_token.revoked?).to be_truthy + end + + context 'with invalid token to revoke' do + it 'should not revoke any tokens and respond successfully' do + num_prev_revoked_tokens = Doorkeeper::AccessToken.where(revoked_at: nil).count + post revocation_token_endpoint_url, { token: 'I_AM_AN_INVALID_TOKEN' }, headers + + # The authorization server responds with HTTP status code 200 even if + # token is invalid + expect(response).to be_successful + expect(Doorkeeper::AccessToken.where(revoked_at: nil).count).to eq(num_prev_revoked_tokens) + end + end + + context 'with bad credentials and a valid token' do + let(:headers) do + client_id = client_application.uid + credentials = Base64.encode64("#{client_id}:poop") + { 'HTTP_AUTHORIZATION' => "Basic #{credentials}" } + end + it 'should not revoke any tokens and respond successfully' do + post revocation_token_endpoint_url, { token: access_token.token }, headers + + access_token.reload + + expect(response).to be_successful + expect(access_token.revoked?).to be_falsey + end + end + + context 'with no credentials and a valid token' do + it 'should not revoke any tokens and respond successfully' do + post revocation_token_endpoint_url, { token: access_token.token } + + access_token.reload + + expect(response).to be_successful + expect(access_token.revoked?).to be_falsey + end + end + + context 'with valid token for another client application' do + let(:other_client_application) { FactoryBot.create :application } + let(:headers) do + client_id = other_client_application.uid + client_secret = other_client_application.secret + credentials = Base64.encode64("#{client_id}:#{client_secret}") + { 'HTTP_AUTHORIZATION' => "Basic #{credentials}" } + end + + it 'should not revoke the token as its unauthorized' do + post revocation_token_endpoint_url, { token: access_token.token }, headers + + access_token.reload + + expect(response).to be_successful + expect(access_token.revoked?).to be_falsey + end + end + end + + context 'with public OAuth 2.0 client/application' do + let(:access_token) do + FactoryBot.create(:access_token, + application: nil, + resource_owner_id: resource_owner.id, + use_refresh_token: true) + end + + it 'should revoke the access token provided' do + post revocation_token_endpoint_url, { token: access_token.token } + + access_token.reload + + expect(response).to be_successful + expect(access_token.revoked?).to be_truthy + end + + it 'should revoke the refresh token provided' do + post revocation_token_endpoint_url, { token: access_token.refresh_token } + + access_token.reload + + expect(response).to be_successful + expect(access_token.revoked?).to be_truthy + end + + context 'with a valid token issued for a confidential client' do + let(:access_token) do + FactoryBot.create(:access_token, + application: client_application, + resource_owner_id: resource_owner.id, + use_refresh_token: true) + end + + it 'should not revoke the access token provided' do + post revocation_token_endpoint_url, { token: access_token.token } + + access_token.reload + + expect(response).to be_successful + expect(access_token.revoked?).to be_falsey + end + + it 'should not revoke the refresh token provided' do + post revocation_token_endpoint_url, { token: access_token.token } + + access_token.reload + + expect(response).to be_successful + expect(access_token.revoked?).to be_falsey + end + end + end + end +end diff --git a/doorkeeper/spec/requests/flows/skip_authorization_spec.rb b/doorkeeper/spec/requests/flows/skip_authorization_spec.rb new file mode 100644 index 0000000000..567fac323f --- /dev/null +++ b/doorkeeper/spec/requests/flows/skip_authorization_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper_integration' + +feature 'Skip authorization form' do + background do + config_is_set(:authenticate_resource_owner) { User.first || redirect_to('/sign_in') } + client_exists + default_scopes_exist :public + optional_scopes_exist :write + end + + context 'for previously authorized clients' do + background do + create_resource_owner + sign_in + end + + scenario 'skips the authorization and return a new grant code' do + client_is_authorized(@client, @resource_owner, scopes: 'public') + visit authorization_endpoint_url(client: @client) + + i_should_not_see 'Authorize' + client_should_be_authorized @client + i_should_be_on_client_callback @client + url_should_have_param 'code', Doorkeeper::AccessGrant.first.token + end + + scenario 'does not skip authorization when scopes differ (new request has fewer scopes)' do + client_is_authorized(@client, @resource_owner, scopes: 'public write') + visit authorization_endpoint_url(client: @client, scope: 'public') + i_should_see 'Authorize' + end + + scenario 'does not skip authorization when scopes differ (new request has more scopes)' do + client_is_authorized(@client, @resource_owner, scopes: 'public write') + visit authorization_endpoint_url(client: @client, scopes: 'public write email') + i_should_see 'Authorize' + end + + scenario 'creates grant with new scope when scopes differ' do + client_is_authorized(@client, @resource_owner, scopes: 'public write') + visit authorization_endpoint_url(client: @client, scope: 'public') + click_on 'Authorize' + access_grant_should_have_scopes :public + end + + scenario 'doesn not skip authorization when scopes are greater' do + client_is_authorized(@client, @resource_owner, scopes: 'public') + visit authorization_endpoint_url(client: @client, scope: 'public write') + i_should_see 'Authorize' + end + + scenario 'creates grant with new scope when scopes are greater' do + client_is_authorized(@client, @resource_owner, scopes: 'public') + visit authorization_endpoint_url(client: @client, scope: 'public write') + click_on 'Authorize' + access_grant_should_have_scopes :public, :write + end + end +end diff --git a/doorkeeper/spec/requests/protected_resources/metal_spec.rb b/doorkeeper/spec/requests/protected_resources/metal_spec.rb new file mode 100644 index 0000000000..8fe08a4e4b --- /dev/null +++ b/doorkeeper/spec/requests/protected_resources/metal_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper_integration' + +describe 'ActionController::Metal API' do + before do + @client = FactoryBot.create(:application) + @resource = User.create!(name: 'Joe', password: 'sekret') + @token = client_is_authorized(@client, @resource) + end + + it 'client requests protected resource with valid token' do + get "/metal.json?access_token=#{@token.token}" + should_have_json 'ok', true + end +end diff --git a/doorkeeper/spec/requests/protected_resources/private_api_spec.rb b/doorkeeper/spec/requests/protected_resources/private_api_spec.rb new file mode 100644 index 0000000000..7f2a8137d1 --- /dev/null +++ b/doorkeeper/spec/requests/protected_resources/private_api_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper_integration' + +feature 'Private API' do + background do + @client = FactoryBot.create(:application) + @resource = User.create!(name: 'Joe', password: 'sekret') + @token = client_is_authorized(@client, @resource) + end + + scenario 'client requests protected resource with valid token' do + with_access_token_header @token.token + visit '/full_protected_resources' + expect(page.body).to have_content('index') + end + + scenario 'client requests protected resource with disabled header authentication' do + config_is_set :access_token_methods, [:from_access_token_param] + with_access_token_header @token.token + visit '/full_protected_resources' + response_status_should_be 401 + end + + scenario 'client attempts to request protected resource with invalid token' do + with_access_token_header 'invalid' + visit '/full_protected_resources' + response_status_should_be 401 + end + + scenario 'client attempts to request protected resource with expired token' do + @token.update_attribute :expires_in, -100 # expires token + with_access_token_header @token.token + visit '/full_protected_resources' + response_status_should_be 401 + end + + scenario 'client requests protected resource with permanent token' do + @token.update_attribute :expires_in, nil # never expires + with_access_token_header @token.token + visit '/full_protected_resources' + expect(page.body).to have_content('index') + end + + scenario 'access token with no default scopes' do + Doorkeeper.configuration.instance_eval { + @default_scopes = Doorkeeper::OAuth::Scopes.from_array([:public]) + @scopes = default_scopes + optional_scopes + } + @token.update_attribute :scopes, 'dummy' + with_access_token_header @token.token + visit '/full_protected_resources' + response_status_should_be 403 + end + + scenario 'access token with no allowed scopes' do + @token.update_attribute :scopes, nil + with_access_token_header @token.token + visit '/full_protected_resources/1.json' + response_status_should_be 403 + end + + scenario 'access token with one of allowed scopes' do + @token.update_attribute :scopes, 'admin' + with_access_token_header @token.token + visit '/full_protected_resources/1.json' + expect(page.body).to have_content('show') + end + + scenario 'access token with another of allowed scopes' do + @token.update_attribute :scopes, 'write' + with_access_token_header @token.token + visit '/full_protected_resources/1.json' + expect(page.body).to have_content('show') + end + + scenario 'access token with both allowed scopes' do + @token.update_attribute :scopes, 'write admin' + with_access_token_header @token.token + visit '/full_protected_resources/1.json' + expect(page.body).to have_content('show') + end +end diff --git a/doorkeeper/spec/routing/custom_controller_routes_spec.rb b/doorkeeper/spec/routing/custom_controller_routes_spec.rb new file mode 100644 index 0000000000..298be9a6c1 --- /dev/null +++ b/doorkeeper/spec/routing/custom_controller_routes_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper_integration' + +describe 'Custom controller for routes' do + it 'GET /space/scope/authorize routes to custom authorizations controller' do + expect(get('/inner_space/scope/authorize')).to route_to('custom_authorizations#new') + end + + it 'POST /space/scope/authorize routes to custom authorizations controller' do + expect(post('/inner_space/scope/authorize')).to route_to('custom_authorizations#create') + end + + it 'DELETE /space/scope/authorize routes to custom authorizations controller' do + expect(delete('/inner_space/scope/authorize')).to route_to('custom_authorizations#destroy') + end + + it 'POST /space/scope/token routes to tokens controller' do + expect(post('/inner_space/scope/token')).to route_to('custom_authorizations#create') + end + + it 'GET /space/scope/applications routes to applications controller' do + expect(get('/inner_space/scope/applications')).to route_to('custom_authorizations#index') + end + + it 'GET /space/scope/token/info routes to the token_info controller' do + expect(get('/inner_space/scope/token/info')).to route_to('custom_authorizations#show') + end + + it 'GET /space/oauth/authorize routes to custom authorizations controller' do + expect(get('/space/oauth/authorize')).to route_to('custom_authorizations#new') + end + + it 'POST /space/oauth/authorize routes to custom authorizations controller' do + expect(post('/space/oauth/authorize')).to route_to('custom_authorizations#create') + end + + it 'DELETE /space/oauth/authorize routes to custom authorizations controller' do + expect(delete('/space/oauth/authorize')).to route_to('custom_authorizations#destroy') + end + + it 'POST /space/oauth/token routes to tokens controller' do + expect(post('/space/oauth/token')).to route_to('custom_authorizations#create') + end + + it 'POST /space/oauth/revoke routes to tokens controller' do + expect(post('/space/oauth/revoke')).to route_to('custom_authorizations#revoke') + end + + it 'POST /space/oauth/introspect routes to tokens controller' do + expect(post('/space/oauth/introspect')).to route_to('custom_authorizations#introspect') + end + + it 'GET /space/oauth/applications routes to applications controller' do + expect(get('/space/oauth/applications')).to route_to('custom_authorizations#index') + end + + it 'GET /space/oauth/token/info routes to the token_info controller' do + expect(get('/space/oauth/token/info')).to route_to('custom_authorizations#show') + end + + it 'POST /outer_space/oauth/token is not be routable' do + expect(post('/outer_space/oauth/token')).not_to be_routable + end + + it 'GET /outer_space/oauth/authorize routes to custom authorizations controller' do + expect(get('/outer_space/oauth/authorize')).to be_routable + end + + it 'GET /outer_space/oauth/applications is not routable' do + expect(get('/outer_space/oauth/applications')).not_to be_routable + end + + it 'GET /outer_space/oauth/token_info is not routable' do + expect(get('/outer_space/oauth/token/info')).not_to be_routable + end +end diff --git a/doorkeeper/spec/routing/default_routes_spec.rb b/doorkeeper/spec/routing/default_routes_spec.rb new file mode 100644 index 0000000000..ba9cfca4b6 --- /dev/null +++ b/doorkeeper/spec/routing/default_routes_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper_integration' + +describe 'Default routes' do + it 'GET /oauth/authorize routes to authorizations controller' do + expect(get('/oauth/authorize')).to route_to('doorkeeper/authorizations#new') + end + + it 'POST /oauth/authorize routes to authorizations controller' do + expect(post('/oauth/authorize')).to route_to('doorkeeper/authorizations#create') + end + + it 'DELETE /oauth/authorize routes to authorizations controller' do + expect(delete('/oauth/authorize')).to route_to('doorkeeper/authorizations#destroy') + end + + it 'POST /oauth/token routes to tokens controller' do + expect(post('/oauth/token')).to route_to('doorkeeper/tokens#create') + end + + it 'POST /oauth/revoke routes to tokens controller' do + expect(post('/oauth/revoke')).to route_to('doorkeeper/tokens#revoke') + end + + it 'POST /oauth/introspect routes to tokens controller' do + expect(post('/oauth/introspect')).to route_to('doorkeeper/tokens#introspect') + end + + it 'GET /oauth/applications routes to applications controller' do + expect(get('/oauth/applications')).to route_to('doorkeeper/applications#index') + end + + it 'GET /oauth/authorized_applications routes to authorized applications controller' do + expect(get('/oauth/authorized_applications')).to route_to('doorkeeper/authorized_applications#index') + end + + it 'GET /oauth/token/info route to authorized tokeninfo controller' do + expect(get('/oauth/token/info')).to route_to('doorkeeper/token_info#show') + end +end diff --git a/doorkeeper/spec/routing/scoped_routes_spec.rb b/doorkeeper/spec/routing/scoped_routes_spec.rb new file mode 100644 index 0000000000..64550ce3c3 --- /dev/null +++ b/doorkeeper/spec/routing/scoped_routes_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper_integration' + +describe 'Scoped routes' do + it 'GET /scope/authorize routes to authorizations controller' do + expect(get('/scope/authorize')).to route_to('doorkeeper/authorizations#new') + end + + it 'POST /scope/authorize routes to authorizations controller' do + expect(post('/scope/authorize')).to route_to('doorkeeper/authorizations#create') + end + + it 'DELETE /scope/authorize routes to authorizations controller' do + expect(delete('/scope/authorize')).to route_to('doorkeeper/authorizations#destroy') + end + + it 'POST /scope/token routes to tokens controller' do + expect(post('/scope/token')).to route_to('doorkeeper/tokens#create') + end + + it 'GET /scope/applications routes to applications controller' do + expect(get('/scope/applications')).to route_to('doorkeeper/applications#index') + end + + it 'GET /scope/authorized_applications routes to authorized applications controller' do + expect(get('/scope/authorized_applications')).to route_to('doorkeeper/authorized_applications#index') + end + + it 'GET /scope/token/info route to authorzed tokeninfo controller' do + expect(get('/scope/token/info')).to route_to('doorkeeper/token_info#show') + end +end diff --git a/doorkeeper/spec/spec_helper.rb b/doorkeeper/spec/spec_helper.rb new file mode 100644 index 0000000000..34c474d00e --- /dev/null +++ b/doorkeeper/spec/spec_helper.rb @@ -0,0 +1,4 @@ +$LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), '../lib')) +$LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), '../app')) + +require 'doorkeeper' diff --git a/doorkeeper/spec/spec_helper_integration.rb b/doorkeeper/spec/spec_helper_integration.rb new file mode 100644 index 0000000000..6e48036096 --- /dev/null +++ b/doorkeeper/spec/spec_helper_integration.rb @@ -0,0 +1,74 @@ +if ENV['TRAVIS'] + require 'coveralls' + + Coveralls.wear!('rails') do + add_filter('/spec/') + add_filter('/lib/generators/doorkeeper/templates/') + end +else + require 'simplecov' + + SimpleCov.start do + add_filter('/spec/') + add_filter('/lib/generators/doorkeeper/templates/') + end +end + +ENV['RAILS_ENV'] ||= 'test' +TABLE_NAME_PREFIX = ENV['table_name_prefix'] || nil +TABLE_NAME_SUFFIX = ENV['table_name_suffix'] || nil + +orm = (ENV['BUNDLE_GEMFILE'] || '').match(/Gemfile\.(.+)\.rb/) +DOORKEEPER_ORM = (orm && orm[1] || :active_record).to_sym + +$LOAD_PATH.unshift File.dirname(__FILE__) + +require 'capybara/rspec' +require 'dummy/config/environment' +require 'rspec/rails' +require 'generator_spec/test_case' +require 'database_cleaner' + +# Load JRuby SQLite3 if in that platform +begin + require 'jdbc/sqlite3' + Jdbc::SQLite3.load_driver +rescue LoadError +end + +Rails.logger.info "====> Doorkeeper.orm = #{Doorkeeper.configuration.orm}" +if Doorkeeper.configuration.orm == :active_record + Rails.logger.info "======> active_record.table_name_prefix = #{Rails.configuration.active_record.table_name_prefix}" + Rails.logger.info "======> active_record.table_name_suffix = #{Rails.configuration.active_record.table_name_suffix}" +end +Rails.logger.info "====> Rails version: #{Rails.version}" +Rails.logger.info "====> Ruby version: #{RUBY_VERSION}" + +require "support/orm/#{DOORKEEPER_ORM}" + +ENGINE_RAILS_ROOT = File.join(File.dirname(__FILE__), '../') + +Dir["#{File.dirname(__FILE__)}/support/{dependencies,helpers,shared}/*.rb"].each { |f| require f } + +# Remove after dropping support of Rails 4.2 +require "#{File.dirname(__FILE__)}/support/http_method_shim.rb" + +RSpec.configure do |config| + config.infer_spec_type_from_file_location! + config.mock_with :rspec + + config.infer_base_class_for_anonymous_controllers = false + + config.include RSpec::Rails::RequestExampleGroup, type: :request + + config.before do + DatabaseCleaner.start + Doorkeeper.configure { orm DOORKEEPER_ORM } + end + + config.after do + DatabaseCleaner.clean + end + + config.order = 'random' +end diff --git a/doorkeeper/spec/support/dependencies/factory_girl.rb b/doorkeeper/spec/support/dependencies/factory_girl.rb new file mode 100644 index 0000000000..98e0cf8d74 --- /dev/null +++ b/doorkeeper/spec/support/dependencies/factory_girl.rb @@ -0,0 +1,2 @@ +require 'factory_bot' +FactoryBot.find_definitions diff --git a/doorkeeper/spec/support/helpers/access_token_request_helper.rb b/doorkeeper/spec/support/helpers/access_token_request_helper.rb new file mode 100644 index 0000000000..97fb184992 --- /dev/null +++ b/doorkeeper/spec/support/helpers/access_token_request_helper.rb @@ -0,0 +1,11 @@ +module AccessTokenRequestHelper + def client_is_authorized(client, resource_owner, access_token_attributes = {}) + attributes = { + application: client, + resource_owner_id: resource_owner.id + }.merge(access_token_attributes) + FactoryBot.create(:access_token, attributes) + end +end + +RSpec.configuration.send :include, AccessTokenRequestHelper diff --git a/doorkeeper/spec/support/helpers/authorization_request_helper.rb b/doorkeeper/spec/support/helpers/authorization_request_helper.rb new file mode 100644 index 0000000000..c25034a50d --- /dev/null +++ b/doorkeeper/spec/support/helpers/authorization_request_helper.rb @@ -0,0 +1,41 @@ +module AuthorizationRequestHelper + def resource_owner_is_authenticated(resource_owner = nil) + resource_owner ||= User.create!(name: 'Joe', password: 'sekret') + Doorkeeper.configuration.instance_variable_set(:@authenticate_resource_owner, proc { resource_owner }) + end + + def resource_owner_is_not_authenticated + Doorkeeper.configuration.instance_variable_set(:@authenticate_resource_owner, proc { redirect_to('/sign_in') }) + end + + def default_scopes_exist(*scopes) + Doorkeeper.configuration.instance_variable_set(:@default_scopes, Doorkeeper::OAuth::Scopes.from_array(scopes)) + end + + def optional_scopes_exist(*scopes) + Doorkeeper.configuration.instance_variable_set(:@optional_scopes, Doorkeeper::OAuth::Scopes.from_array(scopes)) + end + + def client_should_be_authorized(client) + expect(client.access_grants.size).to eq(1) + end + + def client_should_not_be_authorized(client) + expect(client.size).to eq(0) + end + + def i_should_be_on_client_callback(client) + expect(client.redirect_uri).to eq("#{current_uri.scheme}://#{current_uri.host}#{current_uri.path}") + end + + def allowing_forgery_protection(&block) + _original_value = ActionController::Base.allow_forgery_protection + ActionController::Base.allow_forgery_protection = true + + block.call + ensure + ActionController::Base.allow_forgery_protection = _original_value + end +end + +RSpec.configuration.send :include, AuthorizationRequestHelper diff --git a/doorkeeper/spec/support/helpers/config_helper.rb b/doorkeeper/spec/support/helpers/config_helper.rb new file mode 100644 index 0000000000..654f6234fb --- /dev/null +++ b/doorkeeper/spec/support/helpers/config_helper.rb @@ -0,0 +1,9 @@ +module ConfigHelper + def config_is_set(setting, value = nil, &block) + setting_ivar = "@#{setting}" + value = block_given? ? block : value + Doorkeeper.configuration.instance_variable_set(setting_ivar, value) + end +end + +RSpec.configuration.send :include, ConfigHelper diff --git a/doorkeeper/spec/support/helpers/model_helper.rb b/doorkeeper/spec/support/helpers/model_helper.rb new file mode 100644 index 0000000000..a75fa1dbcb --- /dev/null +++ b/doorkeeper/spec/support/helpers/model_helper.rb @@ -0,0 +1,72 @@ +module ModelHelper + def client_exists(client_attributes = {}) + @client = FactoryBot.create(:application, client_attributes) + end + + def create_resource_owner + @resource_owner = User.create!(name: 'Joe', password: 'sekret') + end + + def authorization_code_exists(options = {}) + @authorization = FactoryBot.create(:access_grant, options) + end + + def access_grant_should_exist_for(client, resource_owner) + grant = Doorkeeper::AccessGrant.first + + expect(grant.application).to have_attributes(id: client.id). + and(be_instance_of(Doorkeeper::Application)) + + expect(grant.resource_owner_id).to eq(resource_owner.id) + end + + def access_token_should_exist_for(client, resource_owner) + token = Doorkeeper::AccessToken.first + + expect(token.application).to have_attributes(id: client.id). + and(be_instance_of(Doorkeeper::Application)) + + expect(token.resource_owner_id).to eq(resource_owner.id) + end + + def access_grant_should_not_exist + expect(Doorkeeper::AccessGrant.all).to be_empty + end + + def access_token_should_not_exist + expect(Doorkeeper::AccessToken.all).to be_empty + end + + def access_grant_should_have_scopes(*args) + grant = Doorkeeper::AccessGrant.first + expect(grant.scopes).to eq(Doorkeeper::OAuth::Scopes.from_array(args)) + end + + def access_token_should_have_scopes(*args) + grant = Doorkeeper::AccessToken.last + expect(grant.scopes).to eq(Doorkeeper::OAuth::Scopes.from_array(args)) + end + + def uniqueness_error + case DOORKEEPER_ORM + when :active_record + ActiveRecord::RecordNotUnique + when :sequel + error_classes = [Sequel::UniqueConstraintViolation, Sequel::ValidationFailed] + proc { |error| expect(error.class).to be_in(error_classes) } + when :mongo_mapper + error_classes = [MongoMapper::DocumentNotValid, Mongo::OperationFailure] + proc { |error| expect(error.class).to be_in(error_classes) } + when /mongoid/ + error_classes = [Mongoid::Errors::Validations] + error_classes << Moped::Errors::OperationFailure if defined?(::Moped) # Mongoid 4 + error_classes << Mongo::Error::OperationFailure if defined?(::Mongo) # Mongoid 5 + + proc { |error| expect(error.class).to be_in(error_classes) } + else + raise "'#{DOORKEEPER_ORM}' ORM is not supported!" + end + end +end + +RSpec.configuration.send :include, ModelHelper diff --git a/doorkeeper/spec/support/helpers/request_spec_helper.rb b/doorkeeper/spec/support/helpers/request_spec_helper.rb new file mode 100644 index 0000000000..35bd7b5f21 --- /dev/null +++ b/doorkeeper/spec/support/helpers/request_spec_helper.rb @@ -0,0 +1,88 @@ +module RequestSpecHelper + def i_should_see(content) + expect(page).to have_content(content) + end + + def i_should_not_see(content) + expect(page).to have_no_content(content) + end + + def i_should_be_on(path) + expect(current_path).to eq(path) + end + + def url_should_have_param(param, value) + expect(current_params[param]).to eq(value) + end + + def url_should_not_have_param(param) + expect(current_params).not_to have_key(param) + end + + def current_params + Rack::Utils.parse_query(current_uri.query) + end + + def current_uri + URI.parse(page.current_url) + end + + def request_response + respond_to?(:response) ? response : page.driver.response + end + + def json_response + JSON.parse(request_response.body) + end + + def should_have_header(header, value) + expect(headers[header]).to eq(value) + end + + def with_access_token_header(token) + with_header 'Authorization', "Bearer #{token}" + end + + def with_header(header, value) + page.driver.header header, value + end + + def basic_auth_header_for_client(client) + ActionController::HttpAuthentication::Basic.encode_credentials client.uid, client.secret + end + + def should_have_json(key, value) + expect(json_response.fetch(key)).to eq(value) + end + + def should_have_json_within(key, value, range) + expect(json_response.fetch(key)).to be_within(range).of(value) + end + + def should_not_have_json(key) + expect(json_response).not_to have_key(key) + end + + def sign_in + visit '/' + click_on 'Sign in' + end + + def create_access_token(authorization_code, client) + page.driver.post token_endpoint_url(code: authorization_code, client: client) + end + + def i_should_see_translated_error_message(key) + i_should_see translated_error_message(key) + end + + def translated_error_message(key) + I18n.translate key, scope: %i[doorkeeper errors messages] + end + + def response_status_should_be(status) + expect(request_response.status.to_i).to eq(status) + end +end + +RSpec.configuration.send :include, RequestSpecHelper diff --git a/doorkeeper/spec/support/helpers/url_helper.rb b/doorkeeper/spec/support/helpers/url_helper.rb new file mode 100644 index 0000000000..246c181829 --- /dev/null +++ b/doorkeeper/spec/support/helpers/url_helper.rb @@ -0,0 +1,56 @@ +module UrlHelper + def token_endpoint_url(options = {}) + parameters = { + code: options[:code], + client_id: options[:client_id] || (options[:client] ? options[:client].uid : nil), + client_secret: options[:client_secret] || (options[:client] ? options[:client].secret : nil), + redirect_uri: options[:redirect_uri] || (options[:client] ? options[:client].redirect_uri : nil), + grant_type: options[:grant_type] || 'authorization_code' + } + "/oauth/token?#{build_query(parameters)}" + end + + def password_token_endpoint_url(options = {}) + parameters = { + code: options[:code], + client_id: options[:client_id] || (options[:client] ? options[:client].uid : nil), + client_secret: options[:client_secret] || (options[:client] ? options[:client].secret : nil), + username: options[:resource_owner_username] || (options[:resource_owner] ? options[:resource_owner].name : nil), + password: options[:resource_owner_password] || (options[:resource_owner] ? options[:resource_owner].password : nil), + scope: options[:scope], + grant_type: 'password' + } + "/oauth/token?#{build_query(parameters)}" + end + + def authorization_endpoint_url(options = {}) + parameters = { + client_id: options[:client_id] || options[:client].uid, + redirect_uri: options[:redirect_uri] || options[:client].redirect_uri, + response_type: options[:response_type] || 'code', + scope: options[:scope], + state: options[:state] + }.reject { |_, v| v.blank? } + "/oauth/authorize?#{build_query(parameters)}" + end + + def refresh_token_endpoint_url(options = {}) + parameters = { + refresh_token: options[:refresh_token], + client_id: options[:client_id] || options[:client].uid, + client_secret: options[:client_secret] || options[:client].secret, + grant_type: options[:grant_type] || 'refresh_token' + } + "/oauth/token?#{build_query(parameters)}" + end + + def revocation_token_endpoint_url + '/oauth/revoke' + end + + def build_query(hash) + Rack::Utils.build_query(hash) + end +end + +RSpec.configuration.send :include, UrlHelper diff --git a/doorkeeper/spec/support/http_method_shim.rb b/doorkeeper/spec/support/http_method_shim.rb new file mode 100644 index 0000000000..f187abfbec --- /dev/null +++ b/doorkeeper/spec/support/http_method_shim.rb @@ -0,0 +1,38 @@ +# Rails 5 deprecates calling HTTP action methods with positional arguments +# in favor of keyword arguments. However, the keyword argument form is only +# supported in Rails 5+. Since we support back to 4, we need some sort of shim +# to avoid super noisy deprecations when running tests. +module RoutingHTTPMethodShim + def get(path, params = {}, headers = nil) + super(path, params: params, headers: headers) + end + + def post(path, params = {}, headers = nil) + super(path, params: params, headers: headers) + end + + def put(path, params = {}, headers = nil) + super(path, params: params, headers: headers) + end +end + +module ControllerHTTPMethodShim + def get(path, params = {}) + super(path, params: params) + end + + def post(path, params = {}) + super(path, params: params) + end + + def put(path, params = {}) + super(path, params: params) + end +end + +if ::Rails::VERSION::MAJOR >= 5 + RSpec.configure do |config| + config.include ControllerHTTPMethodShim, type: :controller + config.include RoutingHTTPMethodShim, type: :request + end +end diff --git a/doorkeeper/spec/support/orm/active_record.rb b/doorkeeper/spec/support/orm/active_record.rb new file mode 100644 index 0000000000..ff0cad9b2a --- /dev/null +++ b/doorkeeper/spec/support/orm/active_record.rb @@ -0,0 +1,3 @@ +# load schema to in memory sqlite +ActiveRecord::Migration.verbose = false +load Rails.root + 'db/schema.rb' diff --git a/doorkeeper/spec/support/shared/controllers_shared_context.rb b/doorkeeper/spec/support/shared/controllers_shared_context.rb new file mode 100644 index 0000000000..71c8b1a000 --- /dev/null +++ b/doorkeeper/spec/support/shared/controllers_shared_context.rb @@ -0,0 +1,65 @@ +shared_context 'valid token', token: :valid do + let(:token_string) { '1A2B3C4D' } + + let :token do + double(Doorkeeper::AccessToken, + accessible?: true, includes_scope?: true, acceptable?: true, + previous_refresh_token: "", revoke_previous_refresh_token!: true) + end + + before :each do + allow( + Doorkeeper::AccessToken + ).to receive(:by_token).with(token_string).and_return(token) + end +end + +shared_context 'invalid token', token: :invalid do + let(:token_string) { '1A2B3C4D' } + + let :token do + double(Doorkeeper::AccessToken, + accessible?: false, revoked?: false, expired?: false, + includes_scope?: false, acceptable?: false, + previous_refresh_token: "", revoke_previous_refresh_token!: true) + end + + before :each do + allow( + Doorkeeper::AccessToken + ).to receive(:by_token).with(token_string).and_return(token) + end +end + +shared_context 'authenticated resource owner' do + before do + user = double(:resource, id: 1) + allow(Doorkeeper.configuration).to receive(:authenticate_resource_owner) { proc { user } } + end +end + +shared_context 'not authenticated resource owner' do + before do + allow(Doorkeeper.configuration).to receive(:authenticate_resource_owner) { proc { redirect_to '/' } } + end +end + +shared_context 'valid authorization request' do + let :authorization do + double(:authorization, valid?: true, authorize: true, success_redirect_uri: 'http://something.com/cb?code=token') + end + + before do + allow(controller).to receive(:authorization) { authorization } + end +end + +shared_context 'invalid authorization request' do + let :authorization do + double(:authorization, valid?: false, authorize: false, redirect_on_error?: false) + end + + before do + allow(controller).to receive(:authorization) { authorization } + end +end diff --git a/doorkeeper/spec/support/shared/models_shared_examples.rb b/doorkeeper/spec/support/shared/models_shared_examples.rb new file mode 100644 index 0000000000..b0ea26593d --- /dev/null +++ b/doorkeeper/spec/support/shared/models_shared_examples.rb @@ -0,0 +1,52 @@ +shared_examples 'an accessible token' do + describe :accessible? do + it 'is accessible if token is not expired' do + allow(subject).to receive(:expired?).and_return(false) + should be_accessible + end + + it 'is not accessible if token is expired' do + allow(subject).to receive(:expired?).and_return(true) + should_not be_accessible + end + end +end + +shared_examples 'a revocable token' do + describe :accessible? do + before { subject.save! } + + it 'is accessible if token is not revoked' do + expect(subject).to be_accessible + end + + it 'is not accessible if token is revoked' do + subject.revoke + expect(subject).not_to be_accessible + end + end +end + +shared_examples 'a unique token' do + describe :token do + it 'is generated before validation' do + expect { subject.valid? }.to change { subject.token }.from(nil) + end + + it 'is not valid if token exists' do + token1 = FactoryBot.create factory_name + token2 = FactoryBot.create factory_name + token2.token = token1.token + expect(token2).not_to be_valid + end + + it 'expects database to throw an error when tokens are the same' do + token1 = FactoryBot.create factory_name + token2 = FactoryBot.create factory_name + token2.token = token1.token + expect do + token2.save!(validate: false) + end.to raise_error(uniqueness_error) + end + end +end diff --git a/doorkeeper/spec/validators/redirect_uri_validator_spec.rb b/doorkeeper/spec/validators/redirect_uri_validator_spec.rb new file mode 100644 index 0000000000..0f36beebe6 --- /dev/null +++ b/doorkeeper/spec/validators/redirect_uri_validator_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper_integration' + +describe RedirectUriValidator do + subject do + FactoryBot.create(:application) + end + + it 'is valid when the uri is a uri' do + subject.redirect_uri = 'https://example.com/callback' + expect(subject).to be_valid + end + + # Most mobile and desktop operating systems allow apps to register a custom URL + # scheme that will launch the app when a URL with that scheme is visited from + # the system browser. + # + # @see https://www.oauth.com/oauth2-servers/redirect-uris/redirect-uris-native-apps/ + it 'is valid when the uri is custom native URI' do + subject.redirect_uri = 'myapp://callback' + expect(subject).to be_valid + end + + it 'is valid when the uri has a query parameter' do + subject.redirect_uri = 'https://example.com/abcd?xyz=123' + expect(subject).to be_valid + end + + it 'accepts native redirect uri' do + subject.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob' + expect(subject).to be_valid + end + + it 'rejects if test uri is disabled' do + allow(RedirectUriValidator).to receive(:native_redirect_uri).and_return(nil) + subject.redirect_uri = 'urn:some:test' + expect(subject).not_to be_valid + end + + it 'is invalid when the uri is not a uri' do + subject.redirect_uri = ']' + expect(subject).not_to be_valid + expect(subject.errors[:redirect_uri].first).to eq('must be a valid URI.') + end + + it 'is invalid when the uri is relative' do + subject.redirect_uri = '/abcd' + expect(subject).not_to be_valid + expect(subject.errors[:redirect_uri].first).to eq('must be an absolute URI.') + end + + it 'is invalid when the uri has a fragment' do + subject.redirect_uri = 'https://example.com/abcd#xyz' + expect(subject).not_to be_valid + expect(subject.errors[:redirect_uri].first).to eq('cannot contain a fragment.') + end + + context 'force secured uri' do + it 'accepts an valid uri' do + subject.redirect_uri = 'https://example.com/callback' + expect(subject).to be_valid + end + + it 'accepts native redirect uri' do + subject.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob' + expect(subject).to be_valid + end + + it 'accepts app redirect uri' do + subject.redirect_uri = 'some-awesome-app://oauth/callback' + expect(subject).to be_valid + end + + it 'accepts a non secured protocol when disabled' do + subject.redirect_uri = 'http://example.com/callback' + allow(Doorkeeper.configuration).to receive( + :force_ssl_in_redirect_uri + ).and_return(false) + expect(subject).to be_valid + end + + it 'accepts a non secured protocol when conditional option defined' do + Doorkeeper.configure do + orm DOORKEEPER_ORM + force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' } + end + + application = FactoryBot.build(:application, redirect_uri: 'http://localhost/callback') + expect(application).to be_valid + + application = FactoryBot.build(:application, redirect_uri: 'http://localhost2/callback') + expect(application).not_to be_valid + end + + it 'forbids redirect uri if required' do + subject.redirect_uri = 'javascript://document.cookie' + + Doorkeeper.configure do + orm DOORKEEPER_ORM + forbid_redirect_uri { |uri| uri.scheme == 'javascript' } + end + + expect(subject).to be_invalid + expect(subject.errors[:redirect_uri].first).to eq('is forbidden by the server.') + + subject.redirect_uri = 'https://localhost/callback' + expect(subject).to be_valid + end + + it 'invalidates the uri when the uri does not use a secure protocol' do + subject.redirect_uri = 'http://example.com/callback' + expect(subject).not_to be_valid + error = subject.errors[:redirect_uri].first + expect(error).to eq('must be an HTTPS/SSL URI.') + end + end + + context 'multiple redirect uri' do + it 'invalidates the second uri when the first uri is native uri' do + subject.redirect_uri = "urn:ietf:wg:oauth:2.0:oob\nexample.com/callback" + expect(subject).to be_invalid + end + end +end diff --git a/doorkeeper/spec/version/version_spec.rb b/doorkeeper/spec/version/version_spec.rb new file mode 100644 index 0000000000..22f3c21ceb --- /dev/null +++ b/doorkeeper/spec/version/version_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper_integration' + +describe 'Doorkeeper version' do + context '#gem_version' do + it 'returns Gem::Version instance' do + expect(Doorkeeper.gem_version).to be_an_instance_of(Gem::Version) + end + end + + context 'VERSION' do + it 'returns gem version string' do + expect(Doorkeeper::VERSION::STRING).to match(/^\d+\.\d+\.\d+[.\w]?$/) + end + end +end diff --git a/doorkeeper/vendor/assets/stylesheets/doorkeeper/bootstrap.min.css b/doorkeeper/vendor/assets/stylesheets/doorkeeper/bootstrap.min.css new file mode 100755 index 0000000000..a6db4010a1 --- /dev/null +++ b/doorkeeper/vendor/assets/stylesheets/doorkeeper/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.1.0 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none!important;color:#000!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#999}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.428571429;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.428571429;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=date]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:7px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.428571429;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url(../../../../../../../Downloads/bootstrap-3.1.0/dist/fonts/glyphicons-halflings-regular.eot);src:url(../../../../../../../Downloads/bootstrap-3.1.0/dist/fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../../../../../../../Downloads/bootstrap-3.1.0/dist/fonts/glyphicons-halflings-regular.woff) format('woff'),url(../../../../../../../Downloads/bootstrap-3.1.0/dist/fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../../../../../../../Downloads/bootstrap-3.1.0/dist/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.428571429;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:gray}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;max-width:100%;height:auto;margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group .list-group-item:first-child{border-top:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel>.list-group:first-child .list-group-item:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tfoot>tr:first-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tfoot>tr:first-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:first-child>td{border-top:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.428571429px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top .arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right .arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom .arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom .arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left .arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}@media print{.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}}