pax_global_header00006660000000000000000000000064150300357050014510gustar00rootroot0000000000000052 comment=bbe7ae3786568b041f03f50cb2e48d2b964a4eb4 ruby-jwt-3.1.2/000077500000000000000000000000001503003570500132765ustar00rootroot00000000000000ruby-jwt-3.1.2/.github/000077500000000000000000000000001503003570500146365ustar00rootroot00000000000000ruby-jwt-3.1.2/.github/pull_request_template.md000066400000000000000000000006051503003570500216000ustar00rootroot00000000000000### Description This Pull Request changes/fixes this thing ### Checklist Before the PR can be merged be sure the following are checked: - [ ] There are tests for the fix or feature added/changed - [ ] A description of the changes and a reference to the PR has been added to CHANGELOG.md. More details in the [CONTRIBUTING.md](https://github.com/jwt/ruby-jwt/blob/main/CONTRIBUTING.md) ruby-jwt-3.1.2/.github/workflows/000077500000000000000000000000001503003570500166735ustar00rootroot00000000000000ruby-jwt-3.1.2/.github/workflows/deploy_docs.yml000066400000000000000000000013271503003570500217250ustar00rootroot00000000000000--- name: GitHub Pages on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Install yard run: gem install yard - name: Install redcarpet run: gem install redcarpet - name: Build docs run: yard - name: Configure CNAME run: echo "ruby-jwt.org" > ./doc/CNAME - name: Deploy uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./doc ruby-jwt-3.1.2/.github/workflows/push_gem.yml000066400000000000000000000024521503003570500212300ustar00rootroot00000000000000--- "on": push: tags: - v* name: Push Gem jobs: push: runs-on: ubuntu-latest permissions: contents: write id-token: write steps: - uses: rubygems/configure-rubygems-credentials@main with: role-to-assume: ${{ secrets.RUBYGEMS_PUSH_ROLE }} - uses: actions/checkout@v4 - name: Set remote URL run: | # Attribute commits to the last committer on HEAD git config --global user.email "$(git log -1 --pretty=format:'%ae')" git config --global user.name "$(git log -1 --pretty=format:'%an')" git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY" - name: Set up Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true ruby-version: ruby - name: Release run: bundle exec rake release - name: Wait for release to propagate run: | gem install rubygems-await gem_tuple="$(ruby -rbundler/setup -rbundler -e ' spec = Bundler.definition.specs.find {|s| s.name == ARGV[0] } raise "No spec for #{ARGV[0]}" unless spec print [spec.name, spec.version, spec.platform].join(":") ' "jwt")" gem await "${gem_tuple}" ruby-jwt-3.1.2/.github/workflows/test.yml000066400000000000000000000054631503003570500204050ustar00rootroot00000000000000--- permissions: read-all name: test on: push: branches: - "*" pull_request: branches: - "*" jobs: rubocop: name: RuboCop timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Run RuboCop run: bundle exec rubocop test: name: ${{ matrix.os }} - Ruby ${{ matrix.ruby }} - ${{ matrix.gemfile }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest ruby: - "2.5" - "2.6" - "2.7" - "3.0" - "3.1" - "3.2" - "3.3" - "3.4" gemfile: - gemfiles/standalone.gemfile experimental: [false] include: - os: ubuntu-latest ruby: "2.5" gemfile: gemfiles/openssl.gemfile experimental: false - os: ubuntu-latest ruby: "truffleruby-head" gemfile: "gemfiles/standalone.gemfile" experimental: true - os: ubuntu-latest ruby: head gemfile: gemfiles/standalone.gemfile experimental: true continue-on-error: ${{ matrix.experimental }} env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run tests run: bundle exec rspec - name: Sanitize gemfile path run: echo "SANITIZED_GEMFILE=${{ matrix.gemfile }}" | tr '/' '-' >> $GITHUB_ENV - name: Upload test coverage folder for later reporting uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.os }}-${{ matrix.ruby }}-${{ env.SANITIZED_GEMFILE }} path: coverage/*.json retention-days: 1 coverage: name: Report coverage to Qlty runs-on: ubuntu-latest needs: test if: success() steps: - uses: actions/checkout@v4 - name: Download coverage reports from the test job uses: actions/download-artifact@v4 - uses: qltysh/qlty-action/coverage@v1 with: token: ${{ secrets.QLTY_COVERAGE_TOKEN }} files: coverage-*/*.json smoke: name: Built GEM smoke test timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ruby - name: Build GEM run: gem build - name: Install built GEM run: gem install jwt-*.gem - name: Run test run: bin/smoke.rb ruby-jwt-3.1.2/.gitignore000066400000000000000000000002301503003570500152610ustar00rootroot00000000000000.idea/ jwt.gemspec pkg Gemfile.lock coverage/ .DS_Store .rbenv-gemsets .ruby-version .vscode/ .bundle *gemfile.lock .byebug_history *.gem doc/ .yardoc/ ruby-jwt-3.1.2/.markdownlint.json000066400000000000000000000000251503003570500167550ustar00rootroot00000000000000{ "MD013": false } ruby-jwt-3.1.2/.qlty/000077500000000000000000000000001503003570500143455ustar00rootroot00000000000000ruby-jwt-3.1.2/.qlty/.gitignore000066400000000000000000000000771503003570500163410ustar00rootroot00000000000000* !configs !configs/** !hooks !hooks/** !qlty.toml !.gitignore ruby-jwt-3.1.2/.qlty/configs/000077500000000000000000000000001503003570500157755ustar00rootroot00000000000000ruby-jwt-3.1.2/.qlty/configs/.yamllint.yaml000066400000000000000000000002621503003570500205700ustar00rootroot00000000000000rules: document-start: disable quoted-strings: required: only-when-needed extra-allowed: ["{|}"] key-duplicates: {} octal-values: forbid-implicit-octal: true ruby-jwt-3.1.2/.qlty/qlty.toml000066400000000000000000000030531503003570500162340ustar00rootroot00000000000000# This file was automatically generated by `qlty init`. # You can modify it to suit your needs. # We recommend you to commit this file to your repository. # # This configuration is used by both Qlty CLI and Qlty Cloud. # # Qlty CLI -- Code quality toolkit for developers # Qlty Cloud -- Fully automated Code Health Platform # # Try Qlty Cloud: https://qlty.sh # # For a guide to configuration, visit https://qlty.sh/d/config # Or for a full reference, visit https://qlty.sh/d/qlty-toml config_version = "0" exclude_patterns = [ "*_min.*", "*-min.*", "*.min.*", "**/.yarn/**", "**/*.d.ts", "**/assets/**", "**/bower_components/**", "**/build/**", "**/cache/**", "**/config/**", "**/db/**", "**/deps/**", "**/dist/**", "**/extern/**", "**/external/**", "**/generated/**", "**/Godeps/**", "**/gradlew/**", "**/mvnw/**", "**/node_modules/**", "**/protos/**", "**/seed/**", "**/target/**", "**/templates/**", "**/testdata/**", "**/vendor/**", ] test_patterns = [ "**/test/**", "**/spec/**", "**/*.test.*", "**/*.spec.*", "**/*_test.*", "**/*_spec.*", "**/test_*.*", "**/spec_*.*", ] [smells] mode = "comment" [[source]] name = "default" default = true [[plugin]] name = "actionlint" [[plugin]] name = "checkov" [[plugin]] name = "markdownlint" mode = "comment" [[plugin]] name = "prettier" [[plugin]] name = "ripgrep" mode = "comment" [[plugin]] name = "rubocop" version = "1.76.1" [[plugin]] name = "trivy" drivers = [ "config", ] [[plugin]] name = "trufflehog" [[plugin]] name = "yamllint" ruby-jwt-3.1.2/.rspec000066400000000000000000000000361503003570500144120ustar00rootroot00000000000000--require spec_helper --color ruby-jwt-3.1.2/.rubocop.yml000066400000000000000000000006271503003570500155550ustar00rootroot00000000000000AllCops: TargetRubyVersion: 2.5 NewCops: enable SuggestExtensions: false Exclude: - gemfiles/*.gemfile - vendor/**/* Metrics/AbcSize: Max: 25 Metrics/MethodLength: Max: 18 Metrics/BlockLength: Exclude: - spec/**/*_spec.rb - "*.gemspec" Layout/LineLength: Enabled: false Gemspec/DevelopmentDependencies: EnforcedStyle: gemspec Naming/PredicateMethod: Enabled: false ruby-jwt-3.1.2/.simplecov000066400000000000000000000005361503003570500153040ustar00rootroot00000000000000# frozen_string_literal: true require 'openssl' require 'simplecov_json_formatter' SimpleCov.start do command_name "Job #{File.basename(ENV['BUNDLE_GEMFILE'])}" if ENV['BUNDLE_GEMFILE'] project_name 'Ruby JWT - Ruby JSON Web Token implementation' add_filter 'spec' end SimpleCov.formatters = SimpleCov::Formatter::JSONFormatter if ENV['CI'] ruby-jwt-3.1.2/.yardopts000066400000000000000000000001271503003570500151440ustar00rootroot00000000000000--protected --no-private - README.md CHANGELOG.md CONTRIBUTING.md UPGRADING.md LICENSE ruby-jwt-3.1.2/AUTHORS000066400000000000000000000030721503003570500143500ustar00rootroot00000000000000Tim Rudat Joakim Antman Jeff Lindsay A.B shields Bob Aman Emilio Cristalli Egon Zemmer Zane Shannon Nikita Shatov Paul Battley Oliver blackanger Ville Lautanala Tyler Pickett James Stonehill Adam Michael Martin Emde Saverio Trioni Peter M. Goldstein Korstiaan de Ridder Richard Larocque Andrew Davis Yason Khaburzaniya Klaas Jan Wierenga Nick Hammond Bart de Water Steve Sloan Antonis Berkakis Bill Mill Kevin Olbrich Simon Fish jb08 lukas Rodrigo López Dato ojab Ritikesh sawyerzhang Larry Lv smudge wohlgejm Tom Wey yann ARMAND Brian Flethcer Jurriaan Pruis Erik Michaels-Ober Matthew Simpson Steven Davidovitz Nicolas Leger Pierre Michard RahulBajaj Rob Wygand Ryan Brushett Ryan McIlmoyl Ryan Metzler Severin Schoepke Shaun Guth Steve Teti T.J. Schuck Taiki Sugawara Takehiro Adachi Tobias Haar Toby Pinder Tomé Duarte Travis Hunter Yuji Yaginuma Zuzanna Stolińska aarongray danielgrippi fusagiko/takayamaki mai fujii nycvotes-dev revodoge rono23 antonmorant Adam Greene Alexander Boyd Alexandr Kostrikov Aman Gupta Ariel Salomon Arnaud Mesureur Artsiom Kuts Austin Kabiru B Bouke van der Bijl Brandon Keepers Dan Leyden Dave Grijalva Dmitry Pashkevich Dorian Marié Ernie Miller Evgeni Golov Ewoud Kohl van Wijngaarden HoneyryderChuck Igor Victor Ilyaaaaaaaaaaaaa Zhitomirskiy Jens Hausherr Jeremiah Wuenschel John Downey Jordan Brough Josh Bodah JotaSe Juanito Fatas Julio Lopez Katelyn Kasperowicz Leonardo Saraiva Lowell Kirsh Loïc Lengrand Lucas Mazza Makoto Chiba Manuel Bustillo Marco Adkins Meredith Leu Micah Gates Michał Begejowicz Mike Eirih Mike Pastore Mingan Mitch Birti ruby-jwt-3.1.2/Appraisals000066400000000000000000000002371503003570500153220ustar00rootroot00000000000000# frozen_string_literal: true appraise 'standalone' do remove_gem 'rubocop' end appraise 'openssl' do gem 'openssl', '~> 2.1' remove_gem 'rubocop' end ruby-jwt-3.1.2/CHANGELOG.md000066400000000000000000002024031503003570500151100ustar00rootroot00000000000000# Changelog ## [v3.1.2](https://github.com/jwt/ruby-jwt/tree/v3.1.2) (2025-06-28) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.1.1...v3.1.2) **Fixes and enhancements:** - Avoid using the same digest across calls in JWT::JWA::Ecdsa and JWT::JWA::Rsa [#697](https://github.com/jwt/ruby-jwt/pull/697) - Fix signing with a EC JWK [#699](https://github.com/jwt/ruby-jwt/pull/699) ([@anakinj](https://github.com/anakinj)) ## [v3.1.1](https://github.com/jwt/ruby-jwt/tree/v3.1.1) (2025-06-24) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.1.0...v3.1.1) **Fixes and enhancements:** - Require the algorithm to be provided when signing and verifying tokens using JWKs [#695](https://github.com/jwt/ruby-jwt/pull/695) ([@anakinj](https://github.com/anakinj)) ## [v3.1.0](https://github.com/jwt/ruby-jwt/tree/v3.1.0) (2025-06-23) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.0.0...v3.1.0) **Features:** - Add support for x5t header parameter for X.509 certificate thumbprint verification [#669](https://github.com/jwt/ruby-jwt/pull/669) ([@hieuk09](https://github.com/hieuk09)) - Raise an error if the ECDSA signing or verification key is not an instance of `OpenSSL::PKey::EC` [#688](https://github.com/jwt/ruby-jwt/pull/688) ([@anakinj](https://github.com/anakinj)) - Allow `OpenSSL::PKey::EC::Point` to be used as the verification key in ECDSA [#689](https://github.com/jwt/ruby-jwt/pull/689) ([@anakinj](https://github.com/anakinj)) - Require claims to have been verified before accessing the `JWT::EncodedToken#payload` [#690](https://github.com/jwt/ruby-jwt/pull/690) ([@anakinj](https://github.com/anakinj)) - Support signing and verifying tokens using a JWK [#692](https://github.com/jwt/ruby-jwt/pull/692) ([@anakinj](https://github.com/anakinj)) ## [v3.0.0](https://github.com/jwt/ruby-jwt/tree/v3.0.0) (2025-06-14) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.10.1...v3.0.0) **Breaking changes:** - Require token signature to be verified before accessing payload [#648](https://github.com/jwt/ruby-jwt/pull/648) ([@anakinj](https://github.com/anakinj)) - Drop support for the HS512256 algorithm [#650](https://github.com/jwt/ruby-jwt/pull/650) ([@anakinj](https://github.com/anakinj)) - Remove deprecated claim verification methods [#654](https://github.com/jwt/ruby-jwt/pull/654) ([@anakinj](https://github.com/anakinj)) - Remove dependency to rbnacl [#655](https://github.com/jwt/ruby-jwt/pull/655) ([@anakinj](https://github.com/anakinj)) - Support only stricter base64 decoding (RFC 4648) [#658](https://github.com/jwt/ruby-jwt/pull/658) ([@anakinj](https://github.com/anakinj)) - Custom algorithms are required to include `JWT::JWA::SigningAlgorithm` [#660](https://github.com/jwt/ruby-jwt/pull/660) ([@anakinj](https://github.com/anakinj)) - Require RSA keys to be at least 2048 bits [#661](https://github.com/jwt/ruby-jwt/pull/661) ([@anakinj](https://github.com/anakinj)) - Base64 encode and decode the k value for HMAC JWKs [#662](https://github.com/jwt/ruby-jwt/pull/662) ([@anakinj](https://github.com/anakinj)) Take a look at the [upgrade guide](UPGRADING.md) for more details. **Features:** - JWT::EncodedToken#verify! method that bundles signature and claim validation [#647](https://github.com/jwt/ruby-jwt/pull/647) ([@anakinj](https://github.com/anakinj)) - Do not override the alg header if already given [#659](https://github.com/jwt/ruby-jwt/pull/659) ([@anakinj](https://github.com/anakinj)) - Make `JWK::KeyFinder` compatible with `JWT::EncodedToken` [#663](https://github.com/jwt/ruby-jwt/pull/663) ([@anakinj](https://github.com/anakinj)) **Fixes and enhancements:** - Ruby 3.4 to CI matrix [#649](https://github.com/jwt/ruby-jwt/pull/649) ([@anakinj](https://github.com/anakinj)) - Add logger as development dependency [#670](https://github.com/jwt/ruby-jwt/pull/670) ([@hieuk09](https://github.com/hieuk09)) ## [v2.10.1](https://github.com/jwt/ruby-jwt/tree/v2.10.1) (2024-12-26) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.10.0...v2.10.1) **Fixes and enhancements:** - Make version constants public again [#646](https://github.com/jwt/ruby-jwt/pull/646) ([@anakinj](https://github.com/anakinj)) ## [v2.10.0](https://github.com/jwt/ruby-jwt/tree/v2.10.0) (2024-12-25) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.9.3...v2.10.0) **Features:** - JWT::Token and JWT::EncodedToken for signing and verifying tokens [#621](https://github.com/jwt/ruby-jwt/pull/621) ([@anakinj](https://github.com/anakinj)) - Detached payload support for JWT::Token and JWT::EncodedToken [#630](https://github.com/jwt/ruby-jwt/pull/630) ([@anakinj](https://github.com/anakinj)) - Skip decoding payload if b64 header is present and false [#631](https://github.com/jwt/ruby-jwt/pull/631) ([@anakinj](https://github.com/anakinj)) - Remove a few custom Rubocop configs [#638](https://github.com/jwt/ruby-jwt/pull/638) ([@anakinj](https://github.com/anakinj)) **Fixes and enhancements:** - Deprecation warnings for deprecated methods and classes [#629](https://github.com/jwt/ruby-jwt/pull/629) ([@anakinj](https://github.com/anakinj)) - Improved documentation for public apis [#629](https://github.com/jwt/ruby-jwt/pull/629) ([@anakinj](https://github.com/anakinj)) - Use correct methods when raising error during signing/verification with EdDSA [#633](https://github.com/jwt/ruby-jwt/pull/633) - Fix JWT::EncodedToken behavior with empty string as token [#640](https://github.com/jwt/ruby-jwt/pull/640) ([@ragalie](https://github.com/ragalie)) - Deprecation warnings for rbnacl backed functionality [#641](https://github.com/jwt/ruby-jwt/pull/641) ([@anakinj](https://github.com/anakinj)) ## [v2.9.3](https://github.com/jwt/ruby-jwt/tree/v2.9.3) (2024-10-03) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.9.2...v2.9.3) **Fixes and enhancements:** - Return truthy value for `::JWT::ClaimsValidator#validate!` and `::JWT::Verify.verify_claims` [#628](https://github.com/jwt/ruby-jwt/pull/628) ([@anakinj](https://github.com/anakinj)) ## [v2.9.2](https://github.com/jwt/ruby-jwt/tree/v2.9.2) (2024-10-03) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.9.1...v2.9.2) **Features:** - Standalone claim verification interface [#626](https://github.com/jwt/ruby-jwt/pull/626) ([@anakinj](https://github.com/anakinj)) **Fixes and enhancements:** - Updated README to correctly document `OpenSSL::HMAC` documentation [#617](https://github.com/jwt/ruby-jwt/pull/617) ([@aedryan](https://github.com/aedryan)) - Verify JWT header format [#622](https://github.com/jwt/ruby-jwt/pull/622) ([@304](https://github.com/304)) - Bring back `::JWT::ClaimsValidator`, `::JWT::Verify` and a few other removed interfaces for preserved backwards compatibility [#624](https://github.com/jwt/ruby-jwt/pull/624) ([@anakinj](https://github.com/anakinj)) ## [v2.9.1](https://github.com/jwt/ruby-jwt/tree/v2.9.1) (2024-09-23) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.9.0...v2.9.1) **Fixes and enhancements:** - Fix regression in `iss` and `aud` claim validation [#619](https://github.com/jwt/ruby-jwt/pull/619) ([@anakinj](https://github.com/anakinj)) ## [v2.9.0](https://github.com/jwt/ruby-jwt/tree/v2.9.0) (2024-09-15) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.8.2...v2.9.0) **Features:** - Build and push gem using a GH action [#612](https://github.com/jwt/ruby-jwt/pull/612) ([@anakinj](https://github.com/anakinj)) **Fixes and enhancements:** - Refactor claim validators into their own classes [#605](https://github.com/jwt/ruby-jwt/pull/605) ([@anakinj](https://github.com/anakinj), [@MatteoPierro](https://github.com/MatteoPierro)) - Allow extending available algorithms [#607](https://github.com/jwt/ruby-jwt/pull/607) ([@anakinj](https://github.com/anakinj)) - Do not include the EdDSA algorithm if rbnacl not available [#613](https://github.com/jwt/ruby-jwt/pull/613) ([@anakinj](https://github.com/anakinj)) ## [v2.8.2](https://github.com/jwt/ruby-jwt/tree/v2.8.2) (2024-06-18) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.8.1...v2.8.2) **Fixes and enhancements:** - Print deprecation warnings only on when token decoding succeeds [#600](https://github.com/jwt/ruby-jwt/pull/600) ([@anakinj](https://github.com/anakinj)) - Unify code style [#602](https://github.com/jwt/ruby-jwt/pull/602) ([@anakinj](https://github.com/anakinj)) ## [v2.8.1](https://github.com/jwt/ruby-jwt/tree/v2.8.1) (2024-02-29) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.8.0...v2.8.1) **Features:** - Configurable base64 decode behaviour [#589](https://github.com/jwt/ruby-jwt/pull/589) ([@anakinj](https://github.com/anakinj)) **Fixes and enhancements:** - Output deprecation warnings once [#589](https://github.com/jwt/ruby-jwt/pull/589) ([@anakinj](https://github.com/anakinj)) ## [v2.8.0](https://github.com/jwt/ruby-jwt/tree/v2.8.0) (2024-02-17) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.7.1...v2.8.0) **Features:** - Updated rubocop to 1.56 [#573](https://github.com/jwt/ruby-jwt/pull/573) ([@anakinj](https://github.com/anakinj)) - Run CI on Ruby 3.3 [#577](https://github.com/jwt/ruby-jwt/pull/577) ([@anakinj](https://github.com/anakinj)) - Deprecation warning added for the HMAC algorithm HS512256 (HMAC-SHA-512 truncated to 256-bits) [#575](https://github.com/jwt/ruby-jwt/pull/575) ([@anakinj](https://github.com/anakinj)) - Stop using RbNaCl for standard HMAC algorithms [#575](https://github.com/jwt/ruby-jwt/pull/575) ([@anakinj](https://github.com/anakinj)) **Fixes and enhancements:** - Fix signature has expired error if payload is a string [#555](https://github.com/jwt/ruby-jwt/pull/555) ([@GobinathAL](https://github.com/GobinathAL)) - Fix key base equality and spaceship operators [#569](https://github.com/jwt/ruby-jwt/pull/569) ([@magneland](https://github.com/magneland)) - Remove explicit base64 require from x5c_key_finder [#580](https://github.com/jwt/ruby-jwt/pull/580) ([@anakinj](https://github.com/anakinj)) - Performance improvements and cleanup of tests [#581](https://github.com/jwt/ruby-jwt/pull/581) ([@anakinj](https://github.com/anakinj)) - Repair EC x/y coordinates when importing JWK [#585](https://github.com/jwt/ruby-jwt/pull/585) ([@julik](https://github.com/julik)) - Explicit dependency to the base64 gem [#582](https://github.com/jwt/ruby-jwt/pull/582) ([@anakinj](https://github.com/anakinj)) - Deprecation warning for decoding content not compliant with RFC 4648 [#582](https://github.com/jwt/ruby-jwt/pull/582) ([@anakinj](https://github.com/anakinj)) - Algorithms moved under the `::JWT::JWA` module ([@anakinj](https://github.com/anakinj)) ## [v2.7.1](https://github.com/jwt/ruby-jwt/tree/v2.8.0) (2023-06-09) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.7.0...v2.7.1) **Fixes and enhancements:** - Handle invalid algorithm when decoding JWT [#559](https://github.com/jwt/ruby-jwt/pull/559) ([@nataliastanko](https://github.com/nataliastanko)) - Do not raise error when verifying bad HMAC signature [#563](https://github.com/jwt/ruby-jwt/pull/563) ([@hieuk09](https://github.com/hieuk09)) ## [v2.7.0](https://github.com/jwt/ruby-jwt/tree/v2.7.0) (2023-02-01) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.6.0...v2.7.0) **Features:** - Support OKP (Ed25519) keys for JWKs [#540](https://github.com/jwt/ruby-jwt/pull/540) ([@anakinj](https://github.com/anakinj)) - JWK Sets can now be used for tokens with nil kid [#543](https://github.com/jwt/ruby-jwt/pull/543) ([@bellebaum](https://github.com/bellebaum)) **Fixes and enhancements:** - Fix issue with multiple keys returned by keyfinder and multiple allowed algorithms [#545](https://github.com/jwt/ruby-jwt/pull/545) ([@mpospelov](https://github.com/mpospelov)) - Non-string `kid` header values are now rejected [#543](https://github.com/jwt/ruby-jwt/pull/543) ([@bellebaum](https://github.com/bellebaum)) ## [v2.6.0](https://github.com/jwt/ruby-jwt/tree/v2.6.0) (2022-12-22) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.5.0...v2.6.0) **Features:** - Support custom algorithms by passing algorithm objects [#512](https://github.com/jwt/ruby-jwt/pull/512) ([@anakinj](https://github.com/anakinj)) - Support descriptive (not key related) JWK parameters [#520](https://github.com/jwt/ruby-jwt/pull/520) ([@bellebaum](https://github.com/bellebaum)) - Support for JSON Web Key Sets [#525](https://github.com/jwt/ruby-jwt/pull/525) ([@bellebaum](https://github.com/bellebaum)) - Support HMAC keys over 32 chars when using RbNaCl [#521](https://github.com/jwt/ruby-jwt/pull/521) ([@anakinj](https://github.com/anakinj)) **Fixes and enhancements:** - Raise descriptive error on empty hmac_secret and OpenSSL 3.0/openssl gem <3.0.1 [#530](https://github.com/jwt/ruby-jwt/pull/530) ([@jonmchan](https://github.com/jonmchan)) ## [v2.5.0](https://github.com/jwt/ruby-jwt/tree/v2.5.0) (2022-08-25) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.1...v2.5.0) **Features:** - Support JWK thumbprints as key ids [#481](https://github.com/jwt/ruby-jwt/pull/481) ([@anakinj](https://github.com/anakinj)) - Support OpenSSL >= 3.0 [#496](https://github.com/jwt/ruby-jwt/pull/496) ([@anakinj](https://github.com/anakinj)) **Fixes and enhancements:** - Bring back the old Base64 (RFC2045) deocode mechanisms [#488](https://github.com/jwt/ruby-jwt/pull/488) ([@anakinj](https://github.com/anakinj)) - Rescue RbNaCl exception for EdDSA wrong key [#491](https://github.com/jwt/ruby-jwt/pull/491) ([@n-studio](https://github.com/n-studio)) - New parameter name for cases when kid is not found using JWK key loader proc [#501](https://github.com/jwt/ruby-jwt/pull/501) ([@anakinj](https://github.com/anakinj)) - Fix NoMethodError when a 2 segment token is missing 'alg' header [#502](https://github.com/jwt/ruby-jwt/pull/502) ([@cmrd-senya](https://github.com/cmrd-senya)) ## [v2.4.1](https://github.com/jwt/ruby-jwt/tree/v2.4.1) (2022-06-07) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.0...v2.4.1) **Fixes and enhancements:** - Raise JWT::DecodeError on invalid signature [\#484](https://github.com/jwt/ruby-jwt/pull/484) ([@freakyfelt!](https://github.com/freakyfelt!)) ## [v2.4.0](https://github.com/jwt/ruby-jwt/tree/v2.4.0) (2022-06-06) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.3.0...v2.4.0) **Features:** - Dropped support for Ruby 2.5 and older [#453](https://github.com/jwt/ruby-jwt/pull/453) - ([@anakinj](https://github.com/anakinj)) - Use Ruby built-in url-safe base64 methods [#454](https://github.com/jwt/ruby-jwt/pull/454) - ([@bdewater](https://github.com/bdewater)) - Updated rubocop to 1.23.0 [#457](https://github.com/jwt/ruby-jwt/pull/457) - ([@anakinj](https://github.com/anakinj)) - Add x5c header key finder [#338](https://github.com/jwt/ruby-jwt/pull/338) - ([@bdewater](https://github.com/bdewater)) - Author driven changelog process [#463](https://github.com/jwt/ruby-jwt/pull/463) - ([@anakinj](https://github.com/anakinj)) - Allow regular expressions and procs to verify issuer [\#437](https://github.com/jwt/ruby-jwt/pull/437) ([rewritten](https://github.com/rewritten)) - Add Support to be able to verify from multiple keys [\#425](https://github.com/jwt/ruby-jwt/pull/425) ([ritikesh](https://github.com/ritikesh)) **Fixes and enhancements:** - Readme: Typo fix re MissingRequiredClaim [\#451](https://github.com/jwt/ruby-jwt/pull/451) ([antonmorant](https://github.com/antonmorant)) - Fix RuboCop TODOs [\#476](https://github.com/jwt/ruby-jwt/pull/476) ([typhoon2099](https://github.com/typhoon2099)) - Make specific algorithms in README linkable [\#472](https://github.com/jwt/ruby-jwt/pull/472) ([milieu](https://github.com/milieu)) - Update note about supported JWK types [\#475](https://github.com/jwt/ruby-jwt/pull/475) ([dpashkevich](https://github.com/dpashkevich)) - Create CODE_OF_CONDUCT.md [\#449](https://github.com/jwt/ruby-jwt/pull/449) ([loic5](https://github.com/loic5)) ## [v2.3.0](https://github.com/jwt/ruby-jwt/tree/v2.3.0) (2021-10-03) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.2.3...v2.3.0) **Closed issues:** - \[SECURITY\] Algorithm Confusion Through kid Header [\#440](https://github.com/jwt/ruby-jwt/issues/440) - JWT to memory [\#436](https://github.com/jwt/ruby-jwt/issues/436) - ArgumentError: wrong number of arguments \(given 2, expected 1\) [\#429](https://github.com/jwt/ruby-jwt/issues/429) - HMAC section of README outdated [\#421](https://github.com/jwt/ruby-jwt/issues/421) - NoMethodError: undefined method `zero?' for nil:NilClass if JWT has no 'alg' field [\#410](https://github.com/jwt/ruby-jwt/issues/410) - Release new version [\#409](https://github.com/jwt/ruby-jwt/issues/409) - NameError: uninitialized constant JWT::JWK [\#403](https://github.com/jwt/ruby-jwt/issues/403) **Merged pull requests:** - Release 2.3.0 [\#448](https://github.com/jwt/ruby-jwt/pull/448) ([excpt](https://github.com/excpt)) - Fix Style/MultilineIfModifier issues [\#447](https://github.com/jwt/ruby-jwt/pull/447) ([anakinj](https://github.com/anakinj)) - feat\(EdDSA\): Accept EdDSA as algorithm header [\#446](https://github.com/jwt/ruby-jwt/pull/446) ([Pierre-Michard](https://github.com/Pierre-Michard)) - Pass kid param through JWT::JWK.create_from [\#445](https://github.com/jwt/ruby-jwt/pull/445) ([shaun-guth-allscripts](https://github.com/shaun-guth-allscripts)) - fix document about passing JWKs as a simple Hash [\#443](https://github.com/jwt/ruby-jwt/pull/443) ([takayamaki](https://github.com/takayamaki)) - Tests for mixing JWK keys with mismatching algorithms [\#441](https://github.com/jwt/ruby-jwt/pull/441) ([anakinj](https://github.com/anakinj)) - verify_claims test shouldnt be within the verify_sub test [\#431](https://github.com/jwt/ruby-jwt/pull/431) ([andyjdavis](https://github.com/andyjdavis)) - Allow decode options to specify required claims [\#430](https://github.com/jwt/ruby-jwt/pull/430) ([andyjdavis](https://github.com/andyjdavis)) - Fix OpenSSL::PKey::EC public_key handing in tests [\#427](https://github.com/jwt/ruby-jwt/pull/427) ([anakinj](https://github.com/anakinj)) - Add documentation for find_key [\#426](https://github.com/jwt/ruby-jwt/pull/426) ([ritikesh](https://github.com/ritikesh)) - Give ruby 3.0 as a string to avoid number formatting issues [\#424](https://github.com/jwt/ruby-jwt/pull/424) ([anakinj](https://github.com/anakinj)) - Tests for iat verification behaviour [\#423](https://github.com/jwt/ruby-jwt/pull/423) ([anakinj](https://github.com/anakinj)) - Remove HMAC with nil secret from documentation [\#422](https://github.com/jwt/ruby-jwt/pull/422) ([boardfish](https://github.com/boardfish)) - Update broken link in README [\#420](https://github.com/jwt/ruby-jwt/pull/420) ([severin](https://github.com/severin)) - Add metadata for RubyGems [\#418](https://github.com/jwt/ruby-jwt/pull/418) ([nickhammond](https://github.com/nickhammond)) - Fixed a typo about class name [\#417](https://github.com/jwt/ruby-jwt/pull/417) ([mai-f](https://github.com/mai-f)) - Fix references for v2.2.3 on CHANGELOG [\#416](https://github.com/jwt/ruby-jwt/pull/416) ([vyper](https://github.com/vyper)) - Raise IncorrectAlgorithm if token has no alg header [\#411](https://github.com/jwt/ruby-jwt/pull/411) ([bouk](https://github.com/bouk)) ## [v2.2.3](https://github.com/jwt/ruby-jwt/tree/v2.2.3) (2021-04-19) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.2.2...v2.2.3) **Implemented enhancements:** - Verify algorithm before evaluating keyfinder [\#343](https://github.com/jwt/ruby-jwt/issues/343) - Why jwt depends on json \< 2.0 ? [\#179](https://github.com/jwt/ruby-jwt/issues/179) - Support for JWK in-lieu of rsa_public [\#158](https://github.com/jwt/ruby-jwt/issues/158) - Fix rspec `raise_error` warning [\#413](https://github.com/jwt/ruby-jwt/pull/413) ([excpt](https://github.com/excpt)) - Add support for JWKs with HMAC key type. [\#372](https://github.com/jwt/ruby-jwt/pull/372) ([phlegx](https://github.com/phlegx)) - Improve 'none' algorithm handling [\#365](https://github.com/jwt/ruby-jwt/pull/365) ([danleyden](https://github.com/danleyden)) - Handle parsed JSON JWKS input with string keys [\#348](https://github.com/jwt/ruby-jwt/pull/348) ([martinemde](https://github.com/martinemde)) - Allow Numeric values during encoding [\#327](https://github.com/jwt/ruby-jwt/pull/327) ([fanfilmu](https://github.com/fanfilmu)) **Closed issues:** - "Signature verification raised", yet jwt.io says "Signature Verified" [\#401](https://github.com/jwt/ruby-jwt/issues/401) - truffleruby-head build is failing [\#396](https://github.com/jwt/ruby-jwt/issues/396) - JWT::JWK::EC needs `require 'forwardable'` [\#392](https://github.com/jwt/ruby-jwt/issues/392) - How to use a 'signing key' as used by next-auth [\#389](https://github.com/jwt/ruby-jwt/issues/389) - undefined method `verify' for nil:NilClass when validate a JWT with JWK [\#383](https://github.com/jwt/ruby-jwt/issues/383) - Make specifying "algorithm" optional on decode [\#380](https://github.com/jwt/ruby-jwt/issues/380) - ADFS created access tokens can't be validated due to missing 'kid' header [\#370](https://github.com/jwt/ruby-jwt/issues/370) - new version? [\#355](https://github.com/jwt/ruby-jwt/issues/355) - JWT gitlab OmniAuth provider setup support [\#354](https://github.com/jwt/ruby-jwt/issues/354) - Release with support for RSA.import for ruby \< 2.4 hasn't been released [\#347](https://github.com/jwt/ruby-jwt/issues/347) - cannot load such file -- jwt [\#339](https://github.com/jwt/ruby-jwt/issues/339) **Merged pull requests:** - Prepare 2.2.3 release [\#415](https://github.com/jwt/ruby-jwt/pull/415) ([excpt](https://github.com/excpt)) - Remove codeclimate code coverage dev dependency [\#414](https://github.com/jwt/ruby-jwt/pull/414) ([excpt](https://github.com/excpt)) - Add forwardable dependency [\#408](https://github.com/jwt/ruby-jwt/pull/408) ([anakinj](https://github.com/anakinj)) - Ignore casing of algorithm [\#405](https://github.com/jwt/ruby-jwt/pull/405) ([johnnyshields](https://github.com/johnnyshields)) - Document function and add tests for verify claims method [\#404](https://github.com/jwt/ruby-jwt/pull/404) ([yasonk](https://github.com/yasonk)) - documenting calling verify_jti callback with 2 arguments in the readme [\#402](https://github.com/jwt/ruby-jwt/pull/402) ([HoneyryderChuck](https://github.com/HoneyryderChuck)) - Target the master branch on the build status badge [\#399](https://github.com/jwt/ruby-jwt/pull/399) ([anakinj](https://github.com/anakinj)) - Improving the local development experience [\#397](https://github.com/jwt/ruby-jwt/pull/397) ([anakinj](https://github.com/anakinj)) - Fix sourcelevel broken links [\#395](https://github.com/jwt/ruby-jwt/pull/395) ([anakinj](https://github.com/anakinj)) - Don't recommend installing gem with sudo [\#391](https://github.com/jwt/ruby-jwt/pull/391) ([tjschuck](https://github.com/tjschuck)) - Enable rubocop locally and on ci [\#390](https://github.com/jwt/ruby-jwt/pull/390) ([anakinj](https://github.com/anakinj)) - Ci and test cleanup [\#387](https://github.com/jwt/ruby-jwt/pull/387) ([anakinj](https://github.com/anakinj)) - Make JWT::JWK::EC compatible with Ruby 2.3 [\#386](https://github.com/jwt/ruby-jwt/pull/386) ([anakinj](https://github.com/anakinj)) - Support JWKs for pre 2.3 rubies [\#382](https://github.com/jwt/ruby-jwt/pull/382) ([anakinj](https://github.com/anakinj)) - Replace Travis CI with GitHub Actions \(also favor openssl/rbnacl combinations over rails compatibility tests\) [\#381](https://github.com/jwt/ruby-jwt/pull/381) ([anakinj](https://github.com/anakinj)) - Add auth0 sponsor message [\#379](https://github.com/jwt/ruby-jwt/pull/379) ([excpt](https://github.com/excpt)) - Adapt HMAC to JWK RSA code style. [\#378](https://github.com/jwt/ruby-jwt/pull/378) ([phlegx](https://github.com/phlegx)) - Disable Rails cops [\#376](https://github.com/jwt/ruby-jwt/pull/376) ([anakinj](https://github.com/anakinj)) - Support exporting RSA JWK private keys [\#375](https://github.com/jwt/ruby-jwt/pull/375) ([anakinj](https://github.com/anakinj)) - Ebert is SourceLevel nowadays [\#374](https://github.com/jwt/ruby-jwt/pull/374) ([anakinj](https://github.com/anakinj)) - Add support for JWKs with EC key type [\#371](https://github.com/jwt/ruby-jwt/pull/371) ([richardlarocque](https://github.com/richardlarocque)) - Add Truffleruby head to CI [\#368](https://github.com/jwt/ruby-jwt/pull/368) ([gogainda](https://github.com/gogainda)) - Add more docs about JWK support [\#341](https://github.com/jwt/ruby-jwt/pull/341) ([take](https://github.com/take)) ## [v2.2.2](https://github.com/jwt/ruby-jwt/tree/v2.2.2) (2020-08-18) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.2.1...v2.2.2) **Implemented enhancements:** - JWK does not decode. [\#332](https://github.com/jwt/ruby-jwt/issues/332) - Inconsistent use of symbol and string keys in args \(exp and alrogithm\). [\#331](https://github.com/jwt/ruby-jwt/issues/331) - Pin simplecov to \< 0.18 [\#356](https://github.com/jwt/ruby-jwt/pull/356) ([anakinj](https://github.com/anakinj)) - verifies algorithm before evaluating keyfinder [\#346](https://github.com/jwt/ruby-jwt/pull/346) ([jb08](https://github.com/jb08)) - Update Rails 6 appraisal to use actual release version [\#336](https://github.com/jwt/ruby-jwt/pull/336) ([smudge](https://github.com/smudge)) - Update Travis [\#326](https://github.com/jwt/ruby-jwt/pull/326) ([berkos](https://github.com/berkos)) - Improvement/encode hmac without key [\#312](https://github.com/jwt/ruby-jwt/pull/312) ([JotaSe](https://github.com/JotaSe)) **Fixed bugs:** - v2.2.1 warning: already initialized constant JWT Error [\#335](https://github.com/jwt/ruby-jwt/issues/335) - 2.2.1 is no longer raising `JWT::DecodeError` on `nil` verification key [\#328](https://github.com/jwt/ruby-jwt/issues/328) - Fix algorithm picking from decode options [\#359](https://github.com/jwt/ruby-jwt/pull/359) ([excpt](https://github.com/excpt)) - Raise error when verification key is empty [\#358](https://github.com/jwt/ruby-jwt/pull/358) ([anakinj](https://github.com/anakinj)) **Closed issues:** - JWT RSA: is it possible to encrypt using the public key? [\#366](https://github.com/jwt/ruby-jwt/issues/366) - Example unsigned token that bypasses verification [\#364](https://github.com/jwt/ruby-jwt/issues/364) - Verify exp claim/field even if it's not present [\#363](https://github.com/jwt/ruby-jwt/issues/363) - Decode any token [\#360](https://github.com/jwt/ruby-jwt/issues/360) - \[question\] example of using a pub/priv keys for signing? [\#351](https://github.com/jwt/ruby-jwt/issues/351) - JWT::ExpiredSignature raised for non-JSON payloads [\#350](https://github.com/jwt/ruby-jwt/issues/350) - verify_aud only verifies that at least one aud is expected [\#345](https://github.com/jwt/ruby-jwt/issues/345) - Sinatra 4.90s TTFB [\#344](https://github.com/jwt/ruby-jwt/issues/344) - How to Logout [\#342](https://github.com/jwt/ruby-jwt/issues/342) - jwt token decoding even when wrong token is provided for some letters [\#337](https://github.com/jwt/ruby-jwt/issues/337) - Need to use `symbolize_keys` everywhere! [\#330](https://github.com/jwt/ruby-jwt/issues/330) - eval\(\) used in Forwardable limits usage in iOS App Store [\#324](https://github.com/jwt/ruby-jwt/issues/324) - HS512256 OpenSSL Exception: First num too large [\#322](https://github.com/jwt/ruby-jwt/issues/322) - Can we change the separator character? [\#321](https://github.com/jwt/ruby-jwt/issues/321) - Verifying iat without leeway may break with poorly synced clocks [\#319](https://github.com/jwt/ruby-jwt/issues/319) - Adding support for 'hd' hosted domain string [\#314](https://github.com/jwt/ruby-jwt/issues/314) - There is no "typ" header in version 2.0.0 [\#233](https://github.com/jwt/ruby-jwt/issues/233) **Merged pull requests:** - Release v2.2.2 [\#367](https://github.com/jwt/ruby-jwt/pull/367) ([excpt](https://github.com/excpt)) - Fix 'already initialized constant JWT Error' [\#357](https://github.com/jwt/ruby-jwt/pull/357) ([excpt](https://github.com/excpt)) - Support RSA.import for all Ruby versions. [\#333](https://github.com/jwt/ruby-jwt/pull/333) ([rabajaj0509](https://github.com/rabajaj0509)) - Removed forwardable dependency [\#325](https://github.com/jwt/ruby-jwt/pull/325) ([anakinj](https://github.com/anakinj)) ## [v2.2.1](https://github.com/jwt/ruby-jwt/tree/v2.2.1) (2019-05-24) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.2.0...v2.2.1) **Fixed bugs:** - need to `require 'forwardable'` to use `Forwardable` [\#316](https://github.com/jwt/ruby-jwt/issues/316) - Add forwardable dependency for JWK RSA KeyFinder [\#317](https://github.com/jwt/ruby-jwt/pull/317) ([excpt](https://github.com/excpt)) **Merged pull requests:** - Release 2.2.1 [\#318](https://github.com/jwt/ruby-jwt/pull/318) ([excpt](https://github.com/excpt)) ## [v2.2.0](https://github.com/jwt/ruby-jwt/tree/v2.2.0) (2019-05-23) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.2.0.pre.beta.0...v2.2.0) **Closed issues:** - misspelled es512 curve name [\#310](https://github.com/jwt/ruby-jwt/issues/310) - With Base64 decode i can read the hashed content [\#306](https://github.com/jwt/ruby-jwt/issues/306) - hide post-it's for graphviz views [\#303](https://github.com/jwt/ruby-jwt/issues/303) **Merged pull requests:** - Release 2.2.0 [\#315](https://github.com/jwt/ruby-jwt/pull/315) ([excpt](https://github.com/excpt)) ## [v2.2.0.pre.beta.0](https://github.com/jwt/ruby-jwt/tree/v2.2.0.pre.beta.0) (2019-03-20) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.1.0...v2.2.0.pre.beta.0) **Implemented enhancements:** - Use iat_leeway option [\#273](https://github.com/jwt/ruby-jwt/issues/273) - Use of global state in latest version breaks thread safety of JWT.decode [\#268](https://github.com/jwt/ruby-jwt/issues/268) - JSON support [\#246](https://github.com/jwt/ruby-jwt/issues/246) - Change the Github homepage URL to https [\#301](https://github.com/jwt/ruby-jwt/pull/301) ([ekohl](https://github.com/ekohl)) - Fix Salt length for conformance with PS family specification. [\#300](https://github.com/jwt/ruby-jwt/pull/300) ([tobypinder](https://github.com/tobypinder)) - Add support for Ruby 2.6 [\#299](https://github.com/jwt/ruby-jwt/pull/299) ([bustikiller](https://github.com/bustikiller)) - update homepage in gemspec to use HTTPS [\#298](https://github.com/jwt/ruby-jwt/pull/298) ([evgeni](https://github.com/evgeni)) - Make sure alg parameter value isn't added twice [\#297](https://github.com/jwt/ruby-jwt/pull/297) ([korstiaan](https://github.com/korstiaan)) - Claims Validation [\#295](https://github.com/jwt/ruby-jwt/pull/295) ([jamesstonehill](https://github.com/jamesstonehill)) - JWT::Encode refactorings, alg and exp related bugfixes [\#293](https://github.com/jwt/ruby-jwt/pull/293) ([anakinj](https://github.com/anakinj)) - Proposal of simple JWK support [\#289](https://github.com/jwt/ruby-jwt/pull/289) ([anakinj](https://github.com/anakinj)) - Add RSASSA-PSS signature signing support [\#285](https://github.com/jwt/ruby-jwt/pull/285) ([oliver-hohn](https://github.com/oliver-hohn)) - Add note about using a hard coded algorithm in README [\#280](https://github.com/jwt/ruby-jwt/pull/280) ([revodoge](https://github.com/revodoge)) - Add Appraisal support [\#278](https://github.com/jwt/ruby-jwt/pull/278) ([olbrich](https://github.com/olbrich)) - Fix decode threading issue [\#269](https://github.com/jwt/ruby-jwt/pull/269) ([ab320012](https://github.com/ab320012)) - Removed leeway from verify_iat [\#257](https://github.com/jwt/ruby-jwt/pull/257) ([ab320012](https://github.com/ab320012)) **Fixed bugs:** - Inconsistent handling of payload claim data types [\#282](https://github.com/jwt/ruby-jwt/issues/282) - Issued at validation [\#247](https://github.com/jwt/ruby-jwt/issues/247) - Fix bug and simplify segment validation [\#292](https://github.com/jwt/ruby-jwt/pull/292) ([anakinj](https://github.com/anakinj)) **Security fixes:** - Decoding JWT with ES256 and secp256k1 curve [\#277](https://github.com/jwt/ruby-jwt/issues/277) **Closed issues:** - RS256, public and private keys [\#291](https://github.com/jwt/ruby-jwt/issues/291) - Allow passing current time to `decode` [\#288](https://github.com/jwt/ruby-jwt/issues/288) - Verify exp claim without verifying jwt [\#281](https://github.com/jwt/ruby-jwt/issues/281) - Audience as an array - how to specify? [\#276](https://github.com/jwt/ruby-jwt/issues/276) - signature validation using decode method for JWT [\#271](https://github.com/jwt/ruby-jwt/issues/271) - JWT is easily breakable [\#267](https://github.com/jwt/ruby-jwt/issues/267) - Ruby JWT Token [\#265](https://github.com/jwt/ruby-jwt/issues/265) - ECDSA supported algorithms constant is defined as a string, not an array [\#264](https://github.com/jwt/ruby-jwt/issues/264) - NoMethodError: undefined method `group' for \ [\#261](https://github.com/jwt/ruby-jwt/issues/261) - 'DecodeError'will replace 'ExpiredSignature' [\#260](https://github.com/jwt/ruby-jwt/issues/260) - TypeError: no implicit conversion of OpenSSL::PKey::RSA into String [\#259](https://github.com/jwt/ruby-jwt/issues/259) - NameError: uninitialized constant JWT::Algos::Eddsa::RbNaCl [\#258](https://github.com/jwt/ruby-jwt/issues/258) - Get new token if curren token expired [\#256](https://github.com/jwt/ruby-jwt/issues/256) - Infer algorithm from header [\#254](https://github.com/jwt/ruby-jwt/issues/254) - Why is the result of decode is an array? [\#252](https://github.com/jwt/ruby-jwt/issues/252) - Add support for headless token [\#251](https://github.com/jwt/ruby-jwt/issues/251) - Leeway or exp_leeway [\#215](https://github.com/jwt/ruby-jwt/issues/215) - Could you describe purpose of cert fixtures and their cryptokey lengths. [\#185](https://github.com/jwt/ruby-jwt/issues/185) **Merged pull requests:** - Release v2.2.0-beta.0 [\#302](https://github.com/jwt/ruby-jwt/pull/302) ([excpt](https://github.com/excpt)) - Misc config improvements [\#296](https://github.com/jwt/ruby-jwt/pull/296) ([jamesstonehill](https://github.com/jamesstonehill)) - Fix JSON conflict between \#293 and \#292 [\#294](https://github.com/jwt/ruby-jwt/pull/294) ([anakinj](https://github.com/anakinj)) - Drop Ruby 2.2 from test matrix [\#290](https://github.com/jwt/ruby-jwt/pull/290) ([anakinj](https://github.com/anakinj)) - Remove broken reek config [\#283](https://github.com/jwt/ruby-jwt/pull/283) ([excpt](https://github.com/excpt)) - Add missing test, Update common files [\#275](https://github.com/jwt/ruby-jwt/pull/275) ([excpt](https://github.com/excpt)) - Remove iat_leeway option [\#274](https://github.com/jwt/ruby-jwt/pull/274) ([wohlgejm](https://github.com/wohlgejm)) - improving code quality of jwt module [\#266](https://github.com/jwt/ruby-jwt/pull/266) ([ab320012](https://github.com/ab320012)) - fixed ECDSA supported versions const [\#263](https://github.com/jwt/ruby-jwt/pull/263) ([starbeast](https://github.com/starbeast)) - Added my name to contributor list [\#262](https://github.com/jwt/ruby-jwt/pull/262) ([ab320012](https://github.com/ab320012)) - Use `Class#new` Shorthand For Error Subclasses [\#255](https://github.com/jwt/ruby-jwt/pull/255) ([akabiru](https://github.com/akabiru)) - \[CI\] Test against Ruby 2.5 [\#253](https://github.com/jwt/ruby-jwt/pull/253) ([nicolasleger](https://github.com/nicolasleger)) - Fix README [\#250](https://github.com/jwt/ruby-jwt/pull/250) ([rono23](https://github.com/rono23)) - Fix link format [\#248](https://github.com/jwt/ruby-jwt/pull/248) ([y-yagi](https://github.com/y-yagi)) ## [v2.1.0](https://github.com/jwt/ruby-jwt/tree/v2.1.0) (2017-10-06) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.0.0...v2.1.0) **Implemented enhancements:** - Ed25519 support planned? [\#217](https://github.com/jwt/ruby-jwt/issues/217) - Verify JTI Proc [\#207](https://github.com/jwt/ruby-jwt/issues/207) - Allow a list of algorithms for decode [\#241](https://github.com/jwt/ruby-jwt/pull/241) ([lautis](https://github.com/lautis)) - verify takes 2 params, second being payload closes: \#207 [\#238](https://github.com/jwt/ruby-jwt/pull/238) ([ab320012](https://github.com/ab320012)) - simplified logic for keyfinder [\#237](https://github.com/jwt/ruby-jwt/pull/237) ([ab320012](https://github.com/ab320012)) - Show backtrace if rbnacl-libsodium not loaded [\#231](https://github.com/jwt/ruby-jwt/pull/231) ([buzztaiki](https://github.com/buzztaiki)) - Support for ED25519 [\#229](https://github.com/jwt/ruby-jwt/pull/229) ([ab320012](https://github.com/ab320012)) **Fixed bugs:** - JWT.encode failing on encode for string [\#235](https://github.com/jwt/ruby-jwt/issues/235) - The README says it uses an algorithm by default [\#226](https://github.com/jwt/ruby-jwt/issues/226) - Fix string payload issue [\#236](https://github.com/jwt/ruby-jwt/pull/236) ([excpt](https://github.com/excpt)) **Security fixes:** - Add HS256 algorithm to decode default options [\#228](https://github.com/jwt/ruby-jwt/pull/228) ([marcoadkins](https://github.com/marcoadkins)) **Closed issues:** - Change from 1.5.6 to 2.0.0 and appears a "Completed 401 Unauthorized" [\#240](https://github.com/jwt/ruby-jwt/issues/240) - Why doesn't the decode function use a default algorithm? [\#227](https://github.com/jwt/ruby-jwt/issues/227) **Merged pull requests:** - Release 2.1.0 preparations [\#243](https://github.com/jwt/ruby-jwt/pull/243) ([excpt](https://github.com/excpt)) - Update README.md [\#242](https://github.com/jwt/ruby-jwt/pull/242) ([excpt](https://github.com/excpt)) - Update ebert configuration [\#232](https://github.com/jwt/ruby-jwt/pull/232) ([excpt](https://github.com/excpt)) - added algos/strategy classes + structs for inputs [\#230](https://github.com/jwt/ruby-jwt/pull/230) ([ab320012](https://github.com/ab320012)) ## [v2.0.0](https://github.com/jwt/ruby-jwt/tree/v2.0.0) (2017-09-03) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.0.0.beta1...v2.0.0) **Fixed bugs:** - Support versions outside 2.1 [\#209](https://github.com/jwt/ruby-jwt/issues/209) - Verifying expiration without leeway throws exception [\#206](https://github.com/jwt/ruby-jwt/issues/206) - Ruby interpreter warning [\#200](https://github.com/jwt/ruby-jwt/issues/200) - TypeError: no implicit conversion of String into Integer [\#188](https://github.com/jwt/ruby-jwt/issues/188) - Fix JWT.encode\(nil\) [\#203](https://github.com/jwt/ruby-jwt/pull/203) ([tmm1](https://github.com/tmm1)) **Closed issues:** - Possibility to disable claim verifications [\#222](https://github.com/jwt/ruby-jwt/issues/222) - Proper way to verify Firebase id tokens [\#216](https://github.com/jwt/ruby-jwt/issues/216) **Merged pull requests:** - Release 2.0.0 preparations :\) [\#225](https://github.com/jwt/ruby-jwt/pull/225) ([excpt](https://github.com/excpt)) - Skip 'exp' claim validation for array payloads [\#224](https://github.com/jwt/ruby-jwt/pull/224) ([excpt](https://github.com/excpt)) - Use a default leeway of 0 [\#223](https://github.com/jwt/ruby-jwt/pull/223) ([travisofthenorth](https://github.com/travisofthenorth)) - Fix reported codesmells [\#221](https://github.com/jwt/ruby-jwt/pull/221) ([excpt](https://github.com/excpt)) - Add fancy gem version badge [\#220](https://github.com/jwt/ruby-jwt/pull/220) ([excpt](https://github.com/excpt)) - Add missing dist option to .travis.yml [\#219](https://github.com/jwt/ruby-jwt/pull/219) ([excpt](https://github.com/excpt)) - Fix ruby version requirements in gemspec file [\#218](https://github.com/jwt/ruby-jwt/pull/218) ([excpt](https://github.com/excpt)) - Fix a little typo in the readme [\#214](https://github.com/jwt/ruby-jwt/pull/214) ([RyanBrushett](https://github.com/RyanBrushett)) - Update README.md [\#212](https://github.com/jwt/ruby-jwt/pull/212) ([zuzannast](https://github.com/zuzannast)) - Fix typo in HS512256 algorithm description [\#211](https://github.com/jwt/ruby-jwt/pull/211) ([ojab](https://github.com/ojab)) - Allow configuration of multiple acceptable issuers [\#210](https://github.com/jwt/ruby-jwt/pull/210) ([ojab](https://github.com/ojab)) - Enforce `exp` to be an `Integer` [\#205](https://github.com/jwt/ruby-jwt/pull/205) ([lucasmazza](https://github.com/lucasmazza)) - ruby 1.9.3 support message upd [\#204](https://github.com/jwt/ruby-jwt/pull/204) ([maokomioko](https://github.com/maokomioko)) ## [v2.0.0.beta1](https://github.com/jwt/ruby-jwt/tree/v2.0.0.beta1) (2017-02-27) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v1.5.6...v2.0.0.beta1) **Implemented enhancements:** - Error with method sign for String [\#171](https://github.com/jwt/ruby-jwt/issues/171) - Refactor the encondig code [\#121](https://github.com/jwt/ruby-jwt/issues/121) - Refactor [\#196](https://github.com/jwt/ruby-jwt/pull/196) ([EmilioCristalli](https://github.com/EmilioCristalli)) - Move signature logic to its own module [\#195](https://github.com/jwt/ruby-jwt/pull/195) ([EmilioCristalli](https://github.com/EmilioCristalli)) - Add options for claim-specific leeway [\#187](https://github.com/jwt/ruby-jwt/pull/187) ([EmilioCristalli](https://github.com/EmilioCristalli)) - Add user friendly encode error if private key is a String, \#171 [\#176](https://github.com/jwt/ruby-jwt/pull/176) ([ogonki-vetochki](https://github.com/ogonki-vetochki)) - Return empty string if signature less than byte_size \#155 [\#175](https://github.com/jwt/ruby-jwt/pull/175) ([ogonki-vetochki](https://github.com/ogonki-vetochki)) - Remove 'typ' optional parameter [\#174](https://github.com/jwt/ruby-jwt/pull/174) ([ogonki-vetochki](https://github.com/ogonki-vetochki)) - Pass payload to keyfinder [\#172](https://github.com/jwt/ruby-jwt/pull/172) ([CodeMonkeySteve](https://github.com/CodeMonkeySteve)) - Use RbNaCl for HMAC if available with fallback to OpenSSL [\#149](https://github.com/jwt/ruby-jwt/pull/149) ([mwpastore](https://github.com/mwpastore)) **Fixed bugs:** - ruby-jwt::raw_to_asn1: Fails for signatures less than byte_size [\#155](https://github.com/jwt/ruby-jwt/issues/155) - The leeway parameter is applies to all time based verifications [\#129](https://github.com/jwt/ruby-jwt/issues/129) - Make algorithm option required to verify signature [\#184](https://github.com/jwt/ruby-jwt/pull/184) ([EmilioCristalli](https://github.com/EmilioCristalli)) - Validate audience when payload is a scalar and options is an array [\#183](https://github.com/jwt/ruby-jwt/pull/183) ([steti](https://github.com/steti)) **Closed issues:** - Different encoded value between servers with same password [\#197](https://github.com/jwt/ruby-jwt/issues/197) - Signature is different at each run [\#190](https://github.com/jwt/ruby-jwt/issues/190) - Include custom headers with password [\#189](https://github.com/jwt/ruby-jwt/issues/189) - can't create token - 'NotImplementedError: Unsupported signing method' [\#186](https://github.com/jwt/ruby-jwt/issues/186) - Cannot verify JWT at all?? [\#177](https://github.com/jwt/ruby-jwt/issues/177) - verify_iss: true is raising JWT::DecodeError instead of JWT::InvalidIssuerError [\#170](https://github.com/jwt/ruby-jwt/issues/170) **Merged pull requests:** - Version bump 2.0.0.beta1 [\#199](https://github.com/jwt/ruby-jwt/pull/199) ([excpt](https://github.com/excpt)) - Update CHANGELOG.md and minor fixes [\#198](https://github.com/jwt/ruby-jwt/pull/198) ([excpt](https://github.com/excpt)) - Add Codacy coverage reporter [\#194](https://github.com/jwt/ruby-jwt/pull/194) ([excpt](https://github.com/excpt)) - Add minimum required ruby version to gemspec [\#193](https://github.com/jwt/ruby-jwt/pull/193) ([excpt](https://github.com/excpt)) - Code smell fixes [\#192](https://github.com/jwt/ruby-jwt/pull/192) ([excpt](https://github.com/excpt)) - Version bump to 2.0.0.dev [\#191](https://github.com/jwt/ruby-jwt/pull/191) ([excpt](https://github.com/excpt)) - Basic encode module refactoring \#121 [\#182](https://github.com/jwt/ruby-jwt/pull/182) ([ogonki-vetochki](https://github.com/ogonki-vetochki)) - Fix travis ci build configuration [\#181](https://github.com/jwt/ruby-jwt/pull/181) ([excpt](https://github.com/excpt)) - Fix travis ci build configuration [\#180](https://github.com/jwt/ruby-jwt/pull/180) ([excpt](https://github.com/excpt)) - Fix typo in README [\#178](https://github.com/jwt/ruby-jwt/pull/178) ([tomeduarte](https://github.com/tomeduarte)) - Fix code style [\#173](https://github.com/jwt/ruby-jwt/pull/173) ([excpt](https://github.com/excpt)) - Fixed a typo in a spec name [\#169](https://github.com/jwt/ruby-jwt/pull/169) ([mingan](https://github.com/mingan)) ## [v1.5.6](https://github.com/jwt/ruby-jwt/tree/v1.5.6) (2016-09-19) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v1.5.5...v1.5.6) **Fixed bugs:** - Fix missing symbol handling in aud verify code [\#166](https://github.com/jwt/ruby-jwt/pull/166) ([excpt](https://github.com/excpt)) **Merged pull requests:** - Update changelog [\#168](https://github.com/jwt/ruby-jwt/pull/168) ([excpt](https://github.com/excpt)) - Fix rubocop code smells [\#167](https://github.com/jwt/ruby-jwt/pull/167) ([excpt](https://github.com/excpt)) ## [v1.5.5](https://github.com/jwt/ruby-jwt/tree/v1.5.5) (2016-09-16) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v1.5.4...v1.5.5) **Implemented enhancements:** - JWT.decode always raises JWT::ExpiredSignature for tokens created with Time objects passed as the `exp` parameter [\#148](https://github.com/jwt/ruby-jwt/issues/148) **Fixed bugs:** - expiration check does not give "Signature has expired" error for the exact time of expiration [\#157](https://github.com/jwt/ruby-jwt/issues/157) - JTI claim broken? [\#152](https://github.com/jwt/ruby-jwt/issues/152) - Audience Claim broken? [\#151](https://github.com/jwt/ruby-jwt/issues/151) - 1.5.3 breaks compatibility with 1.5.2 [\#133](https://github.com/jwt/ruby-jwt/issues/133) - Version 1.5.3 breaks 1.9.3 compatibility, but not documented as such [\#132](https://github.com/jwt/ruby-jwt/issues/132) - Fix: exp claim check [\#161](https://github.com/jwt/ruby-jwt/pull/161) ([excpt](https://github.com/excpt)) **Security fixes:** - \[security\] Signature verified after expiration/sub/iss checks [\#153](https://github.com/jwt/ruby-jwt/issues/153) - Signature validation before claim verification [\#160](https://github.com/jwt/ruby-jwt/pull/160) ([excpt](https://github.com/excpt)) **Closed issues:** - Rendering Json Results in JWT::DecodeError [\#162](https://github.com/jwt/ruby-jwt/issues/162) - PHP Libraries [\#154](https://github.com/jwt/ruby-jwt/issues/154) - Is ruby-jwt thread-safe? [\#150](https://github.com/jwt/ruby-jwt/issues/150) - JWT 1.5.3 [\#143](https://github.com/jwt/ruby-jwt/issues/143) - gem install v 1.5.3 returns error [\#141](https://github.com/jwt/ruby-jwt/issues/141) - Adding a CHANGELOG [\#140](https://github.com/jwt/ruby-jwt/issues/140) **Merged pull requests:** - Bump version [\#165](https://github.com/jwt/ruby-jwt/pull/165) ([excpt](https://github.com/excpt)) - Improve error message for exp claim in payload [\#164](https://github.com/jwt/ruby-jwt/pull/164) ([excpt](https://github.com/excpt)) - Fix \#151 and code refactoring [\#163](https://github.com/jwt/ruby-jwt/pull/163) ([excpt](https://github.com/excpt)) - Create specs for README.md examples [\#159](https://github.com/jwt/ruby-jwt/pull/159) ([excpt](https://github.com/excpt)) - Tiny Readme Improvement [\#156](https://github.com/jwt/ruby-jwt/pull/156) ([b264](https://github.com/b264)) - Added test execution to Rakefile [\#147](https://github.com/jwt/ruby-jwt/pull/147) ([jabbrwcky](https://github.com/jabbrwcky)) - Bump version [\#145](https://github.com/jwt/ruby-jwt/pull/145) ([excpt](https://github.com/excpt)) - Add a changelog file [\#142](https://github.com/jwt/ruby-jwt/pull/142) ([excpt](https://github.com/excpt)) - Return decoded_segments [\#139](https://github.com/jwt/ruby-jwt/pull/139) ([akostrikov](https://github.com/akostrikov)) ## [v1.5.4](https://github.com/jwt/ruby-jwt/tree/v1.5.4) (2016-03-24) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v1.5.3...v1.5.4) **Closed issues:** - 404 at [https://rubygems.global.ssl.fastly.net/gems/jwt-1.5.3.gem](https://rubygems.global.ssl.fastly.net/gems/jwt-1.5.3.gem) [\#137](https://github.com/jwt/ruby-jwt/issues/137) **Merged pull requests:** - Update README.md [\#138](https://github.com/jwt/ruby-jwt/pull/138) ([excpt](https://github.com/excpt)) - Fix base64url_decode [\#136](https://github.com/jwt/ruby-jwt/pull/136) ([excpt](https://github.com/excpt)) - Fix ruby 1.9.3 compatibility [\#135](https://github.com/jwt/ruby-jwt/pull/135) ([excpt](https://github.com/excpt)) - iat can be a float value [\#134](https://github.com/jwt/ruby-jwt/pull/134) ([llimllib](https://github.com/llimllib)) ## [v1.5.3](https://github.com/jwt/ruby-jwt/tree/v1.5.3) (2016-02-24) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.5.2...v1.5.3) **Implemented enhancements:** - Refactor obsolete code for ruby 1.8 support [\#120](https://github.com/jwt/ruby-jwt/issues/120) - Fix "Rubocop/Metrics/CyclomaticComplexity" issue in lib/jwt.rb [\#106](https://github.com/jwt/ruby-jwt/issues/106) - Fix "Rubocop/Metrics/CyclomaticComplexity" issue in lib/jwt.rb [\#105](https://github.com/jwt/ruby-jwt/issues/105) - Allow a proc to be passed for JTI verification [\#126](https://github.com/jwt/ruby-jwt/pull/126) ([yahooguntu](https://github.com/yahooguntu)) - Relax restrictions on "jti" claim verification [\#113](https://github.com/jwt/ruby-jwt/pull/113) ([lwe](https://github.com/lwe)) **Closed issues:** - Verifications not functioning in latest release [\#128](https://github.com/jwt/ruby-jwt/issues/128) - Base64 is generating invalid length base64 strings - cross language interop [\#127](https://github.com/jwt/ruby-jwt/issues/127) - Digest::Digest is deprecated; use Digest [\#119](https://github.com/jwt/ruby-jwt/issues/119) - verify_rsa no method 'verify' for class String [\#115](https://github.com/jwt/ruby-jwt/issues/115) - Add a changelog [\#111](https://github.com/jwt/ruby-jwt/issues/111) **Merged pull requests:** - Drop ruby 1.9.3 support [\#131](https://github.com/jwt/ruby-jwt/pull/131) ([excpt](https://github.com/excpt)) - Allow string hash keys in validation configurations [\#130](https://github.com/jwt/ruby-jwt/pull/130) ([tpickett66](https://github.com/tpickett66)) - Add ruby 2.3.0 for travis ci testing [\#123](https://github.com/jwt/ruby-jwt/pull/123) ([excpt](https://github.com/excpt)) - Remove obsolete json code [\#122](https://github.com/jwt/ruby-jwt/pull/122) ([excpt](https://github.com/excpt)) - Add fancy badges to README.md [\#118](https://github.com/jwt/ruby-jwt/pull/118) ([excpt](https://github.com/excpt)) - Refactor decode and verify functionality [\#117](https://github.com/jwt/ruby-jwt/pull/117) ([excpt](https://github.com/excpt)) - Drop echoe dependency for gem releases [\#116](https://github.com/jwt/ruby-jwt/pull/116) ([excpt](https://github.com/excpt)) - Updated readme for iss/aud options [\#114](https://github.com/jwt/ruby-jwt/pull/114) ([ryanmcilmoyl](https://github.com/ryanmcilmoyl)) - Fix error misspelling [\#112](https://github.com/jwt/ruby-jwt/pull/112) ([kat3kasper](https://github.com/kat3kasper)) ## [jwt-1.5.2](https://github.com/jwt/ruby-jwt/tree/jwt-1.5.2) (2015-10-27) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.5.1...jwt-1.5.2) **Implemented enhancements:** - Must we specify algorithm when calling decode to avoid vulnerabilities? [\#107](https://github.com/jwt/ruby-jwt/issues/107) - Code review: Rspec test refactoring [\#85](https://github.com/jwt/ruby-jwt/pull/85) ([excpt](https://github.com/excpt)) **Fixed bugs:** - aud verifies if aud is passed in, :sub does not [\#102](https://github.com/jwt/ruby-jwt/issues/102) - iat check does not use leeway so nbf could pass, but iat fail [\#83](https://github.com/jwt/ruby-jwt/issues/83) **Closed issues:** - Test ticket from Code Climate [\#104](https://github.com/jwt/ruby-jwt/issues/104) - Test ticket from Code Climate [\#100](https://github.com/jwt/ruby-jwt/issues/100) - Is it possible to decode the payload without validating the signature? [\#97](https://github.com/jwt/ruby-jwt/issues/97) - What is audience? [\#96](https://github.com/jwt/ruby-jwt/issues/96) - Options hash uses both symbols and strings as keys. [\#95](https://github.com/jwt/ruby-jwt/issues/95) **Merged pull requests:** - Fix incorrect `iat` examples [\#109](https://github.com/jwt/ruby-jwt/pull/109) ([kjwierenga](https://github.com/kjwierenga)) - Update docs to include instructions for the algorithm parameter. [\#108](https://github.com/jwt/ruby-jwt/pull/108) ([aarongray](https://github.com/aarongray)) - make sure :sub check behaves like :aud check [\#103](https://github.com/jwt/ruby-jwt/pull/103) ([skippy](https://github.com/skippy)) - Change hash syntax [\#101](https://github.com/jwt/ruby-jwt/pull/101) ([excpt](https://github.com/excpt)) - Include LICENSE and README.md in gem [\#99](https://github.com/jwt/ruby-jwt/pull/99) ([bkeepers](https://github.com/bkeepers)) - Remove unused variable in the sample code. [\#98](https://github.com/jwt/ruby-jwt/pull/98) ([hypermkt](https://github.com/hypermkt)) - Fix iat claim example [\#94](https://github.com/jwt/ruby-jwt/pull/94) ([larrylv](https://github.com/larrylv)) - Fix wrong description in README.md [\#93](https://github.com/jwt/ruby-jwt/pull/93) ([larrylv](https://github.com/larrylv)) - JWT and JWA are now RFC. [\#92](https://github.com/jwt/ruby-jwt/pull/92) ([aj-michael](https://github.com/aj-michael)) - Update README.md [\#91](https://github.com/jwt/ruby-jwt/pull/91) ([nsarno](https://github.com/nsarno)) - Fix missing verify parameter in docs [\#90](https://github.com/jwt/ruby-jwt/pull/90) ([ernie](https://github.com/ernie)) - Iat check uses leeway. [\#89](https://github.com/jwt/ruby-jwt/pull/89) ([aj-michael](https://github.com/aj-michael)) - nbf check allows exact time matches. [\#88](https://github.com/jwt/ruby-jwt/pull/88) ([aj-michael](https://github.com/aj-michael)) ## [jwt-1.5.1](https://github.com/jwt/ruby-jwt/tree/jwt-1.5.1) (2015-06-22) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.5.0...jwt-1.5.1) **Implemented enhancements:** - Fix either README or source code [\#78](https://github.com/jwt/ruby-jwt/issues/78) - Validate against draft 20 [\#38](https://github.com/jwt/ruby-jwt/issues/38) **Fixed bugs:** - ECDSA signature verification fails for valid tokens [\#84](https://github.com/jwt/ruby-jwt/issues/84) - Shouldn't verification of additional claims, like iss, aud etc. be enforced when in options? [\#81](https://github.com/jwt/ruby-jwt/issues/81) - decode fails with 'none' algorithm and verify [\#75](https://github.com/jwt/ruby-jwt/issues/75) **Closed issues:** - Doc mismatch: uninitialized constant JWT::ExpiredSignature [\#79](https://github.com/jwt/ruby-jwt/issues/79) - TypeError when specifying a wrong algorithm [\#77](https://github.com/jwt/ruby-jwt/issues/77) - jti verification doesn't prevent replays [\#73](https://github.com/jwt/ruby-jwt/issues/73) **Merged pull requests:** - Correctly sign ECDSA JWTs [\#87](https://github.com/jwt/ruby-jwt/pull/87) ([jurriaan](https://github.com/jurriaan)) - fixed results of decoded tokens in readme [\#86](https://github.com/jwt/ruby-jwt/pull/86) ([piscolomo](https://github.com/piscolomo)) - Force verification of "iss" and "aud" claims [\#82](https://github.com/jwt/ruby-jwt/pull/82) ([lwe](https://github.com/lwe)) ## [jwt-1.5.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.5.0) (2015-05-09) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.4.1...jwt-1.5.0) **Implemented enhancements:** - Needs to support asymmetric key signatures over shared secrets [\#46](https://github.com/jwt/ruby-jwt/issues/46) - Implement Elliptic Curve Crypto Signatures [\#74](https://github.com/jwt/ruby-jwt/pull/74) ([jtdowney](https://github.com/jtdowney)) - Add an option to verify the signature on decode [\#71](https://github.com/jwt/ruby-jwt/pull/71) ([javawizard](https://github.com/javawizard)) **Closed issues:** - Check JWT vulnerability [\#76](https://github.com/jwt/ruby-jwt/issues/76) **Merged pull requests:** - Fixed some examples to make them copy-pastable [\#72](https://github.com/jwt/ruby-jwt/pull/72) ([jer](https://github.com/jer)) ## [jwt-1.4.1](https://github.com/jwt/ruby-jwt/tree/jwt-1.4.1) (2015-03-12) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.4.0...jwt-1.4.1) **Fixed bugs:** - jti verification not working per the spec [\#68](https://github.com/jwt/ruby-jwt/issues/68) - Verify ISS should be off by default [\#66](https://github.com/jwt/ruby-jwt/issues/66) **Merged pull requests:** - Fix \#66 \#68 [\#69](https://github.com/jwt/ruby-jwt/pull/69) ([excpt](https://github.com/excpt)) - When throwing errors, mention expected/received values [\#65](https://github.com/jwt/ruby-jwt/pull/65) ([rolodato](https://github.com/rolodato)) ## [jwt-1.4.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.4.0) (2015-03-10) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.3.0...jwt-1.4.0) **Closed issues:** - The behavior using 'json' differs from 'multi_json' [\#41](https://github.com/jwt/ruby-jwt/issues/41) **Merged pull requests:** - Release 1.4.0 [\#64](https://github.com/jwt/ruby-jwt/pull/64) ([excpt](https://github.com/excpt)) - Update README.md and remove dead code [\#63](https://github.com/jwt/ruby-jwt/pull/63) ([excpt](https://github.com/excpt)) - Add 'iat/ aud/ sub/ jti' support for ruby-jwt [\#62](https://github.com/jwt/ruby-jwt/pull/62) ([ZhangHanDong](https://github.com/ZhangHanDong)) - Add 'iss' support for ruby-jwt [\#61](https://github.com/jwt/ruby-jwt/pull/61) ([ZhangHanDong](https://github.com/ZhangHanDong)) - Clarify .encode API in README [\#60](https://github.com/jwt/ruby-jwt/pull/60) ([jbodah](https://github.com/jbodah)) ## [jwt-1.3.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.3.0) (2015-02-24) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.2.1...jwt-1.3.0) **Closed issues:** - Signature Verification to Return Verification Error rather than decode error [\#57](https://github.com/jwt/ruby-jwt/issues/57) - Incorrect readme for leeway [\#55](https://github.com/jwt/ruby-jwt/issues/55) - What is the reason behind stripping the = in base64 encoding? [\#54](https://github.com/jwt/ruby-jwt/issues/54) - Preperations for version 2.x [\#50](https://github.com/jwt/ruby-jwt/issues/50) - Release a new version [\#47](https://github.com/jwt/ruby-jwt/issues/47) - Catch up for ActiveWhatever 4.1.1 series [\#40](https://github.com/jwt/ruby-jwt/issues/40) **Merged pull requests:** - raise verification error for signiture verification [\#58](https://github.com/jwt/ruby-jwt/pull/58) ([punkle](https://github.com/punkle)) - Added support for not before claim verification [\#56](https://github.com/jwt/ruby-jwt/pull/56) ([punkle](https://github.com/punkle)) ## [jwt-1.2.1](https://github.com/jwt/ruby-jwt/tree/jwt-1.2.1) (2015-01-22) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.2.0...jwt-1.2.1) **Closed issues:** - JWT.encode\({"exp": 10}, "secret"\) [\#52](https://github.com/jwt/ruby-jwt/issues/52) - JWT.encode\({"exp": 10}, "secret"\) [\#51](https://github.com/jwt/ruby-jwt/issues/51) **Merged pull requests:** - Accept expiration claims as string [\#53](https://github.com/jwt/ruby-jwt/pull/53) ([yarmand](https://github.com/yarmand)) ## [jwt-1.2.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.2.0) (2014-11-24) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.13...jwt-1.2.0) **Closed issues:** - set token to expire [\#42](https://github.com/jwt/ruby-jwt/issues/42) **Merged pull requests:** - Added support for `exp` claim [\#45](https://github.com/jwt/ruby-jwt/pull/45) ([zshannon](https://github.com/zshannon)) - rspec 3 breaks passing tests [\#44](https://github.com/jwt/ruby-jwt/pull/44) ([zshannon](https://github.com/zshannon)) ## [jwt-0.1.13](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.13) (2014-05-08) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.0.0...jwt-0.1.13) **Closed issues:** - yanking of version 0.1.12 causes issues [\#39](https://github.com/jwt/ruby-jwt/issues/39) - Semantic versioning [\#37](https://github.com/jwt/ruby-jwt/issues/37) - Update gem to get latest changes [\#36](https://github.com/jwt/ruby-jwt/issues/36) ## [jwt-1.0.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.0.0) (2014-05-07) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.11...jwt-1.0.0) **Closed issues:** - API request - JWT::decoded_header\(\) [\#26](https://github.com/jwt/ruby-jwt/issues/26) **Merged pull requests:** - return header along with playload after decoding [\#35](https://github.com/jwt/ruby-jwt/pull/35) ([sawyerzhang](https://github.com/sawyerzhang)) - Raise JWT::DecodeError on nil token [\#34](https://github.com/jwt/ruby-jwt/pull/34) ([tjmw](https://github.com/tjmw)) - Make MultiJson optional for Ruby 1.9+ [\#33](https://github.com/jwt/ruby-jwt/pull/33) ([petergoldstein](https://github.com/petergoldstein)) - Allow access to header and payload without signature verification [\#32](https://github.com/jwt/ruby-jwt/pull/32) ([petergoldstein](https://github.com/petergoldstein)) - Update specs to use RSpec 3.0.x syntax [\#31](https://github.com/jwt/ruby-jwt/pull/31) ([petergoldstein](https://github.com/petergoldstein)) - Travis - Add Ruby 2.0.0, 2.1.0, Rubinius [\#30](https://github.com/jwt/ruby-jwt/pull/30) ([petergoldstein](https://github.com/petergoldstein)) ## [jwt-0.1.11](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.11) (2014-01-17) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.10...jwt-0.1.11) **Closed issues:** - url safe encode and decode [\#28](https://github.com/jwt/ruby-jwt/issues/28) - Release [\#27](https://github.com/jwt/ruby-jwt/issues/27) **Merged pull requests:** - fixed urlsafe base64 encoding [\#29](https://github.com/jwt/ruby-jwt/pull/29) ([tobscher](https://github.com/tobscher)) ## [jwt-0.1.10](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.10) (2014-01-10) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.8...jwt-0.1.10) **Closed issues:** - change to signature of JWT.decode method [\#14](https://github.com/jwt/ruby-jwt/issues/14) **Merged pull requests:** - Fix warning: assigned but unused variable - e [\#25](https://github.com/jwt/ruby-jwt/pull/25) ([sferik](https://github.com/sferik)) - Echoe doesn't define a license= method [\#24](https://github.com/jwt/ruby-jwt/pull/24) ([sferik](https://github.com/sferik)) - Use OpenSSL::Digest instead of deprecated OpenSSL::Digest::Digest [\#23](https://github.com/jwt/ruby-jwt/pull/23) ([JuanitoFatas](https://github.com/JuanitoFatas)) - Handle some invalid JWTs [\#22](https://github.com/jwt/ruby-jwt/pull/22) ([steved](https://github.com/steved)) - Add MIT license to gemspec [\#21](https://github.com/jwt/ruby-jwt/pull/21) ([nycvotes-dev](https://github.com/nycvotes-dev)) - Tweaks and improvements [\#20](https://github.com/jwt/ruby-jwt/pull/20) ([threedaymonk](https://github.com/threedaymonk)) - Don't leave errors in OpenSSL.errors when there is a decoding error. [\#19](https://github.com/jwt/ruby-jwt/pull/19) ([lowellk](https://github.com/lowellk)) ## [jwt-0.1.8](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.8) (2013-03-14) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.7...jwt-0.1.8) **Merged pull requests:** - Contrib and update [\#18](https://github.com/jwt/ruby-jwt/pull/18) ([threedaymonk](https://github.com/threedaymonk)) - Verify if verify is truthy \(not just true\) [\#17](https://github.com/jwt/ruby-jwt/pull/17) ([threedaymonk](https://github.com/threedaymonk)) ## [jwt-0.1.7](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.7) (2013-03-07) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.6...jwt-0.1.7) **Merged pull requests:** - Catch MultiJson::LoadError and reraise as JWT::DecodeError [\#16](https://github.com/jwt/ruby-jwt/pull/16) ([rwygand](https://github.com/rwygand)) ## [jwt-0.1.6](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.6) (2013-03-05) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.5...jwt-0.1.6) **Merged pull requests:** - Fixes a theoretical timing attack [\#15](https://github.com/jwt/ruby-jwt/pull/15) ([mgates](https://github.com/mgates)) - Use StandardError as parent for DecodeError [\#13](https://github.com/jwt/ruby-jwt/pull/13) ([Oscil8](https://github.com/Oscil8)) ## [jwt-0.1.5](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.5) (2012-07-20) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.4...jwt-0.1.5) **Closed issues:** - Unable to specify signature header fields [\#7](https://github.com/jwt/ruby-jwt/issues/7) **Merged pull requests:** - MultiJson dependency uses ~\> but should be \>= [\#12](https://github.com/jwt/ruby-jwt/pull/12) ([sporkmonger](https://github.com/sporkmonger)) - Oops. :-\) [\#11](https://github.com/jwt/ruby-jwt/pull/11) ([sporkmonger](https://github.com/sporkmonger)) - Fix issue with signature verification in JRuby [\#10](https://github.com/jwt/ruby-jwt/pull/10) ([sporkmonger](https://github.com/sporkmonger)) - Depend on MultiJson [\#9](https://github.com/jwt/ruby-jwt/pull/9) ([lautis](https://github.com/lautis)) - Allow for custom headers on encode and decode [\#8](https://github.com/jwt/ruby-jwt/pull/8) ([dgrijalva](https://github.com/dgrijalva)) - Missing development dependency for echoe gem. [\#6](https://github.com/jwt/ruby-jwt/pull/6) ([sporkmonger](https://github.com/sporkmonger)) ## [jwt-0.1.4](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.4) (2011-11-11) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.3...jwt-0.1.4) **Merged pull requests:** - Fix for RSA verification [\#5](https://github.com/jwt/ruby-jwt/pull/5) ([jordan-brough](https://github.com/jordan-brough)) ## [jwt-0.1.3](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.3) (2011-06-30) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/10d7492ea325c65fce41191c73cd90d4de494772...jwt-0.1.3) **Closed issues:** - signatures calculated incorrectly \(hexdigest instead of digest\) [\#1](https://github.com/jwt/ruby-jwt/issues/1) **Merged pull requests:** - Bumped a version and added a .gemspec using rake build_gemspec [\#3](https://github.com/jwt/ruby-jwt/pull/3) ([zhitomirskiyi](https://github.com/zhitomirskiyi)) - Added RSA support [\#2](https://github.com/jwt/ruby-jwt/pull/2) ([zhitomirskiyi](https://github.com/zhitomirskiyi)) ruby-jwt-3.1.2/CODE_OF_CONDUCT.md000066400000000000000000000124121503003570500160750ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge 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 a positive environment for our community include: - 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 include: - 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 email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities 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. 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 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 to the community leaders responsible for enforcement at . 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 **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 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). ruby-jwt-3.1.2/CONTRIBUTING.md000066400000000000000000000057041503003570500155350ustar00rootroot00000000000000# Contributing to [ruby-jwt](https://github.com/jwt/ruby-jwt) ## Forking the project Fork the project on GitHub and clone your own fork. Instuctions on forking can be found from the [GitHub Docs](https://docs.github.com/en/get-started/quickstart/fork-a-repo) ```bash git clone git@github.com:you/ruby-jwt.git cd ruby-jwt git remote add upstream https://github.com/jwt/ruby-jwt ``` ## Create a branch for your implementation Make sure you have the latest upstream main branch of the project. ```bash git fetch --all git checkout main git rebase upstream/main git push origin main git checkout -b fix-a-little-problem ``` ## Running the tests and linter Before you start with your implementation make sure you are able to get a successful test run with the current revision. The tests are written with rspec and [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features. [Rubocop](https://github.com/rubocop/rubocop) is used to enforce the Ruby style. To run the complete set of tests and linter run the following ```bash bundle install bundle exec appraisal rake test bundle exec rubocop ``` ## Implement your feature Implement tests and your change. Don't be shy adding a little something in the [README](README.md). Add a short description of the change in either the `Features` or `Fixes` section in the [CHANGELOG](CHANGELOG.md) file. The form of the row (You need to return to the row when you know the pull request id) ```markdown - Fix a little problem [#123](https://github.com/jwt/ruby-jwt/pull/123) - [@you](https://github.com/you). ``` ## Push your branch and create a pull request Before pushing make sure the tests pass and RuboCop is happy. ```bash bundle exec appraisal rake test bundle exec rubocop git push origin fix-a-little-problem ``` Make a new pull request on the [ruby-jwt project](https://github.com/jwt/ruby-jwt/pulls) with a description what the change is about. ## Update the CHANGELOG, again Update the [CHANGELOG](CHANGELOG.md) with the pull request id from the previous step. You can amend the previous commit with the updated changelog change and force push your branch. The PR will get automatically updated. ```bash git add CHANGELOG.md git commit --amend --no-edit git push origin fix-a-little-problem -f ``` ## Keep an eye on your pull request A maintainer will review and probably merge you changes when time allows, be patient. ## Keeping your branch up-to-date It's recommended that you keep your branch up-to-date by rebasing to the upstream main. ```bash git fetch upstream git checkout fix-a-little-problem git rebase upstream/main git push origin fix-a-little-problem -f ``` ## Releasing a new version The version is using the [Semantic Versioning](http://semver.org/) and the version is located in the [version.rb](lib/jwt/version.rb) file. Also update the [CHANGELOG](CHANGELOG.md) to reflect the upcoming version release. ```bash rake release ``` ruby-jwt-3.1.2/Gemfile000066400000000000000000000001061503003570500145660ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec ruby-jwt-3.1.2/LICENSE000066400000000000000000000020401503003570500142770ustar00rootroot00000000000000Copyright (c) 2011 Jeff Lindsay Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-jwt-3.1.2/README.md000066400000000000000000000726251503003570500145710ustar00rootroot00000000000000# JWT [![Gem Version](https://badge.fury.io/rb/jwt.svg)](https://badge.fury.io/rb/jwt) [![Build Status](https://github.com/jwt/ruby-jwt/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/jwt/ruby-jwt/actions) [![Maintainability](https://qlty.sh/badges/6f61c5a6-6e23-41a7-8896-a3ce8b006655/maintainability.svg)](https://qlty.sh/gh/jwt/projects/ruby-jwt) [![Code Coverage](https://qlty.sh/badges/6f61c5a6-6e23-41a7-8896-a3ce8b006655/test_coverage.svg)](https://qlty.sh/gh/jwt/projects/ruby-jwt) A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard. If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt). See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes and [upgrade guide](UPGRADING.md) for upgrading between major versions. ## Sponsors | Logo | Message | | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ![auth0 logo](https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png) | If you want to quickly add secure token-based authentication to Ruby projects, feel free to check Auth0's Ruby SDK and free plan at [auth0.com/developers](https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=rubyjwt&utm_content=auth) | ## Installing ### Using Rubygems ```bash gem install jwt ``` ### Using Bundler Add the following to your Gemfile ```bash gem 'jwt' ``` And run `bundle install` Finally require the gem in your application ```ruby require 'jwt' ``` ## Algorithms and Usage The jwt gem natively supports the NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms via the openssl library. The gem can be extended with additional or alternative implementations of the algorithms via extensions. Additionally the EdDSA algorithm is supported via a the [jwt-eddsa gem](https://rubygems.org/gems/jwt-eddsa). For safe cryptographic signing, you need to specify the algorithm in the options hash whenever you call `JWT.decode` to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm** See [JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1) ### **NONE** - none - unsigned token ```ruby payload = { data: 'test' } token = JWT.encode(payload, nil, 'none') # => "eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9." decoded_token = JWT.decode(token, nil, true, { algorithm: 'none' }) # => [ # {"data"=>"test"}, # payload # {"alg"=>"none"} # header # ] ``` ### **HMAC** - HS256 - HMAC using SHA-256 hash algorithm - HS384 - HMAC using SHA-384 hash algorithm - HS512 - HMAC using SHA-512 hash algorithm ```ruby payload = { data: 'test' } hmac_secret = 'my$ecretK3y' token = JWT.encode(payload, hmac_secret, 'HS256') # => "eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y" decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' }) # => [ # {"data"=>"test"}, # payload # {"alg"=>"HS256"} # header # ] ``` ### **RSA** - RS256 - RSA using SHA-256 hash algorithm - RS384 - RSA using SHA-384 hash algorithm - RS512 - RSA using SHA-512 hash algorithm ```ruby payload = { data: 'test' } rsa_private = OpenSSL::PKey::RSA.generate(2048) rsa_public = rsa_private.public_key token = JWT.encode(payload, rsa_private, 'RS256') # => "eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.CCkO35qFPijW8Gwhbt8a80PB9fc9FJ19hCMnXSgoDF6Mlvlt0A4G-ah..." decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'RS256' }) # => [ # {"data"=>"test"}, # payload # {"alg"=>"RS256"} # header # ] ``` ### **ECDSA** - ES256 - ECDSA using P-256 and SHA-256 - ES384 - ECDSA using P-384 and SHA-384 - ES512 - ECDSA using P-521 and SHA-512 - ES256K - ECDSA using P-256K and SHA-256 ```ruby payload = { data: 'test' } ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1') token = JWT.encode(payload, ecdsa_key, 'ES256') # => "eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg" decoded_token = JWT.decode(token, ecdsa_key, true, { algorithm: 'ES256' }) # => [ # {"test"=>"data"}, # payload # {"alg"=>"ES256"} # header # ] ``` ### **EdDSA** Since version 3.0, the EdDSA algorithm has been moved to the [jwt-eddsa gem](https://rubygems.org/gems/jwt-eddsa). ### **RSASSA-PSS** - PS256 - RSASSA-PSS using SHA-256 hash algorithm - PS384 - RSASSA-PSS using SHA-384 hash algorithm - PS512 - RSASSA-PSS using SHA-512 hash algorithm ```ruby payload = { data: 'test' } rsa_private = OpenSSL::PKey::RSA.generate(2048) rsa_public = rsa_private.public_key token = JWT.encode(payload, rsa_private, 'PS256') # => "eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.BRWizdUjD5zAWw-EDBcrl3dDpQDAePz9Ol3XKC43SggU47G8OWwveA_..." decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'PS256' }) # => [ # {"data"=>"test"}, # payload # {"alg"=>"PS256"} # header # ] ``` ### **Custom algorithms** When encoding or decoding a token, you can pass in a custom object through the `algorithm` option to handle signing or verification. This custom object must include or extend the `JWT::JWA::SigningAlgorithm` module and implement certain methods: - For decoding/verifying: The object must implement the methods `alg` and `verify`. - For encoding/signing: The object must implement the methods `alg` and `sign`. For customization options check the details from `JWT::JWA::SigningAlgorithm`. ```ruby module CustomHS512Algorithm extend JWT::JWA::SigningAlgorithm def self.alg 'HS512' end def self.sign(data:, signing_key:) OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), signing_key, data) end def self.verify(data:, signature:, verification_key:) ::OpenSSL.secure_compare(sign(data: data, signing_key: verification_key), signature) end end payload = { data: 'test' } token = JWT.encode(payload, 'secret', CustomHS512Algorithm) # => "eyJhbGciOiJIUzUxMiJ9.eyJkYXRhIjoidGVzdCJ9.aBNoejLEM2WMF3TxzRDKlehYdG2ATvFpGNauTI4GSD2VJseS_sC8covrVMlgslf0aJM4SKb3EIeORJBFPtZ33w" decoded_token = JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm) # => [ # {"data"=>"test"}, # payload # {"alg"=>"HS512"} # header # ] ``` ### Add custom header fields The ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5) To add custom header fields you need to pass `header_fields` parameter ```ruby payload = { data: 'test' } token = JWT.encode(payload, nil, 'none', { typ: 'JWT' }) # => "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9." decoded_token = JWT.decode(token, nil, true, { algorithm: 'none' }) # => [ # {"data"=>"test"}, # payload # {"typ"=>"JWT", "alg"=>"none"} # header # ] ``` ## `JWT::Token` and `JWT::EncodedToken` The `JWT::Token` and `JWT::EncodedToken` classes can be used to manage your JWTs. ### Signing and encoding a token ```ruby payload = { exp: Time.now.to_i + 60, jti: '1234', sub: "my-subject" } header = { kid: 'hmac' } token = JWT::Token.new(payload: payload, header: header) token.sign!(algorithm: 'HS256', key: "secret") token.jwt # => "eyJraWQiOiJobWFjIiwiYWxnIjoiSFMyNTYifQ.eyJleHAiOjE3NTAwMDU0NzksImp0aSI6IjEyMzQiLCJzdWIiOiJteS1zdWJqZWN0In0.NRLcK6fYr3IdNfmncJePMWLQ34M4n14EgqSYrQIjL9w" ``` ### Verifying and decoding a token The `JWT::EncodedToken` can be used as a token object that allows verification of signatures and claims. ```ruby encoded_token = JWT::EncodedToken.new(token.jwt) encoded_token.verify_signature!(algorithm: 'HS256', key: "secret") encoded_token.verify_signature!(algorithm: 'HS256', key: "wrong_secret") # raises JWT::VerificationError encoded_token.verify_claims!(:exp, :jti) encoded_token.verify_claims!(sub: ["not-my-subject"]) # raises JWT::InvalidSubError encoded_token.claim_errors(sub: ["not-my-subject"]).map(&:message) # => ["Invalid subject. Expected [\"not-my-subject\"], received my-subject"] encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' } encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'} ``` The `JWT::EncodedToken#verify!` method can be used to verify signature and claim verification in one go. The `exp` claim is verified by default. ```ruby encoded_token = JWT::EncodedToken.new(token.jwt) encoded_token.verify!(signature: {algorithm: 'HS256', key: "secret"}) encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' } encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'} ``` A JWK can be used to sign and verify the token if it's possible to derive the signing algorithm from the key. ```ruby jwk_json = '{ "kty": "oct", "k": "c2VjcmV0", "alg": "HS256", "kid": "hmac" }' jwk = JWT::JWK.import(JSON.parse(jwk_json)) token = JWT::Token.new(payload: payload, header: header) token.sign!(key: jwk, algorithm: 'HS256') encoded_token = JWT::EncodedToken.new(token.jwt) encoded_token.verify!(signature: { algorithm: ["HS256", "HS512"], key: jwk}) ``` #### Using a keyfinder A keyfinder can be used to verify a signature. A keyfinder is an object responding to the `#call` method. The method expects to receive one argument, which is the token to be verified. An example on using the built-in JWK keyfinder. ```ruby # Create and sign a token jwk = JWT::JWK.new(OpenSSL::PKey::RSA.generate(2048)) token = JWT::Token.new(payload: { pay: 'load' }, header: { kid: jwk.kid }) token.sign!(algorithm: 'RS256', key: jwk.signing_key) # Create keyfinder object, verify and decode token key_finder = JWT::JWK::KeyFinder.new(jwks: JWT::JWK::Set.new(jwk)) encoded_token = JWT::EncodedToken.new(token.jwt) encoded_token.verify!(signature: { algorithm: 'RS256', key_finder: key_finder}) encoded_token.payload # => { 'pay' => 'load' } ``` Using a custom keyfinder proc. ```ruby # Create and sign a token key = OpenSSL::PKey::RSA.generate(2048) token = JWT::Token.new(payload: { pay: 'load' }) token.sign!(algorithm: 'RS256', key: key) # Verify and decode token encoded_token = JWT::EncodedToken.new(token.jwt) encoded_token.verify!(signature: { algorithm: 'RS256', key_finder: ->(_token){ key.public_key }}) encoded_token.payload # => { 'pay' => 'load' } ``` ### Detached payload The `::JWT::Token#detach_payload!` method can be use to detach the payload from the JWT. ```ruby token = JWT::Token.new(payload: { pay: 'load' }) token.sign!(algorithm: 'HS256', key: "secret") token.detach_payload! token.jwt # => "eyJhbGciOiJIUzI1NiJ9..UEhDY1Qlj29ammxuVRA_-gBah4qTy5FngIWg0yEAlC0" token.encoded_payload # => "eyJwYXkiOiJsb2FkIn0" ``` The `JWT::EncodedToken` class can be used to decode a token with a detached payload by providing the payload to the token instance in separate. ```ruby encoded_token = JWT::EncodedToken.new(token.jwt) encoded_token.encoded_payload = "eyJwYXkiOiJsb2FkIn0" encoded_token.verify_signature!(algorithm: 'HS256', key: "secret") encoded_token.payload # => {"pay"=>"load"} ``` ## Claims JSON Web Token defines some reserved claim names and defines how they should be used. JWT supports these reserved claim names: - 'exp' (Expiration Time) Claim - 'nbf' (Not Before Time) Claim - 'iss' (Issuer) Claim - 'aud' (Audience) Claim - 'jti' (JWT ID) Claim - 'iat' (Issued At) Claim - 'sub' (Subject) Claim ### Expiration Time Claim From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.4): > The `exp` (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the `exp` claim requires that the current date/time MUST be before the expiration date/time listed in the `exp` claim. Implementers MAY provide for some small `leeway`, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a **_NumericDate_** value. Use of this claim is OPTIONAL. ```ruby exp = Time.now.to_i + 4 * 3600 exp_payload = { data: 'data', exp: exp } token = JWT.encode(exp_payload, hmac_secret, 'HS256') begin decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' }) rescue JWT::ExpiredSignature # Handle expired token, e.g. logout user or deny access end ``` The Expiration Claim verification can be disabled. ```ruby # Decode token without raising JWT::ExpiredSignature error JWT.decode(token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }) ``` Leeway and the exp claim. ```ruby exp = Time.now.to_i - 10 leeway = 30 # seconds exp_payload = { data: 'data', exp: exp } # build expired token token = JWT.encode(exp_payload, hmac_secret, 'HS256') begin # add leeway to ensure the token is still accepted decoded_token = JWT.decode(token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' }) rescue JWT::ExpiredSignature # Handle expired token, e.g. logout user or deny access end ``` ### Not Before Time Claim From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.5): > The `nbf` (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the `nbf` claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the `nbf` claim. Implementers MAY provide for some small `leeway`, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a **_NumericDate_** value. Use of this claim is OPTIONAL. ```ruby nbf = Time.now.to_i - 3600 nbf_payload = { data: 'data', nbf: nbf } token = JWT.encode(nbf_payload, hmac_secret, 'HS256') begin decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' }) rescue JWT::ImmatureSignature # Handle invalid token, e.g. logout user or deny access end ``` The Not Before Claim verification can be disabled. ```ruby # Decode token without raising JWT::ImmatureSignature error JWT.decode(token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }) ``` Leeway and the nbf claim. ```ruby nbf = Time.now.to_i + 10 leeway = 30 nbf_payload = { data: 'data', nbf: nbf } # build expired token token = JWT.encode(nbf_payload, hmac_secret, 'HS256') begin # add leeway to ensure the token is valid decoded_token = JWT.decode(token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' }) rescue JWT::ImmatureSignature # Handle invalid token, e.g. logout user or deny access end ``` ### Issuer Claim From [Oauth JSON Web Token 4.1.1. "iss" (Issuer) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.1): > The `iss` (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The `iss` value is a case-sensitive string containing a **_StringOrURI_** value. Use of this claim is OPTIONAL. You can pass multiple allowed issuers as an Array, verification will pass if one of them matches the `iss` value in the payload. ```ruby iss = 'My Awesome Company Inc. or https://my.awesome.website/' iss_payload = { data: 'data', iss: iss } token = JWT.encode(iss_payload, hmac_secret, 'HS256') begin # Add iss to the validation to check if the token has been manipulated decoded_token = JWT.decode(token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) rescue JWT::InvalidIssuerError # Handle invalid token, e.g. logout user or deny access end ``` You can also pass a Regexp or Proc (with arity 1), verification will pass if the regexp matches or the proc returns truthy. On supported ruby versions (>= 2.5) you can also delegate to methods, on older versions you will have to convert them to proc (using `to_proc`) ```ruby JWT.decode(token, hmac_secret, true, iss: %r'https://my.awesome.website/', verify_iss: true, algorithm: 'HS256') ``` ```ruby JWT.decode(token, hmac_secret, true, iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') }, verify_iss: true, algorithm: 'HS256') ``` ```ruby JWT.decode(token, hmac_secret, true, iss: method(:valid_issuer?), verify_iss: true, algorithm: 'HS256') # somewhere in the same class: def valid_issuer?(issuer) # custom validation end ``` ### Audience Claim From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3): > The `aud` (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the `aud` claim when this claim is present, then the JWT MUST be rejected. In the general case, the `aud` value is an array of case-sensitive strings, each containing a **_StringOrURI_** value. In the special case when the JWT has one audience, the `aud` value MAY be a single case-sensitive string containing a **_StringOrURI_** value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL. ```ruby aud = ['Young', 'Old'] aud_payload = { data: 'data', aud: aud } token = JWT.encode(aud_payload, hmac_secret, 'HS256') begin # Add aud to the validation to check if the token has been manipulated decoded_token = JWT.decode(token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' }) rescue JWT::InvalidAudError # Handle invalid token, e.g. logout user or deny access puts 'Audience Error' end ``` ### JWT ID Claim From [Oauth JSON Web Token 4.1.7. "jti" (JWT ID) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.7): > The `jti` (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The `jti` claim can be used to prevent the JWT from being replayed. The `jti` value is a case-sensitive string. Use of this claim is OPTIONAL. ```ruby # Use the secret and iat to create a unique key per request to prevent replay attacks jti_raw = [hmac_secret, iat].join(':').to_s jti = Digest::MD5.hexdigest(jti_raw) jti_payload = { data: 'data', iat: iat, jti: jti } token = JWT.encode(jti_payload, hmac_secret, 'HS256') begin # If :verify_jti is true, validation will pass if a JTI is present #decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }) # Alternatively, pass a proc with your own code to check if the JTI has already been used decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }) # or decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }) rescue JWT::InvalidJtiError # Handle invalid token, e.g. logout user or deny access puts 'Error' end ``` ### Issued At Claim From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6): > The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. The `leeway` option is not taken into account when verifying this claim. The `iat_leeway` option was removed in version 2.2.0. Its value MUST be a number containing a **_NumericDate_** value. Use of this claim is OPTIONAL. ```ruby iat = Time.now.to_i iat_payload = { data: 'data', iat: iat } token = JWT.encode(iat_payload, hmac_secret, 'HS256') begin # Add iat to the validation to check if the token has been manipulated decoded_token = JWT.decode(token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' }) rescue JWT::InvalidIatError # Handle invalid token, e.g. logout user or deny access end ``` ### Subject Claim From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.2): > The `sub` (subject) claim identifies the principal that is the subject of the JWT. The Claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The sub value is a case-sensitive string containing a **_StringOrURI_** value. Use of this claim is OPTIONAL. ```ruby sub = 'Subject' sub_payload = { data: 'data', sub: sub } token = JWT.encode(sub_payload, hmac_secret, 'HS256') begin # Add sub to the validation to check if the token has been manipulated decoded_token = JWT.decode(token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' }) rescue JWT::InvalidSubError # Handle invalid token, e.g. logout user or deny access end ``` ### Standalone claim verification The JWT claim verifications can be used to verify any Hash to include expected keys and values. A few example on verifying the claims for a payload: ```ruby JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10}, :numeric, :exp) JWT::Claims.valid_payload?({"exp" => Time.now.to_i + 10}, :exp) # => true JWT::Claims.payload_errors({"exp" => Time.now.to_i - 10}, :exp) # => [#] JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11}) JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10, "sub" => "subject"}, :exp, sub: "subject") ``` ### Finding a Key To dynamically find the key for verifying the JWT signature, pass a block to the decode block. The block receives headers and the original payload as parameters. It should return with the key to verify the signature that was used to sign the JWT. ```ruby issuers = %w[My_Awesome_Company1 My_Awesome_Company2] iss_payload = { data: 'data', iss: issuers.first } secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' } token = JWT.encode(iss_payload, hmac_secret, 'HS256') begin # Add iss to the validation to check if the token has been manipulated decoded_token = JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| secrets[payload['iss']] end rescue JWT::InvalidIssuerError # Handle invalid token, e.g. logout user or deny access end ``` ### Required Claims You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing ```ruby # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent JWT.decode(token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }) ``` ### X.509 certificates in x5c header A JWT signature can be verified using certificate(s) given in the `x5c` header. Before doing that, the trustworthiness of these certificate(s) must be established. This is done in accordance with RFC 5280 which (among other things) verifies the certificate(s) are issued by a trusted root certificate, the timestamps are valid, and none of the certificate(s) are revoked (i.e. being present in the root certificate's Certificate Revocation List). ```ruby root_certificates = [] # trusted `OpenSSL::X509::Certificate` objects crl_uris = root_certificates.map(&:crl_uris) crls = crl_uris.map do |uri| # look up cached CRL by `uri` and return it if found, otherwise continue crl = Net::HTTP.get(uri) crl = OpenSSL::X509::CRL.new(crl) # cache `crl` using `uri` as the key, expiry set to `crl.next_update` timestamp end begin JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls } }) rescue JWT::DecodeError # Handle error, e.g. x5c header certificate revoked or expired end ``` ## JSON Web Key (JWK) JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC, OKP and HMAC keys. OKP support requires [RbNaCl](https://github.com/RubyCrypto/rbnacl) and currently only supports the Ed25519 curve. To encode a JWT using your JWK: ```ruby optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' } jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters) # Encoding payload = { data: 'data' } token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid]) # JSON Web Key Set for advertising your signing keys jwks_hash = JWT::JWK::Set.new(jwk).export ``` To decode a JWT using a trusted entity's JSON Web Key Set (JWKS): ```ruby jwks = JWT::JWK::Set.new(jwks_hash) jwks.filter! {|key| key[:use] == 'sig' } # Signing keys only! algorithms = jwks.map { |key| key[:alg] }.compact.uniq JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks) ``` The `jwks` option can also be given as a lambda that evaluates every time a key identifier is resolved. This can be used to implement caching of remotely fetched JWK Sets. Key identifiers can be specified using `kid`, `x5t` header parameters. If the requested identifier is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`. The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases. Tokens without a specified key identifier (`kid` or `x5t`) are rejected by default. This behaviour may be overwritten by setting the `allow_nil_kid` option for `decode` to `true`. ```ruby jwks_loader = ->(options) do # The jwk loader would fetch the set of JWKs from a trusted source. # To avoid malicious requests triggering cache invalidations there needs to be # some kind of grace time or other logic for determining the validity of the invalidation. # This example only allows cache invalidations every 5 minutes. if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300 logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache") @cached_keys = nil end @cached_keys ||= begin @cache_last_update = Time.now.to_i # Replace with your own JWKS fetching routine jwks = JWT::JWK::Set.new(jwks_hash) jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only jwks end end begin JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader }) rescue JWT::JWKError # Handle problems with the provided JWKs rescue JWT::DecodeError # Handle other decode related issues e.g. no kid in header, no matching public key found etc. end ``` ### Importing and exporting JSON Web Keys The ::JWT::JWK class can be used to import both JSON Web Keys and OpenSSL keys and export to either format with and without the private key included. To include the private key in the export pass the `include_private` parameter to the export method. ```ruby # Import a JWK Hash (showing an HMAC example) jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' }) # Import an OpenSSL key # You can optionally add descriptive parameters to the JWK desc_params = { kid: 'my-kid', use: 'sig' } jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params) # Export as JWK Hash (public key only by default) jwk_hash = jwk.export jwk_hash_with_private_key = jwk.export(include_private: true) # Export as OpenSSL key public_key = jwk.verify_key private_key = jwk.signing_key if jwk.private? # You can also import and export entire JSON Web Key Sets jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] } jwks = JWT::JWK::Set.new(jwks_hash) jwks_hash = jwks.export ``` ### Key ID (kid) and JWKs The key id (kid) generation in the gem is a custom algorithm and not based on any standards. To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration or can be given to the JWK instance on initialization. ```ruby JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint # OR JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint # OR jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint) jwk_hash = jwk.export thumbprint_as_the_kid = jwk_hash[:kid] ``` ## Development and testing The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features. ```bash bundle install bundle exec appraisal rake test ``` ## Releasing To cut a new release adjust the [version.rb](lib/jwt/version.rb) and [CHANGELOG](CHANGELOG.md) with desired version numbers and dates and commit the changes. Tag the release with the version number using the following command: ```bash rake release:source_control_push ``` This will tag a new version an trigger a [GitHub action](.github/workflows/push_gem.yml) that eventually will push the gem to rubygems.org. ## How to contribute See [CONTRIBUTING](CONTRIBUTING.md). ## Contributors See [AUTHORS](AUTHORS). ## License See [LICENSE](LICENSE). ruby-jwt-3.1.2/Rakefile000066400000000000000000000003611503003570500147430ustar00rootroot00000000000000# frozen_string_literal: true require 'bundler/setup' require 'bundler/gem_tasks' require 'rspec/core/rake_task' require 'rubocop/rake_task' RSpec::Core::RakeTask.new(:test) RuboCop::RakeTask.new(:rubocop) task default: %i[rubocop test] ruby-jwt-3.1.2/UPGRADING.md000066400000000000000000000065071503003570500151500ustar00rootroot00000000000000# Upgrading ruby-jwt to >= 3.0.0 ## Removal of the indirect [RbNaCl](https://github.com/RubyCrypto/rbnacl) dependency Historically, the set of supported algorithms was extended by including the `rbnacl` gem in the application's Gemfile. On load, ruby-jwt tried to load the gem and, if available, extend the algorithms to those provided by the `rbnacl/libsodium` libraries. This indirect dependency has caused some maintenance pain and confusion about which versions of the gem are supported. Some work to ease the way alternative algorithms can be implemented has been done. This enables the extraction of the algorithm provided by `rbnacl`. The extracted algorithms now live in the [jwt-eddsa](https://rubygems.org/gems/jwt-eddsa) gem. ### Dropped support for HS512256 The algorithm HS512256 (HMAC-SHA-512 truncated to 256-bits) is not part of any JWA/JWT RFC and therefore will not be supported anymore. It was part of the HMAC algorithms provided by the indirect [RbNaCl](https://github.com/RubyCrypto/rbnacl) dependency. Currently, there are no direct substitutes for the algorithm. ### `JWT::EncodedToken#payload` will raise before token is verified To avoid accidental use of unverified tokens, the `JWT::EncodedToken#payload` method will raise an error if accessed before the token signature has been verified. To access the payload before verification, use the method `JWT::EncodedToken#unverified_payload`. ## Stricter requirements on Base64 encoded data Base64 decoding will no longer fallback on the looser RFC 2045. The biggest difference is that the looser version was ignoring whitespaces and newlines, whereas the stricter version raises errors in such cases. If you, for example, read tokens from files, there could be problems with trailing newlines. Make sure you trim your input before passing it to the decoding mechanisms. ## Claim verification revamp Claim verification has been [split into separate classes](https://github.com/jwt/ruby-jwt/pull/605) and has [a new API](https://github.com/jwt/ruby-jwt/pull/626), leading to the following deprecations: - The `::JWT::ClaimsValidator` class will be removed in favor of the functionality provided by `::JWT::Claims`. - The `::JWT::Claims::verify!` method will be removed in favor of `::JWT::Claims::verify_payload!`. - The `::JWT::JWA.create` method will be removed. - The `::JWT::Verify` class will be removed in favor of the functionality provided by `::JWT::Claims`. - Calling `::JWT::Claims::Numeric.new` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`. - Calling `::JWT::Claims::Numeric.verify!` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`. ## Algorithm restructuring The internal algorithms were [restructured](https://github.com/jwt/ruby-jwt/pull/607) to support extensions from separate libraries. The changes led to a few deprecations and new requirements: - The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed. - Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module. ## Base64 the `k´ value for HMAC JWKs The gem was missing the Base64 encoding and decoding when representing and parsing a HMAC key as a JWK. This issue is now addressed. The added encoding will break compatibility with JWKs produced by older versions of the gem. ruby-jwt-3.1.2/bin/000077500000000000000000000000001503003570500140465ustar00rootroot00000000000000ruby-jwt-3.1.2/bin/console.rb000077500000000000000000000001741503003570500160420ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'bundler/setup' require 'jwt' require 'irb' IRB.start(__FILE__) ruby-jwt-3.1.2/bin/smoke.rb000077500000000000000000000004131503003570500155120ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'jwt' puts "Running simple encode/decode test for #{JWT.gem_version}" secret = 'secretkeyforsigning' token = JWT.encode({ con: 'tent' }, secret, 'HS256') JWT.decode(token, secret, true, algorithm: 'HS256') ruby-jwt-3.1.2/gemfiles/000077500000000000000000000000001503003570500150715ustar00rootroot00000000000000ruby-jwt-3.1.2/gemfiles/openssl.gemfile000066400000000000000000000001631503003570500201060ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" gem "openssl", "< 2.0" gemspec path: "../" ruby-jwt-3.1.2/gemfiles/standalone.gemfile000066400000000000000000000001331503003570500205500ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" gemspec path: "../" ruby-jwt-3.1.2/lib/000077500000000000000000000000001503003570500140445ustar00rootroot00000000000000ruby-jwt-3.1.2/lib/jwt.rb000066400000000000000000000030241503003570500151740ustar00rootroot00000000000000# frozen_string_literal: true require 'jwt/version' require 'jwt/base64' require 'jwt/json' require 'jwt/decode' require 'jwt/configuration' require 'jwt/encode' require 'jwt/error' require 'jwt/jwk' require 'jwt/claims' require 'jwt/encoded_token' require 'jwt/token' # JSON Web Token implementation # # Should be up to date with the latest spec: # https://tools.ietf.org/html/rfc7519 module JWT extend ::JWT::Configuration module_function # Encodes a payload into a JWT. # # @param payload [Hash] the payload to encode. # @param key [String] the key used to sign the JWT. # @param algorithm [String] the algorithm used to sign the JWT. # @param header_fields [Hash] additional headers to include in the JWT. # @return [String] the encoded JWT. def encode(payload, key, algorithm = 'HS256', header_fields = {}) Encode.new(payload: payload, key: key, algorithm: algorithm, headers: header_fields).segments end # Decodes a JWT to extract the payload and header # # @param jwt [String] the JWT to decode. # @param key [String] the key used to verify the JWT. # @param verify [Boolean] whether to verify the JWT signature. # @param options [Hash] additional options for decoding. # @return [Array] the decoded payload and headers. def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) # rubocop:disable Style/OptionalBooleanParameter Decode.new(jwt, key, verify, configuration.decode.to_h.merge(options), &keyfinder).decode_segments end end ruby-jwt-3.1.2/lib/jwt/000077500000000000000000000000001503003570500146505ustar00rootroot00000000000000ruby-jwt-3.1.2/lib/jwt/base64.rb000066400000000000000000000012321503003570500162570ustar00rootroot00000000000000# frozen_string_literal: true require 'base64' module JWT # Base64 encoding and decoding # @api private class Base64 class << self # Encode a string with URL-safe Base64 complying with RFC 4648 (not padded). # @api private def url_encode(str) ::Base64.urlsafe_encode64(str, padding: false) end # Decode a string with URL-safe Base64 complying with RFC 4648. # @api private def url_decode(str) ::Base64.urlsafe_decode64(str) rescue ArgumentError => e raise unless e.message == 'invalid base64' raise Base64DecodeError, 'Invalid base64 encoding' end end end end ruby-jwt-3.1.2/lib/jwt/claims.rb000066400000000000000000000041361503003570500164510ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'claims/audience' require_relative 'claims/crit' require_relative 'claims/decode_verifier' require_relative 'claims/expiration' require_relative 'claims/issued_at' require_relative 'claims/issuer' require_relative 'claims/jwt_id' require_relative 'claims/not_before' require_relative 'claims/numeric' require_relative 'claims/required' require_relative 'claims/subject' require_relative 'claims/verifier' module JWT # JWT Claim verifications # https://datatracker.ietf.org/doc/html/rfc7519#section-4 # # Verification is supported for the following claims: # exp # nbf # iss # iat # jti # aud # sub # required # numeric module Claims # Represents a claim verification error Error = Struct.new(:message, keyword_init: true) class << self # Checks if the claims in the JWT payload are valid. # @example # # ::JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10}, :exp) # ::JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11}) # # @param payload [Hash] the JWT payload. # @param options [Array] the options for verifying the claims. # @return [void] # @raise [JWT::DecodeError] if any claim is invalid. def verify_payload!(payload, *options) Verifier.verify!(VerificationContext.new(payload: payload), *options) end # Checks if the claims in the JWT payload are valid. # # @param payload [Hash] the JWT payload. # @param options [Array] the options for verifying the claims. # @return [Boolean] true if the claims are valid, false otherwise def valid_payload?(payload, *options) payload_errors(payload, *options).empty? end # Returns the errors in the claims of the JWT token. # # @param options [Array] the options for verifying the claims. # @return [Array] the errors in the claims of the JWT def payload_errors(payload, *options) Verifier.errors(VerificationContext.new(payload: payload), *options) end end end end ruby-jwt-3.1.2/lib/jwt/claims/000077500000000000000000000000001503003570500161205ustar00rootroot00000000000000ruby-jwt-3.1.2/lib/jwt/claims/audience.rb000066400000000000000000000020151503003570500202200ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # The Audience class is responsible for validating the audience claim ('aud') in a JWT token. class Audience # Initializes a new Audience instance. # # @param expected_audience [String, Array] the expected audience(s) for the JWT token. def initialize(expected_audience:) @expected_audience = expected_audience end # Verifies the audience claim ('aud') in the JWT token. # # @param context [Object] the context containing the JWT payload. # @param _args [Hash] additional arguments (not used). # @raise [JWT::InvalidAudError] if the audience claim is invalid. # @return [nil] def verify!(context:, **_args) aud = context.payload['aud'] raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || ''}" if ([*aud] & [*expected_audience]).empty? end private attr_reader :expected_audience end end end ruby-jwt-3.1.2/lib/jwt/claims/crit.rb000066400000000000000000000022321503003570500174050ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # Responsible of validation the crit header class Crit # Initializes a new Crit instance. # # @param expected_crits [String] the expected crit header values for the JWT token. def initialize(expected_crits:) @expected_crits = Array(expected_crits) end # Verifies the critical claim ('crit') in the JWT token header. # # @param context [Object] the context containing the JWT payload and header. # @param _args [Hash] additional arguments (not used). # @raise [JWT::InvalidCritError] if the crit claim is invalid. # @return [nil] def verify!(context:, **_args) raise(JWT::InvalidCritError, 'Crit header missing') unless context.header['crit'] raise(JWT::InvalidCritError, 'Crit header should be an array') unless context.header['crit'].is_a?(Array) missing = (expected_crits - context.header['crit']) raise(JWT::InvalidCritError, "Crit header missing expected values: #{missing.join(', ')}") if missing.any? nil end private attr_reader :expected_crits end end end ruby-jwt-3.1.2/lib/jwt/claims/decode_verifier.rb000066400000000000000000000030301503003570500215570ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # Context class to contain the data passed to individual claim validators # # @api private VerificationContext = Struct.new(:payload, keyword_init: true) # Verifiers to support the ::JWT.decode method # # @api private module DecodeVerifier VERIFIERS = { verify_expiration: ->(options) { Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]) }, verify_not_before: ->(options) { Claims::NotBefore.new(leeway: options[:nbf_leeway] || options[:leeway]) }, verify_iss: ->(options) { options[:iss] && Claims::Issuer.new(issuers: options[:iss]) }, verify_iat: ->(*) { Claims::IssuedAt.new }, verify_jti: ->(options) { Claims::JwtId.new(validator: options[:verify_jti]) }, verify_aud: ->(options) { options[:aud] && Claims::Audience.new(expected_audience: options[:aud]) }, verify_sub: ->(options) { options[:sub] && Claims::Subject.new(expected_subject: options[:sub]) }, required_claims: ->(options) { Claims::Required.new(required_claims: options[:required_claims]) } }.freeze private_constant(:VERIFIERS) class << self # @api private def verify!(payload, options) VERIFIERS.each do |key, verifier_builder| next unless options[key] || options[key.to_s] verifier_builder&.call(options)&.verify!(context: VerificationContext.new(payload: payload)) end nil end end end end end ruby-jwt-3.1.2/lib/jwt/claims/expiration.rb000066400000000000000000000020331503003570500206250ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # The Expiration class is responsible for validating the expiration claim ('exp') in a JWT token. class Expiration # Initializes a new Expiration instance. # # @param leeway [Integer] the amount of leeway (in seconds) to allow when validating the expiration time. Default: 0. def initialize(leeway:) @leeway = leeway || 0 end # Verifies the expiration claim ('exp') in the JWT token. # # @param context [Object] the context containing the JWT payload. # @param _args [Hash] additional arguments (not used). # @raise [JWT::ExpiredSignature] if the token has expired. # @return [nil] def verify!(context:, **_args) return unless context.payload.is_a?(Hash) return unless context.payload.key?('exp') raise JWT::ExpiredSignature, 'Signature has expired' if context.payload['exp'].to_i <= (Time.now.to_i - leeway) end private attr_reader :leeway end end end ruby-jwt-3.1.2/lib/jwt/claims/issued_at.rb000066400000000000000000000014161503003570500204270ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # The IssuedAt class is responsible for validating the issued at claim ('iat') in a JWT token. class IssuedAt # Verifies the issued at claim ('iat') in the JWT token. # # @param context [Object] the context containing the JWT payload. # @param _args [Hash] additional arguments (not used). # @raise [JWT::InvalidIatError] if the issued at claim is invalid. # @return [nil] def verify!(context:, **_args) return unless context.payload.is_a?(Hash) return unless context.payload.key?('iat') iat = context.payload['iat'] raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > Time.now.to_f end end end end ruby-jwt-3.1.2/lib/jwt/claims/issuer.rb000066400000000000000000000020571503003570500177630ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # The Issuer class is responsible for validating the issuer claim ('iss') in a JWT token. class Issuer # Initializes a new Issuer instance. # # @param issuers [String, Symbol, Array] the expected issuer(s) for the JWT token. def initialize(issuers:) @issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item } end # Verifies the issuer claim ('iss') in the JWT token. # # @param context [Object] the context containing the JWT payload. # @param _args [Hash] additional arguments (not used). # @raise [JWT::InvalidIssuerError] if the issuer claim is invalid. # @return [nil] def verify!(context:, **_args) case (iss = context.payload['iss']) when *issuers nil else raise JWT::InvalidIssuerError, "Invalid issuer. Expected #{issuers}, received #{iss || ''}" end end private attr_reader :issuers end end end ruby-jwt-3.1.2/lib/jwt/claims/jwt_id.rb000066400000000000000000000021701503003570500177250ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # The JwtId class is responsible for validating the JWT ID claim ('jti') in a JWT token. class JwtId # Initializes a new JwtId instance. # # @param validator [#call] an object responding to `call` to validate the JWT ID. def initialize(validator:) @validator = validator end # Verifies the JWT ID claim ('jti') in the JWT token. # # @param context [Object] the context containing the JWT payload. # @param _args [Hash] additional arguments (not used). # @raise [JWT::InvalidJtiError] if the JWT ID claim is invalid or missing. # @return [nil] def verify!(context:, **_args) jti = context.payload['jti'] if validator.respond_to?(:call) verified = validator.arity == 2 ? validator.call(jti, context.payload) : validator.call(jti) raise(JWT::InvalidJtiError, 'Invalid jti') unless verified elsif jti.to_s.strip.empty? raise(JWT::InvalidJtiError, 'Missing jti') end end private attr_reader :validator end end end ruby-jwt-3.1.2/lib/jwt/claims/not_before.rb000066400000000000000000000020641503003570500205710ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # The NotBefore class is responsible for validating the 'nbf' (Not Before) claim in a JWT token. class NotBefore # Initializes a new NotBefore instance. # # @param leeway [Integer] the amount of leeway (in seconds) to allow when validating the 'nbf' claim. Defaults to 0. def initialize(leeway:) @leeway = leeway || 0 end # Verifies the 'nbf' (Not Before) claim in the JWT token. # # @param context [Object] the context containing the JWT payload. # @param _args [Hash] additional arguments (not used). # @raise [JWT::ImmatureSignature] if the 'nbf' claim has not been reached. # @return [nil] def verify!(context:, **_args) return unless context.payload.is_a?(Hash) return unless context.payload.key?('nbf') raise JWT::ImmatureSignature, 'Signature nbf has not been reached' if context.payload['nbf'].to_i > (Time.now.to_i + leeway) end private attr_reader :leeway end end end ruby-jwt-3.1.2/lib/jwt/claims/numeric.rb000066400000000000000000000024231503003570500201100ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # The Numeric class is responsible for validating numeric claims in a JWT token. # The numeric claims are: exp, iat and nbf class Numeric # List of numeric claims that can be validated. NUMERIC_CLAIMS = %i[ exp iat nbf ].freeze private_constant(:NUMERIC_CLAIMS) # Verifies the numeric claims in the JWT context. # # @param context [Object] the context containing the JWT payload. # @raise [JWT::InvalidClaimError] if any numeric claim is invalid. # @return [nil] def verify!(context:) validate_numeric_claims(context.payload) end private def validate_numeric_claims(payload) NUMERIC_CLAIMS.each do |claim| validate_is_numeric(payload, claim) end end def validate_is_numeric(payload, claim) return unless payload.is_a?(Hash) return unless payload.key?(claim) || payload.key?(claim.to_s) return if payload[claim].is_a?(::Numeric) || payload[claim.to_s].is_a?(::Numeric) raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{(payload[claim] || payload[claim.to_s]).class}" end end end end ruby-jwt-3.1.2/lib/jwt/claims/required.rb000066400000000000000000000020571503003570500202710ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # The Required class is responsible for validating that all required claims are present in a JWT token. class Required # Initializes a new Required instance. # # @param required_claims [Array] the list of required claims. def initialize(required_claims:) @required_claims = required_claims end # Verifies that all required claims are present in the JWT payload. # # @param context [Object] the context containing the JWT payload. # @param _args [Hash] additional arguments (not used). # @raise [JWT::MissingRequiredClaim] if any required claim is missing. # @return [nil] def verify!(context:, **_args) required_claims.each do |required_claim| next if context.payload.is_a?(Hash) && context.payload.key?(required_claim) raise JWT::MissingRequiredClaim, "Missing required claim #{required_claim}" end end private attr_reader :required_claims end end end ruby-jwt-3.1.2/lib/jwt/claims/subject.rb000066400000000000000000000017551503003570500201140ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # The Subject class is responsible for validating the subject claim ('sub') in a JWT token. class Subject # Initializes a new Subject instance. # # @param expected_subject [String] the expected subject for the JWT token. def initialize(expected_subject:) @expected_subject = expected_subject.to_s end # Verifies the subject claim ('sub') in the JWT token. # # @param context [Object] the context containing the JWT payload. # @param _args [Hash] additional arguments (not used). # @raise [JWT::InvalidSubError] if the subject claim is invalid. # @return [nil] def verify!(context:, **_args) sub = context.payload['sub'] raise(JWT::InvalidSubError, "Invalid subject. Expected #{expected_subject}, received #{sub || ''}") unless sub.to_s == expected_subject end private attr_reader :expected_subject end end end ruby-jwt-3.1.2/lib/jwt/claims/verifier.rb000066400000000000000000000041001503003570500202530ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Claims # @api private module Verifier VERIFIERS = { exp: ->(options) { Claims::Expiration.new(leeway: options.dig(:exp, :leeway)) }, nbf: ->(options) { Claims::NotBefore.new(leeway: options.dig(:nbf, :leeway)) }, iss: ->(options) { Claims::Issuer.new(issuers: options[:iss]) }, iat: ->(*) { Claims::IssuedAt.new }, jti: ->(options) { Claims::JwtId.new(validator: options[:jti]) }, aud: ->(options) { Claims::Audience.new(expected_audience: options[:aud]) }, sub: ->(options) { Claims::Subject.new(expected_subject: options[:sub]) }, crit: ->(options) { Claims::Crit.new(expected_crits: options[:crit]) }, required: ->(options) { Claims::Required.new(required_claims: options[:required]) }, numeric: ->(*) { Claims::Numeric.new } }.freeze private_constant(:VERIFIERS) class << self # @api private def verify!(context, *options) iterate_verifiers(*options) do |verifier, verifier_options| verify_one!(context, verifier, verifier_options) end nil end # @api private def errors(context, *options) errors = [] iterate_verifiers(*options) do |verifier, verifier_options| verify_one!(context, verifier, verifier_options) rescue ::JWT::DecodeError => e errors << Error.new(message: e.message) end errors end private def iterate_verifiers(*options) options.each do |element| if element.is_a?(Hash) element.each_key { |key| yield(key, element) } else yield(element, {}) end end end def verify_one!(context, verifier, options) verifier_builder = VERIFIERS.fetch(verifier) { raise ArgumentError, "#{verifier} not a valid claim verifier" } verifier_builder.call(options || {}).verify!(context: context) end end end end end ruby-jwt-3.1.2/lib/jwt/configuration.rb000066400000000000000000000012241503003570500200430ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'configuration/container' module JWT # The Configuration module provides methods to configure JWT settings. module Configuration # Configures the JWT settings. # # @yield [config] Gives the current configuration to the block. # @yieldparam config [JWT::Configuration::Container] the configuration container. def configure yield(configuration) end # Returns the JWT configuration container. # # @return [JWT::Configuration::Container] the configuration container. def configuration @configuration ||= ::JWT::Configuration::Container.new end end end ruby-jwt-3.1.2/lib/jwt/configuration/000077500000000000000000000000001503003570500175175ustar00rootroot00000000000000ruby-jwt-3.1.2/lib/jwt/configuration/container.rb000066400000000000000000000034111503003570500220250ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'decode_configuration' require_relative 'jwk_configuration' module JWT module Configuration # The Container class holds the configuration settings for JWT. class Container # @!attribute [rw] decode # @return [DecodeConfiguration] the decode configuration. # @!attribute [rw] jwk # @return [JwkConfiguration] the JWK configuration. # @!attribute [rw] strict_base64_decoding # @return [Boolean] whether strict Base64 decoding is enabled. attr_accessor :decode, :jwk, :strict_base64_decoding # @!attribute [r] deprecation_warnings # @return [Symbol] the deprecation warnings setting. attr_reader :deprecation_warnings # Initializes a new Container instance and resets the configuration. def initialize reset! end # Resets the configuration to default values. # # @return [void] def reset! @decode = DecodeConfiguration.new @jwk = JwkConfiguration.new self.deprecation_warnings = :once end DEPRECATION_WARNINGS_VALUES = %i[once warn silent].freeze private_constant(:DEPRECATION_WARNINGS_VALUES) # Sets the deprecation warnings setting. # # @param value [Symbol] the deprecation warnings setting. Must be one of `:once`, `:warn`, or `:silent`. # @raise [ArgumentError] if the value is not one of the supported values. # @return [void] def deprecation_warnings=(value) raise ArgumentError, "Invalid deprecation_warnings value #{value}. Supported values: #{DEPRECATION_WARNINGS_VALUES}" unless DEPRECATION_WARNINGS_VALUES.include?(value) @deprecation_warnings = value end end end end ruby-jwt-3.1.2/lib/jwt/configuration/decode_configuration.rb000066400000000000000000000045761503003570500242320ustar00rootroot00000000000000# frozen_string_literal: true module JWT module Configuration # The DecodeConfiguration class holds the configuration settings for decoding JWT tokens. class DecodeConfiguration # @!attribute [rw] verify_expiration # @return [Boolean] whether to verify the expiration claim. # @!attribute [rw] verify_not_before # @return [Boolean] whether to verify the not before claim. # @!attribute [rw] verify_iss # @return [Boolean] whether to verify the issuer claim. # @!attribute [rw] verify_iat # @return [Boolean] whether to verify the issued at claim. # @!attribute [rw] verify_jti # @return [Boolean] whether to verify the JWT ID claim. # @!attribute [rw] verify_aud # @return [Boolean] whether to verify the audience claim. # @!attribute [rw] verify_sub # @return [Boolean] whether to verify the subject claim. # @!attribute [rw] leeway # @return [Integer] the leeway in seconds for time-based claims. # @!attribute [rw] algorithms # @return [Array] the list of acceptable algorithms. # @!attribute [rw] required_claims # @return [Array] the list of required claims. attr_accessor :verify_expiration, :verify_not_before, :verify_iss, :verify_iat, :verify_jti, :verify_aud, :verify_sub, :leeway, :algorithms, :required_claims # Initializes a new DecodeConfiguration instance with default settings. def initialize @verify_expiration = true @verify_not_before = true @verify_iss = false @verify_iat = false @verify_jti = false @verify_aud = false @verify_sub = false @leeway = 0 @algorithms = ['HS256'] @required_claims = [] end # @api private def to_h { verify_expiration: verify_expiration, verify_not_before: verify_not_before, verify_iss: verify_iss, verify_iat: verify_iat, verify_jti: verify_jti, verify_aud: verify_aud, verify_sub: verify_sub, leeway: leeway, algorithms: algorithms, required_claims: required_claims } end end end end ruby-jwt-3.1.2/lib/jwt/configuration/jwk_configuration.rb000066400000000000000000000014211503003570500235640ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../jwk/kid_as_key_digest' require_relative '../jwk/thumbprint' module JWT module Configuration # @api private class JwkConfiguration def initialize self.kid_generator_type = :key_digest end def kid_generator_type=(value) self.kid_generator = case value when :key_digest JWT::JWK::KidAsKeyDigest when :rfc7638_thumbprint JWT::JWK::Thumbprint else raise ArgumentError, "#{value} is not a valid kid generator type." end end attr_accessor :kid_generator end end end ruby-jwt-3.1.2/lib/jwt/decode.rb000066400000000000000000000076341503003570500164320ustar00rootroot00000000000000# frozen_string_literal: true require 'json' require 'jwt/x5c_key_finder' module JWT # The Decode class is responsible for decoding and verifying JWT tokens. class Decode # Order is very important - first check for string keys, next for symbols ALGORITHM_KEYS = ['algorithm', :algorithm, 'algorithms', :algorithms].freeze # Initializes a new Decode instance. # # @param jwt [String] the JWT to decode. # @param key [String, Array] the key(s) to use for verification. # @param verify [Boolean] whether to verify the token's signature. # @param options [Hash] additional options for decoding and verification. # @param keyfinder [Proc] an optional key finder block to dynamically find the key for verification. # @raise [JWT::DecodeError] if decoding or verification fails. def initialize(jwt, key, verify, options, &keyfinder) raise JWT::DecodeError, 'Nil JSON web token' unless jwt @token = EncodedToken.new(jwt) @key = key @options = options @verify = verify @keyfinder = keyfinder end # Decodes the JWT token and verifies its segments if verification is enabled. # # @return [Array] an array containing the decoded payload and header. def decode_segments validate_segment_count! if @verify verify_algo set_key verify_signature Claims::DecodeVerifier.verify!(token.unverified_payload, @options) end [token.unverified_payload, token.header] end private attr_reader :token def verify_signature return if none_algorithm? raise JWT::DecodeError, 'No verification key available' unless @key token.verify_signature!(algorithm: allowed_and_valid_algorithms, key: @key) end def verify_algo raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty? raise JWT::DecodeError, 'Token header not a JSON object' unless token.header.is_a?(Hash) raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty? end def set_key @key = find_key(&@keyfinder) if @keyfinder if @options[:jwks] @key = ::JWT::JWK::KeyFinder.new( jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid], key_fields: @options[:key_fields] ).call(token) end return unless (x5c_options = @options[:x5c]) @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c']) end def allowed_and_valid_algorithms @allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) } end def given_algorithms alg_key = ALGORITHM_KEYS.find { |key| @options[key] } Array(@options[alg_key]) end def allowed_algorithms @allowed_algorithms ||= resolve_allowed_algorithms end def resolve_allowed_algorithms given_algorithms.map { |alg| JWA.resolve(alg) } end def find_key(&keyfinder) key = (keyfinder.arity == 2 ? yield(token.header, token.unverified_payload) : yield(token.header)) # key can be of type [string, nil, OpenSSL::PKey, Array] return key if key && !Array(key).empty? raise JWT::DecodeError, 'No verification key available' end def validate_segment_count! segment_count = token.jwt.count('.') + 1 return if segment_count == 3 return if !@verify && segment_count == 2 # If no verifying required, the signature is not needed return if segment_count == 2 && none_algorithm? raise JWT::DecodeError, 'Not enough or too many segments' end def none_algorithm? alg_in_header == 'none' end def alg_in_header token.header['alg'] end end end ruby-jwt-3.1.2/lib/jwt/encode.rb000066400000000000000000000017371503003570500164420ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'jwa' module JWT # The Encode class is responsible for encoding JWT tokens. class Encode # Initializes a new Encode instance. # # @param options [Hash] the options for encoding the JWT token. # @option options [Hash] :payload the payload of the JWT token. # @option options [Hash] :headers the headers of the JWT token. # @option options [String] :key the key used to sign the JWT token. # @option options [String] :algorithm the algorithm used to sign the JWT token. def initialize(options) @token = Token.new(payload: options[:payload], header: options[:headers]) @key = options[:key] @algorithm = options[:algorithm] end # Encodes the JWT token and returns its segments. # # @return [String] the encoded JWT token. def segments @token.verify_claims!(:numeric) @token.sign!(algorithm: @algorithm, key: @key) @token.jwt end end end ruby-jwt-3.1.2/lib/jwt/encoded_token.rb000066400000000000000000000176601503003570500200100ustar00rootroot00000000000000# frozen_string_literal: true module JWT # Represents an encoded JWT token # # Processing an encoded and signed token: # # token = JWT::Token.new(payload: {pay: 'load'}) # token.sign!(algorithm: 'HS256', key: 'secret') # # encoded_token = JWT::EncodedToken.new(token.jwt) # encoded_token.verify_signature!(algorithm: 'HS256', key: 'secret') # encoded_token.payload # => {'pay' => 'load'} class EncodedToken # @private # Allow access to the unverified payload for claim verification. class ClaimsContext extend Forwardable def_delegators :@token, :header, :unverified_payload def initialize(token) @token = token end def payload unverified_payload end end DEFAULT_CLAIMS = [:exp].freeze private_constant(:DEFAULT_CLAIMS) # Returns the original token provided to the class. # @return [String] The JWT token. attr_reader :jwt # Initializes a new EncodedToken instance. # # @param jwt [String] the encoded JWT token. # @raise [ArgumentError] if the provided JWT is not a String. def initialize(jwt) raise ArgumentError, 'Provided JWT must be a String' unless jwt.is_a?(String) @jwt = jwt @signature_verified = false @claims_verified = false @encoded_header, @encoded_payload, @encoded_signature = jwt.split('.') end # Returns the decoded signature of the JWT token. # # @return [String] the decoded signature. def signature @signature ||= ::JWT::Base64.url_decode(encoded_signature || '') end # Returns the encoded signature of the JWT token. # # @return [String] the encoded signature. attr_reader :encoded_signature # Returns the decoded header of the JWT token. # # @return [Hash] the header. def header @header ||= parse_and_decode(@encoded_header) end # Returns the encoded header of the JWT token. # # @return [String] the encoded header. attr_reader :encoded_header # Returns the payload of the JWT token. Access requires the signature and claims to have been verified. # # @return [Hash] the payload. # @raise [JWT::DecodeError] if the signature has not been verified. def payload raise JWT::DecodeError, 'Verify the token signature before accessing the payload' unless @signature_verified raise JWT::DecodeError, 'Verify the token claims before accessing the payload' unless @claims_verified decoded_payload end # Returns the payload of the JWT token without requiring the signature to have been verified. # @return [Hash] the payload. def unverified_payload decoded_payload end # Sets or returns the encoded payload of the JWT token. # # @return [String] the encoded payload. attr_accessor :encoded_payload # Returns the signing input of the JWT token. # # @return [String] the signing input. def signing_input [encoded_header, encoded_payload].join('.') end # Verifies the token signature and claims. # By default it verifies the 'exp' claim. # # @example # encoded_token.verify!(signature: { algorithm: 'HS256', key: 'secret' }, claims: [:exp]) # # @param signature [Hash] the parameters for signature verification (see {#verify_signature!}). # @param claims [Array, Hash] the claims to verify (see {#verify_claims!}). # @return [nil] # @raise [JWT::DecodeError] if the signature or claim verification fails. def verify!(signature:, claims: nil) verify_signature!(**signature) claims.is_a?(Array) ? verify_claims!(*claims) : verify_claims!(claims) nil end # Verifies the token signature and claims. # By default it verifies the 'exp' claim. # @param signature [Hash] the parameters for signature verification (see {#verify_signature!}). # @param claims [Array, Hash] the claims to verify (see {#verify_claims!}). # @return [Boolean] true if the signature and claims are valid, false otherwise. def valid?(signature:, claims: nil) valid_signature?(**signature) && (claims.is_a?(Array) ? valid_claims?(*claims) : valid_claims?(claims)) end # Verifies the signature of the JWT token. # # @param algorithm [String, Array, Object, Array] the algorithm(s) to use for verification. # @param key [String, Array] the key(s) to use for verification. # @param key_finder [#call] an object responding to `call` to find the key for verification. # @return [nil] # @raise [JWT::VerificationError] if the signature verification fails. # @raise [ArgumentError] if neither key nor key_finder is provided, or if both are provided. def verify_signature!(algorithm:, key: nil, key_finder: nil) return if valid_signature?(algorithm: algorithm, key: key, key_finder: key_finder) raise JWT::VerificationError, 'Signature verification failed' end # Checks if the signature of the JWT token is valid. # # @param algorithm [String, Array, Object, Array] the algorithm(s) to use for verification. # @param key [String, Array, JWT::JWK::KeyBase, Array] the key(s) to use for verification. # @param key_finder [#call] an object responding to `call` to find the key for verification. # @return [Boolean] true if the signature is valid, false otherwise. def valid_signature?(algorithm: nil, key: nil, key_finder: nil) raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil? keys = Array(key || key_finder.call(self)) verifiers = JWA.create_verifiers(algorithms: algorithm, keys: keys, preferred_algorithm: header['alg']) raise JWT::VerificationError, 'No algorithm provided' if verifiers.empty? valid = verifiers.any? do |jwa| jwa.verify(data: signing_input, signature: signature) end valid.tap { |verified| @signature_verified = verified } end # Verifies the claims of the token. # @param options [Array, Hash] the claims to verify. By default, it checks the 'exp' claim. # @raise [JWT::DecodeError] if the claims are invalid. def verify_claims!(*options) Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap do @claims_verified = true end rescue StandardError @claims_verified = false raise end # Returns the errors of the claims of the token. # @param options [Array, Hash] the claims to verify. By default, it checks the 'exp' claim. # @return [Array] the errors of the claims. def claim_errors(*options) Claims::Verifier.errors(ClaimsContext.new(self), *claims_options(options)) end # Returns whether the claims of the token are valid. # @param options [Array, Hash] the claims to verify. By default, it checks the 'exp' claim. # @return [Boolean] whether the claims are valid. def valid_claims?(*options) claim_errors(*claims_options(options)).empty?.tap { |verified| @claims_verified = verified } end alias to_s jwt private def claims_options(options) return DEFAULT_CLAIMS if options.first.nil? options end def decode_payload raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == '' if unencoded_payload? verify_claims!(crit: ['b64']) return parse_unencoded(encoded_payload) end parse_and_decode(encoded_payload) end def unencoded_payload? header['b64'] == false end def parse_and_decode(segment) parse(::JWT::Base64.url_decode(segment || '')) end def parse_unencoded(segment) parse(segment) end def parse(segment) JWT::JSON.parse(segment) rescue ::JSON::ParserError raise JWT::DecodeError, 'Invalid segment encoding' end def decoded_payload @decoded_payload ||= decode_payload end end end ruby-jwt-3.1.2/lib/jwt/error.rb000066400000000000000000000042321503003570500163270ustar00rootroot00000000000000# frozen_string_literal: true module JWT # The EncodeError class is raised when there is an error encoding a JWT. class EncodeError < StandardError; end # The DecodeError class is raised when there is an error decoding a JWT. class DecodeError < StandardError; end # The VerificationError class is raised when there is an error verifying a JWT. class VerificationError < DecodeError; end # The ExpiredSignature class is raised when the JWT signature has expired. class ExpiredSignature < DecodeError; end # The IncorrectAlgorithm class is raised when the JWT algorithm is incorrect. class IncorrectAlgorithm < DecodeError; end # The ImmatureSignature class is raised when the JWT signature is immature. class ImmatureSignature < DecodeError; end # The InvalidIssuerError class is raised when the JWT issuer is invalid. class InvalidIssuerError < DecodeError; end # The UnsupportedEcdsaCurve class is raised when the ECDSA curve is unsupported. class UnsupportedEcdsaCurve < IncorrectAlgorithm; end # The InvalidIatError class is raised when the JWT issued at (iat) claim is invalid. class InvalidIatError < DecodeError; end # The InvalidAudError class is raised when the JWT audience (aud) claim is invalid. class InvalidAudError < DecodeError; end # The InvalidSubError class is raised when the JWT subject (sub) claim is invalid. class InvalidSubError < DecodeError; end # The InvalidCritError class is raised when the JWT crit header is invalid. class InvalidCritError < DecodeError; end # The InvalidJtiError class is raised when the JWT ID (jti) claim is invalid. class InvalidJtiError < DecodeError; end # The InvalidPayload class is raised when the JWT payload is invalid. class InvalidPayload < DecodeError; end # The MissingRequiredClaim class is raised when a required claim is missing from the JWT. class MissingRequiredClaim < DecodeError; end # The Base64DecodeError class is raised when there is an error decoding a Base64-encoded string. class Base64DecodeError < DecodeError; end # The JWKError class is raised when there is an error with the JSON Web Key (JWK). class JWKError < DecodeError; end end ruby-jwt-3.1.2/lib/jwt/json.rb000066400000000000000000000003711503003570500161470ustar00rootroot00000000000000# frozen_string_literal: true require 'json' module JWT # @api private class JSON class << self def generate(data) ::JSON.generate(data) end def parse(data) ::JSON.parse(data) end end end end ruby-jwt-3.1.2/lib/jwt/jwa.rb000066400000000000000000000053061503003570500157620ustar00rootroot00000000000000# frozen_string_literal: true require 'openssl' require_relative 'jwa/signing_algorithm' require_relative 'jwa/ecdsa' require_relative 'jwa/hmac' require_relative 'jwa/none' require_relative 'jwa/ps' require_relative 'jwa/rsa' require_relative 'jwa/unsupported' module JWT # The JWA module contains all supported algorithms. module JWA # @api private class VerifierContext attr_reader :jwa def initialize(jwa:, keys:) @jwa = jwa @keys = Array(keys) end def verify(*args, **kwargs) @keys.any? do |key| @jwa.verify(*args, **kwargs, verification_key: key) end end end # @api private class SignerContext attr_reader :jwa def initialize(jwa:, key:) @jwa = jwa @key = key end def sign(*args, **kwargs) @jwa.sign(*args, **kwargs, signing_key: @key) end end class << self # @api private def resolve(algorithm) return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol) raise ArgumentError, 'Algorithm must be provided' if algorithm.nil? raise ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm' unless algorithm.is_a?(SigningAlgorithm) algorithm end # @api private def resolve_and_sort(algorithms:, preferred_algorithm:) Array(algorithms).map { |alg| JWA.resolve(alg) } .partition { |alg| alg.valid_alg?(preferred_algorithm) } .flatten end # @api private def create_signer(algorithm:, key:) if key.is_a?(JWK::KeyBase) validate_jwk_algorithms!(key, algorithm, DecodeError) return key end SignerContext.new(jwa: resolve(algorithm), key: key) end # @api private def create_verifiers(algorithms:, keys:, preferred_algorithm:) jwks, other_keys = keys.partition { |key| key.is_a?(JWK::KeyBase) } validate_jwk_algorithms!(jwks, algorithms, VerificationError) jwks + resolve_and_sort(algorithms: algorithms, preferred_algorithm: preferred_algorithm) .map { |jwa| VerifierContext.new(jwa: jwa, keys: other_keys) } end # @api private def validate_jwk_algorithms!(jwks, algorithms, error_class) algorithms = Array(algorithms) return if algorithms.empty? return if Array(jwks).all? do |jwk| algorithms.any? do |alg| jwk.jwa.valid_alg?(alg) end end raise error_class, "Provided JWKs do not support one of the specified algorithms: #{algorithms.join(', ')}" end end end end ruby-jwt-3.1.2/lib/jwt/jwa/000077500000000000000000000000001503003570500154315ustar00rootroot00000000000000ruby-jwt-3.1.2/lib/jwt/jwa/ecdsa.rb000066400000000000000000000076741503003570500170530ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWA # ECDSA signing algorithm class Ecdsa include JWT::JWA::SigningAlgorithm def initialize(alg, digest) @alg = alg @digest = digest end def sign(data:, signing_key:) raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::EC instance") unless signing_key.is_a?(::OpenSSL::PKey::EC) raise_sign_error!('The given key is not a private key') unless signing_key.private? curve_definition = curve_by_name(signing_key.group.curve_name) key_algorithm = curve_definition[:algorithm] raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm asn1_to_raw(signing_key.dsa_sign_asn1(OpenSSL::Digest.new(digest).digest(data)), signing_key) end def verify(data:, signature:, verification_key:) verification_key = self.class.create_public_key_from_point(verification_key) if verification_key.is_a?(::OpenSSL::PKey::EC::Point) raise_verify_error!("The given key is a #{verification_key.class}. It has to be an OpenSSL::PKey::EC instance") unless verification_key.is_a?(::OpenSSL::PKey::EC) curve_definition = curve_by_name(verification_key.group.curve_name) key_algorithm = curve_definition[:algorithm] raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided" if alg != key_algorithm verification_key.dsa_verify_asn1(OpenSSL::Digest.new(digest).digest(data), raw_to_asn1(signature, verification_key)) rescue OpenSSL::PKey::PKeyError raise JWT::VerificationError, 'Signature verification raised' end NAMED_CURVES = { 'prime256v1' => { algorithm: 'ES256', digest: 'sha256' }, 'secp256r1' => { # alias for prime256v1 algorithm: 'ES256', digest: 'sha256' }, 'secp384r1' => { algorithm: 'ES384', digest: 'sha384' }, 'secp521r1' => { algorithm: 'ES512', digest: 'sha512' }, 'secp256k1' => { algorithm: 'ES256K', digest: 'sha256' } }.freeze NAMED_CURVES.each_value do |v| register_algorithm(new(v[:algorithm], v[:digest])) end # @api private def self.curve_by_name(name) NAMED_CURVES.fetch(name) do raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported" end end if ::JWT.openssl_3? def self.create_public_key_from_point(point) sequence = OpenSSL::ASN1::Sequence([ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(point.group.curve_name)]), OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed)) ]) OpenSSL::PKey::EC.new(sequence.to_der) end else def self.create_public_key_from_point(point) OpenSSL::PKey::EC.new(point.group.curve_name).tap do |key| key.public_key = point end end end private attr_reader :digest def curve_by_name(name) self.class.curve_by_name(name) end def raw_to_asn1(signature, private_key) byte_size = (private_key.group.degree + 7) / 8 sig_bytes = signature[0..(byte_size - 1)] sig_char = signature[byte_size..-1] || '' OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der end def asn1_to_raw(signature, public_key) byte_size = (public_key.group.degree + 7) / 8 OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join end end end end ruby-jwt-3.1.2/lib/jwt/jwa/hmac.rb000066400000000000000000000053721503003570500166750ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWA # Implementation of the HMAC family of algorithms class Hmac include JWT::JWA::SigningAlgorithm def initialize(alg, digest) @alg = alg @digest = digest end def sign(data:, signing_key:) signing_key ||= '' raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String) OpenSSL::HMAC.digest(digest.new, signing_key, data) rescue OpenSSL::HMACError => e raise_verify_error!('OpenSSL 3.0 does not support nil or empty hmac_secret') if signing_key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure' raise e end def verify(data:, signature:, verification_key:) SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key)) end register_algorithm(new('HS256', OpenSSL::Digest::SHA256)) register_algorithm(new('HS384', OpenSSL::Digest::SHA384)) register_algorithm(new('HS512', OpenSSL::Digest::SHA512)) private attr_reader :digest # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb # rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate module SecurityUtils # Constant time string comparison, for fixed length strings. # # The values compared should be of fixed length, such as strings # that have already been processed by HMAC. Raises in case of length mismatch. if defined?(OpenSSL.fixed_length_secure_compare) def fixed_length_secure_compare(a, b) OpenSSL.fixed_length_secure_compare(a, b) end else # :nocov: def fixed_length_secure_compare(a, b) raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize l = a.unpack "C#{a.bytesize}" res = 0 b.each_byte { |byte| res |= byte ^ l.shift } res == 0 end # :nocov: end module_function :fixed_length_secure_compare # Secure string comparison for strings of variable length. # # While a timing attack would not be able to discern the content of # a secret compared via secure_compare, it is possible to determine # the secret length. This should be considered when using secure_compare # to compare weak, short secrets to user input. def secure_compare(a, b) a.bytesize == b.bytesize && fixed_length_secure_compare(a, b) end module_function :secure_compare end # rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate end end end ruby-jwt-3.1.2/lib/jwt/jwa/none.rb000066400000000000000000000005251503003570500167170ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWA # Implementation of the none algorithm class None include JWT::JWA::SigningAlgorithm def initialize @alg = 'none' end def sign(*) '' end def verify(*) true end register_algorithm(new) end end end ruby-jwt-3.1.2/lib/jwt/jwa/ps.rb000066400000000000000000000022461503003570500164040ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWA # Implementation of the RSASSA-PSS family of algorithms class Ps include JWT::JWA::SigningAlgorithm def initialize(alg) @alg = alg @digest_algorithm = alg.sub('PS', 'sha') end def sign(data:, signing_key:) raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance.") unless signing_key.is_a?(::OpenSSL::PKey::RSA) raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048 signing_key.sign_pss(digest_algorithm, data, salt_length: :digest, mgf1_hash: digest_algorithm) end def verify(data:, signature:, verification_key:) verification_key.verify_pss(digest_algorithm, signature, data, salt_length: :auto, mgf1_hash: digest_algorithm) rescue OpenSSL::PKey::PKeyError raise JWT::VerificationError, 'Signature verification raised' end register_algorithm(new('PS256')) register_algorithm(new('PS384')) register_algorithm(new('PS512')) private attr_reader :digest_algorithm end end end ruby-jwt-3.1.2/lib/jwt/jwa/rsa.rb000066400000000000000000000020631503003570500165440ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWA # Implementation of the RSA family of algorithms class Rsa include JWT::JWA::SigningAlgorithm def initialize(alg) @alg = alg @digest = alg.sub('RS', 'SHA') end def sign(data:, signing_key:) raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance") unless signing_key.is_a?(OpenSSL::PKey::RSA) raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048 signing_key.sign(OpenSSL::Digest.new(digest), data) end def verify(data:, signature:, verification_key:) verification_key.verify(OpenSSL::Digest.new(digest), signature, data) rescue OpenSSL::PKey::PKeyError raise JWT::VerificationError, 'Signature verification raised' end register_algorithm(new('RS256')) register_algorithm(new('RS384')) register_algorithm(new('RS512')) private attr_reader :digest end end end ruby-jwt-3.1.2/lib/jwt/jwa/signing_algorithm.rb000066400000000000000000000025431503003570500214660ustar00rootroot00000000000000# frozen_string_literal: true module JWT # JSON Web Algorithms module JWA # Base functionality for signing algorithms module SigningAlgorithm # Class methods for the SigningAlgorithm module module ClassMethods def register_algorithm(algo) ::JWT::JWA.register_algorithm(algo) end end def self.included(klass) klass.extend(ClassMethods) end attr_reader :alg def valid_alg?(alg_to_check) alg&.casecmp(alg_to_check)&.zero? == true end def header(*) { 'alg' => alg } end def sign(*) raise_sign_error!('Algorithm implementation is missing the sign method') end def verify(*) raise_verify_error!('Algorithm implementation is missing the verify method') end def raise_verify_error!(message) raise(DecodeError.new(message).tap { |e| e.set_backtrace(caller(1)) }) end def raise_sign_error!(message) raise(EncodeError.new(message).tap { |e| e.set_backtrace(caller(1)) }) end end class << self def register_algorithm(algo) algorithms[algo.alg.to_s.downcase] = algo end def find(algo) algorithms.fetch(algo.to_s.downcase, Unsupported) end private def algorithms @algorithms ||= {} end end end end ruby-jwt-3.1.2/lib/jwt/jwa/unsupported.rb000066400000000000000000000006231503003570500203470ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWA # Represents an unsupported algorithm module Unsupported class << self include JWT::JWA::SigningAlgorithm def sign(*) raise_sign_error!('Unsupported signing method') end def verify(*) raise JWT::VerificationError, 'Algorithm not supported' end end end end end ruby-jwt-3.1.2/lib/jwt/jwk.rb000066400000000000000000000024551503003570500157760ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'jwk/key_finder' require_relative 'jwk/set' module JWT # JSON Web Key (JWK) module JWK class << self def create_from(key, params = nil, options = {}) if key.is_a?(Hash) jwk_kty = key[:kty] || key['kty'] raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty return mappings.fetch(jwk_kty.to_s) do |kty| raise JWT::JWKError, "Key type #{kty} not supported" end.new(key, params, options) end mappings.fetch(key.class) do |klass| raise JWT::JWKError, "Cannot create JWK from a #{klass.name}" end.new(key, params, options) end def classes @mappings = nil # reset the cached mappings @classes ||= [] end alias new create_from alias import create_from private def mappings @mappings ||= generate_mappings end def generate_mappings classes.each_with_object({}) do |klass, hash| next unless klass.const_defined?('KTYS') Array(klass::KTYS).each do |kty| hash[kty] = klass end end end end end end require_relative 'jwk/key_base' require_relative 'jwk/ec' require_relative 'jwk/rsa' require_relative 'jwk/hmac' ruby-jwt-3.1.2/lib/jwt/jwk/000077500000000000000000000000001503003570500154435ustar00rootroot00000000000000ruby-jwt-3.1.2/lib/jwt/jwk/ec.rb000066400000000000000000000207751503003570500163720ustar00rootroot00000000000000# frozen_string_literal: true require 'forwardable' module JWT module JWK # JWK representation for Elliptic Curve (EC) keys class EC < KeyBase # rubocop:disable Metrics/ClassLength KTY = 'EC' KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze BINARY = 2 EC_PUBLIC_KEY_ELEMENTS = %i[kty crv x y].freeze EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze ZERO_BYTE = "\0".b.freeze def initialize(key, params = nil, options = {}) params ||= {} # For backwards compatibility when kid was a String params = { kid: params } if params.is_a?(String) key_params = extract_key_params(key) params = params.transform_keys(&:to_sym) check_jwk_params!(key_params, params) super(options, key_params.merge(params)) end def keypair ec_key end def private? ec_key.private_key? end def signing_key ec_key end def verify_key ec_key end def public_key ec_key end def members EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } end def export(options = {}) exported = parameters.clone exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true exported end def key_digest _crv, x_octets, y_octets = keypair_components(ec_key) sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)), OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))]) OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end def []=(key, value) raise ArgumentError, 'cannot overwrite cryptographic key attributes' if EC_KEY_ELEMENTS.include?(key.to_sym) super end def jwa return super if self[:alg] curve_name = self.class.to_openssl_curve(self[:crv]) JWA.resolve(JWA::Ecdsa.curve_by_name(curve_name)[:algorithm]) end private def ec_key @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d]) end def extract_key_params(key) case key when JWT::JWK::EC key.export(include_private: true) when OpenSSL::PKey::EC # Accept OpenSSL key as input @ec_key = key # Preserve the object to avoid recreation parse_ec_key(key) when Hash key.transform_keys(&:to_sym) else raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters' end end def check_jwk_params!(key_params, params) raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty? raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y] end def keypair_components(ec_keypair) encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY) case ec_keypair.group.curve_name when 'prime256v1' crv = 'P-256' x_octets, y_octets = encoded_point.unpack('xa32a32') when 'secp256k1' crv = 'P-256K' x_octets, y_octets = encoded_point.unpack('xa32a32') when 'secp384r1' crv = 'P-384' x_octets, y_octets = encoded_point.unpack('xa48a48') when 'secp521r1' crv = 'P-521' x_octets, y_octets = encoded_point.unpack('xa66a66') else raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'" end [crv, x_octets, y_octets] end def encode_octets(octets) return unless octets ::JWT::Base64.url_encode(octets) end def parse_ec_key(key) crv, x_octets, y_octets = keypair_components(key) octets = key.private_key&.to_bn&.to_s(BINARY) { kty: KTY, crv: crv, x: encode_octets(x_octets), y: encode_octets(y_octets), d: encode_octets(octets) }.compact end def create_point(jwk_crv, jwk_x, jwk_y) curve = EC.to_openssl_curve(jwk_crv) x_octets = decode_octets(jwk_x) y_octets = decode_octets(jwk_y) # The details of the `Point` instantiation are covered in: # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html # - https://tools.ietf.org/html/rfc5480#section-2.2 # - https://www.secg.org/SEC1-Ver-1.0.pdf # Section 2.3.3 of the last of these references specifies that the # encoding of an uncompressed point consists of the byte `0x04` followed # by the x value then the y value. OpenSSL::PKey::EC::Point.new( OpenSSL::PKey::EC::Group.new(curve), OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2) ) end if ::JWT.openssl_3? def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) point = create_point(jwk_crv, jwk_x, jwk_y) return ::JWT::JWA::Ecdsa.create_public_key_from_point(point) unless jwk_d # https://datatracker.ietf.org/doc/html/rfc5915.html # ECPrivateKey ::= SEQUENCE { # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), # privateKey OCTET STRING, # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, # publicKey [1] BIT STRING OPTIONAL # } sequence = OpenSSL::ASN1::Sequence([ OpenSSL::ASN1::Integer(1), OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)), OpenSSL::ASN1::ObjectId(point.group.curve_name, 0, :EXPLICIT), OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT) ]) OpenSSL::PKey::EC.new(sequence.to_der) end else def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) point = create_point(jwk_crv, jwk_x, jwk_y) ::JWT::JWA::Ecdsa.create_public_key_from_point(point).tap do |key| key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d end end end def decode_octets(base64_encoded_coordinate) bytes = ::JWT::Base64.url_decode(base64_encoded_coordinate) # Some base64 encoders on some platform omit a single 0-byte at # the start of either Y or X coordinate of the elliptic curve point. # This leads to an encoding error when data is passed to OpenSSL BN. # It is know to have happened to exported JWKs on a Java application and # on a Flutter/Dart application (both iOS and Android). All that is # needed to fix the problem is adding a leading 0-byte. We know the # required byte is 0 because with any other byte the point is no longer # on the curve - and OpenSSL will actually communicate this via another # exception. The indication of a stripped byte will be the fact that the # coordinates - once decoded into bytes - should always be an even # bytesize. For example, with a P-521 curve, both x and y must be 66 bytes. # With a P-256 curve, both x and y must be 32 and so on. The simplest way # to check for this truncation is thus to check whether the number of bytes # is odd, and restore the leading 0-byte if it is. if bytes.bytesize.odd? ZERO_BYTE + bytes else bytes end end class << self def import(jwk_data) new(jwk_data) end def to_openssl_curve(crv) # The JWK specs and OpenSSL use different names for the same curves. # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some # pointers on different names for common curves. case crv when 'P-256' then 'prime256v1' when 'P-384' then 'secp384r1' when 'P-521' then 'secp521r1' when 'P-256K' then 'secp256k1' else raise JWT::JWKError, 'Invalid curve provided' end end end end end end ruby-jwt-3.1.2/lib/jwt/jwk/hmac.rb000066400000000000000000000053321503003570500167030ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWK # JWK for HMAC keys class HMAC < KeyBase KTY = 'oct' KTYS = [KTY, String, JWT::JWK::HMAC].freeze HMAC_PUBLIC_KEY_ELEMENTS = %i[kty].freeze HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze def initialize(key, params = nil, options = {}) params ||= {} # For backwards compatibility when kid was a String params = { kid: params } if params.is_a?(String) key_params = extract_key_params(key) params = params.transform_keys(&:to_sym) check_jwk(key_params, params) super(options, key_params.merge(params)) end def keypair secret end def private? true end def public_key nil end def verify_key secret end def signing_key secret end # See https://tools.ietf.org/html/rfc7517#appendix-A.3 def export(options = {}) exported = parameters.clone exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true exported end def members HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } end def key_digest sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key), OpenSSL::ASN1::UTF8String.new(KTY)]) OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end def []=(key, value) raise ArgumentError, 'cannot overwrite cryptographic key attributes' if HMAC_KEY_ELEMENTS.include?(key.to_sym) super end private def secret @secret ||= ::JWT::Base64.url_decode(self[:k]) end def extract_key_params(key) case key when JWT::JWK::HMAC key.export(include_private: true) when String # Accept String key as input { kty: KTY, k: ::JWT::Base64.url_encode(key) } when Hash key.transform_keys(&:to_sym) else raise ArgumentError, 'key must be of type String or Hash with key parameters' end end def check_jwk(keypair, params) raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty? raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k] end class << self def import(jwk_data) new(jwk_data) end end end end end ruby-jwt-3.1.2/lib/jwt/jwk/key_base.rb000066400000000000000000000032461503003570500175570ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWK # Base for JWK implementations class KeyBase def self.inherited(klass) super ::JWT::JWK.classes << klass end def initialize(options, params = {}) options ||= {} @parameters = params.transform_keys(&:to_sym) # Uniform interface # For backwards compatibility, kid_generator may be specified in the parameters options[:kid_generator] ||= @parameters.delete(:kid_generator) # Make sure the key has a kid kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator self[:kid] ||= kid_generator.new(self).generate end def kid self[:kid] end def hash self[:kid].hash end def [](key) @parameters[key.to_sym] end def []=(key, value) @parameters[key.to_sym] = value end def ==(other) other.is_a?(::JWT::JWK::KeyBase) && self[:kid] == other[:kid] end def verify(**kwargs) jwa.verify(**kwargs, verification_key: verify_key) end def sign(**kwargs) jwa.sign(**kwargs, signing_key: signing_key) end alias eql? == def <=>(other) return nil unless other.is_a?(::JWT::JWK::KeyBase) self[:kid] <=> other[:kid] end def jwa raise JWT::JWKError, 'Could not resolve the JWA, the "alg" parameter is missing' unless self[:alg] JWA.resolve(self[:alg]).tap do |jwa| raise JWT::JWKError, 'none algorithm usage not supported via JWK' if jwa.is_a?(JWA::None) end end attr_reader :parameters end end end ruby-jwt-3.1.2/lib/jwt/jwk/key_finder.rb000066400000000000000000000050571503003570500201160ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWK # JSON Web Key keyfinder # To find the key for a given kid class KeyFinder # Initializes a new KeyFinder instance. # @param [Hash] options the options to create a KeyFinder with # @option options [Proc, JWT::JWK::Set] :jwks the jwks or a loader proc # @option options [Boolean] :allow_nil_kid whether to allow nil kid # @option options [Array] :key_fields the fields to use for key matching, # the order of the fields are used to determine # the priority of the keys. def initialize(options) @allow_nil_kid = options[:allow_nil_kid] jwks_or_loader = options[:jwks] @jwks_loader = if jwks_or_loader.respond_to?(:call) jwks_or_loader else ->(_options) { jwks_or_loader } end @key_fields = options[:key_fields] || %i[kid] end # Returns the verification key for the given kid # @param [String] kid the key id def key_for(kid, key_field = :kid) raise ::JWT::DecodeError, "Invalid type for #{key_field} header parameter" unless kid.nil? || kid.is_a?(String) jwk = resolve_key(kid, key_field) raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any? raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk jwk.verify_key end # Returns the key for the given token # @param [JWT::EncodedToken] token the token def call(token) @key_fields.each do |key_field| field_value = token.header[key_field.to_s] return key_for(field_value, key_field) if field_value end raise ::JWT::DecodeError, 'No key id (kid) or x5t found from token headers' unless @allow_nil_kid kid = token.header['kid'] key_for(kid) end private def resolve_key(kid, key_field) key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[key_field] == kid } # First try without invalidation to facilitate application caching @jwks ||= JWT::JWK::Set.new(@jwks_loader.call(key_field => kid)) jwk = @jwks.find { |key| key_matcher.call(key) } return jwk if jwk # Second try, invalidate for backwards compatibility @jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, key_field => kid)) @jwks.find { |key| key_matcher.call(key) } end end end end ruby-jwt-3.1.2/lib/jwt/jwk/kid_as_key_digest.rb000066400000000000000000000003421503003570500214300ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWK # @api private class KidAsKeyDigest def initialize(jwk) @jwk = jwk end def generate @jwk.key_digest end end end end ruby-jwt-3.1.2/lib/jwt/jwk/rsa.rb000066400000000000000000000155651503003570500165710ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWK # JSON Web Key (JWK) representation of a RSA key class RSA < KeyBase # rubocop:disable Metrics/ClassLength BINARY = 2 KTY = 'RSA' KTYS = [KTY, OpenSSL::PKey::RSA, JWT::JWK::RSA].freeze RSA_PUBLIC_KEY_ELEMENTS = %i[kty n e].freeze RSA_PRIVATE_KEY_ELEMENTS = %i[d p q dp dq qi].freeze RSA_KEY_ELEMENTS = (RSA_PRIVATE_KEY_ELEMENTS + RSA_PUBLIC_KEY_ELEMENTS).freeze RSA_OPT_PARAMS = %i[p q dp dq qi].freeze RSA_ASN1_SEQUENCE = (%i[n e d] + RSA_OPT_PARAMS).freeze # https://www.rfc-editor.org/rfc/rfc3447#appendix-A.1.2 def initialize(key, params = nil, options = {}) params ||= {} # For backwards compatibility when kid was a String params = { kid: params } if params.is_a?(String) key_params = extract_key_params(key) params = params.transform_keys(&:to_sym) check_jwk_params!(key_params, params) super(options, key_params.merge(params)) end def keypair rsa_key end def private? rsa_key.private? end def public_key rsa_key.public_key end def signing_key rsa_key if private? end def verify_key rsa_key.public_key end def export(options = {}) exported = parameters.clone exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true exported end def members RSA_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } end def key_digest sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n), OpenSSL::ASN1::Integer.new(public_key.e)]) OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end def []=(key, value) raise ArgumentError, 'cannot overwrite cryptographic key attributes' if RSA_KEY_ELEMENTS.include?(key.to_sym) super end private def rsa_key @rsa_key ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty]))) end def extract_key_params(key) case key when JWT::JWK::RSA key.export(include_private: true) when OpenSSL::PKey::RSA # Accept OpenSSL key as input @rsa_key = key # Preserve the object to avoid recreation parse_rsa_key(key) when Hash key.transform_keys(&:to_sym) else raise ArgumentError, 'key must be of type OpenSSL::PKey::RSA or Hash with key parameters' end end def check_jwk_params!(key_params, params) raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (RSA_KEY_ELEMENTS & params.keys).empty? raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY raise JWT::JWKError, 'Key format is invalid for RSA' unless key_params[:n] && key_params[:e] end def parse_rsa_key(key) { kty: KTY, n: encode_open_ssl_bn(key.n), e: encode_open_ssl_bn(key.e), d: encode_open_ssl_bn(key.d), p: encode_open_ssl_bn(key.p), q: encode_open_ssl_bn(key.q), dp: encode_open_ssl_bn(key.dmp1), dq: encode_open_ssl_bn(key.dmq1), qi: encode_open_ssl_bn(key.iqmp) }.compact end def jwk_attributes(*attributes) attributes.each_with_object({}) do |attribute, hash| hash[attribute] = decode_open_ssl_bn(self[attribute]) end end def encode_open_ssl_bn(key_part) return unless key_part ::JWT::Base64.url_encode(key_part.to_s(BINARY)) end def decode_open_ssl_bn(jwk_data) self.class.decode_open_ssl_bn(jwk_data) end class << self def import(jwk_data) new(jwk_data) end def decode_open_ssl_bn(jwk_data) return nil unless jwk_data OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY) end def create_rsa_key_using_der(rsa_parameters) validate_rsa_parameters!(rsa_parameters) sequence = RSA_ASN1_SEQUENCE.each_with_object([]) do |key, arr| next if rsa_parameters[key].nil? arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key]) end if sequence.size > 2 # Append "two-prime" version for private key sequence.unshift(OpenSSL::ASN1::Integer.new(0)) raise JWT::JWKError, 'Creating a RSA key with a private key requires the CRT parameters to be defined' if sequence.size < RSA_ASN1_SEQUENCE.size end OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der) end def create_rsa_key_using_sets(rsa_parameters) validate_rsa_parameters!(rsa_parameters) OpenSSL::PKey::RSA.new.tap do |rsa_key| rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d]) rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q] rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi] end end # :nocov: # Before openssl 2.0, we need to use the accessors to set the key def create_rsa_key_using_accessors(rsa_parameters) # rubocop:disable Metrics/AbcSize validate_rsa_parameters!(rsa_parameters) OpenSSL::PKey::RSA.new.tap do |rsa_key| rsa_key.n = rsa_parameters[:n] rsa_key.e = rsa_parameters[:e] rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d] rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p] rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q] rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp] rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq] rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi] end end # :nocov: def validate_rsa_parameters!(rsa_parameters) return unless rsa_parameters.key?(:d) parameters = RSA_OPT_PARAMS - rsa_parameters.keys return if parameters.empty? || parameters.size == RSA_OPT_PARAMS.size raise JWT::JWKError, 'When one of p, q, dp, dq or qi is given all the other optimization parameters also needs to be defined' # https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2 end if ::JWT.openssl_3? alias create_rsa_key create_rsa_key_using_der elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key) alias create_rsa_key create_rsa_key_using_sets else alias create_rsa_key create_rsa_key_using_accessors end end end end end ruby-jwt-3.1.2/lib/jwt/jwk/set.rb000066400000000000000000000036011503003570500165630ustar00rootroot00000000000000# frozen_string_literal: true require 'forwardable' module JWT module JWK # JSON Web Key Set (JWKS) representation # https://tools.ietf.org/html/rfc7517 class Set include Enumerable extend Forwardable attr_reader :keys def initialize(jwks = nil, options = {}) # rubocop:disable Metrics/CyclomaticComplexity jwks ||= {} @keys = case jwks when JWT::JWK::Set # Simple duplication jwks.keys when JWT::JWK::KeyBase # Singleton [jwks] when Hash jwks = jwks.transform_keys(&:to_sym) [*jwks[:keys]].map { |k| JWT::JWK.new(k, nil, options) } when Array jwks.map { |k| JWT::JWK.new(k, nil, options) } else raise ArgumentError, 'Can only create new JWKS from Hash, Array and JWK' end end def export(options = {}) { keys: @keys.map { |k| k.export(options) } } end def_delegators :@keys, :each, :size, :delete, :dig def select!(&block) return @keys.select! unless block self if @keys.select!(&block) end def reject!(&block) return @keys.reject! unless block self if @keys.reject!(&block) end def uniq!(&block) self if @keys.uniq!(&block) end def merge(enum) @keys += JWT::JWK::Set.new(enum.to_a).keys self end def union(enum) dup.merge(enum) end def add(key) @keys << JWT::JWK.new(key) self end def ==(other) other.is_a?(JWT::JWK::Set) && keys.sort == other.keys.sort end alias eql? == alias filter! select! alias length size # For symbolic manipulation alias | union alias + union alias << add end end end ruby-jwt-3.1.2/lib/jwt/jwk/thumbprint.rb000066400000000000000000000007141503003570500201660ustar00rootroot00000000000000# frozen_string_literal: true module JWT module JWK # https://tools.ietf.org/html/rfc7638 class Thumbprint attr_reader :jwk def initialize(jwk) @jwk = jwk end def generate ::Base64.urlsafe_encode64( Digest::SHA256.digest( JWT::JSON.generate( jwk.members.sort.to_h ) ), padding: false ) end alias to_s generate end end end ruby-jwt-3.1.2/lib/jwt/token.rb000066400000000000000000000102501503003570500163130ustar00rootroot00000000000000# frozen_string_literal: true module JWT # Represents a JWT token # # Basic token signed using the HS256 algorithm: # # token = JWT::Token.new(payload: {pay: 'load'}) # token.sign!(algorithm: 'HS256', key: 'secret') # token.jwt # => eyJhb.... # # Custom headers will be combined with generated headers: # token = JWT::Token.new(payload: {pay: 'load'}, header: {custom: "value"}) # token.sign!(algorithm: 'HS256', key: 'secret') # token.header # => {"custom"=>"value", "alg"=>"HS256"} # class Token # Initializes a new Token instance. # # @param header [Hash] the header of the JWT token. # @param payload [Hash] the payload of the JWT token. def initialize(payload:, header: {}) @header = header&.transform_keys(&:to_s) @payload = payload end # Returns the decoded signature of the JWT token. # # @return [String] the decoded signature of the JWT token. def signature @signature ||= ::JWT::Base64.url_decode(encoded_signature || '') end # Returns the encoded signature of the JWT token. # # @return [String] the encoded signature of the JWT token. def encoded_signature @encoded_signature ||= ::JWT::Base64.url_encode(signature) end # Returns the decoded header of the JWT token. # # @return [Hash] the header of the JWT token. attr_reader :header # Returns the encoded header of the JWT token. # # @return [String] the encoded header of the JWT token. def encoded_header @encoded_header ||= ::JWT::Base64.url_encode(JWT::JSON.generate(header)) end # Returns the payload of the JWT token. # # @return [Hash] the payload of the JWT token. attr_reader :payload # Returns the encoded payload of the JWT token. # # @return [String] the encoded payload of the JWT token. def encoded_payload @encoded_payload ||= ::JWT::Base64.url_encode(JWT::JSON.generate(payload)) end # Returns the signing input of the JWT token. # # @return [String] the signing input of the JWT token. def signing_input @signing_input ||= [encoded_header, encoded_payload].join('.') end # Returns the JWT token as a string. # # @return [String] the JWT token as a string. # @raise [JWT::EncodeError] if the token is not signed or other encoding issues def jwt @jwt ||= (@signature && [encoded_header, @detached_payload ? '' : encoded_payload, encoded_signature].join('.')) || raise(::JWT::EncodeError, 'Token is not signed') end # Detaches the payload according to https://datatracker.ietf.org/doc/html/rfc7515#appendix-F # def detach_payload! @detached_payload = true nil end # Signs the JWT token. # # @param key [String, JWT::JWK::KeyBase] the key to use for signing. # @param algorithm [String, Object] the algorithm to use for signing. # @return [void] # @raise [JWT::EncodeError] if the token is already signed or other problems when signing def sign!(key:, algorithm:) raise ::JWT::EncodeError, 'Token already signed' if @signature JWA.create_signer(algorithm: algorithm, key: key).tap do |signer| header.merge!(signer.jwa.header) { |_key, old, _new| old } @signature = signer.sign(data: signing_input) end nil end # Verifies the claims of the token. # @param options [Array, Hash] the claims to verify. # @raise [JWT::DecodeError] if the claims are invalid. def verify_claims!(*options) Claims::Verifier.verify!(self, *options) end # Returns the errors of the claims of the token. # @param options [Array, Hash] the claims to verify. # @return [Array] the errors of the claims. def claim_errors(*options) Claims::Verifier.errors(self, *options) end # Returns whether the claims of the token are valid. # @param options [Array, Hash] the claims to verify. # @return [Boolean] whether the claims are valid. def valid_claims?(*options) claim_errors(*options).empty? end # Returns the JWT token as a string. # # @return [String] the JWT token as a string. alias to_s jwt end end ruby-jwt-3.1.2/lib/jwt/version.rb000066400000000000000000000024201503003570500166600ustar00rootroot00000000000000# frozen_string_literal: true # JSON Web Token implementation # # Should be up to date with the latest spec: # https://tools.ietf.org/html/rfc7519 module JWT # Returns the gem version of the JWT library. # # @return [Gem::Version] the gem version. def self.gem_version Gem::Version.new(VERSION::STRING) end # Version constants module VERSION MAJOR = 3 MINOR = 1 TINY = 2 PRE = nil STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end # Checks if the OpenSSL version is 3 or greater. # # @return [Boolean] true if OpenSSL version is 3 or greater, false otherwise. # @api private def self.openssl_3? return false if OpenSSL::OPENSSL_VERSION.include?('LibreSSL') true if 3 * 0x10000000 <= OpenSSL::OPENSSL_VERSION_NUMBER end # Checks if there is an OpenSSL 3 HMAC empty key regression. # # @return [Boolean] true if there is an OpenSSL 3 HMAC empty key regression, false otherwise. # @api private def self.openssl_3_hmac_empty_key_regression? openssl_3? && openssl_version <= ::Gem::Version.new('3.0.0') end # Returns the OpenSSL version. # # @return [Gem::Version] the OpenSSL version. # @api private def self.openssl_version @openssl_version ||= ::Gem::Version.new(OpenSSL::VERSION) end end ruby-jwt-3.1.2/lib/jwt/x5c_key_finder.rb000066400000000000000000000034761503003570500201050ustar00rootroot00000000000000# frozen_string_literal: true module JWT # If the x5c header certificate chain can be validated by trusted root # certificates, and none of the certificates are revoked, returns the public # key from the first certificate. # See https://tools.ietf.org/html/rfc7515#section-4.1.6 class X5cKeyFinder def initialize(root_certificates, crls = nil) raise ArgumentError, 'Root certificates must be specified' unless root_certificates @store = build_store(root_certificates, crls) end def from(x5c_header_or_certificates) signing_certificate, *certificate_chain = parse_certificates(x5c_header_or_certificates) store_context = OpenSSL::X509::StoreContext.new(@store, signing_certificate, certificate_chain) if store_context.verify signing_certificate.public_key else error = "Certificate verification failed: #{store_context.error_string}." if (current_cert = store_context.current_cert) error = "#{error} Certificate subject: #{current_cert.subject}." end raise JWT::VerificationError, error end end private def build_store(root_certificates, crls) store = OpenSSL::X509::Store.new store.purpose = OpenSSL::X509::PURPOSE_ANY store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL root_certificates.each { |certificate| store.add_cert(certificate) } crls&.each { |crl| store.add_crl(crl) } store end def parse_certificates(x5c_header_or_certificates) if x5c_header_or_certificates.all? { |obj| obj.is_a?(OpenSSL::X509::Certificate) } x5c_header_or_certificates else x5c_header_or_certificates.map do |encoded| OpenSSL::X509::Certificate.new(::JWT::Base64.url_decode(encoded)) end end end end end ruby-jwt-3.1.2/ruby-jwt.gemspec000066400000000000000000000027551503003570500164370ustar00rootroot00000000000000# frozen_string_literal: true lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'jwt/version' Gem::Specification.new do |spec| spec.name = 'jwt' spec.version = JWT.gem_version spec.authors = [ 'Tim Rudat' ] spec.email = 'timrudat@gmail.com' spec.summary = 'JSON Web Token implementation in Ruby' spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.' spec.homepage = 'https://github.com/jwt/ruby-jwt' spec.license = 'MIT' spec.required_ruby_version = '>= 2.5' spec.metadata = { 'bug_tracker_uri' => 'https://github.com/jwt/ruby-jwt/issues', 'changelog_uri' => "https://github.com/jwt/ruby-jwt/blob/v#{JWT.gem_version}/CHANGELOG.md", 'rubygems_mfa_required' => 'true' } spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(spec|gemfiles|coverage|bin)/}) || # Irrelevant folders f.match(/^\.+/) || # Files and folders starting with . f.match(/^(Appraisals|Gemfile|Rakefile)$/) # Irrelevant files end spec.executables = [] spec.require_paths = %w[lib] spec.add_dependency 'base64' spec.add_development_dependency 'appraisal' spec.add_development_dependency 'bundler' spec.add_development_dependency 'irb' spec.add_development_dependency 'logger' spec.add_development_dependency 'rake' spec.add_development_dependency 'rspec' spec.add_development_dependency 'rubocop' spec.add_development_dependency 'simplecov' end ruby-jwt-3.1.2/spec/000077500000000000000000000000001503003570500142305ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/fixtures/000077500000000000000000000000001503003570500161015ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/fixtures/keys/000077500000000000000000000000001503003570500170545ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/fixtures/keys/ec256-private-v2.pem000066400000000000000000000003431503003570500224000ustar00rootroot00000000000000-----BEGIN EC PRIVATE KEY----- MHcCAQEEIFZpgytOAXPVreqGsHPdD9pojw30bnlqfUAqFZ3V3/qeoAoGCCqGSM49 AwEHoUQDQgAE7JbAf3pWEEPje6NG+4dGOwIZnNwRFIe7DnQ4xFWKPrL5tVWlBh7N DFhjGNhiyO+aQjbcx9uWV74ifq7i21Bemg== -----END EC PRIVATE KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec256-private.pem000066400000000000000000000003431503003570500220530ustar00rootroot00000000000000-----BEGIN EC PRIVATE KEY----- MHcCAQEEIJmVse5uPfj6B4TcXrUAvf9/8pJh+KrKKYLNcmOnp/vPoAoGCCqGSM49 AwEHoUQDQgAEAr+WbDE5VtIDGhtYMxvEc6cMsDBc/DX1wuhIMu8dQzOLSt0tpqK9 MVfXbVfrKdayVFgoWzs8MilcYq0QIhKx/w== -----END EC PRIVATE KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec256-public-v2.pem000066400000000000000000000002621503003570500222040ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7JbAf3pWEEPje6NG+4dGOwIZnNwR FIe7DnQ4xFWKPrL5tVWlBh7NDFhjGNhiyO+aQjbcx9uWV74ifq7i21Bemg== -----END PUBLIC KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec256-public.pem000066400000000000000000000002621503003570500216570ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAr+WbDE5VtIDGhtYMxvEc6cMsDBc /DX1wuhIMu8dQzOLSt0tpqK9MVfXbVfrKdayVFgoWzs8MilcYq0QIhKx/w== -----END PUBLIC KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec256-wrong-public.pem000066400000000000000000000002561503003570500230140ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEPmuXZT3jpJnEMVPOW6RMsmxeGLOCE1PN 6fwvUwOsxv7YnyoQ5/bpo64n+Jp4slSl1aUNoCBF2oz9bS0iyBo3jg== -----END PUBLIC KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec256k-private.pem000066400000000000000000000003371503003570500222310ustar00rootroot00000000000000-----BEGIN EC PRIVATE KEY----- MHQCAQEEIMTine3s8tT+8bswDM4/z8o+wIYGb9PQPrw8x6Nu6QDdoAcGBSuBBAAK oUQDQgAEy8wuv6+fXodLPLfhxm132y1R8m4dkng7tHe7N+sULV2Eth6AxEXQfd+E 4nuceR21UNCvQKqxiYwCzVwIKcHe/A== -----END EC PRIVATE KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec256k-public.pem000066400000000000000000000002561503003570500220350ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEy8wuv6+fXodLPLfhxm132y1R8m4dkng7 tHe7N+sULV2Eth6AxEXQfd+E4nuceR21UNCvQKqxiYwCzVwIKcHe/A== -----END PUBLIC KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec384-private.pem000066400000000000000000000004401503003570500220530ustar00rootroot00000000000000-----BEGIN EC PRIVATE KEY----- MIGkAgEBBDDxOljqUKw9YNhkluSJIBAYO1YXcNtS+vckd5hpTZ5toxsOlwbmyrnU Tn+D5Xma1m2gBwYFK4EEACKhZANiAASQwYTiRvXu1hMHceSosMs/8uf50sJI3jvK kdSkvuRAPxSzhtrUvCQDnVsThFq4aOdZZY1qh2ErJGtzmrx+pEsJvJnvfOTG3NGU KRalek+LQfVqAUSvDMKlxdkz2e67tso= -----END EC PRIVATE KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec384-public.pem000066400000000000000000000003271503003570500216630ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEkMGE4kb17tYTB3HkqLDLP/Ln+dLCSN47 ypHUpL7kQD8Us4ba1LwkA51bE4RauGjnWWWNaodhKyRrc5q8fqRLCbyZ73zkxtzR lCkWpXpPi0H1agFErwzCpcXZM9nuu7bK -----END PUBLIC KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec512-private.pem000066400000000000000000000005551503003570500220530ustar00rootroot00000000000000-----BEGIN EC PRIVATE KEY----- MIHcAgEBBEIB0/+ffxEj7j62xvGaB5pvzk888e412ESO/EK/K0QlS9dSF8+Rj1rG zqpRB8fvDnoe8xdmkW/W5GKzojMyv7YQYumgBwYFK4EEACOhgYkDgYYABAEw74Yw aTbPY6TtWmxx6LJDzCX2nKWCPnKdZcEH9Ncu8g5RjRBRq2yacja3OoS6nA2YeDng reBJxZr376P6Ns6XcQFWDA6K/MCTrEBCsPxXZNxd8KR9vMGWhgNtWRrcKzwJfQkr suyehZkbbYyFnAWyARKHZuV7VUXmeEmRS/f93MPqVA== -----END EC PRIVATE KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/ec512-public.pem000066400000000000000000000004141503003570500216510ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBMO+GMGk2z2Ok7VpsceiyQ8wl9pyl gj5ynWXBB/TXLvIOUY0QUatsmnI2tzqEupwNmHg54K3gScWa9++j+jbOl3EBVgwO ivzAk6xAQrD8V2TcXfCkfbzBloYDbVka3Cs8CX0JK7LsnoWZG22MhZwFsgESh2bl e1VF5nhJkUv3/dzD6lQ= -----END PUBLIC KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/rsa-2048-private.pem000066400000000000000000000032171503003570500224120ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA4GzZTLU48c4WbyvHi+QKrB71x+T0eq5hqDbQqnlYjhD1Ika7 io1iplsdJWJuyxfYbUkb2Ol0fj4koZ/GS6lgCZr4+8UHbr1qf0Eu5HZSpszs2YxY 8U5RHnrpw67co7hlgAR9HbyNf5XIYgLV9ldHH/eazwnc3F/hgNsV0xjScVilejgo cJ4zcsyymvW8t42lteM7bI867ZuJhGop/V+Y0HFyrMsPoQyLuCUpr6ulOfrkr7ZO dhAIG8r1HcjOp/AUjM15vfXcbUZjkM/VloifX1YitU3upMGJ8/DpFGffMOImrn5r 6BT494V8rRyN2qvQoAkLJpqZ0avLxwiR2lgVQQIDAQABAoIBAEH0Ozgr2fxWEInD V/VooypKPvjr9F1JejGxSkmPN9MocKIOH3dsbZ1uEXa3ItBUxan4XlK06SNgp+tH xULfF/Y6sQlsse59hBq50Uoa69dRShn1AP6JgZVvkduMPBNxUYL5zrs6emsQXb9Q DglDRQfEAJ7vyxSIqQDxYcyT8uSUF70dqFe+E9B2VE3D6ccHc98k41pJrAFAUFH1 wwvDhfyYr7/Ultut9wzpZvU1meF3Vna3GOUHfxrG6wu1G+WIWHGjouzThsc1qiVI BtMCJxuCt5fOXRbU4STbMqhB6sZHiOh6J/dZU6JwRYt+IS8FB6kCNFSEWZWQledJ XqtYSQECgYEA9nmnFTRj3fTBq9zMXfCRujkSy6X2bOb39ftNXzHFuc+I6xmv/3Bs P9tDdjueP/SnCb7i/9hXkpEIcxjrjiqgcvD2ym1hE4q+odMzRAXYMdnmzI34SVZE U5hYJcYsXNKrTTleba7QgqdORmyJ9FwqLO40udvmrZMY223XDwgRkOkCgYEA6RkO 5wjjrWWp/G1YN3KXZTS1m2/eGrUThohXKAfAjbWWiouNLW2msXrxEWsPRL6xKiHu X9cwZwzi3MstAgk+bphUGUVUkGKNDjWHJA25tDYjbPtkd6xbL4eCHsKpNL3HNYr9 N0CIvgn7qjaHRBem0iK7T6keY4axaSVddEwYapkCgYEA13K5qaB1F4Smcpt8DTWH vPe8xUUaZlFzOJLmLCsuwmB2N8Ppg2j7RspcaxJsH021YaB5ftjWm+ipMSr8ZPY/ 8JlPsNzxuYpTXtNmAbT2KYVm6THEch61dTk6/DIBf1YrpUJbl5by7vJeStL/uBmE SGgksL5XIyzs0opuLdaIvFkCgYAyBLWE8AxjFfCvAQuwAj/ocLITo6KmWnrRIIqL RXaVMgUWv7FQsTnW1cnK8g05tC2yG8vZ9wQk6Mf5lwOWb0NdWgSZ0528ydj41pWk L+nMeN2LMjqxz2NVxJ8wWJcUgTCxFZ0WcRumo9/D+6V1ABpE9zz4cBLcSnfhVypB nV6T6QKBgQCSZNCQ9HPxjAgYcsqc5sjNwuN1GHQZSav3Tye3k6zHENe1lsteT9K8 xciGIuhybKZBvB4yImIIHCtnH+AS+mHAGqHarjNDMfvjOq0dMibPx4+bkIiHdBIH Xz+j5kmntvFiUnzr0Z/Tcqo+r8FvyCo1YWgwqGP8XoFrswD7gy7cZw== -----END RSA PRIVATE KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/rsa-2048-public.pem000066400000000000000000000007031503003570500222130ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4GzZTLU48c4WbyvHi+QK rB71x+T0eq5hqDbQqnlYjhD1Ika7io1iplsdJWJuyxfYbUkb2Ol0fj4koZ/GS6lg CZr4+8UHbr1qf0Eu5HZSpszs2YxY8U5RHnrpw67co7hlgAR9HbyNf5XIYgLV9ldH H/eazwnc3F/hgNsV0xjScVilejgocJ4zcsyymvW8t42lteM7bI867ZuJhGop/V+Y 0HFyrMsPoQyLuCUpr6ulOfrkr7ZOdhAIG8r1HcjOp/AUjM15vfXcbUZjkM/Vloif X1YitU3upMGJ8/DpFGffMOImrn5r6BT494V8rRyN2qvQoAkLJpqZ0avLxwiR2lgV QQIDAQAB -----END PUBLIC KEY----- ruby-jwt-3.1.2/spec/fixtures/keys/rsa-2048-wrong-public.pem000066400000000000000000000007031503003570500233450ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzHAVGaW9j4l3/b4ngcjj oIoIcnsQEWOMqErb5VhLZMGIq1gEO5qxPDAwooKsNotzcAOB3ZyLn7p5D+dmOrNU YkYWgYITNGeSifrnVqQugd5Fh1L8K7zOGltUo2UtjbN4uJ56tzxBMZp2wejs2/Qu 0eu0xZK3To+YkDcWOk92rmNgmUSQC/kNyIOj+yBvOo3wTk6HvbhoIarCgJ6Lay1v /hMLyQLzwRY/Qfty1FTIDyTv2dch47FsfkZ1KAL+MbUnHuCBPzGxRjXa8Iy9Z7YG xrYasUt1b0um64bscxoIiCu8yLL8jlg01Rwrjr/MTwKRhwXlMp8B7HTonwtaG6ar JwIDAQAB -----END PUBLIC KEY----- ruby-jwt-3.1.2/spec/integration/000077500000000000000000000000001503003570500165535ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/integration/readme_examples_spec.rb000066400000000000000000000370261503003570500232550ustar00rootroot00000000000000# frozen_string_literal: true require 'logger' RSpec.describe 'README.md code test' do context 'algorithm usage' do let(:payload) { { data: 'test' } } it 'NONE' do token = JWT.encode payload, nil, 'none' decoded_token = JWT.decode token, nil, false expect(token).to eq 'eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.' expect(decoded_token).to eq [ { 'data' => 'test' }, { 'alg' => 'none' } ] end it 'decodes with HMAC algorithm with secret key' do token = JWT.encode payload, 'my$ecretK3y', 'HS256' decoded_token = JWT.decode token, 'my$ecretK3y', false expect(token).to eq 'eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y' expect(decoded_token).to eq [ { 'data' => 'test' }, { 'alg' => 'HS256' } ] end it 'decodes with HMAC algorithm without secret key' do pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression? token = JWT.encode payload, nil, 'HS256' decoded_token = JWT.decode token, nil, false expect(token).to eq 'eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pVzcY2dX8JNM3LzIYeP2B1e1Wcpt1K3TWVvIYSF4x-o' expect(decoded_token).to eq [ { 'data' => 'test' }, { 'alg' => 'HS256' } ] end it 'RSA' do rsa_private = OpenSSL::PKey::RSA.generate 2048 rsa_public = rsa_private.public_key token = JWT.encode payload, rsa_private, 'RS256' decoded_token = JWT.decode token, rsa_public, true, algorithm: 'RS256' expect(decoded_token).to eq [ { 'data' => 'test' }, { 'alg' => 'RS256' } ] end it 'ECDSA' do ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1') token = JWT.encode payload, ecdsa_key, 'ES256' decoded_token = JWT.decode token, ecdsa_key, true, algorithm: 'ES256' expect(decoded_token).to eq [ { 'data' => 'test' }, { 'alg' => 'ES256' } ] end if Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new('2.1') it 'RSASSA-PSS' do rsa_private = OpenSSL::PKey::RSA.generate 2048 rsa_public = rsa_private.public_key token = JWT.encode payload, rsa_private, 'PS256' decoded_token = JWT.decode token, rsa_public, true, algorithm: 'PS256' expect(decoded_token).to eq [ { 'data' => 'test' }, { 'alg' => 'PS256' } ] end end end context 'claims' do let(:hmac_secret) { 'MyP4ssW0rD' } context 'exp' do it 'without leeway' do exp = Time.now.to_i + (4 * 3600) exp_payload = { data: 'data', exp: exp } token = JWT.encode exp_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, algorithm: 'HS256' end.not_to raise_error end it 'with leeway' do exp = Time.now.to_i - 10 leeway = 30 # seconds exp_payload = { data: 'data', exp: exp } token = JWT.encode exp_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, leeway: leeway, algorithm: 'HS256' end.not_to raise_error end end context 'nbf' do it 'without leeway' do nbf = Time.now.to_i - 3600 nbf_payload = { data: 'data', nbf: nbf } token = JWT.encode nbf_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, algorithm: 'HS256' end.not_to raise_error end it 'with leeway' do nbf = Time.now.to_i + 10 leeway = 30 nbf_payload = { data: 'data', nbf: nbf } token = JWT.encode nbf_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, leeway: leeway, algorithm: 'HS256' end.not_to raise_error end end it 'iss' do iss = 'My Awesome Company Inc. or https://my.awesome.website/' iss_payload = { data: 'data', iss: iss } token = JWT.encode iss_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, iss: iss, algorithm: 'HS256' end.not_to raise_error end context 'aud' do it 'array' do aud = %w[Young Old] aud_payload = { data: 'data', aud: aud } token = JWT.encode aud_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, aud: %w[Old Young], verify_aud: true, algorithm: 'HS256' end.not_to raise_error end it 'string' do aud = 'Kids' aud_payload = { data: 'data', aud: aud } token = JWT.encode aud_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, aud: 'Kids', verify_aud: true, algorithm: 'HS256' end.not_to raise_error end end it 'jti' do iat = Time.now.to_i hmac_secret = 'test' jti_raw = [hmac_secret, iat].join(':').to_s jti = Digest::MD5.hexdigest(jti_raw) jti_payload = { data: 'data', iat: iat, jti: jti } token = JWT.encode jti_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, verify_jti: true, algorithm: 'HS256' end.not_to raise_error end context 'iat' do it 'without leeway' do iat = Time.now.to_i iat_payload = { data: 'data', iat: iat } token = JWT.encode iat_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, verify_iat: true, algorithm: 'HS256' end.not_to raise_error end it 'with leeway' do iat = Time.now.to_i - 7 iat_payload = { data: 'data', iat: iat, leeway: 10 } token = JWT.encode iat_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, verify_iat: true, algorithm: 'HS256' end.not_to raise_error end end context 'custom header fields' do it 'with custom field' do payload = { data: 'test' } token = JWT.encode payload, nil, 'none', typ: 'JWT' _, header = JWT.decode token, nil, false expect(header['typ']).to eq 'JWT' end end it 'sub' do sub = 'Subject' sub_payload = { data: 'data', sub: sub } token = JWT.encode sub_payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' } end.not_to raise_error expect do JWT.decode token, hmac_secret, true, { sub: 'sub', verify_sub: true, algorithm: 'HS256' } end.to raise_error(JWT::InvalidSubError) expect do JWT.decode token, hmac_secret, true, { 'sub' => 'sub', verify_sub: true, algorithm: 'HS256' } end.not_to raise_error end it 'required_claims' do payload = { data: 'test' } token = JWT.encode payload, hmac_secret, 'HS256' expect do JWT.decode token, hmac_secret, true, required_claims: ['exp'], algorithm: 'HS256' end.to raise_error(JWT::MissingRequiredClaim) expect do JWT.decode token, hmac_secret, true, required_claims: ['data'], algorithm: 'HS256' end.not_to raise_error end it 'find_key' do issuers = %w[My_Awesome_Company1 My_Awesome_Company2] iss_payload = { data: 'data', iss: issuers.first } secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' } token = JWT.encode iss_payload, hmac_secret, 'HS256' expect do # Add iss to the validation to check if the token has been manipulated JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| secrets[payload['iss']] end end.not_to raise_error end context 'The JWK based encode/decode routine' do it 'works as expected' do # ---------- ENCODE ---------- optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' } jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters) # Encoding payload = { data: 'data' } token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid]) # JSON Web Key Set for advertising your signing keys jwks_hash = JWT::JWK::Set.new(jwk).export # ---------- DECODE ---------- jwks = JWT::JWK::Set.new(jwks_hash) jwks.filter! { |key| key[:use] == 'sig' } # Signing keys only! algorithms = jwks.map { |key| key[:alg] }.compact.uniq JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks) end end context 'The JWKS loader example' do let(:logger_output) { StringIO.new } let(:logger) { Logger.new(logger_output) } it 'works as expected (legacy)' do jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'optional-kid') payload = { data: 'data' } headers = { kid: jwk.kid } token = JWT.encode(payload, jwk.signing_key, 'RS512', headers) # The jwk loader would fetch the set of JWKs from a trusted source, # to avoid malicious invalidations some kind of protection needs to be implemented. # This example only allows cache invalidations every 5 minutes. jwk_loader = lambda do |options| if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300 logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache") @cached_keys = nil end @cached_keys ||= begin @cache_last_update = Time.now.to_i { keys: [jwk.export] } end end begin JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) rescue JWT::JWKError # Handle problems with the provided JWKs rescue JWT::DecodeError # Handle other decode related issues e.g. no kid in header, no matching public key found etc. end ## This is not in the example but verifies that the cache is invalidated after 5 minutes jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'new-kid') headers = { kid: jwk.kid } token = JWT.encode(payload, jwk.signing_key, 'RS512', headers) @cache_last_update = Time.now.to_i - 301 JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) expect(logger_output.string.chomp).to match(/^I, .* : Invalidating JWK cache. new-kid not found from previous cache/) jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'yet-another-new-kid') headers = { kid: jwk.kid } token = JWT.encode(payload, jwk.signing_key, 'RS512', headers) expect { JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) }.to raise_error(JWT::DecodeError, 'Could not find public key for kid yet-another-new-kid') end it 'works as expected' do jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), use: 'sig') jwks_hash = JWT::JWK::Set.new(jwk) payload = { data: 'data' } headers = { kid: jwk.kid } token = JWT.encode(payload, jwk.signing_key, 'RS512', headers) jwks_loader = lambda do |options| # The jwk loader would fetch the set of JWKs from a trusted source. # To avoid malicious requests triggering cache invalidations there needs to be # some kind of grace time or other logic for determining the validity of the invalidation. # This example only allows cache invalidations every 5 minutes. if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300 logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache") @cached_keys = nil end @cached_keys ||= begin @cache_last_update = Time.now.to_i # Replace with your own JWKS fetching routine jwks = JWT::JWK::Set.new(jwks_hash) jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only jwks end end begin JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader }) rescue JWT::JWKError # Handle problems with the provided JWKs rescue JWT::DecodeError # Handle other decode related issues e.g. no kid in header, no matching public key found etc. end end end it 'JWK import and export' do # Import a JWK Hash (showing an HMAC example) _jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' }) # Import an OpenSSL key # You can optionally add descriptive parameters to the JWK desc_params = { kid: 'my-kid', use: 'sig' } jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params) # Export as JWK Hash (public key only by default) _jwk_hash = jwk.export _jwk_hash_with_private_key = jwk.export(include_private: true) # Export as OpenSSL key _public_key = jwk.verify_key _private_key = jwk.signing_key if jwk.private? # You can also import and export entire JSON Web Key Sets jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] } jwks = JWT::JWK::Set.new(jwks_hash) _jwks_hash = jwks.export end it 'JWK with thumbprint as kid via symbol' do JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048)) jwk_hash = jwk.export expect(jwk_hash[:kid].size).to eq(43) end it 'JWK with thumbprint as kid via type' do JWT.configuration.jwk.kid_generator = JWT::JWK::Thumbprint jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048)) jwk_hash = jwk.export expect(jwk_hash[:kid].size).to eq(43) end it 'JWK with thumbprint given in the initializer (legacy)' do jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid_generator: JWT::JWK::Thumbprint) jwk_hash = jwk.export expect(jwk_hash[:kid].size).to eq(43) end it 'JWK with thumbprint given in the initializer' do jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: JWT::JWK::Thumbprint) jwk_hash = jwk.export expect(jwk_hash[:kid].size).to eq(43) end end context 'custom algorithm example' do it 'allows a module to be used as algorithm on encode and decode' do custom_hs512_alg = Module.new do extend JWT::JWA::SigningAlgorithm def self.alg 'HS512' end def self.sign(data:, signing_key:) OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), signing_key, data) end def self.verify(data:, signature:, verification_key:) sign(data: data, signing_key: verification_key) == signature end end token = JWT.encode({ 'pay' => 'load' }, 'secret', custom_hs512_alg) _payload, header = JWT.decode(token, 'secret', true, algorithm: custom_hs512_alg) expect(header).to include('alg' => 'HS512') end end context 'JWK to verify a signature' do it 'allows to verify a signature with a JWK' do payload = { exp: Time.now.to_i + 60, jti: '1234', sub: 'my-subject' } header = { kid: 'hmac' } jwk_json = '{ "kty": "oct", "k": "c2VjcmV0", "alg": "HS256", "kid": "hmac" }' jwk = JWT::JWK.import(JSON.parse(jwk_json)) token = JWT::Token.new(payload: payload, header: header) token.sign!(key: jwk, algorithm: 'HS256') encoded_token = JWT::EncodedToken.new(token.jwt) expect { encoded_token.verify!(signature: { algorithm: %w[HS256 HS512], key: jwk }) }.not_to raise_error end end end ruby-jwt-3.1.2/spec/jwt/000077500000000000000000000000001503003570500150345ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/jwt/claims/000077500000000000000000000000001503003570500163045ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/jwt/claims/audience_spec.rb000066400000000000000000000042251503003570500214230ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::Audience do let(:payload) { { 'nbf' => (Time.now.to_i + 5) } } describe '#verify!' do let(:scalar_aud) { 'ruby-jwt-aud' } let(:array_aud) { %w[ruby-jwt-aud test-aud ruby-ruby-ruby] } subject(:verify!) { described_class.new(expected_audience: expected_audience).verify!(context: SpecSupport::Token.new(payload: payload)) } context 'when the singular audience does not match' do let(:expected_audience) { 'no-match' } let(:payload) { { 'aud' => scalar_aud } } it 'raises JWT::InvalidAudError' do expect do subject end.to raise_error JWT::InvalidAudError end end context 'when the payload has an array and none match the supplied value' do let(:expected_audience) { 'no-match' } let(:payload) { { 'aud' => array_aud } } it 'raises JWT::InvalidAudError' do expect do subject end.to raise_error JWT::InvalidAudError end end context 'when single audience is required' do let(:expected_audience) { scalar_aud } let(:payload) { { 'aud' => scalar_aud } } it 'passes validation' do subject end end context 'when any value in payload matches a single expected' do let(:expected_audience) { array_aud.first } let(:payload) { { 'aud' => array_aud } } it 'passes validation' do subject end end context 'when an array with any value matching the one in the options' do let(:expected_audience) { array_aud.first } let(:payload) { { 'aud' => array_aud } } it 'passes validation' do subject end end context 'when an array with any value matching all in the options' do let(:expected_audience) { array_aud } let(:payload) { { 'aud' => array_aud } } it 'passes validation' do subject end end context 'when a singular audience payload matching any value in the options array' do let(:expected_audience) { array_aud } let(:payload) { { 'aud' => scalar_aud } } it 'passes validation' do subject end end end end ruby-jwt-3.1.2/spec/jwt/claims/crit_spec.rb000066400000000000000000000027721503003570500206140ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::Crit do subject(:verify!) { described_class.new(expected_crits: expected_crits).verify!(context: SpecSupport::Token.new(header: header)) } let(:expected_crits) { [] } let(:header) { {} } context 'when header is missing' do it 'raises JWT::InvalidCritError' do expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header missing') end end context 'when header is not an array' do let(:header) { { 'crit' => 'not_an_array' } } it 'raises JWT::InvalidCritError' do expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header should be an array') end end context 'when header is an array and not containing the expected value' do let(:header) { { 'crit' => %w[crit1] } } let(:expected_crits) { %w[crit2] } it 'raises an InvalidCritError' do expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header missing expected values: crit2') end end context 'when header is an array containing exactly the expected values' do let(:header) { { 'crit' => %w[crit1 crit2] } } let(:expected_crits) { %w[crit1 crit2] } it 'does not raise an error' do expect(verify!).to eq(nil) end end context 'when header is an array containing at least the expected values' do let(:header) { { 'crit' => %w[crit1 crit2 crit3] } } let(:expected_crits) { %w[crit1 crit2] } it 'does not raise an error' do expect(verify!).to eq(nil) end end end ruby-jwt-3.1.2/spec/jwt/claims/expiration_spec.rb000066400000000000000000000020621503003570500220250ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::Expiration do let(:payload) { { 'exp' => (Time.now.to_i + 5) } } let(:leeway) { 0 } subject(:verify!) { described_class.new(leeway: leeway).verify!(context: SpecSupport::Token.new(payload: payload)) } context 'when token is expired' do let(:payload) { { 'exp' => (Time.now.to_i - 5) } } it 'must raise JWT::ExpiredSignature when the token has expired' do expect { verify! }.to(raise_error(JWT::ExpiredSignature)) end end context 'when token is expired but some leeway is defined' do let(:payload) { { 'exp' => (Time.now.to_i - 5) } } let(:leeway) { 10 } it 'passes validation' do verify! end end context 'when token exp is set to current time' do let(:payload) { { 'exp' => Time.now.to_i } } it 'fails validation' do expect { verify! }.to(raise_error(JWT::ExpiredSignature)) end end context 'when token is not a Hash' do let(:payload) { 'beautyexperts_nbf_iat' } it 'passes validation' do verify! end end end ruby-jwt-3.1.2/spec/jwt/claims/issued_at_spec.rb000066400000000000000000000020071503003570500216220ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::IssuedAt do let(:payload) { { 'iat' => Time.now.to_f } } subject(:verify!) { described_class.new.verify!(context: SpecSupport::Token.new(payload: payload)) } context 'when iat is now' do it 'passes validation' do verify! end end context 'when iat is now as a integer' do let(:payload) { { 'iat' => Time.now.to_i } } it 'passes validation' do verify! end end context 'when iat is not a number' do let(:payload) { { 'iat' => 'not_a_number' } } it 'fails validation' do expect { verify! }.to raise_error(JWT::InvalidIatError) end end context 'when iat is in the future' do let(:payload) { { 'iat' => Time.now.to_f + 120.0 } } it 'fails validation' do expect { verify! }.to raise_error(JWT::InvalidIatError) end end context 'when payload is a string containing iat' do let(:payload) { 'beautyexperts_nbf_iat' } it 'passes validation' do verify! end end end ruby-jwt-3.1.2/spec/jwt/claims/issuer_spec.rb000066400000000000000000000076261503003570500211700ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::Issuer do let(:issuer) { 'ruby-jwt-gem' } let(:payload) { { 'iss' => issuer } } let(:expected_issuers) { 'ruby-jwt-gem' } subject(:verify!) { described_class.new(issuers: expected_issuers).verify!(context: SpecSupport::Token.new(payload: payload)) } context 'when expected issuer is a string that matches the payload' do it 'passes validation' do verify! end end context 'when expected issuer is a string that does not match the payload' do let(:issuer) { 'mismatched-issuer' } it 'raises JWT::InvalidIssuerError' do expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["ruby-jwt-gem"], received mismatched-issuer') end end context 'when payload does not contain any issuer' do let(:payload) { {} } it 'raises JWT::InvalidIssuerError' do expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["ruby-jwt-gem"], received ') end end context 'when expected issuer is an array that matches the payload' do let(:expected_issuers) { ['first', issuer, 'third'] } it 'passes validation' do verify! end end context 'when expected issuer is an array that does not match the payload' do let(:expected_issuers) { %w[first second] } it 'raises JWT::InvalidIssuerError' do expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["first", "second"], received ruby-jwt-gem') end end context 'when expected issuer is an array and payload does not have any issuer' do let(:payload) { {} } let(:expected_issuers) { %w[first second] } it 'raises JWT::InvalidIssuerError' do expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["first", "second"], received ') end end context 'when issuer is given as a RegExp' do let(:issuer) { 'ruby-jwt-gem' } let(:expected_issuers) { /\A(first|#{issuer}|third)\z/ } it 'passes validation' do verify! end end context 'when issuer is given as a RegExp and does not match the payload' do let(:issuer) { 'mismatched-issuer' } let(:expected_issuers) { /\A(first|second)\z/ } it 'raises JWT::InvalidIssuerError' do expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected [/\A(first|second)\z/], received mismatched-issuer') end end context 'when issuer is given as a RegExp and payload does not have any issuer' do let(:payload) { {} } let(:expected_issuers) { /\A(first|second)\z/ } it 'raises JWT::InvalidIssuerError' do expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected [/\A(first|second)\z/], received ') end end context 'when issuer is given as a Proc' do let(:issuer) { 'ruby-jwt-gem' } let(:expected_issuers) { ->(iss) { iss.start_with?('ruby') } } it 'passes validation' do verify! end end context 'when issuer is given as a Proc and does not match the payload' do let(:issuer) { 'mismatched-issuer' } let(:expected_issuers) { ->(iss) { iss.start_with?('ruby') } } it 'raises JWT::InvalidIssuerError' do expect { verify! }.to raise_error(JWT::InvalidIssuerError, /received mismatched-issuer/) end end context 'when issuer is given as a Proc and payload does not have any issuer' do let(:payload) { {} } let(:expected_issuers) { ->(iss) { iss&.start_with?('ruby') } } it 'raises JWT::InvalidIssuerError' do expect { verify! }.to raise_error(JWT::InvalidIssuerError, /received /) end end context 'when issuer is given as a Method instance' do def issuer_start_with_ruby?(issuer) issuer&.start_with?('ruby') end let(:issuer) { 'ruby-jwt-gem' } let(:expected_issuers) { method(:issuer_start_with_ruby?) } it 'passes validation' do verify! end end end ruby-jwt-3.1.2/spec/jwt/claims/jwt_id_spec.rb000066400000000000000000000034611503003570500211270ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::JwtId do let(:jti) { 'some-random-uuid-or-whatever' } let(:payload) { { 'jti' => jti } } let(:validator) { nil } subject(:verify!) { described_class.new(validator: validator).verify!(context: SpecSupport::Token.new(payload: payload)) } context 'when payload contains a jti' do it 'passes validation' do verify! end end context 'when payload is missing a jti' do let(:payload) { {} } it 'raises JWT::InvalidJtiError' do expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') end end context 'when payload contains a jti that is an empty string' do let(:jti) { '' } it 'raises JWT::InvalidJtiError' do expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') end end context 'when payload contains a jti that is a blank string' do let(:jti) { ' ' } it 'raises JWT::InvalidJtiError' do expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') end end context 'when jti validator is a proc returning false' do let(:validator) { ->(_jti) { false } } it 'raises JWT::InvalidJtiError' do expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Invalid jti') end end context 'when jti validator is a proc returning true' do let(:validator) { ->(_jti) { true } } it 'passes validation' do verify! end end context 'when jti validator has 2 args' do let(:validator) { ->(_jti, _pl) { true } } it 'passes validation' do verify! end end context 'when jti validator has 2 args' do it 'the second arg is the payload' do described_class.new(validator: ->(_jti, pl) { expect(pl).to eq(payload) }).verify!(context: SpecSupport::Token.new(payload: payload)) end end end ruby-jwt-3.1.2/spec/jwt/claims/not_before_spec.rb000066400000000000000000000016231503003570500217670ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::NotBefore do let(:payload) { { 'nbf' => (Time.now.to_i + 5) } } describe '#verify!' do context 'when nbf is in the future' do it 'raises JWT::ImmatureSignature' do expect { described_class.new(leeway: 0).verify!(context: SpecSupport::Token.new(payload: payload)) }.to raise_error JWT::ImmatureSignature end end context 'when nbf is in the past' do let(:payload) { { 'nbf' => (Time.now.to_i - 5) } } it 'does not raise error' do expect { described_class.new(leeway: 0).verify!(context: SpecSupport::Token.new(payload: payload)) }.not_to raise_error end end context 'when leeway is given' do it 'does not raise error' do expect { described_class.new(leeway: 10).verify!(context: SpecSupport::Token.new(payload: payload)) }.not_to raise_error end end end end ruby-jwt-3.1.2/spec/jwt/claims/numeric_spec.rb000066400000000000000000000045151503003570500213120ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::Numeric do shared_examples_for 'a NumericDate claim' do |claim| context "when #{claim} payload is an integer" do let(:claims) { { claim => 12_345 } } it 'does not raise error' do expect { subject }.not_to raise_error end context 'and key is a string' do let(:claims) { { claim.to_s => 43.32 } } it 'does not raise error' do expect { subject }.not_to raise_error end end end context "when #{claim} payload is a float" do let(:claims) { { claim => 43.32 } } it 'does not raise error' do expect { subject }.not_to raise_error end end context "when #{claim} payload is a string" do let(:claims) { { claim => '1' } } it 'raises error' do expect { subject }.to raise_error JWT::InvalidPayload end context 'and key is a string' do let(:claims) { { claim.to_s => '1' } } it 'raises error' do expect { subject }.to raise_error JWT::InvalidPayload end end end context "when #{claim} payload is a Time object" do let(:claims) { { claim => Time.now } } it 'raises error' do expect { subject }.to raise_error JWT::InvalidPayload end end context "when #{claim} payload is a string" do let(:claims) { { claim => '1' } } it 'raises error' do expect { subject }.to raise_error JWT::InvalidPayload end end end let(:validator) { described_class.new } describe '#verify!' do subject { validator.verify!(context: JWT::Claims::VerificationContext.new(payload: claims)) } context 'exp claim' do it_should_behave_like 'a NumericDate claim', :exp end context 'iat claim' do it_should_behave_like 'a NumericDate claim', :iat end context 'nbf claim' do it_should_behave_like 'a NumericDate claim', :nbf end end describe 'use via ::JWT::Claims.verify_payload!' do subject { JWT::Claims.verify_payload!(claims, :numeric) } context 'exp claim' do it_should_behave_like 'a NumericDate claim', :exp end context 'iat claim' do it_should_behave_like 'a NumericDate claim', :iat end context 'nbf claim' do it_should_behave_like 'a NumericDate claim', :nbf end end end ruby-jwt-3.1.2/spec/jwt/claims/required_spec.rb000066400000000000000000000013361503003570500214660ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::Required do let(:payload) { { 'data' => 'value' } } subject(:verify!) { described_class.new(required_claims: required_claims).verify!(context: SpecSupport::Token.new(payload: payload)) } context 'when payload is missing the required claim' do let(:required_claims) { ['exp'] } it 'raises JWT::MissingRequiredClaim' do expect { verify! }.to raise_error JWT::MissingRequiredClaim, 'Missing required claim exp' end end context 'when payload has the required claims' do let(:payload) { { 'exp' => 'exp', 'custom_claim' => true } } let(:required_claims) { %w[exp custom_claim] } it 'passes validation' do verify! end end end ruby-jwt-3.1.2/spec/jwt/claims/verifier_spec.rb000066400000000000000000000012071503003570500214560ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims::Verifier do describe '.verify!' do context 'when all claims are given' do let(:options) do [ :exp, :nbf, { iss: 'issuer' }, :iat, :jti, { aud: 'aud' }, :sub, :crit, { required: [] }, :numeric ] end it 'verifies all claims' do token = SpecSupport::Token.new(payload: { 'iss' => 'issuer', 'jti' => 1, 'aud' => 'aud' }, header: { 'crit' => [] }) expect(described_class.verify!(token, *options)).to eq(nil) end end end end ruby-jwt-3.1.2/spec/jwt/claims_spec.rb000066400000000000000000000112021503003570500176370ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Claims do let(:payload) { { 'pay' => 'load' } } describe '.verify_payload!' do context 'when required_claims is passed' do it 'raises error' do expect { described_class.verify_payload!(payload, required: ['exp']) }.to raise_error(JWT::MissingRequiredClaim, 'Missing required claim exp') end end context 'exp claim' do let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } it 'verifies the exp' do described_class.verify_payload!(payload, required: ['exp']) expect { described_class.verify_payload!(payload, exp: {}) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') described_class.verify_payload!(payload, exp: { leeway: 1000 }) end context 'when claims given as symbol' do it 'validates the claim' do expect { described_class.verify_payload!(payload, :exp) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end context 'when claims given as a list of symbols' do it 'validates the claim' do expect { described_class.verify_payload!(payload, :exp, :nbf) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end context 'when claims given as a list of symbols and hashes' do it 'validates the claim' do expect { described_class.verify_payload!(payload, { exp: { leeway: 1000 }, nbf: {} }, :exp, :nbf) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end end end describe '.valid_payload?' do context 'exp claim' do let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } context 'when claim is valid' do it 'returns true' do expect(described_class.valid_payload?(payload, exp: { leeway: 1000 })).to be(true) end end context 'when claim is invalid' do it 'returns false' do expect(described_class.valid_payload?(payload, :exp)).to be(false) end end end context 'various types of params' do context 'when payload is missing most of the claims' do it 'raises an error' do expect do described_class.verify_payload!(payload, :nbf, iss: ['www.host.com', 'https://other.host.com'].freeze, aud: 'aud', exp: { leeway: 10 }) end.to raise_error(JWT::InvalidIssuerError) end end context 'when payload has everything that is expected of it' do let(:payload) { { 'iss' => 'www.host.com', 'aud' => 'audience', 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } it 'does not raise' do expect do described_class.verify_payload!(payload, :nbf, iss: ['www.host.com', 'https://other.host.com'].freeze, aud: 'audience', exp: { leeway: 11 }) end.not_to raise_error end end end end describe '.payload_errors' do context 'exp claim' do let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } context 'when claim is valid' do it 'returns empty array' do expect(described_class.payload_errors(payload, exp: { leeway: 1000 })).to be_empty end end context 'when claim is invalid' do it 'returns array with error objects' do expect(described_class.payload_errors(payload, :exp).map(&:message)).to eq(['Signature has expired']) end end end context 'various types of params' do let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } context 'when payload is most of the claims' do it 'raises an error' do messages = described_class.payload_errors(payload, :nbf, iss: ['www.host.com', 'https://other.host.com'].freeze, aud: 'aud', exp: { leeway: 10 }).map(&:message) expect(messages).to eq(['Invalid issuer. Expected ["www.host.com", "https://other.host.com"], received ', 'Invalid audience. Expected aud, received ', 'Signature has expired']) end end end end end ruby-jwt-3.1.2/spec/jwt/concurrent_encode_decode_spec.rb000066400000000000000000000027471503003570500234070ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWA::Ecdsa do context 'used across threads for encoding and decoding' do it 'successfully encodes, decodes, and verifies' do threads = 10.times.map do Thread.new do public_key_pem = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcKuFOqoNEN+TXylz4MVAWREa9yA8\npOF9QgGchnAy6Ad4P7yCpk+R3wCGTDLfNboYqUmbK5Hd9uHszf+EMTi22g==\n-----END PUBLIC KEY-----\n" private_key_pem = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiF/iNuQem/yQyd16\nc9shf2Y9vMycOU7g6W6LTmkyj1ehRANCAARwq4U6qg0Q35NfKXPgxUBZERr3IDyk\n4X1CAZyGcDLoB3g/vIKmT5HfAIZMMt81uhipSZsrkd324ezN/4QxOLba\n-----END PRIVATE KEY-----\n" full_pem = private_key_pem + public_key_pem curve = OpenSSL::PKey.read(full_pem) public_key = OpenSSL::PKey::EC.new(public_key_pem) 10.times do input_payload = { 'aud' => 'https://fcm.googleapis.com', 'exp' => (Time.now.to_i + 600), 'sub' => 'mailto:example@example.com' } input_header = { 'typ' => 'JWT', 'alg' => 'ES256' } token = JWT.encode(input_payload, curve, 'ES256', input_header) output_payload, output_header = JWT.decode(token, public_key, true, { algorithm: 'ES256', verify_expiration: true }) expect(output_payload).to eq input_payload expect(output_header).to eq input_header end end end threads.each(&:join) end end end ruby-jwt-3.1.2/spec/jwt/configuration/000077500000000000000000000000001503003570500177035ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/jwt/configuration/jwk_configuration_spec.rb000066400000000000000000000011231503003570500247610ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Configuration::JwkConfiguration do describe '.kid_generator_type=' do context 'when invalid value is passed' do it 'raises ArgumentError' do expect { subject.kid_generator_type = :foo }.to raise_error(ArgumentError, 'foo is not a valid kid generator type.') end end context 'when valid value is passed' do it 'sets the generator matching the value' do subject.kid_generator_type = :rfc7638_thumbprint expect(subject.kid_generator).to eq(JWT::JWK::Thumbprint) end end end end ruby-jwt-3.1.2/spec/jwt/configuration_spec.rb000066400000000000000000000011001503003570500212320ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT do describe 'JWT.configure' do it 'yields the configuration' do expect { |b| described_class.configure(&b) }.to yield_with_args(described_class.configuration) end it 'allows configuration to be changed via the block' do expect(described_class.configuration.decode.verify_expiration).to eq(true) described_class.configure do |config| config.decode.verify_expiration = false end expect(described_class.configuration.decode.verify_expiration).to eq(false) end end end ruby-jwt-3.1.2/spec/jwt/encoded_token_spec.rb000066400000000000000000000411761503003570500212050ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::EncodedToken do let(:payload) { { 'pay' => 'load' } } let(:header) { {} } let(:encoded_token) { JWT::Token.new(payload: payload, header: header).tap { |t| t.sign!(algorithm: 'HS256', key: 'secret') }.jwt } let(:detached_payload_token) do JWT::Token.new(payload: payload).tap do |t| t.detach_payload! t.sign!(algorithm: 'HS256', key: 'secret') end end subject(:token) { described_class.new(encoded_token) } describe '#unverified_payload' do it { expect(token.unverified_payload).to eq(payload) } context 'when payload is detached' do let(:encoded_token) { detached_payload_token.jwt } context 'when payload provided in separate' do before { token.encoded_payload = detached_payload_token.encoded_payload } it { expect(token.unverified_payload).to eq(payload) } end context 'when payload is not provided' do it 'raises decode error' do expect { token.unverified_payload }.to raise_error(JWT::DecodeError, 'Encoded payload is empty') end end end context 'when payload is not encoded and the b64 crit is enabled' do subject(:token) { described_class.new(encoded_token) } let(:encoded_token) { 'eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..signature' } before { token.encoded_payload = '{"foo": "bar"}' } it 'handles the payload encoding' do expect(token.unverified_payload).to eq({ 'foo' => 'bar' }) end end context 'when token is the empty string' do let(:encoded_token) { '' } it 'raises decode error' do expect { token.unverified_payload }.to raise_error(JWT::DecodeError, 'Invalid segment encoding') end end end describe '#payload' do context 'when token is verified using #valid?' do before do token.valid?(signature: { algorithm: 'HS256', key: 'secret' }) end it { expect(token.payload).to eq(payload) } end context 'when token is verified using #verify_signature! and #verify_claims!' do before do token.verify_signature!(algorithm: 'HS256', key: 'secret') token.verify_claims! end it { expect(token.payload).to eq(payload) } end context 'when token is checked using #valid_signature? and #valid_claims?' do before do token.valid_signature?(algorithm: 'HS256', key: 'secret') token.valid_claims? end it { expect(token.payload).to eq(payload) } end context 'when token is verified using #verify_signature!' do before { token.verify_signature!(algorithm: 'HS256', key: 'secret') } it 'raises an error' do expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token claims before accessing the payload') end end context 'when token is verified using #valid_signature? but is not valid' do before { token.valid_signature?(algorithm: 'HS256', key: 'wrong') } it 'raises an error' do expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload') end end context 'when token is not verified' do it 'raises an error' do expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload') end end end describe '#header' do it { expect(token.header).to eq({ 'alg' => 'HS256' }) } context 'when token is the empty string' do let(:encoded_token) { '' } it 'raises decode error' do expect { token.header }.to raise_error(JWT::DecodeError, 'Invalid segment encoding') end end end describe '#signature' do it { expect(token.signature).to be_a(String) } end describe '#signing_input' do it { expect(token.signing_input).to eq('eyJhbGciOiJIUzI1NiJ9.eyJwYXkiOiJsb2FkIn0') } end describe '#verify_signature!' do context 'when key is valid' do it 'does not raise' do expect(token.verify_signature!(algorithm: 'HS256', key: 'secret')).to eq(nil) end end context 'when key is invalid' do it 'raises an error' do expect { token.verify_signature!(algorithm: 'HS256', key: 'wrong') }.to raise_error(JWT::VerificationError, 'Signature verification failed') end end context 'when key is an array with one valid entry' do it 'does not raise' do expect(token.verify_signature!(algorithm: 'HS256', key: %w[wrong secret])).to eq(nil) end end context 'when algorithm is an empty array' do it 'raises an error' do expect { token.verify_signature!(key: 'secret', algorithm: []) }.to raise_error(JWT::VerificationError, 'No algorithm provided') end end context 'when algorithm is not given' do it 'raises an error' do expect { token.verify_signature!(key: 'secret') }.to raise_error(ArgumentError, /missing keyword/) end end context 'when header has invalid alg value' do let(:header) { { 'alg' => 'HS123' } } it 'does not raise' do expect(token.header).to eq(header) expect(token.verify_signature!(algorithm: 'HS256', key: 'secret')).to eq(nil) end end context 'when payload is detached' do let(:encoded_token) { detached_payload_token.jwt } context 'when payload provided in separate' do before { token.encoded_payload = detached_payload_token.encoded_payload } it 'does not raise' do expect(token.verify_signature!(algorithm: 'HS256', key: 'secret')).to eq(nil) end end context 'when payload is not provided' do it 'raises VerificationError' do expect { token.verify_signature!(algorithm: 'HS256', key: 'secret') }.to raise_error(JWT::VerificationError, 'Signature verification failed') end end end context 'when key_finder is given' do it 'uses key provided by keyfinder' do expect(token.verify_signature!(algorithm: 'HS256', key_finder: ->(_token) { 'secret' })).to eq(nil) end it 'can utilize an array provided by keyfinder' do expect(token.verify_signature!(algorithm: 'HS256', key_finder: ->(_token) { %w[wrong secret] })).to eq(nil) end end context 'when neither key or key_finder is given' do it 'raises an ArgumentError' do expect { token.verify_signature!(algorithm: 'HS256') }.to raise_error(ArgumentError, 'Provide either key or key_finder, not both or neither') end end context 'when both key or key_finder is given' do it 'raises an ArgumentError' do expect { token.verify_signature!(algorithm: 'HS256', key: 'key', key_finder: 'finder') }.to raise_error(ArgumentError, 'Provide either key or key_finder, not both or neither') end end context 'when payload is not encoded' do let(:encoded_token) { 'eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..A5dxf2s96_n5FLueVuW1Z_vh161FwXZC4YLPff6dmDY' } before { token.encoded_payload = '$.02' } let(:key) { Base64.urlsafe_decode64('AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow') } it 'does not raise' do expect(token.verify_signature!(algorithm: 'HS256', key: key)).to eq(nil) end end context 'when JWT::KeyFinder is used as a key_finder' do let(:jwk) { JWT::JWK.new(test_pkey('rsa-2048-private.pem')) } let(:encoded_token) do JWT::Token.new(payload: payload, header: { kid: jwk.kid }) .tap { |t| t.sign!(algorithm: 'RS256', key: jwk.signing_key) } .jwt end it 'uses the keys provided by the JWK key finder' do key_finder = JWT::JWK::KeyFinder.new(jwks: JWT::JWK::Set.new(jwk)) expect(token.verify_signature!(algorithm: 'RS256', key_finder: key_finder)).to eq(nil) end end context 'when RSA JWK is given as a key' do let(:jwk) { JWT::JWK.new(test_pkey('rsa-2048-private.pem'), alg: 'RS256') } let(:encoded_token) do JWT::Token.new(payload: payload) .tap { |t| t.sign!(algorithm: 'RS256', key: jwk.signing_key) } .jwt end context 'with empty algorithm array provided' do it 'uses the JWK for verification' do expect(token.verify_signature!(key: jwk, algorithm: [])).to eq(nil) end end context 'with algorithms supported by key provided' do it 'uses the JWK for verification' do expect(token.verify_signature!(algorithm: %w[RS256 RS512], key: jwk)).to eq(nil) end end context 'with algorithms not supported by key provided' do it 'raises JWT::VerificationError' do expect { token.verify_signature!(algorithm: %w[RS384 RS512], key: jwk) }.to raise_error(JWT::VerificationError, 'Provided JWKs do not support one of the specified algorithms: RS384, RS512') end end end end describe '#verify_claims!' do context 'when required_claims is passed' do it 'raises error' do expect { token.verify_claims!(required: ['exp']) }.to raise_error(JWT::MissingRequiredClaim, 'Missing required claim exp') end end context 'exp claim' do let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } it 'verifies the exp' do token.verify_claims!(required: ['exp']) expect { token.verify_claims!(exp: {}) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') token.verify_claims!(exp: { leeway: 1000 }) end context 'when no claims are provided' do it 'raises ExpiredSignature error' do expect { token.verify_claims! }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end context 'when claim validation skips verifying the exp claim' do it 'does not raise' do expect { token.verify_claims!({}) }.not_to raise_error end end context 'when claims given as symbol' do it 'validates the claim' do expect { token.verify_claims!(:exp) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end context 'when claims given as a list of symbols' do it 'validates the claim' do expect { token.verify_claims!(:exp, :nbf) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end context 'when claims given as a list of symbols and hashes' do it 'validates the claim' do expect { token.verify_claims!({ exp: { leeway: 1000 }, nbf: {} }, :exp, :nbf) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end context 'when payload is detached' do let(:encoded_token) { detached_payload_token.jwt } context 'when payload provided in separate' do before { token.encoded_payload = detached_payload_token.encoded_payload } it 'raises claim verification error' do expect { token.verify_claims!(:exp, :nbf) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end context 'when payload is not provided' do it 'raises decode error' do expect { token.verify_claims!(:exp, :nbf) }.to raise_error(JWT::DecodeError, 'Encoded payload is empty') end end end end context 'when header contains crits header' do let(:header) { { crit: ['b64'] } } context 'when expected crits are missing' do it 'raises an error' do expect { token.verify_claims!(crit: ['other']) }.to raise_error(JWT::InvalidCritError, 'Crit header missing expected values: other') end end context 'when expected crits are present' do it 'passes verification' do expect { token.verify_claims!(crit: ['b64']) }.not_to raise_error end end end end context '#verify!' do context 'when key is valid' do it 'does not raise' do expect(token.verify!(signature: { algorithm: 'HS256', key: 'secret' })).to eq(nil) end end context 'when key is invalid' do it 'raises an error' do expect { token.verify!(signature: { algorithm: 'HS256', key: 'wrong' }) }.to raise_error(JWT::VerificationError, 'Signature verification failed') end end context 'when claims are invalid' do let(:payload) { { 'pay' => 'load', exp: Time.now.to_i - 1000 } } it 'raises an error' do expect do token.verify!(signature: { algorithm: 'HS256', key: 'secret' }, claims: { exp: { leeway: 900 } }) end.to raise_error(JWT::ExpiredSignature, 'Signature has expired') end end end context '#valid?' do context 'when key is valid' do it 'returns true' do expect(token.valid?(signature: { algorithm: 'HS256', key: 'secret' })).to be(true) end end context 'when key is invalid' do it 'returns false' do expect(token.valid?(signature: { algorithm: 'HS256', key: 'wrong' })).to be(false) end end context 'when claims are provided as an array' do it 'returns true' do expect( token.valid?(signature: { algorithm: 'HS256', key: 'secret' }, claims: [:exp]) ).to be(true) end end context 'when claims are invalid' do let(:payload) { { 'pay' => 'load', exp: Time.now.to_i - 1000 } } it 'returns false' do expect( token.valid?(signature: { algorithm: 'HS256', key: 'secret' }, claims: { exp: { leeway: 900 } }) ).to be(false) end end end describe '#valid_claims?' do context 'exp claim' do let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } context 'when claim is valid' do it 'returns true' do expect(token.valid_claims?(exp: { leeway: 1000 })).to be(true) end end context 'when claim is invalid' do it 'returns true' do expect(token.valid_claims?(:exp)).to be(false) end end context 'when no claims are provided' do it 'validates the exp claim and returns false' do expect(token.valid_claims?).to be(false) end end context 'when claim validation skips verifying the exp claim' do it 'returns true' do expect(token.valid_claims?({})).to be(true) end end end end describe '#claim_errors' do context 'exp claim' do let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } context 'when claim is valid' do it 'returns empty array' do expect(token.claim_errors(exp: { leeway: 1000 })).to be_empty end end context 'when claim is invalid' do it 'returns array with error objects' do expect(token.claim_errors(:exp).map(&:message)).to eq(['Signature has expired']) end end end end describe 'integration use-cases' do context 'simple verify HS256 with defaults' do let(:encoded_token) do JWT::Token.new(payload: { 'pay' => 'load' }) .tap { |t| t.sign!(algorithm: 'HS256', key: 'secret_signing_key') } .jwt end it 'protects the user from unverified payload access' do token = described_class.new(encoded_token) expect(token.unverified_payload).to eq({ 'pay' => 'load' }) expect(token.header).to eq({ 'alg' => 'HS256' }) expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload') expect(token.valid_signature?(algorithm: 'HS256', key: 'invalid_signing_key')).to be(false) expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload') expect(token.valid_signature?(algorithm: 'HS256', key: 'secret_signing_key')).to be(true) expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token claims before accessing the payload') expect(token.valid_claims?(iss: 'issuer')).to be(false) expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token claims before accessing the payload') expect(token.valid_claims?).to be(true) expect(token.payload).to eq({ 'pay' => 'load' }) token = described_class.new(encoded_token) expect(token.valid?(signature: { algorithm: 'HS256', key: 'invalid_signing_key' })).to be(false) expect { token.payload }.to raise_error(JWT::DecodeError, 'Verify the token signature before accessing the payload') expect(token.valid?(signature: { algorithm: 'HS256', key: 'secret_signing_key' })).to be(true) expect(token.payload).to eq({ 'pay' => 'load' }) end end end end ruby-jwt-3.1.2/spec/jwt/jwa/000077500000000000000000000000001503003570500156155ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/jwt/jwa/ecdsa_spec.rb000066400000000000000000000075631503003570500202460ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWA::Ecdsa do describe '.curve_by_name' do subject { described_class.curve_by_name(curve_name) } context 'when secp256r1 is given' do let(:curve_name) { 'secp256r1' } it { is_expected.to eq(algorithm: 'ES256', digest: 'sha256') } end context 'when prime256v1 is given' do let(:curve_name) { 'prime256v1' } it { is_expected.to eq(algorithm: 'ES256', digest: 'sha256') } end context 'when secp521r1 is given' do let(:curve_name) { 'secp521r1' } it { is_expected.to eq(algorithm: 'ES512', digest: 'sha512') } end context 'when secp256k1 is given' do let(:curve_name) { 'secp256k1' } it { is_expected.to eq(algorithm: 'ES256K', digest: 'sha256') } end context 'when unknown is given' do let(:curve_name) { 'unknown' } it 'raises an error' do expect { subject }.to raise_error(JWT::UnsupportedEcdsaCurve) end end end let(:ecdsa_key) { test_pkey('ec256-private.pem') } let(:data) { 'test data' } let(:instance) { described_class.new('ES256', 'sha256') } let(:signature) { instance.sign(data: data, signing_key: ecdsa_key) } describe '#verify' do context 'when the verification key is valid' do it 'returns true for a valid signature' do expect(instance.verify(data: data, signature: signature, verification_key: ecdsa_key)).to be true end it 'returns false for an invalid signature' do expect(instance.verify(data: data, signature: 'invalid_signature', verification_key: ecdsa_key)).to be false end end context 'when verification results in a OpenSSL::PKey::PKeyError error' do it 'raises a JWT::VerificationError' do allow(ecdsa_key).to receive(:dsa_verify_asn1).and_raise(OpenSSL::PKey::PKeyError.new('Error')) expect do instance.verify(data: data, signature: '', verification_key: ecdsa_key) end.to raise_error(JWT::VerificationError, 'Signature verification raised') end end context 'when the verification key is not an OpenSSL::PKey::EC instance' do it 'raises a JWT::DecodeError' do expect do instance.verify(data: data, signature: '', verification_key: 'not_a_key') end.to raise_error(JWT::DecodeError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance') end end context 'when the verification key is a point' do it 'verifies the signature' do expect(ecdsa_key.public_key).to be_a(OpenSSL::PKey::EC::Point) expect(instance.verify(data: data, signature: signature, verification_key: ecdsa_key.public_key)).to be(true) end end end describe '#sign' do context 'when the signing key is valid' do it 'returns a valid signature' do expect(signature).to be_a(String) expect(signature.length).to be > 0 end end context 'when the signing key is a public key' do it 'raises a JWT::DecodeError' do public_key = test_pkey('ec256-public.pem') expect do instance.sign(data: data, signing_key: public_key) end.to raise_error(JWT::EncodeError, 'The given key is not a private key') end end context 'when the signing key is not an OpenSSL::PKey::EC instance' do it 'raises a JWT::DecodeError' do expect do instance.sign(data: data, signing_key: 'not_a_key') end.to raise_error(JWT::EncodeError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance') end end context 'when the signing key is invalid' do it 'raises a JWT::DecodeError' do invalid_key = OpenSSL::PKey::EC.generate('sect571r1') expect do instance.sign(data: data, signing_key: invalid_key) end.to raise_error(JWT::DecodeError, "The ECDSA curve 'sect571r1' is not supported") end end end end ruby-jwt-3.1.2/spec/jwt/jwa/hmac_spec.rb000066400000000000000000000107011503003570500200630ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWA::Hmac do let(:instance) { described_class.new('HS256', OpenSSL::Digest::SHA256) } let(:valid_signature) { [60, 56, 87, 72, 185, 194, 150, 13, 18, 148, 76, 245, 94, 91, 201, 64, 111, 91, 167, 156, 43, 148, 41, 113, 168, 156, 137, 12, 11, 31, 58, 97].pack('C*') } let(:hmac_secret) { 'secret_key' } describe '#sign' do subject { instance.sign(data: 'test', signing_key: hmac_secret) } context 'when signing with a key' do it { is_expected.to eq(valid_signature) } end # Address OpenSSL 3.0 errors with empty hmac_secret - https://github.com/jwt/ruby-jwt/issues/526 context 'when nil hmac_secret is passed' do let(:hmac_secret) { nil } context 'when OpenSSL 3.0 raises a malloc failure' do before do allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure')) end it 'raises JWT::DecodeError' do expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret') end end context 'when OpenSSL raises any other error' do before do allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error')) end it 'raises the original error' do expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error') end end context 'when other versions of openssl do not raise an exception' do let(:response) { Base64.decode64("Q7DO+ZJl+eNMEOqdNQGSbSezn1fG1nRWHYuiNueoGfs=\n") } before do allow(OpenSSL::HMAC).to receive(:digest).and_return(response) end it { is_expected.to eql(response) } end end context 'when blank hmac_secret is passed' do let(:hmac_secret) { '' } context 'when OpenSSL 3.0 raises a malloc failure' do before do allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure')) end it 'raises JWT::DecodeError' do expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret') end end context 'when OpenSSL raises any other error' do before do allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error')) end it 'raises the original error' do expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error') end end context 'when other versions of openssl do not raise an exception' do let(:response) { Base64.decode64("Q7DO+ZJl+eNMEOqdNQGSbSezn1fG1nRWHYuiNueoGfs=\n") } before do allow(OpenSSL::HMAC).to receive(:digest).and_return(response) end it { is_expected.to eql(response) } end end context 'when hmac_secret is passed' do let(:hmac_secret) { 'test' } context 'when OpenSSL 3.0 raises a malloc failure' do before do allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure')) end it 'raises the original error' do expect { subject }.to raise_error(OpenSSL::HMACError, 'EVP_PKEY_new_mac_key: malloc failure') end end context 'when OpenSSL raises any other error' do before do allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error')) end it 'raises the original error' do expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error') end end context 'when other versions of openssl do not raise an exception' do let(:response) { Base64.decode64("iM0hCLU0fZc885zfkFPX3UJwSHbYyam9ji0WglnT3fc=\n") } before do allow(OpenSSL::HMAC).to receive(:digest).and_return(response) end it { is_expected.to eql(response) } end end end describe '#verify' do subject { instance.verify(data: 'test', signature: signature, verification_key: hmac_secret) } context 'when signature is valid' do let(:signature) { valid_signature } it { is_expected.to be(true) } end context 'when signature is invalid' do let(:signature) { [60, 56, 87, 72, 185, 194].pack('C*') } it { is_expected.to be(false) } end end end ruby-jwt-3.1.2/spec/jwt/jwa/none_spec.rb000066400000000000000000000005471503003570500201210ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWA::None do subject { described_class.new } describe '#sign' do it 'returns an empty string' do expect(subject.sign('data', 'key')).to eq('') end end describe '#verify' do it 'returns true' do expect(subject.verify('data', 'signature', 'key')).to be true end end end ruby-jwt-3.1.2/spec/jwt/jwa/ps_spec.rb000066400000000000000000000070411503003570500176000ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWA::Ps do let(:rsa_key) { test_pkey('rsa-2048-private.pem') } let(:data) { 'test data' } let(:ps256_instance) { described_class.new('PS256') } let(:ps384_instance) { described_class.new('PS384') } let(:ps512_instance) { described_class.new('PS512') } before do skip 'OpenSSL gem missing RSA-PSS support' unless OpenSSL::PKey::RSA.method_defined?(:sign_pss) end describe '#initialize' do it 'initializes with the correct algorithm and digest' do expect(ps256_instance.instance_variable_get(:@alg)).to eq('PS256') expect(ps256_instance.send(:digest_algorithm)).to eq('sha256') expect(ps384_instance.instance_variable_get(:@alg)).to eq('PS384') expect(ps384_instance.send(:digest_algorithm)).to eq('sha384') expect(ps512_instance.instance_variable_get(:@alg)).to eq('PS512') expect(ps512_instance.send(:digest_algorithm)).to eq('sha512') end end describe '#sign' do context 'with a valid RSA key' do it 'signs the data with PS256' do expect(ps256_instance.sign(data: data, signing_key: rsa_key)).not_to be_nil end it 'signs the data with PS384' do expect(ps384_instance.sign(data: data, signing_key: rsa_key)).not_to be_nil end it 'signs the data with PS512' do expect(ps512_instance.sign(data: data, signing_key: rsa_key)).not_to be_nil end end context 'with an invalid key' do it 'raises an error' do expect do ps256_instance.sign(data: data, signing_key: 'invalid_key') end.to raise_error(JWT::EncodeError, /The given key is a String. It has to be an OpenSSL::PKey::RSA instance./) end end context 'with a key length less than 2048 bits' do let(:rsa_key) { OpenSSL::PKey::RSA.generate(2047) } it 'raises an error' do expect do ps256_instance.sign(data: data, signing_key: rsa_key) end.to raise_error(JWT::EncodeError, 'The key length must be greater than or equal to 2048 bits') end end end describe '#verify' do let(:ps256_signature) { ps256_instance.sign(data: data, signing_key: rsa_key) } let(:ps384_signature) { ps384_instance.sign(data: data, signing_key: rsa_key) } let(:ps512_signature) { ps512_instance.sign(data: data, signing_key: rsa_key) } context 'with a valid RSA key' do it 'verifies the signature with PS256' do expect(ps256_instance.verify(data: data, signature: ps256_signature, verification_key: rsa_key)).to be(true) end it 'verifies the signature with PS384' do expect(ps384_instance.verify(data: data, signature: ps384_signature, verification_key: rsa_key)).to be(true) end it 'verifies the signature with PS512' do expect(ps512_instance.verify(data: data, signature: ps512_signature, verification_key: rsa_key)).to be(true) end end context 'with an invalid signature' do it 'raises a verification error' do expect(ps256_instance.verify(data: data, signature: 'invalid_signature', verification_key: rsa_key)).to be(false) end end context 'when verification results in a OpenSSL::PKey::PKeyError error' do it 'raises a JWT::VerificationError' do allow(rsa_key).to receive(:verify_pss).and_raise(OpenSSL::PKey::PKeyError.new('Error')) expect do ps256_instance.verify(data: data, signature: ps256_signature, verification_key: rsa_key) end.to raise_error(JWT::VerificationError, 'Signature verification raised') end end end end ruby-jwt-3.1.2/spec/jwt/jwa/rsa_spec.rb000066400000000000000000000037731503003570500177530ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWA::Rsa do let(:rsa_key) { test_pkey('rsa-2048-private.pem') } let(:data) { 'test data' } let(:rsa_instance) { described_class.new('RS256') } describe '#initialize' do it 'initializes with the correct algorithm and digest' do expect(rsa_instance.instance_variable_get(:@alg)).to eq('RS256') expect(rsa_instance.send(:digest)).to eq('SHA256') end end describe '#sign' do context 'with a valid RSA key' do it 'signs the data' do signature = rsa_instance.sign(data: data, signing_key: rsa_key) expect(signature).not_to be_nil end end context 'with a key length less than 2048 bits' do let(:rsa_key) { OpenSSL::PKey::RSA.generate(2047) } it 'raises an error' do expect do rsa_instance.sign(data: data, signing_key: rsa_key) end.to raise_error(JWT::EncodeError, 'The key length must be greater than or equal to 2048 bits') end end context 'with an invalid key' do it 'raises an error' do expect do rsa_instance.sign(data: data, signing_key: 'invalid_key') end.to raise_error(JWT::EncodeError, /The given key is a String. It has to be an OpenSSL::PKey::RSA instance/) end end end describe '#verify' do let(:signature) { rsa_instance.sign(data: data, signing_key: rsa_key) } context 'with a valid RSA key' do it 'returns true' do expect(rsa_instance.verify(data: data, signature: signature, verification_key: rsa_key)).to be(true) end end context 'with an invalid signature' do it 'returns false' do expect(rsa_instance.verify(data: data, signature: 'invalid_signature', verification_key: rsa_key)).to be(false) end end context 'with an invalid key' do it 'returns false' do expect(rsa_instance.verify(data: data, signature: 'invalid_signature', verification_key: OpenSSL::PKey::RSA.generate(2048))).to be(false) end end end end ruby-jwt-3.1.2/spec/jwt/jwa/unsupported_spec.rb000066400000000000000000000010011503003570500215340ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWA::Unsupported do describe '.sign' do it 'raises an error for unsupported signing method' do expect { described_class.sign('data', 'key') }.to raise_error(JWT::EncodeError, 'Unsupported signing method') end end describe '.verify' do it 'raises an error for unsupported algorithm' do expect { described_class.verify('data', 'signature', 'key') }.to raise_error(JWT::VerificationError, 'Algorithm not supported') end end end ruby-jwt-3.1.2/spec/jwt/jwa_spec.rb000066400000000000000000000014171503003570500171570ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWA do describe '.resolve_and_sort' do let(:subject) { described_class.resolve_and_sort(algorithms: algorithms, preferred_algorithm: preferred_algorithm).map(&:alg) } context 'when algorithms have the preferred last' do let(:algorithms) { %w[HS256 HS512 RS512] } let(:preferred_algorithm) { 'RS512' } it 'places the preferred algorithm first' do is_expected.to eq(%w[RS512 HS256 HS512]) end end context 'when algorithms have the preferred in the middle' do let(:algorithms) { %w[HS512 HS256 RS512] } let(:preferred_algorithm) { 'HS256' } it 'places the preferred algorithm first' do is_expected.to eq(%w[HS256 HS512 RS512]) end end end end ruby-jwt-3.1.2/spec/jwt/jwk/000077500000000000000000000000001503003570500156275ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/jwt/jwk/decode_with_jwk_spec.rb000066400000000000000000000210141503003570500223150ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT do describe '.decode for JWK usecase' do let(:keypair) { test_pkey('rsa-2048-private.pem') } let(:jwk) { JWT::JWK.new(keypair) } let(:valid_key) { jwk.export } let(:public_jwks) { { keys: [valid_key, { kid: 'not_the_correct_one', kty: 'oct', k: 'secret' }] } } let(:token_payload) { { 'data' => 'something' } } let(:token_headers) { { kid: jwk.kid } } let(:algorithm) { 'RS512' } let(:signed_token) { described_class.encode(token_payload, jwk.signing_key, algorithm, token_headers) } context 'when JWK features are used manually' do it 'is able to decode the token' do payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm] }) do |header, _payload| JWT::JWK.import(public_jwks[:keys].find { |key| key[:kid] == header['kid'] }).verify_key end expect(payload).to eq(token_payload) end end context 'when jwk keys are given as an array' do context 'and kid is in the set' do it 'is able to decode the token' do payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) expect(payload).to eq(token_payload) end end context 'and kid is not in the set' do before do public_jwks[:keys].first[:kid] = 'NOT_A_MATCH' end it 'raises an exception' do expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( JWT::DecodeError, /Could not find public key for kid .*/ ) end end context 'and x5t is in the set' do let(:x5t) { Base64.urlsafe_encode64(OpenSSL::Digest::SHA1.new(keypair.to_der).digest, padding: false) } let(:valid_key) { jwk.export.merge({ x5t: x5t }) } let(:token_headers) { { x5t: x5t } } it 'is able to decode the token' do payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks, key_fields: [:x5t] }) expect(payload).to eq(token_payload) end end context 'and both kid and x5t is in the set' do let(:x5t) { Base64.urlsafe_encode64(OpenSSL::Digest::SHA1.new(keypair.to_der).digest, padding: false) } let(:valid_key) { jwk.export.merge({ x5t: x5t }) } let(:token_headers) { { x5t: x5t, kid: 'NOT_A_MATCH' } } it 'is able to decode the token based on the priority of the key defined in key_fields' do payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks, key_fields: %i[x5t kid] }) expect(payload).to eq(token_payload) end end context 'no keys are found in the set' do let(:public_jwks) { { keys: [] } } it 'raises an exception' do expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( JWT::DecodeError, /No keys found in jwks/ ) end end context 'token does not know the kid' do let(:token_headers) { {} } it 'raises an exception' do expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( JWT::DecodeError, 'No key id (kid) or x5t found from token headers' ) end end end context 'when jwk keys are loaded using a proc/lambda' do it 'decodes the token' do payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: ->(_opts) { public_jwks } }) expect(payload).to eq(token_payload) end end context 'when jwk keys are rotated' do it 'decodes the token' do key_loader = ->(options) { options[:invalidate] ? public_jwks : { keys: [] } } payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: key_loader }) expect(payload).to eq(token_payload) end end context 'when jwk keys are loaded from JSON with string keys' do it 'decodes the token' do key_loader = ->(_options) { JSON.parse(JSON.generate(public_jwks)) } payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: key_loader }) expect(payload).to eq(token_payload) end end context 'when the token kid is nil' do let(:token_headers) { {} } context 'and allow_nil_kid is specified' do it 'decodes the token' do key_loader = ->(_options) { JSON.parse(JSON.generate(public_jwks)) } payload, _header = described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: key_loader, allow_nil_kid: true }) expect(payload).to eq(token_payload) end end end context 'when the token kid is not a string' do let(:token_headers) { { kid: 5 } } it 'raises an exception' do expect { described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: public_jwks }) }.to raise_error( JWT::DecodeError, 'Invalid type for kid header parameter' ) end end context 'mixing algorithms using kid header' do let(:hmac_jwk) { JWT::JWK.new('secret') } let(:rsa_jwk) { JWT::JWK.new(test_pkey('rsa-2048-private.pem')) } let(:ec_jwk_secp384r1) { JWT::JWK.new(test_pkey('ec384-private.pem')) } let(:ec_jwk_secp521r1) { JWT::JWK.new(test_pkey('ec384-private.pem')) } let(:jwks) { { keys: [hmac_jwk.export(include_private: true), rsa_jwk.export, ec_jwk_secp384r1.export, ec_jwk_secp521r1.export] } } context 'when RSA key is pointed to as HMAC secret' do let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, 'is not really relevant in the scenario', 'HS256', { kid: rsa_jwk.kid }) } it 'raises JWT::DecodeError' do expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') end end context 'when EC key is pointed to as HMAC secret' do let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, 'is not really relevant in the scenario', 'HS256', { kid: ec_jwk_secp384r1.kid }) } it 'raises JWT::DecodeError' do expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') end end context 'when EC key is pointed to as RSA public key' do let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.signing_key, algorithm, { kid: ec_jwk_secp384r1.kid }) } it 'fails in some way' do expect { described_class.decode(signed_token, nil, true, algorithms: [algorithm], jwks: jwks) }.to( raise_error(JWT::VerificationError, 'Signature verification raised') ) end end context 'when HMAC secret is pointed to as RSA public key' do let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.signing_key, algorithm, { kid: hmac_jwk.kid }) } it 'fails in some way' do expect { described_class.decode(signed_token, nil, true, algorithms: [algorithm], jwks: jwks) }.to( raise_error(NoMethodError, /undefined method .*verify/) ) end end context 'when HMAC secret is pointed to as EC public key' do let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES384', { kid: hmac_jwk.kid }) } it 'fails in some way' do expect { described_class.decode(signed_token, nil, true, algorithms: ['ES384'], jwks: jwks) }.to( raise_error(JWT::DecodeError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance') ) end end context 'when ES384 key is pointed to as ES512 key' do let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES512', { kid: ec_jwk_secp521r1.kid }) } it 'fails in some way' do expect { described_class.decode(signed_token, nil, true, algorithms: ['ES512'], jwks: jwks) }.to( raise_error(JWT::IncorrectAlgorithm, 'payload algorithm is ES512 but ES384 signing key was provided') ) end end end end end ruby-jwt-3.1.2/spec/jwt/jwk/ec_spec.rb000066400000000000000000000226311503003570500175610ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWK::EC do let(:ec_key) { test_pkey('ec384-private.pem') } describe '.new' do subject { described_class.new(keypair) } context 'when a keypair with both keys given' do let(:keypair) { ec_key } it 'creates an instance of the class' do expect(subject).to be_a described_class expect(subject.private?).to eq true end end context 'when a keypair with only public key is given' do let(:keypair) { test_pkey('ec256-public.pem') } it 'creates an instance of the class' do expect(subject).to be_a described_class expect(subject.private?).to eq false end end context 'when a number is given' do let(:keypair) { 1234 } it 'raises an argument error' do expect { subject }.to raise_error(ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters') end end context 'when EC with unsupported curve is given' do let(:keypair) { OpenSSL::PKey::EC.generate('prime239v2') } it 'raises an error' do expect { subject }.to raise_error(JWT::JWKError, "Unsupported curve 'prime239v2'") end end end describe '#keypair' do subject(:jwk) { described_class.new(ec_key) } it 'returns the key' do expect(jwk.keypair).to eq(ec_key) end end describe '#public_key' do subject(:jwk) { described_class.new(ec_key) } it 'returns the key' do expect(jwk.public_key).to eq(ec_key) end end describe '#export' do let(:kid) { nil } subject { described_class.new(keypair, kid).export } context 'when keypair with private key is exported' do let(:keypair) { ec_key } it 'returns a hash with the both parts of the key' do expect(subject).to be_a Hash expect(subject).to include(:kty, :kid, :x, :y) # Exported keys do not currently include private key info, # event if the in-memory key had that information. This is # done to match the traditional behavior of RSA JWKs. ## expect(subject).to include(:d) end end context 'when keypair with public key is exported' do let(:keypair) { test_pkey('ec256-public.pem') } it 'returns a hash with the public parts of the key' do expect(subject).to be_a Hash expect(subject).to include(:kty, :kid, :x, :y) # Don't include private `d` if not explicitly requested. expect(subject).not_to include(:d) end context 'when a custom "kid" is provided' do let(:kid) { 'custom_key_identifier' } it 'exports it' do expect(subject[:kid]).to eq 'custom_key_identifier' end end end context 'when private key is requested' do subject { described_class.new(keypair).export(include_private: true) } let(:keypair) { ec_key } it 'returns a hash with the both parts of the key' do expect(subject).to be_a Hash expect(subject).to include(:kty, :kid, :x, :y) # `d` is the private part. expect(subject).to include(:d) end end context 'when a common parameter is given' do let(:parameters) { { use: 'sig' } } let(:keypair) { ec_key } subject { described_class.new(keypair, parameters).export } it 'returns a hash including the common parameter' do expect(subject).to include(:use) end end end describe '#verify' do let(:data) { 'data_to_sign' } let(:signature) { jwk.sign(data: data) } context 'when jwk is missing the alg parameter' do let(:jwk) { described_class.new(ec_key) } context 'when the signature is valid' do it 'returns true' do expect(jwk.verify(data: data, signature: signature)).to be(true) end end end context 'when jwk has alg parameter' do let(:jwk) { described_class.new(ec_key, alg: 'ES384') } context 'when the signature is valid' do it 'returns true' do expect(jwk.verify(data: data, signature: signature)).to be(true) end end context 'when the signature is invalid' do it 'returns false' do expect(jwk.verify(data: data, signature: 'invalid')).to be(false) end end end context 'when the jwk has an invalid alg header' do let(:rsa) { described_class.new(ec_key, alg: 'INVALID') } it 'raises JWT::VerificationError' do expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::VerificationError, 'Algorithm not supported') end end context 'when the jwk has none as the alg parameter' do let(:rsa) { described_class.new(ec_key, alg: 'none') } it 'raises JWT::JWKError' do expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::JWKError, 'none algorithm usage not supported via JWK') end end context 'when the jwk has HS256 as the alg parameter' do let(:rsa) { described_class.new(ec_key, alg: 'HS256') } it 'raises JWT::DecodeError' do expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') end end end describe '.to_openssl_curve' do context 'when a valid curve name is given' do it 'returns the corresponding OpenSSL curve name' do expect(JWT::JWK::EC.to_openssl_curve('P-256')).to eq('prime256v1') expect(JWT::JWK::EC.to_openssl_curve('P-384')).to eq('secp384r1') expect(JWT::JWK::EC.to_openssl_curve('P-521')).to eq('secp521r1') expect(JWT::JWK::EC.to_openssl_curve('P-256K')).to eq('secp256k1') end end context 'when an invalid curve name is given' do it 'raises an error' do expect { JWT::JWK::EC.to_openssl_curve('invalid-curve') }.to raise_error(JWT::JWKError, 'Invalid curve provided') end end end describe '.import' do subject { described_class.import(params) } let(:include_private) { false } let(:exported_key) { described_class.new(keypair).export(include_private: include_private) } %w[P-256 P-384 P-521 P-256K].each do |crv| context "when crv=#{crv}" do let(:openssl_curve) { JWT::JWK::EC.to_openssl_curve(crv) } let(:ec_key) { OpenSSL::PKey::EC.generate(openssl_curve) } context 'when keypair is private' do let(:include_private) { true } let(:keypair) { ec_key } let(:params) { exported_key } it 'returns a private key' do expect(subject.private?).to eq true expect(subject).to be_a described_class # Regular export returns only the non-private parts. public_only = exported_key.reject { |k, _v| k == :d } expect(subject.export).to eq(public_only) # Private export returns the original input. expect(subject.export(include_private: true)).to eq(exported_key) end context 'with a custom "kid" value' do let(:exported_key) do super().merge(kid: 'custom_key_identifier') end it 'imports that "kid" value' do expect(subject.kid).to eq('custom_key_identifier') end end end context 'when keypair is public' do context 'returns a public key' do let(:keypair) { test_pkey('ec256-public.pem') } let(:params) { exported_key } it 'returns a hash with the public parts of the key' do expect(subject).to be_a described_class expect(subject.private?).to eq false expect(subject.export).to eq(exported_key) end end end end context 'with missing 0-byte at the start of EC coordinates' do let(:example_keysets) do [ '{"kty":"EC","crv":"P-256","x":"0Nv5IKAlkvXuAKmOmFgmrwXKR7qGePOzu_7RXg5msw","y":"FqnPSNutcjfvXNlufwb7nLJuUEnBkbMdZ3P79nY9c3k"}', '{"kty":"EC","crv":"P-256","x":"xGjPg-7meZamM_yfkGeBUB2eJ5c82Y8vQdXwi5cVGw","y":"9FwKAuJacVyEy71yoVn1u1ETsQoiwF7QfkfXURGxg14"}', '{"kty":"EC","crv":"P-256","x":"yTvy0bwt5s29mIg1DMq-IjZH4pDgZIN9keEEaSuWZhk","y":"a0nrmd8qz8jpZDgpY82Rgv3vZ5xiJuiAoMIuRlGnaw"}', '{"kty":"EC","crv":"P-256","x":"yJen7AW4lLUTMH4luDj0wlMNSGCuOBB5R-ZoxlAU_g","y":"aMbA-M6ORHePSatiPVz_Pzu7z2XRnKMzK-HIscpfud8"}', '{"kty":"EC","crv":"P-256","x":"p_D00Z1ydC7mBIpSKPUUrzVzY9Fr5NMhhGfnf4P9guw","y":"lCqM3B_s04uhm7_91oycBvoWzuQWJCbMoZc46uqHXA"}', '{"kty":"EC","crv":"P-256","x":"hKS-vxV1bvfZ2xOuHv6Qt3lmHIiArTnhWac31kXw3w","y":"f_UWjrTpmq_oTdfss7YJ-9dEiYw_JC90kwAE-y0Yu-w"}', '{"kty":"EC","crv":"P-256","x":"3W22hN16OJN1XPpUQuCxtwoBRlf-wGyBNIihQiTmSdI","y":"eUaveaPQ4CpyfY7sfCqEF1DCOoxHdMpPHW15BmUF0w"}', '{"kty":"EC","crv":"P-256","x":"oq_00cGL3SxUZTA-JvcXALhfQya7elFuC7jcJScN7Bs","y":"1nNPIinv_gQiwStfx7vqs7Vt_MSyzoQDy9sCnZlFfg"}', '{"crv":"P-521","kty":"EC","x":"AMNQr/q+YGv4GfkEjrXH2N0+hnGes4cCqahJlV39m3aJpqSK+uiAvkRE5SDm2bZBc3YHGzhDzfMTUpnvXwjugUQP","y":"fIwouWsnp44Fjh2gBmO8ZafnpXZwLOCoaT5itu/Q4Z6j3duRfqmDsqyxZueDA3Gaac2LkbWGplT7mg4j7vCuGsw="}' ] end it 'prepends a 0-byte to either X or Y coordinate so that the keys decode correctly' do example_keysets.each do |keyset_json| jwk = described_class.import(JSON.parse(keyset_json)) expect(jwk).to be_kind_of(JWT::JWK::EC) end end end end end end ruby-jwt-3.1.2/spec/jwt/jwk/hmac_spec.rb000066400000000000000000000105071503003570500201010ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWK::HMAC do let(:hmac_key) { 'secret-key' } let(:key) { hmac_key } subject(:jwk) { described_class.new(key) } describe '.new' do context 'when a secret key given' do it 'creates an instance of the class' do expect(jwk).to be_a described_class expect(jwk.private?).to eq true end end context 'when key is a number' do let(:key) { 123 } it 'raises an ArgumentError' do expect { jwk }.to raise_error(ArgumentError, 'key must be of type String or Hash with key parameters') end end end describe '#keypair' do it 'returns a string' do expect(jwk.keypair).to eq(key) end end describe '#export' do let(:kid) { nil } context 'when key is exported' do let(:key) { hmac_key } subject { described_class.new(key, kid).export } it 'returns a hash with the key' do expect(subject).to be_a Hash expect(subject).to include(:kty, :kid) end end context 'when key is exported with private key' do let(:key) { hmac_key } subject { described_class.new(key, kid).export(include_private: true) } it 'returns a hash with the key' do expect(subject).to be_a Hash expect(subject).to include(:kty, :kid, :k) end end end describe '.import' do subject { described_class.import(params) } let(:exported_key) { described_class.new(key).export(include_private: true) } context 'when secret key is given' do let(:key) { hmac_key } let(:params) { exported_key } it 'returns a key' do expect(subject).to be_a described_class expect(subject.export(include_private: true)).to eq(exported_key) end context 'with a custom "kid" value' do let(:exported_key) do super().merge(kid: 'custom_key_identifier') end it 'imports that "kid" value' do expect(subject.kid).to eq('custom_key_identifier') end end context 'with a common parameter' do let(:exported_key) do super().merge(use: 'sig') end it 'imports that common parameter' do expect(subject[:use]).to eq('sig') end end end context 'when example from RFC' do let(:params) { { kty: 'oct', k: 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow' } } it 'decodes the k' do expected_key = "\x03#5K+\x0F\xA5\xBC\x83~\x06ew{\xA6\x8FZ\xB3(\xE6\xF0T\xC9(\xA9\x0F\x84\xB2\xD2P.\xBF\xD3\xFBZ\x92\xD2\x06G\xEF\x96\x8A\xB4\xC3wb=\"=.!r\x05.O\b\xC0\xCD\x9A\xF5g\xD0\x80\xA3".dup.force_encoding('ASCII-8BIT') expect(subject.verify_key).to eq(expected_key) end end end describe '#[]=' do context 'when k is given' do it 'raises an error' do expect { jwk[:k] = 'new_secret' }.to raise_error(ArgumentError, 'cannot overwrite cryptographic key attributes') end end end describe '#==' do it 'is equal to itself' do other = jwk expect(jwk == other).to eq true end it 'is equal to a clone of itself' do other = jwk.clone expect(jwk == other).to eq true end it 'is not equal to nil' do other = nil expect(jwk == other).to eq false end it 'is not equal to boolean true' do other = true expect(jwk == other).to eq false end it 'is not equal to a non-key' do other = Object.new expect(jwk == other).to eq false end it 'is not equal to a different key' do other = described_class.new('other-key') expect(jwk == other).to eq false end end describe '#<=>' do it 'is equal to itself' do other = jwk expect(jwk <=> other).to eq 0 end it 'is equal to a clone of itself' do other = jwk.clone expect(jwk <=> other).to eq 0 end it 'is not comparable to nil' do other = nil expect(jwk <=> other).to eq nil end it 'is not comparable to boolean true' do other = true expect(jwk <=> other).to eq nil end it 'is not comparable to a non-key' do other = Object.new expect(jwk <=> other).to eq nil end it 'is not equal to a different key' do other = described_class.new('other-key') expect(jwk <=> other).not_to eq 0 end end end ruby-jwt-3.1.2/spec/jwt/jwk/rsa_spec.rb000066400000000000000000000241311503003570500177540ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWK::RSA do let(:rsa_key) { OpenSSL::PKey::RSA.new(2048) } describe '.new' do subject { described_class.new(keypair) } context 'when a keypair with both keys given' do let(:keypair) { rsa_key } it 'creates an instance of the class' do expect(subject).to be_a described_class expect(subject.private?).to eq true end end context 'when a keypair with only public key is given' do let(:keypair) { rsa_key.public_key } it 'creates an instance of the class' do expect(subject).to be_a described_class expect(subject.private?).to eq false end end end describe '#keypair' do subject(:jwk) { described_class.new(rsa_key) } it 'warns to stderr' do expect(jwk.keypair).to eq(rsa_key) end end describe '#export' do subject { described_class.new(keypair).export } context 'when keypair with private key is exported' do let(:keypair) { rsa_key } it 'returns a hash with the public parts of the key' do expect(subject).to be_a Hash expect(subject).to include(:kty, :n, :e, :kid) expect(subject).not_to include(:d, :p, :dp, :dq, :qi) end end context 'when keypair with public key is exported' do let(:keypair) { rsa_key.public_key } it 'returns a hash with the public parts of the key' do expect(subject).to be_a Hash expect(subject).to include(:kty, :n, :e, :kid) expect(subject).not_to include(:d, :p, :dp, :dq, :qi) end end context 'when unsupported keypair is given' do let(:keypair) { 'key' } it 'raises an error' do expect { subject }.to raise_error(ArgumentError) end end context 'when private key is requested' do subject { described_class.new(keypair).export(include_private: true) } let(:keypair) { rsa_key } it 'returns a hash with the public AND private parts of the key' do expect(subject).to be_a Hash expect(subject).to include(:kty, :n, :e, :kid, :d, :p, :q, :dp, :dq, :qi) end end end describe '.kid' do context 'when configuration says to use :rfc7638_thumbprint' do before do JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint end it 'generates the kid based on the thumbprint' do expect(described_class.new(OpenSSL::PKey::RSA.new(2048)).kid.size).to eq(43) end end context 'when kid is given as a String parameter' do it 'uses the given kid' do expect(described_class.new(OpenSSL::PKey::RSA.new(2048), 'given').kid).to eq('given') end end context 'when kid is given in a hash parameter' do it 'uses the given kid' do expect(described_class.new(OpenSSL::PKey::RSA.new(2048), kid: 'given').kid).to eq('given') end end end describe '#verify' do let(:rsa) { described_class.new(rsa_key, alg: 'RS256') } let(:data) { 'data_to_sign' } let(:signature) { rsa.sign(data: data) } context 'when the signature is valid' do it 'returns true' do expect(rsa.verify(data: data, signature: signature)).to be(true) end end context 'when the signature is invalid' do it 'returns false' do expect(rsa.verify(data: data, signature: 'invalid_signature')).to be(false) end end context 'when the jwk is missing the alg header' do let(:rsa) { described_class.new(rsa_key) } it 'raises JWT::JWKError' do expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::JWKError, 'Could not resolve the JWA, the "alg" parameter is missing') end end context 'when the jwk has an invalid alg header' do let(:rsa) { described_class.new(rsa_key, alg: 'INVALID') } it 'raises JWT::VerificationError' do expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::VerificationError, 'Algorithm not supported') end end context 'when the jwk has none as the alg parameter' do let(:rsa) { described_class.new(rsa_key, alg: 'none') } it 'raises JWT::JWKError' do expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::JWKError, 'none algorithm usage not supported via JWK') end end context 'when the jwk has HS256 as the alg parameter' do let(:rsa) { described_class.new(rsa_key, alg: 'HS256') } it 'raises JWT::DecodeError' do expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') end end end describe '.common_parameters' do context 'when a common parameters hash is given' do it 'imports the common parameter' do expect(described_class.new(OpenSSL::PKey::RSA.new(2048), use: 'sig')[:use]).to eq('sig') end it 'converts string keys to symbol keys' do expect(described_class.new(OpenSSL::PKey::RSA.new(2048), { 'use' => 'sig' })[:use]).to eq('sig') end end end describe '.import' do subject { described_class.import(params) } let(:exported_key) { described_class.new(rsa_key).export } context 'when keypair is imported with symbol keys' do let(:params) { { kty: 'RSA', e: exported_key[:e], n: exported_key[:n] } } it 'returns a hash with the public parts of the key' do expect(subject).to be_a described_class expect(subject.private?).to eq false expect(subject.export).to eq(exported_key) end end context 'when keypair is imported with string keys from JSON' do let(:params) { { 'kty' => 'RSA', 'e' => exported_key[:e], 'n' => exported_key[:n] } } it 'returns a hash with the public parts of the key' do expect(subject).to be_a described_class expect(subject.private?).to eq false expect(subject.export).to eq(exported_key) end end context 'when private key is included in the data' do let(:exported_key) { described_class.new(rsa_key).export(include_private: true) } let(:params) { exported_key } it 'creates a complete keypair' do expect(subject).to be_a described_class expect(subject.private?).to eq true end end context 'when jwk_data is given without e and/or n' do let(:params) { { kty: 'RSA' } } it 'raises an error' do expect { subject }.to raise_error(JWT::JWKError, 'Key format is invalid for RSA') end end end shared_examples 'creating an RSA object from complete JWK parameters' do let(:rsa_parameters) { jwk_parameters.transform_values { |value| described_class.decode_open_ssl_bn(value) } } let(:all_jwk_parameters) { described_class.new(rsa_key).export(include_private: true) } context 'when public parameters (e, n) are given' do let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n) } it 'creates a valid RSA object representing a public key' do expect(subject).to be_a(OpenSSL::PKey::RSA) expect(subject.private?).to eq(false) end end context 'when only e, n, d, p and q are given' do let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d, :p, :q) } it 'raises an error telling all the exponents are required' do expect { subject }.to raise_error(JWT::JWKError, 'When one of p, q, dp, dq or qi is given all the other optimization parameters also needs to be defined') end end context 'when all key components n, e, d, p, q, dp, dq, qi are given' do let(:jwk_parameters) { all_jwk_parameters.slice(:n, :e, :d, :p, :q, :dp, :dq, :qi) } it 'creates a valid RSA object representing a public key' do expect(subject).to be_a(OpenSSL::PKey::RSA) expect(subject.private?).to eq(true) end end end shared_examples 'creating an RSA object from partial JWK parameters' do context 'when e, n, d is given' do let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d) } before do skip 'OpenSSL prior to 2.2 does not seem to support partial parameters' if JWT.openssl_version < Gem::Version.new('2.2') end it 'creates a valid RSA object representing a private key' do expect(subject).to be_a(OpenSSL::PKey::RSA) expect(subject.private?).to eq(true) end it 'can be used for encryption and decryption' do expect(subject.private_decrypt(subject.public_encrypt('secret'))).to eq('secret') end it 'can be used for signing and verification' do data = 'data_to_sign' signature = subject.sign(OpenSSL::Digest.new('SHA512'), data) expect(subject.verify(OpenSSL::Digest.new('SHA512'), signature, data)).to eq(true) end end end describe '.create_rsa_key_using_der' do subject(:rsa) { described_class.create_rsa_key_using_der(rsa_parameters) } include_examples 'creating an RSA object from complete JWK parameters' context 'when e, n, d is given' do let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d) } it 'expects all CRT parameters given and raises error' do expect { subject }.to raise_error(JWT::JWKError, 'Creating a RSA key with a private key requires the CRT parameters to be defined') end end end describe '.create_rsa_key_using_sets' do before do skip 'OpenSSL without the RSA#set_key method not supported' unless OpenSSL::PKey::RSA.new.respond_to?(:set_key) skip 'OpenSSL 3.0 does not allow mutating objects anymore' if JWT.openssl_3? end subject(:rsa) { described_class.create_rsa_key_using_sets(rsa_parameters) } include_examples 'creating an RSA object from complete JWK parameters' include_examples 'creating an RSA object from partial JWK parameters' end describe '.create_rsa_key_using_accessors' do before do skip 'OpenSSL if RSA#d= is not available there is no accessors anymore' unless OpenSSL::PKey::RSA.new.respond_to?(:d=) end subject(:rsa) { described_class.create_rsa_key_using_accessors(rsa_parameters) } include_examples 'creating an RSA object from complete JWK parameters' include_examples 'creating an RSA object from partial JWK parameters' end end ruby-jwt-3.1.2/spec/jwt/jwk/set_spec.rb000066400000000000000000000116101503003570500177600ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWK::Set do describe '.new' do it 'can create an empty set' do expect(described_class.new.keys).to eql([]) end context 'can create a set' do it 'from a JWK' do jwk = JWT::JWK.new 'testkey' expect(described_class.new(jwk).keys).to eql([jwk]) end it 'from a JWKS hash with symbol keys' do jwks = { keys: [{ kty: 'oct', k: Base64.strict_encode64('testkey') }] } jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) expect(described_class.new(jwks).keys).to eql([jwk]) end it 'from a JWKS hash with string keys' do jwks = { 'keys' => [{ 'kty' => 'oct', 'k' => Base64.strict_encode64('testkey') }] } jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) expect(described_class.new(jwks).keys).to eql([jwk]) end it 'from an array of keys' do jwk = JWT::JWK.new 'testkey' expect(described_class.new([jwk]).keys).to eql([jwk]) end it 'from an existing JWT::JWK::Set' do jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwks = described_class.new(jwk) expect(described_class.new(jwks)).to eql(jwks) end end it 'raises an error on invalid inputs' do expect { described_class.new(42) }.to raise_error(ArgumentError) end end describe '.export' do it 'exports the JWKS to Hash' do jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwks = described_class.new(jwk) exported = jwks.export expect(exported[:keys].size).to eql(1) expect(exported[:keys][0]).to eql(jwk.export) end end describe '.eql?' do it 'correctly classifies equal sets' do jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwks1 = described_class.new(jwk) jwks2 = described_class.new(jwk) expect(jwks1).to eql(jwks2) end it 'correctly classifies different sets' do jwk1 = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwk2 = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkex') }) jwks1 = described_class.new(jwk1) jwks2 = described_class.new(jwk2) expect(jwks1).not_to eql(jwks2) end end # TODO: No idea why this does not work. eql? returns true for the two elements, # but Array#uniq! doesn't recognize this, despite the documentation saying otherwise describe '.uniq!' do it 'filters out equal keys' do jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwk2 = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwks = described_class.new([jwk, jwk2]) jwks.uniq! expect(jwks.keys.size).to eql(1) end end describe '.select!' do it 'filters the keyset' do jwks = described_class.new([]) jwks << JWT::JWK.new(test_pkey('rsa-2048-private.pem')) jwks << JWT::JWK.new(test_pkey('ec384-private.pem')) jwks.select! { |k| k[:kty] == 'RSA' } expect(jwks.size).to eql(1) expect(jwks.keys[0][:kty]).to eql('RSA') end end describe '.reject!' do it 'filters the keyset' do jwks = described_class.new([]) jwks << JWT::JWK.new(test_pkey('rsa-2048-private.pem')) jwks << JWT::JWK.new(test_pkey('ec384-private.pem')) jwks.reject! { |k| k[:kty] == 'RSA' } expect(jwks.size).to eql(1) expect(jwks.keys[0][:kty]).to eql('EC') end end describe '.merge' do context 'merges two JWKSs' do it 'when called via .union' do jwks1 = described_class.new(JWT::JWK.new(test_pkey('rsa-2048-private.pem'))) jwks2 = described_class.new(JWT::JWK.new(test_pkey('ec384-private.pem'))) jwks3 = jwks1.union(jwks2) expect(jwks1.size).to eql(1) expect(jwks2.size).to eql(1) expect(jwks3.size).to eql(2) expect(jwks3.keys).to include(jwks1.keys[0]) expect(jwks3.keys).to include(jwks2.keys[0]) end it 'when called via "|" operator' do jwks1 = described_class.new(JWT::JWK.new(test_pkey('rsa-2048-private.pem'))) jwks2 = described_class.new(JWT::JWK.new(test_pkey('ec384-private.pem'))) jwks3 = jwks1 | jwks2 expect(jwks1.size).to eql(1) expect(jwks2.size).to eql(1) expect(jwks3.size).to eql(2) expect(jwks3.keys).to include(jwks1.keys[0]) expect(jwks3.keys).to include(jwks2.keys[0]) end it 'when called directly' do jwks1 = described_class.new(JWT::JWK.new(test_pkey('rsa-2048-private.pem'))) jwks2 = described_class.new(JWT::JWK.new(test_pkey('ec384-private.pem'))) jwks3 = jwks1.merge(jwks2) expect(jwks1.size).to eql(2) expect(jwks2.size).to eql(1) expect(jwks3).to eql(jwks1) expect(jwks3.keys).to include(jwks2.keys[0]) end end end end ruby-jwt-3.1.2/spec/jwt/jwk/thumbprint_spec.rb000066400000000000000000000032641503003570500213670ustar00rootroot00000000000000# frozen_string_literal: true describe JWT::JWK::Thumbprint do describe '#to_s' do let(:jwk_json) { nil } let(:jwk) { JWT::JWK.import(JSON.parse(jwk_json)) } subject(:thumbprint) { described_class.new(jwk).to_s } context 'when example from RFC is given' do let(:jwk_json) do ' { "kty": "RSA", "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt' \ 'VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6' \ '4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD' \ 'W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9' \ '1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH' \ 'aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", "e": "AQAB", "alg": "RS256" } ' end it { is_expected.to eq('NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs') } end context 'when HMAC key is given' do let(:jwk_json) do ' { "kty":"oct", "alg":"HS512", "k":"B4uZ7IbZTnjdCQjUBXTpzMUznCYj3wdYDZcceeU0mLg" } ' end it { is_expected.to eq('wPf4ZF5qlzoFxsGkft4eu1iWcehgAcahZL4XPV4dT-s') } end context 'when EC key is given' do let(:jwk_json) do ' { "kty":"EC", "crv":"P-384", "x":"sbOnPOXPBULpeizfstr8b6b31QmvEnChXJNYBhXlmpGbs3vZtomBxNORYTT9Wylq", "y":"mfyY4VJDbdKGVjBSIhN9BJEq--6IPuKy3gbIr734n6Xd81lnvKslPwjB-sdGouD6" } ' end it { is_expected.to eq('dO52_we59sdR49HsGCpVzlDUQNvT3KxCTGakk4Un8qc') } end end end ruby-jwt-3.1.2/spec/jwt/jwk_spec.rb000066400000000000000000000061211503003570500171660ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::JWK do let(:rsa_key) { test_pkey('rsa-2048-private.pem') } let(:ec_key) { test_pkey('ec256k-private.pem') } describe '.import' do let(:keypair) { rsa_key.public_key } let(:exported_key) { described_class.new(keypair).export } let(:params) { exported_key } subject { described_class.import(params) } it 'creates a ::JWT::JWK::RSA instance' do expect(subject).to be_a JWT::JWK::RSA expect(subject.export).to eq(exported_key) end context 'when number is given' do let(:params) { 1234 } it 'raises an error' do expect { subject }.to raise_error(JWT::JWKError, 'Cannot create JWK from a Integer') end end context 'parsed from JSON' do let(:params) { exported_key } it 'creates a ::JWT::JWK::RSA instance from JSON parsed JWK' do expect(subject).to be_a JWT::JWK::RSA expect(subject.export).to eq(exported_key) end end context 'when keytype is not supported' do let(:params) { { kty: 'unsupported' } } it 'raises an error' do expect { subject }.to raise_error(JWT::JWKError) end end context 'when keypair with defined kid is imported' do it 'returns the predefined kid if jwt_data contains a kid' do params[:kid] = 'CUSTOM_KID' expect(subject.export).to eq(params) end end context 'when a common JWK parameter is specified' do it 'returns the defined common JWK parameter' do params[:use] = 'sig' expect(subject.export).to eq(params) end end end describe '.new' do let(:options) { nil } subject { described_class.new(keypair, options) } context 'when RSA key is given' do let(:keypair) { rsa_key } it { is_expected.to be_a JWT::JWK::RSA } end context 'when secret key is given' do let(:keypair) { 'secret-key' } it { is_expected.to be_a JWT::JWK::HMAC } end context 'when EC key is given' do let(:keypair) { ec_key } it { is_expected.to be_a JWT::JWK::EC } end context 'when kid is given' do let(:keypair) { rsa_key } let(:options) { 'CUSTOM_KID' } it 'sets the kid' do expect(subject.kid).to eq(options) end end context 'when a common parameter is given' do subject { described_class.new(keypair, params) } let(:keypair) { rsa_key } let(:params) { { 'use' => 'sig' } } it 'sets the common parameter' do expect(subject[:use]).to eq('sig') end end end describe '.[]' do let(:params) { { use: 'sig' } } let(:keypair) { rsa_key } subject { described_class.new(keypair, params) } it 'allows to read common parameters via the key-accessor' do expect(subject[:use]).to eq('sig') end it 'allows to set common parameters via the key-accessor' do subject[:use] = 'enc' expect(subject[:use]).to eq('enc') end it 'rejects key parameters as keys via the key-accessor' do expect { subject[:kty] = 'something' }.to raise_error(ArgumentError) end end end ruby-jwt-3.1.2/spec/jwt/jwt_spec.rb000066400000000000000000001027171503003570500172070ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT do let(:payload) { { 'user_id' => 'some@user.tld' } } let(:data) do { :empty_token => 'e30K.e30K.e30K', :empty_token_2_segment => 'e30K.e30K.', :invalid_header_token => 'W10.e30K.e30K', :secret => 'My$ecretK3y', :rsa_private => test_pkey('rsa-2048-private.pem'), :rsa_public => test_pkey('rsa-2048-public.pem'), :wrong_rsa_private => test_pkey('rsa-2048-wrong-public.pem'), :wrong_rsa_public => test_pkey('rsa-2048-wrong-public.pem'), 'ES256_private' => test_pkey('ec256-private.pem'), 'ES256_public' => test_pkey('ec256-public.pem'), 'ES256_private_v2' => test_pkey('ec256-private-v2.pem'), 'ES256_public_v2' => test_pkey('ec256-public-v2.pem'), 'ES384_private' => test_pkey('ec384-private.pem'), 'ES384_public' => test_pkey('ec384-public.pem'), 'ES512_private' => test_pkey('ec512-private.pem'), 'ES512_public' => test_pkey('ec512-public.pem'), 'ES256K_private' => test_pkey('ec256k-private.pem'), 'ES256K_public' => test_pkey('ec256k-public.pem'), 'NONE' => 'eyJhbGciOiJub25lIn0.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.', 'HS256' => 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.kWOVtIOpWcG7JnyJG0qOkTDbOy636XrrQhMm_8JrRQ8', 'HS384' => 'eyJhbGciOiJIUzM4NCJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.VuV4j4A1HKhWxCNzEcwc9qVF3frrEu-BRLzvYPkbWO0LENRGy5dOiBQ34remM3XH', 'HS512' => 'eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.8zNtCBTJIZTHpZ-BkhR-6sZY1K85Nm5YCKqV3AxRdsBJDt_RR-REH2db4T3Y0uQwNknhrCnZGvhNHrvhDwV1kA', 'RS256' => 'eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.eSXvWP4GViiwUALj_-qTxU68I1oM0XjgDsCZBBUri2Ghh9d75QkVDoZ_v872GaqunN5A5xcnBK0-cOq-CR6OwibgJWfOt69GNzw5RrOfQ2mz3QI3NYEq080nF69h8BeqkiaXhI24Q51joEgfa9aj5Y-oitLAmtDPYTm7vTcdGufd6AwD3_3jajKBwkh0LPSeMtbe_5EyS94nFoEF9OQuhJYjUmp7agsBVa8FFEjVw5jEgVqkvERSj5hSY4nEiCAomdVxIKBfykyi0d12cgjhI7mBFwWkPku8XIPGZ7N8vpiSLdM68BnUqIK5qR7NAhtvT7iyLFgOqhZNUQ6Ret5VpQ', 'RS384' => 'eyJhbGciOiJSUzM4NCJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.Sfgk56moPghtsjaP4so6tOy3I553mgwX-5gByMC6dX8lpeWgsxSeAd_K8IyO7u4lwYOL0DSftnqO1HEOuN1AKyBbDvaTXz3u2xNA2x4NYLdW4AZA6ritbYcKLO5BHTXw5ueMbtA1jjGXP0zI_aK2iJTMBmB8SCF88RYBUH01Tyf4PlLj98pGL-v3prZd6kZkIeRJ3326h04hslcB5HQKmgeBk24QNLIoIC-CD329HPjJ7TtGx01lj-ehTBnwVbBGzYFAyoalV5KgvL_MDOfWPr1OYHnR5s_Fm6_3Vg4u6lBljvHOrmv4Nfx7d8HLgbo8CwH4qn1wm6VQCtuDd-uhRg', 'RS512' => 'eyJhbGciOiJSUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.LIIAUEuCkGNdpYguOO5LoW4rZ7ED2POJrB0pmEAAchyTdIK4HKh1jcLxc6KyGwZv40njCgub3y72q6vcQTn7oD0zWFCVQRIDW1911Ii2hRNHuigiPUnrnZh1OQ6z65VZRU6GKs8omoBGU9vrClBU0ODqYE16KxYmE_0n4Xw2h3D_L1LF0IAOtDWKBRDa3QHwZRM9sHsHNsBuD5ye9KzDYN1YALXj64LBfA-DoCKfpVAm9NkRPOyzjR2X2C3TomOSJgqWIVHJucudKDDAZyEbO4RA5pI-UFYy1370p9bRajvtDyoBuLDCzoSkMyQ4L2DnLhx5CbWcnD7Cd3GUmnjjTA', 'ES256' => '', 'ES384' => '', 'ES512' => '', 'PS256' => '', 'PS384' => '', 'PS512' => '' } end context 'alg: NONE' do let(:alg) { 'none' } let(:encoded_token) { data['NONE'] } it 'should generate a valid token' do token = JWT.encode payload, nil, alg expect(token).to eq encoded_token end context 'decoding without verification' do it 'should decode a valid token' do jwt_payload, header = JWT.decode encoded_token, nil, false expect(header['alg']).to eq alg expect(jwt_payload).to eq payload end end context 'decoding with verification' do context 'without specifying the none algorithm' do it 'should fail to decode the token' do expect do JWT.decode encoded_token, nil, true end.to raise_error JWT::IncorrectAlgorithm end end context 'specifying the none algorithm' do context 'when the claims are valid' do it 'should decode the token' do jwt_payload, header = JWT.decode encoded_token, nil, true, { algorithms: 'none' } expect(header['alg']).to eq 'none' expect(jwt_payload).to eq payload end end context 'when the claims are invalid' do let(:encoded_token) { JWT.encode({ exp: 0 }, nil, 'none') } it 'should fail to decode the token' do expect do JWT.decode encoded_token, nil, true end.to raise_error JWT::DecodeError end end end end end %w[HS256 HS384 HS512].each do |alg| context "alg: #{alg}" do it 'should generate a valid token' do token = JWT.encode payload, data[:secret], alg expect(token).to eq data[alg] end it 'should decode a valid token' do jwt_payload, header = JWT.decode data[alg], data[:secret], true, algorithm: alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload end it 'wrong secret should raise JWT::DecodeError' do expect do JWT.decode data[alg], 'wrong_secret', true, algorithm: alg end.to raise_error JWT::VerificationError end it 'wrong secret and verify = false should not raise JWT::DecodeError' do expect do JWT.decode data[alg], 'wrong_secret', false end.not_to raise_error end end end %w[RS256 RS384 RS512].each do |alg| context "alg: #{alg}" do it 'should generate a valid token' do token = JWT.encode payload, data[:rsa_private], alg expect(token).to eq data[alg] end it 'should decode a valid token' do jwt_payload, header = JWT.decode data[alg], data[:rsa_public], true, algorithm: alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload end it 'should decode a valid token using algorithm hash string key' do jwt_payload, header = JWT.decode data[alg], data[:rsa_public], true, 'algorithm' => alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload end it 'wrong key should raise JWT::DecodeError' do key = test_pkey('rsa-2048-wrong-public.pem') expect do JWT.decode data[alg], key, true, algorithm: alg end.to raise_error JWT::DecodeError end it 'wrong key and verify = false should not raise JWT::DecodeError' do key = test_pkey('rsa-2048-wrong-public.pem') expect do JWT.decode data[alg], key, false end.not_to raise_error end end end %w[ES256 ES384 ES512 ES256K].each do |alg| before do skip 'OpenSSL gem missing RSA-PSS support' unless OpenSSL::PKey::RSA.method_defined?(:sign_pss) end context "alg: #{alg}" do before(:each) do data[alg] = JWT.encode(payload, data["#{alg}_private"], alg) end let(:wrong_key) { test_pkey('ec256-wrong-public.pem') } it 'should generate a valid token' do jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"], true, algorithm: alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload end it 'should decode a valid token' do jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"], true, algorithm: alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload end it 'wrong key should raise JWT::DecodeError' do expect do JWT.decode data[alg], wrong_key end.to raise_error JWT::DecodeError end it 'wrong key and verify = false should not raise JWT::DecodeError' do expect do JWT.decode data[alg], wrong_key, false end.not_to raise_error end end end %w[PS256 PS384 PS512].each do |alg| context "alg: #{alg}" do before(:each) do data[alg] = JWT.encode payload, data[:rsa_private], alg end let(:wrong_key) { data[:wrong_rsa_public] } it 'should generate a valid token' do token = data[alg] header, body, signature = token.split('.') expect(header).to eql(Base64.strict_encode64({ alg: alg }.to_json)) expect(body).to eql(Base64.strict_encode64(payload.to_json)) # Validate signature is made of up header and body of JWT translated_alg = alg.gsub('PS', 'sha') valid_signature = data[:rsa_public].verify_pss( translated_alg, JWT::Base64.url_decode(signature), [header, body].join('.'), salt_length: :auto, mgf1_hash: translated_alg ) expect(valid_signature).to be true end it 'should decode a valid token' do jwt_payload, header = JWT.decode data[alg], data[:rsa_public], true, algorithm: alg expect(header['alg']).to eq alg expect(jwt_payload).to eq payload end it 'wrong key should raise JWT::DecodeError' do expect do JWT.decode data[alg], wrong_key end.to raise_error JWT::DecodeError end it 'wrong key and verify = false should not raise JWT::DecodeError' do expect do JWT.decode data[alg], wrong_key, false end.not_to raise_error end end end context 'Invalid' do it 'algorithm should raise DecodeError' do expect do JWT.encode payload, 'secret', 'HS255' end.to raise_error JWT::EncodeError end it 'raises "No verification key available" error' do token = JWT.encode({}, 'foo') expect { JWT.decode(token, nil, true) }.to raise_error(JWT::DecodeError, 'No verification key available') end it 'ECDSA curve_name should raise JWT::IncorrectAlgorithm' do key = OpenSSL::PKey::EC.generate('secp256k1') expect do JWT.encode payload, key, 'ES256' end.to raise_error JWT::IncorrectAlgorithm token = JWT.encode payload, data['ES256_private'], 'ES256' expect do JWT.decode token, key end.to raise_error JWT::IncorrectAlgorithm end end context 'Verify' do context 'when key given as an array with multiple possible keys' do let(:payload) { { 'data' => 'data' } } let(:token) { JWT.encode(payload, secret, 'HS256') } let(:secret) { 'hmac_secret' } it 'should be able to verify signature when block returns multiple keys' do decoded_token = JWT.decode(token, nil, true, { algorithm: 'HS256' }) do ['not_the_secret', secret] end expect(decoded_token.first).to eq(payload) end it 'should be able to verify signature when multiple keys given as a parameter' do decoded_token = JWT.decode(token, ['not_the_secret', secret], true, { algorithm: 'HS256' }) expect(decoded_token.first).to eq(payload) end it 'should fail if only invalid keys are given' do expect do JWT.decode(token, %w[not_the_secret not_the_secret_2], true, { algorithm: 'HS256' }) end.to raise_error(JWT::VerificationError, 'Signature verification failed') end end context 'when encoded payload is used to extract key through find_key' do it 'should be able to find a key using the block passed to decode' do payload_data = { key: 'secret' } token = JWT.encode payload_data, data[:secret], 'HS256' expect do JWT.decode(token, nil, true, { algorithm: 'HS256' }) do |_headers, payload| data[payload['key'].to_sym] end end.not_to raise_error end it 'should be able to verify signature when block returns multiple keys' do iss = 'My_Awesome_Company' iss_payload = { data: 'data', iss: iss } secrets = { iss => ['hmac_secret2', data[:secret]] } token = JWT.encode iss_payload, data[:secret], 'HS256' expect do JWT.decode(token, nil, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| secrets[payload['iss']] end end.not_to raise_error end it 'should be able to find a key using the block passed to decode with iss verification' do iss = 'My_Awesome_Company' iss_payload = { data: 'data', iss: iss } secrets = { iss => data[:secret] } token = JWT.encode iss_payload, data[:secret], 'HS256' expect do JWT.decode(token, nil, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| secrets[payload['iss']] end end.not_to raise_error end it 'should be able to verify signature when block returns multiple keys with iss verification' do iss = 'My_Awesome_Company' iss_payload = { data: 'data', iss: iss } secrets = { iss => ['hmac_secret2', data[:secret]] } token = JWT.encode iss_payload, data[:secret], 'HS256' expect do JWT.decode(token, nil, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| secrets[payload['iss']] end end.not_to raise_error end it 'should be able to find a key using a block with multiple issuers' do issuers = %w[My_Awesome_Company1 My_Awesome_Company2] iss_payload = { data: 'data', iss: issuers.first } secrets = { issuers.first => data[:secret], issuers.last => 'hmac_secret2' } token = JWT.encode iss_payload, data[:secret], 'HS256' expect do JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| secrets[payload['iss']] end end.not_to raise_error end it 'should be able to verify signature when block returns multiple keys with multiple issuers' do issuers = %w[My_Awesome_Company1 My_Awesome_Company2] iss_payload = { data: 'data', iss: issuers.first } secrets = { issuers.first => [data[:secret], 'hmac_secret1'], issuers.last => 'hmac_secret2' } token = JWT.encode iss_payload, data[:secret], 'HS256' expect do JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload| secrets[payload['iss']] end end.not_to raise_error end end context 'algorithm' do it 'should raise JWT::IncorrectAlgorithm on mismatch' do token = JWT.encode payload, data[:secret], 'HS256' expect do JWT.decode token, data[:secret], true, algorithm: 'HS384' end.to raise_error JWT::IncorrectAlgorithm expect do JWT.decode token, data[:secret], true, algorithm: 'HS256' end.not_to raise_error end it 'should raise JWT::IncorrectAlgorithm on mismatch prior to kid public key network call' do token = JWT.encode payload, data[:rsa_private], 'RS256' expect do JWT.decode(token, nil, true, { algorithms: ['RS384'] }) do |_, _| # unsuccessful keyfinder public key network call here end end.to raise_error JWT::IncorrectAlgorithm expect do JWT.decode(token, nil, true, { 'algorithms' => ['RS384'] }) do |_, _| # unsuccessful keyfinder public key network call here end end.to raise_error JWT::IncorrectAlgorithm end it 'raises error when keyfinder does not find anything' do token = JWT.encode(payload, 'secret', 'HS256') expect do JWT.decode(token, nil, true, algorithm: 'HS256') do nil end end.to raise_error JWT::DecodeError, 'No verification key available' end it 'should raise JWT::IncorrectAlgorithm when algorithms array does not contain algorithm' do token = JWT.encode payload, data[:secret], 'HS512' expect do JWT.decode token, data[:secret], true, algorithms: ['HS384'] end.to raise_error JWT::IncorrectAlgorithm expect do JWT.decode token, data[:secret], true, 'algorithms' => ['HS384'] end.to raise_error JWT::IncorrectAlgorithm expect do JWT.decode token, data[:secret], true, algorithms: %w[HS512 HS384] end.not_to raise_error expect do JWT.decode token, data[:secret], true, 'algorithms' => %w[HS512 HS384] end.not_to raise_error end context 'no algorithm provided' do it 'should use the default decode algorithm' do token = JWT.encode payload, data[:rsa_public].to_s jwt_payload, header = JWT.decode token, data[:rsa_public].to_s expect(header['alg']).to eq 'HS256' expect(jwt_payload).to eq payload end end context 'token is missing algorithm' do it 'should raise JWT::IncorrectAlgorithm' do expect do JWT.decode data[:empty_token] end.to raise_error JWT::IncorrectAlgorithm end context 'invalid header format' do it 'should raise JWT::DecodeError' do expect do JWT.decode data[:invalid_header_token] end.to raise_error JWT::DecodeError end end context '2-segment token' do it 'should raise JWT::IncorrectAlgorithm' do expect do JWT.decode data[:empty_token_2_segment] end.to raise_error JWT::DecodeError end end end end context 'issuer claim' do let(:iss) { 'ruby-jwt-gem' } let(:invalid_token) { JWT.encode payload, data[:secret] } let(:token) do iss_payload = payload.merge(iss: iss) JWT.encode iss_payload, data[:secret] end it 'if verify_iss is set to false (default option) should not raise JWT::InvalidIssuerError' do expect do JWT.decode token, data[:secret], true, iss: iss, algorithm: 'HS256' end.not_to raise_error end context 'when verify_iss is set to true and no issues given' do it 'does not raise' do expect do JWT.decode(token, data[:secret], true, verify_iss: true, algorithm: 'HS256') end.not_to raise_error end end end context 'audience claim' do let(:token) { JWT.encode(payload, data[:secret]) } context 'when verify_aud is set to true and no audience given' do it 'does not raise' do expect do JWT.decode(token, data[:secret], true, verify_aud: true, algorithm: 'HS256') end.not_to raise_error end end end context 'claim verification order' do let(:token) { JWT.encode({ nbf: Time.now.to_i + 100 }, 'secret') } context 'when two claims are invalid' do it 'depends on the order of the parameters what error is raised' do expect { JWT.decode(token, 'secret', true, { verify_jti: true, verify_not_before: true }) }.to raise_error(JWT::ImmatureSignature, 'Signature nbf has not been reached') end end end end context 'a token with no segments' do it 'raises JWT::DecodeError' do expect { JWT.decode('ThisIsNotAValidJWTToken', nil, true) }.to raise_error(JWT::DecodeError, 'Not enough or too many segments') end end context 'a token with not enough segments' do it 'raises JWT::DecodeError' do token = JWT.encode('ThisIsNotAValidJWTToken', 'secret').split('.').slice(1, 2).join expect { JWT.decode(token, nil, true) }.to raise_error(JWT::DecodeError, 'Not enough or too many segments') end end context 'a token with not too many segments' do it 'raises JWT::DecodeError' do expect { JWT.decode('ThisIsNotAValidJWTToken.second.third.signature', nil, true) }.to raise_error(JWT::DecodeError, 'Not enough or too many segments') end end context 'a token with invalid Base64 segments' do it 'raises JWT::Base64DecodeError' do expect { JWT.decode('hello.there.world') }.to raise_error(JWT::Base64DecodeError, 'Invalid base64 encoding') end end context 'a token with two segments but does not require verifying' do it 'raises something else than "Not enough or too many segments"' do expect { JWT.decode('ThisIsNotAValidJWTToken.second', nil, false) }.to raise_error(JWT::Base64DecodeError, 'Invalid base64 encoding') end end it 'should not verify token even if the payload has claims' do head = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9' load = 'eyJ1c2VyX2lkIjo1NCwiZXhwIjoxNTA0MzkwODA0fQ' sign = 'Skpi6FfYMbZ-DwW9ocyRIosNMdPMAIWRLYxRO68GTQk' expect do JWT.decode([head, load, sign].join('.'), '', false) end.not_to raise_error end it 'should not raise InvalidPayload exception if payload is an array' do expect do JWT.encode(%w[my payload], 'secret') end.not_to raise_error end it 'should encode string payloads' do expect do JWT.encode 'Hello World', 'secret' end.not_to raise_error end context 'when the alg value is given as a header parameter' do it 'overrides the actual algorithm used' do headers = JSON.parse(JWT::Base64.url_decode(JWT.encode('Hello World', 'secret', 'HS256', { alg: 'HS123' }).split('.').first)) expect(headers['alg']).to eq('HS123') end it 'should generate the same token' do expect(JWT.encode('Hello World', 'secret', 'HS256', { alg: 'HS256' })).to eq JWT.encode('Hello World', 'secret', 'HS256') end end context 'when hmac algorithm is used without secret key' do it 'encodes payload' do pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression? payload = { a: 1, b: 'b' } token = JWT.encode(payload, '', 'HS256') expect do token_without_secret = JWT.encode(payload, nil, 'HS256') expect(token).to eq(token_without_secret) end.not_to raise_error end end context 'algorithm case insensitivity' do let(:payload) { { 'a' => 1, 'b' => 'b' } } it 'ignores algorithm casing during encode/decode' do enc = JWT.encode(payload, 'secret', 'hs256') expect(JWT.decode(enc, 'secret')).to eq([payload, { 'alg' => 'HS256' }]) enc = JWT.encode(payload, data[:rsa_private], 'rs512') expect(JWT.decode(enc, data[:rsa_public], true, algorithm: 'RS512')).to eq([payload, { 'alg' => 'RS512' }]) enc = JWT.encode(payload, data[:rsa_private], 'RS512') expect(JWT.decode(enc, data[:rsa_public], true, algorithm: 'rs512')).to eq([payload, { 'alg' => 'RS512' }]) end it 'raises error for invalid algorithm' do expect do JWT.encode(payload, '', 'xyz') end.to raise_error(JWT::EncodeError) end end describe '::JWT.decode with verify_iat parameter' do let!(:time_now) { Time.now } let(:token) { JWT.encode({ pay: 'load', iat: iat }, 'secret', 'HS256') } subject(:decoded_token) { JWT.decode(token, 'secret', true, verify_iat: true) } before { allow(Time).to receive(:now) { time_now } } context 'when iat is exactly the same as Time.now and iat is given as a float' do let(:iat) { time_now.to_f } it 'considers iat valid' do expect(decoded_token).to be_an(Array) end end context 'when iat is exactly the same as Time.now and iat is given as floored integer' do let(:iat) { time_now.to_f.floor } it 'considers iat valid' do expect(decoded_token).to be_an(Array) end end context 'when iat is 1 second before Time.now' do let(:iat) { time_now.to_i + 1 } it 'raises an error' do expect { decoded_token }.to raise_error(JWT::InvalidIatError, 'Invalid iat') end end end describe '::JWT.decode with x5c parameter' do let(:alg) { 'RS256' } let(:root_certificates) { [instance_double('OpenSSL::X509::Certificate')] } let(:key_finder) { instance_double('::JWT::X5cKeyFinder') } before do expect(JWT::X5cKeyFinder).to receive(:new).with(root_certificates, nil).and_return(key_finder) expect(key_finder).to receive(:from).and_return(data[:rsa_public]) end subject(:decoded_token) { JWT.decode(data[alg], nil, true, algorithm: alg, x5c: { root_certificates: root_certificates }) } it 'calls X5cKeyFinder#from to verify the signature and return the payload' do jwt_payload, header = decoded_token expect(header['alg']).to eq alg expect(jwt_payload).to eq payload end end describe 'when keyfinder given with 1 argument' do let(:token) { JWT.encode(payload, 'HS256', 'HS256') } it 'decodes the token' do expect(JWT.decode(token, nil, true, algorithm: 'HS256') { |header| header['alg'] }).to include(payload) end end describe 'when keyfinder given with 2 arguments' do let(:token) { JWT.encode(payload, payload['user_id'], 'HS256') } it 'decodes the token' do expect(JWT.decode(token, nil, true, algorithm: 'HS256') { |_header, payload| payload['user_id'] }).to include(payload) end end describe 'when keyfinder given with 3 arguments' do let(:token) { JWT.encode(payload, 'HS256', 'HS256') } it 'decodes the token but does not pass the payload' do expect(JWT.decode(token, nil, true, algorithm: 'HS256') do |header, token_payload, nothing| expect(token_payload).to eq(nil) # This behaviour is not correct, the payload should be available in the keyfinder expect(nothing).to eq(nil) header['alg'] end).to include(payload) end end describe 'when none token is and decoding without key and with verification' do let(:none_token) { JWT.encode(payload, nil, 'none') } it 'decodes the token' do expect(JWT.decode(none_token, nil, true, algorithms: 'none')).to eq([payload, { 'alg' => 'none' }]) end end describe 'when none token is decoded with a key given' do let(:none_token) { JWT.encode(payload, nil, 'none') } it 'decodes the token' do expect(JWT.decode(none_token, 'key', true, algorithms: 'none')).to eq([payload, { 'alg' => 'none' }]) end end describe 'when none token is decoded without verify' do let(:none_token) { JWT.encode(payload, nil, 'none') } it 'decodes the token' do expect(JWT.decode(none_token, 'key', false)).to eq([payload, { 'alg' => 'none' }]) end end describe 'when token signed with nil and decoded with nil' do let(:no_key_token) { JWT.encode(payload, nil, 'HS512') } it 'raises JWT::DecodeError' do pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression? expect { JWT.decode(no_key_token, nil, true, algorithms: 'HS512') }.to raise_error(JWT::DecodeError, 'No verification key available') end end context 'when token ends with a newline char' do let(:token) { "#{JWT.encode(payload, 'secret', 'HS256')}\n" } it 'raises an error' do expect { JWT.decode(token, 'secret', true, algorithm: 'HS256') }.to raise_error(JWT::Base64DecodeError, 'Invalid base64 encoding') end end context 'when token ends with a newline char and strict_decoding enabled' do let(:token) { "#{JWT.encode(payload, 'secret', 'HS256')}\n" } before do JWT.configuration.strict_base64_decoding = true end it 'raises JWT::DecodeError' do expect { JWT.decode(token, 'secret', true, algorithm: 'HS256') }.to raise_error(JWT::DecodeError, 'Invalid base64 encoding') end end context 'when multiple algorithms given' do let(:token) { JWT.encode(payload, 'secret', 'HS256') } it 'starts trying with the algorithm referred in the header' do allow(JWT::JWA::Rsa).to receive(:verify) JWT.decode(token, 'secret', true, algorithm: %w[RS512 HS256]) expect(JWT::JWA::Rsa).not_to have_received(:verify) end end context 'when keyfinder resolves to multiple keys and multiple algorithms given' do let(:iss_key_mappings) do { 'ES256' => [data['ES256_public_v2'], data['ES256_public']], 'HS256' => data['HS256'] } end context 'with issue with ES256 keys' do it 'tries until the first match' do token = JWT.encode(payload, data['ES256_private'], 'ES256', 'iss' => 'ES256') result = JWT.decode(token, nil, true, algorithm: %w[ES256 HS256]) do |header, _| iss_key_mappings[header['iss']] end expect(result).to include(payload) end it 'tries until the first match' do token = JWT.encode(payload, data['ES256_private_v2'], 'ES256', 'iss' => 'ES256') result = JWT.decode(token, nil, true, algorithm: %w[ES256 HS256]) do |header, _| iss_key_mappings[header['iss']] end expect(result).to include(payload) end end context 'with issue with HS256 keys' do it 'tries until the first match' do token = JWT.encode(payload, data['HS256'], 'HS256', 'iss' => 'HS256') result = JWT.decode(token, nil, true, algorithm: %w[ES256 HS256]) do |header, _| iss_key_mappings[header['iss']] end expect(result).to include(payload) end end end context 'when token is missing the alg header' do let(:token) { 'e30.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.DIKUOt1lwwzWSPBf508IYqk0KzC2PL97OZc6pECzE1I' } it 'raises JWT::IncorrectAlgorithm error' do expect { JWT.decode(token, 'secret', true, algorithm: 'HS256') }.to raise_error(JWT::IncorrectAlgorithm, 'Token is missing alg header') end end context 'when token has null as the alg header' do let(:token) { 'eyJhbGciOm51bGx9.eyJwYXkiOiJsb2FkIn0.pizVPWJMK-GUuXXEcQD_faZGnZqz_6wKZpoGO4RdqbY' } it 'raises JWT::IncorrectAlgorithm error' do expect { JWT.decode(token, 'secret', true, algorithm: 'HS256') }.to raise_error(JWT::IncorrectAlgorithm, 'Token is missing alg header') end end context 'when the alg is invalid' do let(:token) { 'eyJhbGciOiJIUzI1NiJ9.eyJwYXkiOiJsb2FkIn0.ZpAhTTtuo-CmbgT6-95NaM_wFckKeyI157baZ29H41o' } it 'raises JWT::IncorrectAlgorithm error' do expect { JWT.decode(token, 'secret', true, algorithm: 'invalid-HS256') }.to raise_error(JWT::IncorrectAlgorithm, 'Expected a different algorithm') end end context 'when algorithm is a custom class' do let(:custom_algorithm) do Class.new do include JWT::JWA::SigningAlgorithm def initialize(signature: 'custom_signature', alg: 'custom') @signature = signature @alg = alg end def sign(*) @signature end def verify(data:, signature:, verification_key:) # rubocop:disable Lint/UnusedMethodArgument signature == @signature end end end let(:token) { JWT.encode(payload, 'secret', custom_algorithm.new) } let(:expected_token) { 'eyJhbGciOiJjdXN0b20ifQ.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.Y3VzdG9tX3NpZ25hdHVyZQ' } it 'can be used for encoding' do expect(token).to eq(expected_token) end it 'can be used for decoding' do expect(JWT.decode(token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom' }]) end context 'when multiple custom algorithms are given for decoding' do it 'tries until the first match' do expect(JWT.decode(token, 'secret', true, algorithms: [custom_algorithm.new(signature: 'not_this'), custom_algorithm.new])).to eq([payload, { 'alg' => 'custom' }]) end end context 'when class has custom header method' do before do custom_algorithm.class_eval do def header(*) { 'alg' => alg, 'foo' => 'bar' } end end end it 'uses the provided header' do expect(JWT.decode(token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom', 'foo' => 'bar' }]) end end context 'when class is not utilizing the ::JWT::JWA::SigningAlgorithm module' do let(:custom_algorithm) do Class.new do attr_reader :alg def initialize(signature: 'custom_signature', alg: 'custom') @signature = signature @alg = alg end def header(*) { 'alg' => @alg, 'foo' => 'bar' } end def sign(*) @signature end def verify(*) true end end end it 'raises an error' do expect { token }.to raise_error(ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm') end end context 'when alg is not matching' do it 'fails the validation process' do expect { JWT.decode(token, 'secret', true, algorithms: custom_algorithm.new(alg: 'not_a_match')) }.to raise_error(JWT::IncorrectAlgorithm, 'Expected a different algorithm') end end context 'when signature is not matching' do it 'fails the validation process' do expect { JWT.decode(token, 'secret', true, algorithms: custom_algorithm.new(signature: 'not_a_match')) }.to raise_error(JWT::VerificationError, 'Signature verification failed') end end context 'when #sign method is missing' do before do custom_algorithm.instance_eval do remove_method :sign end end it 'raises an error on encoding' do expect { token }.to raise_error(JWT::EncodeError, /missing the sign method/) end it 'allows decoding' do expect(JWT.decode(expected_token, 'secret', true, algorithm: custom_algorithm.new)).to eq([payload, { 'alg' => 'custom' }]) end end context 'when #verify method is missing' do before do custom_algorithm.instance_eval do remove_method :verify end end it 'can be used for encoding' do expect(token).to eq(expected_token) end it 'raises error on decoding' do expect { JWT.decode(expected_token, 'secret', true, algorithm: custom_algorithm.new) }.to raise_error(JWT::DecodeError, /missing the verify method/) end end end end ruby-jwt-3.1.2/spec/jwt/token_spec.rb000066400000000000000000000111101503003570500175050ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::Token do let(:payload) { { 'pay' => 'load' } } let(:header) { {} } subject(:token) { described_class.new(payload: payload, header: header) } describe '#sign!' do it 'signs the token' do token.sign!(algorithm: 'HS256', key: 'secret') expect(JWT::EncodedToken.new(token.jwt).valid_signature?(algorithm: 'HS256', key: 'secret')).to be(true) end context 'when signed twice' do before do token.sign!(algorithm: 'HS256', key: 'secret') end it 'raises' do expect { token.sign!(algorithm: 'HS256', key: 'secret') }.to raise_error(JWT::EncodeError) end end context 'when RSA JWK is given as key' do let(:jwk) { JWT::JWK::RSA.new(OpenSSL::PKey::RSA.new(2048), alg: 'RS256') } it 'signs the token' do token.sign!(key: jwk, algorithm: []) # any algorithm is fine here expect(JWT::EncodedToken.new(token.jwt).valid_signature?(algorithm: 'RS256', key: jwk.verify_key)).to be(true) end context 'with algorithm provided in sign call' do it 'signs the token' do token.sign!(algorithm: %w[RS256 RS512], key: jwk) expect(JWT::EncodedToken.new(token.jwt).valid_signature?(algorithm: 'RS256', key: jwk.verify_key)).to be(true) end end context 'with mismatching algorithm provided in sign call' do it 'signs the token' do expect { token.sign!(algorithm: %w[RS384 RS512], key: jwk) }.to raise_error(JWT::DecodeError, 'Provided JWKs do not support one of the specified algorithms: RS384, RS512') end end end context 'when string key is given but not algorithm' do it 'raises an error' do expect { token.sign!(key: 'secret') }.to raise_error(ArgumentError, /missing keyword/) end end end context 'when EC JWK is given as key' do let(:jwk) { JWT::JWK::EC.new(test_pkey('ec384-private.pem')) } it 'signs the token' do token.sign!(key: jwk, algorithm: []) expect(JWT::EncodedToken.new(token.jwt).valid_signature?(algorithm: [], key: jwk)).to be(true) end end describe '#jwt' do context 'when token is signed' do before do token.sign!(algorithm: 'HS256', key: 'secret') end it 'returns a signed and encoded token' do expect(token.jwt).to eq('eyJhbGciOiJIUzI1NiJ9.eyJwYXkiOiJsb2FkIn0.UEhDY1Qlj29ammxuVRA_-gBah4qTy5FngIWg0yEAlC0') expect(JWT.decode(token.jwt, 'secret', true, algorithm: 'HS256')).to eq([{ 'pay' => 'load' }, { 'alg' => 'HS256' }]) end end context 'when token is not signed' do it 'returns a signed and encoded token' do expect { token.jwt }.to raise_error(JWT::EncodeError) end end context 'when alg is given in header' do let(:header) { { 'alg' => 'HS123' } } before do token.sign!(algorithm: 'HS256', key: 'secret') end it 'returns a signed and encoded token' do expect(JWT::EncodedToken.new(token.jwt).header).to eq({ 'alg' => 'HS123' }) end end end describe '#detach_payload!' do context 'before token is signed' do it 'detaches the payload' do token.detach_payload! token.sign!(algorithm: 'HS256', key: 'secret') expect(token.jwt).to eq('eyJhbGciOiJIUzI1NiJ9..UEhDY1Qlj29ammxuVRA_-gBah4qTy5FngIWg0yEAlC0') end end end describe '#verify_claims!' do context 'when required_claims is passed' do it 'raises error' do expect { token.verify_claims!(required: ['exp']) }.to raise_error(JWT::MissingRequiredClaim, 'Missing required claim exp') end end end describe '#valid_claims?' do context 'exp claim' do let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } context 'when claim is valid' do it 'returns true' do expect(token.valid_claims?(exp: { leeway: 1000 })).to be(true) end end context 'when claim is invalid' do it 'returns true' do expect(token.valid_claims?(:exp)).to be(false) end end end end describe '#claim_errors' do context 'exp claim' do let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } context 'when claim is valid' do it 'returns empty array' do expect(token.claim_errors(exp: { leeway: 1000 })).to be_empty end end context 'when claim is invalid' do it 'returns array with error objects' do expect(token.claim_errors(:exp).map(&:message)).to eq(['Signature has expired']) end end end end end ruby-jwt-3.1.2/spec/jwt/version_spec.rb000066400000000000000000000013271503003570500200630ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT do describe '.gem_version' do it 'returns the gem version' do expect(described_class.gem_version).to eq(Gem::Version.new(JWT::VERSION::STRING)) end end describe 'VERSION constants' do it 'has a MAJOR version' do expect(JWT::VERSION::MAJOR).to be_a(Integer) end it 'has a MINOR version' do expect(JWT::VERSION::MINOR).to be_a(Integer) end it 'has a TINY version' do expect(JWT::VERSION::TINY).to be_a(Integer) end it 'has a PRE version' do expect(JWT::VERSION::PRE).to be_a(String).or be_nil end it 'has a STRING version' do expect(JWT::VERSION::STRING).to be_a(String) end end end ruby-jwt-3.1.2/spec/jwt/x5c_key_finder_spec.rb000066400000000000000000000156321503003570500213000ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe JWT::X5cKeyFinder do let(:root_key) { test_pkey('rsa-2048-private.pem') } let(:root_dn) { OpenSSL::X509::Name.parse('/DC=org/DC=fake-ca/CN=Fake CA') } let(:root_certificate) { generate_root_cert(root_dn, root_key) } let(:leaf_key) { generate_key } let(:leaf_dn) { OpenSSL::X509::Name.parse('/DC=org/DC=fake/CN=Fake') } let(:leaf_serial) { 2 } let(:leaf_not_after) { Time.now + 3600 } let(:leaf_signing_key) { root_key } let(:leaf_certificate) do cert = generate_cert( leaf_dn, leaf_key.public_key, leaf_serial, issuer: root_certificate, not_after: leaf_not_after ) ef = OpenSSL::X509::ExtensionFactory.new ef.config = OpenSSL::Config.parse(leaf_cdp) ef.subject_certificate = cert cert.add_extension(ef.create_extension('crlDistributionPoints', '@crlDistPts')) cert.sign(leaf_signing_key, 'sha256') cert end let(:leaf_cdp) { <<-_CNF_ } [crlDistPts] URI.1 = http://www.example.com/crl _CNF_ let(:crl) { issue_crl([], issuer: root_certificate, issuer_key: root_key) } let(:x5c_header) { [Base64.strict_encode64(leaf_certificate.to_der)] } subject(:keyfinder) { described_class.new([root_certificate], [crl]).from(x5c_header) } it 'returns the public key from a certificate that is signed by trusted roots and not revoked' do expect(keyfinder).to be_a(OpenSSL::PKey::RSA) expect(keyfinder.public_key.to_der).to eq(leaf_certificate.public_key.to_der) end context 'already parsed certificates' do let(:x5c_header) { [leaf_certificate] } it 'returns the public key from a certificate that is signed by trusted roots and not revoked' do expect(keyfinder).to be_a(OpenSSL::PKey::RSA) expect(keyfinder.public_key.to_der).to eq(leaf_certificate.public_key.to_der) end end context '::JWT.decode' do let(:token_payload) { { 'data' => 'something' } } let(:encoded_token) { JWT.encode(token_payload, leaf_key, 'RS256', { 'x5c' => x5c_header }) } let(:decoded_payload) do JWT.decode(encoded_token, nil, true, algorithms: ['RS256'], x5c: { root_certificates: [root_certificate], crls: [crl] }).first end it 'returns the encoded payload after successful certificate path verification' do expect(decoded_payload).to eq(token_payload) end end context 'certificate' do context 'expired' do let(:leaf_not_after) { Time.now - 3600 } it 'raises an error' do error = 'Certificate verification failed: certificate has expired. Certificate subject: /DC=org/DC=fake/CN=Fake.' expect { keyfinder }.to raise_error(JWT::VerificationError, error) end end context 'signature could not be verified with the given trusted roots' do let(:leaf_signing_key) { generate_key } it 'raises an error' do error = 'Certificate verification failed: certificate signature failure. Certificate subject: /DC=org/DC=fake/CN=Fake.' expect { keyfinder }.to raise_error(JWT::VerificationError, error) end end context 'could not be chained to a trusted root certificate' do context 'given an array' do subject(:keyfinder) { described_class.new([], [crl]).from(x5c_header) } it 'raises a verification error' do error = 'Certificate verification failed: unable to get local issuer certificate. Certificate subject: /DC=org/DC=fake/CN=Fake.' expect { keyfinder }.to raise_error(JWT::VerificationError, error) end end context 'given nil' do subject(:keyfinder) { described_class.new(nil, [crl]).from(x5c_header) } it 'raises a decode error' do error = 'Root certificates must be specified' expect { keyfinder }.to raise_error(ArgumentError, error) end end end context 'revoked' do let(:revocation) { [leaf_serial, Time.now - 60, 1] } let(:crl) { issue_crl([revocation], issuer: root_certificate, issuer_key: root_key) } it 'raises an error' do error = 'Certificate verification failed: certificate revoked. Certificate subject: /DC=org/DC=fake/CN=Fake.' expect { keyfinder }.to raise_error(JWT::VerificationError, error) end end end context 'CRL' do context 'expired' do let(:next_up) { Time.now - 60 } let(:crl) { issue_crl([], next_up: next_up, issuer: root_certificate, issuer_key: root_key) } it 'raises an error' do error = 'Certificate verification failed: CRL has expired. Certificate subject: /DC=org/DC=fake/CN=Fake.' expect { keyfinder }.to raise_error(JWT::VerificationError, error) end end context 'signature could not be verified with the given trusted roots' do let(:crl) { issue_crl([], issuer: root_certificate, issuer_key: generate_key) } it 'raises an error' do error = 'Certificate verification failed: CRL signature failure. Certificate subject: /DC=org/DC=fake/CN=Fake.' expect { keyfinder }.to raise_error(JWT::VerificationError, error) end end context 'not given' do subject(:keyfinder) { described_class.new([root_certificate], nil).from(x5c_header) } it 'raises an error' do error = 'Certificate verification failed: unable to get certificate CRL. Certificate subject: /DC=org/DC=fake/CN=Fake.' expect { keyfinder }.to raise_error(JWT::VerificationError, error) end end end private def generate_key OpenSSL::PKey::RSA.new(2048) end def generate_root_cert(root_dn, root_key) cert = generate_cert(root_dn, root_key, 1) ef = OpenSSL::X509::ExtensionFactory.new cert.add_extension(ef.create_extension('basicConstraints', 'CA:TRUE', true)) cert.sign(root_key, 'sha256') cert end def generate_cert(subject, key, serial, issuer: nil, not_after: nil) cert = OpenSSL::X509::Certificate.new issuer ||= cert cert.version = 2 cert.serial = serial cert.subject = subject cert.issuer = issuer.subject cert.public_key = key now = Time.now cert.not_before = now - 3600 cert.not_after = not_after || (now + 3600) cert end def issue_crl(revocations, issuer:, issuer_key:, next_up: nil) crl = OpenSSL::X509::CRL.new crl.issuer = issuer.subject crl.version = 1 now = Time.now crl.last_update = now - 3600 crl.next_update = next_up || (now + 3600) revocations.each do |rserial, time, reason_code| revoked = build_revoked(rserial, time, reason_code) crl.add_revoked(revoked) end crlnum = OpenSSL::ASN1::Integer(1) crl.add_extension(OpenSSL::X509::Extension.new('crlNumber', crlnum)) crl.sign(issuer_key, 'sha256') crl end def build_revoked(rserial, time, reason_code) revoked = OpenSSL::X509::Revoked.new revoked.serial = rserial revoked.time = time enum = OpenSSL::ASN1::Enumerated(reason_code) ext = OpenSSL::X509::Extension.new('CRLReason', enum) revoked.add_extension(ext) revoked end end ruby-jwt-3.1.2/spec/spec_helper.rb000066400000000000000000000013021503003570500170420ustar00rootroot00000000000000# frozen_string_literal: true require 'rspec' require 'simplecov' require 'jwt' require_relative 'spec_support/test_keys' require_relative 'spec_support/token' puts "OpenSSL::VERSION: #{OpenSSL::VERSION}" puts "OpenSSL::OPENSSL_VERSION: #{OpenSSL::OPENSSL_VERSION}" puts "OpenSSL::OPENSSL_LIBRARY_VERSION: #{OpenSSL::OPENSSL_LIBRARY_VERSION}\n\n" RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = :expect end config.include(SpecSupport::TestKeys) config.before(:example) do JWT.configuration.reset! JWT.configuration.deprecation_warnings = :warn end config.run_all_when_everything_filtered = true config.filter_run :focus config.order = 'random' end ruby-jwt-3.1.2/spec/spec_support/000077500000000000000000000000001503003570500167565ustar00rootroot00000000000000ruby-jwt-3.1.2/spec/spec_support/test_keys.rb000066400000000000000000000005621503003570500213200ustar00rootroot00000000000000# frozen_string_literal: true module SpecSupport module TestKeys KEY_FIXTURE_PATH = File.join(__dir__, '..', 'fixtures', 'keys') def test_pkey(key) TestKeys.keys[key] ||= read_pkey(key) end def read_pkey(key) OpenSSL::PKey.read(File.read(File.join(KEY_FIXTURE_PATH, key))) end def self.keys @keys ||= {} end end end ruby-jwt-3.1.2/spec/spec_support/token.rb000066400000000000000000000001621503003570500204220ustar00rootroot00000000000000# frozen_string_literal: true module SpecSupport Token = Struct.new(:payload, :header, keyword_init: true) end