diff --git a/.document b/.document index ecf36731..6f40d63c 100644 --- a/.document +++ b/.document @@ -1,5 +1,4 @@ -README.rdoc +README.md lib/**/*.rb bin/* -features/**/*.feature LICENSE diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..e21342e5 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [pboling] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: galtzo # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: pboling # Replace with a single Ko-fi username +tidelift: rubygems/oauth2 # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: pboling # Replace with a single Liberapay username +issuehunt: pboling # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 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/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..0dc599b6 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main, "*-stable" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, "*-stable" ] + schedule: + - cron: '35 1 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'ruby' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..8cb14169 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,109 @@ +name: Code Coverage + +env: + CI_CODECOV: true + COVER_ALL: true + +on: + push: + branches: + - 'main' + - '*-maintenance' + - '*-dev' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Specs with Coverage - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ 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: + experimental: [false] + rubygems: + - latest + bundler: + - latest + ruby: + - "2.7" + + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - uses: amancevice/setup-code-climate@v0 + name: CodeClimate Install + if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always() + with: + cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }} + + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: CodeClimate Pre-build Notification + run: cc-test-reporter before-build + if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always() + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Run tests + run: bundle exec rake test + + - name: Code Coverage Summary Report + uses: irongut/CodeCoverageSummary@v1.2.0 + with: + filename: ./coverage/coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: true + hide_complexity: true + indicators: true + output: both + thresholds: '95 97' + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: matrix.ruby == '2.7' && github.event_name == 'pull_request' && always() + with: + recreate: true + path: code-coverage-results.md + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Coveralls + uses: coverallsapp/github-action@master + if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always() + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: ${{ matrix.experimental != 'false' }} + +# Using the codecov gem instead. +# - name: CodeCov +# uses: codecov/codecov-action@v2 +# if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always() +# with: +# files: ./coverage/coverage.xml +# flags: unittests +# name: codecov-upload +# fail_ci_if_error: true +# continue-on-error: ${{ matrix.experimental != 'false' }} diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 00000000..b8fdb277 --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,44 @@ +name: What's up Danger? + +on: + pull_request: + branches: + - 'main' + - '*-stable' + +jobs: + danger: + runs-on: ubuntu-latest + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + if: false + # if: github.event_name == 'pull_request' # if only run pull request when multiple trigger workflow + strategy: + fail-fast: false + matrix: + gemfile: + - f2 + rubygems: + - latest + bundler: + - latest + ruby: + - 2.7 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + - uses: MeilCli/danger-action@v5 + with: + plugins_file: 'Gemfile' + install_path: 'vendor/bundle' + danger_file: 'Dangerfile' + danger_id: 'danger-pr' + env: + DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} diff --git a/.github/workflows/heads.yml b/.github/workflows/heads.yml new file mode 100644 index 00000000..3cf68f50 --- /dev/null +++ b/.github/workflows/heads.yml @@ -0,0 +1,70 @@ +name: Heads + +on: + push: + branches: + - 'main' + - '*-maintenance' + - '*-dev' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }} + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + strategy: + fail-fast: false + matrix: + experimental: [true] + gemfile: + - f0 + - f1 + - f2 + rubygems: + - latest + bundler: + - latest + ruby: + - truffleruby+graalvm-head + - truffleruby-head + - jruby-head + - ruby-head + include: + # Includes a new variable experimental with a value of false + # for the matrix legs matching rubygems: latest, which is all of them. + # This is here for parity with the unsupported.yml + # This is a hack. Combined with continue-on-error it should allow us + # to have a workflow with allowed failure. + # This is the "supported" build matrix, so only the "head" builds are experimental here. + - rubygems: latest + experimental: true + + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + - name: Run tests + run: bundle exec rake test diff --git a/.github/workflows/macos-ancient.yml b/.github/workflows/macos-ancient.yml new file mode 100644 index 00000000..ee26cd1a --- /dev/null +++ b/.github/workflows/macos-ancient.yml @@ -0,0 +1,57 @@ +name: Old MacOS + +on: + push: + branches: + - 'main' + - '*-maintenance' + - '*-dev' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }} + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + if: false + # if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + strategy: + fail-fast: false + matrix: + experimental: [true] + gemfile: + - f0 + rubygems: + - "2.7.11" + ruby: + - "1.9" + - "2.0" + - "2.1" + - "2.2" + + runs-on: macos-10.15 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler-cache: true + - name: Run tests + run: bundle exec rake test diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 00000000..940f230c --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,60 @@ +name: MacOS + +on: + push: + branches: + - 'main' + - '*-maintenance' + - '*-dev' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }} + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + strategy: + fail-fast: false + matrix: + experimental: [true] + gemfile: + - f2 + rubygems: + - latest + bundler: + - latest + ruby: + - "2.7" + - "3.0" + - "3.1" + - truffleruby + - jruby + + runs-on: macos-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + - name: Run tests + run: bundle exec rake test diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 00000000..a489dccc --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,42 @@ +name: Code Style Checks + +on: + push: + branches: + - 'main' + - '*-maintenance' + - '*-dev' + - '*-stable' + 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: + experimental: [false] + rubygems: + - latest + bundler: + - latest + ruby: + - 2.7 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + - name: Run Rubocop + run: bundle exec rubocop -DES diff --git a/.github/workflows/supported.yml b/.github/workflows/supported.yml new file mode 100644 index 00000000..9fe394dd --- /dev/null +++ b/.github/workflows/supported.yml @@ -0,0 +1,62 @@ +name: Official Support + +on: + push: + branches: + - 'main' + - '*-maintenance' + - '*-dev' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }} + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + strategy: + fail-fast: false + matrix: + experimental: [false] + gemfile: + - f0 + - f1 + - f2 + rubygems: + - latest + bundler: + - latest + ruby: + - "2.7" + - "3.0" + - "3.1" + - truffleruby + - jruby + + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + - name: Run tests + run: bundle exec rake test diff --git a/.github/workflows/unsupported.yml b/.github/workflows/unsupported.yml new file mode 100644 index 00000000..53bc31e6 --- /dev/null +++ b/.github/workflows/unsupported.yml @@ -0,0 +1,70 @@ +name: Unofficial Support + +on: + push: + branches: + - 'main' + - '*-maintenance' + - '*-dev' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }} + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + strategy: + fail-fast: false + matrix: + experimental: [false] + gemfile: + - f0 + - f1 + - f2 + rubygems: + - latest + bundler: + - latest + ruby: + - "2.3" + - "2.4" + - "2.5" + - "2.6" + exclude: + - ruby: "2.3" + gemfile: "f1" + - ruby: "2.3" + gemfile: "f2" + - ruby: "2.4" + gemfile: "f2" + - ruby: "2.5" + gemfile: "f2" + + runs-on: ubuntu-20.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + - name: Run tests + run: bundle exec rake test diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 00000000..cd848566 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,59 @@ +name: Windows + +on: + push: + branches: + - 'main' + - '*-maintenance' + - '*-dev' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }} + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + strategy: + fail-fast: false + matrix: + experimental: [true] + gemfile: + - f2 + rubygems: + - latest + bundler: + - latest + ruby: + - "2.7" + - "3.0" + - "3.1" + - jruby + + runs-on: windows-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + - name: Run tests + run: bundle exec rake test 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..d83a5c0a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,20 +1,40 @@ -require: rubocop-rspec +inherit_gem: + rubocop-lts: rubocop-lts.yml + +require: + - 'rubocop-rspec' + - 'rubocop-thread_safety' + inherit_from: - .rubocop_todo.yml - .rubocop_rspec.yml + AllCops: DisplayCopNames: true # Display the name of the failing cops - TargetRubyVersion: 2.1 Exclude: - 'gemfiles/vendor/**/*' - 'vendor/**/*' - '**/.irbrc' -Gemspec/RequiredRubyVersion: - Enabled: false - -Metrics/BlockLength: - Enabled: false +#Metrics/BlockLength: +# IgnoredMethods: +# - context +# - describe +# - it +# - shared_context +# - shared_examples +# - shared_examples_for +# - namespace +# - draw + +#Gemspec/RequiredRubyVersion: +# Enabled: false + +Style/PercentLiteralDelimiters: + PreferredDelimiters: + default: '[]' + '%i': '()' + '%w': '[]' Metrics/BlockNesting: Max: 2 @@ -22,19 +42,16 @@ Metrics/BlockNesting: Metrics/LineLength: Enabled: false -Metrics/MethodLength: - Max: 15 - Metrics/ParameterLists: Max: 4 -Layout/AccessModifierIndentation: +Style/AccessModifierIndentation: EnforcedStyle: outdent -Layout/DotPosition: +Style/DotPosition: EnforcedStyle: trailing -Layout/SpaceInsideHashLiteralBraces: +Style/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space Lint/UnusedBlockArgument: @@ -44,13 +61,6 @@ Lint/UnusedBlockArgument: - 'vendor/**/*' - '**/.irbrc' -RSpec/DescribeClass: - Exclude: - - 'spec/examples/*' - -RSpec/NestedGroups: - Enabled: false - Style/ClassVars: Enabled: false @@ -67,14 +77,44 @@ Style/Documentation: Style/DoubleNegation: Enabled: false -Style/EmptyMethod: - EnforcedStyle: expanded +#Style/EmptyMethod: +# EnforcedStyle: expanded Style/Encoding: Enabled: false -Style/TrailingCommaInArrayLiteral: +Style/TrailingCommaInLiteral: EnforcedStyleForMultiline: comma -Style/TrailingCommaInHashLiteral: - EnforcedStyleForMultiline: comma +#Style/TrailingCommaInArrayLiteral: +# EnforcedStyleForMultiline: comma +# +#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 + +# Turn back on once old Rubies are dropped +Style/IfUnlessModifier: + Enabled: false + +#Style/SafeNavigation: +# Enabled: false diff --git a/.rubocop_rspec.yml b/.rubocop_rspec.yml index 347777dc..b8ab6fa1 100644 --- a/.rubocop_rspec.yml +++ b/.rubocop_rspec.yml @@ -1,3 +1,7 @@ +RSpec/DescribeClass: + Exclude: + - 'spec/examples/*' + RSpec/FilePath: Enabled: false @@ -24,3 +28,6 @@ RSpec/NestedGroups: RSpec/ExpectInHook: Enabled: false + +RSpec/MultipleMemoizedHelpers: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e7701734..64def32c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,15 +1,29 @@ -Style/HashSyntax: - EnforcedStyle: hash_rockets +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2022-05-03 08:04:37 +0700 using RuboCop version 0.41.2. +# 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 +Lint/UselessAssignment: + Exclude: + - 'spec/oauth2/client_spec.rb' -Style/SymbolArray: - Enabled: false +# Offense count: 4 +Metrics/AbcSize: + Max: 39 -Style/EachWithObject: - Enabled: false +# Offense count: 1 +Metrics/CyclomaticComplexity: + Max: 11 -# Once we drop Rubies that lack support for __dir__ we can turn this on. -Style/ExpandPathArguments: - Enabled: false +# Offense count: 8 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 37 + +# Offense count: 1 +Metrics/PerceivedComplexity: + Max: 14 diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 68b3a4cd..00000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -1.9.3-p551 diff --git a/.simplecov b/.simplecov new file mode 100644 index 00000000..77f4b3f3 --- /dev/null +++ b/.simplecov @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# To get coverage +# On Local, default (HTML) output, it just works, coverage is turned on: +# bundle exec rspec spec +# On Local, all output formats: +# COVER_ALL=true bundle exec rspec spec +# +# On CI, all output formats, the ENV variables CI is always set, +# and COVER_ALL, and CI_CODECOV, are set in the coverage.yml workflow only, +# so coverage only runs in that workflow, and outputs all formats. +# +if RUN_COVERAGE + SimpleCov.start do + enable_coverage :branch + primary_coverage :branch + + if ENV['COVER_ALL'] + require 'codecov' + require 'simplecov-lcov' + require 'simplecov-cobertura' + + SimpleCov::Formatter::LcovFormatter.config do |c| + c.report_with_single_file = true + c.single_report_path = 'coverage/lcov.info' + end + + SimpleCov.formatters = [ + SimpleCov::Formatter::CoberturaFormatter, + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::LcovFormatter, + SimpleCov::Formatter::Codecov, + ] + else + formatter SimpleCov::Formatter::HTMLFormatter + end + + add_filter '/spec' + minimum_coverage(85) + end +end diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 79b25172..00000000 --- a/.travis.yml +++ /dev/null @@ -1,68 +0,0 @@ -before_install: - # rubygems 2.7.8 and greater include bundler - # - Ruby 2.2, and under, get RubyGems ~> 2.7.10, (includes bundler 1.17.3) - # - Anything else, including Ruby 2.3, and above, gets RubyGems ~> 3, and update bundler to latest - # - NOTE ON JRUBY: identifies as RUBY_VERSION ~> 1.9, 2.0, 2.3, or 2.5. - # - NOTE ON TRUFFLERUBY: identifies as RUBY_VERSION ~> 2.6 - - | - rv="$(ruby -e 'STDOUT.write RUBY_VERSION')" - echo "Discovered Ruby Version of =====> $rv" - if [ "$rv" \< "2.3" ]; then - gem update --system 2.7.10 - elif [ "$rv" \< "2.4" ]; then - gem update --system 2.7.10 --no-document - elif [ "$rv" = "2.5.3" ]; then - # JRUBY 9.2 Identifies as 2.5.3, and it fails to update rubygems - gem install --no-document bundler "bundler:>=2.0" - else - gem update --system --no-document --conservative - gem install --no-document bundler "bundler:>=2.0" - fi - -bundler_args: --no-deployment --jobs 3 --retry 3 - -cache: bundler - -language: ruby - -matrix: - allow_failures: - - rvm: jruby-head - - rvm: ruby-head - - rvm: truffleruby - - rvm: jruby-9.0 # targets MRI v2.0 - gemfile: gemfiles/jruby_9.0.gemfile - fast_finish: true - include: -# - rvm: jruby-1.7 # targets MRI v1.9 -# gemfile: gemfiles/jruby_1.7.gemfile - - rvm: 1.9 - gemfile: gemfiles/ruby_1.9.gemfile - - rvm: 2.0 - gemfile: gemfiles/ruby_2.0.gemfile - - rvm: 2.1 - gemfile: gemfiles/ruby_2.1.gemfile - # DEPRECATION WARNING - # 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 - - 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 - -sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index fa957410..e445e555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,67 @@ -# Change Log +# Changelog All notable changes to this project will be documented in this file. ## [unreleased] -- no changes yet +## [1.4.11] - 2022-09-16 +- Complete migration to main branch as default (@pboling) +- Complete migration to Gitlab, updating all links, and references in VCS-managed files (@pboling) + +## [1.4.10] - 2022-07-01 +- FIPS Compatibility [#587](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/587) (@akostadinov) + +## [1.4.9] - 2022-02-20 +- Fixes compatibility with Faraday v2 [572](https://gitlab.com/oauth-xx/oauth2/-/issues/572) +- Includes supported versions of Faraday in test matrix: + - Faraday ~> 2.2.0 with Ruby >= 2.6 + - Faraday ~> 1.10 with Ruby >= 2.4 + - Faraday ~> 0.17.3 with Ruby >= 1.9 +- Add Windows and MacOS to test matrix + +## [1.4.8] - 2022-02-18 +- MFA is now required to push new gem versions (@pboling) +- README overhaul w/ new Ruby Version and Engine compatibility policies (@pboling) +- [#569](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/569) Backport fixes ([#561](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/561) by @ryogift), and add more fixes, to allow faraday 1.x and 2.x (@jrochkind) +- Improve Code Coverage tracking (Coveralls, CodeCov, CodeClimate), and enable branch coverage (@pboling) +- Add CodeQL, Security Policy, Funding info (@pboling) +- Added Ruby 3.1, jruby, jruby-head, truffleruby, truffleruby-head to build matrix (@pboling) +- [#543](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/543) - Support for more modern Open SSL libraries (@pboling) + +## [1.4.7] - 2021-03-19 +- [#541](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/541) - Backport fix to expires_at handling [#533](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/533) to 1-4-stable branch. (@dobon) + +## [1.4.6] - 2021-03-19 +- [#540](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/540) - Add VERSION constant (@pboling) +- [#537](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/537) - Fix crash in OAuth2::Client#get_token (@anderscarling) +- [#538](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/538) - Remove reliance on globally included OAuth2 in tests, analogous to [#539](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/539) on main branch (@anderscarling) + +## [1.4.5] - 2021-03-18 +- [#535](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/535) - Compatibility with range of supported Ruby OpenSSL versions, Rubocop updates, Github Actions, analogous to [#536](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/536) on main branch (@pboling) +- [#518](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/518) - Add extract_access_token option to OAuth2::Client (@jonspalmer) +- [#507](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/507) - Fix camel case content type, response keys (@anvox) +- [#500](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/500) - Fix YARD documentation formatting (@olleolleolle) + +## [1.4.4] - 2020-02-12 +- [#408](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/408) - Fixed expires_at for formatted time (@Lomey) + +## [1.4.3] - 2020-01-29 +- [#483](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/483) - add project metadata to gemspec (@orien) +- [#495](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/495) - support additional types of access token requests (@SteveyblamFreeagent, @thomcorley, @dgholz) + - Adds support for private_key_jwt and tls_client_auth +- [#433](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/433) - allow field names with square brackets and numbers in params (@asm256) + +## [1.4.2] - 2019-10-01 +- [#478](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/478) - support latest version of faraday & fix build (@pboling) + - Officially support Ruby 2.6 and truffleruby ## [1.4.1] - 2018-10-13 - -- [#417](https://github.com/oauth-xx/oauth2/pull/417) - update jwt dependency (@thewoolleyman) -- [#419](https://github.com/oauth-xx/oauth2/pull/419) - remove rubocop dependency (temporary, added back in [#423](https://github.com/oauth-xx/oauth2/pull/423)) (@pboling) -- [#418](https://github.com/oauth-xx/oauth2/pull/418) - update faraday dependency (@pboling) -- [#420](https://github.com/oauth-xx/oauth2/pull/420) - update [oauth2.gemspec](https://github.com/oauth-xx/oauth2/blob/1-4-stable/oauth2.gemspec) (@pboling) -- [#421](https://github.com/oauth-xx/oauth2/pull/421) - fix [CHANGELOG.md](https://github.com/oauth-xx/oauth2/blob/1-4-stable/CHANGELOG.md) for previous releases (@pboling) -- [#422](https://github.com/oauth-xx/oauth2/pull/422) - update [LICENSE](https://github.com/oauth-xx/oauth2/blob/1-4-stable/LICENSE) and [README.md](https://github.com/oauth-xx/oauth2/blob/1-4-stable/README.md) (@pboling) -- [#423](https://github.com/oauth-xx/oauth2/pull/423) - update [builds](https://travis-ci.org/oauth-xx/oauth2/builds), [Rakefile](https://github.com/oauth-xx/oauth2/blob/1-4-stable/Rakefile) (@pboling) +- [#417](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/417) - update jwt dependency (@thewoolleyman) +- [#419](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/419) - remove rubocop dependency (temporary, added back in [#423](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/423)) (@pboling) +- [#418](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/418) - update faraday dependency (@pboling) +- [#420](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/420) - update [oauth2.gemspec](https://gitlab.com/oauth-xx/oauth2/-/blob/1-4-stable/oauth2.gemspec) (@pboling) +- [#421](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/421) - fix [CHANGELOG.md](https://gitlab.com/oauth-xx/oauth2/-/blob/1-4-stable/CHANGELOG.md) for previous releases (@pboling) +- [#422](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/422) - update [LICENSE](https://gitlab.com/oauth-xx/oauth2/-/blob/1-4-stable/LICENSE) and [README.md](https://gitlab.com/oauth-xx/oauth2/-/blob/1-4-stable/README.md) (@pboling) +- [#423](https://gitlab.com/oauth-xx/oauth2/-/merge_requests/423) - update [builds](https://travis-ci.org/oauth-xx/oauth2/builds), [Rakefile](https://gitlab.com/oauth-xx/oauth2/-/blob/1-4-stable/Rakefile) (@pboling) - officially document supported Rubies * Ruby 1.9.3 * Ruby 2.0.0 @@ -33,19 +81,16 @@ All notable changes to this project will be documented in this file. [jruby-9.2]: https://www.jruby.org/2018/05/24/jruby-9-2-0-0.html ## [1.4.0] - 2017-06-09 - - Drop Ruby 1.8.7 support (@sferik) - Fix some RuboCop offenses (@sferik) - _Dependency_: Remove Yardstick (@sferik) - _Dependency_: Upgrade Faraday to 0.12 (@sferik) ## [1.3.1] - 2017-03-03 - - Add support for Ruby 2.4.0 (@pschambacher) - _Dependency_: Upgrade Faraday to Faraday 0.11 (@mcfiredrill, @rhymes, @pschambacher) ## [1.3.0] - 2016-12-28 - - Add support for header-based authentication to the `Client` so it can be used across the library (@bjeanes) - Default to header-based authentication when getting a token from an authorisation code (@maletor) - **Breaking**: Allow an `auth_scheme` (`:basic_auth` or `:request_body`) to be set on the client, defaulting to `:request_body` to maintain backwards compatibility (@maletor, @bjeanes) @@ -55,32 +100,27 @@ All notable changes to this project will be documented in this file. - Add support for Faraday 0.10 (@rhymes) ## [1.2.0] - 2016-07-01 - - Properly handle encoding of error responses (so we don't blow up, for example, when Google's response includes a ∞) (@Motoshi-Nishihira) - Make a copy of the options hash in `AccessToken#from_hash` to avoid accidental mutations (@Linuus) - Use `raise` rather than `fail` to throw exceptions (@sferik) ## [1.1.0] - 2016-01-30 - - Various refactors (eliminating `Hash#merge!` usage in `AccessToken#refresh!`, use `yield` instead of `#call`, freezing mutable objects in constants, replacing constants with class variables) (@sferik) - Add support for Rack 2, and bump various other dependencies (@sferik) ## [1.0.0] - 2014-07-09 - ### Added - Add an implementation of the MAC token spec. - ### Fixed - Fix Base64.strict_encode64 incompatibility with Ruby 1.8.7. ## [0.5.0] - 2011-07-29 - ### Changed -- [breaking] `oauth_token` renamed to `oauth_bearer`. -- [breaking] `authorize_path` Client option renamed to `authorize_url`. -- [breaking] `access_token_path` Client option renamed to `token_url`. -- [breaking] `access_token_method` Client option renamed to `token_method`. -- [breaking] `web_server` renamed to `auth_code`. +- *breaking* `oauth_token` renamed to `oauth_bearer`. +- *breaking* `authorize_path` Client option renamed to `authorize_url`. +- *breaking* `access_token_path` Client option renamed to `token_url`. +- *breaking* `access_token_method` Client option renamed to `token_method`. +- *breaking* `web_server` renamed to `auth_code`. ## [0.4.1] - 2011-04-20 @@ -108,32 +148,41 @@ All notable changes to this project will be documented in this file. ## [0.0.4] + [0.0.3] + [0.0.2] + [0.0.1] - 2010-04-22 - -[0.0.1]: https://github.com/oauth-xx/oauth2/compare/311d9f4...v0.0.1 -[0.0.2]: https://github.com/oauth-xx/oauth2/compare/v0.0.1...v0.0.2 -[0.0.3]: https://github.com/oauth-xx/oauth2/compare/v0.0.2...v0.0.3 -[0.0.4]: https://github.com/oauth-xx/oauth2/compare/v0.0.3...v0.0.4 -[0.0.5]: https://github.com/oauth-xx/oauth2/compare/v0.0.4...v0.0.5 -[0.0.6]: https://github.com/oauth-xx/oauth2/compare/v0.0.5...v0.0.6 -[0.0.7]: https://github.com/oauth-xx/oauth2/compare/v0.0.6...v0.0.7 -[0.0.8]: https://github.com/oauth-xx/oauth2/compare/v0.0.7...v0.0.8 -[0.0.9]: https://github.com/oauth-xx/oauth2/compare/v0.0.8...v0.0.9 -[0.0.10]: https://github.com/oauth-xx/oauth2/compare/v0.0.9...v0.0.10 -[0.0.11]: https://github.com/oauth-xx/oauth2/compare/v0.0.10...v0.0.11 -[0.0.12]: https://github.com/oauth-xx/oauth2/compare/v0.0.11...v0.0.12 -[0.0.13]: https://github.com/oauth-xx/oauth2/compare/v0.0.12...v0.0.13 -[0.1.0]: https://github.com/oauth-xx/oauth2/compare/v0.0.13...v0.1.0 -[0.1.1]: https://github.com/oauth-xx/oauth2/compare/v0.1.0...v0.1.1 -[0.2.0]: https://github.com/oauth-xx/oauth2/compare/v0.1.1...v0.2.0 -[0.3.0]: https://github.com/oauth-xx/oauth2/compare/v0.2.0...v0.3.0 -[0.4.0]: https://github.com/oauth-xx/oauth2/compare/v0.3.0...v0.4.0 -[0.4.1]: https://github.com/oauth-xx/oauth2/compare/v0.4.0...v0.4.1 -[0.5.0]: https://github.com/oauth-xx/oauth2/compare/v0.4.1...v0.5.0 -[1.0.0]: https://github.com/oauth-xx/oauth2/compare/v0.9.4...v1.0.0 -[1.1.0]: https://github.com/oauth-xx/oauth2/compare/v1.0.0...v1.1.0 -[1.2.0]: https://github.com/oauth-xx/oauth2/compare/v1.1.0...v1.2.0 -[1.3.0]: https://github.com/oauth-xx/oauth2/compare/v1.2.0...v1.3.0 -[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 -[unreleased]: https://github.com/oauth-xx/oauth2/compare/v1.4.1...HEAD +[0.0.1]: https://gitlab.com/oauth-xx/oauth2/-/compare/311d9f4...v0.0.1 +[0.0.2]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.1...v0.0.2 +[0.0.3]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.2...v0.0.3 +[0.0.4]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.3...v0.0.4 +[0.0.5]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.4...v0.0.5 +[0.0.6]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.5...v0.0.6 +[0.0.7]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.6...v0.0.7 +[0.0.8]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.7...v0.0.8 +[0.0.9]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.8...v0.0.9 +[0.0.10]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.9...v0.0.10 +[0.0.11]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.10...v0.0.11 +[0.0.12]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.11...v0.0.12 +[0.0.13]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.12...v0.0.13 +[0.1.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.0.13...v0.1.0 +[0.1.1]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.1.0...v0.1.1 +[0.2.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.1.1...v0.2.0 +[0.3.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.2.0...v0.3.0 +[0.4.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.3.0...v0.4.0 +[0.4.1]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.4.0...v0.4.1 +[0.5.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.4.1...v0.5.0 +[1.0.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v0.9.4...v1.0.0 +[1.1.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.0.0...v1.1.0 +[1.2.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.1.0...v1.2.0 +[1.3.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.2.0...v1.3.0 +[1.3.1]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.3.0...v1.3.1 +[1.4.0]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.3.1...v1.4.0 +[1.4.1]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.0...v1.4.1 +[1.4.2]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.1...v1.4.2 +[1.4.3]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.2...v1.4.3 +[1.4.4]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.3...v1.4.4 +[1.4.5]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.4...v1.4.5 +[1.4.6]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.5...v1.4.6 +[1.4.7]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.6...v1.4.7 +[1.4.8]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.7...v1.4.8 +[1.4.9]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.8...v1.4.9 +[1.4.10]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.9...v1.4.10 +[1.4.11]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.10...v1.4.11 +[unreleased]: https://gitlab.com/oauth-xx/oauth2/-/compare/v1.4.11...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/CONTRIBUTING.md b/CONTRIBUTING.md index 61aa667f..a0a0933b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,12 @@ -## Submitting a Pull Request +## Contributing + +Bug reports and pull requests are welcome on GitHub at [https://gitlab.com/oauth-xx/oauth2][source] +. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to +the [code of conduct][conduct]. + +To submit a patch, please fork the project and create a patch with tests. Once you're happy with it send a pull request! + +## Detailed instructions on Submitting a Pull Request 1. [Fork the repository.][fork] 2. [Create a topic branch.][branch] 3. Add specs for your unimplemented feature or bug fix. @@ -16,3 +24,21 @@ [fork]: http://help.github.com/fork-a-repo/ [branch]: http://learn.github.com/p/branching.html [pr]: http://help.github.com/send-pull-requests/ + +## Contributors + +[![Contributors](https://contrib.rocks/image?repo=oauth-xx/oauth2)][contributors] + +Made with [contributors-img][contrib-rocks]. + +[comment]: <> (Following links are used by README, CONTRIBUTING) + +[conduct]: https://gitlab.com/oauth-xx/oauth2/-/blob/main/CODE_OF_CONDUCT.md + +[contrib-rocks]: https://contrib.rocks + +[contributors]: https://gitlab.com/oauth-xx/oauth2/-/graphs/main + +[comment]: <> (Following links are used by README, CONTRIBUTING, Homepage) + +[source]: https://gitlab.com/oauth-xx/oauth2 diff --git a/Dangerfile b/Dangerfile new file mode 100644 index 00000000..2f8600bb --- /dev/null +++ b/Dangerfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Ideas... +# 1. Check for hashtags in PR title, and disallow changes to changelog? +# e.g. github.pr_title.include? "#trivial" + +# Make it more obvious that a PR is a work in progress and shouldn't be merged yet +warn('PR is classed as Work in Progress') if github.pr_title.include? '[WIP]' + +# Warn when there is a big PR +warn('Big PR') if git.lines_of_code > 500 + +# Don't let testing shortcuts get into main by accident +raise('fdescribe left in tests') if `grep -r fdescribe specs/ `.length > 1 +raise('fit left in tests') if `grep -r fit specs/ `.length > 1 diff --git a/Gemfile b/Gemfile index 0dbce159..dfade746 100644 --- a/Gemfile +++ b/Gemfile @@ -1,40 +1,44 @@ +# 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' +gem 'overcommit' 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 - end - gem 'pry', '~> 0.11' if ruby_version >= Gem::Version.new('2.0') - - 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 'addressable', '>= 2.3' gem 'rspec', '>= 3' - gem 'simplecov', '>= 0.9' - - platforms :jruby_18, :ruby_18 do - gem 'mime-types', '~> 1.25' - gem 'rest-client', '~> 1.6.0' - end - - platforms :ruby_18, :ruby_19 do - gem 'json', '< 2.0' - gem 'term-ansicolor', '< 1.4.0' - gem 'tins', '< 1.7' + platforms :mri do + ruby_version = Gem::Version.new(RUBY_VERSION) + minimum_version = ->(version) { ruby_version >= Gem::Version.new(version) && RUBY_ENGINE == 'ruby' } + linting = minimum_version.call('2.7') + coverage = minimum_version.call('2.7') + debug = minimum_version.call('2.5') + if linting + gem 'rubocop-rspec', '1.5.0', :require => false + gem 'rubocop-thread_safety', '0.3.1', :require => false + end + if coverage + gem 'codecov', :require => false, :group => :test + gem 'simplecov', '~> 0.21', :require => false + gem 'simplecov-cobertura' # XML for Jenkins + gem 'simplecov-lcov', '~> 0.8', :require => false + end + if debug + # No need to run byebug / pry on earlier versions + gem 'byebug' + gem 'pry' + gem 'pry-byebug' + end end end -gemspec +### deps for documentation and rdoc.info +group :documentation do + gem 'github-markup', :platform => :mri + gem 'redcarpet', :platform => :mri + gem 'yard', :require => false +end diff --git a/LICENSE b/LICENSE index 6dda5ca1..5fd4bd3c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Copyright (c) 2011 - 2013 Michael Bleigh and Intridea, Inc. -Copyright (c) 2017 - 2018 oauth-xx organization, https://github.com/oauth-xx +Copyright (c) 2017 - 2022 oauth-xx organization, https://gitlab.com/oauth-xx Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 77054ded..6b78b998 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,295 @@ -# OAuth2 - -[![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] -[![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] -[![Chat](https://img.shields.io/gitter/room/oauth-xx/oauth2.svg)](https://gitter.im/oauth-xx/oauth2) -[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)][source-license] -[![Documentation](http://inch-ci.org/github/oauth-xx/oauth2.png)][inch-ci] - -[gem]: https://rubygems.org/gems/oauth2 -[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 -[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 - -A Ruby wrapper for the [OAuth 2.0 specification][oauth2-spec]. +

