diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..db04a98 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,11 @@ +# 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/omniauth-jwt2 # 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 +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 0000000..ba08aff --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,2 @@ +ignore: + - dependency-name: "rubocop-lts" diff --git a/.github/workflows/ancient.yml b/.github/workflows/ancient.yml new file mode 100644 index 0000000..f5dcbd5 --- /dev/null +++ b/.github/workflows/ancient.yml @@ -0,0 +1,53 @@ +name: Ancient Ruby Support + +on: + push: + branches: + - 'main' + 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: Ruby ${{ matrix.ruby }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + 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 + strategy: + fail-fast: false + matrix: + experimental: [false] + rubygems: + - "2.7.11" + bundler: + - none + gemfile: + - ancient + ruby: + - "2.3.8" + - "2.2.10" + runs-on: ubuntu-20.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - 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 rspec diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5142bf8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: Omniauth JWT Tests + +on: + push: + branches: + - 'main' + 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: + build: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + 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]')" + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + strategy: + fail-fast: false + matrix: + experimental: [false] + rubygems: + - latest + bundler: + - latest + gemfile: + - vanilla + ruby: + - "2.7" + - "3.0" + - "3.1" + - "3.2" + exclude: + # Vanilla + 3.2 is effectively run by coverage workflow + - gemfile: vanilla + ruby: "3.2" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Ruby & Bundle + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + - name: Tests + run: bundle exec rspec diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..85114ed --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,91 @@ +name: Code Coverage + +env: + K_SOUP_COV_MIN_BRANCH: 85 + K_SOUP_COV_MIN_LINE: 87 + +on: + push: + branches: + - 'main' + 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 }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + 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 + CI_CODECOV: true + COVER_ALL: true + strategy: + fail-fast: false + matrix: + experimental: [false] + rubygems: + - latest + bundler: + - latest + gemfile: + - coverage + ruby: + - "3.2" + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - 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 RSpec tests + run: | + bundle exec rspec + + - name: Code Coverage Summary Report + uses: irongut/CodeCoverageSummary@v1.3.0 + if: ${{ github.event_name == 'pull_request' }} + with: + filename: ./coverage/coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + # https://github.com/irongut/CodeCoverageSummary#thresholds + thresholds: "75 85" + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event_name == 'pull_request' }} + with: + recreate: true + path: code-coverage-results.md + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: ${{ matrix.experimental != 'false' }} diff --git a/.github/workflows/legacy.yml b/.github/workflows/legacy.yml new file mode 100644 index 0000000..393da86 --- /dev/null +++ b/.github/workflows/legacy.yml @@ -0,0 +1,54 @@ +name: Legacy Ruby Support + +on: + push: + branches: + - 'main' + 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: Ruby ${{ matrix.ruby }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + 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 + strategy: + fail-fast: false + matrix: + experimental: [false] + rubygems: + - "2.7.11" + bundler: + - none + gemfile: + - legacy + ruby: + - "2.6" + - "2.5" + - "2.4" + runs-on: ubuntu-20.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - 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 rspec diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 0000000..800b5fa --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,43 @@ +name: Code Style + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + +jobs: + rubocop: + name: RuboCop + strategy: + fail-fast: false + matrix: + experimental: [false] + rubygems: + - latest + bundler: + - latest + gemfile: + - style + ruby: + - "3.2" + + 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 + steps: + - name: Checkout + uses: actions/checkout@v4 + - 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 Gradual + run: bundle exec rake rubocop_gradual:check diff --git a/.gitignore b/.gitignore index d87d4be..13bd945 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ spec/reports test/tmp test/version_tmp tmp +gemfiles/*.gemfile.lock diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..6de99cd --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,2 @@ +inherit_gem: + rubocop-lts: config/rubygem_rspec.yml diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock new file mode 100644 index 0000000..8a46ddf --- /dev/null +++ b/.rubocop_gradual.lock @@ -0,0 +1,39 @@ +{ + "lib/omniauth/strategies/jwt.rb:543932255": [ + [60, 9, 76, "Lint/RescueException: Avoid rescuing the `Exception` class. Perhaps you meant to rescue `StandardError`?", 967033479] + ], + "omniauth-jwt2.gemspec:998952283": [ + [18, 16, 16, "Packaging/GemspecGit: Avoid using git to produce lists of files. Downstreams often need to build your package in an environment that does not have git (on purpose). Use some pure Ruby alternative, like `Dir` or `Dir.glob`.", 1973161220] + ], + "spec/lib/omniauth/strategies/jwt_spec.rb:2698313308": [ + [3, 1, 34, "RSpec/FilePath: Spec path should end with `omni_auth/strategies/jwt*_spec.rb`.", 1935033905], + [3, 1, 34, "RSpec/SpecFilePathFormat: Spec path should end with `omni_auth/strategies/jwt*_spec.rb`.", 1935033905], + [12, 13, 25, "RSpec/DescribedClass: Use `described_class` instead of `OmniAuth::Strategies::JWT`.", 2234488924], + [19, 11, 15, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 391893083], + [20, 5, 42, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 4106660663], + [29, 11, 16, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1409468707], + [51, 7, 51, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 4149552871], + [51, 7, 531, "RSpec/ExampleLength: Example has too many lines. [6/5]", 2143440997], + [91, 3, 3512, "RSpec/MultipleMemoizedHelpers: Example group has too many memoized helpers [10/5]", 2363831099], + [110, 7, 7, "RSpec/NamedSubject: Name your test subject if you need to reference it explicitly.", 1892732441], + [111, 7, 7, "RSpec/NamedSubject: Name your test subject if you need to reference it explicitly.", 1892732441], + [116, 9, 6, "RSpec/ExpectInHook: Do not use `expect` in `before` hook", 1179768986], + [116, 9, 20, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2951559342], + [116, 33, 7, "RSpec/MessageSpies: Prefer `have_received` for setting message expectations. Setup `rack_request` as a spy using `allow` or `instance_spy`.", 1384559950], + [130, 5, 56, "Performance/RedundantMerge: Use `algos[OpenSSL::PKey::EC] = %w[ES256 ES384 ES512]` instead of `algos.merge!(OpenSSL::PKey::EC => %w[ES256 ES384 ES512])`.", 2983772293], + [133, 9, 846, "RSpec/MultipleMemoizedHelpers: Example group has too many memoized helpers [10/5]", 3520352246], + [152, 22, 7, "RSpec/NamedSubject: Name your test subject if you need to reference it explicitly.", 1892732441], + [160, 5, 310, "RSpec/MultipleMemoizedHelpers: Example group has too many memoized helpers [10/5]", 3501674141], + [160, 13, 28, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 1877551307], + [170, 18, 7, "RSpec/NamedSubject: Name your test subject if you need to reference it explicitly.", 1892732441], + [174, 5, 515, "RSpec/MultipleMemoizedHelpers: Example group has too many memoized helpers [10/5]", 1246671601], + [185, 9, 7, "RSpec/NamedSubject: Name your test subject if you need to reference it explicitly.", 1892732441], + [189, 18, 7, "RSpec/NamedSubject: Name your test subject if you need to reference it explicitly.", 1892732441], + [193, 5, 537, "RSpec/MultipleMemoizedHelpers: Example group has too many memoized helpers [10/5]", 3770030886], + [205, 9, 7, "RSpec/NamedSubject: Name your test subject if you need to reference it explicitly.", 1892732441], + [209, 18, 7, "RSpec/NamedSubject: Name your test subject if you need to reference it explicitly.", 1892732441] + ], + "spec/support/hash.rb:812296649": [ + [2, 3, 110, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 3570181400] + ] +} diff --git a/.simplecov b/.simplecov new file mode 100644 index 0000000..94c2158 --- /dev/null +++ b/.simplecov @@ -0,0 +1,2 @@ +require "kettle/soup/cover/config" +SimpleCov.start # you could do this somewhere else, up to you, but you do have to do it diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..e80a70a --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 2.3.8 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 587a483..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: ruby -rvm: -- 2.0.0 -- 1.9.3 -- jruby-19mode -- rbx-19mode \ No newline at end of file diff --git a/Gemfile b/Gemfile index a39e1ca..3ddcf9f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,17 @@ -source 'https://rubygems.org' +source "https://rubygems.org" # Specify your gem's dependencies in omniauth-jwt.gemspec gemspec + +# Development dependencies that rely on Ruby version >= +# Style +eval_gemfile "gemfiles/contexts/style.gemfile" + +# Coverage +eval_gemfile "gemfiles/contexts/coverage.gemfile" + +# Testing +eval_gemfile "gemfiles/contexts/testing.gemfile" + +# Debug +eval_gemfile "gemfiles/contexts/debug.gemfile" diff --git a/Guardfile b/Guardfile index bf867bf..162f282 100644 --- a/Guardfile +++ b/Guardfile @@ -3,6 +3,6 @@ guard :rspec do watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - watch('spec/spec_helper.rb') { "spec" } -end \ No newline at end of file + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } + watch("spec/spec_helper.rb") { "spec" } +end diff --git a/LICENSE.txt b/LICENSE.txt index 9d3a08d..cea5ac2 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,5 @@ Copyright (c) 2013 Michael Bleigh +Copyright (c) 2023 Peter Boling of railsbling.com MIT License diff --git a/README.md b/README.md index 9a37c75..48a0e0b 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,61 @@ # OmniAuth::JWT -[![Build Status](https://travis-ci.org/mbleigh/omniauth-jwt.png)](https://travis-ci.org/mbleigh/omniauth-jwt) +
+ +[🚎ciwf]: https://github.com/pboling/omniauth-jwt2/actions/workflows/ci.yml +[🚎ciwfi]: https://github.com/pboling/omniauth-jwt2/actions/workflows/ci.yml/badge.svg +[🖐cowf]: https://github.com/pboling/omniauth-jwt2/actions/workflows/coverage.yml +[🖐cowfi]: https://github.com/pboling/omniauth-jwt2/actions/workflows/coverage.yml/badge.svg +[🧮swf]: https://github.com/pboling/omniauth-jwt2/actions/workflows/style.yml +[🧮swfi]: https://github.com/pboling/omniauth-jwt2/actions/workflows/style.yml/badge.svg +[🧮lwf]: https://github.com/pboling/omniauth-jwt2/actions/workflows/legacy.yml +[🧮lwfi]: https://github.com/pboling/omniauth-jwt2/actions/workflows/legacy.yml/badge.svg +[🧮awf]: https://github.com/pboling/omniauth-jwt2/actions/workflows/ancient.yml +[🧮awfi]: https://github.com/pboling/omniauth-jwt2/actions/workflows/ancient.yml/badge.svg + +[⛳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_Me!-pboling.svg?style=social&logo=github +[🖇sponsor]: https://github.com/sponsors/pboling [JSON Web Token](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) (JWT) is a simple way to send verified information between two parties online. This can be useful as a mechanism for providing Single Sign-On (SSO) to an application by allowing an authentication server to send a validated -claim and log the user in. This is how [Zendesk does SSO](https://support.zendesk.com/entries/23675367-Setting-up-single-sign-on-with-JWT-JSON-Web-Token-), +claim and log the user in. This is how [Zendesk does SSO](https://support.zendesk.com/hc/en-us/articles/4408845838874-Enabling-JWT-JSON-Web-Token-single-sign-on), for example. OmniAuth::JWT provides a clean, simple wrapper on top of JWT so that you can easily implement this kind of SSO either between your own applications or allow third parties to delegate authentication. +## History + +This library is a fork of the [original](https://github.com/mbleigh/omniauth-jwt) +by Michael Bleigh which stopped development in 2013. +It incorporates *all* of the fixes and features from the main forks by Aha, Discourse, +and GitLab (which has been vendored inside GitLab, and isn't even in the fork network). + ## Installation Add this line to your application's Gemfile: - gem 'omniauth-jwt' + gem 'omniauth-jwt2' And then execute: @@ -23,14 +63,14 @@ And then execute: Or install it yourself as: - $ gem install omniauth-jwt + $ gem install omniauth-jwt2 ## Usage You use OmniAuth::JWT just like you do any other OmniAuth strategy: ```ruby -use OmniAuth::JWT, 'SHAREDSECRET', auth_url: 'http://example.com/login' +use OmniAuth::JWT, "SHAREDSECRET", auth_url: "http://example.com/login" ``` The first parameter is the shared secret that will be used by the external authenticator to verify @@ -48,7 +88,7 @@ in. Other available options are: * **valid_within:** integer of how many seconds of time skew you will allow. Defaults to `nil`. If this is set, the `iat` claim becomes required and must be within the specified number of seconds of the current time. This helps to prevent replay attacks. - + ### Authentication Process When you authenticate through `omniauth-jwt` you can send users to `/auth/jwt` and it will redirect @@ -56,22 +96,22 @@ them to the URL specified in the `auth_url` option. From there, the provider mus and send it to the `/auth/jwt/callback` URL as a "jwt" parameter: /auth/jwt/callback?jwt=ENCODEDJWTGOESHERE - + An example of how to do that in Sinatra: ```ruby -require 'jwt' +require "jwt" -get '/login/sso/other-app' do +get "/login/sso/other-app" do # assuming the user is already logged in and this is available as current_user claims = { id: current_user.id, name: current_user.name, email: current_user.email, - iat: Time.now.to_i + iat: Time.now.to_i, } - - payload = JWT.encode(claims, ENV['SSO_SECRET']) + + payload = JWT.encode(claims, ENV["SSO_SECRET"]) redirect "http://other-app.com/auth/jwt/callback?jwt=#{payload}" end ``` diff --git a/Rakefile b/Rakefile index 6723109..3e31426 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,23 @@ require "bundler/gem_tasks" + require "rspec/core/rake_task" +RSpec::Core::RakeTask.new(:spec) + +desc "alias test task to spec" +task test: :spec + +begin + require "kettle-soup-cover" + Kettle::Soup::Cover.install_tasks +rescue LoadError + # NOOP +end -RSpec::Core::RakeTask.new +begin + require "rubocop/lts" + Rubocop::Lts.install_tasks +rescue LoadError + # NOOP +end -task :default => :spec \ No newline at end of file +task default: :spec diff --git a/gemfiles/ancient.gemfile b/gemfiles/ancient.gemfile new file mode 100644 index 0000000..a95a67c --- /dev/null +++ b/gemfiles/ancient.gemfile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Gemfile is only for local development. +# On CI we only need the gemspecs' dependencies (including development dependencies). +# Exceptions, if any, will be found in gemfiles/* + +# Testing +gem "rack", "~> 2.1.4.3" # ruby 2.2.2 +gem "json", "~> 2.5.1" # ruby 2.0 + +# Debugging +eval_gemfile "contexts/debug.gemfile" + +gemspec path: "../" + +gem "omniauth", "< 2" diff --git a/gemfiles/contexts/coverage.gemfile b/gemfiles/contexts/coverage.gemfile new file mode 100644 index 0000000..07806f3 --- /dev/null +++ b/gemfiles/contexts/coverage.gemfile @@ -0,0 +1,2 @@ +# Coverage +gem "kettle-soup-cover", "~> 1.0", ">= 1.0.2" # ruby 2.7 diff --git a/gemfiles/contexts/debug.gemfile b/gemfiles/contexts/debug.gemfile new file mode 100644 index 0000000..eb9bf00 --- /dev/null +++ b/gemfiles/contexts/debug.gemfile @@ -0,0 +1,6 @@ +# Ancient rubies do not have String#casecmp? +debugging = ENV["CI"].nil? && ENV.fetch("DEBUG", "false") + +if debugging && debugging[/true/i] + gem "byebug" +end diff --git a/gemfiles/contexts/style.gemfile b/gemfiles/contexts/style.gemfile new file mode 100644 index 0000000..fb2cbbe --- /dev/null +++ b/gemfiles/contexts/style.gemfile @@ -0,0 +1,5 @@ +# Style +gem "rubocop-lts", "~> 8.1", ">= 8.1.1" # ruby 2.7 - Lint Support for Ruby 2.2+ +gem "rubocop-packaging", "~> 0.5", ">= 0.5.2" # ruby 2.6 +gem "rubocop-rspec", "~> 2.25" # ruby 2.7 +gem "rspec-block_is_expected", "~> 1.0", ">= 1.0.5" # ruby 1.8.7 diff --git a/gemfiles/contexts/testing.gemfile b/gemfiles/contexts/testing.gemfile new file mode 100644 index 0000000..ca6444d --- /dev/null +++ b/gemfiles/contexts/testing.gemfile @@ -0,0 +1,8 @@ +# Testing +gem "ed25519", "~> 1.3" # ruby 2.4 +gem "json", "~> 2.6", ">= 2.6.3" # ruby 2.3 +gem "openssl", ">= 2.0" # ruby 2.3, v3.0 is >= 2.6, v3.2 is >= 2.7 +gem "openssl-signature_algorithm", "~> 1.3" # ruby 2.4 +gem "rack", "~> 3.0", ">= 3.0.8" # ruby 2.4 +gem "rack-session", "~> 2.0" # ruby 2.4 +gem "rspec-block_is_expected", "~> 1.0", ">= 1.0.5" # ruby 1.8.7 diff --git a/gemfiles/coverage.gemfile b/gemfiles/coverage.gemfile new file mode 100644 index 0000000..1861922 --- /dev/null +++ b/gemfiles/coverage.gemfile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Gemfile is only for local development. +# On CI we only need the gemspecs' dependencies (including development dependencies). +# Exceptions, if any, will be found in gemfiles/* + +# Coverage +eval_gemfile "contexts/coverage.gemfile" + +# Testing +eval_gemfile "contexts/testing.gemfile" + +# Debugging +eval_gemfile "contexts/debug.gemfile" + +gemspec path: "../" diff --git a/gemfiles/legacy.gemfile b/gemfiles/legacy.gemfile new file mode 100644 index 0000000..de1d28b --- /dev/null +++ b/gemfiles/legacy.gemfile @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Gemfile is only for local development. +# On CI we only need the gemspecs' dependencies (including development dependencies). +# Exceptions, if any, will be found in gemfiles/* + +# Testing +gem "rspec", "~> 3.12" # ruby * +gem "rack-test", "~> 2.1" # ruby 2.0 +gem "rack" # ruby 2.4 +gem "rack-session", "< 2", github: "pboling/rack-session", branch: "fix-missing-rack-session" # ruby < 2.4 +gem "json" # ruby 2.3 +gem "openssl" # ruby 2.3 +gem "openssl-signature_algorithm" # ruby 2.4 +gem "ed25519" # ruby 2.4 + +# Debugging +eval_gemfile "contexts/debug.gemfile" + +gemspec path: "../" + +gem "omniauth", "< 2" diff --git a/gemfiles/style.gemfile b/gemfiles/style.gemfile new file mode 100644 index 0000000..a2aa37e --- /dev/null +++ b/gemfiles/style.gemfile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Gemfile is only for local development. +# On CI we only need the gemspecs' dependencies (including development dependencies). +# Exceptions, if any, will be found in gemfiles/* + +# Coverage +eval_gemfile "contexts/coverage.gemfile" + +# Style +eval_gemfile "contexts/style.gemfile" + +# Debugging +eval_gemfile "contexts/debug.gemfile" + +gemspec path: "../" diff --git a/gemfiles/vanilla.gemfile b/gemfiles/vanilla.gemfile new file mode 100644 index 0000000..1861922 --- /dev/null +++ b/gemfiles/vanilla.gemfile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Gemfile is only for local development. +# On CI we only need the gemspecs' dependencies (including development dependencies). +# Exceptions, if any, will be found in gemfiles/* + +# Coverage +eval_gemfile "contexts/coverage.gemfile" + +# Testing +eval_gemfile "contexts/testing.gemfile" + +# Debugging +eval_gemfile "contexts/debug.gemfile" + +gemspec path: "../" diff --git a/lib/omniauth/jwt.rb b/lib/omniauth/jwt.rb index 6d062b5..b3dc1fc 100644 --- a/lib/omniauth/jwt.rb +++ b/lib/omniauth/jwt.rb @@ -1,2 +1,10 @@ +# External gems +require "version_gem" + +# This gem require "omniauth/jwt/version" -require "omniauth/strategies/jwt" \ No newline at end of file +require "omniauth/strategies/jwt" + +Omniauth::JWT::Version.class_eval do + extend VersionGem::Basic +end diff --git a/lib/omniauth/jwt/version.rb b/lib/omniauth/jwt/version.rb index 41e6061..6b98e5a 100644 --- a/lib/omniauth/jwt/version.rb +++ b/lib/omniauth/jwt/version.rb @@ -1,5 +1,7 @@ module Omniauth module JWT - VERSION = "0.0.3" + module Version + VERSION = "0.1.0" + end end end diff --git a/lib/omniauth/strategies/jwt.rb b/lib/omniauth/strategies/jwt.rb index 310d7eb..9f7cdd9 100644 --- a/lib/omniauth/strategies/jwt.rb +++ b/lib/omniauth/strategies/jwt.rb @@ -1,64 +1,97 @@ -require 'omniauth' -require 'jwt' +require "omniauth" +require "jwt" module OmniAuth module Strategies class JWT class ClaimInvalid < StandardError; end + class BadJwt < StandardError; end - + include OmniAuth::Strategy - + args [:secret] - + option :secret, nil option :decode_options, {} - option :uid_claim, 'email' + option :jwks_loader + option :algorithm, "HS256" # overridden by options.decode_options[:algorithms] + option :decode_options, {} + option :uid_claim, "email" option :required_claims, %w(name email) - option :info_map, {"name" => "name", "email" => "email"} + option :info_map, {name: "name", email: "email"} option :auth_url, nil option :valid_within, nil - + def request_phase - redirect options.auth_url + redirect(options.auth_url) end - + def decoded begin - @decoded ||= ::JWT.decode(request.params['jwt'], options.secret, true, options.decode_options).first + secret = if defined?(OpenSSL) + case options.algorithm + when "RS256", "RS384", "RS512" + OpenSSL::PKey::RSA.new(options.secret).public_key + when "ES256", "ES384", "ES512" + OpenSSL::PKey::EC.new(options.secret) + when "HS256", "HS384", "HS512" + options.secret + else + raise NotImplementedError, "Unsupported algorithm: #{options.algorithm}" + end + else + options.secret + end + + # JWT.decode can handle either algorithms or algorithm, but not both. + default_algos = options.decode_options.key?(:algorithms) ? options.decode_options[:algorithms] : [options.algorithm] + @decoded ||= ::JWT.decode( + request.params["jwt"], + secret, + true, + options.decode_options.merge( + { + algorithms: default_algos, + jwks: options.jwks_loader, + }.delete_if { |_, v| v.nil? }, + ), + )[0] rescue Exception => e - raise BadJwt.new(e.message) + raise BadJwt.new("#{e.class}: #{e.message}") end (options.required_claims || []).each do |field| raise ClaimInvalid.new("Missing required '#{field}' claim.") if !@decoded.key?(field.to_s) end raise ClaimInvalid.new("Missing required 'iat' claim.") if options.valid_within && !@decoded["iat"] - raise ClaimInvalid.new("'iat' timestamp claim is too skewed from present.") if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within + if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within.to_i + raise ClaimInvalid, "'iat' timestamp claim is too skewed from present" + end + @decoded end - + def callback_phase super rescue BadJwt => e - fail! 'bad_jwt', e + fail!("bad_jwt", e) rescue ClaimInvalid => e - fail! :claim_invalid, e + fail!(:claim_invalid, e) end - - uid{ decoded[options.uid_claim] } - + + uid { decoded[options.uid_claim] } + extra do - {:raw_info => decoded} + {raw_info: decoded} end - + info do - options.info_map.inject({}) do |h,(k,v)| + options.info_map.each_with_object({}) do |(k, v), h| h[k.to_s] = decoded[v.to_s] - h end end end - + class Jwt < JWT; end end end diff --git a/omniauth-jwt.gemspec b/omniauth-jwt.gemspec deleted file mode 100644 index 703215c..0000000 --- a/omniauth-jwt.gemspec +++ /dev/null @@ -1,30 +0,0 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'omniauth/jwt/version' - -Gem::Specification.new do |spec| - spec.name = "omniauth-jwt" - spec.version = Omniauth::JWT::VERSION - spec.authors = ["Michael Bleigh"] - spec.email = ["mbleigh@mbleigh.com"] - spec.description = %q{An OmniAuth strategy to accept JWT-based single sign-on.} - spec.summary = %q{An OmniAuth strategy to accept JWT-based single sign-on.} - spec.homepage = "http://github.com/mbleigh/omniauth-jwt" - spec.license = "MIT" - - spec.files = `git ls-files`.split($/) - spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) - spec.require_paths = ["lib"] - - spec.add_development_dependency "bundler" - spec.add_development_dependency "rake" - spec.add_development_dependency "rspec" - spec.add_development_dependency "guard" - spec.add_development_dependency "guard-rspec" - spec.add_development_dependency "rack-test" - - spec.add_dependency "jwt", ">= 2.0.0" - spec.add_dependency "omniauth", "~> 1.1" -end diff --git a/omniauth-jwt2.gemspec b/omniauth-jwt2.gemspec new file mode 100644 index 0000000..2d59756 --- /dev/null +++ b/omniauth-jwt2.gemspec @@ -0,0 +1,41 @@ +# Get the GEMFILE_VERSION without *require* "my_gem/version", for code coverage accuracy +# See: https://github.com/simplecov-ruby/simplecov/issues/557#issuecomment-825171399 +load "lib/omniauth/jwt/version.rb" +gem_version = Omniauth::JWT::Version::VERSION +Omniauth::JWT::Version.send(:remove_const, :VERSION) + +Gem::Specification.new do |spec| + spec.name = "omniauth-jwt2" + spec.version = gem_version + spec.authors = ["Michael Bleigh", "Robin Ward", "Peter Boling"] + spec.email = ["mbleigh@mbleigh.com", "robin.ward@gmail.com", "peter.boling@gmail.com"] + spec.description = "An OmniAuth strategy to accept JWT-based single sign-on." + spec.summary = "An OmniAuth strategy to accept JWT-based single sign-on." + spec.homepage = "http://github.com/pboling/omniauth-jwt2" + spec.license = "MIT" + spec.required_ruby_version = ">= 2.2" + + spec.files = %x(git ls-files).split($/) + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + # TODO: Since this gem supports Ruby >= 2.2 we need to ensure no gems are + # added here that require a newer version. Once this gem progresses to + # only support non-EOL Rubies, all dependencies can be listed in this + # gemspec, and the gemfiles/* pattern can be dispensed with. + spec.add_dependency("jwt", "~> 2.2", ">= 2.2.1") # ruby 2.1 + spec.add_dependency("omniauth", ">= 1.1") # ruby 2.2 + + # Utilities + spec.add_dependency("version_gem", "~> 1.1", ">= 1.1.3") # ruby 2.2 + spec.add_development_dependency("rake", "~> 13.0") # ruby 2.2, v13.1 is >= 2.3 + + # Hot reload + spec.add_development_dependency("guard", "~> 2.18", ">= 2.18.1") # ruby 1.9.3 + spec.add_development_dependency("guard-rspec", "~> 4.7", ">= 4.7.3") # ruby * + + # Testing + spec.add_development_dependency("rack-test", "~> 2.1") # ruby 2.0 + spec.add_development_dependency("rspec", "~> 3.12") # ruby * + spec.add_development_dependency("rspec-pending_for", "~> 0.1") # ruby * +end diff --git a/spec/lib/omniauth/strategies/jwt_spec.rb b/spec/lib/omniauth/strategies/jwt_spec.rb index 8349413..a5ddff7 100644 --- a/spec/lib/omniauth/strategies/jwt_spec.rb +++ b/spec/lib/omniauth/strategies/jwt_spec.rb @@ -1,85 +1,213 @@ -require 'spec_helper' +require "spec_helper" describe OmniAuth::Strategies::JWT do - let(:response_json){ JSON.parse(last_response.body) } - let(:args){ ['imasecret', {auth_url: 'http://example.com/login'}] } + let(:response_json) { JSON.parse(last_response.body) } + let(:rand_secret) { SecureRandom.hex(10) } + let(:args) { [rand_secret, {auth_url: "http://example.com/login"}] } - let(:app){ + let(:app) { the_args = args Rack::Builder.new do |b| - b.use Rack::Session::Cookie, secret: 'sekrit' + b.use Rack::Session::Cookie, secret: SecureRandom.hex(32) b.use OmniAuth::Strategies::JWT, *the_args - b.run lambda{|env| [200, {}, [(env['omniauth.auth'] || {}).to_json]]} + b.run lambda { |env| + [200, {}, [(env["omniauth.auth"] || {}).to_json]] + } end } - context 'request phase' do - it 'should redirect to the configured login url' do - get '/auth/jwt' + context "request phase" do + it "redirects to the configured login url" do + # TODO: Figure out how to write this test without using the deprecated + # and unsafe, "get" method for the request phase. + get "/auth/jwt" expect(last_response.status).to eq(302) - expect(last_response.headers['Location']).to eq('http://example.com/login') + expect(last_response.headers["Location"]).to eq("http://example.com/login") end end - context 'callback phase' do - it 'should decode the response' do - encoded = JWT.encode({name: 'Bob', email: 'steve@example.com'}, 'imasecret') - get '/auth/jwt/callback?jwt=' + encoded + context "callback phase" do + it "decodes the response" do + encoded = JWT.encode({name: "Bob", email: "steve@example.com"}, rand_secret) + get "/auth/jwt/callback?jwt=" + encoded expect(response_json["info"]["email"]).to eq("steve@example.com") end - it 'should not work without required fields' do - encoded = JWT.encode({name: 'Steve'}, 'imasecret') - get '/auth/jwt/callback?jwt=' + encoded + it "does not work without required fields" do + encoded = JWT.encode({name: "Steve"}, rand_secret) + get "/auth/jwt/callback?jwt=" + encoded expect(last_response.status).to eq(302) end - it 'should assign the uid' do - encoded = JWT.encode({name: 'Steve', email: 'dude@awesome.com'}, 'imasecret') - get '/auth/jwt/callback?jwt=' + encoded - expect(response_json["uid"]).to eq('dude@awesome.com') + it "assigns the uid" do + encoded = JWT.encode({name: "Steve", email: "dude@awesome.com"}, rand_secret) + get "/auth/jwt/callback?jwt=" + encoded + expect(response_json["uid"]).to eq("dude@awesome.com") end - context 'with a non-default encoding algorithm' do - let(:args){ ['imasecret', {auth_url: 'http://example.com/login', decode_options: { algorithms: ['HS512', 'HS256'] }}] } + context "with a non-default encoding algorithm" do + let(:args) { [rand_secret, {auth_url: "http://example.com/login", decode_options: {algorithms: ["HS512", "HS256"]}}] } - it 'should decode the response with an allowed algorithm' do - encoded = JWT.encode({name: 'Bob', email: 'steve@example.com'}, 'imasecret', 'HS512') - get '/auth/jwt/callback?jwt=' + encoded + it "decodes the response with an allowed algorithm" do + encoded = JWT.encode({name: "Bob", email: "steve@example.com"}, rand_secret, "HS512") + get "/auth/jwt/callback?jwt=" + encoded expect(JSON.parse(last_response.body)["info"]["email"]).to eq("steve@example.com") - encoded = JWT.encode({name: 'Bob', email: 'steve@example.com'}, 'imasecret', 'HS256') - get '/auth/jwt/callback?jwt=' + encoded + encoded = JWT.encode({name: "Bob", email: "steve@example.com"}, rand_secret, "HS256") + get "/auth/jwt/callback?jwt=" + encoded expect(JSON.parse(last_response.body)["info"]["email"]).to eq("steve@example.com") end - it 'should fail decoding the response with a different algorithm' do - encoded = JWT.encode({name: 'Bob', email: 'steve@example.com'}, 'imasecret', 'HS384') - get '/auth/jwt/callback?jwt=' + encoded + it "fails decoding the response with a different algorithm" do + encoded = JWT.encode({name: "Bob", email: "steve@example.com"}, rand_secret, "HS384") + get "/auth/jwt/callback?jwt=" + encoded expect(last_response.headers["Location"]).to include("/auth/failure") end end - context 'with a :valid_within option set' do - let(:args){ ['imasecret', {auth_url: 'http://example.com/login', valid_within: 300}] } + context "with a :valid_within option set" do + let(:args) { [rand_secret, {auth_url: "http://example.com/login", valid_within: 300}] } - it 'should work if the iat key is within the time window' do - encoded = JWT.encode({name: 'Ted', email: 'ted@example.com', iat: Time.now.to_i}, 'imasecret') - get '/auth/jwt/callback?jwt=' + encoded + it "works if the iat key is within the time window" do + encoded = JWT.encode({name: "Ted", email: "ted@example.com", iat: Time.now.to_i}, rand_secret) + get "/auth/jwt/callback?jwt=" + encoded expect(last_response.status).to eq(200) end - it 'should not work if the iat key is outside the time window' do - encoded = JWT.encode({name: 'Ted', email: 'ted@example.com', iat: Time.now.to_i + 500}, 'imasecret') - get '/auth/jwt/callback?jwt=' + encoded + it "does not work if the iat key is outside the time window" do + encoded = JWT.encode({name: "Ted", email: "ted@example.com", iat: Time.now.to_i + 500}, rand_secret) + get "/auth/jwt/callback?jwt=" + encoded expect(last_response.status).to eq(302) end - it 'should not work if the iat key is missing' do - encoded = JWT.encode({name: 'Ted', email: 'ted@example.com'}, 'imasecret') - get '/auth/jwt/callback?jwt=' + encoded + it "does not work if the iat key is missing" do + encoded = JWT.encode({name: "Ted", email: "ted@example.com"}, rand_secret) + get "/auth/jwt/callback?jwt=" + encoded expect(last_response.status).to eq(302) end end end + + describe "#decoded" do + subject { described_class.new({}) } + + let(:timestamp) { Time.now.to_i } + let(:claims) do + { + id: 123, + name: "user_example", + email: "user@example.com", + iat: timestamp, + } + end + + let(:algorithm) { "HS256" } + let(:secret) { rand_secret } + let(:private_key) { secret } + let(:payload) { JWT.encode(claims, private_key, algorithm) } + + before do + subject.options[:secret] = secret + subject.options[:algorithm] = algorithm + + # We use Rack::Request instead of ActionDispatch::Request because + # Rack::Test::Methods enables testing of this module. + expect_next_instance_of(Rack::Request) do |rack_request| + expect(rack_request).to receive(:params).and_return("jwt" => payload) + end + end + + ecdsa_named_curves = { + "ES256" => "prime256v1", + "ES384" => "secp384r1", + "ES512" => "secp521r1", + }.freeze + + algos = { + OpenSSL::PKey::RSA => %w[RS256 RS384 RS512], + String => %w[HS256 HS384 HS512], + } + algos.merge!(OpenSSL::PKey::EC => %w[ES256 ES384 ES512]) unless ["2.2.10", "2.3.8"].include?(RubyVersion.to_s) + algos.each do |private_key_class, algorithms| + algorithms.each do |algorithm| + context "when the #{algorithm} algorithm is used" do + let(:algorithm) { algorithm } + let(:secret) do + # rubocop:disable Style/CaseLikeIf + if private_key_class == OpenSSL::PKey::RSA + private_key_class.generate(2048) + .to_pem + elsif private_key_class == OpenSSL::PKey::EC + private_key_class.generate(ecdsa_named_curves[algorithm]) + .to_pem + else + private_key_class.new(rand_secret) + end + # rubocop:enable Style/CaseLikeIf + end + + let(:private_key) { private_key_class ? private_key_class.new(secret) : secret } + + it "decodes the user information" do + result = subject.decoded + + expect(result).to eq(claims.stringify_keys) + end + end + end + end + + context "required claims is missing" do + let(:claims) do + { + id: 123, + email: "user@example.com", + iat: timestamp, + } + end + + it "raises error" do + expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid) + end + end + + context "when valid_within is specified but iat attribute is missing in response" do + let(:claims) do + { + id: 123, + name: "user_example", + email: "user@example.com", + } + end + + before do + # Omniauth config values are always strings! + subject.options[:valid_within] = (60 * 60 * 24 * 2).to_s # 2 days + end + + it "raises error" do + expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid) + end + end + + context "when timestamp claim is too skewed from present" do + let(:claims) do + { + id: 123, + name: "user_example", + email: "user@example.com", + iat: timestamp - (60 * 60 * 10), # minus ten minutes + } + end + + before do + # Omniauth config values are always strings! + subject.options[:valid_within] = "2" # 2 seconds + end + + it "raises error" do + expect { subject.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid) + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1303bb9..3b185f3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,9 +1,43 @@ -$:.unshift File.dirname(__FILE__) + "/../lib" -require 'rack/test' -require 'json' +# Std Lib +require "securerandom" -require 'omniauth/jwt' -OmniAuth.config.logger = Logger.new('/dev/null') +# 3rd party gems +require "rspec/pending_for" +begin + require "rack/session" +rescue LoadError + nil # File won't exist in old rack for Ruby 2.2 & 2.3 +end +require "rack/test" +require "json" +require "omniauth" +begin + require "openssl" + require "openssl/signature_algorithm" + require "ed25519" +rescue LoadError + nil # Gem doesn't exist for ancient Rubies 2.2 & 2.3 +end + +require "byebug" if ENV["DEBUG"] == "true" +# This does not require "simplecov", +# because that has a side-effect of running `.simplecov` +begin + require "kettle-soup-cover" +rescue LoadError + puts "Not analyzing test coverage" +end + +require "support/hash" +require "support/next_instance_of" + +OmniAuth.config.logger = Logger.new("/dev/null") +require "omniauth/version" +puts "OMNIAUTH VERSION: #{OmniAuth::VERSION}" +if Gem::Version.new(OmniAuth::VERSION) > Gem::Version.new("2.0") + OmniAuth.config.silence_get_warning = true + OmniAuth.config.allowed_request_methods |= [:get, :post] +end # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # Require this file using `require "spec_helper"` to ensure that it is only @@ -11,15 +45,20 @@ # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| - config.treat_symbols_as_metadata_keys_with_true_values = true config.run_all_when_everything_filtered = true config.filter_run :focus - + include Rack::Test::Methods + include NextInstanceOf # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 - config.order = 'random' + config.order = "random" end + +# Last thing before loading this library, load simplecov: +require "simplecov" if defined?(Kettle::Soup::Cover) && Kettle::Soup::Cover::DO_COV + +require "omniauth/jwt" diff --git a/spec/support/hash.rb b/spec/support/hash.rb new file mode 100644 index 0000000..21b1e19 --- /dev/null +++ b/spec/support/hash.rb @@ -0,0 +1,9 @@ +class Hash + def self.stringify_keys(h) + h.is_a?(Hash) ? h.collect { |k, v| [k.to_s, stringify_keys(v)] }.to_h : h + end + + def stringify_keys + self.class.stringify_keys(self) + end +end diff --git a/spec/support/next_instance_of.rb b/spec/support/next_instance_of.rb new file mode 100644 index 0000000..1b6408e --- /dev/null +++ b/spec/support/next_instance_of.rb @@ -0,0 +1,43 @@ +# From: https://github.com/gitlabhq/gitlabhq/blob/master/gems/gitlab-rspec/lib/gitlab/rspec/next_instance_of.rb#L4 +module NextInstanceOf + def expect_next_instance_of(klass, *new_args, &blk) + stub_new(expect(klass), nil, false, *new_args, &blk) + end + + def expect_next_instances_of(klass, number, ordered = false, *new_args, &blk) + stub_new(expect(klass), number, ordered, *new_args, &blk) + end + + def allow_next_instance_of(klass, *new_args, &blk) + stub_new(allow(klass), nil, false, *new_args, &blk) + end + + def allow_next_instances_of(klass, number, ordered = false, *new_args, &blk) + stub_new(allow(klass), number, ordered, *new_args, &blk) + end + + private + + def stub_new(target, number, ordered = false, *new_args, &blk) + receive_new = receive(:new) + receive_new.ordered if ordered + receive_new.with(*new_args) if !(new_args.empty? || new_args.blank?) + + if number.is_a?(Range) + receive_new.at_least(number.begin).times if number.begin + receive_new.at_most(number.end).times if number.end + elsif number + receive_new.exactly(number).times + end + + target.to receive_new.and_wrap_original do |*original_args, **original_kwargs| + method, *original_args = original_args + begin + method.call(*original_args, **original_kwargs).tap(&blk) + rescue ArgumentError + # Kludge for old ruby < 2.7 + method.call(*original_args).tap(&blk) + end + end + end +end