diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..a0267668 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + time: "04:28" + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 00000000..fd211ace --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,37 @@ +name: Code Style Checks + +on: + push: + branches: + - 'main' + - 'master' + - '*-maintenance' + - '*-dev' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + +jobs: + rubocop: + name: Rubocop + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + strategy: + fail-fast: false + matrix: + ruby: + - 2.7 + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Install dependencies + run: bundle install --jobs 3 --retry 3 + - name: Run Rubocop + run: bundle exec rubocop -DESP \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..9d0352ab --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +name: Unit Tests + +on: + push: + branches: + - 'main' + - 'master' + - '*-maintenance' + - '*-dev' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{ matrix.name_extra || '' }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + strategy: + fail-fast: false + matrix: + ruby: + - 3.0.0 + - 2.7 + - 2.6 + - 2.5 + - 2.4 + - 2.3 + - 2.2 + - 2.1 + runs-on: ubuntu-20.04 + continue-on-error: ${{ matrix.allow_failure || endsWith(matrix.ruby, 'head') }} + steps: + - uses: amancevice/setup-code-climate@v0 + name: CodeClimate Install + if: matrix.ruby == '2.7' && github.event_name != 'pull_request' + with: + cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }} + - uses: actions/checkout@v2 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + bundler: ${{ matrix.bundler || 2 }} + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + - name: Install dependencies + run: bundle install --jobs 3 --retry 3 --binstubs --standalone + - name: CodeClimate Pre-build Notification + run: cc-test-reporter before-build + if: matrix.ruby == '2.7' && github.event_name != 'pull_request' + continue-on-error: ${{ matrix.allow_failures != 'false' }} + - name: Run tests + run: bundle exec rake test + - name: CodeClimate Post-build Notification + run: cc-test-reporter after-build + if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always() + continue-on-error: ${{ matrix.allow_failures != 'false' }} diff --git a/.gitignore b/.gitignore index 36762cf9..6480a03a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ rdoc/* /gemfiles/*.gemfile.lock # CI bundle -/gemfiles/vendor/ \ No newline at end of file +/gemfiles/vendor/ diff --git a/.rspec b/.rspec index 09127182..3629a4a1 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,4 @@ --color --order random +--require helper +--format=documentation diff --git a/.rubocop.yml b/.rubocop.yml index c6a41973..3c1afd65 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,30 +1,42 @@ -require: rubocop-rspec inherit_from: - .rubocop_todo.yml - .rubocop_rspec.yml + +require: + - 'rubocop-md' + - 'rubocop-packaging' + - 'rubocop-performance' + - 'rubocop-rake' + - 'rubocop-rspec' + AllCops: + NewCops: enable DisplayCopNames: true # Display the name of the failing cops - TargetRubyVersion: 2.1 Exclude: - 'gemfiles/vendor/**/*' - 'vendor/**/*' - '**/.irbrc' -Gemspec/RequiredRubyVersion: - Enabled: false - Metrics/BlockLength: + IgnoredMethods: + - context + - describe + - it + - shared_context + - shared_examples + - shared_examples_for + - namespace + - draw + +Gemspec/RequiredRubyVersion: Enabled: false Metrics/BlockNesting: Max: 2 -Metrics/LineLength: +Layout/LineLength: Enabled: false -Metrics/MethodLength: - Max: 15 - Metrics/ParameterLists: Max: 4 @@ -78,3 +90,23 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: comma + +Style/HashSyntax: + EnforcedStyle: hash_rockets + +Style/Lambda: + Enabled: false + +Style/SymbolArray: + Enabled: false + +Style/EachWithObject: + Enabled: false + +# Once we drop Rubies that lack support for __dir__ we can turn this on. +Style/ExpandPathArguments: + Enabled: false + +# On Ruby 1.9 array.to_h isn't available, needs to be Hash[array] +Style/HashConversion: + Enabled: false \ No newline at end of file diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e7701734..f430c2d1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,15 +1,113 @@ -Style/HashSyntax: - EnforcedStyle: hash_rockets +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2021-03-18 18:59:52 UTC using RuboCop version 1.11.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. -Style/Lambda: - Enabled: false +# Offense count: 1 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Exclude: + - 'spec/oauth2/client_spec.rb' -Style/SymbolArray: - Enabled: false +# Offense count: 1 +Lint/UselessAssignment: + Exclude: + - '**/*.md' + - '**/*.markdown' + - 'spec/oauth2/client_spec.rb' -Style/EachWithObject: - Enabled: false +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +# IgnoredMethods: refine +Metrics/BlockLength: + Max: 27 + +# Offense count: 4 +# Configuration parameters: IgnoredMethods. +Metrics/CyclomaticComplexity: + Max: 11 + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +Metrics/MethodLength: + Max: 18 + +# Offense count: 3 +# Configuration parameters: IgnoredMethods. +Metrics/PerceivedComplexity: + Max: 11 + +# Offense count: 14 +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers. +# SupportedStyles: snake_case, normalcase, non_integer +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 +Naming/VariableNumber: + Exclude: + - 'Gemfile' + +# Offense count: 1 +Packaging/GemspecGit: + Exclude: + - 'oauth2.gemspec' -# Once we drop Rubies that lack support for __dir__ we can turn this on. -Style/ExpandPathArguments: +# Offense count: 2 +# Configuration parameters: MinSize. +Performance/CollectionLiteralInLoop: + Exclude: + - 'spec/oauth2/strategy/auth_code_spec.rb' + - 'spec/oauth2/strategy/client_credentials_spec.rb' + +# Offense count: 7 +# Configuration parameters: Prefixes. +# Prefixes: when, with, without +RSpec/ContextWording: + Exclude: + - 'spec/oauth2/access_token_spec.rb' + - 'spec/oauth2/authenticator_spec.rb' + - 'spec/oauth2/client_spec.rb' + +# Offense count: 1 +RSpec/LeakyConstantDeclaration: + Exclude: + - 'spec/oauth2/client_spec.rb' + +# Offense count: 8 +# Configuration parameters: AllowSubject. +RSpec/MultipleMemoizedHelpers: + Max: 6 + +# Offense count: 1 +Rake/Desc: + Exclude: + - 'Rakefile' + +# Offense count: 40 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, always_true, never +Style/FrozenStringLiteralComment: Enabled: false + +# Offense count: 1 +Style/MixinUsage: + Exclude: + - 'spec/helper.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'lib/oauth2/error.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +Style/StringConcatenation: + Exclude: + - 'lib/oauth2/authenticator.rb' + - 'spec/oauth2/authenticator_spec.rb' diff --git a/.ruby-version b/.ruby-version index 68b3a4cd..24ba9a38 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -1.9.3-p551 +2.7.0 diff --git a/.travis.yml b/.travis.yml index 79b25172..26059437 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,10 +19,23 @@ before_install: gem install --no-document bundler "bundler:>=2.0" fi +before_script: + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + - ./cc-test-reporter before-build + +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT + bundler_args: --no-deployment --jobs 3 --retry 3 cache: bundler +env: + global: + - JRUBY_OPTS="$JRUBY_OPTS -Xcli.debug=true --debug" + - CC_TEST_REPORTER_ID=29caf9cf27d27ae609c088feb9d4ba34460f7a39251f2e8615c9a16f3075530e + language: ruby matrix: @@ -30,8 +43,9 @@ matrix: - rvm: jruby-head - rvm: ruby-head - rvm: truffleruby - - rvm: jruby-9.0 # targets MRI v2.0 - gemfile: gemfiles/jruby_9.0.gemfile + - rvm: jruby-9.0 + - rvm: jruby-9.1 # jruby-9.1 often fails to download, thus failing the build. + - rvm: jruby-9.2 # jruby-9.2 often fails to download, thus failing the build. fast_finish: true include: # - rvm: jruby-1.7 # targets MRI v1.9 @@ -40,29 +54,22 @@ matrix: gemfile: gemfiles/ruby_1.9.gemfile - rvm: 2.0 gemfile: gemfiles/ruby_2.0.gemfile - - rvm: 2.1 - gemfile: gemfiles/ruby_2.1.gemfile + - rvm: jruby-9.0 # targets MRI v2.0 + gemfile: gemfiles/jruby_9.0.gemfile # DEPRECATION WARNING + # NOTE: Specs for Ruby 2.1 are now running with Github Actions # oauth2 1.x series releases are the last to support Ruby versions above # oauth2 2.x series releases will support Ruby versions below, and not above + # NOTE: Specs for Ruby 2.2, 2.3, 2.4, 2.5, 2.6, 2.7 & 3.0 are now running with Github Actions - rvm: jruby-9.1 # targets MRI v2.3 gemfile: gemfiles/jruby_9.1.gemfile - - rvm: 2.2 - gemfile: gemfiles/ruby_2.2.gemfile - - rvm: 2.3 - gemfile: gemfiles/ruby_2.3.gemfile - - rvm: 2.4 - gemfile: gemfiles/ruby_2.4.gemfile - rvm: jruby-9.2 # targets MRI v2.5 gemfile: gemfiles/jruby_9.2.gemfile - - rvm: 2.5 - gemfile: gemfiles/ruby_2.5.gemfile - - rvm: 2.6 - gemfile: gemfiles/ruby_2.6.gemfile - rvm: jruby-head gemfile: gemfiles/jruby_head.gemfile - rvm: ruby-head gemfile: gemfiles/ruby_head.gemfile - rvm: truffleruby + gemfile: gemfiles/truffleruby.gemfile sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index fa957410..2d6ad964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,34 @@ # Change Log All notable changes to this project will be documented in this file. -## [unreleased] +## unreleased -- no changes yet +## [1.4.6] - 2021-03-18 + +- [#537](https://github.com/oauth-xx/oauth2/pull/537) - Fix crash in OAuth2::Client#get_token (@anderscarling) +- [#538](https://github.com/oauth-xx/oauth2/pull/538) - Remove reliance on globally included OAuth2 in tests for version 1.4 (@anderscarling) +- [#540](https://github.com/oauth-xx/oauth2/pull/540) - Add Oauth::Version::VERSION constant (@pboling) + +## [1.4.5] - 2021-03-18 + +- [#535](https://github.com/oauth-xx/oauth2/pull/535) - Compatibility with range of supported Ruby OpenSSL versions, Rubocop updates, Github Actions (@pboling) +- [#518](https://github.com/oauth-xx/oauth2/pull/518) - Add extract_access_token option to OAuth2::Client (@jonspalmer) + +## [1.4.4] - 2020-02-12 + +- [#408](https://github.com/oauth-xx/oauth2/pull/408) - Fixed expires_at for formatted time (@Lomey) + +## [1.4.3] - 2020-01-29 + +- [#483](https://github.com/oauth-xx/oauth2/pull/483) - add project metadata to gemspec (@orien) +- [#495](https://github.com/oauth-xx/oauth2/pull/495) - support additional types of access token requests (@SteveyblamFreeagent, @thomcorley, @dgholz) + - Adds support for private_key_jwt and tls_client_auth +- [#433](https://github.com/oauth-xx/oauth2/pull/433) - allow field names with square brackets and numbers in params (@asm256) + +## [1.4.2] - 2019-10-01 + +- [#478](https://github.com/oauth-xx/oauth2/pull/478) - support latest version of faraday & fix build (@pboling) + - officially support Ruby 2.6 and truffleruby ## [1.4.1] - 2018-10-13 @@ -136,4 +161,6 @@ All notable changes to this project will be documented in this file. [1.3.1]: https://github.com/oauth-xx/oauth2/compare/v1.3.0...v1.3.1 [1.4.0]: https://github.com/oauth-xx/oauth2/compare/v1.3.1...v1.4.0 [1.4.1]: https://github.com/oauth-xx/oauth2/compare/v1.4.0...v1.4.1 +[1.4.2]: https://github.com/oauth-xx/oauth2/compare/v1.4.1...v1.4.2 +[1.4.3]: https://github.com/oauth-xx/oauth2/compare/v1.4.2...v1.4.3 [unreleased]: https://github.com/oauth-xx/oauth2/compare/v1.4.1...HEAD diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 395b407d..99ab478b 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,74 +1,133 @@ + # 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. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community 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 +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or 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 +* Publishing others' private information, such as a physical or email + address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement 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. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -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. +Community leaders 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, and will communicate reasons for moderation +decisions when appropriate. ## 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. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at peter.boling@gmail.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and 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. +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning -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. +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. ## 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] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/Gemfile b/Gemfile index 0dbce159..4657b438 100644 --- a/Gemfile +++ b/Gemfile @@ -1,29 +1,52 @@ +# frozen_string_literal: true + source 'https://rubygems.org' +gemspec + git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } gem 'faraday', ['>= 0.8', '< 2.0'], :platforms => [:jruby_18, :ruby_18] gem 'jwt', '< 1.5.2', :platforms => [:jruby_18, :ruby_18] gem 'rake', '< 11.0' -gem 'rdoc', '~> 4.2.2' -group :test do - ruby_version = Gem::Version.new(RUBY_VERSION) - if ruby_version >= Gem::Version.new('2.1') - # TODO: Upgrade to >= 0.59 when we drop Rubies below 2.2 - # Error: Unsupported Ruby version 2.1 found in `TargetRubyVersion` parameter (in .rubocop.yml). 2.1-compatible analysis was dropped after version 0.58. - # Supported versions: 2.2, 2.3, 2.4, 2.5 - gem 'rubocop', '~> 0.57.0' - gem 'rubocop-rspec', '~> 1.27.0' # last version that can use rubocop < 0.58 +ruby_version = Gem::Version.new(RUBY_VERSION) + +### deps for documentation and rdoc.info +group :documentation do + gem 'github-markup', :platform => :mri + gem 'rdoc' + gem 'redcarpet', :platform => :mri + gem 'yard', :require => false +end + +group :development, :test do + if ruby_version >= Gem::Version.new('2.4') + # No need to run byebug / pry on earlier versions + gem 'byebug', :platform => :mri + gem 'pry', :platform => :mri + gem 'pry-byebug', :platform => :mri end - gem 'pry', '~> 0.11' if ruby_version >= Gem::Version.new('2.0') + if ruby_version >= Gem::Version.new('2.7') + # No need to run rubocop or simplecov on earlier versions + gem 'rubocop', '~> 1.9', :platform => :mri + gem 'rubocop-md', :platform => :mri + gem 'rubocop-packaging', :platform => :mri + gem 'rubocop-performance', :platform => :mri + gem 'rubocop-rake', :platform => :mri + gem 'rubocop-rspec', :platform => :mri + + gem 'coveralls' + gem 'simplecov', :platform => :mri + end +end + +group :test do gem 'addressable', '~> 2.3.8' gem 'backports' - gem 'coveralls' gem 'rack', '~> 1.2', :platforms => [:jruby_18, :jruby_19, :ruby_18, :ruby_19, :ruby_20, :ruby_21] gem 'rspec', '>= 3' - gem 'simplecov', '>= 0.9' platforms :jruby_18, :ruby_18 do gem 'mime-types', '~> 1.25' @@ -36,5 +59,3 @@ group :test do gem 'tins', '< 1.7' end end - -gemspec diff --git a/README.md b/README.md index 77054ded..8cff087e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,26 @@ # OAuth2 +If you need the readme for a released version of the gem please find it below: + +| Version | Release Date | Readme | +|----------|--------------|----------------------------------------------------------| +| 1.4.4 | Feb 12, 2020 | https://github.com/oauth-xx/oauth2/blob/v1.4.4/README.md | +| 1.4.3 | Jan 29, 2020 | https://github.com/oauth-xx/oauth2/blob/v1.4.3/README.md | +| 1.4.2 | Oct 1, 2019 | https://github.com/oauth-xx/oauth2/blob/v1.4.2/README.md | +| 1.4.1 | Oct 13, 2018 | https://github.com/oauth-xx/oauth2/blob/v1.4.1/README.md | +| 1.4.0 | Jun 9, 2017 | https://github.com/oauth-xx/oauth2/blob/v1.4.0/README.md | +| 1.3.1 | Mar 3, 2017 | https://github.com/oauth-xx/oauth2/blob/v1.3.1/README.md | +| 1.3.0 | Dec 27, 2016 | https://github.com/oauth-xx/oauth2/blob/v1.3.0/README.md | +| 1.2.0 | Jun 30, 2016 | https://github.com/oauth-xx/oauth2/blob/v1.2.0/README.md | +| 1.1.0 | Jan 30, 2016 | https://github.com/oauth-xx/oauth2/blob/v1.1.0/README.md | +| 1.0.0 | May 23, 2014 | https://github.com/oauth-xx/oauth2/blob/v1.0.0/README.md | +| < 1.0.0 | Find here | https://github.com/oauth-xx/oauth2/tags | + [![Gem Version](http://img.shields.io/gem/v/oauth2.svg)][gem] [![Total Downloads](https://img.shields.io/gem/dt/oauth2.svg)][gem] [![Downloads Today](https://img.shields.io/gem/rt/oauth2.svg)][gem] [![Build Status](https://travis-ci.org/oauth-xx/oauth2.svg?branch=1-4-stable)][travis] -[![Coverage Status](http://img.shields.io/coveralls/intridea/oauth2.svg)][coveralls] +[![Test Coverage](https://api.codeclimate.com/v1/badges/688c612528ff90a46955/test_coverage)][codeclimate-coverage] [![Maintainability](https://api.codeclimate.com/v1/badges/688c612528ff90a46955/maintainability)][codeclimate-maintainability] [![Depfu](https://badges.depfu.com/badges/6d34dc1ba682bbdf9ae2a97848241743/count.svg)][depfu] [![Open Source Helpers](https://www.codetriage.com/oauth-xx/oauth2/badges/users.svg)][code-triage] @@ -16,10 +32,12 @@ [travis]: http://travis-ci.org/oauth-xx/oauth2 [coveralls]: https://coveralls.io/r/oauth-xx/oauth2 [codeclimate-maintainability]: https://codeclimate.com/github/oauth-xx/oauth2/maintainability +[codeclimate-coverage]: https://codeclimate.com/github/oauth-xx/oauth2/test_coverage [depfu]: https://depfu.com/github/oauth-xx/oauth2 [source-license]: https://opensource.org/licenses/MIT [inch-ci]: http://inch-ci.org/github/oauth-xx/oauth2 [code-triage]: https://www.codetriage.com/oauth-xx/oauth2 +[fossa1]: https://app.fossa.io/projects/git%2Bgithub.com%2Foauth-xx%2Foauth2?ref=badge_shield A Ruby wrapper for the [OAuth 2.0 specification][oauth2-spec]. @@ -49,7 +67,7 @@ Or install it yourself as: [code]: https://github.com/oauth-xx/oauth2 [issues]: https://github.com/oauth-xx/oauth2/issues -[wiki]: https://github.com/oauth-xx/oauth2/wiki +[wiki]: https://wiki.github.com/oauth-xx/oauth2 ## Usage Examples @@ -61,11 +79,12 @@ client.auth_code.authorize_url(:redirect_uri => 'http://localhost:8080/oauth2/ca # => "https://example.org/oauth/authorization?response_type=code&client_id=client_id&redirect_uri=http://localhost:8080/oauth2/callback" token = client.auth_code.get_token('authorization_code_value', :redirect_uri => 'http://localhost:8080/oauth2/callback', :headers => {'Authorization' => 'Basic some_password'}) -response = token.get('/api/resource', :params => { 'query_foo' => 'bar' }) +response = token.get('/api/resource', :params => {'query_foo' => 'bar'}) response.class.name # => OAuth2::Response ``` ## OAuth2::Response + The AccessToken methods #get, #post, #put and #delete and the generic #request will return an instance of the #OAuth2::Response class. @@ -78,12 +97,14 @@ The original response body, headers, and status can be accessed via their respective methods. ## OAuth2::AccessToken + If you have an existing Access Token for a user, you can initialize an instance using various class methods including the standard new, from_hash (if you have a hash of the values), or from_kvform (if you have an application/x-www-form-urlencoded encoded string of the values). ## OAuth2::Error + On 400+ status code responses, an OAuth2::Error will be raised. If it is a standard OAuth2 error response, the body will be parsed and #code and #description will contain the values provided from the error and error_description parameters. The #response property of OAuth2::Error will @@ -95,6 +116,7 @@ instance will be returned as usual and on 400+ status code responses, the Response instance will contain the OAuth2::Error instance. ## Authorization Grants + Currently the Authorization Code, Implicit, Resource Owner Password Credentials, Client Credentials, and Assertion authentication grant types have helper strategy classes that simplify client use. They are available via the #auth_code, #implicit, #password, #client_credentials, and #assertion methods respectively. @@ -129,29 +151,34 @@ requests for tokens for any Authentication grant type. This library aims to support and is [tested against][travis] the following Ruby implementations: -### Rubies with support ending at Oauth2 2.x +### Rubies with support ending at Oauth2 1.x * Ruby 1.9.3 + - [JRuby 1.7][jruby-1.7] (targets MRI v1.9) + * Ruby 2.0.0 + - [JRuby 9.0][jruby-9.0] (targets MRI v2.0) * Ruby 2.1 -* Ruby 2.2 -* [JRuby 1.7][jruby-1.7] (targets MRI v1.9) -* [JRuby 9.0][jruby-9.0] (targets MRI v2.0) --- ### Rubies with continued support past Oauth2 2.x -* Ruby 2.3 - Support through version 3.x series -* Ruby 2.4 -* Ruby 2.5 -* [JRuby 9.1][jruby-9.1] (targets MRI v2.3) -* [JRuby 9.2][jruby-9.2] (targets MRI v2.5) +* Ruby 2.2 - Support ends with version 2.x series +* Ruby 2.3 - Support ends with version 3.x series + - [JRuby 9.1][jruby-9.1] (targets MRI v2.3) +* Ruby 2.4 - Support ends with version 4.x series +* Ruby 2.5 - Support ends with version 5.x series + - [JRuby 9.2][jruby-9.2] (targets MRI v2.5) + - [truffleruby][truffleruby] (targets MRI 2.5) +* Ruby 2.6 - Support ends with version 6.x series +* Ruby 2.7 - Support ends with version 7.x series [jruby-1.7]: https://www.jruby.org/2017/05/11/jruby-1-7-27.html [jruby-9.0]: https://www.jruby.org/2016/01/26/jruby-9-0-5-0.html [jruby-9.1]: https://www.jruby.org/2017/05/16/jruby-9-1-9-0.html [jruby-9.2]: https://www.jruby.org/2018/05/24/jruby-9-2-0-0.html +[truffleruby]: https://github.com/oracle/truffleruby If something doesn't work on one of these interpreters, it's a bug. @@ -203,7 +230,7 @@ spec.add_dependency 'oauth2', '~> 1.4' ## Development -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). diff --git a/Rakefile b/Rakefile index 1066a1b6..19bdfa31 100644 --- a/Rakefile +++ b/Rakefile @@ -33,7 +33,7 @@ end namespace :doc do require 'rdoc/task' - require File.expand_path('../lib/oauth2/version', __FILE__) + require 'oauth2/version' RDoc::Task.new do |rdoc| rdoc.rdoc_dir = 'rdoc' rdoc.title = "oauth2 #{OAuth2::Version}" diff --git a/gemfiles/jruby_1.7.gemfile b/gemfiles/jruby_1.7.gemfile index bbef7523..276621ef 100644 --- a/gemfiles/jruby_1.7.gemfile +++ b/gemfiles/jruby_1.7.gemfile @@ -4,7 +4,7 @@ gem 'faraday', '~> 0.15.4' gem 'json', '< 2.0' gem 'rack', '~> 1.2' -gem 'rake', [">= 10.0", "< 12"] +gem 'rake', ['>= 10.0', '< 12'] gem 'term-ansicolor', '< 1.4.0' gem 'tins', '< 1.7' diff --git a/gemfiles/jruby_9.0.gemfile b/gemfiles/jruby_9.0.gemfile index 13fd08d3..6d3ebd53 100644 --- a/gemfiles/jruby_9.0.gemfile +++ b/gemfiles/jruby_9.0.gemfile @@ -2,6 +2,6 @@ source 'https://rubygems.org' gem 'faraday', '~> 0.15.4' -gem 'rake', [">= 10.0", "< 12"] +gem 'rake', ['>= 10.0', '< 12'] gemspec :path => '../' diff --git a/gemfiles/ruby_1.9.gemfile b/gemfiles/ruby_1.9.gemfile index bbef7523..276621ef 100644 --- a/gemfiles/ruby_1.9.gemfile +++ b/gemfiles/ruby_1.9.gemfile @@ -4,7 +4,7 @@ gem 'faraday', '~> 0.15.4' gem 'json', '< 2.0' gem 'rack', '~> 1.2' -gem 'rake', [">= 10.0", "< 12"] +gem 'rake', ['>= 10.0', '< 12'] gem 'term-ansicolor', '< 1.4.0' gem 'tins', '< 1.7' diff --git a/gemfiles/ruby_2.1.gemfile b/gemfiles/ruby_2.1.gemfile deleted file mode 100644 index 87a679f6..00000000 --- a/gemfiles/ruby_2.1.gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source 'https://rubygems.org' - -gem 'faraday', '~> 0.15.4' -gem 'rack', '~> 1.2' - -gemspec :path => '../' diff --git a/gemfiles/ruby_2.3.gemfile b/gemfiles/ruby_2.3.gemfile deleted file mode 100644 index a02c547f..00000000 --- a/gemfiles/ruby_2.3.gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source 'https://rubygems.org' - -gemspec :path => '../' diff --git a/gemfiles/ruby_2.4.gemfile b/gemfiles/ruby_2.4.gemfile deleted file mode 100644 index a02c547f..00000000 --- a/gemfiles/ruby_2.4.gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source 'https://rubygems.org' - -gemspec :path => '../' diff --git a/gemfiles/ruby_2.5.gemfile b/gemfiles/ruby_2.5.gemfile deleted file mode 100644 index a02c547f..00000000 --- a/gemfiles/ruby_2.5.gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source 'https://rubygems.org' - -gemspec :path => '../' diff --git a/gemfiles/ruby_2.6.gemfile b/gemfiles/ruby_2.6.gemfile deleted file mode 100644 index 822e2f2c..00000000 --- a/gemfiles/ruby_2.6.gemfile +++ /dev/null @@ -1,9 +0,0 @@ -source 'https://rubygems.org' - -group :development do - gem 'pry' - gem 'byebug' - gem 'pry-byebug' -end - -gemspec :path => '../' diff --git a/gemfiles/ruby_head.gemfile b/gemfiles/ruby_head.gemfile index 822e2f2c..c7a3bfd9 100644 --- a/gemfiles/ruby_head.gemfile +++ b/gemfiles/ruby_head.gemfile @@ -1,8 +1,8 @@ source 'https://rubygems.org' group :development do - gem 'pry' gem 'byebug' + gem 'pry' gem 'pry-byebug' end diff --git a/gemfiles/ruby_2.2.gemfile b/gemfiles/truffleruby.gemfile similarity index 100% rename from gemfiles/ruby_2.2.gemfile rename to gemfiles/truffleruby.gemfile diff --git a/lib/oauth2/access_token.rb b/lib/oauth2/access_token.rb index 1b178390..db8c2239 100644 --- a/lib/oauth2/access_token.rb +++ b/lib/oauth2/access_token.rb @@ -3,6 +3,7 @@ class AccessToken attr_reader :client, :token, :expires_in, :expires_at, :params attr_accessor :options, :refresh_token + # Should these methods be deprecated? class << self # Initializes an AccessToken from a Hash # @@ -46,11 +47,11 @@ def initialize(client, token, opts = {}) # rubocop:disable Metrics/AbcSize end @expires_in ||= opts.delete('expires') @expires_in &&= @expires_in.to_i - @expires_at &&= @expires_at.to_i + @expires_at &&= convert_expires_at(@expires_at) @expires_at ||= Time.now.to_i + @expires_in if @expires_in - @options = {:mode => opts.delete(:mode) || :header, + @options = {:mode => opts.delete(:mode) || :header, :header_format => opts.delete(:header_format) || 'Bearer %s', - :param_name => opts.delete(:param_name) || 'access_token'} + :param_name => opts.delete(:param_name) || 'access_token'} @params = opts end @@ -81,6 +82,7 @@ def expired? # @note options should be carried over to the new AccessToken def refresh!(params = {}) raise('A refresh_token is not available') unless refresh_token + params[:grant_type] = 'refresh_token' params[:refresh_token] = refresh_token new_token = @client.get_token(params) @@ -149,7 +151,7 @@ def headers private - def configure_authentication!(opts) # rubocop:disable MethodLength, Metrics/AbcSize + def configure_authentication!(opts) # rubocop:disable Metrics/AbcSize case options[:mode] when :header opts[:headers] ||= {} @@ -169,5 +171,13 @@ def configure_authentication!(opts) # rubocop:disable MethodLength, Metrics/AbcS raise("invalid :mode option of #{options[:mode]}") end end + + def convert_expires_at(expires_at) + expires_at_i = expires_at.to_i + return expires_at_i if expires_at_i > Time.now.utc.to_i + return Time.parse(expires_at).to_i if expires_at.is_a?(String) + + expires_at_i + end end end diff --git a/lib/oauth2/authenticator.rb b/lib/oauth2/authenticator.rb index ce627920..9588da34 100644 --- a/lib/oauth2/authenticator.rb +++ b/lib/oauth2/authenticator.rb @@ -25,6 +25,10 @@ def apply(params) apply_basic_auth(params) when :request_body apply_params_auth(params) + when :tls_client_auth + apply_client_id(params) + when :private_key_jwt + params else raise NotImplementedError end @@ -42,6 +46,12 @@ def apply_params_auth(params) {'client_id' => id, 'client_secret' => secret}.merge(params) end + # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth, + # we don't want to send the secret + def apply_client_id(params) + {'client_id' => id}.merge(params) + end + # Adds an `Authorization` header with Basic Auth credentials if and only if # it is not already set in the params. def apply_basic_auth(params) diff --git a/lib/oauth2/client.rb b/lib/oauth2/client.rb index 356f93e7..f98a9f37 100644 --- a/lib/oauth2/client.rb +++ b/lib/oauth2/client.rb @@ -4,6 +4,8 @@ module OAuth2 # The OAuth2::Client class class Client # rubocop:disable Metrics/ClassLength + RESERVED_PARAM_KEYS = %w[headers parse].freeze + attr_reader :id, :secret, :site attr_accessor :options attr_writer :connection @@ -23,8 +25,8 @@ class Client # rubocop:disable Metrics/ClassLength # @option opts [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body) # @option opts [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with # @option opts [FixNum] :max_redirects (5) maximum number of redirects to follow - # @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error - # on responses with 400+ status codes + # @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes + # @option opts [Proc] :extract_access_token proc that extracts the access token from the response # @yield [builder] The Faraday connection builder def initialize(client_id, client_secret, options = {}, &block) opts = options.dup @@ -32,14 +34,18 @@ def initialize(client_id, client_secret, options = {}, &block) @secret = client_secret @site = opts.delete(:site) ssl = opts.delete(:ssl) - @options = {:authorize_url => '/oauth/authorize', - :token_url => '/oauth/token', - :token_method => :post, - :auth_scheme => :request_body, - :connection_opts => {}, - :connection_build => block, - :max_redirects => 5, - :raise_errors => true}.merge(opts) + + @options = { + :authorize_url => '/oauth/authorize', + :token_url => '/oauth/token', + :token_method => :post, + :auth_scheme => :request_body, + :connection_opts => {}, + :connection_build => block, + :max_redirects => 5, + :raise_errors => true, + :extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN, + }.merge(opts) @options[:connection_opts][:ssl] = ssl if ssl end @@ -91,12 +97,13 @@ def token_url(params = nil) # code response for this request. Will default to client option # @option opts [Symbol] :parse @see Response::initialize # @yield [req] The Faraday request - def request(verb, url, opts = {}) # rubocop:disable CyclomaticComplexity, MethodLength, Metrics/AbcSize + def request(verb, url, opts = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize connection.response :logger, ::Logger.new($stdout) if ENV['OAUTH_DEBUG'] == 'true' - url = connection.build_url(url, opts[:params]).to_s + url = connection.build_url(url).to_s response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req| + req.params.update(opts[:params]) if opts[:params] yield(req) if block_given? end response = Response.new(response, :parse => opts[:parse]) @@ -106,6 +113,7 @@ def request(verb, url, opts = {}) # rubocop:disable CyclomaticComplexity, Method opts[:redirect_count] ||= 0 opts[:redirect_count] += 1 return response if opts[:redirect_count] > options[:max_redirects] + if response.status == 303 verb = :get opts.delete(:body) @@ -117,6 +125,7 @@ def request(verb, url, opts = {}) # rubocop:disable CyclomaticComplexity, Method when 400..599 error = Error.new(response) raise(error) if opts.fetch(:raise_errors, options[:raise_errors]) + response.error = error response else @@ -130,8 +139,17 @@ def request(verb, url, opts = {}) # rubocop:disable CyclomaticComplexity, Method # @param [Hash] params a Hash of params for the token endpoint # @param [Hash] access token options, to pass to the AccessToken object # @param [Class] class of access token for easier subclassing OAuth2::AccessToken - # @return [AccessToken] the initalized AccessToken - def get_token(params, access_token_opts = {}, access_token_class = AccessToken) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # @return [AccessToken] the initialized AccessToken + def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + params = params.map do |key, value| + if RESERVED_PARAM_KEYS.include?(key) + [key.to_sym, value] + else + [key, value] + end + end + params = Hash[params] + params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params) opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)} headers = params.delete(:headers) || {} @@ -144,11 +162,18 @@ def get_token(params, access_token_opts = {}, access_token_class = AccessToken) end opts[:headers].merge!(headers) response = request(options[:token_method], token_url, opts) - if options[:raise_errors] && !(response.parsed.is_a?(Hash) && response.parsed['access_token']) + + access_token = begin + build_access_token(response, access_token_opts, extract_access_token) + rescue StandardError + nil + end + + if options[:raise_errors] && !access_token error = Error.new(response) raise(error) end - access_token_class.from_hash(self, response.parsed.merge(access_token_opts)) + access_token end # The Authorization Code strategy @@ -206,5 +231,27 @@ def redirection_params {} end end + + DEFAULT_EXTRACT_ACCESS_TOKEN = proc do |client, hash| + token = hash.delete('access_token') || hash.delete(:access_token) + token && AccessToken.new(client, token, hash) + end + + private + + def build_access_token(response, access_token_opts, extract_access_token) + parsed_response = response.parsed.dup + return unless parsed_response.is_a?(Hash) + + hash = parsed_response.merge(access_token_opts) + + # Provide backwards compatibility for old AcessToken.form_hash pattern + # Should be deprecated in 2.x + if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash) + extract_access_token.from_hash(self, hash) + else + extract_access_token.call(self, hash) + end + end end end diff --git a/lib/oauth2/mac_token.rb b/lib/oauth2/mac_token.rb index db7d4d77..99c6f5ec 100644 --- a/lib/oauth2/mac_token.rb +++ b/lib/oauth2/mac_token.rb @@ -98,9 +98,17 @@ def algorithm=(alg) @algorithm = begin case alg.to_s when 'hmac-sha-1' - OpenSSL::Digest::SHA1.new + begin + OpenSSL::Digest('SHA1').new + rescue StandardError + OpenSSL::Digest.new('SHA1') + end when 'hmac-sha-256' - OpenSSL::Digest::SHA256.new + begin + OpenSSL::Digest('SHA256').new + rescue StandardError + OpenSSL::Digest.new('SHA256') + end else raise(ArgumentError, 'Unsupported algorithm') end diff --git a/lib/oauth2/response.rb b/lib/oauth2/response.rb index 13657fd9..fd98617b 100644 --- a/lib/oauth2/response.rb +++ b/lib/oauth2/response.rb @@ -11,9 +11,9 @@ class Response # Procs that, when called, will parse a response body according # to the specified format. @@parsers = { - :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable RescueModifier + :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable Style/RescueModifier :query => lambda { |body| Rack::Utils.parse_query(body) }, - :text => lambda { |body| body }, + :text => lambda { |body| body }, } # Content type assignments for various potential HTTP content types. @@ -68,6 +68,7 @@ def body # application/json Content-Type response bodies def parsed return nil unless @@parsers.key?(parser) + @parsed ||= @@parsers[parser].call(body) end @@ -79,11 +80,12 @@ def content_type # Determines the parser that will be used to supply the content of #parsed def parser return options[:parse].to_sym if @@parsers.key?(options[:parse]) + @@content_types[content_type] end end end OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body| - MultiXml.parse(body) rescue body # rubocop:disable RescueModifier + MultiXml.parse(body) rescue body # rubocop:disable Style/RescueModifier end diff --git a/lib/oauth2/strategy/assertion.rb b/lib/oauth2/strategy/assertion.rb index b3b577be..8dc27b50 100644 --- a/lib/oauth2/strategy/assertion.rb +++ b/lib/oauth2/strategy/assertion.rb @@ -50,10 +50,10 @@ def get_token(params = {}, opts = {}) def build_request(params) assertion = build_assertion(params) { - :grant_type => 'assertion', + :grant_type => 'assertion', :assertion_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer', - :assertion => assertion, - :scope => params[:scope], + :assertion => assertion, + :scope => params[:scope], } end diff --git a/lib/oauth2/strategy/password.rb b/lib/oauth2/strategy/password.rb index 49bfc6e3..075dec51 100644 --- a/lib/oauth2/strategy/password.rb +++ b/lib/oauth2/strategy/password.rb @@ -18,8 +18,8 @@ def authorize_url # @param [Hash] params additional params def get_token(username, password, params = {}, opts = {}) params = {'grant_type' => 'password', - 'username' => username, - 'password' => password}.merge(params) + 'username' => username, + 'password' => password}.merge(params) @client.get_token(params, opts) end end diff --git a/lib/oauth2/version.rb b/lib/oauth2/version.rb index 6b63a98c..6b7b63e0 100644 --- a/lib/oauth2/version.rb +++ b/lib/oauth2/version.rb @@ -1,5 +1,7 @@ module OAuth2 module Version + VERSION = to_s + module_function # The major version @@ -20,7 +22,7 @@ def minor # # @return [Integer] def patch - 1 + 5 end # The pre-release version, if any diff --git a/maintenance-branch b/maintenance-branch new file mode 100644 index 00000000..8b25206f --- /dev/null +++ b/maintenance-branch @@ -0,0 +1 @@ +master \ No newline at end of file diff --git a/oauth2.gemspec b/oauth2.gemspec index be52c417..86a825c7 100644 --- a/oauth2.gemspec +++ b/oauth2.gemspec @@ -20,7 +20,15 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 1.9.0' spec.required_rubygems_version = '>= 1.3.5' spec.summary = 'A Ruby wrapper for the OAuth 2.0 protocol.' - spec.version = OAuth2::Version + spec.version = OAuth2::Version.to_s + + spec.metadata = { + 'bug_tracker_uri' => 'https://github.com/oauth-xx/oauth2/issues', + 'changelog_uri' => "https://github.com/oauth-xx/oauth2/blob/v#{spec.version}/CHANGELOG.md", + 'documentation_uri' => "https://www.rubydoc.info/gems/oauth2/#{spec.version}", + 'source_code_uri' => "https://github.com/oauth-xx/oauth2/tree/v#{spec.version}", + 'wiki_uri' => 'https://github.com/oauth-xx/oauth2/wiki', + } spec.require_paths = %w[lib] spec.bindir = 'exe' @@ -36,9 +44,9 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rake', '~> 12.3' spec.add_development_dependency 'rdoc', ['>= 5.0', '< 7'] spec.add_development_dependency 'rspec', '~> 3.0' - spec.add_development_dependency 'rspec-stubbed_env' - spec.add_development_dependency 'rspec-pending_for' spec.add_development_dependency 'rspec-block_is_expected' + spec.add_development_dependency 'rspec-pending_for' + spec.add_development_dependency 'rspec-stubbed_env' spec.add_development_dependency 'silent_stream' spec.add_development_dependency 'wwtd' end diff --git a/spec/helper.rb b/spec/helper.rb index e3cf4805..8b64d420 100644 --- a/spec/helper.rb +++ b/spec/helper.rb @@ -1,6 +1,8 @@ DEBUG = ENV['DEBUG'] == 'true' -if RUBY_VERSION >= '1.9' +ruby_version = Gem::Version.new(RUBY_VERSION) + +if ruby_version >= Gem::Version.new('2.7') require 'simplecov' require 'coveralls' @@ -12,9 +14,7 @@ end end -if DEBUG && RUBY_VERSION >= '2.6' - require 'byebug' -end +require 'byebug' if DEBUG && ruby_version >= Gem::Version.new('2.4') require 'oauth2' require 'addressable/uri' @@ -30,11 +30,6 @@ Faraday.default_adapter = :test -# This is dangerous - HERE BE DRAGONS. -# It allows us to refer to classes without the namespace, but at what cost?!? -# TODO: Refactor to use explicit references everywhere -include OAuth2 - RSpec.configure do |conf| conf.include SilentStream end diff --git a/spec/oauth2/access_token_spec.rb b/spec/oauth2/access_token_spec.rb index ad53b2ac..c586bc4c 100644 --- a/spec/oauth2/access_token_spec.rb +++ b/spec/oauth2/access_token_spec.rb @@ -1,17 +1,16 @@ -require 'helper' - -describe AccessToken do +describe OAuth2::AccessToken do subject { described_class.new(client, token) } let(:token) { 'monkey' } let(:refresh_body) { MultiJson.encode(:access_token => 'refreshed_foo', :expires_in => 600, :refresh_token => 'refresh_bar') } let(:client) do - Client.new('abc', 'def', :site => 'https://api.example.com') do |builder| + OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com') do |builder| builder.request :url_encoded builder.adapter :test do |stub| VERBS.each do |verb| stub.send(verb, '/token/header') { |env| [200, {}, env[:request_headers]['Authorization']] } stub.send(verb, "/token/query?access_token=#{token}") { |env| [200, {}, Addressable::URI.parse(env[:url]).query_values['access_token']] } + stub.send(verb, '/token/query_string') { |env| [200, {}, CGI.unescape(Addressable::URI.parse(env[:url]).query)] } stub.send(verb, '/token/body') { |env| [200, {}, env[:body]] } end stub.post('/oauth/token') { |env| [200, {'Content-Type' => 'application/json'}, refresh_body] } @@ -72,10 +71,12 @@ def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize end it 'initializes with a string expires_at' do - hash = {:access_token => token, :expires_at => '1361396829', 'foo' => 'bar'} + future = Time.now.utc + 100_000 + hash = {:access_token => token, :expires_at => future.iso8601, 'foo' => 'bar'} target = described_class.from_hash(client, hash) assert_initialized_token(target) expect(target.expires_at).to be_a(Integer) + expect(target.expires_at).to eql(future.to_i) end end @@ -101,6 +102,11 @@ def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do expect(subject.post('/token/query').body).to eq(token) end + + it "sends a #{verb.to_s.upcase} request and options[:param_name] include [number]." do + subject.options[:param_name] = 'auth[1]' + expect(subject.__send__(verb, '/token/query_string').body).to include("auth[1]=#{token}") + end end end @@ -115,6 +121,14 @@ def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize end end end + + context 'params include [number]' do + VERBS.each do |verb| + it "sends #{verb.to_s.upcase} correct query" do + expect(subject.__send__(verb, '/token/query_string', :params => {'foo[bar][1]' => 'val'}).body).to include('foo[bar][1]=val') + end + end + end end describe '#expires?' do @@ -151,8 +165,8 @@ def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize describe '#refresh!' do let(:access) do described_class.new(client, token, :refresh_token => 'abaca', - :expires_in => 600, - :param_name => 'o_param') + :expires_in => 600, + :param_name => 'o_param') end it 'returns a refresh token with appropriate values carried over' do diff --git a/spec/oauth2/authenticator_spec.rb b/spec/oauth2/authenticator_spec.rb index 49838da3..48cbac9b 100644 --- a/spec/oauth2/authenticator_spec.rb +++ b/spec/oauth2/authenticator_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - describe OAuth2::Authenticator do subject do described_class.new(client_id, client_secret, mode) @@ -38,6 +36,24 @@ :headers => {'A' => 'b'} ) end + + context 'using tls client authentication' do + let(:mode) { :tls_client_auth } + + it 'does not add client_secret' do + output = subject.apply({}) + expect(output).to eq('client_id' => 'foo') + end + end + + context 'using private key jwt authentication' do + let(:mode) { :private_key_jwt } + + it 'does not add client_secret or client_id' do + output = subject.apply({}) + expect(output).to eq({}) + end + end end context 'with Basic authentication' do diff --git a/spec/oauth2/client_spec.rb b/spec/oauth2/client_spec.rb index 464a5b5a..583ea12f 100644 --- a/spec/oauth2/client_spec.rb +++ b/spec/oauth2/client_spec.rb @@ -157,6 +157,68 @@ client.auth_code.get_token('code') end end + + describe 'custom headers' do + context 'string key headers' do + it 'adds the custom headers to request' do + client = described_class.new('abc', 'def', :site => 'https://api.example.com', :auth_scheme => :request_body) do |builder| + builder.adapter :test do |stub| + stub.post('/oauth/token') do |env| + expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'}) + [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}'] + end + end + end + header_params = {'headers' => {'CustomHeader' => 'CustomHeader'}} + client.auth_code.get_token('code', header_params) + end + end + + context 'symbol key headers' do + it 'adds the custom headers to request' do + client = described_class.new('abc', 'def', :site => 'https://api.example.com', :auth_scheme => :request_body) do |builder| + builder.adapter :test do |stub| + stub.post('/oauth/token') do |env| + expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'}) + [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}'] + end + end + end + header_params = {:headers => {'CustomHeader' => 'CustomHeader'}} + client.auth_code.get_token('code', header_params) + end + end + + context 'string key custom headers with basic auth' do + it 'adds the custom headers to request' do + client = described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder| + builder.adapter :test do |stub| + stub.post('/oauth/token') do |env| + expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'}) + [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}'] + end + end + end + header_params = {'headers' => {'CustomHeader' => 'CustomHeader'}} + client.auth_code.get_token('code', header_params) + end + end + + context 'symbol key custom headers with basic auth' do + it 'adds the custom headers to request' do + client = described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder| + builder.adapter :test do |stub| + stub.post('/oauth/token') do |env| + expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'}) + [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}'] + end + end + end + header_params = {:headers => {'CustomHeader' => 'CustomHeader'}} + client.auth_code.get_token('code', header_params) + end + end + end end describe '#request' do @@ -212,12 +274,13 @@ end end + # rubocop:disable Style/RedundantBegin it 're-encodes response body in the error message' do begin subject.request(:get, '/ascii_8bit_encoding') - rescue StandardError => ex - expect(ex.message.encoding.name).to eq('UTF-8') - expect(ex.message).to eq("invalid_request: é\n{\"error\":\"invalid_request\",\"error_description\":\"��\"}") + rescue StandardError => e + expect(e.message.encoding.name).to eq('UTF-8') + expect(e.message).to eq("invalid_request: é\n{\"error\":\"invalid_request\",\"error_description\":\"��\"}") end end @@ -240,20 +303,22 @@ expect(e.to_s).to match(/unknown error/) end end + # rubocop:enable Style/RedundantBegin context 'with ENV' do include_context 'with stubbed env' before do stub_env('OAUTH_DEBUG' => 'true') end + it 'outputs to $stdout when OAUTH_DEBUG=true' do output = capture(:stdout) do subject.request(:get, '/success') end logs = [ - 'INFO -- request: GET https://api.example.com/success', - 'INFO -- response: Status 200', - 'DEBUG -- response: Content-Type: "text/awesome"' + '-- request: GET https://api.example.com/success', + '-- response: Status 200', + '-- response: Content-Type: "text/awesome"', ] expect(output).to include(*logs) end @@ -286,12 +351,129 @@ client = stubbed_client(:auth_scheme => :basic_auth) do |stub| stub.post('/oauth/token') do |env| raise Faraday::Adapter::Test::Stubs::NotFound unless env[:request_headers]['Authorization'] == OAuth2::Authenticator.encode_basic_auth('abc', 'def') + [200, {'Content-Type' => 'application/json'}, MultiJson.encode('access_token' => 'the-token')] end end client.get_token({}) end + describe 'extract_access_token option' do + let(:client) do + client = stubbed_client(:extract_access_token => extract_access_token) do |stub| + stub.post('/oauth/token') do + [200, {'Content-Type' => 'application/json'}, MultiJson.encode('data' => {'access_token' => 'the-token'})] + end + end + end + + context 'with proc extract_access_token' do + let(:extract_access_token) do + proc do |client, hash| + token = hash['data']['access_token'] + OAuth2::AccessToken.new(client, token, hash) + end + end + + it 'returns a configured AccessToken' do + token = client.get_token({}) + expect(token).to be_a OAuth2::AccessToken + expect(token.token).to eq('the-token') + end + end + + context 'with depracted Class.from_hash option' do + let(:extract_access_token) do + CustomAccessToken = Class.new(OAuth2::AccessToken) + CustomAccessToken.define_singleton_method(:from_hash) do |client, hash| + token = hash['data']['access_token'] + OAuth2::AccessToken.new(client, token, hash) + end + CustomAccessToken + end + + it 'returns a configured AccessToken' do + token = client.get_token({}) + expect(token).to be_a OAuth2::AccessToken + expect(token.token).to eq('the-token') + end + end + end + + describe ':raise_errors flag' do + let(:options) { {} } + let(:token_response) { nil } + + let(:client) do + stubbed_client(options.merge(:raise_errors => raise_errors)) do |stub| + stub.post('/oauth/token') do + # stub 200 response so that we're testing the get_token handling of :raise_errors flag not request + [200, {'Content-Type' => 'application/json'}, token_response] + end + end + end + + context 'when set to false' do + let(:raise_errors) { false } + + context 'when the request body is nil' do + it 'returns a nil :access_token' do + expect(client.get_token({})).to eq(nil) + end + end + + context 'when the request body is missing the access_token' do + let(:token_response) { MultiJson.encode('unexpected_access_token' => 'the-token') } + + it 'returns a nil :access_token' do + expect(client.get_token({})).to eq(nil) + end + end + + context 'when extract_access_token raises an exception' do + let(:options) do + { + :extract_access_token => proc { |client, hash| raise ArgumentError }, + } + end + + it 'returns a nil :access_token' do + expect(client.get_token({})).to eq(nil) + end + end + end + + context 'when set to true' do + let(:raise_errors) { true } + + context 'when the request body is nil' do + it 'raises an error' do + expect { client.get_token({}) }.to raise_error OAuth2::Error + end + end + + context 'when the request body is missing the access_token' do + let(:token_response) { MultiJson.encode('unexpected_access_token' => 'the-token') } + + it 'raises an error' do + expect { client.get_token({}) }.to raise_error OAuth2::Error + end + end + + context 'when extract_access_token raises an exception' do + let(:options) do + { + :extract_access_token => proc { |client, hash| raise ArgumentError }, + } + end + + it 'raises an error' do + expect { client.get_token({}) }.to raise_error OAuth2::Error + end + end + end + end + def stubbed_client(params = {}, &stubs) params = {:site => 'https://api.example.com'}.merge(params) OAuth2::Client.new('abc', 'def', params) do |builder| diff --git a/spec/oauth2/mac_token_spec.rb b/spec/oauth2/mac_token_spec.rb index add5f72e..0685350f 100644 --- a/spec/oauth2/mac_token_spec.rb +++ b/spec/oauth2/mac_token_spec.rb @@ -1,11 +1,9 @@ -require 'helper' - -describe MACToken do +describe OAuth2::MACToken do subject { described_class.new(client, token, 'abc123') } let(:token) { 'monkey' } let(:client) do - Client.new('abc', 'def', :site => 'https://api.example.com') do |builder| + OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com') do |builder| builder.request :url_encoded builder.adapter :test do |stub| VERBS.each do |verb| @@ -91,7 +89,7 @@ subject { described_class.from_access_token(access_token, 'hello') } let(:access_token) do - AccessToken.new( + OAuth2::AccessToken.new( client, token, :expires_at => 1, :expires_in => 1, diff --git a/spec/oauth2/response_spec.rb b/spec/oauth2/response_spec.rb index ace9ac52..3171fd26 100644 --- a/spec/oauth2/response_spec.rb +++ b/spec/oauth2/response_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - describe OAuth2::Response do describe '#initialize' do let(:status) { 200 } @@ -8,9 +6,9 @@ it 'returns the status, headers and body' do response = double('response', :headers => headers, - :status => status, - :body => body) - subject = Response.new(response) + :status => status, + :body => body) + subject = described_class.new(response) expect(subject.headers).to eq(headers) expect(subject.status).to eq(status) expect(subject.body).to eq(body) @@ -45,7 +43,7 @@ headers = {'Content-Type' => 'application/x-www-form-urlencoded'} body = 'foo=bar&answer=42' response = double('response', :headers => headers, :body => body) - subject = Response.new(response) + subject = described_class.new(response) expect(subject.parsed.keys.size).to eq(2) expect(subject.parsed['foo']).to eq('bar') expect(subject.parsed['answer']).to eq('42') @@ -55,7 +53,7 @@ headers = {'Content-Type' => 'application/json'} body = MultiJson.encode(:foo => 'bar', :answer => 42) response = double('response', :headers => headers, :body => body) - subject = Response.new(response) + subject = described_class.new(response) expect(subject.parsed.keys.size).to eq(2) expect(subject.parsed['foo']).to eq('bar') expect(subject.parsed['answer']).to eq(42) @@ -71,7 +69,7 @@ expect(MultiJson).not_to receive(:load) expect(Rack::Utils).not_to receive(:parse_query) - subject = Response.new(response) + subject = described_class.new(response) expect(subject.parsed).to be_nil end end diff --git a/spec/oauth2/strategy/assertion_spec.rb b/spec/oauth2/strategy/assertion_spec.rb index 36ea17de..148e3e3a 100644 --- a/spec/oauth2/strategy/assertion_spec.rb +++ b/spec/oauth2/strategy/assertion_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - describe OAuth2::Strategy::Assertion do subject { client.assertion } @@ -22,8 +20,8 @@ let(:params) do { - :hmac_secret => 'foo', - :exp => Time.now.utc.to_i + 3600 + :hmac_secret => 'foo', + :exp => Time.now.utc.to_i + 3600, } end diff --git a/spec/oauth2/strategy/auth_code_spec.rb b/spec/oauth2/strategy/auth_code_spec.rb index bcb1984f..36cd3a5a 100644 --- a/spec/oauth2/strategy/auth_code_spec.rb +++ b/spec/oauth2/strategy/auth_code_spec.rb @@ -1,7 +1,5 @@ # encoding: utf-8 -require 'helper' - describe OAuth2::Strategy::AuthCode do subject { client.auth_code } diff --git a/spec/oauth2/strategy/base_spec.rb b/spec/oauth2/strategy/base_spec.rb index fb59e963..67c66d1a 100644 --- a/spec/oauth2/strategy/base_spec.rb +++ b/spec/oauth2/strategy/base_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - describe OAuth2::Strategy::Base do it 'initializes with a Client' do expect { described_class.new(OAuth2::Client.new('abc', 'def')) }.not_to raise_error diff --git a/spec/oauth2/strategy/client_credentials_spec.rb b/spec/oauth2/strategy/client_credentials_spec.rb index 11d3b525..4f91bfd5 100644 --- a/spec/oauth2/strategy/client_credentials_spec.rb +++ b/spec/oauth2/strategy/client_credentials_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - describe OAuth2::Strategy::ClientCredentials do subject { client.client_credentials } diff --git a/spec/oauth2/strategy/implicit_spec.rb b/spec/oauth2/strategy/implicit_spec.rb index 7f48ec04..c4df8ebd 100644 --- a/spec/oauth2/strategy/implicit_spec.rb +++ b/spec/oauth2/strategy/implicit_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - describe OAuth2::Strategy::Implicit do subject { client.implicit } diff --git a/spec/oauth2/strategy/password_spec.rb b/spec/oauth2/strategy/password_spec.rb index 0c9a07dd..c8b006aa 100644 --- a/spec/oauth2/strategy/password_spec.rb +++ b/spec/oauth2/strategy/password_spec.rb @@ -1,5 +1,3 @@ -require 'helper' - describe OAuth2::Strategy::Password do subject { client.password } diff --git a/spec/oauth2/version_spec.rb b/spec/oauth2/version_spec.rb new file mode 100644 index 00000000..3e395eac --- /dev/null +++ b/spec/oauth2/version_spec.rb @@ -0,0 +1,5 @@ +describe OAuth2::Version do + it 'VERSION a sting' do + expect(OAuth2::Version::VERSION).to be_a(String) + end +end