+ + OAuth 2.0 Logo by Chris Messina, CC BY-SA 3.0 + + + Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5 + +

+ +## What + +OAuth 2.0 is the industry-standard protocol for authorization. +OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, +desktop applications, mobile phones, and living room devices. +This is a RubyGem for implementing OAuth 2.0 clients (not servers) in Ruby applications. +See the sibling `oauth` gem for OAuth 1.0 implementations in Ruby. + +⚠️⚠️⚠️ **_WARNING_**: You are viewing the `README` of the +[supported-only-for-critical-enterprise-security-issues](#oauth2-for-enterprise) `1-4-stable` +branch. Please do not use this, and instead upgrade to version 2! ⚠️⚠️⚠️ + +No further releases of 1.x series are planned! [Version 2](https://gitlab.com/oauth-xx/oauth2/#what-is-new-for-v20) has *tons* of improvements! + +If you must continue using 1.4.x please consider purchasing an open source security maintenance contract from [Tidelift][tidelift-ref]. + +--- + +* [OAuth 2.0 Spec][oauth2-spec] +* [OAuth 1.0 sibling gem][sibling-gem] [oauth2-spec]: https://oauth.net/2/ +[sibling-gem]: https://gitlab.com/oauth-xx/oauth + +## Release Documentation + +
+ 1.4.x Readmes + +| Version | Release Date | Readme | +|---------|--------------|-------------------------------------------------------------| +| 1.4.11 | Sep 16, 2022 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.11/README.md | +| 1.4.10 | Jul 1, 2022 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.10/README.md | +| 1.4.9 | Feb 20, 2022 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.9/README.md | +| 1.4.8 | Feb 18, 2022 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.8/README.md | +| 1.4.7 | Mar 19, 2021 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.7/README.md | +| 1.4.6 | Mar 19, 2021 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.6/README.md | +| 1.4.5 | Mar 18, 2021 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.5/README.md | +| 1.4.4 | Feb 12, 2020 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.4/README.md | +| 1.4.3 | Jan 29, 2020 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.3/README.md | +| 1.4.2 | Oct 1, 2019 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.2/README.md | +| 1.4.1 | Oct 13, 2018 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.1/README.md | +| 1.4.0 | Jun 9, 2017 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.4.0/README.md | +
+ +
+ 1.3.x Readmes + +| Version | Release Date | Readme | +|----------|--------------|----------------------------------------------------------| +| 1.3.1 | Mar 3, 2017 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.3.1/README.md | +| 1.3.0 | Dec 27, 2016 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.3.0/README.md | +
+ +
+ ≤= 1.2.x Readmes (2016 and before) + +| Version | Release Date | Readme | +|----------|--------------|----------------------------------------------------------| +| 1.2.0 | Jun 30, 2016 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.2.0/README.md | +| 1.1.0 | Jan 30, 2016 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.1.0/README.md | +| 1.0.0 | May 23, 2014 | https://gitlab.com/oauth-xx/oauth2/-/blob/v1.0.0/README.md | +| < 1.0.0 | Find here | https://gitlab.com/oauth-xx/oauth2/-/tags | +
+ +## Status + + + +| | Project | bundle add oauth2 | +|:----|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 1️⃣ | name, license, docs | [![RubyGems.org][⛳️name-img]][⛳️gem] [![License: MIT][🖇src-license-img]][🖇src-license] [![FOSSA][🏘fossa-img]][🏘fossa] [![RubyDoc.info][🚎yard-img]][🚎yard] [![InchCI][🖐inch-ci-img]][🚎yard] | +| 2️⃣ | version & activity | [![Gem Version][⛳️version-img]][⛳️gem] [![Total Downloads][🖇DL-total-img]][⛳️gem] [![Download Rank][🏘DL-rank-img]][⛳️gem] [![Source Code][🚎src-home-img]][🚎src-home] | +| 3️⃣ | maintanence & linting | [![Maintainability][⛳cclim-maint-img♻️]][⛳cclim-maint] [![Helpers][🖇triage-help-img]][🖇triage-help] [![Depfu][🏘depfu-img♻️]][🏘depfu♻️] [![Contributors][🚎contributors-img]][🚎contributors] [![Style][🖐style-wf-img]][🖐style-wf] [![Kloc Roll][🧮kloc-img]][🧮kloc] | +| 4️⃣ | testing | [![Supported][🏘sup-wf-img]][🏘sup-wf] [![Heads][🚎heads-wf-img]][🚎heads-wf] [![Unofficial Support][🖐uns-wf-img]][🖐uns-wf] [![MacOS][🧮mac-wf-img]][🧮mac-wf] [![Windows][📗win-wf-img]][📗win-wf] | +| 5️⃣ | coverage & security | [![CodeClimate][⛳cclim-cov-img♻️]][⛳cclim-cov] [![CodeCov][🖇codecov-img♻️]][🖇codecov] [![Coveralls][🏘coveralls-img]][🏘coveralls] [![Security Policy][🚎sec-pol-img]][🚎sec-pol] [![CodeQL][🖐codeQL-img]][🖐codeQL] [![Code Coverage][🧮cov-wf-img]][🧮cov-wf] | +| 6️⃣ | resources | [![Discussion][⛳gg-discussions-img]][⛳gg-discussions] [![Get help on Codementor][🖇codementor-img]][🖇codementor] [![Chat][🏘chat-img]][🏘chat] [![Blog][🚎blog-img]][🚎blog] [![Blog][🖐wiki-img]][🖐wiki] | +| 7️⃣ | spread 💖 | [![Liberapay Patrons][⛳liberapay-img]][⛳liberapay] [![Sponsor Me][🖇sponsor-img]][🖇sponsor] [![Tweet @ Peter][🏘tweet-img]][🏘tweet] [🌏][aboutme] [👼][angelme] [💻][coderme] | + + + + +[⛳️gem]: https://rubygems.org/gems/oauth2 +[⛳️name-img]: https://img.shields.io/badge/name-oauth2-brightgreen.svg?style=flat +[🖇src-license]: https://opensource.org/licenses/MIT +[🖇src-license-img]: https://img.shields.io/badge/License-MIT-green.svg +[🏘fossa]: https://app.fossa.io/projects/git%2Bgithub.amrom.workers.dev%2Foauth-xx%2Foauth2?ref=badge_shield +[🏘fossa-img]: https://app.fossa.io/api/projects/git%2Bgithub.amrom.workers.dev%2Foauth-xx%2Foauth2.svg?type=shield +[🚎yard]: https://www.rubydoc.info/github/oauth-xx/oauth2 +[🚎yard-img]: https://img.shields.io/badge/documentation-rubydoc-brightgreen.svg?style=flat +[🖐inch-ci-img]: http://inch-ci.org/github/oauth-xx/oauth2.png + + +[⛳️version-img]: http://img.shields.io/gem/v/oauth2.svg +[🖇DL-total-img]: https://img.shields.io/gem/dt/oauth2.svg +[🏘DL-rank-img]: https://img.shields.io/gem/rt/oauth2.svg +[🚎src-home]: https://gitlab.com/oauth-xx/oauth2/ +[🚎src-home-img]: https://img.shields.io/badge/source-gitlab-blue.svg?style=flat + + +[⛳cclim-maint]: https://codeclimate.com/github/oauth-xx/oauth2/maintainability +[⛳cclim-maint-img♻️]: https://api.codeclimate.com/v1/badges/688c612528ff90a46955/maintainability +[🖇triage-help]: https://www.codetriage.com/oauth-xx/oauth2 +[🖇triage-help-img]: https://www.codetriage.com/oauth-xx/oauth2/badges/users.svg +[🏘depfu♻️]: https://depfu.com/github/oauth-xx/oauth2?project_id=4445 +[🏘depfu-img♻️]: https://badges.depfu.com/badges/6d34dc1ba682bbdf9ae2a97848241743/count.svg +[🚎contributors]: https://gitlab.com/oauth-xx/oauth2/-/graphs/main +[🚎contributors-img]: https://img.shields.io/github/contributors-anon/oauth-xx/oauth2 +[🖐style-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/style.yml +[🖐style-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/style.yml/badge.svg +[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ +[🧮kloc-img]: https://img.shields.io/tokei/lines/github.com/oauth-xx/oauth2 + + +[🏘sup-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/supported.yml +[🏘sup-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/supported.yml/badge.svg +[🚎heads-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/heads.yml +[🚎heads-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/heads.yml/badge.svg +[🖐uns-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/unsupported.yml +[🖐uns-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/unsupported.yml/badge.svg +[🧮mac-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/macos.yml +[🧮mac-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/macos.yml/badge.svg +[📗win-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/windows.yml +[📗win-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/windows.yml/badge.svg + + +[⛳cclim-cov]: https://codeclimate.com/github/oauth-xx/oauth2/test_coverage +[⛳cclim-cov-img♻️]: https://api.codeclimate.com/v1/badges/688c612528ff90a46955/test_coverage +[🖇codecov-img♻️]: https://codecov.io/gh/oauth-xx/oauth2/branch/1-4-stable/graph/badge.svg?token=bNqSzNiuo2 +[🖇codecov]: https://codecov.io/gh/oauth-xx/oauth2 +[🏘coveralls]: https://coveralls.io/github/oauth-xx/oauth2?branch=1-4-stable +[🏘coveralls-img]: https://coveralls.io/repos/github/oauth-xx/oauth2/badge.svg?branch=1-4-stable +[🚎sec-pol]: https://gitlab.com/oauth-xx/oauth2/-/blob/main/SECURITY.md +[🚎sec-pol-img]: https://img.shields.io/badge/security-policy-brightgreen.svg?style=flat +[🖐codeQL]: https://github.com/oauth-xx/oauth2/security/code-scanning +[🖐codeQL-img]: https://github.com/oauth-xx/oauth2/actions/workflows/codeql-analysis.yml/badge.svg +[🧮cov-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/coverage.yml +[🧮cov-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/coverage.yml/badge.svg + + +[⛳gg-discussions]: https://groups.google.com/g/oauth-ruby +[⛳gg-discussions-img]: https://img.shields.io/badge/google-group-purple.svg?style=flat +[🖇codementor]: https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github +[🖇codementor-img]: https://cdn.codementor.io/badges/get_help_github.svg +[🏘chat]: https://gitter.im/oauth-xx/oauth2 +[🏘chat-img]: https://img.shields.io/gitter/room/oauth-xx/oauth2.svg +[🚎blog]: http://www.railsbling.com/tags/oauth2/ +[🚎blog-img]: https://img.shields.io/badge/blog-railsbling-brightgreen.svg?style=flat +[🖐wiki]: https://gitlab.com/oauth-xx/oauth2/-/wikis/home +[🖐wiki-img]: https://img.shields.io/badge/wiki-examples-brightgreen.svg?style=flat + + +[⛳liberapay-img]: https://img.shields.io/liberapay/patrons/pboling.svg?logo=liberapay +[⛳liberapay]: https://liberapay.com/pboling/donate +[🖇sponsor-img]: https://img.shields.io/badge/sponsor-pboling.svg?style=social&logo=github +[🖇sponsor]: https://github.com/sponsors/pboling +[🏘tweet-img]: https://img.shields.io/twitter/follow/galtzo.svg?style=social&label=Follow +[🏘tweet]: http://twitter.com/galtzo + + +[railsbling]: http://www.railsbling.com +[peterboling]: http://www.peterboling.com +[aboutme]: https://about.me/peter.boling +[angelme]: https://angel.co/peter-boling +[coderme]:http://coderwall.com/pboling ## Installation -Add this line to your application's Gemfile: +Install the gem and add to the application's Gemfile by executing: -```ruby -gem 'oauth2' -``` + $ bundle add oauth2 -And then execute: +If bundler is not being used to manage dependencies, install the gem by executing: - $ bundle + $ gem install oauth2 -Or install it yourself as: +## OAuth2 for Enterprise - $ gem install oauth2 +Available as part of the Tidelift Subscription. + +The maintainers of OAuth2 and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. [Learn more.][tidelift-ref] + +[tidelift-ref]: https://tidelift.com/subscription/pkg/rubygems-oauth2?utm_source=rubygems-oauth2&utm_medium=referral&utm_campaign=enterprise + +## Security contact information + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. + +For more see [SECURITY.md][🚎sec-pol]. + +## Why should you upgrade to version v2.0? + +- Officially support Ruby versions >= 2.7 +- Unofficially support Ruby versions >= 2.5 +- Incidentally support Ruby versions >= 2.2 +- Drop support for the expired MAC Draft (all versions) +- Support IETF rfc7523 JWT Bearer Tokens +- Support IETF rfc7231 Relative Location in Redirect +- Support IETF rfc6749 Don't set oauth params when nil +- Support [OIDC 1.0 Private Key JWT](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication); based on the OAuth JWT assertion specification [(RFC 7523)](https://tools.ietf.org/html/rfc7523) +- Support new formats, including from [jsonapi.org](http://jsonapi.org/format/): `application/vdn.api+json`, `application/vnd.collection+json`, `application/hal+json`, `application/problem+json` +- Adds new option to `OAuth2::Client#get_token`: + - `:access_token_class` (`AccessToken`); user specified class to use for all calls to `get_token` +- Adds new option to `OAuth2::AccessToken#initialize`: + - `:expires_latency` (`nil`); number of seconds by which AccessToken validity will be reduced to offset latency +- By default, keys are transformed to camel case. + - Original keys will still work as previously, in most scenarios, thanks to `rash_alt` gem. + - However, this is a _breaking_ change if you rely on `response.parsed.to_h`, as the keys in the result will be camel case. + - As of version 2.0.4 you can turn key transformation off with the `snaky: false` option. +- By default, the `:auth_scheme` is now `:basic_auth` (instead of `:request_body`) + - Third-party strategies and gems may need to be updated if a provider was requiring client id/secret in the request body +- [... A lot more](https://gitlab.com/oauth-xx/oauth2/-/blob/main/CHANGELOG.md#2.0.0) + +## Compatibility -## Resources +Targeted ruby compatibility is 2.7, 3.0 and 3.1. Compatibility is further distinguished by +supported and unsupported versions of Ruby. +This gem will work with Ruby versions back to 1.9, though it remains unsupported. +Ruby is limited to 1.9+ in the gemspec for the 1.4.x series and is be 2.2+ for next major version releases (see `master` branch). -* [View Source on GitHub][code] -* [Report Issues on GitHub][issues] -* [Read More at the Wiki][wiki] +
+ Ruby Engine Compatibility Policy -[code]: https://github.com/oauth-xx/oauth2 -[issues]: https://github.com/oauth-xx/oauth2/issues -[wiki]: https://github.com/oauth-xx/oauth2/wiki +This gem is tested against MRI, JRuby, and Truffleruby. +Each of those has varying versions that target a specific version of MRI Ruby. +This gem should work in the just-listed Ruby engines according to the targeted MRI compatibility in the table below. +If you would like to add support for additional engines, + see `gemfiles/README.md`, then submit a PR to the correct maintenance branch as according to the table below. +
+ +
+ Ruby Version Compatibility Policy + +If something doesn't work on one of these interpreters, it's a bug. + +This library may inadvertently work (or seem to work) on other Ruby +implementations, however support will only be provided for the versions listed +above. + +If you would like this library to support another Ruby version, you may +volunteer to be a maintainer. Being a maintainer entails making sure all tests +run and pass on that implementation. When something breaks on your +implementation, you will be responsible for providing patches in a timely +fashion. If critical issues for a particular implementation exist at the time +of a major release, support for that Ruby version may be dropped. +
+ +| | Ruby OAuth2 Version | Maintenance Branch | Supported Officially | Supported Unofficially | Supported Incidentally | +|:----|---------------------|--------------------|-------------------------|------------------------|------------------------| +| 1️⃣ | 2.0.x | `main` | 2.7, 3.0, 3.1 | 2.5, 2.6 | 2.2, 2.3, 2.4 | +| 2️⃣ | 1.4.x | `1-4-stable` | 2.5, 2.6, 2.7, 3.0, 3.1 | 2.1, 2.2, 2.3, 2.4 | 1.9, 2.0 | +| 3️⃣ | older | N/A | Best of luck to you! | Please upgrade! | | + +NOTE: The 1.4 series will only receive critical security updates. +See [SECURITY.md][🚎sec-pol] ## Usage Examples @@ -61,16 +301,42 @@ 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 ``` + +
+ Debugging + +Set an environment variable, however you would [normally do that](https://github.com/bkeepers/dotenv). + +```ruby +# will log both request and response, including bodies +ENV['OAUTH_DEBUG'] = 'true' +``` + +By default, debug output will go to `$stdout`. This can be overridden when +initializing your OAuth2::Client. + +```ruby +require 'oauth2' +client = OAuth2::Client.new( + 'client_id', + 'client_secret', + :site => 'https://example.org', + :logger => Logger.new('example.log', 'weekly') +) +``` +
+ ## OAuth2::Response -The AccessToken methods #get, #post, #put and #delete and the generic #request + +The `AccessToken` methods `#get`, `#post`, `#put` and `#delete` and the generic `#request` will return an instance of the #OAuth2::Response class. -This instance contains a #parsed method that will parse the response body and -return a Hash if the Content-Type is application/x-www-form-urlencoded or if +This instance contains a `#parsed` method that will parse the response body and +return a Hash if the `Content-Type` is `application/x-www-form-urlencoded` or if the body is a JSON object. It will return an Array if the body is a JSON array. Otherwise, it will return the original body string. @@ -78,26 +344,29 @@ 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). +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 -always contain the OAuth2::Response instance. -If you do not want an error to be raised, you may use :raise_errors => false -option on initialization of the client. In this case the OAuth2::Response +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 +always contain the `OAuth2::Response` instance. + +If you do not want an error to be raised, you may use `:raise_errors => false` +option on initialization of the client. In this case the `OAuth2::Response` instance will be returned as usual and on 400+ status code responses, the -Response instance will contain the OAuth2::Error instance. +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. +use. They are available via the `#auth_code`, `#implicit`, `#password`, `#client_credentials`, and `#assertion` methods respectively. ```ruby auth_url = client.auth_code.authorize_url(:redirect_uri => 'http://localhost:8080/oauth/callback') @@ -121,51 +390,9 @@ request, add a 'headers' hash under 'params': token = client.auth_code.get_token('code_value', :redirect_uri => 'http://localhost:8080/oauth/callback', :headers => {'Some' => 'Header'}) ``` -You can always use the #request method on the OAuth2::Client instance to make +You can always use the `#request` method on the `OAuth2::Client` instance to make requests for tokens for any Authentication grant type. -## Supported Ruby Versions - -This library aims to support and is [tested against][travis] the following Ruby -implementations: - -### Rubies with support ending at Oauth2 2.x - -* Ruby 1.9.3 -* Ruby 2.0.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) - -[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 - -If something doesn't work on one of these interpreters, it's a bug. - -This library may inadvertently work (or seem to work) on other Ruby -implementations, however support will only be provided for the versions listed -above. - -If you would like this library to support another Ruby version, you may -volunteer to be a maintainer. Being a maintainer entails making sure all tests -run and pass on that implementation. When something breaks on your -implementation, you will be responsible for providing patches in a timely -fashion. If critical issues for a particular implementation exist at the time -of a major release, support for that Ruby version may be dropped. - ## Versioning This library aims to adhere to [Semantic Versioning 2.0.0][semver]. @@ -189,16 +416,16 @@ spec.add_dependency 'oauth2', '~> 1.4' ## License -[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)][source-license] +[![License: MIT][🖇src-license-img]][🖇src-license] - Copyright (c) 2011-2013 Michael Bleigh and Intridea, Inc. -- Copyright (c) 2017-2018 [oauth-xx organization][oauth-xx] +- Copyright (c) 2017-2022 [oauth-xx organization][oauth-xx] - See [LICENSE][license] for details. [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.amrom.workers.dev%2Foauth-xx%2Foauth2.svg?type=large)][fossa2] -[license]: LICENSE -[oauth-xx]: https://github.com/oauth-xx +[license]: https://gitlab.com/oauth-xx/oauth2/-/blob/main/LICENSE +[oauth-xx]: https://gitlab.com/oauth-xx [fossa2]: https://app.fossa.io/projects/git%2Bgithub.amrom.workers.dev%2Foauth-xx%2Foauth2?ref=badge_large ## Development @@ -209,8 +436,16 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/oauth-xx/oauth2. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. +See [CONTRIBUTING.md][contributing] + +[contributing]: https://gitlab.com/oauth-xx/oauth2/-/blob/main/CONTRIBUTING.md + +## Contributors + +[![Contributors](https://contrib.rocks/image?repo=oauth-xx/oauth2)]("https://gitlab.com/oauth-xx/oauth2/-/graphs/main") + +Made with [contributors-img](https://contrib.rocks). ## Code of Conduct -Everyone interacting in the OAuth2 project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/oauth-xx/oauth2/blob/master/CODE_OF_CONDUCT.md). +Everyone interacting in the OAuth2 project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://gitlab.com/oauth-xx/oauth2/-/blob/main/CODE_OF_CONDUCT.md). diff --git a/Rakefile b/Rakefile index 1066a1b6..f8c96f3a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,15 +1,10 @@ # encoding: utf-8 +# frozen_string_literal: true # !/usr/bin/env rake require 'bundler/gem_tasks' -begin - require 'wwtd/tasks' -rescue LoadError - puts 'failed to load wwtd' -end - begin require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) @@ -31,15 +26,15 @@ rescue LoadError end end -namespace :doc do - require 'rdoc/task' - require File.expand_path('../lib/oauth2/version', __FILE__) - RDoc::Task.new do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = "oauth2 #{OAuth2::Version}" - rdoc.main = 'README.md' - rdoc.rdoc_files.include('README.md', 'LICENSE.md', 'lib/**/*.rb') - end -end +# namespace :doc do +# require 'rdoc/task' +# require 'oauth2/version' +# RDoc::Task.new do |rdoc| +# rdoc.rdoc_dir = 'rdoc' +# rdoc.title = "oauth2 #{OAuth2::Version}" +# rdoc.main = 'README.md' +# rdoc.rdoc_files.include('README.md', 'LICENSE.md', 'lib/**/*.rb') +# end +# end task :default => [:test, :rubocop] diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..274337d2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | EOL | Post-EOL / Enterprise | +|----------|-----------|---------|---------------------------------------| +| 2.latest | ✅ | 04/2024 | [Tidelift Subscription][tidelift-ref] | +| 1.latest | ✅ | 04/2023 | [Tidelift Subscription][tidelift-ref] | +| <= 1 | ⛔ | ⛔ | ⛔ | + +### EOL Policy + +Non-commercial support for the oldest version of Ruby (which itself is going EOL) will be dropped each year in April. + +## Reporting a Vulnerability + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. + +## OAuth2 for Enterprise + +Available as part of the Tidelift Subscription. + +The maintainers of oauth2 and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. [Learn more.][tidelift-ref] + +[tidelift-ref]: https://tidelift.com/subscription/pkg/rubygems-oauth2?utm_source=rubygems-oauth2&utm_medium=referral&utm_campaign=enterprise&utm_term=repo diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000..fe8b5051 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,116 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__) + end + + def env_var_version + ENV['BUNDLER_VERSION'] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update` + + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + bundler_version = a if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + + bundler_version = Regexp.last_match(1) + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV['BUNDLE_GEMFILE'] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path('../Gemfile', __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem.rubygems_version < Gem::Version.new('2.7.0') + + requirement += '.a' if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV['BUNDLE_GEMFILE'] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem 'bundler', bundler_requirement + end + return if gem_error.nil? + + require_error = activation_error_handling do + require 'bundler/version' + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script? diff --git a/bin/console b/bin/console new file mode 100755 index 00000000..d8fb16d0 --- /dev/null +++ b/bin/console @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'oauth2' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. +require 'byebug' if ENV['DEBUG'] == 'true' + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require 'irb' +IRB.start(__FILE__) diff --git a/bin/rake b/bin/rake new file mode 100755 index 00000000..5f615c2a --- /dev/null +++ b/bin/rake @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rake' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('rake', 'rake') diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 00000000..d3f4959a --- /dev/null +++ b/bin/rspec @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('rspec-core', 'rspec') diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 00000000..cc105e8d --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rubocop' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('rubocop', 'rubocop') diff --git a/bin/setup b/bin/setup new file mode 100755 index 00000000..dce67d86 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/docs/images/logo/README.txt b/docs/images/logo/README.txt new file mode 100644 index 00000000..bb405554 --- /dev/null +++ b/docs/images/logo/README.txt @@ -0,0 +1,15 @@ +The OAuth 2.0 Logo - oauth2-logo-124px.png (resized) + +https://oauth.net/about/credits/ + +The OAuth logo was designed by Chris Messina. + +--- + +The Ruby Logo - ruby-logo-124px.jpeg (resized) + +https://www.ruby-lang.org/en/about/logo/ + +Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5 + +https://creativecommons.org/licenses/by-sa/2.5 diff --git a/docs/images/logo/oauth2-logo-124px.png b/docs/images/logo/oauth2-logo-124px.png new file mode 100644 index 00000000..41a8d35a Binary files /dev/null and b/docs/images/logo/oauth2-logo-124px.png differ diff --git a/docs/images/logo/ruby-logo-198px.svg b/docs/images/logo/ruby-logo-198px.svg new file mode 100644 index 00000000..59cf324f --- /dev/null +++ b/docs/images/logo/ruby-logo-198px.svg @@ -0,0 +1,948 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gemfiles/README.md b/gemfiles/README.md new file mode 100644 index 00000000..1ac3a713 --- /dev/null +++ b/gemfiles/README.md @@ -0,0 +1,106 @@ +# History + +`faraday` v0.17.3 is the first version that stops using `&Proc.new` for block forwarding, + and thus is the oldest version oauth2 is compatible with. + +```ruby +gem 'faraday', ['>= 0.17.3', '< 3.0'] +``` + +# Ruby + +We use the Github Action `ruby/setup-ruby@master` to install Ruby, and it has a matrix of +[supported versions](https://github.com/ruby/setup-ruby/blob/master/README.md#supported-versions) (copied below). + +| Interpreter | Versions | +|-----------------------|------------------------------------------------------------------------------------------| +| `ruby` | 1.9.3, 2.0.0, 2.1.9, 2.2, all versions from 2.3.0 until 3.1.1, head, debug, mingw, mswin | +| `jruby` | 9.1.17.0 - 9.3.3.0, head | +| `truffleruby` | 19.3.0 - 22.0.0, head | +| `truffleruby+graalvm` | 21.2.0 - 22.0.0, head | + +In the naming of gemfiles, we will use the below shorthand for interpreter, +and version. Platforms will be represented without modification. + +| Interpreter | Shorthand | +|-----------------------|-----------| +| `ruby` | r | +| `jruby` | jr | +| `truffleruby` | tr | +| `truffleruby+graalvm` | trg | + +Building onto that, we can add the MRI target spec, +since that's what all Rubygems use for minimum version compatibility. + +| Interpreter + Version | MRI spec | Shorthand | +|----------------------------|----------|------------| +| ruby-1.9.3 | 1.9 | r1_9 | +| ruby-2.0.0 | 2.0 | r2_0 | +| ruby-2.1.9 | 2.1 | r2_1 | +| ruby-2.2.x | 2.2 | r2_2 | +| ruby-2.3.x | 2.3 | r2_3 | +| ruby-2.4.x | 2.4 | r2_4 | +| ruby-2.5.x | 2.5 | r2_5 | +| ruby-2.6.x | 2.6 | r2_6 | +| ruby-2.7.x | 2.7 | r2_7 | +| ruby-3.0.x | 3.0 | r3_0 | +| ruby-3.1.x | 3.1 | r3_1 | +| ruby-head | 3.2 | rH3_2 | +| ruby-mingw | (?) | rmin | +| ruby-mswin | (?) | rMS | +| jruby-9.1.x.x | 2.3 | jr9_1-r2_3 | +| jruby-9.2.x.x | 2.5 | jr9_2-r2_5 | +| jruby-9.3.x.x | 2.6 | jr9_3-r2_6 | +| jruby-head | 2.7 | jrH-r2_7 | +| truffleruby-19.3.x | 2.5(?) | tr19-r2_5 | +| truffleruby-20.x.x | 2.6.5 | tr20-r2_6 | +| truffleruby-21.x.x | 2.7.4 | tr21-r2_7 | +| truffleruby-22.x.x | 3.0.2 | tr22-r3_0 | +| truffleruby-head | 3.1(?) | trH-r3_1 | +| truffleruby+graalvm-21.2.x | 2.7.4 | trg21-r2_7 | +| truffleruby+graalvm-22.x.x | 3.0.2 | trg22-r3_0 | +| truffleruby+graalvm-head | 3.1(?) | trgH-r3_1 | + +We will run tests on as many of these as possible, in a matrix with each supported major version of `faraday`, +which means 0.17.3+ (as `f0`), 1.10.x (as `f1`), 2.2.x (as `f2`). + +Discrete versions of `faraday` to test against, as of 2022.02.19, with minimum version of Ruby for each: + +* 2.2.0, Ruby >= 2.6 +* 1.10.0, Ruby >= 2.4 +* 0.17.4, Ruby >= 1.9 + +❌ - Incompatible +✅ - Official Support +🚧 - Unofficial Support +🤡 - Incidental Compatibility +🙈 - Unknown Compatibility + +| Shorthand | f0 - 0.17.3+ | f1 - 1.10.x | f2 - 2.2.x | +|------------|------------------|------------------|-----------------| +| r1_9 | 🤡 f0-r1_9 | ❌ | ❌ | +| r2_0 | 🤡 f0-r2_0 | ❌ | ❌ | +| r2_1 | 🤡 f0-r2_1 | ❌ | ❌ | +| r2_2 | 🤡 f0-r2_2 | ❌ | ❌ | +| r2_3 | 🚧 f0-r2_3 | ❌ | ❌ | +| r2_4 | 🚧 f0-r2_4 | 🚧 f1-r2_4 | ❌ | +| r2_5 | 🚧 f0-r2_5 | 🚧 f1-r2_5 | ❌ | +| r2_6 | 🚧 f0-r2_6 | 🚧 f1-r2_6 | 🚧 f2-r2_6 | +| r2_7 | ✅ f0-r2_7 | ✅ f1-r2_7 | ✅ f2-r2_7 | +| r3_0 | ✅ f0-r3_0 | ✅ f1-r3_0 | ✅ f2-r3_0 | +| r3_1 | ✅ f0-r3_1 | ✅ f1-r3_1 | ✅ f2-r3_1 | +| rH3_2 | 🚧 f0-rH3_2 | 🚧 f1-rH3_2 | 🚧 f2-rH3_2 | +| rmin | 🙈 f0-rmin | 🙈 f1-rmin | 🙈 f2-rmin | +| rMS | 🙈 f0-rMS | 🙈 f1-rMS | 🙈 f2-rMS | +| jr9_1-r2_3 | 🚧 f0-jr9_1-r2_3 | ❌ | ❌ | +| jr9_2-r2_5 | 🚧 f0-jr9_2-r2_5 | 🚧 f1-jr9_2-r2_5 | ❌ | +| jr9_3-r2_6 | ✅ f0-jr9_3-r2_6 | ✅ f1-jr9_3-r2_6 | ✅ f2-jr9_3-r2_6 | +| jrH-r2_7 | 🚧 f0-jrH-r2_7 | 🚧 f1-jrH-r2_7 | 🚧 f2-jrH-r2_7 | +| tr19-r2_5 | 🚧 f0-tr19-r2_5 | 🚧 f1-tr19-r2_5 | ❌ | +| tr20-r2_6 | 🚧 f0-tr20-r2_6 | 🚧 f1-tr20-r2_6 | 🚧 f2-tr20-r2_6 | +| tr21-r2_7 | ✅ f0-tr21-r2_7 | ✅ f1-tr21-r2_7 | ✅ f2-tr21-r2_7 | +| tr22-r3_0 | ✅ f0-tr22-r3_0 | ✅ f1-tr22-r3_0 | ✅ f2-tr22-r3_0 | +| trH-r3_1 | 🚧 f0-trH-r3_1 | 🚧 f1-trH-r3_1 | 🚧 f2-trH-r3_1 | +| trg21-r2_7 | ✅ f0-trg21-r2_7 | ✅ f1-trg21-r2_7 | ✅ f2-trg21-r2_7 | +| trg22-r3_0 | ✅ f0-trg22-r3_0 | ✅ f1-trg22-r3_0 | ✅ f2-trg22-r3_0 | +| trgH-r3_1 | 🚧 f0-trgH-r3_1 | 🚧 f1-trgH-r3_1 | 🚧 f2-trgH-r3_1 | diff --git a/gemfiles/f0.gemfile b/gemfiles/f0.gemfile new file mode 100644 index 00000000..67b7e01f --- /dev/null +++ b/gemfiles/f0.gemfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# See README.md in this directory + +# 0.17.3 is the first version that stops using &Proc.new for block forwarding, +# and thus is the oldest version oauth2 is compatible with. +gem 'faraday', '~> 0.17.4' + +gemspec :path => '../' diff --git a/gemfiles/f1.gemfile b/gemfiles/f1.gemfile new file mode 100644 index 00000000..f048505b --- /dev/null +++ b/gemfiles/f1.gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# See README.md in this directory + +gem 'faraday', '~> 1.10' + +gemspec :path => '../' diff --git a/gemfiles/f2.gemfile b/gemfiles/f2.gemfile new file mode 100644 index 00000000..4724ca55 --- /dev/null +++ b/gemfiles/f2.gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# See README.md in this directory + +gem 'faraday', '~> 2.2' + +gemspec :path => '../' diff --git a/gemfiles/jruby_1.7.gemfile b/gemfiles/jruby_1.7.gemfile index bbef7523..5d23c8b9 100644 --- a/gemfiles/jruby_1.7.gemfile +++ b/gemfiles/jruby_1.7.gemfile @@ -1,10 +1,12 @@ +# frozen_string_literal: true + source 'https://rubygems.org' 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..1a2afca9 100644 --- a/gemfiles/jruby_9.0.gemfile +++ b/gemfiles/jruby_9.0.gemfile @@ -1,7 +1,9 @@ +# frozen_string_literal: true + 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/jruby_9.1.gemfile b/gemfiles/jruby_9.1.gemfile index a02c547f..c7770283 100644 --- a/gemfiles/jruby_9.1.gemfile +++ b/gemfiles/jruby_9.1.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec :path => '../' diff --git a/gemfiles/jruby_9.2.gemfile b/gemfiles/jruby_9.2.gemfile index a02c547f..c7770283 100644 --- a/gemfiles/jruby_9.2.gemfile +++ b/gemfiles/jruby_9.2.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec :path => '../' diff --git a/gemfiles/jruby_head.gemfile b/gemfiles/jruby_head.gemfile index a02c547f..c7770283 100644 --- a/gemfiles/jruby_head.gemfile +++ b/gemfiles/jruby_head.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec :path => '../' diff --git a/gemfiles/ruby_1.9.gemfile b/gemfiles/ruby_1.9.gemfile index bbef7523..5d23c8b9 100644 --- a/gemfiles/ruby_1.9.gemfile +++ b/gemfiles/ruby_1.9.gemfile @@ -1,10 +1,12 @@ +# frozen_string_literal: true + source 'https://rubygems.org' 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.0.gemfile b/gemfiles/ruby_2.0.gemfile index 87a679f6..cc675c47 100644 --- a/gemfiles/ruby_2.0.gemfile +++ b/gemfiles/ruby_2.0.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gem 'faraday', '~> 0.15.4' 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.2.gemfile b/gemfiles/ruby_2.2.gemfile deleted file mode 100644 index a02c547f..00000000 --- a/gemfiles/ruby_2.2.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..03b6a53c 100644 --- a/gemfiles/ruby_head.gemfile +++ b/gemfiles/ruby_head.gemfile @@ -1,8 +1,10 @@ +# frozen_string_literal: true + source 'https://rubygems.org' group :development do - gem 'pry' gem 'byebug' + gem 'pry' gem 'pry-byebug' end diff --git a/gemfiles/ruby_2.3.gemfile b/gemfiles/truffleruby.gemfile similarity index 63% rename from gemfiles/ruby_2.3.gemfile rename to gemfiles/truffleruby.gemfile index a02c547f..c7770283 100644 --- a/gemfiles/ruby_2.3.gemfile +++ b/gemfiles/truffleruby.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec :path => '../' diff --git a/lib/oauth2.rb b/lib/oauth2.rb index 5a7ed9fe..77a93f56 100644 --- a/lib/oauth2.rb +++ b/lib/oauth2.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'oauth2/error' require 'oauth2/authenticator' require 'oauth2/client' diff --git a/lib/oauth2/access_token.rb b/lib/oauth2/access_token.rb index 1b178390..c6568049 100644 --- a/lib/oauth2/access_token.rb +++ b/lib/oauth2/access_token.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + module OAuth2 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 # @@ -37,7 +40,7 @@ def from_kvform(client, kvform) # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the # Access Token value in :body or :query transmission mode - def initialize(client, token, opts = {}) # rubocop:disable Metrics/AbcSize + def initialize(client, token, opts = {}) @client = client @token = token.to_s opts = opts.dup @@ -46,11 +49,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 +84,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 +153,7 @@ def headers private - def configure_authentication!(opts) # rubocop:disable MethodLength, Metrics/AbcSize + def configure_authentication!(opts) case options[:mode] when :header opts[:headers] ||= {} @@ -169,5 +173,11 @@ def configure_authentication!(opts) # rubocop:disable MethodLength, Metrics/AbcS raise("invalid :mode option of #{options[:mode]}") end end + + def convert_expires_at(expires_at) + Time.iso8601(expires_at.to_s).to_i + rescue ArgumentError + expires_at.to_i + end end end diff --git a/lib/oauth2/authenticator.rb b/lib/oauth2/authenticator.rb index ce627920..34b696a3 100644 --- a/lib/oauth2/authenticator.rb +++ b/lib/oauth2/authenticator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'base64' module OAuth2 @@ -25,6 +27,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 +48,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) @@ -50,7 +62,7 @@ def apply_basic_auth(params) params.merge(:headers => headers) end - # @see https://tools.ietf.org/html/rfc2617#section-2 + # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2 def basic_auth_header {'Authorization' => self.class.encode_basic_auth(id, secret)} end diff --git a/lib/oauth2/client.rb b/lib/oauth2/client.rb index 356f93e7..2b9d5558 100644 --- a/lib/oauth2/client.rb +++ b/lib/oauth2/client.rb @@ -1,9 +1,14 @@ +# frozen_string_literal: true + require 'faraday' require 'logger' module OAuth2 + ConnectionError = Class.new(Faraday::ConnectionFailed) # 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 @@ -14,17 +19,18 @@ class Client # rubocop:disable Metrics/ClassLength # # @param [String] client_id the client_id value # @param [String] client_secret the client_secret value - # @param [Hash] opts the options to create the client with - # @option opts [String] :site the OAuth2 provider site host - # @option opts [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange - # @option opts [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint - # @option opts [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint - # @option opts [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post) - # @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 + # @param [Hash] options the options to create the client with + # @option options [String] :site the OAuth2 provider site host + # @option options [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange + # @option options [String] :authorize_url ('oauth/authorize') absolute or relative URL path to the Authorization endpoint + # @option options [String] :token_url ('oauth/token') absolute or relative URL path to the Token endpoint + # @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post) + # @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body) + # @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with + # @option options [FixNum] :max_redirects (5) maximum number of redirects to follow + # @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes + # @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled + # @option options [Proc] (DEPRECATED) :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,20 +38,22 @@ 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 => {}, + @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) + :max_redirects => 5, + :raise_errors => true, + :extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN, # DEPRECATED + :logger => ::Logger.new($stdout)}.merge(opts) @options[:connection_opts][:ssl] = ssl if ssl end # Set the site host # - # @param [String] the OAuth2 provider site host + # @param value [String] the OAuth2 provider site host def site=(value) @connection = nil @site = value @@ -53,15 +61,16 @@ def site=(value) # The Faraday connection object def connection - @connection ||= begin - conn = Faraday.new(site, options[:connection_opts]) - if options[:connection_build] - conn.build do |b| - options[:connection_build].call(b) + @connection ||= + Faraday.new(site, options[:connection_opts]) do |builder| + oauth_debug_logging(builder) + if options[:connection_build] + options[:connection_build].call(builder) + else + builder.request :url_encoded # form-encode POST params + builder.adapter Faraday.default_adapter # make requests with Net::HTTP end end - conn - end end # The authorize endpoint URL of the OAuth2 provider @@ -91,14 +100,18 @@ 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 - connection.response :logger, ::Logger.new($stdout) if ENV['OAUTH_DEBUG'] == 'true' - - url = connection.build_url(url, opts[:params]).to_s + def request(verb, url, opts = {}) # rubocop:disable Metrics/AbcSize + url = connection.build_url(url).to_s - response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req| - yield(req) if block_given? + begin + 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 + rescue Faraday::ConnectionFailed => e + raise ConnectionError, e end + response = Response.new(response, :parse => opts[:parse]) case response.status @@ -106,17 +119,25 @@ 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) end - request(verb, response.headers['location'], opts) + location = response.headers['location'] + if location + request(verb, location, opts) + else + error = Error.new(response) + raise(error, "Got #{response.status} status code, but no Location header was present") + end when 200..299, 300..399 # on non-redirecting 3xx statuses, just return the response response when 400..599 error = Error.new(response) raise(error) if opts.fetch(:raise_errors, options[:raise_errors]) + response.error = error response else @@ -127,54 +148,83 @@ def request(verb, url, opts = {}) # rubocop:disable CyclomaticComplexity, Method # Initializes an AccessToken by making a request to the token endpoint # - # @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 - params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params) + # @param params [Hash] a Hash of params for the token endpoint + # @param access_token_opts [Hash] access token options, to pass to the AccessToken object + # @param access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken + # @return [AccessToken] the initialized AccessToken + def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity 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.apply(params) opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)} headers = params.delete(:headers) || {} if options[:token_method] == :post - opts[:body] = params + opts[:body] = if headers['Content-Type'] == 'application/json' + params.to_json + else + params + end opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'} else opts[:params] = params opts[:headers] = {} 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']) + opts[:headers] = opts[:headers].merge(headers) + http_method = options[:token_method] + response = request(http_method, token_url, opts) + + access_token = begin + build_access_token(response, access_token_opts, extract_access_token) + rescue StandardError + nil + end + + response_contains_token = access_token || ( + response.parsed.is_a?(Hash) && + (response.parsed['access_token'] || response.parsed['id_token']) + ) + + if options[:raise_errors] && !response_contains_token error = Error.new(response) raise(error) + elsif !response_contains_token + return nil end - access_token_class.from_hash(self, response.parsed.merge(access_token_opts)) + + access_token end # The Authorization Code strategy # - # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1 + # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1 def auth_code @auth_code ||= OAuth2::Strategy::AuthCode.new(self) end # The Implicit strategy # - # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2 + # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2 def implicit @implicit ||= OAuth2::Strategy::Implicit.new(self) end # The Resource Owner Password Credentials strategy # - # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3 + # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3 def password @password ||= OAuth2::Strategy::Password.new(self) end # The Client Credentials strategy # - # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4 + # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4 def client_credentials @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self) end @@ -194,10 +244,10 @@ def assertion # # @api semipublic # - # @see https://tools.ietf.org/html/rfc6749#section-4.1 - # @see https://tools.ietf.org/html/rfc6749#section-4.1.3 - # @see https://tools.ietf.org/html/rfc6749#section-4.2.1 - # @see https://tools.ietf.org/html/rfc6749#section-10.6 + # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1 + # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 + # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1 + # @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6 # @return [Hash] the params to add to a request or URL def redirection_params if options[:redirect_uri] @@ -206,5 +256,41 @@ 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 + + # Returns the authenticator object + # + # @return [Authenticator] the initialized Authenticator + def authenticator + Authenticator.new(id, secret, options[:auth_scheme]) + end + + # Builds the access token from the response of the HTTP call + # + # @return [AccessToken] the initialized AccessToken + 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 AccessToken.form_hash pattern + # Will 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 + + def oauth_debug_logging(builder) + builder.response :logger, options[:logger], :bodies => true if ENV['OAUTH_DEBUG'] == 'true' + end end end diff --git a/lib/oauth2/error.rb b/lib/oauth2/error.rb index c2618a18..64f63292 100644 --- a/lib/oauth2/error.rb +++ b/lib/oauth2/error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OAuth2 class Error < StandardError attr_reader :response, :code, :description @@ -23,7 +25,7 @@ def initialize(response) def error_message(response_body, opts = {}) message = [] - opts[:error_description] && message << opts[:error_description] + opts[:error_description] && (message << opts[:error_description]) error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding) script_encoding = opts[:error_description].encoding diff --git a/lib/oauth2/mac_token.rb b/lib/oauth2/mac_token.rb index db7d4d77..3d82d0f0 100644 --- a/lib/oauth2/mac_token.rb +++ b/lib/oauth2/mac_token.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'base64' require 'digest' require 'openssl' @@ -60,7 +62,7 @@ def headers # @param [String] url the HTTP URL path of the request def header(verb, url) timestamp = Time.now.utc.to_i - nonce = Digest::MD5.hexdigest([timestamp, SecureRandom.hex].join(':')) + nonce = Digest::SHA256.hexdigest([timestamp, SecureRandom.hex].join(':')) uri = URI.parse(url) @@ -95,16 +97,22 @@ def signature(timestamp, nonce, verb, uri) # # @param [String] alg the algorithm to use (one of 'hmac-sha-1', 'hmac-sha-256') def algorithm=(alg) - @algorithm = begin - case alg.to_s - when 'hmac-sha-1' - OpenSSL::Digest::SHA1.new - when 'hmac-sha-256' - OpenSSL::Digest::SHA256.new - else - raise(ArgumentError, 'Unsupported algorithm') - end - end + @algorithm = case alg.to_s + when 'hmac-sha-1' + begin + OpenSSL::Digest('SHA1').new + rescue StandardError + OpenSSL::Digest.new('SHA1') + end + when 'hmac-sha-256' + begin + OpenSSL::Digest('SHA256').new + rescue StandardError + OpenSSL::Digest.new('SHA256') + end + else + raise(ArgumentError, 'Unsupported algorithm') + end end private diff --git a/lib/oauth2/response.rb b/lib/oauth2/response.rb index 13657fd9..9a4f5d5c 100644 --- a/lib/oauth2/response.rb +++ b/lib/oauth2/response.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'multi_json' require 'multi_xml' require 'rack' @@ -11,9 +13,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 +70,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 +82,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..2f3351aa 100644 --- a/lib/oauth2/strategy/assertion.rb +++ b/lib/oauth2/strategy/assertion.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require 'jwt' module OAuth2 module Strategy # The Client Assertion Strategy # - # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.3 + # @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-10#section-4.1.3 # # Sample usage: # client = OAuth2::Client.new(client_id, client_secret, @@ -50,10 +52,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/auth_code.rb b/lib/oauth2/strategy/auth_code.rb index 76f92aa9..b019ad95 100644 --- a/lib/oauth2/strategy/auth_code.rb +++ b/lib/oauth2/strategy/auth_code.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module OAuth2 module Strategy # The Authorization Code Strategy # - # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1 + # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1 class AuthCode < Base # The required query parameters for the authorize URL # diff --git a/lib/oauth2/strategy/base.rb b/lib/oauth2/strategy/base.rb index 9d16bb4a..801a723e 100644 --- a/lib/oauth2/strategy/base.rb +++ b/lib/oauth2/strategy/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OAuth2 module Strategy class Base diff --git a/lib/oauth2/strategy/client_credentials.rb b/lib/oauth2/strategy/client_credentials.rb index 35ac5fd8..778c4fa4 100644 --- a/lib/oauth2/strategy/client_credentials.rb +++ b/lib/oauth2/strategy/client_credentials.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module OAuth2 module Strategy # The Client Credentials Strategy # - # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4 + # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4 class ClientCredentials < Base # Not used for this strategy # diff --git a/lib/oauth2/strategy/implicit.rb b/lib/oauth2/strategy/implicit.rb index 6ab505db..00d3e4e9 100644 --- a/lib/oauth2/strategy/implicit.rb +++ b/lib/oauth2/strategy/implicit.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module OAuth2 module Strategy # The Implicit Strategy # - # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2 + # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2 class Implicit < Base # The required query parameters for the authorize URL # diff --git a/lib/oauth2/strategy/password.rb b/lib/oauth2/strategy/password.rb index 49bfc6e3..d41ca07a 100644 --- a/lib/oauth2/strategy/password.rb +++ b/lib/oauth2/strategy/password.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module OAuth2 module Strategy # The Resource Owner Password Credentials Authorization Strategy # - # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3 + # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3 class Password < Base # Not used for this strategy # @@ -18,8 +20,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..44885708 100644 --- a/lib/oauth2/version.rb +++ b/lib/oauth2/version.rb @@ -1,5 +1,9 @@ +# frozen_string_literal: true + module OAuth2 module Version + VERSION = to_s + module_function # The major version @@ -20,12 +24,12 @@ def minor # # @return [Integer] def patch - 1 + 11 end # The pre-release version, if any # - # @return [Integer, NilClass] + # @return [String, NilClass] def pre nil end @@ -53,7 +57,9 @@ def to_a # # @return [String] def to_s - to_a.join('.') + v = [major, minor, patch].compact.join('.') + v += "-#{pre}" if pre + v end end end diff --git a/maintenance-branch b/maintenance-branch new file mode 100644 index 00000000..88d050b1 --- /dev/null +++ b/maintenance-branch @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/oauth2.gemspec b/oauth2.gemspec index be52c417..01ebfa53 100644 --- a/oauth2.gemspec +++ b/oauth2.gemspec @@ -1,44 +1,76 @@ -# coding: utf-8 +# encoding: utf-8 +# frozen_string_literal: true lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'oauth2/version' Gem::Specification.new do |spec| - spec.add_dependency 'faraday', ['>= 0.8', '< 2.0'] + spec.add_dependency 'faraday', ['>= 0.17.3', '< 3.0'] spec.add_dependency 'jwt', ['>= 1.0', '< 3.0'] spec.add_dependency 'multi_json', '~> 1.3' spec.add_dependency 'multi_xml', '~> 0.5' - spec.add_dependency 'rack', ['>= 1.2', '< 3'] + spec.add_dependency 'rack', ['>= 1.2', '< 4'] - spec.authors = ['Peter Boling', 'Michael Bleigh', 'Erik Michaels-Ober'] + spec.authors = ['Peter Boling', 'Erik Michaels-Ober', 'Michael Bleigh'] spec.description = 'A Ruby wrapper for the OAuth 2.0 protocol built with a similar style to the original OAuth spec.' spec.email = ['peter.boling@gmail.com'] - spec.homepage = 'https://github.com/oauth-xx/oauth2' + spec.homepage = 'https://gitlab.com/oauth-xx/oauth2' spec.licenses = %w[MIT] spec.name = 'oauth2' 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.post_install_message = " +You have installed oauth2 version #{OAuth2::Version}, which is EOL. +No further support is anticipated for the 1.4.x series. + +OAuth2 version 2 is released. +There are BREAKING changes, but most will not encounter them, and upgrading should be easy! + +We have made two other major migrations: +1. master branch renamed to main +2. Github has been replaced with Gitlab + +Please see: +• https://gitlab.com/oauth-xx/oauth2#what-is-new-for-v20 +• https://gitlab.com/oauth-xx/oauth2/-/blob/main/CHANGELOG.md +• https://groups.google.com/g/oauth-ruby/c/QA_dtrXWXaE + +Please upgrade, report issues, and support the project! Thanks, |7eter l-|. l3oling + +" + + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = "#{spec.homepage}/-/tree/v#{spec.version}" + spec.metadata['changelog_uri'] = "#{spec.homepage}/-/blob/v#{spec.version}/CHANGELOG.md" + spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/-/issues" + spec.metadata['documentation_uri'] = "https://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" + spec.metadata['wiki_uri'] = "#{spec.homepage}/-/wiki" + spec.metadata['funding_uri'] = 'https://liberapay.com/pboling' + spec.metadata['rubygems_mfa_required'] = 'true' spec.require_paths = %w[lib] spec.bindir = 'exe' - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.files = `git ls-files -z`.split("\x0").reject do |f| - f.match(%r{^(bin|test|spec|features)/}) - end + spec.files = Dir[ + 'lib/**/*', + 'CHANGELOG.md', + 'CODE_OF_CONDUCT.md', + 'CONTRIBUTING.md', + 'LICENSE', + 'README.md', + 'SECURITY.md', + ] + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.add_development_dependency 'addressable', '~> 2.3' - spec.add_development_dependency 'backports', '~> 3.11' + spec.add_development_dependency 'addressable', '>= 2' spec.add_development_dependency 'bundler', '>= 1.16' - spec.add_development_dependency 'coveralls', '~> 0.8' - 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 'rake', '>= 12' + spec.add_development_dependency 'rexml', '>= 3' + spec.add_development_dependency 'rspec', '>= 3' 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 'rubocop-lts', ['>= 2.0.3', '~>2.0'] spec.add_development_dependency 'silent_stream' - spec.add_development_dependency 'wwtd' end diff --git a/spec/fixtures/README.md b/spec/fixtures/README.md new file mode 100644 index 00000000..e8ed536a --- /dev/null +++ b/spec/fixtures/README.md @@ -0,0 +1,11 @@ +# RS256 + +## How keys were made + +```shell +# No passphrase +# Generates the public and private keys: +ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key +# Converts the key to PEM format +openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub +``` diff --git a/spec/fixtures/RS256/jwtRS256.key b/spec/fixtures/RS256/jwtRS256.key new file mode 100644 index 00000000..72005e50 --- /dev/null +++ b/spec/fixtures/RS256/jwtRS256.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKwIBAAKCAgEA5hdXV/4YSymY1T9VNvK2bWRfulwIty1RnAPNINQmfh3aRRkV ++PNrbC2Crji9G0AHmQwgW1bZ3kgkkpIm6RVn44fHvBvuXkZ9ABgXw0d2cLIHmwOF +xSKmWAm/EW//GszUTLLLsMZUe2udtFJW0jxXB2GRY0WVYuo6Oo58RCeP719lw3Ag +s0YF9/IobxKkGd4BautUPw6ZszAa3o+j0zR74x7ouPxybZAOuPsMxqanyeYJeH4o +sJjLMYV9qem9uG2sj7GENJ8UszcpmGbqxBhexPEB7mgDeONIF0XJF23zdOf8ANE5 +mAU2h2v7M6moAfkdUzJ+j48+VT2omHAzAL5yNcmrl2xiWdyoxOw1Y1UmfEmJYV5V +gGYyZ12JZRKY+szPT+vR+MDuYxbquF40O7kvkFNBfL1yCpzfSQCLnEs4rX8qRzZX +ciLeyq4Ht5FLuRFgxjA//XI8LAmp0u7gk+Q7FUH1UgW3kmJDTG0XaxQxYTBSIO7m +cmyjDyBgKVuQmt5E1ycFeteOVdPD/CG/fPYhthvc4UytEFwsMdNy3iD6/wuUH68t +AKam28UZaOb0qK+00cQQD8fulY9rKtSL10LvJFWUOa/SJyLvk9vUmfvFn182il1n +X6GpyxyMmE/FCnH4CT/DjrSZf08mOO8eL5ofYHMK/oiXr1eODqx+pOwClNsCAwEA +AQKCAgEAy34vMFI4WBk04rx9d/hWoQ7Znu8QgjihaZLvEy6t0HJEfUH/bcqS4fyq +C72Aeh452gCgiUeZrf4t4jdCFHhrBg8q9dHaEiTTHocwVPPZ6zd4hH8sCrpnVYth +IWHkw2YOCLtEbFYrl3AI7Na5lHvrGEsREzQSN4Yh83Has0guAy1iyeNb+FFgq/XO +DtX0ri/rHw1717zo8FIGIXn2EK/lNWw7tIcICKAUdUMK/JGd6XD6RUeGYxDu/CAs +kF55/Sd6Kyd7XjKnUwzhS7kRvlYzUog4BgqVr4+LTZHZlFAYtfcJqAtinXFW1ZQJ +eZp9TSlt5wvMZNjx7t92QUNRyEGmrQAU+8COHnT0/drFf0MCiyHSUN0E7/5fswhc +uMSU9XiJA9G0wYvJl4zIuOuIYWZWhIqvjYSkvdlP70t9XO2gk/ZcCWsMW8i+xbwC +w1+MMjsKsNedXxI99TIPPHcCNMxqlt1E1kHH3SAwCuEH/ez7PRMyEQQ0EyAk22x/ +piYIWXkX5835cLbLRIYafXgOiugWZjCwIqfRIcIpscmcijZwCF2DyevveYdx3krR +FGA2PFydFyxCNG7XwvKb9kHb7WBERUPV/H3eCqu2SZ/RvF+I94LUYP4bu6CmFdO9 +wCJcGJoL1P7tVhS9lA5Oj0QWczrjnejCoI9XMMduWk032rR1VYECggEBAPZDnTBY +H2uiVmGdMfWTAmX86kiHVpkL03OG6rgvDMsMOYKnik9Lb3gNeUIuPeAWFNrXCoD1 +qp0loxPhKSojNOOM8Yiz/GwQ/QI9dzgtxs7E7rFFyTuJcY48Do8uOFyUHbAbeOBF +b9UL/uBfWZGVV1YY753xyqYlCpxTVQGms1jsbVFdZE1iVpOwAkFVuoLYaHLut4zB +01ORyBSoWan173P+IQH6F1uNXE2Kk/FIMDN6bgP1pXkdkrTx4WjAmRnP/Sc4r38/ +F1xN+gxnWGPUKDVRPYBpVzDR036w65ODgg2FROK2vIxlStiAC/rc0JLsvaWfb1Rn +dsWdJJ1V6mZ6a5sCggEBAO8wC1jcIoiBz3xoA8E5BSt8qLJ7ZuSFaaidvWX2/xj6 +lSWJxCGQfhR7P6ozvH6UDo1WbJT6nNyXPkiDkAzcmAdsYVjULW3K2LI9oPajaJxY +L7KJpylgh9JhMvbMz3VVjTgYRt+kjX+3uFMZNx1YfiBP+S6xx5sjK9CKDz3H99kC +q9bX95YFqZ7yFE3aBCR6CENo2tXpMN96CLQGpwa0bwt3xNzC4MhZMXbGR3DdBYbD +tS9lJfQvAVUYxbSE/2FBgjpO6ArMyU2ZUEDFx9J6IhfhVbQV4VeITMyRNo0XwBiQ +/+XpLXgHkw7LiNMIoc7d+M7yLA1Vz7+r8XxWHHZCL8ECggEBAPK8VrYORno7e1Wg +MlxS2WxZzTxMWmlkpLoc5END7SI/HHjSV5wtSORWs40uM0MrwMasa+gNPmzDamjv +6Tllln4ssO8EKe0DGcAZgefYBzxMFNKbbOzIXyvJurga4Ocv/8tUaOL2znJ67nGO +yqSbRYjR724JpKv7mufXo9SK0gD2mhI3MeSs55WPScnIjJzoXpva/QU7D+gxq7vg +7PCAP9RfS329W0Sco7yyuXx8oTY8mTBB8ybcpXzBZmNwY/hzcJ42W5XbRFVxbuTH +APL1beSP/UUTkCPIzuTz0mCGoaxeDjZB1Lu2I/4eyLAu80+/FneoHX5etU23xR1o +UDFOvb0CggEBALTTc6CoPAtLaBs7X6tSelAYHEli9bTKD8kEB83wX4b42ozYjEh7 +vnWpf8Yi+twO/rlnnws6NCCoztNvcxXmJ6FlFGtdbULV2eFWqjwL6ehY2yZ03sVv +Tv+DsE3ZJPYlyW+hGuO0uazWrilUpNAwuJmhHFdq2+azPkqYNVGVvhB37oWsHGd0 +vHmHtkXtDris8VZVDSwu8V3iGnZPmTJ+cn0O/OuRAPM2SyjqWdQ/pA/wIShFpd3n +M3CsG7uP2KokJloCkXaov39E6uEtJRZAc0nudyaAbC4Kw1Tca4tba0SnSm78S/20 +bD8BLN2uZvXH5nQ9rYQfXcIgMZ64UygsfYECggEBAIw0fQaIVmafa0Hz3ipD4PJI +5QNkh2t9hvOCSKm1xYTNATl0q/VIkZoy1WoxY6SSchcObLxQKbJ9ORi4XNr+IJK5 +3C1Qz/3iv/S3/ktgmqGhQiqybkkHZcbqTXB2wxrx+aaLS7PEfYiuYCrPbX93160k +MVns8PjvYU8KCNMbL2e+AiKEt1KkKAZIpNQdeeJOEhV9wuLYFosd400aYssuSOVW +IkJhGI0lT/7FDJaw0LV98DhQtauANPSUQKN5iw6vciwtsaF1kXMfGlMXj58ntiMq +NizQPR6/Ar1ewLPMh1exDoAfLnCIMk8nbSraW+cebLAZctPugUpfpu3j2LM98aE= +-----END RSA PRIVATE KEY----- diff --git a/spec/fixtures/RS256/jwtRS256.key.pub b/spec/fixtures/RS256/jwtRS256.key.pub new file mode 100644 index 00000000..1a2f63d1 --- /dev/null +++ b/spec/fixtures/RS256/jwtRS256.key.pub @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5hdXV/4YSymY1T9VNvK2 +bWRfulwIty1RnAPNINQmfh3aRRkV+PNrbC2Crji9G0AHmQwgW1bZ3kgkkpIm6RVn +44fHvBvuXkZ9ABgXw0d2cLIHmwOFxSKmWAm/EW//GszUTLLLsMZUe2udtFJW0jxX +B2GRY0WVYuo6Oo58RCeP719lw3Ags0YF9/IobxKkGd4BautUPw6ZszAa3o+j0zR7 +4x7ouPxybZAOuPsMxqanyeYJeH4osJjLMYV9qem9uG2sj7GENJ8UszcpmGbqxBhe +xPEB7mgDeONIF0XJF23zdOf8ANE5mAU2h2v7M6moAfkdUzJ+j48+VT2omHAzAL5y +Ncmrl2xiWdyoxOw1Y1UmfEmJYV5VgGYyZ12JZRKY+szPT+vR+MDuYxbquF40O7kv +kFNBfL1yCpzfSQCLnEs4rX8qRzZXciLeyq4Ht5FLuRFgxjA//XI8LAmp0u7gk+Q7 +FUH1UgW3kmJDTG0XaxQxYTBSIO7mcmyjDyBgKVuQmt5E1ycFeteOVdPD/CG/fPYh +thvc4UytEFwsMdNy3iD6/wuUH68tAKam28UZaOb0qK+00cQQD8fulY9rKtSL10Lv +JFWUOa/SJyLvk9vUmfvFn182il1nX6GpyxyMmE/FCnH4CT/DjrSZf08mOO8eL5of +YHMK/oiXr1eODqx+pOwClNsCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/spec/helper.rb b/spec/helper.rb index e3cf4805..03d97bcc 100644 --- a/spec/helper.rb +++ b/spec/helper.rb @@ -1,25 +1,21 @@ -DEBUG = ENV['DEBUG'] == 'true' +# frozen_string_literal: true -if RUBY_VERSION >= '1.9' - require 'simplecov' - require 'coveralls' +DEBUG = ENV['DEBUG'] == 'true' +RUN_COVERAGE = ENV['CI_CODECOV'] || ENV['CI'].nil? - SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter] +ruby_version = Gem::Version.new(RUBY_VERSION) +minimum_version = ->(version) { ruby_version >= Gem::Version.new(version) && RUBY_ENGINE == 'ruby' } +coverage = minimum_version.call('2.7') && RUN_COVERAGE +debug = minimum_version.call('2.5') && DEBUG - SimpleCov.start do - add_filter '/spec' - minimum_coverage(95) - end -end - -if DEBUG && RUBY_VERSION >= '2.6' - require 'byebug' -end +require 'simplecov' if coverage +require 'byebug' if debug require 'oauth2' require 'addressable/uri' require 'rspec' require 'rspec/stubbed_env' +require 'rspec/pending_for' require 'silent_stream' RSpec.configure do |config| @@ -30,11 +26,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..a205f443 100644 --- a/spec/oauth2/access_token_spec.rb +++ b/spec/oauth2/access_token_spec.rb @@ -1,17 +1,18 @@ -require 'helper' +# frozen_string_literal: true -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] } @@ -31,7 +32,7 @@ expect(target.params['foo']).to eq('bar') end - def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize + def assert_initialized_token(target) expect(target.token).to eq(token) expect(target).to be_expires expect(target.params.keys).to include('foo') @@ -51,7 +52,7 @@ def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize expect(hash).to eq(hash_before) end - it 'initalizes with a form-urlencoded key/value string' do + it 'initializes with a form-urlencoded key/value string' do kvform = "access_token=#{token}&expires_at=#{Time.now.to_i + 200}&foo=bar" target = described_class.from_kvform(client, kvform) assert_initialized_token(target) @@ -71,11 +72,33 @@ def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize expect(opts).to eq(opts_before) end - it 'initializes with a string expires_at' do - hash = {:access_token => token, :expires_at => '1361396829', 'foo' => 'bar'} - target = described_class.from_hash(client, hash) - assert_initialized_token(target) - expect(target.expires_at).to be_a(Integer) + describe 'expires_at' do + let(:expires_at) { 1_361_396_829 } + let(:hash) do + { + :access_token => token, + :expires_at => expires_at.to_s, + 'foo' => 'bar', + } + end + + it 'initializes with an integer timestamp expires_at' do + target = described_class.from_hash(client, hash.merge(:expires_at => expires_at)) + assert_initialized_token(target) + expect(target.expires_at).to eql(expires_at) + end + + it 'initializes with a string timestamp expires_at' do + target = described_class.from_hash(client, hash) + assert_initialized_token(target) + expect(target.expires_at).to eql(expires_at) + end + + it 'initializes with a string time expires_at' do + target = described_class.from_hash(client, hash.merge(:expires_at => Time.at(expires_at).iso8601)) + assert_initialized_token(target) + expect(target.expires_at).to eql(expires_at) + end end end @@ -101,6 +124,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 +143,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 +187,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..4448b98a 100644 --- a/spec/oauth2/authenticator_spec.rb +++ b/spec/oauth2/authenticator_spec.rb @@ -1,4 +1,4 @@ -require 'helper' +# frozen_string_literal: true describe OAuth2::Authenticator do subject do @@ -38,6 +38,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..a27cda83 100644 --- a/spec/oauth2/client_spec.rb +++ b/spec/oauth2/client_spec.rb @@ -1,11 +1,11 @@ # coding: utf-8 +# frozen_string_literal: true -require 'helper' require 'nkf' describe OAuth2::Client do subject do - described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder| + described_class.new('abc', 'def', {:site => 'https://api.example.com'}.merge(options)) do |builder| builder.adapter :test do |stub| stub.get('/success') { |env| [200, {'Content-Type' => 'text/awesome'}, 'yay'] } stub.get('/reflect') { |env| [200, {}, env[:body]] } @@ -13,6 +13,7 @@ stub.get('/unauthorized') { |env| [401, {'Content-Type' => 'application/json'}, MultiJson.encode(:error => error_value, :error_description => error_description_value)] } stub.get('/conflict') { |env| [409, {'Content-Type' => 'text/plain'}, 'not authorized'] } stub.get('/redirect') { |env| [302, {'Content-Type' => 'text/plain', 'location' => '/success'}, ''] } + stub.get('/redirect_no_loc') { |_env| [302, {'Content-Type' => 'text/plain'}, ''] } stub.post('/redirect') { |env| [303, {'Content-Type' => 'text/plain', 'location' => '/reflect'}, ''] } stub.get('/error') { |env| [500, {'Content-Type' => 'text/plain'}, 'unknown error'] } stub.get('/empty_get') { |env| [204, {}, nil] } @@ -24,6 +25,7 @@ let!(:error_value) { 'invalid_token' } let!(:error_description_value) { 'bad bad token' } + let(:options) { {} } describe '#initialize' do it 'assigns id and secret' do @@ -44,10 +46,10 @@ end it 'is able to pass a block to configure the connection' do - connection = double('connection') builder = double('builder') - allow(connection).to receive(:build).and_yield(builder) - allow(Faraday::Connection).to receive(:new).and_return(connection) + + allow(Faraday).to receive(:new).and_yield(builder) + allow(builder).to receive(:response) expect(builder).to receive(:adapter).with(:test) @@ -70,7 +72,7 @@ it 'allows override of raise_errors option' do client = described_class.new('abc', 'def', :site => 'https://api.example.com', :raise_errors => true) do |builder| builder.adapter :test do |stub| - stub.get('/notfound') { |env| [404, {}, nil] } + stub.get('/notfound') { |_env| [404, {}, nil] } end end expect(client.options[:raise_errors]).to be true @@ -109,6 +111,30 @@ subject.options[:"#{url_type}_url"] = 'https://api.foo.com/oauth/custom' expect(subject.send("#{url_type}_url")).to eq('https://api.foo.com/oauth/custom') end + + context 'when a URL with path is used in the site' do + let(:options) do + {:site => 'https://example.com/blog'} + end + + it 'generates an authorization URL relative to the site' do + expect(subject.send("#{url_type}_url")).to eq("https://example.com/blog/oauth/#{url_type}") + end + end + + context 'when a URL with path is used in the site and urls overridden' do + let(:options) do + { + :site => 'https://example.com/blog', + :authorize_url => "oauth/#{url_type}/lampoon", + :token_url => "oauth/#{url_type}/lampoon", + } + end + + it 'generates an authorization URL relative to the site' do + expect(subject.send("#{url_type}_url")).to eq("https://example.com/blog/oauth/#{url_type}/lampoon") + end + end end end @@ -157,6 +183,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 @@ -211,13 +299,12 @@ expect { subject.request(:get, error_path) }.to raise_error(OAuth2::Error) end end - 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 +327,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 +375,153 @@ 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(:post_args) { [] } + + let(:client) do + stubbed_client(options.merge(:raise_errors => raise_errors)) do |stub| + stub.post('/oauth/token', *post_args) 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 the request body has an access token' do + let(:token_response) { MultiJson.encode('access_token' => 'the-token') } + + it 'returns the parsed :access_token from body' do + token = client.get_token({}) + expect(token).to be_a OAuth2::AccessToken + expect(token.token).to eq('the-token') + end + + context 'when :auth_scheme => :request_body' do + context 'when arbitrary params are present' do + let(:post_args) { ['arbitrary' => 'parameter', 'client_id' => 'abc', 'client_secret' => 'def'] } + let(:options) { {:auth_scheme => :request_body} } + + it 'does not affect access token' do + token = client.get_token(*post_args) + expect(token).to be_a OAuth2::AccessToken + expect(token.token).to eq('the-token') + end + end + 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| @@ -311,7 +541,7 @@ def stubbed_client(params = {}, &stubs) context 'with SSL options' do subject do cli = described_class.new('abc', 'def', :site => 'https://api.example.com', :ssl => {:ca_file => 'foo.pem'}) - cli.connection.build do |b| + cli.connection = Faraday.new(cli.site, cli.options[:connection_opts]) do |b| b.adapter :test end cli diff --git a/spec/oauth2/mac_token_spec.rb b/spec/oauth2/mac_token_spec.rb index add5f72e..eab2599b 100644 --- a/spec/oauth2/mac_token_spec.rb +++ b/spec/oauth2/mac_token_spec.rb @@ -1,11 +1,11 @@ -require 'helper' +# frozen_string_literal: true -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| @@ -26,15 +26,18 @@ end it 'defaults algorithm to hmac-sha-256' do + pending_for(:engine => 'ruby', :versions => '1.9.3', :reason => "Ruby 1.9's OpenSSL uses instance of OpenSSL::Digest") expect(subject.algorithm).to be_instance_of(OpenSSL::Digest::SHA256) end it 'handles hmac-sha-256' do + pending_for(:engine => 'ruby', :versions => '1.9.3', :reason => "Ruby 1.9's OpenSSL uses instance of OpenSSL::Digest") mac = described_class.new(client, token, 'abc123', :algorithm => 'hmac-sha-256') expect(mac.algorithm).to be_instance_of(OpenSSL::Digest::SHA256) end it 'handles hmac-sha-1' do + pending_for(:engine => 'ruby', :versions => '1.9.3', :reason => "Ruby 1.9's OpenSSL uses instance of OpenSSL::Digest") mac = described_class.new(client, token, 'abc123', :algorithm => 'hmac-sha-1') expect(mac.algorithm).to be_instance_of(OpenSSL::Digest::SHA1) end @@ -91,7 +94,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..8e703aa1 100644 --- a/spec/oauth2/response_spec.rb +++ b/spec/oauth2/response_spec.rb @@ -1,4 +1,4 @@ -require 'helper' +# frozen_string_literal: true describe OAuth2::Response do describe '#initialize' do @@ -8,9 +8,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 +45,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 +55,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,12 +71,16 @@ 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 context 'with xml parser registration' do + before do + MultiXml.parser = :rexml + end + it 'tries to load multi_xml and use it' do expect(described_class.send(:class_variable_get, :@@parsers)[:xml]).not_to be_nil end diff --git a/spec/oauth2/strategy/assertion_spec.rb b/spec/oauth2/strategy/assertion_spec.rb index 36ea17de..cd54ba05 100644 --- a/spec/oauth2/strategy/assertion_spec.rb +++ b/spec/oauth2/strategy/assertion_spec.rb @@ -1,11 +1,14 @@ -require 'helper' +# frozen_string_literal: true + +require 'openssl' describe OAuth2::Strategy::Assertion do - subject { client.assertion } + let(:client_assertion) { client.assertion } let(:client) do cli = OAuth2::Client.new('abc', 'def', :site => 'http://api.example.com') - cli.connection.build do |b| + cli.connection = Faraday.new(cli.site, cli.options[:connection_opts]) do |b| + b.request :url_encoded b.adapter :test do |stub| stub.post('/oauth/token') do |env| case @mode @@ -22,38 +25,88 @@ let(:params) do { - :hmac_secret => 'foo', - :exp => Time.now.utc.to_i + 3600 + :hmac_secret => 'foo', + :exp => Time.now.utc.to_i + 3600, } end describe '#authorize_url' do it 'raises NotImplementedError' do - expect { subject.authorize_url }.to raise_error(NotImplementedError) + expect { client_assertion.authorize_url }.to raise_error(NotImplementedError) end end %w[json formencoded].each do |mode| - describe "#get_token (#{mode})" do - before do - @mode = mode - @access = subject.get_token(params) - end + before { @mode = mode } - it 'returns AccessToken with same Client' do - expect(@access.client).to eq(client) - end + shared_examples_for "get_token #{mode}" do + describe "#get_token (#{mode})" do + subject(:get_token) { client_assertion.get_token(params) } - it 'returns AccessToken with #token' do - expect(@access.token).to eq('salmon') + it 'returns AccessToken with same Client' do + expect(get_token.client).to eq(client) + end + + it 'returns AccessToken with #token' do + expect(get_token.token).to eq('salmon') + end + + it 'returns AccessToken with #expires_in' do + expect(get_token.expires_in).to eq(600) + end + + it 'returns AccessToken with #expires_at' do + expect(get_token.expires_at).not_to be_nil + end end + end - it 'returns AccessToken with #expires_in' do - expect(@access.expires_in).to eq(600) + it_behaves_like "get_token #{mode}" + describe "#build_assertion (#{mode})" do + context 'with hmac_secret' do + subject(:build_assertion) { client_assertion.build_assertion(params) } + + let(:hmac_secret) { '1883be842495c3b58f68ca71fbf1397fbb9ed2fdf8990f8404a25d0a1b995943' } + let(:params) do + { + :iss => 2345, + :aud => 'too', + :prn => 'much', + :exp => 123_456_789, + :hmac_secret => hmac_secret, + } + end + let(:jwt) { 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOjIzNDUsImF1ZCI6InRvbyIsInBybiI6Im11Y2giLCJleHAiOjEyMzQ1Njc4OX0.GnZjgcdc5WSWKNW0p9S4GuhpBs3LJCEqjPm6turLG-c' } + + it 'returns JWT' do + expect(build_assertion).to eq(jwt) + end + + it_behaves_like "get_token #{mode}" end - it 'returns AccessToken with #expires_at' do - expect(@access.expires_at).not_to be_nil + context 'with private_key' do + subject(:build_assertion) { client_assertion.build_assertion(params) } + + let(:private_key_file) { 'spec/fixtures/RS256/jwtRS256.key' } + let(:password) { '' } + let(:private_key) { OpenSSL::PKey::RSA.new(File.read(private_key_file), password) } + let(:params) do + { + :iss => 2345, + :aud => 'too', + :prn => 'much', + :exp => 123_456_789, + :private_key => private_key, + } + end + let(:jwt) { 'eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOjIzNDUsImF1ZCI6InRvbyIsInBybiI6Im11Y2giLCJleHAiOjEyMzQ1Njc4OX0.vJ32OiPVMdJrlNkPw02Y9u6beiFY0Mfndhg_CkEDLtOYn8dscQIEpWoR4GzH8tiQVOQ1fOkqxE95tNIKOTjnIoskmYnfzhzIl9fnfQ_lsEuLC-nq45KhPzSM2wYgF2ZEIjDq51daK70bRPzTBr1Id45cTY-jJSito0lbKXj2nPa_Gs-_vyEU2MSxjiMaIxxccfY4Ow5zN3AUMTKp6LjrpDKFxag3fJ1nrb6iDATa504gyJHVLift3ovhAwYidkA81WnmEtISWBY904CKIcZD9Cx3ifS5bc3JaLAteIBKAAyD8o7D60vOKutsjCMHUCKL357BQ36bW7fmaEtW367Ri-xgOsCY0_HeWp991vrJ-DxhFPeuF-8hn_9KggBzKbA2eKEOOY4iDKSFwjWQUFOcRdvHw9RgbGt0IjY3wdo8CaJVlhynh54YlaLgOFhTBPeMgZdqQUHOztljaK9zubeVkrDGNnGuSuq0KR82KArb1x2z7XyZpxiV5ZatP9SNyhn-YIWk7UeQYXaS0UfsBX7L5T1y_FZj84r7Vl42lj1DfdR5DyGvHfZyHotTnejdIrDuQfDL_bGe24eHsilzuEFaajYmu10hxflZ6Apm-lekRRV47tbxTF1zI5we14XsTeklrTXqgDkSw6gyOoNUJm-cQkJpfdvBgUHYGInC1ttz7NU' } + + it 'returns JWT' do + expect(build_assertion).to eq(jwt) + end + + it_behaves_like "get_token #{mode}" end end end diff --git a/spec/oauth2/strategy/auth_code_spec.rb b/spec/oauth2/strategy/auth_code_spec.rb index bcb1984f..465521f8 100644 --- a/spec/oauth2/strategy/auth_code_spec.rb +++ b/spec/oauth2/strategy/auth_code_spec.rb @@ -1,6 +1,5 @@ # encoding: utf-8 - -require 'helper' +# frozen_string_literal: true 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..87dd3266 100644 --- a/spec/oauth2/strategy/base_spec.rb +++ b/spec/oauth2/strategy/base_spec.rb @@ -1,4 +1,4 @@ -require 'helper' +# frozen_string_literal: true describe OAuth2::Strategy::Base do it 'initializes with a Client' do diff --git a/spec/oauth2/strategy/client_credentials_spec.rb b/spec/oauth2/strategy/client_credentials_spec.rb index 11d3b525..e5b800e7 100644 --- a/spec/oauth2/strategy/client_credentials_spec.rb +++ b/spec/oauth2/strategy/client_credentials_spec.rb @@ -1,4 +1,4 @@ -require 'helper' +# frozen_string_literal: true describe OAuth2::Strategy::ClientCredentials do subject { client.client_credentials } @@ -11,7 +11,7 @@ builder.adapter :test do |stub| stub.post('/oauth/token', 'grant_type' => 'client_credentials') do |env| client_id, client_secret = Base64.decode64(env[:request_headers]['Authorization'].split(' ', 2)[1]).split(':', 2) - client_id == 'abc' && client_secret == 'def' || raise(Faraday::Adapter::Test::Stubs::NotFound) + (client_id == 'abc' && client_secret == 'def') || raise(Faraday::Adapter::Test::Stubs::NotFound) case @mode when 'formencoded' [200, {'Content-Type' => 'application/x-www-form-urlencoded'}, kvform_token] diff --git a/spec/oauth2/strategy/implicit_spec.rb b/spec/oauth2/strategy/implicit_spec.rb index 7f48ec04..72042120 100644 --- a/spec/oauth2/strategy/implicit_spec.rb +++ b/spec/oauth2/strategy/implicit_spec.rb @@ -1,4 +1,4 @@ -require 'helper' +# frozen_string_literal: true 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..e6084740 100644 --- a/spec/oauth2/strategy/password_spec.rb +++ b/spec/oauth2/strategy/password_spec.rb @@ -1,11 +1,12 @@ -require 'helper' +# frozen_string_literal: true describe OAuth2::Strategy::Password do subject { client.password } let(:client) do cli = OAuth2::Client.new('abc', 'def', :site => 'http://api.example.com') - cli.connection.build do |b| + cli.connection = Faraday.new(cli.site, cli.options[:connection_opts]) do |b| + b.request :url_encoded b.adapter :test do |stub| stub.post('/oauth/token') do |env| case @mode diff --git a/spec/oauth2/version_spec.rb b/spec/oauth2/version_spec.rb new file mode 100644 index 00000000..854f1bc0 --- /dev/null +++ b/spec/oauth2/version_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.describe OAuth2::Version do + it 'has a version number' do + expect(described_class).not_to be nil + end + + it 'can be a string' do + expect(described_class.to_s).to be_a(String) + end + + it 'allows Constant access' do + expect(described_class::VERSION).to be_a(String) + end + + it 'is greater than 0.1.0' do + expect(Gem::Version.new(described_class) > Gem::Version.new('0.1.0')).to be(true) + end + + it 'is not a pre-release' do + expect(Gem::Version.new(described_class).prerelease?).to be(false) + end +end